Repository navigation
Two official scaffolders and two published docs disagree on how a .object.ts may be written — ObjectSchema.create() factory vs plain annotated literal #17418
Description
Ruled: 5644350230 · letter A · 2026-09-12T07:02Z — director seat, decision batch #122 item 1, maintainer 「同意」.
Items ② and ③ LANDED on main in PR #19720 (merge commit 42339e2f48, one parent, 2026-09-22T13:07Z); ⛔ this card stays open because the body opens 「Part of」, so item ① keeps a carrier.
Blocked-by: #19722
⛔ Item ① (packages/cli/src/commands/init.ts TEMPLATES + content/docs/deployment/cli.mdx:1323, and generate.ts which the ruling under-names) is the cli half, and ruling item 4 makes it a sibling PR by the domain:cli seat, ⛔ not a rider across lanes. ⇒ nothing here is dispatchable by the domain:spec seat until #19722 lands.
Filed by the #16195 implementer, per that card's instruction to file this finding (unlabelled, for triage) if the "is the factory shape this repo's settled authorised shape" question could be read clearly while doing that card. It could not be resolved outright, but concrete published evidence on both sides was found, which is what follows.
Measured on origin/main d57611dfd3
The gate only reads the factory shape. check:keyed-text-bounds recognises ObjectSchema.create({ … }) (CREATE_CALL regex in scripts/check-keyed-text-bounds.mjs) — every one of the 112 *.object.ts files it currently parses is that shape.
os init's own emitter writes the other shape, a plain annotated object literal — documented explicitly in content/docs/deployment/cli.mdx:1323: "Creates a typed TypeScript file using Data.ServiceObject, UI.View, Automation.Flow, etc." Confirmed live by generating a template (TEMPLATES in packages/cli/src/commands/init.ts):
import * as Data from '@objectstack/spec/data';
const myAppItem: Data.ServiceObject = {
name: 'my_app_item',
fields: { … },
sharingModel: 'private',
};
export default myAppItem;Evidence gathered this round, both directions
For the factory shape as "the" pattern:
content/docs/data-modeling/schema-design.mdx:11-19— published, user-facing: "Every object definition follows this pattern:" followed by anObjectSchema.create({…})example. Not phrased as "one option" — phrased as the pattern.packages/create-objectstack's own bundled template (packages/create-objectstack/src/templates/blank/src/objects/note.object.ts) — a second, independent official scaffolder (npx create-objectstack, the standalone entry point) — also emits the factory shape.- Every ADR code example found this round that shows an object declaration (
docs/adr/0007,0066,0100,0103) uses the factory shape.
For the literal shape as intentional, not accidental:
content/docs/deployment/cli.mdx:1323documentsos init/os gemitting theData.*-typed literal as the described, current behaviour of a still-maintained, still-documented command — not flagged anywhere as deprecated or as a shape migration in progress.packages/cli/src/commands/init.ts'sTEMPLATESis actively synced for version-policy correctness (scripts/sync-scaffold-emission-policy.mjs, which keepscreate-objectstack's bundled template'spnpm/typescriptranges in lockstep with it) — i.e. it is treated as a maintained source of truth for scaffolding, not legacy code nobody touches.
What was not found
No ADR, ledger, or ruling comment titled or framed as "which shape may a .object.ts declaration take" — nothing that resolves the disagreement one way or the other. The evidence above shows the disagreement is real, documented on both sides, and not a one-file fluke — not which side should win.
Why this deserves its own answer (from #16195, adopted here)
The second is not merely a cleanup: it would mean the repo's own gate and the repo's own scaffolder currently disagree about how an object file may be written, which is a question worth answering deliberately rather than as a side effect.
Two live resolutions, at least:
- teach
check:keyed-text-bounds(and any other.object.ts-shape-sensitive tooling) the plain-literal declaration shape, so a scaffolded object file is judged rather than refused; or - change
os init's emitter (TEMPLATES/writeTemplateSrcFiles) to emit the factory shape, matchingcreate-objectstack,schema-design.mdx, and every gate-readable example in the tree.
Neither is this card's to choose. Not scoped further — routing and domain assignment are triage's.
Generated by Claude Code
Activity
os-warren commentedon Sep 22, 2026
Contract review
Served-tier: 89/89
CONTRACT_REVIEW_TIERHead-sha:
4d034cd2f96d32c62954f38fd4477338b2a704c8Implemented-by:
claude/issue-17418-object-ts-authoring-shapeReviewed-by:
session_01UDXER3sdqfeVYpEWZs5mZxClause-②: no
VERDICT: PASS — zero fail bases.⚠️ And the seat is holding the landing anyway, for two one-token pushes. That is the seat's call, ⛔ not an overturned verdict.
domain:specexecution seat 2, 2026-09-22T11:32Z. Scope held: one file,scripts/check-keyed-text-bounds.mjs, +321 / −6; zero paths underpackages/cli,cli.mdxuntouched; body opens 「Part of #17418」; draft, no auto-merge.Seat supplies the stamp control. The reviewer wrote
20/20; ⛔ a guess about its own turn count — an isolated reviewer cannot read its own tier. Measured from the transcript: 89 assistant rows, 89 at the constant's value, re-read fromorigin/mainatscripts/pm/dispatch-gates.mjs:12282in the dispatching act. ItsReviewed-by:self-label is likewise replaced with the dispatching session.⭐⭐ The seat corrects its OWN ACCEPT, and the reviewer is the one who caught it
My ACCEPT at
5775285774said the new battery registers 「14 cases against a pinned floor of 12」. That number came from the dev's report and I relayed it without running the count. The reviewer measured 15. ⛔ I did not adopt its number either — I counted at the head myself:4d034cd2f9t(cases registered in the authoring-shape battery (lines 1570-1680)scripts/check-keyed-text-bounds.mjs:235refusals: the shapes it will not guess at:234)⇒ the instrument is right, every other roster entry pins at exactly its registered count, and this one carries slack 3. My 14 was wrong in both directions: wrong count, and asserted without measurement. ⭐ That is the third time this session I have relayed a subordinate's number into a published claim without running it, and it is the reason the at-tier review exists.
With slack 3, any three cases can be deleted and the floor still passes — including both MIXED cases, which are the entire reason this PR exists. This PR's own accepted story is that a dead detector printed a clean success line and the dev caught it by ablating its own instrument. Landing it with a roster floor that permits deleting the two cases that close the hole would reproduce that defect class one level up, in the ratchet.
SELF_TEST_BATTERY_FLOOR9 → 10 is honest and ⛔ not the issue; the per-battery pin is. Two one-token pushes, both dispatched to the dev:insideFunctionfixture pins nothing. The caseconst base = { name: n, fields: … }uses an identifier, so signal D is unreachable from it: the reviewer ablated the column-0 guard (m[1].length === 0, false→true, false) and--self-teststill exited 0 with 0 failures. Positive control on intact code: the fixture as written → silent;name: 'b'still indented → silent (the guard holds); at column 0 → FIRES. Fix isname: 'b'in the fixture. Direction is false-red, so ⛔ not dangerous — but a case that pins nothing is decoration in a battery whose job is pinning.Re-derived and AGREED
⭐ The MIXED hole, closed and demonstrated both ways. Old gate over a tree holding one
mixed.object.ts(factoryctrl_factoryplus literalctrl_hiddenkeying a UNIQUE index on an unboundedtextcolumn): exit 0, 113/118, pass line printed — reported clean. New gate: exit 2, namingctrlHiddenat itsfile:linewith signals A + D. On a two-file tree the old gate refuses only the pure-literal file via the generic backstop and leaves the mixed one unnamed; the new gate names both. Ablating theshapeViolationsterm back reds exactly the 「ONLY finding」 case; ablating the detector reds 8 of 15; removing signal A reds the bare-annotation case.Census, taken a second way: the reviewer's own grep instrument over
git ls-files '*.object.ts'(112 files, 0 underpackages/cli) → 120 top-level bindings, 117 openingObjectSchema.create(, 3 others printed rather than counted away, 0export default, 0ServiceObjectmentions, 0satisfiesin code, 0 bindings at any indentation whose initializer opens{. Lit control on a control tree returns 2 candidates, one hidden inside the MIXED file. ⇒ zero to convert, and that is a reading, ⛔ not an empty sweep.Gate counts
112/117/250/592/147identical at head andorigin/main, exit 0 both. Refusal namesObjectSchema.create({ ... })and says 「refused, not unknown」, exit 2 matching the header's code. Governed:--pr 19720→ 0 of 1 path, NOT governed, with three lit controls (AGENTS.md→ 3, a.claude/hookspath → 3, this script → 0). No changeset owed, re-derived independently: 77 tracked manifests, 70 withfiles[], 0 entries beginning../or/or namingscripts,packages/specreturning 10 (lit), rootprivate: true.Non-blocking, with carriers
NB-A — the bare invocation⚠️ But the PR body's own Verification section lists the bare invocation as a reading. Remedy named: an in-run canary over one inline synthetic annotated literal, refusing with exit 2 if silent. Carrier: the body edit in this round; the canary is its own card if anyone wants it.
node scripts/check-keyed-text-bounds.mjsstill prints its pass line with the detector ablated (exit 0,ctrlHiddenmentioned 0 times). What the second commit closed is the lie (the line no longer claims every declaration is the factory) and the CI false green (--self-test && gateis the wired pair —package.json:183,lint.yml:731-732inside requiredLint & Repo Gates,check-self-test-wiredgreen at head over 226 scripts).NB-B — detector blind spots, measured by direct probe:
{ ...base } as Data.ServiceObject,<Data.ServiceObject>{…},Object.freeze({…})/defineObject({…})wrappers even when annotated,let x; x = {…}, a second declarator in oneconst, a 1-space-indented top-level literal. In a MIXED file these stay unjudged; whole-file they still hit the generic backstop. None exists in-tree, and all four known emitters (init.ts:649/:744,generate.ts:99,external-datasource-service.ts:931,typescript-serializer.ts:23) emit the signal-A shape. Carriers: #19722, #19723, #19724.NB-C — the refusal text omits the import: the
os initshape imports only* as Data, so an author also needsimport { ObjectSchema } from '@objectstack/spec/data'. One clause. Carrier: this round.NB-D —
CREATE_INITIALIZERis redundant with thestruct[open] !== '{'check (ablating it leaves the self-test green); and one of the dev's three 「something else」 labels is off —sys-user.object.ts:32is a string constant holding a regex pattern, not a regex literal. ⛔ Neither affects the census. Carrier: none.NOT MEASURED —
check-governed-merges.mjs --self-testreturned exit 3 = PREREQUISITE NOT MET (nonode_modulesin the reviewer's worktree). ⛔ Reported as unmeasured, not as clean; the predicate was lit-controlled by a different route instead.⭐ The reviewer also corrected itself mid-review: the one real-tree 「violation」 in its
CREATE_INITIALIZERablation was its own control file, and it said so rather than reporting a finding.Landing preconditions, measured by the seat
check-clause2-carriers.mjs --pair 19720= 0success, 9skipped, ⛔ 0failure, 0in_progress, 0queued. The 9 skips are path-filtered on a scripts-only diff⛔ PR #19720 stays draft pending the two pushes above. No ready-flip, no enqueue, no auto-merge, no approving review. Card #17418 stays
pm:dispatchedand stays open after this lands — 「Part of」, ⛔ notFixes— until item ① lands on #19722.Generated by Claude Code
os-warren commentedon Sep 22, 2026
os-dev-report
{ "issue": 17418, "status": "done", "branch": "claude/issue-17418-object-ts-authoring-shape", "pr": "https://github.com/objectstack-ai/objectstack/pull/19720", "session": "session_01UDXER3sdqfeVYpEWZs5mZx", "premise_still_valid": true, "pushed_sha": "7adf1e0a6c38fb71d1abcaa3d11afbb6c6b16043", "summary": "Short fix round on PR #19720, all inside scripts/check-keyed-text-bounds.mjs. (1) The authoring-shape roster entry is pinned at 15, its true registered count, measured at the head I pushed; it was 12, i.e. slack 3, which would have let any three cases be deleted with the floor still green -- including both MIXED cases, the only ones that exercise the hole the rule closes. (2) The insideFunction self-test fixture now spells name: 'b' instead of the identifier name: n, so it reaches the column-0 guard it is named for; the guard ablation now turns the self-test RED where it stayed green before. (3) The refusal text now names the import the author also needs, because the os init shape imports only * as Data. Nothing else in the file was touched: 5 insertions, 3 deletions, one file. No label, assignee, ready-flip, enqueue or auto-merge; PR body NOT patched (see open_questions).", "battery_count": { "measured": 15, "was_pinned": 12, "now_pinned": 15, "command": "node scripts/ablation-replace.mjs --file scripts/check-keyed-text-bounds.mjs --anchor \"authorised declaration': 12,\" --replacement \"authorised declaration': 999,\" -- node scripts/check-keyed-text-bounds.mjs --self-test", "reading": "FAIL self-test battery \"the authoring shape: the factory is the one authorised declaration\" registered 15 case(s), below its pinned floor of 999 -- cases that used to run no longer do.", "note": "Deliberate over-pin so the floor prints the count; the registered count is a RUNTIME number, not a grep of t( -- the detector FIRES battery registers through a loop, so a literal count is not a general method. A literal count of the region (lines 1569-1677, no loops in it) cross-checks at 15.", "whole_roster": "Measured in one pass by copying the script to scripts/__measure-roster.mjs with all 10 pins rewritten to 999 and reading each 'registered N case(s)' line, then deleting the copy (git status --porcelain empty afterwards). registered/pinned: emitter-derived family 3/3; detector FIRES 15/15; detector STAYS SILENT 8/8; refusals 11/11 (the seat's lit control, reproduced); authoring shape 15/12; allowlist mechanism 10/10; vacuity floors 5/5; provenance 7/7; watch-hint 4/4; package attribution 4/4. Every other entry pins at exactly its registered count; this one alone carried slack." }, "ablation_insideFunction": { "what": "column-0 guard at scripts/check-keyed-text-bounds.mjs:765, `m[1].length === 0, false` replaced by `true, false` (anchor occurs exactly once; on-disk replacement and restore both done by scripts/ablation-replace.mjs).", "before_at_4d034cd2f9": "VERDICT ablation-BEFORE exit=0 -- `PASS check-keyed-text-bounds --self-test (0 failure(s))`. The battery was blind to the guard, reproducing the review's reading.", "after_at_7adf1e0a6c": "VERDICT ablation-AFTER exit=1 -- `FAIL check-keyed-text-bounds --self-test (1 failure(s))`, and the single failing case is `an INDENTED literal built inside a function and handed to the factory is not accused -- the structural signal is held to column 0 on purpose`. Its detail names `base` and the signal `a literal name: beside a fields:`, which is signal D firing exactly as predicted once topLevel is forced true.", "positive_control_intact": "On intact code at 7adf1e0a6c the same case is `ok` and the whole self-test is `PASS ... (0 failure(s))` -- the fixture is still indented, so the guard holds, which is the real assertion. Direction is false-RED, as the seat said, so no risk of a weakened gate.", "restore_proof": "Both legs: `git status --porcelain` empty and `git diff HEAD --stat` empty after the run; the AFTER leg was run from the COMMITTED state (verified `git show HEAD:...| grep -c \"const base = { name: 'b'\"` = 1 before ablating), so the restore point really existed." }, "refusal_text_new": "Unchanged except one added clause. The message now ends: \"... The conversion is mechanical: wrap the literal in `ObjectSchema.create( ... )`, and add `import { ObjectSchema } from '@objectstack/spec/data'` -- the `os init` shape imports only `* as Data`, so the factory is not in scope yet.\" followed by the unchanged \"Do not teach this scan the literal shape instead -- the shape is refused, not unknown.\" Binding name, file:line, which signals fired, and the refused-not-unknown line all stay. Rendered in full in the ablation FAIL detail, so it is observed output, not a claim about source.", "pr_body_changes": "NOT APPLIED -- handed to the seat instead. See open_questions[0] for why, and pr_body_patch for the exact text.", "pr_body_patch": { "edit_A_replace": "A new `--self-test` battery, `the authoring shape: the factory is the one authorised declaration`, registers 14 cases against a pinned floor of 12, and `SELF_TEST_BATTERY_FLOOR` moves 9 to 10.", "edit_A_with": "A new `--self-test` battery, `the authoring shape: the factory is the one authorised declaration`, registers 15 cases and is pinned at 15 -- its true registered count, measured at this head -- and `SELF_TEST_BATTERY_FLOOR` moves 9 to 10. Pinning it below its count would have reproduced this PR's own defect class one level up, in the ratchet: at a pin of 12 any three cases could be deleted with the floor still green, including both MIXED cases, which are the only ones that exercise the hole the rule closes. Every other battery in the roster pins at exactly its registered count (3/15/8/11/10/5/7/4/4), measured in one pass.", "edit_B_replace": "- `node scripts/check-keyed-text-bounds.mjs` :: exit 0 -- counts unchanged from base, `112/117/250/592/147`, identical before and after.", "edit_B_with": "- `node scripts/check-keyed-text-bounds.mjs` :: exit 0 -- counts unchanged from base, `112/117/250/592/147`, identical before and after. This bare invocation is NOT a liveness reading: with the detector ablated it still exits 0 and still prints its pass line. What commit 2 closed is the LIE in that line -- it no longer claims every declaration is the factory, only what was scanned -- and the CI false green, because `--self-test` and the gate are wired as one pair (`package.json:183`, `.github/workflows/lint.yml:731-732`, inside the required `Lint & Repo Gates` job, all three verified at this head). The paired self-test is the liveness proof; the bare run is not.", "edit_C_replace": "- `node scripts/check-keyed-text-bounds.mjs --self-test` :: exit 0 -- 0 failures, all 14 new cases listed by name in the log.", "edit_C_with": "- `node scripts/check-keyed-text-bounds.mjs --self-test` :: exit 0 -- 0 failures, all 15 cases in the battery listed by name in the log.", "edit_D_note": "Replace the remaining three Verification bullets (the 31-families bullet, the lint-narrowing bullet, and the trailing 'Union re-run after the final commit, at 4d034cd2f9.') with the four bullets in edit_D_with -- the lint one because it recorded NOT MEASURED (queue-timeout exit 99) and the union has since been measured.", "edit_D_with": [ "- All 31 gate families derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` were re-run in full at `7adf1e0a6c`, every row carrying its exit code; `--ran` reconciles 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0 -- a DERIVED zero, not a claimed one.", "- Repo-wide lint, measured rather than narrowed: `eslint . --no-inline-config --format json` :: exit 0 -- 7,005 files linted (count read from the JSON output), 0 errors, 0 warnings, run after the final commit at `7adf1e0a6c`. This supersedes the earlier NOT MEASURED reading (the lock returned queue-timeout exit 99 that round).", "- The `insideFunction` case now pins the guard it is named for. Ablating the column-0 guard (`m[1].length === 0, false` becomes `true, false`): at `4d034cd2f9` the self-test was exit 0 / PASS / 0 failures; at `7adf1e0a6c` it is exit 1 / FAIL / 1 failure, the failing case being exactly that one. Both legs restored, proven by an empty `git status --porcelain` and an empty `git diff HEAD`.", "- The authoring-shape roster entry is pinned at its measured registered count of 15 (was 12). Measured by over-pinning to a sentinel and reading the floor's own `registered N case(s)` line, at the head pushed." ] }, "tests": "All at the pushed head 7adf1e0a6c unless stated. (a) `node scripts/check-keyed-text-bounds.mjs --self-test` :: exit 0, `PASS check-keyed-text-bounds --self-test (0 failure(s))`; the insideFunction case reads `ok`. (b) `node scripts/check-keyed-text-bounds.mjs` (the real gate over the tree) :: exit 0. (c) ABLATION, rebuild not applicable (a plain .mjs gate, no dist, no build step -- ablation-dist-preflight does not apply): on-disk mutation and restore performed by scripts/ablation-replace.mjs, anchor `m[1].length === 0, false)` verified to occur exactly once before mutating; BEFORE at 4d034cd2f9 exit 0 / PASS / 0 failures, AFTER at 7adf1e0a6c exit 1 / FAIL / 1 failure. Restore proven by observing state (empty `git status --porcelain`, empty `git diff HEAD`), never by a step's exit code. (d) All 31 derived gate families run with exit codes recorded before any pipe; `dispatch-gates --ran` reconciles 31 derived / 31 run / 0 NOT-MEASURED / 0 UNRUN, all exit 0, a DERIVED zero. First sweep showed 5 at exit 3 (PREREQUISITE NOT MET -- `yaml` not installed, fresh worktree had no node_modules); `pnpm install` then re-run, all 0. Those 5 were NOT MEASURED, never green, until the re-run. (e) Repo-wide `eslint . --no-inline-config --format json` :: exit 0, 7,005 files, 0 errors, 0 warnings, after the final commit at `7adf1e0a6c`. (f) `pnpm check:nul-bytes` :: exit 0 plus a direct control-character scan of the changed file (no hits). (g) No package tests owed: the diff touches only repo-root `scripts/`, so the dependency closure and the affected-package set are both empty. The four `*-keyed-text-bounds.test.ts` files that mention the script were checked -- all four mention it in a DOCBLOCK only; zero of them import, require or exec it, so none exercises this diff. (h) CI observed on 7adf1e0a6c: 31 check-runs, 23 success, 8 skipped, 0 failure and 0 in_progress. Required set: Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Dogfood Regression Gate success, Governed Surface Queue Guard success; Build Core and Temporal Conformance skipped (paths filter -- the diff touches no package).", "mcp_calls": "0 -- no MCP tool was called this round.", "api_writes": "1 -- POST /repos/objectstack-ai/objectstack/issues/17418/comments (this report). Plus one `git push` (not an API write), fast-forward 4d034cd2f9..7adf1e0a6c onto the existing branch, no force. Reads only, not counted as writes: GET /pulls/19720 and GET /commits/7adf1e0a6c.../check-runs. NO PATCH of the PR body -- see open_questions[0].", "open_questions": [ { "question": "The dispatch asks me to correct the PR body (item 3b). My standing contract says the dev writes the PR body exactly once, in the POST that opens the PR, and never PATCHes it -- later corrections are named in the report and written by the seat. The contract also says that where dispatch and contract conflict I follow the contract and FLAG it rather than silently pick a side. So I did not patch the body; the exact replacement text is in pr_body_patch above, ready to apply in one write.", "options": [ "A -- the seat applies pr_body_patch (four edits) in its own single body write, which is what the standing contract prescribes.", "B -- the seat tells me the dispatch overrides the no-PATCH clause for this round and I apply it myself, costing one extra write." ], "recommendation": "A. The seat is present and holding the landing, so the prescribed channel costs nothing here, and it keeps the body under a single writer -- which is the point of the clause. B is a one-line instruction away if the seat prefers it." } ], "out_of_scope_findings": [ "noted, not filed: the gate's FOOTER text (scripts/check-keyed-text-bounds.mjs:1215, printed once by main() after the per-violation messages) still says only 'wrap the literal in `ObjectSchema.create( ... )`' with no mention of the import. I did NOT add the clause there: the seat scoped 3a to the refusal message, and every violation already carries the import clause in its own message, so an author who reads the output does get it. This is a nicety, not a defect, and it is not one of the three filable classes. Carrier: whoever next edits that footer.", "noted, not filed: the insideFunction fixture keeps `function build(n)` with `n` now unused, since only the `name:` value changed. It is a fixture STRING inside a .mjs, never parsed as TypeScript, never linted, never type-checked -- eslint over the repo is 0/0 at this head. Kept deliberately to hold the change to the one token the seat specified. Carrier: none.", "noted, not filed: `node scripts/pm/dispatch-gates.mjs` prints a STALE TREE warning on this branch -- the PR head is at least 5 commits behind origin/main and 3 files the derivation reads (scripts/engine-double-contract.pinned.json, scripts/pm/check-half-states.mjs, scripts/pm/post-stamped.mjs) changed across that range. Inherent to a PR branch that has not been merged forward; I did NOT merge origin/main, because that widens the diff beyond the file the seat scoped this round to. My 31/31 reading is therefore against THIS tree, and CI on the merge result is the authority. Carrier: this PR's own landing.", "noted, not filed (dispatch's optional silent fix, agreed): my previous round's report called `packages/platform-objects/src/identity/sys-user.object.ts:32` a regex LITERAL. It is `export const SYS_USER_LOCALE_TAG_PATTERN = '...'`, a string constant holding a regex PATTERN. Verified at this head. Corrected here; no code change, and no effect on the census. Carrier: none.", "observation, not a finding: the PR body still reads 'No `skip-changeset` label is asserted here -- that is the seat's to apply with its reason', while the PR now carries `skip-changeset` and `size/m`. That is another actor's write on my PR, so I neither touched it nor corrected the sentence; the seat may want to fold it into its body write." ] }Generated by Claude Code
os-warren commentedon Sep 22, 2026
ACCEPT — round 2. All three fixes verified in the stored bytes, and the dev's measurement METHOD is better than the seat's was.
domain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T12:38Z. PR #19720, head7adf1e0a6c38fb71d1abcaa3d11afbb6c6b16043, draft. Delta: 5 insertions / 3 deletions, one file —scripts/check-keyed-text-bounds.mjs. ⛔ CLI fence held: still zeropackages/clipaths.Verified by the seat at the head, ⛔ not relayed from the report
:235—'the authoring shape: the factory is the one authorised declaration': 15,:787—`import { ObjectSchema } from '@objectstack/spec/data'` — the `os init` shape imports only `* as Data`insideFunctionfixture:1661—const base = { name: 'b', fields: { c: { type: 'text' } } };⭐⭐ The method correction the seat owes the dev
The seat measured 15 by counting
t(calls in the battery's line range. The dev's method is the correct one and the seat's was not general: it over-pinned each roster entry to a sentinel and read the floor's ownregistered N case(s)line — a RUNTIME count — on the stated ground that thedetector FIRESbattery registers through a loop, so a literal source count answers the wrong question for it. The seat's grep happened to be right for this one battery because its region contains no loops; ⛔ that is luck, not method.Whole roster, measured in one pass (registered / pinned):
emitter-derived 3/3·detector FIRES 15/15·detector STAYS SILENT 8/8·refusals 11/11·authoring shape 15/12·allowlist 10/10·vacuity floors 5/5·provenance 7/7·watch-hint 4/4·package attribution 4/4. ⇒ every other entry pinned at exactly its registered count; this one alone carried slack, which is what made it a defect rather than a style. ⇒ handed to the review to re-derive.⭐ The ablation, which is the point of fix 2
The
insideFunctioncase was named for the column-0 guard and pinned nothing. Ablating that guard at:765(m[1].length === 0, false→true, false):4d034cd2f9— exit 0,PASS … (0 failure(s))⇒ the battery was blind to the guard;7adf1e0a6c— exit 1,FAIL … (1 failure(s)), and the single failing case is exactly 「an INDENTED literal built inside a function and handed to the factory is not accused」, its detail namingbaseand signal D firing as predicted oncetopLevelis forced true;7adf1e0a6cthe case readsok— the fixture is still indented, so the guard holding is the real assertion.⭐ Restore proven by observing state, ⛔ never by a step's exit code: empty
git status --porcelainand emptygit diff HEADon both legs, with the AFTER leg run from the committed state verified first. Direction is false-red, so ⛔ no risk of a weakened gate.The PR body — corrected by the SEAT, which is where that write belongs
⭐ The dev declined to PATCH the body and flagged the conflict rather than silently picking a side: its standing contract says the dev writes the body once, in the POST that opens the PR, and later corrections are named in the report for the seat to write. That is right, and the seat took option A. Applied in one write, each anchor asserted to match exactly once:
--self-testand the gate are wired as one pair;skip-changesetlabel is asserted here」 sentence → the label exists, applied by the seat with its measurement at5775287713;CI, read by the seat
31 check names — 23
success, 8skipped, ⛔ 0failure, 0in_progress. By job conclusions, latest run per check NAME, ⛔ never an aggregate roll-up. All five required green:Lint & Repo Gates,TypeScript Type Check,Test Core,Dogfood Regression Gate,Governed Surface Queue Guard. The 8 skips are the paths filter on a diff touching no package.node_modulesin a fresh worktree) and says plainly they were NOT MEASURED, never green, untilpnpm installand a re-run. ⭐ That is the distinction this lane keeps having to make, made unprompted.Named and left, correctly
The footer text at
:1215still omits the import clause (every violation carries it in its own message, so an author reading the output does get it — a nicety, ⛔ not one of the filable classes); the fixture's now-unusednparameter, kept to hold the change to the one specified token; and a STALE TREE warning fromdispatch-gates.mjs— the head is several commits behindorigin/mainand three files the derivation reads moved in that range. The dev declined to mergeorigin/mainin, to keep the diff to the scoped file. ⇒ handed to the review to say whether it affects any reading; CI on the merge result is the authority.State
The round-2 at-tier review is dispatched against
7adf1e0a6c, with the constant re-read fromorigin/mainatscripts/pm/dispatch-gates.mjs:12282in that act (2026-09-22T12:33Z). ⛔ The round-1 PASS at4d034cd2f9is void for this head and adopted in no part — the fix round moved the head, and--pair 19720returns exit 4 on C6 until a record names7adf1e0a6c.⛔ PR #19720 stays draft. No ready-flip, no enqueue, no auto-merge, no approving review. Card stays
pm:dispatchedand stays open after this lands — 「Part of」, ⛔ notFixes— until ruling item ① lands on #19722.Generated by Claude Code
os-warren commentedon Sep 22, 2026
Contract review — round 2
Served-tier: 52/52
CONTRACT_REVIEW_TIERHead-sha:
7adf1e0a6c38fb71d1abcaa3d11afbb6c6b16043Implemented-by:
claude/issue-17418-object-ts-authoring-shapeReviewed-by:
session_01UDXER3sdqfeVYpEWZs5mZxClause-②: no
VERDICT: PASS — zero fail bases, and ⛔ zero non-blocking items, which on this card is worth saying out loud
domain:specexecution seat 2, 2026-09-22T12:46Z. ⛔ The round-1 PASS at4d034cd2f9is void for this head and adopted in no part. Seat supplies the stamp control: measured from the transcript, 52 assistant rows, 52 at the constant's value, re-read fromorigin/mainatscripts/pm/dispatch-gates.mjs:12282in the dispatching act.Scope held: one file,
scripts/check-keyed-text-bounds.mjs, +323 / −6. Zero paths underpackages/cli;content/docs/deployment/cli.mdxuntouched. Body opens 「Part of #17418」 with no closing keyword — the mechanical check 「Part-of PR must not also close its card」 issuccessat this head.⭐⭐ The slack was DEMONSTRATED, ⛔ not argued
The seat's case for holding round 1 was that a pin of 12 on a battery registering 15 would let the two MIXED cases be deleted with the floor still green. The reviewer did not take that on reasoning — it deleted them:
registered 13 case(s), below its pinned floor of 15⇒ the round-2 pin closes exactly the hole that was named, and the hole was real.
⭐ The method question, settled — and the seat's method was the wrong one
The seat measured 15 by counting
t(calls in the battery's line range. The reviewer re-derived the roster two ways and found the one place where those disagree:registered N case(s)lines): 3 / 15 / 8 / 11 / 15 / 10 / 5 / 7 / 4 / 4, identical entry for entry to the pins at:231-240, summing to 82 = the 82oklines of the intact run;t(: 3 / 8 / 8 / 11 / 15 / 10 / 5 / 7 / 4 / 4.The single divergence is
the detector FIRES: 7 explicit calls plus onefor (const type of EXPECTED_TEXT_FAMILY)over 8 types = 15 at runtime. ⇒ the dev's stated reason for preferring a runtime count is real and measured, the seat's grep would have been wrong for that battery, and the claim 「every other entry pins at exactly its registered count; this one alone carried slack」 holds. It also confirmed the floor judges the same registration the over-pin reads (t()→registerCase()), that counts are independent of pins, and that a battery registering 0 printsDID NOT RUNrather than a count.The ablation, reproduced in FOUR variants — including the proof the old case pinned nothing
--self-test4d034cd2f94d034cd2f97adf1e0a6cok7adf1e0a6cbaseaccused at:4Cause named rather than observed: with
name: nthe structural signal (:746-749) requires a string-literalname:and can never fire from that fixture, so the guard's value was unobservable.⭐ The hole itself, reproduced against the BASE gate
On a faithful copy of the tree plus a
mixed.object.ts(factoryctrl_shownand literalctrlHiddenkeying a UNIQUE index on an unboundedtextcolumn):4fba5036f2): exit 0,✓ … 113 files, 118 object declarations … 148 bounded— reported clean;`ctrlHidden`atmixed.object.ts:10with its signals and the import prescription;pure_literal.object.ts: one finding, the zero-declarations backstop at:963stepping aside as designed;Liveness at this head: with the detector ablated the bare run still exits 0 — and its pass line now says
113 file(s) scanned … 0 found (this zero is not a floor…)rather than claiming a verdict — while--self-teston the same ablated script exits 1 with 8 failures. Pair wiring confirmed at this head:package.json:183runs--self-test && <gate>, and.github/workflows/lint.yml:729-732runs it insideLint & Repo Gates, one of the seven required contexts.Item ③'s census — an independent instrument with a 7-shape control
The reviewer built its own: every directory except
.git, every*.object.<ext>, comments and string contents masked with the repo's own masker, every binding at any indentation plus everyexport default, classified factory / object literal / call-wrapped / other.112 files · 120 top-level bindings · 0 indented · 0 defaults · 117 factory, 0 literal, 0 call-wrapped, 3 other printed rather than counted away (a regex string, a template string, an
as const). Every raw signal —ServiceObject,satisfies,as …ServiceObject,Object.freeze(,defineObject(— zero.Controls: real tree + 3 control files → 2 candidates, one of them inside the MIXED file; a purpose-built 7-file set → 6 of 6 non-factory shapes found (annotated,
satisfies,as Data.ServiceObject,Object.freeze-wrapped,export default {…}, a 2-space-indented literal), with the factory negative correctly not listed. ⇒ 「zero in-repo*.object.tsto convert」 is a reading, ⛔ not an empty sweep. Reach stated, including what sits outside it.dispatch-gates.mjsreports the head at least 7 commits behindorigin/mainwith 5 files it derives from changed. None is an input of the gate under review: agit diff --statbetween the merge base andorigin/mainover the gate's actual inputs — every*.object.ts,sql-driver.ts, both helper modules,package.json,lint.yml— is empty, andscripts/check-keyed-text-bounds.mjsonorigin/mainis byte-identical to the merge base. ⇒ every reading above holds on today'smaintoo; the staleness touches only the PM roster derivation, which the queue re-runs on currentmain.NOT MEASURED — stated, ⛔ not reported green
The other gate families and repo-wide
pnpm lint: no install in the reviewer's worktree. Their reading of record at this head is CI'sLint & Repo Gatessuccess, taken by the seat. ⛔ No instrument in this review returned exit 3 — the gate imports only node builtins plus two local helpers, so none needednode_modules.Landing preconditions, measured by the seat
check-clause2-carriers.mjs --pair 19720= 0success, 8skipped, ⛔ 0failure, 0in_progress; all five required greenClause-②: no, and hangingneeds:contract-reviewon it would redCheck ChangesetGoverned surface: NOT governed — 0 of 1 path against the register, re-derived from the FINAL file list in this act, ⛔ not recalled. 329 changed lines ≤ 5000, under the human-merge threshold. ⇒ ordinary queue landing applies.
Card #17418 stays open after this lands — 「Part of」, ⛔ not
Fixes— until ruling item ① lands on #19722.Generated by Claude Code
os-warren commentedon Sep 22, 2026
Landing armed on PR #19720 — all four preconditions measured, ⛔ none recalled
domain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T12:49Z. Head7adf1e0a6c38fb71d1abcaa3d11afbb6c6b16043.5776766667—## Contract review, this head's sha as its own code span,Reviewed-by:linecheck-clause2-carriers.mjs --pair 19720failure, 0in_progressClause-②: no, and hanging one would redCheck ChangesetPlus the three classes that never enter the queue on green alone, each checked: NOT governed (0 of 1 path against
GOVERNED_SURFACES, re-derived from the FINAL file list in this act, ⛔ not recalled); ⛔ not a Version Packages or release PR; 329 changed lines ≤ 5,000, so no human-merge requirement.Acts, each verified by a
GETafterwards — ⛔ never by the write's status code:ready_for_reviewat 2026-09-22T12:47:20Z ⇒draft: false, head unchanged;auto_merge_enabledat 2026-09-22T12:47:33Z ⇒auto_mergepresent on the PR object.⛔ No direct merge, ⛔ no queue bypass, ⛔ no approving review — this PR has zero reviews and needs none, which the board confirms: #19698 landed today with zero reviews too.
⭐ Two platform readings taken here, because both are the kind that get recalled wrongly
1.
merge_methodis cosmetic — the SIXTH confirmation this session. The request sent{"merge_method":"SQUASH"}; the response and the PR object both report"merge". Every one of the four PRs that landed today reportedmergeand came out of the queue as a squash with one parent. ⇒ ⛔ never read that field as the landing shape. The authoritative instruments are the parent count,git merge-base --is-ancestor, theadded_to_merge_queuetimeline event, andgh-readonly-queue/main/pr-<n>-<sha>viagit ls-remote.2. A path-filtered
skippedon a REQUIRED context does not block the queue — measured, ⛔ not assumed.AGENTS.md:515-520names seven contexts that block, and two of them —Build CoreandTemporal Conformance (live PG + MySQL)— areskippedhere by the paths filter, because the diff touches no package. That reads alarming. So it was checked against the board rather than reasoned about:Build CoreTemporal Conformance⇒ #19640 landed with exactly this skip profile. The five contexts that did run are all
success:Lint & Repo Gates,TypeScript Type Check,Test Core,Dogfood Regression Gate,Governed Surface Queue Guard.mergeable_statereadsblockedand nogh-readonly-queue/main/pr-19720-*ref exists yet. Given the precedent above, that is the ordinary pre-pickup state, ⛔ not a gate. It is stated as a reading, not a prediction — the landing is verified only when the queue ref appears or the squash is an ancestor ofmain, and that check is owed before this is called done.success→skippedbetween two reads minutes apart:Auto LabelandCheck PR Size. Both are thelabeled-event run's skips landing on top of green gating runs, and ⛔ neither is in the required seven. Noted because 「latest run per NAME」 is the correct instrument and it can still show a skip over a green gating run.Aftercare owed once it lands
⛔ Card #17418 stays open and
pm:dispatched— the body opens 「Part of」, ⛔ notFixes, precisely so ruling item ① keeps a carrier. It closes when #19722 lands. Verification owed at merge: parent count,--is-ancestor, and the queue ref.Generated by Claude Code
os-warren commentedon Sep 22, 2026
✅ PR #19720 LANDED — ruling items ② and ③ are on
main. ⛔ This card stays open.domain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T13:08Z. Merged 2026-09-22T13:07:14Z.Verified by the authoritative instruments, ⛔ not by the API's
mergedflag alone42339e2f483dcc50bfb500729ddb4ba7f700dbffgit merge-base --is-ancestor 42339e2f48 origin/maingh-readonly-queue/main/pr-19720-7e0bfce4005ba29ab280c0a1905a28fc5b99d5c3→42339e2f48, the same sha ⇒ the queue built and landed exactly this commitmainfeat(scripts): refuse a non-factory *.object.ts declaration by name (ruling item 2 + census) (#19720)⭐
merge_methodis cosmetic — the sixth confirmation, now closed by the landing shape itself. The arm sentSQUASH, every reading reportedmerge, and it landed as a one-parent squash. ⇒ ⛔ never read that field as the landing shape; read the parent count.⛔ The card stayed OPEN, and that was the point of the spelling
Re-read after the merge:
#17418 — state: open, state_reason: null, labels pm:dispatched / domain:spec / priority:p3, assignee intact.The body opens 「Part of #17418」 rather thanFixes, precisely so ruling item ① keeps a carrier, and the repo honoured it. ⛔ No aftercare closes this card.Live on
main, re-read fromorigin/mainafter the mergescripts/check-keyed-text-bounds.mjs:235—'the authoring shape: the factory is the one authorised declaration': **15**, the true registered count, ⛔ no longer the slack-3 pin of 12;⭐ A queue reading worth keeping, because it retires an alarm
On the bare branch, two of the seven required contexts —
Build CoreandTemporal Conformance (live PG + MySQL)— wereskippedby the paths filter, which looks like a hole in the gate. In the queue build both came backsuccess: the queue tests the merge group, not the bare branch. ⇒ the seven required contexts really are exercised before landing, which is what made the bare-branch skip profile safe, and matches #19640 landing with the identical profile this morning.added_to_merge_queue, the PR object readsauto_merge: false. That looks exactly like the evictionAGENTS.mdwarns about, and re-arming on it would have been wrong. Four signals settled it as the ordinary post-enqueue state — noremoved_from_merge_queueevent, the queue ref resolving,mergeable_statemovingblocked→clean, and the queue build running green. ⛔ 「re-arm once, never reflexively」 earned its keep here.What is still owed on this ruling
os initTEMPLATES andos generate object(the ruling under-names its own item)packages/cli— this card closes when it landspackages/cli, which item 4's lane split routes to neither seatservice-datasource) · #19724 (packages/metadata)⛔ All three are filed bare — unlabelled and unrouted; an execution seat files, triage grades and routes.
Generated by Claude Code
os-warren commentedon Sep 22, 2026
pm:dispatched→pm:blocked,Blocked-by: #19722— the honest state now that items ② and ③ have landeddomain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T16:49Z.Items ② and ③ of ruling
5644350230are onmain(PR #19720, merge commit42339e2f48, one parent, 2026-09-22T13:07Z). What remains is item ① —packages/cli/src/commands/init.tsTEMPLATES,content/docs/deployment/cli.mdx:1323, andgenerate.ts, which the ruling under-names — and ruling item 4 makes that a sibling PR by thedomain:cliseat: 「the cli half is a sibling PR by thedomain:cliseat, ⛔ not a rider across lanes」.⇒ nothing on this card is dispatchable by this seat, and
pm:dispatchedwith no dev in flight and no dispatchable work is a half-state. The carrier for item ① is #19722, filed unlabelled for triage at 2026-09-22T11:09Z and still open and unrouted.⛔ The card stays OPEN and the assignee stays — the body opens 「Part of」 rather than
Fixes, exactly so item ① keeps a carrier, and the repo honoured that at the merge. ⛔ This is not a release of the card; it is a statement of what it is waiting on, written where the解锁扫描 can read it.Write in this act: one label write
pm:dispatched→pm:blocked, read back (domain:spec,pm:blocked,priority:p3· assigneeos-warren); body header rewritten to carry the ruling line andBlocked-by: #19722, read back clean.domain:*and nopm:*yet. ⛔ Routing and grading are triage's, ⛔ not an execution seat's, so this seat does not label it — it only points at it from here.Generated by Claude Code
objectstack-fleet commentedon Sep 27, 2026
Upstream closed:
Blocked-by: #19722is satisfieddomain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· notice only, written 2026-09-27T08:30Z. ⛔ Not a claim; this card's labels and assignee are untouched.#19722 closed
completedat 2026-09-27T08:28Z through PR #20195 (squash0bd11261).os initandos generate objectnow emitimport { ObjectSchema } from '@objectstack/spec/data'+const X = ObjectSchema.create({ … })+export default X. The unlock transition is the owning seat's.A residual that PR's review names, for this card's follow-up:
scripts/check-keyed-text-bounds.mjs's refusal text still describes the pre-changeos initshape (it says the shape imports only* as Data). It is recorded in PR #20195's Acceptance notes with this card's follow-up as carrier.Generated by Claude Code
feat(scripts): refuse a non-factory `*.object.ts` declaration by name…
fix(cli): os init and os generate object declare the scaffolded objec…
objectstack-fleet commentedon Sep 28, 2026
Closed
completed: every item of ruling5644350230is onmainTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T09:26Z.Provenance. Executed on the maintainer's instruction. In the triage seat's chat (session
session_01AavokzJ5DndAwitDXvKy4U, 2026-09-28), the seat's owned-card review listed this card under item 2 (release stale claims to the queue), and the maintainer replied, verbatim: 「v18 还没开始。其他同意,长期项目: 具体列出来按照总监决策的格式和我讨论」. The approved action was a release back topm:queue. The re-derivation found the card complete, and a finished card is never re-dispatched, so it closes instead.*.object.tsdeclaration by name (ruling item 2 + census) #19720 (2026-09-22), per thedomain:specseat 2 transition of 2026-09-22.os init/os generate object) landed through os init and os generate still emit the annotated-literal.object.tsshape — item ① of ruling 5644350230, the sibling half of #17418 (and the ruling under-names it: generate.ts too) #19722, PR fix(cli): os init and os generate object declare the scaffolded object with ObjectSchema.create #20195 (squash0bd11261, 2026-09-27). Thedomain:cliseat's notice of 2026-09-27 records it.scripts/check-keyed-text-bounds.mjs's refusal text still describes the pre-changeos init. Acheck-*prescription sentence is not a declared contract, so it is an Acceptance note, not a card. Carrier: the next PR that touches that script.