Repository navigation
fix(cli)!: os generate binds declared metadata (--object, --flow), never the item name, and every scaffold passes validate, build and lint - #21369
Conversation
…/action/object scaffolds pass the gates Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…ss the three doors Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…nces Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…d gate pin on the fresh starter Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…nerate-scaffolds-pass-gates
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ef83797d0dd30393701d31f289816c77b6cf7130 && git checkout ef83797d0dd30393701d31f289816c77b6cf7130
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9360df4138615a646630c0727c3b975711fb2eb1 c0190c97429a215c3badeca6a828b5943860056a && git checkout -B drift-repro 9360df4138615a646630c0727c3b975711fb2eb1 && git merge --no-ff c0190c97429a215c3badeca6a828b5943860056a
node scripts/docs-audit/affected-docs.mjs --json 9360df4138615a646630c0727c3b975711fb2eb1
|
…nerate-scaffolds-pass-gates
Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #21325 body and comments ① Derived judgments(a) Bindings never derived from the item's name — right, for every reference each scaffold writes. At the head each generator declares (b) Refusals — right: loud, remedy named, nothing written. All five routes land in (c) View container (d) Pin family close-out — right. (e) Published text — every factual sentence true at the head. ② Semver levelRight. ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…its that decided them (stage 5 of objectstack-ai#20595) (objectstack-ai#21375) Part of objectstack-ai#20595 Clause-②: no ## What changed Stage 5 of the `domain:engine` lane of the dead-citation sweep: `packages/drivers/driver-memory/**`, comment and docblock prose only, per the claim (`5947712781`). Stages 1 to 4 landed as `a7d9768ec`, `d150c3039`, `4bf4e7e70` and `13a24ece2`; objectstack-ai#20595 stays open for the next stage. Every comment or docblock site in the package that cited a tracker number answering 404 is rewritten in ruling C+D's form C (record `5749154545` on objectstack-ai#19123): the ADR when one records the decision, otherwise the commit in this repository's history that made it. That is **104 sites on 103 lines in 27 files, covering 13 numbers**: - **59 census sites** (59 lines, 5 files under `src/`): the whole `allocated-but-absent` population of the gate's own census in this package at the base; - **4 sites on 3 lines outside the census glob, inside the claimed surface**: `objectstack.config.ts:19` (objectstack-ai#11332) and `:26` (objectstack-ai#10724), and `tsconfig.typecheck.json:11` (objectstack-ai#11332 and objectstack-ai#10724 on one `//` line); - **41 test-comment sites** (41 lines, 20 test files), which the census defers. They carry 11 numbers: 6 the census itself reads as dead in this package's `src`, 2 it reads as dead elsewhere in the repository (objectstack-ai#6075, objectstack-ai#17590), and 3 it never judges because they stand only in test files (objectstack-ai#14434, objectstack-ai#16838, objectstack-ai#17348), which the board and a single read each settle. No comment-id citation in the package is dead (see Census). **Anchors: 13 numbers by commit, 0 by ADR, 0 by words alone.** 11 numbers reuse the anchor another lane or stage already measured for them; 2 were measured here (objectstack-ai#14435, objectstack-ai#16838). 13 distinct shas. No ADR or ruling record names any of the 13 for this subject: `git grep` over `docs/adr` and `scripts/adr-anchors` finds only ADR-0088's correction row for objectstack-ai#10724, which is about `contributes.functions`, not the `contributes.drivers` retirement cited here. Only comments changed. Every file keeps its line count (103 lines out, 103 in, plus the changeset), so no line citation into any of them moves. No code token moves (the guard below). **No citation number is added**: on every changed line, the numbers on the new text are a subset of those on the old, and the diff-scoped gate judged the 7 citations left on changed lines: all 7 resolve. **A `patch` changeset**: 20 of the 62 rewritten non-test lines are in the published `dist` (the `.d.ts` keeps JSDoc on exported members, and esbuild keeps some comments in the JS), and `dist` is not byte-identical with the base text (see Changeset). ## H0: the largest remaining `domain:engine` package The gate's own `node scripts/check-issue-citations.mjs --census --json` at base `5fd4855a9` (the before run below), `allocated-but-absent` per package over every `domain:engine` family in `.claude/skills/pm-dispatch/SKILL.md`'s lane table: | package | sites | |---|---| | `drivers/driver-memory` | **59** | | `metadata` | 41 | | `core` | 40 | | `platform-objects` | 27 | | `metadata-core` | 19 | | `drivers/driver-turso` | 14 | | `drivers/driver-mongodb` | 9 | | `formula` | 4 | | `metadata-fs` | 2 | | `drivers/driver-sqlite-wasm`, `plugins/plugin-pinyin-search`, `objectql`, `metadata-protocol`, `drivers/driver-sql` | 0 each | `driver-memory` is still the largest (lane total 215), so this stage went ahead. ## Census: `driver-memory`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count is its `allocated-but-absent` findings under `packages/drivers/driver-memory/`. | reading | tree | board | whole-repo `allocated-but-absent` | sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `5fd4855a9`, run 07:54:54Z to 07:58:32Z | enumerated, 192 pages, frontier objectstack-ai#21366, 19,187 records (newest number read before and after the run: objectstack-ai#21366) | 330 | **59** | 59 | 5 | 6 | | after | `17f667f86`, run 08:13:08Z to 08:17:18Z | enumerated, 192 pages, frontier objectstack-ai#21370, 19,191 records (newest before and after: objectstack-ai#21370) | 271 | **0** | 0 | 0 | 0 | The whole-repo drop is 59, and the two finding sets differ by exactly the 59 rows of this package, removed; none was added. `resolves` (34,880), `resolves-as-pull-request` (2,378) and `cross-repo-unjudged` (1,165) did not move. The card's 62 was taken at `f11b5f20a2` with the older extractor; the base here reads 59. The head's later commits are the changeset (outside the census surface) and one merge of `main` that touches no file under `packages/drivers/driver-memory`. **Supplementary instrument, the whole package.** The census reads neither test files nor strings nor files outside `src`. A second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) over every tracked file in the package (89: 82 `.ts`, `package.json`, `tsconfig.json`, `tsconfig.typecheck.json`, `README.md`, `CHANGELOG.md`, `LICENSE`, `objectstack.config.ts` among the `.ts`), and classifies each citation with the gate's `classifyCitation` against one board enumerated by the gate's `enumerateBoard` (192 pages, frontier objectstack-ai#21369, 19,190 records, 07:59:39Z to 08:03:13Z). Every one of the 13 numbers in the population was then read on its own over the issues endpoint (08:04Z): **all 13 answer 404**, and the lit controls `objectstack-ai#5286` and `objectstack-ai#12624` answer 200. | reading | citations | dead | src comment | test comment | other (`tsconfig.typecheck.json`) | test string | changelog | |---|---|---|---|---|---|---|---| | before, `5fd4855a9` | 1,720 | **143** | 61 | 41 | 2 | 21 | 18 | | after, `17f667f86` | 1,616 | **39** | 0 | 0 | 0 | 21 | 18 | `src comment` here is the 59 census sites plus the 2 in `objectstack.config.ts`. The drop of 104 citations is exactly the rewritten sites, and the live counts did not move (src comment: 494 resolve, 13 as pull requests, 1 cross-repo; test comment: 504, 20 and 4). A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it) counts 1,736 before and 1,632 after: a drop of 104. **Comment ids.** Three distinct comment-id citations stand on 6 sites in this package: `5234868334` (1 site), `5861435168` (2) and `5865693155` (3). Each was read over the issue-comments endpoint and answers 200 (control `5947712781`, 200). So no comment id is in the population. ## Per-number table `src` counts the census sites plus the four outside the census glob; `test` counts test-comment sites. Every sha below matches exactly one commit (`git rev-parse --disambiguate`, count 1) and is an ancestor of the base `5fd4855a9` (`git merge-base --is-ancestor`, exit 0 for all 13 shas; the clone is not shallow). The `+` lines introduce exactly these 13 nine-hex spans and no other. The message or the diff of each one names the number it replaces: 6 in the message and the diff (`56c093c4d`, `0f38ab084`, `9dac1ae01`, `be21955ba`, `e04a0aff2`, `d367f03d6`), 6 in the diff alone (`555a89cbd` 28 added lines, `20950404c` 7, `dce5cd4f0` 26, `51efbf116` 4, `20032594f` 8, `b90aff81f` 9), and 1 only in the subject's squash suffix (`93940d492`, where objectstack-ai#14434 was that pull request's own number). Each anchor touches `packages/drivers/driver-memory` itself, except `e04a0aff2`, whose two sites name the `driver-sql` fixture it created. `git blame` at the base puts 67 of the 104 sites on their anchor. The other 37 were written by a different commit that cites the number as another change: for objectstack-ai#13197, the declared-index commit `b7f645a8a` and the atomicity commits `34e80993f` and `4642f4c64`; for objectstack-ai#13195, the clobber generalisation `df1812050`, the reference-matcher retirement `8fec76a2b`, and two commits from while that card was open (`178325bcb`, `9268aec56`, see Wordings to check). In each case the anchor is the commit that made the change the sentence credits to the number. `source` says whether another lane or stage already used this anchor for this number (`reused`) or it was measured here (`measured`). | number | src | test | anchor | kind | source | what it decided | |---|---|---|---|---|---|---| | `objectstack-ai#6075` | 0 | 1 | `d367f03d6` | commit | reused (stage 4) | five drivers' query parameters follow `DriverQuery` | | `objectstack-ai#10724` | 2 | 0 | `be21955ba` | commit | reused (cli lane, the eleven-package sweep `aaad682dbc`, spec) | retire the nine dead `contributes` members, `drivers` among them, with driver-memory's manifest corrected in the same change | | `objectstack-ai#11065` | 2 | 3 | `20950404c` | commit | reused (stage 4) | count a boolean aggregand as 1/0 in avg and sum, on driver-memory's data face and analytics face | | `objectstack-ai#11332` | 2 | 0 | `dce5cd4f0` | commit | reused (cli lane, the eleven-package sweep, spec) | retire the plugin manifest's dead top-level containers (`capabilities`, `configuration`, `extensions`), driver-memory's manifest included | | `objectstack-ai#13195` | 8 | 11 | `9dac1ae01` | commit | reused (spec stage 4) | `$exists` means HAS A VALUE on the live mingo path, the analytics face and `translateFilter`, with the lowered `$exists` promoted to its own `$and` branch when its key is taken; its changeset records the maintainer's 2026-08-30 ruling | | `objectstack-ai#13197` | 16 | 11 | `56c093c4d` | commit | reused (stage 3, the types, rest and spec lanes) | driver-memory enforces field-level `unique`, refusing a colliding write with the ADR-0112 envelope; `updateMany` checks every row before mutating any | | `objectstack-ai#14434` | 0 | 1 | `93940d492` | commit | reused (stage 4) | un-mask driver-memory's published `update` / `upsert` types (that pull request's squash) | | `objectstack-ai#14435` | 3 | 3 | `20032594f` | commit | measured | publish the declared return types on `find()`, `findOne()` and `create()` | | `objectstack-ai#16589` | 23 | 3 | `555a89cbd` | commit | reused (the services lane's pointer `5904005321` on this card) | refuse a tenant-scoped call instead of returning cross-organization rows: seam 3, `assertCallNotTenantScoped` | | `objectstack-ai#16729` | 7 | 1 | `0f38ab084` | commit | reused (stage 4) | an explicit tenancy opt-out survives a partial `syncSchema` re-registration, on driver-memory and driver-sql | | `objectstack-ai#16838` | 0 | 4 | `b90aff81f` | commit | measured | read a stored ARRAY as its elements in the equality arm, so both filter faces answer one filter one way | | `objectstack-ai#17348` | 0 | 1 | `51efbf116` | commit | reused (spec stage 3) | type-gate a text operator over a declared temporal column; it pinned driver-memory's divergence by name and recorded the follow-up as filed | | `objectstack-ai#17590` | 0 | 2 | `e04a0aff2` | commit | reused (stage 4) | compile `$contains` on a JSON column as a per-dialect MEMBERSHIP test; it created the `driver-sql` fixture these two sites copy | ## Wordings to check Most rewrites swap a tag in place (`[#N]` to `[commit SHA]`, `(#N)` to `(commit SHA)`, `#N's X` to `commit SHA's X`, `since #N` to `since commit SHA`), the form the landed stages use. These say more than the tag: - **`objectstack-ai#17348`** (`memory-driver-filter-logic-conformance.test.ts:598`): 「the repair is in the driver, and it is objectstack-ai#17348's, filed」 became 「the repair is in the driver, named as owed in commit 51efbf1 and filed」, with `:599` (「against objectstack-ai#5499's investment freeze.」) untouched. `51efbf116` wrote this note, and its changeset records 「That divergence is filed as objectstack-ai#17348 and pinned by name」. - **`objectstack-ai#13195` where it was written while that card was open** (`memory-matcher-no-value-negated-operators.test.ts:32`): the sentence says `$exists` 「is the neighbouring cell」 and 「still read[s] it as key-presence」, and `:47` already calls that caution history. A bare swap would credit `9dac1ae01` with a still-open divergence, so it reads 「(the card commit 9dac1ae closed)」. The other site written while that card was open, `memory-exists-has-value-faces.test.ts:4`, is the file's title from the pin `9268aec56` took before the fix; the header under it says the file 「is no longer a pin of a divergence」 but was inverted in place, and `9dac1ae01` is the commit that inverted it, so a tag swap reads true there. - **A quotation of this note's own earlier text** (`memory-driver-document-not.test.ts:239`-`:241`, 「It used to say "… the direction is now objectstack-ai#13166's and objectstack-ai#13195's to settle."」): the dead number inside the quote became a bracketed editorial substitution, 「[commit 9dac1ae]'s」, so the quote stays recognisable as a quote and says where the edit is. It is the note quoting itself, not a ruling. - **Where the number named the defect, not the change**: 「(this is the objectstack-ai#16589 defect)」 became 「(this is the defect commit 555a89c fixed)」 (`memory-tenancy-guard.ts:271`); 「This is the objectstack-ai#16589 answer」 became 「This is the defect's answer (fixed in commit 555a89c)」 (`memory-tenant-scope-refusal.test.ts:139`); and in that file's reading table (`:29`) the cell 「the objectstack-ai#16589 defect」 became 「what commit 555a89c fixed」, which fits the column's 28-character width, so the table stays aligned and no row is added. - **Slash and list pairs**: 「objectstack-ai#13197/objectstack-ai#13239」 became 「commit 56c093c and objectstack-ai#13239」 (two sites); 「objectstack-ai#13197 / objectstack-ai#13239」 became 「commit 56c093c / objectstack-ai#13239」; 「[objectstack-ai#13197, objectstack-ai#13239]」 became 「[commit 56c093c, objectstack-ai#13239]」 (four sites); 「(objectstack-ai#11332 / objectstack-ai#10724 / objectstack-ai#4914)」 became 「(commit dce5cd4 / commit be21955 / objectstack-ai#4914)」, the exact form the eleven-package sweep (`aaad682dbc`) gave the same sentence in `plugin-hono-server`. - **`driver-sql`'s fixture** (`memory-20874-contains-membership.test.ts:63`, `:110`): 「`driver-sql`'s objectstack-ai#17590 fixture」 / 「assertions」 became 「`driver-sql`'s commit e04a0af fixture」 / 「assertions」; `e04a0aff2` created `sql-driver-17590-json-column-membership.test.ts`. - **Dated rulings keep their date**: the `objectstack-ai#13195` sites that say 「ruled … 2026-08-30」 keep it (`9dac1ae01`'s changeset and docblocks carry 「the maintainer ruled on 2026-08-30」). - **Capitalisation**: where the number opened a sentence, the new text opens with 「Commit」 (five sites). - No line was reflowed, so some are longer than their block's wrap (`eslint.config.mjs` declares no line-length rule, and a reflow would move neighbouring lines and every line citation into the file). ## Sites left - **In comments (src, test, the two config files): none.** - **String literals: 21 test-string sites, 7 numbers, 11 files** (`describe` and `it` titles, assertion arguments): `objectstack-ai#13195` 7, `objectstack-ai#13197` 7, `objectstack-ai#11065` 2, `objectstack-ai#16838` 2, `objectstack-ai#14435` 1, `objectstack-ai#16589` 1, `objectstack-ai#16729` 1. Every one of the 7 is in this stage's population, so the table above holds an anchor for each. Non-test strings carry none. Strings are outside this stage's surface. - **Outside `src`:** the release-owned `CHANGELOG.md` names dead numbers on 18 sites (11 numbers); left. `package.json`, `tsconfig.json`, `README.md` and `LICENSE` cite no dead number. ## Mechanical guard: no code token moves The guard (stages 2 to 4's) compares base `5fd4855a9` against the working tree over all 27 touched files, with TypeScript 6.0.3 (`tsconfig.typecheck.json` through `ts.parseJsonText`, the same two walks): - **Reading 1**: the parser's leaf nodes, from a `forEachChild` walk. Comments are trivia there, and JSDoc is never visited. A leaf that is not itself a token is re-scanned with trivia skipped. - **Reading 2**: the full token stream in parser context, from a `getChildren` walk, JSDoc nodes skipped. String, template and numeric literals are compared in full on both readings. Results: - Real run at the head: 61,808 base tokens, **0 files with a token change** (exit 0). - Comment control (「The declaration dies with the table.」 to 「DIES」, `memory-driver.ts`): 0 files changed (exit 0). - Positive control, an identifier (`tenantFieldOf` to `tenantFieldOfX`, `memory-unique-constraint.ts`): DIFFER on both readings (exit 1). - Positive control, a string literal (`'MEMORY_MULTI_TENANT_UNSUPPORTED'` to `…X`, `memory-tenancy-guard.ts`): DIFFER on both readings (exit 1). - Positive control, a numeric literal (`UNIQUE_VIOLATION_STATUS = 409` to `410`, `memory-unique-constraint.ts`): DIFFER on both readings (exit 1). - Positive control, a JSON value (`"rootDir": "."` to `"./"`, `tsconfig.typecheck.json`): DIFFER on both readings (exit 1). Each mutation went through `scripts/ablation-replace.mjs` (wrap mode, anchor hit 1 to 0) under a shell trap that restores by absolute path from `HEAD`. Each restore was proven equal to its `HEAD` blob (`ac823bf8fa9b`, `eb0eb11358ea`, `7b008c1f12b7`, `eb0eb11358ea`, `88035b359542`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset: `patch` (`dist` measured) `files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is not private. The dependency closure and the package were built first (`turbo run build --filter='@objectstack/driver-memory^...' --filter=@objectstack/driver-memory`, 5 of 5 tasks, VERDICT command-exit 0). Then the package's own `build` (tsup plus `check-dts-emitted`) ran three times in one script under the shared verify lock (VERDICT command-exit 0), at `17f667f86`: - **Leg 1**, the head text: 6 `dist` files hashed. Of the 62 rewritten non-test lines, 20 appear verbatim in `dist`: 11 from `memory-driver.ts`, 4 each from `memory-tenancy-guard.ts` and `memory-unique-constraint.ts`, 1 from `memory-analytics.ts`; in `index.d.ts` / `index.d.mts` (16) and `index.js` / `index.mjs` (10). - **Leg 2**, the base text put back in the 7 non-test touched files (7 of 7 proven equal to their base blob): 4 of the 6 files differ from leg 1 (`index.d.ts`, `index.d.mts`, `index.js`, `index.mjs`), and `scripts/ablation-dist-preflight.mjs` finds the base marker 「[objectstack-ai#16729] One driver's record」 in 2 built files (exit 0). - **Leg 3**, after the proven restore (7 of 7 equal to their `HEAD` blob, `git diff HEAD` empty, porcelain empty): all 6 files are byte-identical to leg 1, and the preflight's `--absent` reading exits 0, so the build is deterministic and the difference is the rewrite. So the rewrite ships, and `.changeset/20595-driver-memory-provenance-anchors.md` declares a `patch` for `@objectstack/driver-memory`, comment text only, with the claim's `Clause-②: no` line. The later changeset commit and the merge of `main` touch no file under `packages/drivers/driver-memory`. ## Gates (head `381dddc9c`) - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `381dddc9c` (28 paths against merge base `fbe2debb7`, 220 changed lines) derived 62 commands. All 62 ran, each exit code captured before any pipe: 62 exit 0. `--ran` reports 「62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero) and exits 0. - **Roster families the derivation flags under a touched directory**, also run, each exit 0: `node scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm check:error-code-casing`, `pnpm check:filter-alias-parity`, `pnpm check:object-def-param-keys`, `pnpm check:tenant-chokepoint`. - **Named readings:** `node scripts/check-issue-citations.mjs` exits 0 (「every citation this change adds resolves」, 7 judged across 5 files); `pnpm check:issue-citations` exits 0 (self-test, 173 cases, 9 batteries); `pnpm check:doc-authoring` exits 0 (the sibling-package prose-id baseline holds, no growth); `pnpm check:nul-bytes` exits 0 (9,669 files, no raw control bytes), and a control-byte grep over the 28 changed files finds none (exit 1). - **Build before the gates:** `turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2` at `381dddc9c`, 71 of 71 tasks, VERDICT command-exit 0. - **Tests and typecheck, under the verify lock, at `381dddc9c`:** `pnpm --filter @objectstack/driver-memory test`: 70 test files pass (70), 1,718 tests pass (1,718), exit 0; `pnpm --filter @objectstack/driver-memory typecheck` (both programs, `tsconfig.json` and `tsconfig.typecheck.json`) exits 0. `tsc --listFiles` puts all 70 tracked test files and the 25 changed `src` files in the `tsconfig.json` program, and `objectstack.config.ts` in the `tsconfig.typecheck.json` one. - **Lint, as a proven narrowing:** eslint with inline config disabled, over the 26 touched `.ts` files plus `dist/index.js` as the control: 27 results, 0 errors and 1 warning, the control's ignore notice; none of the 26 is reported ignored. `eslint.config.mjs` never enables type-aware linting (its lines 327-328 say so), so a comment edit cannot move the verdict on an untouched file. `tsconfig.typecheck.json` is not an eslint target (run alone, eslint answers 「File ignored because no matching configuration was supplied」); the token guard covers it. The repo-wide `pnpm lint` is CI's run. ## Acceptance notes - **Base.** The branch was cut at `5fd4855a9` and merged with `main` once, at `fbe2debb7`, before the gates (merge `381dddc9c`, no conflict, no file under `packages/drivers/driver-memory`, and neither `check-issue-citations.mjs` nor `dispatch-gates.mjs`). `45ce12a48` (the last `driver-memory` change before this stage) is in the base (`merge-base --is-ancestor` exit 0; control `13a24ece2`, exit 0). The net diff against `main` is the 27 rewritten files (+103/−103) and the changeset. - **One sentence keeps a deictic whose antecedent was the number.** `memory-exists-has-value-faces.test.ts:264` says 「not in the card」 two lines under the tag that named that card, now `[commit 9dac1ae]`. It has no number of its own, so it was not touched. - **Wording only:** no line without a number was changed. --- _Generated by [Claude Code](https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21325
Clause-②: yes (narrowing)
The
Clause-②arm was measured on the real diff, and it is(narrowing), not the(widening)the claim expected.--objectand--flowwiden the command. The diff also narrows it: an invocation that used to write a scaffold bound by name is now refused.os g flow customerin a project that declares several objects is one example, thecli.mdxexampleos g action customeranother. The changeset carries the BREAKING banner and its ADR-0087 disposition (not-required (no-migration-prescription)).check-adr-0087-registrationandcheck-changeset-no-majorboth pass on it.What changed
Triage's direction (
5945870485):flow,actionandapptake their target object from an argument or from the stack's existing objects, never from the new item's name. With nothing named and nothing to choose, the command refuses.viewwrites the shapeos lintrequires. Every generator kind, on a fresh scaffold, passesos validate,os buildandos lintwith zero findings.packages/cli/src/commands/generate.tsbinds(ScaffoldBinds).viewis named after its object.flow,actionandapptake the object from--object, or the stack's only object.actiontakes its flow from--flow, or the stack's only flow.resolveScaffoldBindingsresolves every reference against the loaded stack before anything is rendered. It runs below the namespace gates and above the render, the parse check and--dry-run. It is a pure function: it returns a verdict, and the command prints the verdict and exits.generatereceives the resolvedScaffoldBindings, and a binding scaffold rendered without them throws. Nothing falls back to the item's name.--object; an--objector--flowthat names nothing declared; no flow or several and no--flow; a view whose name is not a declared object; a binding scaffold outside a project (no config, so no stack to check against);--objector--flowon a type that takes neither (unusedBindingFlagRefusal), including thetypes,clientandmigrationroutes, which now sit in oneSUB_COMMANDStable.object: nodescriptionfield (field-no-consumerson every generated object once the stack had any consumer root).view: no containernameorlabel(twoliveness-dead-propertywarnings). It writeslist.label(os lintrequired/labelerror), and its columns are every field the bound object declares (a fixednamecolumn was alist-view-field-unknownerror on any object without one). It is sorted by the title field.flow:status: 'active'(flow-draft-status-ambiguous; draft already fires, so the runtime behaviour is unchanged).action:locations: ['record_header'](action-no-placement).app: the nav entry is labelled with the bound object's plural label.--objectand--flowflags. Their help text names the types that read them, read off the roster.packages/cli/src/utils/project-namespace.ts: aloadedresult also carries the evaluatedconfig, so the bindings are read off the same load that supplies the namespace.packages/cli/src/utils/scaffold-wiring.ts:registeredItemNamereturns the key an item is registered under. For an aggregated views container that key isderiveViewContainerObject, the boot registrar's own derivation.stackCarries(the reach report) reads it, so a container with nonameis still found.Argument shape, and why
--object OBJECTand--flow FLOWare long flags with a value, the way every other option onos generateis spelled (--dir,--output,--format). The object is accepted as declared (tasks_app_task), or without the namespace prefix (task). In the second case it is looked up through the same prefix ruleos g objectapplies, never re-spelled.Object: tasks_app_note (the only object this stack declares; --object binds another). This is the non-interactive reading of the direction's "or from the stack's existing objects". With several declared, the command lists them and asks for the flag instead of choosing. Noos generatecode path prompts, and this change adds no prompt (os migrateprompts only for a yes/no confirmation).os g view task), unchanged. A views container is registered under the object it binds, so the name and the object are one fact.--objectonviewis refused with that explanation.--flowgoes beyond the single object argument the claim anticipated. An action's flow was derived from its name in exactly the same way (complete_task_flow).defineStackrefused it whenever flows existed. With no flows declared, the action loaded with a dangling flow reference: measured on97239c3c8a,os g action approvethen exits 0 andos validateexits 0,target: 'approve_flow'. The same rule (argument, or the stack's only one) is applied to the second reference.The view container's
nameandlabel: decided from the code that reads themThe scaffold's comment said the server refuses a container whose
namedisagrees.os validatesaid both keys are dead. Read from their readers, both statements are true and they do not conflict:name: the boot registrar files a container underderiveViewContainerObject(packages/metadata/src/view-container.ts): its ownobject, elselist.data.object/form.data.object, and only thenname. Withobjectset,nameis never the key. Its one reader isviewContainerNameRefusal(packages/objectql/src/view-container-name-refusal.ts). That function refuses anamethat is set AND differs from the key, and passes a container that has noname. Sonamecan only restate the key or contradict it, which is the liveness ledger'sdead(packages/spec/liveness/view.json). The refusal's own remedy is "dropname".label:expandViewContainerWithDiagnostics(packages/spec/src/ui/view.zod.ts) gives every expanded ViewItem the label of its list or form entry, never the container's. The ledger records no Studio reader either.So both keys are dropped, and the label a person sees is
list.label, whichos lintrequires.Measured: the card's scenario
npm create objectstack(the in-repo on-rampbin/,--skip-install, namespacetasks_app), thenos g object projectandos g object task, run with the in-repo CLI build.97239c3c8a)de6107c8e1)os g flow task_donetasks_app_task_done--objectos g flow task_done --object tasktasks_app_taskos g view taskos g action complete_taskdefineStack: objecttasks_app_complete_task, flowcomplete_task_flow)--objectos g action complete_task --object tasktask_done_flow(the only flow)os g app tasksdefineStack: objecttasks_app_tasks)--objectos g app tasks --object taskos validateos buildos lintrequired/labelat views[0].list.label, plus 7 warningsThe remaining warning in the "after" column is
field-no-consumersatobjects[0].fields.body. It sits on the STARTER's ownnoteobject and is not written byos g; see Acceptance notes.Measured: every generator kind, three gates
A fresh starter plus one
os ginvocation per leg. "Before" binds by name (os g KIND probe_thing, and the view on the starter'snote). "After" runs the pin's own chain: the prerequisitesbindsdeclares, then the kind. Findings are counted from the--jsonoutput of each gate, at every severity.list-view-field-unknown,sort-field-unknownand (lint)required/labelerrors;liveness-dead-propertyonnameandlabelos g view notealso leaves zero, the ledger finding included)os gexit 1 (object derived from its name)flow-trigger-unknown-object,flow-draft-status-ambiguous, starternote.body)note.body)os gexit 1 (object derived from its name)note.body)The pin, family close-out
packages/cli/test/generate-scaffold-validates.test.ts(extended). For every kind inGENERATOR_SCAFFOLD_TARGETS, each prerequisite named bybinds(an object, a flow) is generated first. The bindings are resolved throughstackBindingCandidates, the reader the command uses. The target is deliberately not named like the item. The test then runs what the three commands run:defineStack;normalizeStackInputwith the unknown-key lints, the parse andrunAuthoringRules('validate');runAuthoringRules('build'); andlintConfig. It asserts zero findings at every severity. Before this change it judged only the error half ofos validate, beside an object named like the item, which is why none of the card's cases showed up there. A kind added later is measured with no edit to the file. Also added: binding scaffolds write exactly the binding they are handed, and throw without one.packages/cli/test/generate-scaffold-gates.e2e.test.ts(new). One fresh starter per kind (the on-rampbin/), the prerequisite chain derived frombinds, thenos validate --json,os build --jsonandos lint --json. Each gate must exit 0 with no finding beyond a one-entry starter ledger. A CONTROL leg (the bare starter) must report zero findings at all three gates. The ledger is shrink-only. Its entry must sit on an object the bare starter declares, read from the compiled artifact, and must still fire in at least one leg. It is in e2e because the run is about forty oclif + tsx cold starts, roughly 4 minutes on this box. That is the same reasongenerate-scaffolds-reach-stack.e2e.test.tsis nightly.packages/cli/test/generate-binds-from-stack.test.ts(new). It covers every branch ofresolveScaffoldBindingsandunusedBindingFlagRefusal, asserting on the named subjects, not on the prose.Existing tests the behaviour change required updating. Each needed a binding handed over or a flag added; none had an assertion weakened:
generate-object-namespace-prefix.test.ts: the set is composed object first. A binding scaffold must write the object it is handed, whatever the namespace.generate-scaffold-wiring.test.ts: the registered key, and the container writes noname/label.generate-emission-parses.test.ts,scaffold-emission-typechecks.test.tsandgenerate-refuses-name-outside-charset.test.ts: the probe bindings from the newtest/helpers/scaffold-bindings.ts.generate-refuses-unimportable-alias.test.ts:dashboardandskillcarry the alias-layer cases thatviewandflowcarried. In config-less directories a binding scaffold is now refused first.dashboardsuffixes its binding the same way, so the layer it pins is unchanged.generate-stack-reach.test.ts: an action whose--objectnames nothing; a view outside a project; the action control generating once an object and a flow exist; the no-config "Not wired" case carried bydashboard.create-objectstack-stack-reach.test.tsandgenerate-scaffolds-reach-stack.e2e.test.ts:--object. The order is derived frombinds.Ablations
The fix was committed first. Each leg went through
scripts/ablation-replace.mjs(wrap mode: the anchor must hit, the counts are checked on disk, and the file is restored to blob == HEAD with an emptygit diff HEAD), inside a script with an EXIT/INT/TERM trap. Every leg below ended with the file restored (blob2c8b969c1f07for generate.ts,0d902b75dff7for scaffold-wiring.ts).status: 'draft'nameandlabelagainlist.labelremovedlocationsremoveddescriptionagain--objectlookup--object/--flowon a non-taking type ignorednameonlystatus: 'draft', through distTwo first attempts were void and were redone with new anchors: the tool refused them because the counts did not move. In A3 the replacement re-contained the anchor. In A10 the replacement
;was already everywhere. E1's first run was also void, and in a way worth knowing: the mutation landed insrc/and the e2e stayed green, 38 of 38. The spawnedbin/run-dev.jsservespackages/cli/distwhen dist is built. Measured: withsrcmutated tostatus: 'draft',tsx bin/run-dev.js g flow … --dry-runprintedstatus: 'active'. E1 was then rerun as a dist ablation: mutate, rebuild the CLI, check withablation-dist-preflight(marker present indist/commands/generate.js), run the e2e (6 red), restore, rebuild, check with--absent(clean). See Acceptance notes.Verification (on
de6107c8e1, which mergesorigin/mainatf9bcd08bef)turbo run build --filter=@objectstack/cli..., 59 of 59 tasks.pnpm --filter @objectstack/cli exec vitest run --project unit: 245 files, 3485 tests passed.--project integrationontest/generate-andtest/create-objectstack: 7 files, 67 tests passed, run against a dist built from this head.OS_TEST_TIERS=nightly):generate-scaffold-gates,generate-scaffolds-reach-stack,generate-object-namespace-prefixandgenerate-skill. 4 files, 69 tests passed.pnpm --filter @objectstack/cli typecheck: exit 0. That includescheck:test-typecheck; the debt ledger did not change.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 95 commands, every one exit 0. Three first exited 3 (PREREQUISITE NOT MET:check:skill-examples,check:dual-build-cjs-loads,check:i18n-coverage) and were rerun after building their prerequisites.--ranreconciliation: 95 derived, 95 run, 0 NOT MEASURED, a derived zero.pnpm lint(repo-wide eslint, not narrowed): exit 0.Docs and changeset
content/docs/deployment/cli.mdx, the#### os generateentry under### Scaffolding: where bindings come from, the refusals, the new options, the examples, and the view no longer writingname..changeset/21325-generate-binds-from-stack.md:@objectstack/climinor, BREAKING banner,Clause-②: yes (narrowing), ADR-0087not-required (no-migration-prescription).Acceptance notes
cli.mdxfalse, so they were edited too. The Quick Start "Add more metadata" block,os generate flow customer/os generate action customer, is refused on the starter, which declares two objects oncecustomerexists. The "Typical Workflow" stepos g flow opportunitycomes after four objects. Each now passes--object. Neither block is in### Quality, the region of sibling card objectstack verify reports "✓ verify passed" (exit 0) on a stack that os validate, os build and os lint refuse — and the published CLI docs do not document verify #21323.packages/cli/README.mdis fixed in this PR. The claim fenced it to cli README: documents-h/-vshort flags that exit 2, and says there is noos plugincommand group whileos plugin build|sign|publishis registered #21310, which has since closed (landed), so the seat lifted the fence under the same claim, 5946394138. Its Quick Start lineos generate flow taskwould be refused after this change, because theapptemplate's object plustaskmakes two objects. It now readsos generate flow task_changed --object task, measured on a realos init my-appproject (exit 0, thenos validate/os build/os lintexit 0). The Typical Workflow lineos generate view customerstill holds and is unchanged.npm create objectstackstarter'snoteobject declaresbody, which nothing reads.field-no-consumersstays silent while a stack holds no consumer root. The first view, flow, action, app, dashboard or skill, generated or hand-written, wakes it on the starter's object.os init -t app'sitemobject has the same shape (description,status).os gcannot give someone else's field a consumer, so the literal "zero findings on a fresh starter" holds only for kinds that add no consumer root. Each kind's own output is measured at zero, and the pin records the remainder in a shrink-only ledger that turns red when the starter is fixed. The template fix is outside this card's surface. It is reported to the seat for a separate card.bin/run-dev.jsservesdist/when it exists. It is documented in several CLI test headers as the source entry, and the suite says it does not depend on dist. Measured above (E1), it serves the built tree. So a spawned CLI pin reads build state. A test written against an unbuilt change reads stale behaviour, and an ablation ofsrc/alone stays green. This PR's spawned tests were rerun against a dist built from this head. No carrier is named for the test infrastructure.project-namespace.tsandscaffold-wiring.ts. Also new: one test helper,test/helpers/scaffold-bindings.ts, and two test files.Generated by Claude Code