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-litant commentedon Sep 10, 2026
Triage: routed to the decision box — it asks which authoring shape is authorised, which is an authoring-surface decision.
domain:spec,needs-user-decision.Measured on
origin/maind57611dfd3: two official scaffolders and two published docs disagree on how a.object.tsmay be written —ObjectSchema.create()factory versus a plain annotated literal — and the gate only reads the factory shape (check:keyed-text-bounds'CREATE_CALLregex).⇒ ⛔ Not
pm:queue: the gate's blindness is evidence of a convention, ⛔ not a ruling on one. Deciding which shape authors may write changes the authoring surface.⭐ Filed exactly as #16195 instructed — "file this finding if the question could be read clearly while doing that card". It could not be resolved outright, and the filer brought published evidence on both sides rather than picking. That is the right shape for a decision card.
维护者速读
改了什么 — 什么都还没改。这是问「写一个对象定义文件,到底哪种写法是我们认可的」。
为什么改 — 我们自己的两个脚手架生成的写法不一样,两份已发布文档教的写法也不一样:一种用工厂函数⚠️ 而门禁只认工厂那一种 —— 也就是说,按另一种写法写出来的文件,不受任何检查保护,而它是我们自己的文档教的。
ObjectSchema.create({...}),另一种直接写一个带类型标注的普通对象。风险与代价(含回滚) — A(定工厂为唯一写法):门禁立刻覆盖全部文件,代价是要改另一个脚手架和一份文档,且已按字面量写法写好的用户文件会变成不合规。B(两种都认):谁都不用改,代价是门禁要学会读第二种形态(更复杂),而且我们永久维护两种写法。回滚:两者都可回,但 A 一旦执行会触及用户已有文件,回退成本更高。
席位意见 — 荐 A(工厂为唯一认可写法)。理由:门禁只认它,说明实现早就把它当成正规形态了;而「一个操作两种写法」正是让 AI 和人各写一半、最后没人受检查的结构。⚠️ 但 A 有一个我读不到的代价 —— 有多少现存文件是按字面量写法写的,见置信缺口。
你要做的(一个动作) — 选一个字母:A(只认工厂写法)/ B(两种都认,门禁学会读第二种)。
os-decision-facets
推荐:A(工厂为唯一认可写法)。回退项:B,若既存字面量文件数量大到迁移不划算。
.object.ts有多少(仓内示例、模板、以及任何真实部署)。若接近零,A 几乎无代价;若数量可观,A 需要配一次迁移,推荐可能翻向 B。⇒ 这个数一次普查就能得到;若你希望先看数再裁,把卡打回并说一声。分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T14:20Z · 本评论来自分诊座位Generated by Claude Code
os-tesla commentedon Sep 12, 2026
Ruling recorded — A:
ObjectSchema.create({ … })is the one authorised shape for a.object.ts;os initand its doc page move to it; the gate refuses the annotated-literal shape by name (director seat, decision batch #122 item 1, 2026-09-12)Maintainer, verbatim (live PM chat, 2026-09-12T05:5xZ), to decision batch #122 presented as
1A·2A·3A·4A·5A: 「同意」.Derived first from the long-term axis: one authoring shape, validated against the protocol at the moment of definition. The factory parses the object against
ObjectSchemawhen the file is evaluated, so an error surfaces where it was written; the typed literal defers everything to a build the author may never run. The gate (check:keyed-text-bounds),create-objectstack's template,schema-design.mdx("Every object definition follows this pattern") and every ADR example already stand on the factory;os init'sData.ServiceObjectliteral is the single outlier. B would keep two parallel shapes with every future check written twice.What is ruled
packages/cli/src/commands/init.tsTEMPLATESemit the factory shape;content/docs/deployment/cli.mdx:1323describes it.*.object.tsthat does not use the factory is refused by name — the existing gate (or a lint id if the gate is the wrong home) says "useObjectSchema.create", so a file written the old way is never silently unprotected again.Clause-②: no(no accept-set change on a published payload; an authoring-shape rule tightens on the CLI surface). The taker declares the file surface; if it crossespackages/cliandpackages/spec/scripts, the cli half is a sibling PR by thedomain:cliseat, ⛔ not a rider across lanes.State
needs-user-decision→pm:queue;domain:spec/priority:p3kept.Generated by Claude Code
os-warren commentedon Sep 22, 2026
Claim: PM loop round 18
Session:
session_01UDXER3sdqfeVYpEWZs5mZxBranch:
claude/issue-17418-object-ts-authoring-shapeWorktree:
objectstack-issue-17418Domain:
domain:specSeat:
domain:spec#2File surface:
scripts/check-keyed-text-bounds.mjs(the gate — ruling item 2), any in-repo literal-shaped*.object.tsthe census finds outsidepackages/cli(ruling item 3), and.changeset/**. ⛔ NOTpackages/cli/src/commands/init.ts, ⛔ NOTcontent/docs/deployment/cli.mdx— see the lane split below.Container & model:
M,mode:subagent, model: the CEILING tier (CONTRACT_REVIEW_TIER), re-read fromorigin/mainin this act —scripts/pm/dispatch-gates.mjs:12282Clause-②: no
Thread-read: 5644350230
Serial constraints cleared: open-PR file census re-taken in this act over all open PRs, ⛔ not inherited.
scripts/check-keyed-text-bounds.mjs— zero open PRs touch it. One.object.tsIS held: PR #19618 holdspackages/platform-objects/src/identity/sys-api-key.object.ts. ⇒ if the conversion census names that file, it serializes behind #19618 or is skipped with a declared reason; every other*.object.tsis free. Lit control on the same pass: the census does return hits (that one), so the zero on the gate file is a reading.Labels written and read back before this comment:
pm:dispatched,domain:spec,priority:p3; assigneeos-warren.⛔ This card is HALF of a ruled change, and the split is the ruling's own
Ruling
5644350230(director seat, batch #122 item 1, maintainer 「同意」 2026-09-12), item 4, quoted:It crosses. ⇒ the split, declared:
TEMPLATESinpackages/cli/src/commands/init.tsemit the factory shape;content/docs/deployment/cli.mdx:1323describes itpackages/clidomain:cli— a sibling PR*.object.tsnot using the factory is refused by name — 「useObjectSchema.create」scripts/check-keyed-text-bounds.mjs*.object.tspackages/clidomain:clicards and found none for item ①. Re-reading item 4, that is not a dropped obligation — the sibling PR is the taker's to raise on taking, and this seat will file it bare for triage once the dev reports the exactpackages/clisurface, so the card names real files rather than a guess.⭐ The sequencing question the dev must answer FIRST, before writing the gate
If the gate refuses the annotated-literal shape while
os initstill emits it, a freshly scaffolded project is born refused. That would be a bad landing order.check:keyed-text-boundsis a repo script that runs on this repo's*.object.tsfiles, whileos initemits into a user's project, which never runs it. ⛔ That is a reading, not a measurement, and the dev must settle it:packages/cliowns — itsTEMPLATESstring literals, its fixtures, its tests?If yes ⇒ ⛔ stop and report; the order becomes ① then ②, and this card waits. If no ⇒ ② and ③ land independently and ① follows on its own card. Say which, with the reading.
Measured starting points, ⛔ re-derive rather than inherit
The card reports the gate's
CREATE_CALLregex recognisesObjectSchema.create({ … })and that all 112*.object.tsfiles it currently parses are that shape. This seat counted 112*.object.tsfiles onorigin/main— the same number, which is consistent with an empty conversion set for item ③ but ⛔ does not establish it: the gate's parsed population and the tree's file population are two different sets, and a literal-shaped file the gate cannot parse would be invisible to exactly the instrument the card used.⇒ item ③'s census needs an instrument that finds
*.object.tsfiles the gate does not parse, with a lit control. If it finds none, say so with the control and the reach; 「zero to convert」 is a fine answer and a very different one from 「the gate saw none」.⛔ Boundaries
⛔ No
packages/clifile, for any reason — that is the sibling lane's, by the ruling. ⛔ Do not editcontent/docs/releases/. ⛔ No label, assignee, ready-flip, auto-merge or enqueue. ⛔ Do not touchsys-api-key.object.tswithout declaring the serial hold above.Changeset: measure whether one is owed and apply the measured answer; the ruling asks the changeset to state the one mechanical user rewrite (wrap the literal). ⛔ Do not assert
skip-changeset— if the measurement says the PR releases nothing, name it in the report and the seat applies the label with its reason.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, "sequencing_answer": { "verdict": "NO — landing item 2 alone turns nothing red that only item 1 can fix. Items 2 and 3 land independently; item 1 follows on its own card. The seat reading holds, and it is now a measurement rather than a reading.", "reading": [ "The gate population is a FILENAME SUFFIX, not a shape: walkObjectFiles collects files whose name ends .object.ts, repo-wide minus SKIP_DIRS. Measured on origin/main 4fba5036f2: 112 files, ZERO of them under packages/cli. git ls-files filtered on the same suffix also returns 112, so no untracked file is hiding either.", "The packages/cli TEMPLATES are string literals inside packages/cli/src/commands/init.ts — a .ts file, not a *.object.ts file. The walk matches by filename, so the template strings are never read by this gate, in any shape. Same answer for packages/cli fixtures and tests: zero *.object.ts files are tracked anywhere under packages/cli.", "Every packages/cli scaffold test writes into os.tmpdir() via mkdtempSync(join(tmpdir(), ...)), never into the repo tree — so no test run can transiently materialise a literal-shaped *.object.ts inside the walk. git status --ignored shows no ignored *.object.ts either.", "os init emits into a USER project, which does not carry this repo scripts/ directory." ] }, "census": { "result": "ZERO to convert. No *.object.ts file in the tree carries a literal-shaped declaration.", "instrument": "An independent scratchpad instrument, NOT the gate. Over the same walk it lists every top-level binding of an object literal or of a call, plus every export default, then subtracts the ones whose initializer is an ObjectSchema.create call. Whatever remains is a candidate for hand triage. Deliberately over-inclusive and shape-agnostic: it does not depend on knowing the spelling Data.ServiceObject, so a literal declaration written with any annotation, or none, is still listed.", "reading": "112 files walked; 120 top-level bindings/defaults seen; 117 initialized by an ObjectSchema.create call; 0 initialized by a bare object literal; 3 initialized by something else. The 117 equals the gate own parsed declaration count exactly, reached by a different reader. The 3 were PRINTED rather than counted away, and are plainly not declarations: a regex literal, a template string, and a string const.", "control": "LIT. The same instrument over a two-file control tree returns 2 candidates — a pure literal file, and one hidden behind a factory declaration in a MIXED file. So the zero above is a reading, not an empty sweep.", "reach": "Population is **/*.object.ts, which is the RULED population: the object row of DEFAULT_METADATA_TYPE_REGISTRY (packages/spec/src/kernel/metadata-plugin.zod.ts:725) declares filePatterns for .object.ts, .object.yml and .object.json. BLIND TO: declarations in files not carrying that suffix; the .yml and .json patterns (not TypeScript, so the factory does not apply); declarations assembled at runtime rather than written as a literal. Cross-checks over the whole tree, reported rather than assumed: zero export default and zero satisfies occurrences in any of the 112 files outside comments and strings, and zero indented object-literal bindings.", "sys_api_key": "NOT named by the census, so no serialization behind PR #19618 was needed and packages/platform-objects/src/identity/sys-api-key.object.ts was not touched." }, "gate_refusal_message": " packages/platform-objects/src/__shape_demo__/mixed.object.ts:10\n `ctrlHidden` is declared as a plain object literal — use `ObjectSchema.create({ ... })`.\n Recognised as a declaration by a `ServiceObject` type annotation, and a literal `name:` beside a `fields:`, the two keys the factory parser requires.\n `ObjectSchema.create` is the one authorised shape for a `.object.ts` declaration: it parses\n the declaration against ObjectSchema when the file is evaluated, so an error surfaces where\n it was written. A typed literal defers every check to a build the author may never run — and\n this gate cannot read it at all, so every keyed text column in it goes unjudged.\n The conversion is mechanical: wrap the literal in `ObjectSchema.create( ... )`.\n ⛔ Do not teach this scan the literal shape instead — the shape is refused, not unknown.", "cli_surface_for_item_1": { "note": "Measured for the domain:cli sibling card. Ruling item 1 UNDER-NAMES the surface: generate.ts is a second emitter carrying a declared coupling to init.ts, and the ruling does not name it.", "emitters": [ "packages/cli/src/commands/init.ts:649 and :744 — the two TEMPLATES entries for src/objects/__name___item.object.ts, both emitting a Data.ServiceObject-annotated object literal.", "packages/cli/src/commands/generate.ts:99 — os generate object emits the same annotated literal. NOT NAMED BY THE RULING, AND IT MUST BE: its own docblock declares the coupling — \"#9666 took it once for the os init templates, and this emits the SAME value with the same explanation, so the two doors an author can arrive through agree. If that template value ever moves, this one moves with it.\" Converting init.ts alone breaks the parity that docblock asserts." ], "pins_that_move_with_them": [ "packages/cli/test/generate-refuses-unparseable-name.test.ts:255 — asserts the emitted source contains a Data.ServiceObject binding line.", "packages/cli/test/generate-emission-parses.test.ts:148 — same assertion, plus its docblock worked example at :14.", "packages/cli/test/scaffold-emission-typechecks.test.ts:26 — docblock states the repair is Data.ServiceObject.", "packages/cli/test/init.test.ts:493 — reads the scaffolded my_app_item.object.ts; re-check its assertions against the new bytes.", "packages/cli/test/init-template-comments-self-contained.test.ts — the templates carry a long authored org-wide-default comment block that must survive the rewrite." ], "docblock_only_no_behaviour_change": "packages/cli/src/utils/emitted-source-parses.ts:14 — the utility itself is shape-agnostic (it asks the TypeScript parser whether the emitted bytes parse); only its worked example names the literal shape.", "coupled_probably_no_edit": "scripts/sync-scaffold-emission-policy.mjs (POLICY_SOURCE is packages/cli/src/commands/init.ts) syncs pnpm/typescript version ranges, not declaration shape — but the sibling should re-run it, because create-objectstack bundled note.object.ts is already the factory shape and the two scaffolders would finally agree.", "docs": "content/docs/deployment/cli.mdx:1323" }, "changeset": { "measurement": "NO changeset owed — this PR publishes nothing. scripts/check-keyed-text-bounds.mjs sits inside NO workspace package directory (checked against every tracked */package.json directory), and npm packs relative to the package directory, so it cannot be packed. The repo-root manifest is private: true. No non-private package lists a scripts directory in its files array. POSITIVE CONTROL: packages/spec is non-private and ships dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface and spec-changes.json — a real files array exists and does not reach repo-root scripts/. The new symbol literalShapeDeclarations occurs in exactly one file, that one.", "label": "skip-changeset NOT asserted, per the dispatch. The seat applies it with its reason. Expect the Check Changeset job red until it does.", "ruling_obligation": "The ruling asks the changeset to state the one mechanical user rewrite (wrap the literal). That rewrite is something a USER experiences, and the user-facing change is item 1 — os init and os g emitting the factory shape from a PUBLISHED package. So the ruling changeset obligation attaches to the domain:cli sibling PR, not to this half." }, "summary": "Implemented ruling items 2 and 3. check:keyed-text-bounds now refuses a non-factory *.object.ts declaration BY NAME (\"use ObjectSchema.create\"), through an authoring-shape scan that runs independently of the factory parse. The hole this actually closes is not the whole-file case (already refused, though generically and with advice to widen the parser) but the MIXED case: the old zero-declarations backstop is conditioned on objects.length === 0, so a file holding a factory declaration AND a literal one read as complete and the literal one was judged by nothing — measured on a control tree where a literal ctrl_hidden keying a UNIQUE index on an unbounded text column was swept, parsed as 1 object, and reported clean. Item 3 census returns zero to convert, taken with an independent instrument and a lit control. Gate counts over the tree are unchanged: 112/117/250/592/147 before and after. Assignee os-warren was already set by the dispatch; I did not write it.", "tests": "node scripts/check-keyed-text-bounds.mjs :: exit 0 — counts 112/117/250/592/147, identical to base. node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0, 0 failures; the new battery \"the authoring shape: the factory is the one authorised declaration\" registers 14 cases against a pinned floor of 12, SELF_TEST_BATTERY_FLOOR moves 9 to 10, and all 14 appear by name in the log. ALL 31 derived gate families were run TWICE — at beef275c08 and again in full at the final commit 4d034cd2f9 — and dispatch-gates --ran reconciles both: 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN, every row carrying its exit code, all 0; the derived set is identical at both heads. ABLATION (no rebuild leg applies — a plain .mjs script with no dist, so ablation-dist-preflight is not in the chain): carried by scripts/ablation-replace.mjs so the on-disk proof is the tool own verdict rather than a remembered claim. Anchor \"literalShapeDeclarations(struct, masked).map\" hit x1 then x0; blob bce8e230a48d then 71718fb1874b. With the detector present the gate exits 2 and NAMES ctrlHidden; with it ablated the gate exits 0 and ctrlHidden is unnamed. Restore verified: blob equals HEAD (bce8e230a48d) and git diff HEAD empty. The ablation also FOUND A DEFECT in my first draft: the ablated run still printed \"0 literal-shaped declarations ... every declaration is ObjectSchema.create\" over a tree holding an unbounded keyed text column judged by nothing. That zero has no floor under it, so a dead detector printed the identical line; the pass line now reports what was SCANNED and names --self-test as the liveness proof (second commit). LINT: a declared narrowing, not a repo-wide scan. eslint --no-inline-config --format json on the one changed file :: exit 0, 1 file linted (count read from the JSON output), 0 errors, 0 warnings. The narrowing excludes nothing: this repo runs one eslint.config.mjs, which per its own comment at line 327 never enables type-aware linting (no parserOptions.project, no typed rules) for ANY file, measured there with a positive control — so a one-file diff cannot move the verdict on any file it does not touch. NOT MEASURED: repo-wide pnpm lint — scripts/pm/os-verify-lock.sh returned queue-timeout (exit 99) after 9m00s, never acquired, holder pid 19251 running scratchpad/issue-19580/val6.sh, held 1391s. It is CI-owned and not owed by this card. DEVIATION: that lock call was made without OS_VERIFY_LOCK_SLOT set, so the queue position was not retained; I declared the narrowing instead of re-queueing from the back.", "ci": "in_progress — PR #19720 was opened at the close of this run and ZERO job conclusions were read. No CI-by-JOB table is offered because none was measured, and no aggregate roll-up is offered in its place. Expect Check Changeset red until the seat applies skip-changeset with its reason.", "mcp_calls": "0 — no MCP GitHub tool was called; every GitHub read and write went through the REST proxy with curl.", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR #19720) and POST /repos/objectstack-ai/objectstack/issues/17418/comments (this report). 0 label writes: the dispatch forbade label, assignee, ready-flip, auto-merge and enqueue. 3 git pushes, which are not REST.", "open_questions": [ { "question": "Two further emitters of the now-outlawed literal shape live OUTSIDE packages/cli, so the ruling item-4 lane split routes them to neither seat. Who owns them?", "options": [ "A. File them as their own cards in the lane that owns each package (service-datasource, metadata), each with its own changeset, since both sit in PUBLISHED packages and changing either alters a published payload.", "B. Fold them into the domain:cli sibling as \"all scaffolder doors\", accepting that the sibling then crosses three packages and two lanes — which is the cross-lane rider ruling item 4 forbids by name.", "C. Leave them, and accept that os datasource introspect keeps writing a *.object.ts in the shape this gate now refuses." ], "recommendation": "A. The ruling makes ObjectSchema.create the ONE authorised shape for a *.object.ts and both of these write one, so C contradicts the ruling outright. B is exactly the cross-lane rider item 4 names. A also keeps this PR Clause-②: no reading intact — converting either emitter changes a published payload and would re-open it." }, { "question": "Does this PR owe the Chinese maintainer-read section? My dispatch conditions it on managed/governed paths, and this diff touches none — scripts/check-keyed-text-bounds.mjs is not a governed surface under Prime Directive #14.", "options": [ "A. No section owed — the requirement partitions PRs that touch managed paths, and AGENTS.md Communication says GitHub artefacts are English.", "B. Owed on every rules-layer PR regardless of governance, in which case this body is missing it." ], "recommendation": "A. I read the classification sentence as partitioning PRs that touch managed paths, not all PRs; adding a section with an empty seat-opinion placeholder to a PR that owes none would signal a review obligation that does not exist. Flagging rather than choosing silently, because the body is written once — if the seat reads it as B, the seat adds it." } ], "out_of_scope_findings": [ "to file (3 classes; dedupe words: introspect draft, external datasource, ServiceObject literal, ADR-0015, third scaffolder) — class (b) contract violation. packages/services/service-datasource/src/external-datasource-service.ts:931 emits a ServiceObject-annotated object literal as the draft that os datasource introspect --out objects/x.object.ts writes into a USER project (ADR-0015). The ruling makes ObjectSchema.create the one authorised shape for a *.object.ts; this writes one in the refused shape, from a published package, and it is a third scaffolder door the ruling names nowhere. Pinned at packages/services/service-datasource/src/__tests__/external-object-draft-os-build.test.ts:138. Seam: spec:ObjectSchema.create authoring shape (ruling 5644350230 item 2) -> runtime:external-datasource-service.ts:931 | consumer: packages/cli/src/commands/datasource/introspect.ts:80 writeFile", "to file (3 classes; dedupe words: TypeScriptSerializer, metadata serializer, ServiceObject literal, typescript format) — class (b) contract violation. packages/metadata/src/serializers/typescript-serializer.ts:23 emits a ServiceObject-annotated metadata literal plus a default export for the typescript metadata format — the same refused shape, from a published package. Seam: spec:ObjectSchema.create authoring shape (ruling 5644350230 item 2) -> runtime:typescript-serializer.ts:23 TypeScriptSerializer.serialize | consumer: none found in-repo outside the serializer registry", "to file (3 classes; dedupe words: os generate, generate.ts, TEMPLATES coupling, scaffolder parity) — belongs ON the domain:cli sibling card rather than as a card of its own: ruling item 1 names only init.ts TEMPLATES, but packages/cli/src/commands/generate.ts:99 emits the identical literal and its own docblock declares the coupling (\"If that template value ever moves, this one moves with it\"). Converting init.ts alone breaks the parity that docblock asserts.", "noted, not filed: provenanceLine still reports -1/-1/-5/-2/-4 against MEASURED.ref fa5d137ab0, which that file header defines as information and never a verdict — no action. Carrier: none; no PR or person is due to touch it.", "noted, not filed: packages/objectql carries roughly forty ServiceObject-annotated literal fixtures in *.test.ts files. They are in-memory engine fixtures, not object FILES — outside the ruled population (**/*.object.ts) and outside this gate walk — and several deliberately construct off-spec objects to probe engine behaviour, so converting them would change what those tests test. Carrier: any future card that widens the authoring rule beyond *.object.ts." ] }Generated by Claude Code
os-warren commentedon Sep 22, 2026
skip-changesetapplied to PR #19720 — route 1, with the measurement it owesdomain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T11:00Z. Label write landed and read back:size/m,skip-changeset.⭐ This was the seat's act by design, ⛔ not the dev's omission. The dispatch told it: 「⛔ Do not assert
skip-changeset— if the measurement says this PR releases nothing, name it in the report and the seat applies the label with its reason.」Check Changesetgoing red before the label exists is the expected shape of that division, ⛔ not a failure.The measurement — route 1, 「releases nothing」
PR #19720 changes exactly one file:
scripts/check-keyed-text-bounds.mjs, at the repo root, outside every one of the 76packages/*directories. npm packs only from a package's own directory.package.json@objectstack/spec-monorepo,private: true⇒ publishes nothingscripts/, ⛔ inside no package dirfiles[]entries beginning../or/, or naming this file)packages/**andapps/**Lit control on the same instrument: it reads
files[]correctly —packages/specreturns 10 entries — so the zero is a reading and ⛔ not a dead scan.head -40), and it widened before concluding rather than after. That is the third occurrence of the truncation shape this session, and it is the one that was caught before the claim was published rather than after.⇒ nothing in this PR reaches a published tarball. Route 1, and
skip-changesetis the true declaration — ⛔ not a convenience.Ruling D checked, ⛔ not assumed
Ruling D on #18375 (maintainer, 2026-09-18): the
skip-changesetlabel is never applied to a PR that edits an existing changeset. Measured on this PR's file list: zero.changeset/files touched. ⇒ the ruling does not bar the label here.The re-run, because the label alone does not clear the red
Check Changesetran before the label existed, and — per the mechanism recorded at5772068108— alabeledevent spawns a run whose jobs skip, so nothing in the event graph ever re-judges the earlier red. The seat re-runs the failed job in this same act, which is where the live label re-read makes it green.⛔ Not a flake re-run and ⛔ not spent against that budget: the gate's input changed after it ran.
Generated by Claude Code
os-warren commentedon Sep 22, 2026
ACCEPT — ruling items ② and ③. ⭐ And the best thing in this round is a defect the dev found in its own work.
domain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T11:03Z. PR #19720, head4d034cd2f96d32c62954f38fd4477338b2a704c8, draft, one file:scripts/check-keyed-text-bounds.mjs, +321 / −6. ⛔ CLI fence held — zeropackages/clipaths, zerocli.mdx, verified by the seat against the file list.⭐ The body opens 「Part of #17418」, ⛔ not
Fixes— correct, and the repo gates it. The card stays open andpm:dispatchedafter this lands, because ruling item ① is another lane's.⭐⭐ The ablation found a defect in the dev's own first draft
With the detector ablated, the gate still printed 「0 literal-shaped declarations … every declaration is
ObjectSchema.create」 — over a control tree holding an unbounded keyed text column judged by nothing. That zero had no floor under it: a dead detector produced the identical success line.⇒ fixed in a second commit — the pass line now reports what was scanned and names
--self-testas the liveness proof.This is the defect class that makes a green gate worthless, and it was caught by the dev ablating its own instrument rather than by any reviewer. ⭐ An ablation that only confirms what you hoped is decoration; this one was run honestly enough to fail.
Two claims the seat re-derived first-hand, ⛔ not adopted
1. The sequencing answer is now a MEASUREMENT, not the seat's reading. The seat's dispatch offered a reading that the hazard was narrower than it looked. The dev replaced it with the mechanism:
walkObjectFilesselects by filename suffix, not by shape. Seat's own count onorigin/main: 112*.object.tsfiles, ZERO underpackages/cli. The cli TEMPLATES are string literals inside a.tsfile, so the walk never reads them; and every cli scaffold test writes intoos.tmpdir(), never the repo tree. ⇒ landing ② alone turns nothing red that only ① can fix. Items ② and ③ land independently.2. ⭐ The hole this closes is NOT the one the card describes, and this reframing is the round's most valuable output. Confirmed at source by the seat:
scripts/check-keyed-text-bounds.mjs:792reads⇒ the zero-declarations backstop fires only when nothing was recognised. A file carrying a factory declaration and a literal one parses as 1 object, never trips it, and the literal declaration is judged by nothing — measured by the dev on a control tree where a literal
ctrl_hiddenkeying a UNIQUE index on an unbounded text column was swept, parsed as 1 object and reported clean. The whole-file case was already refused; the MIXED case was the real hole.Item ③'s census — the instrument was built the way the dispatch demanded
The dispatch warned that the card's two 「112」s are not the same fact, and that a literal-shaped file the gate cannot parse is invisible to exactly the instrument the card used. The dev built an independent, over-inclusive, shape-agnostic scan instead: 112 files, 120 top-level bindings/defaults, 117 initialised by an
ObjectSchema.createcall, 0 by a bare object literal, 3 by something else — ⭐ printed rather than counted away (a regex literal, a template string, a string const). The 117 equals the gate's own parsed count, reached by a different reader.Lit control: the same instrument over a two-file control tree returns 2 candidates — a pure literal file and one hidden behind a factory in a MIXED file. ⇒ 「zero to convert」 is a reading, ⛔ not an empty sweep. Reach stated, including what it is blind to.
sys-api-key.object.tswas not named, so ⛔ no serialization behind PR #19618 was needed and it was not touched.The seat's answers to the two open questions
Q1 — the two emitters outside
packages/cli: option A, and the seat files them.service-datasource/src/external-datasource-service.ts:931andmetadata/src/serializers/typescript-serializer.ts:23both emit the now-refused shape from published packages, and ruling item 4's lane split routes them to neither seat. ⇒ A: each becomes its own card in the lane that owns its package. ⛔ B is precisely the cross-lane rider item 4 forbids by name; ⛔ C contradicts the ruling outright, since the ruling makes the factory the one authorised shape for a*.object.tsand both of these write one. The dev's reasoning is adopted verbatim and its recommendation is right.Q2 — no Chinese maintainer-read section is owed. Option A. The four Chinese channels are 维护者速读 / 轮报 / 裁决引文 / 四维分析, and the 速读 obligation attaches to decision cards, not to every PR. This is a
Clause-②: noPR touching no governed path. ⭐ Flagging it rather than choosing silently was right — the body is written once — but the answer is A, and ⛔ adding an empty placeholder would signal a review obligation that does not exist.Ruling item ① names only
init.tsTEMPLATES. The dev measured a second emitter:packages/cli/src/commands/generate.ts:99emits the identical literal, and its own docblock declares the coupling — 「#9666 took it once for theos inittemplates, and this emits the SAME value with the same explanation … If that template value ever moves, this one moves with it.」 ⇒ convertinginit.tsalone breaks the parity that docblock asserts. The sibling card will name both, plus the five pins that move with them.Declared deviation, accepted
Repo-wide
pnpm lintis NOT MEASURED:scripts/pm/os-verify-lock.shreturned queue-timeout (exit 99) after 9m00s, never acquiring; holder pid 19251, held 1391s. Declared as CI-owned rather than silently skipped. ⭐ And the dev disclosed its own procedural slip inside that — the lock call was made withoutOS_VERIFY_LOCK_SLOT, so queue position was not retained, and it declared the narrowing instead of re-queueing from the back. The single-file lint narrowing it did run carries all three pieces (population from the config, count from--format json, invariance from the config's own no-type-aware-linting statement with a positive control).State
skip-changesetwas applied by the seat with its measurement at5775287713(route 1: the one changed file is repo-rootscripts/, inside no package dir; root manifestprivate: true; 77 manifests censused forfiles[]escape hatches, zero found, lit control passing), and the staleCheck Changesetred was re-run in the same act — ⛔ not a flake re-run, the gate's input changed after it ran.CI at head: 30 names — 18
success, 9skipped, 2in_progress, 1queued, ⛔ 0failure⇒ NOT MEASURED.An at-tier review is dispatched against this head, with the constant re-read from⚠️ a value that changed this morning by maintainer ruling; see #19603. PR #19720 stays draft. ⛔ No ready-flip, no enqueue, no auto-merge.
origin/mainatscripts/pm/dispatch-gates.mjs:12282in that act —Generated by Claude Code
os-warren commentedon Sep 22, 2026
Q1 discharged — all three sibling cards now exist, and the ruling's own lane split is what put them there
domain:specexecution seat 2, sessionsession_01UDXER3sdqfeVYpEWZs5mZx, 2026-09-22T11:20Z. This closes the commitment the seat made in its round ACCEPT: 「option A, and the seat files them」. ⛔ Nothing on this card changes state; it stayspm:dispatchedbehind PR #19720.5644350230itemscripts/check-keyed-text-bounds.mjsFixes)os initTEMPLATES — andos generate object, which the ruling does not namepackages/clipackages/cli, which item 4's lane split routes to neither seatpackages/services/service-datasourcepackages/metadataAll three are filed bare — ⛔ unlabelled and unrouted. An execution seat files; triage grades and routes. Each carries a suggested lane and the reason for it, ⛔ not a
domain:*label.What the seat measured first-hand before filing, ⛔ not relayed from the dev's report
#19723 — the emitted bytes really are destined for a
⚠️ Its lane may split again by ruling item 4 — the emitter is
*.object.ts, which is what separates it from a stylistic card. Four readings atorigin/main, quoted on the card: the generator's own docblock atexternal-datasource-service.ts:857(「Render a reviewable*.object.tssource string」),cli/src/commands/datasource/introspect.ts:18and its published example at:26(--out objects/wh_order.object.ts), and the service's own error text at:507telling the user to 「commit the generated *.object.ts instead」. ⇒ once item ② lands, the gate refuses exactly the fileos datasource introspectjust told the user to commit.⭐ And the package has been here before: its CHANGELOG at
:1422records this same generator once emitting 「a*.object.tsthe platform's own validator refuses, on two independent counts」. The card tells the taker to read that precedent first.packages/services, the door ispackages/cli. Declared on the card as the taker's call, ⛔ not resolved by this seat.#19724 — the package's own README already states the contract the code breaks.⚠️ Not crashing is the hazard here, not the safety.
packages/metadata/README.md:77reads 「TypeScriptSerializer —.ts/.jsmodule exports (forObjectSchema.create(),defineView(), etc.)」 whileserializers/typescript-serializer.ts:22-24emitsexport const metadata: ServiceObject = …. ⇒ a declared ≠ enforced finding that stands whether or not the ruling reaches this site.⛔ And one thing the seat did NOT measure, declared as NOT MEASURED rather than assumed: whether this serializer's output ever lands in a file named
*.object.ts. The registration is traced (metadata-manager.ts:572/:575, re-exported atindex.ts:53, so it is public API of a published package) but no write path with a filename was found — ⛔ an absence with an unstated reach is not a finding. That is the taker's first question, because it decides the card's scope.⭐ The card also names the round-trip coupling the conversion must be tested through:
deserializeat:31-56finds the first{afterexport constand brace-matches. UnderObjectSchema.create({ … })that brace is still the literal's — so the parser probably does not crash, and would silently drop the factory wrapper.Why option A and not B or C, restated because the boundary is the ruling's
⛔ B (ride them on PR #19720) is precisely the cross-lane rider ruling item 4 forbids by name. ⛔ C (leave them) contradicts the ruling outright: it makes the factory the one authorised shape for a
*.object.ts, and both of these write one. The dev raised the question rather than choosing silently, and its recommendation was right.This card's own state, unchanged
#17418 stays open and
pm:dispatchedafter PR #19720 lands — the body says 「Part of」 for exactly that reason, and the repo gates it. It closes when item ① lands on #19722, ⛔ not when #19720 merges.Generated by Claude Code
1 remaining item
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.