feat(scripts): refuse a non-factory *.object.ts declaration by name (ruling item 2 + census) - #19720
Merged
Merged
Conversation
`check:keyed-text-bounds` finds object declarations by `CREATE_CALL` (`ObjectSchema.create(`) and nothing else, so a declaration written as a plain annotated literal is invisible to it. One backstop existed -- a file yielding zero declarations and zero refusals is refused -- but it 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 two-file control tree: a literal `ctrl_hidden` keying a UNIQUE index on an unbounded `text` column -- the exact defect this gate exists to catch -- was swept, parsed as 1 object, and reported clean. The authoring-shape scan now runs independently of the factory parse, over every `*.object.ts`, and refuses a literal-shaped declaration BY NAME: "use `ObjectSchema.create`". Four signals, stated in the header: a `ServiceObject` annotation, a `satisfies ServiceObject`, the file's default export, or a literal `name:` beside a `fields:` at column 0. The zero-declarations backstop steps aside when the shape scan already named the reason, so a literal-shaped file gets one finding that prescribes the factory rather than a second one inviting the parser to be widened. Counts are unchanged over the tree (112/117/250/592/147) and the pass line now reports the authoring-shape reading beside them. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
…verdict Ablating the shape scan left the pass line claiming "0 literal-shaped declarations ... every declaration is `ObjectSchema.create`" over a tree that held `ctrl_hidden.slug` -- an unbounded text column keyed by a UNIQUE index -- judged by nothing. The tree carries no literal-shaped declaration, so that zero has no floor under it and a dead detector prints the identical line. State what was scanned instead, and name `--self-test` as the liveness proof, so a green log cannot be read as a verdict the detector can no longer back. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 22, 2026
The roster pinned this battery at 12 while it registers 15, so any three cases could be deleted and the floor would still pass -- including both MIXED cases, which are the only ones that exercise the hole the rule was written to close. Pin it at 15, measured at this head. The `insideFunction` case pinned nothing: its fixture spelled `name: n`, an identifier, so the structural signal was unreachable from it whatever the column-0 guard did. Ablating that guard left `--self-test` green. With `name: 'b'` the case reaches the guard and the ablation goes red. The refusal text named the conversion but not the import: the `os init` shape imports only `* as Data`, so `ObjectSchema` is not in scope for the author being told to call it. The message now says so. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
os-warren
marked this pull request as ready for review
September 22, 2026 12:47
os-warren
enabled auto-merge
September 22, 2026 12:47
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…ape for object drafts (objectstack-ai#19776) Fixes objectstack-ai#19723 Clause-②: no — this pulls the emitter back onto a shape a ruling already declared; it adds no exported symbol and no key on any published payload. Verified: `git diff` touches no `packages/spec` path and adds no `export` to `@objectstack/service-datasource`'s public surface. ## The defect, re-measured at `origin/main` (e99a14c) `renderObjectSource` (`packages/services/service-datasource/src/external-datasource-service.ts`) emitted the annotated-object-literal form — `import type { ServiceObject } …`, an unexported `const NAME : ServiceObject = { … }`, closed by `export default NAME`. Ruling `5644350230` (director seat, decision batch objectstack-ai#122 item 1, maintainer 「同意」 2026-09-12) makes `ObjectSchema.create({ … })` the one authorised shape for a `*.object.ts`: > 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 `ObjectSchema` when 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. Both premises on the card hold as written — I re-read them on `origin/main` rather than inheriting them: - the shape at `:931` is the annotated literal (confirmed); - the function's own docblock at `:857` reads 「Render a reviewable `*.object.ts` source string for an object draft」 (confirmed), and `os datasource introspect --out objects/wh_order.object.ts` is the command's published example, so the bytes are destined for a committed file the ruling governs. ## What changed One emitter, three lines of rendered output: - `import type { ServiceObject } from '@objectstack/spec/data';` becomes a **value** import of `ObjectSchema`. `import type` is elided at compile time, so a type-only import beside a factory call would leave a module that throws on its own first line — this is the half a shape-only fix silently gets wrong. - `const NAME : ServiceObject = {` becomes `export const NAME = ObjectSchema.create({`, closed with `});`. - `export default NAME;` is dropped. The export is named only, which is what the scaffolded barrel re-exports (`packages/create-objectstack/src/templates/blank/src/objects/index.ts` is `export { Note } from './note.object.js';`). A second export form in a generated file is the parallel shape the ruling closed. Both authored comment blocks the card names are preserved byte-for-byte: the remote-primary-key tombstone and the ADR-0028 unprefixed-name TODO. The renderer's docblock moved with the code — it no longer claims `tsc` over the string is the whole acceptance instrument, and it names the pin that now holds the other half. The `REMOTE_PRIMARY_KEY_COMMENT` docblock, which described the emitted file as "annotated", was corrected in the same edit. Real generated output, from `generateObjectDraft('warehouse', 'fact_orders')` with a namespace resolved and a remote primary key reported: ```ts // Generated by `os datasource introspect` (ADR-0015). Review before committing. import { ObjectSchema } from '@objectstack/spec/data'; export const wh_fact_orders = ObjectSchema.create({ name: 'wh_fact_orders', label: 'Fact Orders', datasource: 'warehouse', external: { remoteSchema: 'mart', remoteName: 'fact_orders' }, // Remote primary key: order_id // Preserved as a COMMENT because 'ServiceObject' has no authorable key for a // federated object's remote primary key (objectstack-ai#11000): 'fields.[f].primaryKey' is // not part of the field schema, so emitting it produced a draft that neither // 'tsc' nor 'ObjectSchema' accepted. Nothing below reads this line. // It names the column(s) THIS DRAFT WAS GIVEN as the key. For a COMPOSITE key // some drivers report only the first column (objectstack-ai#10997), so treat the list as a // lower bound and check it against the remote table before relying on it. fields: { order_id: { type: 'text' }, customer: { type: 'text' }, placed_at: { type: 'datetime' }, geom: { type: 'text' }, // REVIEW: unrecognised remote type — defaulted to 'text', verify }, // Org-wide default (OWD): who can see records they do NOT own. ADR-0090 D1 // requires this to be an authored decision rather than an accident — the // `security-owd-unset` author-time rule refuses an object without it, so a // draft that omitted it could not compile. 'private' is the rule's own // recommended default: owner + explicit shares. Widen it deliberately. sharingModel: 'private', }); ``` (The remote-key block renders `fields.[f].primaryKey` with angle brackets in the real output; they are spelled with square brackets here only because this body is stored through an API that rewrites short angle-bracket fragments.) ## The pin — form AND round-trip, because the form alone can be satisfied by a broken file `packages/services/service-datasource/src/__tests__/external-object-draft-authorised-shape.test.ts` (new, 12 cases). It is not a snapshot: 1. **form** — asserts the value import, `export const wh_customers = ObjectSchema.create({`, the call's closing `});`, and — separately — the **absence** of each piece of the refused form by name (`: ServiceObject = {`, the `import type` of `ServiceObject`, the unexported `const`, `export default`). A pin that only asserts the new spelling cannot say the old one left. 2. **round-trip** — strips the import line, rewrites the single named export to a `return`, and evaluates the emitted module body through the **real** `ObjectSchema` from `@objectstack/spec/data` with `new Function`. That is the same factory call the committed file makes on the author's machine; if `create()` would throw there, it throws here. The evaluated value is asserted equal to `ObjectSchema.parse(draft.definition)`, so the rendered file and the structured definition are proven to describe one object. 3. **negative control** — the same harness is fed a source with one unknown top-level key spliced in and is required to throw. A round-trip that cannot fail is not a measurement. The round-trip earned its keep on the first run: it failed because `external.writable: false` is applied by the schema's own default, which is standing evidence that the file really parsed rather than an object literal being handed back. That key is now asserted explicitly instead of the assertion being loosened. `external-object-draft-os-build.test.ts` had the only other shape assertion; it moved to the factory spelling. Its sibling `not.toContain('_customers:')` leg is replaced rather than deleted: the old spelling read the annotated literal's `const _customers:` and goes **vacuous** once the type annotation leaves the file, and a bare `not.toContain('_customers')` cannot replace it because the no-namespace TODO block legitimately renders that substring. It is now `toContain('export const customers = ObjectSchema.create({')` plus `not.toContain('export const _customers')`. ## Lane split — it stays in this lane The card and ruling item 4 warn that the door is a `packages/cli` command. Measured: `packages/cli/src/commands/datasource/introspect.ts` is a pure pass-through — it POSTs to the draft route and writes `draft.source` verbatim (`await writeFile(target, draft.source, 'utf8')`), with no knowledge of the shape. Its description, its two `--out objects/wh_order.object.ts` examples and its `*.object.ts` prose describe the destination, not the form, and all stay true. **No `packages/cli` change is owed**, so no sibling PR is needed for this card. ## Verification, all at `78a7783cd5` | what | command | result | |:--|:--|:--| | dependency closure | `pnpm --filter '@objectstack/service-datasource^...' build --concurrency=2` | `VERDICT command-exit 0` | | package tests | `pnpm --filter @objectstack/service-datasource test` | `Test Files 34 passed (34)` · `Tests 693 passed (693)` | | package typecheck | `pnpm --filter @objectstack/service-datasource typecheck` | `VERDICT command-exit 0` | | repo lint | `pnpm lint` (full repo, `eslint . --no-inline-config`) | exit 0 | | derived gate families | `node scripts/pm/dispatch-gates.mjs --commands` then `--ran` | 61 derived, 59 run green, 0 unrun | `tsc --listFiles` confirms the new test file is inside the package's own tsc program (1 hit), so the typecheck above really covers it. Two of the 61 families are **NOT MEASURED**, not green and not red: `check:dual-build-cjs-loads` and `check:type-check-debt` both exit **3** — `PREREQUISITE NOT MET` — because each reads built output for the whole workspace ("Run `pnpm build` first. ⛔ This is NOT a pass: nothing was measured."). That is a full-farm build this lane does not own; CI runs both against a built tree. ## Acceptance notes - `packages/cli/src/commands/init.ts` (two `TEMPLATES` sites) and `packages/cli/src/commands/generate.ts` still render `Data.ServiceObject = {`. Those are ruling item 1 / the `domain:cli` half, tracked as objectstack-ai#19722 — ⛔ deliberately not touched here, per ruling item 4's lane split. - `check:keyed-text-bounds` selects its population by filename suffix over the repo tree, and no `*.object.ts` file exists under `packages/cli` or `packages/services`. The strings changed here are template literals inside a `.ts` file, so the gate never reads them in any shape; this PR neither depends on nor unblocks objectstack-ai#19720. --- _Generated by [Claude Code](https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 28, 2026
…t with ObjectSchema.create (objectstack-ai#20195) Fixes objectstack-ai#19722 Clause-②: no Ruling `5644350230` (director seat, decision batch objectstack-ai#122 item 1, maintainer 「同意」 2026-09-12), item 1: 「`packages/cli/src/commands/init.ts` `TEMPLATES` emit the factory shape; `content/docs/deployment/cli.mdx:1323` describes it.」 Item 3: 「the changeset states how a user converts theirs (one mechanical rewrite: wrap the literal).」 This is the `domain:cli` half; the spec/scripts half landed as PR objectstack-ai#19720 (`42339e2f`). objectstack-ai#17418 remains open (it carries `Blocked-by` on this card and is the spec lane's to move). objectstack-ai#19098 remains open (the other `generate.ts` card, serial behind this one). ## What changed Both doors that write a `*.object.ts` now emit the one authorised shape, `ObjectSchema.create({ … })`: | door | before | after | |:--|:--|:--| | `os init -t app` / `-t plugin` (`TEMPLATES[…].srcFiles`) | `import * as Data …` + `const myAppItem: Data.ServiceObject = { … };` | `import { ObjectSchema } …` + `const myAppItem = ObjectSchema.create({ … });` | | `os generate object` (`GENERATORS.object`) | `const orderLine: Data.ServiceObject = { … };` | `const orderLine = ObjectSchema.create({ … });` | - The emitted shape is exactly what the changeset's user rewrite produces from the old one (wrap the literal, drop the annotation, import the factory), so a scaffold and a converted file look the same. - `ObjectSchema` is a **value** import: `import type` is erased at compile time and the module would throw on first evaluation. - The binding stays the file's **default export**. Both barrels (`os init`'s `src/objects/index.ts` and the line `os generate` appends for all seven generators) re-export `default`, so no barrel spelling moves and no user barrel needs touching. - The authored OWD comment block is unchanged byte for byte in all three emitters (only the closing `};` became `});`); `init-template-comments-self-contained.test.ts` is green. - `generate.ts`'s docblock states that the init/generate parity now covers the declaration shape as well as the `sharingModel` value, and names the pin that holds it. ## Premise check (on `origin/main`, sites located by symbol) - `TEMPLATES` (both object-bearing entries) and `GENERATORS.object.generate` emitted the annotated literal: confirmed. - `create-objectstack`'s bundled `blank/src/objects/note.object.ts` is already `export const Note = ObjectSchema.create({ … })`: confirmed. - `scripts/sync-scaffold-emission-policy.mjs` syncs the pnpm/TypeScript ranges only and reads no declaration shape; `pnpm check:scaffold-emission-policy` was run (read-only `--check`) and is green. - The ruling's `cli.mdx:1323` anchor has drifted with later edits. The page's only description of the scaffolded object shape was the `os generate` "What it does" line (it named `Data.ServiceObject`); that line now describes the factory (and names `defineSkill({ … })` for `skill`, the one non-object type that is not a typed literal), and the `os init` section gains a short paragraph naming the shape and the one mechanical rewrite for older projects. ## Measured: does the objectstack-ai#19720 gate reach a scaffold? Before and after Built `@objectstack/cli` at the base and at this branch, ran `os init my-app -t app` and `os init my-plugin -t plugin` (`--no-install`, under `packages/cli/node_modules` so `@objectstack/spec` is found by the upward walk), then `os g object my_app_order_line` in each, then the project's own gates. The repo gate was driven through its exported `sweep()` over a tree holding the four scaffolded object files (plus the driver file it reads its text family from). | reading | before (base `3bd28e2b`) | after (this branch) | |:--|:--|:--| | `os validate` / `os compile` / `tsc --noEmit`, init only | exit 0 / 0 / 0 (both templates) | exit 0 / 0 / 0 | | same, after `os g object` | exit 0 / 0 / 0 | exit 0 / 0 / 0 | | `check-keyed-text-bounds` `sweep()` over the 4 scaffolded files | 0 objects parsed, **4 shape violations** (`… is declared as a plain object literal — use ObjectSchema.create`) | **4 objects parsed, 0 shape violations**, 0 refusals | | compiled `dist/objectstack.json` | sha256 `3981f1ab…` (app), `e6d2c61d…` (plugin) | **byte-identical** (`cmp` equal) | So the platform's own shape gate refused every scaffold before this change, but only as a repo script: a user project carries no `scripts/`, and `os validate` / `os compile` never judged the shape. After it, the gate parses all four. The compiled artifact is byte-identical, which is the measured basis for `Clause-②: no` (no published payload changes). ## Pins - **New**: `packages/cli/test/scaffold-object-declaration-shape.test.ts` reads every emitter's bytes with the TypeScript parser (roster derived from `TEMPLATES` and `GENERATOR_SCAFFOLD_TARGETS`) and asserts: value import of `ObjectSchema` from `@objectstack/spec/data`; exactly one top-level declaration, initialised by `ObjectSchema.create({…})`, with no annotation; the default export is that binding; and **one signature across `os init` and `os generate object`**, which is the parity the docblock claims. Two controls prove the reader can refuse each half (the pre-ruling annotated literal; a type-only factory import). - **Repointed** (they asserted the refused spelling, per the `domain:services` pointer `5788276757`): `generate-emission-parses.test.ts` (`:148` and the `class` discriminator, which asserted `const class:`), `generate-refuses-unparseable-name.test.ts:255`, and the worked examples in `emitted-source-parses.ts`, `generate-emission-parses.test.ts` and the `generate.ts` refusal comment. Docblock-only: `scaffold-emission-typechecks.test.ts` (why the pin still stands after the annotation is gone) and `generate-refuses-name-outside-charset.test.ts` (`const class:` → `const class =`). - Unchanged and still covering it: `scaffold-emission-typechecks.test.ts` (tsc over every emitted scaffold), `generate-scaffold-validates.test.ts` and `init-scaffold-authoring-rules.test.ts` (runtime loads, which now execute the factory), `init.test.ts` (its assertions are name and barrel, not shape). ### Ablation (the new pin can fail) Committed first, then `node scripts/ablation-replace.mjs` swapped the `os generate object` emitter's `import { ObjectSchema }` for `import type { ObjectSchema }` and ran the pin: **2 failed / 5 passed**. The failures were `'os generate object order_line'` (`is not value-imported … (type-only)`) and `one signature across every door` (the generate door's signature diverged). Restore proven by the tool: blob `03b8006959dc` == HEAD and `git diff HEAD` empty. The direction observed was red, as expected. ## Verification (head `3082b024`, after merging `origin/main` `836aad2a`; round 1 at `468000c4` below) `main` moved under this branch with PR objectstack-ai#20164 (same package), so the suite was re-run after the merge: - `@objectstack/cli` unit tier, `vitest run --project unit --maxWorkers=2 --shard=N/4` × 4: **226 files / 3199 tests passed** (949 + 780 + 723 + 747). - `@objectstack/cli` integration tier, run locally because the diff touches two integration-tier files: `generate-refuses-unparseable-name` + `generate-refuses-name-outside-charset`, **2 files / 25 tests passed**. The rest of the integration tier is declared to CI. - `pnpm --filter @objectstack/cli typecheck` (tsc + `check:test-typecheck`): exit 0; the new test file is in the test program (`tsc -p tsconfig.test.json --listFilesOnly` counts it). - `pnpm lint` (full, `eslint . --no-inline-config`): exit 0. - Gates derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (94): all 94 exit 0; `--ran` verdict: `94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED`. - Before the merge (head `20526f3d`): unit tier 225 files / 3163 tests passed, the same two integration files 25/25, typecheck exit 0. - Round 1 (head `468000c4`: `origin/main` `d7c02413` merged as `e5499d52`, then the one-sentence `cli.mdx` correction naming `defineSkill` for `skill`): the 41 docs-scoped gates (`dispatch-gates --commands content/docs/deployment/cli.mdx`) all exit 0, `--ran` 41 of 41 accounted for, 0 NOT-MEASURED; `pnpm lint` exit 0; `node scripts/check-issue-citations.mjs` answered `no issue citations added against d7c0241 (3 file(s) read)`. The cli test tiers were not re-run locally on this head; CI runs them. ## Acceptance notes - `scripts/check-keyed-text-bounds.mjs`'s refusal text says 「the `os init` shape imports only `* as Data`」. After this change that describes the shape older `os init` releases emitted, not the current one; it is still the right advice for a converted file. `scripts/**` is read-only for this lane. Carrier: the spec lane when it next touches that gate (for example when objectstack-ai#17418 is unblocked). Noted, not filed. - Reported to the seat, not addressed here: in an `os init` project, `os g object order_line` writes `name: 'order_line'`, and the project's own `os validate` then refuses it (`Object 'order_line' is missing the package namespace prefix`). Measured at the base; this PR does not change it. - Local tooling observation: `pnpm check:type-check-debt` (`--re-measure`) runs a whole-workspace `turbo run build` before tsc. A local timeout that kills it mid-build leaves some packages' `dist/` without declarations, and `check:dual-build-cjs-loads` then flags them. Rebuilding the two packages cleared it; CI builds fresh. --- _Generated by [Claude Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #17418 — this is the spec/scripts half of the ruled change. It deliberately does not close the card; ruling item 1 (
packages/cli) is a sibling PR by thedomain:cliseat and is listed below with its real file surface.Clause-②: no
Ruling: comment 5644350230 (director seat, decision batch #122 item 1, maintainer 「同意」 2026-09-12).
The lane split, and what is NOT here
TEMPLATESinpackages/cli/src/commands/init.tsemit the factory shape;content/docs/deployment/cli.mdx:1323describes itdomain:clisibling; surface measured below*.object.tsnot using the factory is refused by name — "useObjectSchema.create"No file under
packages/cliis touched.content/docs/deployment/cli.mdxis not touched.packages/platform-objects/src/identity/sys-api-key.object.ts(held by #19618) is not touched — the census did not name it, so no serialization was needed.Landing order — measured, not assumed
The hazard to rule out: if the gate refuses the annotated-literal shape while
os initstill emits it, a freshly scaffolded project would be born refused.It does not arise. Landing item 2 alone turns nothing red that only item 1 can fix. Four readings, all on
origin/mainat4fba5036f2:walkObjectFilescollects files whose name ends.object.ts, repo-wide minusSKIP_DIRS. Measured: 112 files, zero of them underpackages/cli(git ls-files | grep '\.object\.ts$'also returns 112, so nothing untracked is hiding either).TEMPLATESare string literals insidepackages/cli/src/commands/init.ts— a.tsfile, not a*.object.tsfile. The walk matches by filename, so the template strings are never read by this gate, in any shape.os.tmpdir(), never into the repo tree (mkdtempSync(join(tmpdir(), …))throughoutpackages/cli/test/). No test run can transiently materialise a literal-shaped*.object.tsinside the walk, and the tree carries no ignored one either.os initemits into a user's project, which does not carry this repo'sscripts/.So the seat's reading holds, and it is now a measurement rather than a reading. Items 2 and 3 land independently; item 1 follows on its own card.
Item 2 — the rule, and the hole it actually closes
The gate found object declarations by
CREATE_CALL(ObjectSchema.create() and nothing else. One backstop existed: a file yielding zero declarations and zero refusals is refused. That backstop is conditioned onobjects.length === 0, so a file holding a factory declaration and a literal one read as complete, and the literal one was judged by nothing at all.Measured on a two-file control tree, with the pre-change gate:
mixed.object.tsproduced nothing — and the literal declaration inside it (ctrl_hidden) keys a UNIQUE index on an unboundedtextcolumn, which is the exact defect this gate exists to catch. That is the "never silently unprotected again" the ruling names.The authoring-shape scan now runs independently of the factory parse, over every
*.object.ts. Its refusal, verbatim from the real gate binary:Four signals, published in the file header because a source scan sees only the spellings it knows: a
ServiceObjectannotation (any indentation), asatisfies ServiceObject(any indentation), the file's default export (top level), and a literalname:beside afields:(top level only — held to column 0 so a helper literal built inside a function and handed to the factory is not accused).The zero-declarations backstop now steps aside when the shape scan already named the reason, so a literal-shaped file gets one finding prescribing the factory rather than a second one inviting the parser to be widened.
Item 3 — the census: instrument, control, reach
The card's "112 parsed, all factory" and the seat's "112 files on main" are not the same fact, and a literal-shaped file the gate cannot parse is invisible to exactly the instrument the card used. So the census was taken with a separate instrument.
Instrument — over-inclusive and shape-agnostic: for every
*.object.tsin the same walk, list every top-level binding of an object literal or of a call, plus everyexport default, then subtract the ones whose initializer isObjectSchema.create(. Whatever is left is a candidate for hand triage. It does not depend on knowing the spellingData.ServiceObject.Result:
The 117 matches the gate's own parsed count exactly, from a different reader. The 3 non-literal bindings were printed rather than counted, and are plainly not declarations: a regex literal, a template string and
'sys_http_delivery' as const.Control (lit): the same instrument over a two-file control tree returns 2 candidates — the pure literal file and the one hidden behind a factory declaration in a mixed file — so a zero from it is a reading, not an empty sweep.
Reach, stated: the population is
**/*.object.ts, which is the ruled population — theobjectrow ofDEFAULT_METADATA_TYPE_REGISTRY(packages/spec/src/kernel/metadata-plugin.zod.ts:725) declaresfilePatterns: ['**/*.object.ts', '**/*.object.yml', '**/*.object.json']. Blind to: declarations in files not carrying that suffix, the.yml/.jsonpatterns (not TypeScript, the factory does not apply), and declarations assembled at runtime rather than written as a literal. Cross-checks run against the whole tree and reported separately: zeroexport defaultand zerosatisfiesoccurrences in any of the 112 files outside comments and strings; zero indented object-literal bindings.So: zero to convert — and that is a different answer from "the gate saw none".
The rule can fail — ablation
The detector was neutered on the committed tree and the mixed control re-run.
scripts/ablation-replace.mjscarried the mutation, so the anchor hit and the blob move are its own verdict rather than a remembered claim:The ablation also found a defect in the first draft of this change: the ablated run still printed
Authoring shape: 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-testas the liveness proof (commit 2).A new
--self-testbattery,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 — andSELF_TEST_BATTERY_FLOORmoves 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 by over-pinning each entry to a sentinel and reading the floor's ownregistered N case(s)line — a RUNTIME count, because one battery registers through a loop and a literal source count is not a general method. The regression case isMIXED: a whole-file fixture alone would pass identically with the detector deleted, because the whole-file case was already refused by the old backstop.Changeset — measured, and it is owed by the other half
This PR publishes nothing.
scripts/check-keyed-text-bounds.mjssits inside no workspace package directory (checked against every tracked*/package.jsondirectory), and npm packs relative to the package directory, so it cannot be packed. The repo-root manifest isprivate: true. No non-private package lists ascriptsdirectory infiles[]. Positive control:packages/specis non-private and ships["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"]— a realfiles[]exists and does not reach repo-rootscripts/. The new symbolliteralShapeDeclarationsoccurs in exactly one file, that one.skip-changesetwas applied by the seat, with its own measurement recorded at comment 5775287713 (route 1: the one changed file is repo-rootscripts/, inside no package directory; root manifestprivate: true; 77 manifests censused forfiles[]escape hatches, zero found, with a lit control). The staleCheck Changesetred was re-run in that same act — ⛔ not a flake re-run: the gate's input changed after it ran.Worth the seat's attention: the ruling asks the changeset to state the one mechanical user rewrite (wrap the literal). The rewrite is something a user experiences, and the change a user experiences is item 1 —
os init/os gemitting the factory shape from a published package. So the ruling's changeset obligation attaches to the half that publishes, i.e. thedomain:clisibling PR, not to this one.What item 1 actually requires — real files
The seat files the
domain:clicard from this list. Measured, not guessed:Emitters (behaviour change):
packages/cli/src/commands/init.ts:649and:744— the twoTEMPLATESentries forsrc/objects/__name___item.object.ts, both emittingconst ${toCamelCase(namespace)}Item: Data.ServiceObject = {.packages/cli/src/commands/generate.ts:99—os generate objectemitsconst ${toCamelCase(name)}: Data.ServiceObject = {. The ruling's item 1 does not name this file, and it must. Its own docblock declares the coupling: "os init -t appscaffold does not compile on CLI 17.0.0 — the template's own object trips thesecurity-owd-unsetauthor-time rule #9666 took it once for theos inittemplates, and this emits the SAME value with the same explanation, so the two doors an author can arrive through agree. If that template's value ever moves, this one moves with it."Pins that move with them:
packages/cli/test/generate-refuses-unparseable-name.test.ts:255—expect(scaffold).toContain('const orderLine: Data.ServiceObject = {').packages/cli/test/generate-emission-parses.test.ts:148—expect(scaffold.source).toContain('const foo.bar: Data.ServiceObject = {'), plus its docblock at:14.packages/cli/test/scaffold-emission-typechecks.test.ts:26— docblock states "The repair isData.ServiceObject".packages/cli/test/init.test.ts:493— reads the scaffoldedmy_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 OWD 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 TypeScript's own parser whether the emitted bytes parse); only its worked example names the literal shape.Coupled, probably no edit:
scripts/sync-scaffold-emission-policy.mjskeepscreate-objectstack's bundled template'spnpm/typescriptranges in lockstep withpackages/cli/src/commands/init.ts(POLICY_SOURCE). It syncs version ranges, not declaration shape — but the sibling should re-run it, becausecreate-objectstack's bundlednote.object.tsis already the factory shape and the two scaffolders would finally agree.Docs:
content/docs/deployment/cli.mdx:1323.Acceptance notes
Two further emitters of the outlawed shape exist outside
packages/cli, which the ruling names nowhere and which the lane split therefore routes to neither seat. Both are reported rather than changed: neither is a*.object.tsfile, so item 3's population does not include them, and both sit in published packages, so converting either would change a published payload and re-open theClause-②: noreading this PR carries.packages/services/service-datasource/src/external-datasource-service.ts:931emitsconst ${definition.name}: ServiceObject = {as the object draft thatos datasource introspect --out objects/x.object.tswrites into a user's project (ADR-0015). A third scaffolder door, server-side. Pinned atpackages/services/service-datasource/src/__tests__/external-object-draft-os-build.test.ts:138.packages/metadata/src/serializers/typescript-serializer.ts:23emitsexport const metadata: ServiceObject = ${jsonStr};for thetypescriptmetadata format.Noted, not filed:
provenanceLine's record still reads-1/-1/-5/-2/-4againstMEASURED.reffa5d137ab0, which is information and not a verdict per that file's own header — no action, and no PR or person is due to touch it. Carrier: none.Verification
node scripts/check-keyed-text-bounds.mjs:: exit 0 — counts unchanged from base,112/117/250/592/147, identical before and after.--self-testand the gate are wired as one pair (package.json:183,.github/workflows/lint.yml:731-732, inside the requiredLint & Repo Gatesjob, all three verified at this head). The paired self-test is the liveness proof; the bare run is not.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.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwere run atbeef275c08and again in full at4d034cd2f9,--ranreconciling both as 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN, every row carrying its exit code, all 0.7adf1e0a6creports re-running all 31 there with the same reconciliation, and reports that its first sweep read 5 at exit 3 — PREREQUISITE NOT MET, a missingnode_modulesin a fresh worktree — which were NOT MEASURED and never green untilpnpm installand a re-run. That last reading is the delivering round's, ⛔ not re-derived by the seat; CI at this head is the seat's own reading, below.eslint --no-inline-config --format json scripts/check-keyed-text-bounds.mjs:: exit 0, 1 file linted (count read from the JSON output), 0 errors, 0 warnings. The narrowing excludes nothing: this repo runs oneeslint.config.mjs, which per its own comment at line 327 "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) 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. The repo-widepnpm lintwas NOT MEASURED in that round —scripts/pm/os-verify-lock.shreturnedqueue-timeout (exit 99)after 9m00s, never acquiring, holder pid 19251 — neither green nor red. ⭐ That NOT MEASURED is now superseded by the delivering round at7adf1e0a6c, which reportseslint . --no-inline-config --format json:: exit 0 over 7,005 files, 0 errors, 0 warnings.7adf1e0a6c, read by the SEAT by job conclusions, latest run per check NAME — ⛔ never an aggregate roll-up: 31 names, 23success, 8skipped, 0failure, 0in_progress. All five required checkssuccess: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 that touches no package.insideFunctioncase now pins the guard it is named for. Ablating the column-0 guard (m[1].length === 0, false→true, false): at4d034cd2f9the self-test was exit 0 / PASS / 0 failures; at7adf1e0a6cit is exit 1 / FAIL / 1 failure, the failing case being exactly that one. Both legs restored, proven by an emptygit status --porcelainand an emptygit diff HEADrather than by a step's exit code.No label, assignee, ready-flip, auto-merge or enqueue was performed, per the dispatch.
Generated by Claude Code