Skip to content

fix(cli): generated scaffolds reach the stack, or os g says they do not (#20215) - #20329

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-20215-generate-scaffolds-reach-stack
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-20215-generate-scaffolds-reach-stack

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20215
Clause-②: no

os init (the app and plugin templates) now wires every barrel os generate writes into, and declares the capabilities the flow scaffold runs on. After writing, os g loads the config again and says whether the new item reached the stack. When the write makes a config that used to load stop loading, os g refuses and takes the write back out. os g never edits a config.

Premise, re-measured on origin/main 6a6a17b6 before any edit

I built the CLI's dependency closure at 6a6a17b6, ran os init my-app -t app --no-install, generated each of the seven types as order_line, and then ran os validate:

step exit what it printed
each os g 0 Tip: Run objectstack validate to check your config
os validate 0 Data: 2 Objects 5 Fields · UI: 0 Apps · Logic: 0 Flows

Row 2 reproduces. With all six barrels wired by hand, os validate exits 1 with "flow 'order_line_flow' declares a 'record_change' trigger but requires does not include 'triggers'". Adding requires: ['triggers'] gives exit 0, UI: 1 Apps 1 Views 1 Dashboards 1 Actions, Logic: 1 Flows.

Booting that hand-wired project with os serve --dev measured two more facts:

  1. The view scaffold is refused at boot. The server said "Invalid views: container from manifest 'com.example.my-app': the container's own name is 'order_line', which disagrees with the object key it binds to, 'my_app_order_line' … drop name, or set it to 'my_app_order_line'". os validate had passed it. So wiring src/views alone would have turned "the view is silently absent" into "the server does not boot" on the road's next step.
  2. triggers is not enough for a flow to run. With requires: ['triggers'] the server booted and printed "Flows: 1 flow(s) declared but the automation engine is not enabled — they will never run. Add requires: ['automation', 'triggers']". Each trigger plugin logged "automation service not available — … NOT installed". With both tokens it printed Flows: 1 flow(s) 1 bound to triggers (record_change, schedule, time_relative, api) · 1 draft.

Row 1: the route, measured

The route is: os init imports every generator's barrel, and os g reports whether its file reached the stack without ever editing the config.

The floor is exact for every config shape. After writing, os g loads the config through the same loadConfig that os validate uses, folds it the way the counter does (authoringRuleUnionStack), and looks for the item's metadata name under the stack key (singularToPlural(type)). It never parses the config's text, so a reordered config, variables, .mjs and packages[] are all read the same way.

os g object, os g view and os g flow (order_line) were run in each shape. The config hash is sha1, taken before and after the three runs:

config shape config hash before → after what os g view / os g flow said os validate
(a) fresh os init -t app fbea6b0e → fbea6b0e reaches the stack, both exit 0, 1 Views, 1 Flows
(b1) hand-edited: imports and keys reordered, an extra import unchanged reaches, both exit 0, counted
(b2) hand-edited: defineStack fed from variables (const ui = {…}; const stack = {…, ...ui}) unchanged reaches, both exit 0, counted
(b3) the pre-fix os init config (./src/objects only) unchanged not wired: prints the import and the key (and requires for the flow) exit 0, 0 Apps, 0 Flows
(b4) the same config as objectstack.config.mjs unchanged reaches, both exit 0, counted
(b5) the create-objectstack blank shape (./src/objects/index.js, requires: ['automation']) unchanged not wired; the flow advice prints the whole list requires: ['automation', 'triggers'], exit 0, 0 Flows
(c) no config n/a not wired: no config here, prints the lines n/a
(d) fresh os init -t plugin unchanged reaches, both exit 0, counted

These rows were measured on cae468f49. The wiring-advice text in (b5) is from c21f96460.

Why not "os g edits the config". That route would be a config editor, a capability the CLI has nowhere today: os init only ever writes a fresh config, and no command rewrites one. Its safety would rest on a recognizer for the author's file. For example, shape (b2) has no defineStack object literal to insert a key into, so an editor must detect it and fall back to the message. The route above changes no config byte in any shape, and needs no editor. That is why this is not a needs_decision. The editor route was not built, so its column is analysis, NOT MEASURED.

Empty barrels. os init writes an index.ts containing only export {}; for each directory the template puts nothing in, and never overwrites an existing one (keyed by renderer, so the objects barrel keeps its old write). The empty barrels must not break the build or typecheck:

  • os validate exits 0 on a fresh project: UI: 0 Apps, Logic: 0 Flows.
  • os compile exits 0.
  • The emitted project's own tsc --noEmit exits 0, measured by the existing scaffold-emission-typechecks.test.ts, which went red on the first version of this change. It led to one design change, described in the next paragraph.

exportsOf, not Object.values. Object.values(emptyBarrel) does not type-check against defineStack for the keys that also accept a name-keyed map. With no export to infer from, TypeScript takes the element type from the map branch, whose name is optional. Measured: TS2322 on actions, flows, dashboards and apps of a fresh project, while views and skills, which have no map form, passed. Three alternatives were measured and all still failed: a spread, Array.from, and .flat(). The template therefore declares one local helper, exportsOf, whose element type comes from the barrel alone: an empty list while the barrel exports nothing, and the exported type once it does. Both states type-check with 0 errors, and a populated barrel is checked exactly as strictly as before.

Prefixed names survive. Object names still go through objectNameFor. The reach check looks for exactly the name the scaffold writes (itemName, held equal to the emitted name by a pin, with and without a namespace).

Row 2: the template declares what the flow needs

Every template that wires src/flows declares requires: ['automation', 'triggers']. The list is derived as the union of the generators' own requires, which today is the flow scaffold's pair. The flow scaffold's header also states the pair.

I chose this over "os g flow adds triggers to requires" because adding to requires is the same config editor. It includes automation as well because of the boot measurement above: without it, the flow validates and never runs.

The one cost is that a fresh project that never holds a flow still mounts the automation engine and the trigger plugins. The config comment says both tokens can go if the project will never hold a flow.

Where the stack carries a flow but lacks a token, os g flow warns and prints the whole requires list to use.

What os g says now

verdict when exit what is left on disk
reaches the stack the loaded stack carries the item 0 the scaffold and the barrel line
cannot run reached, and the stack's requires lacks a token the scaffold runs on 0 as above; the whole requires list is printed
not wired the config loads and does not carry it, or there is no config 0 as above, the config untouched; the import and the key are printed
refused the config loaded before the write and does not load after it 1 nothing: the scaffold, the barrel line and any directory this run created are removed, and the tree is byte-identical
cannot tell the config did not load before the write either 0 as before this change; the verdict says it cannot tell

"Refused" is what the wired barrels make reachable. Measured on c21f96460 in a fresh project:

  • os g action approve without an approve object: exit 1 with defineStack's own "Action 'approve' references object 'my_app_approve' which is not defined in objects", tree unchanged.
  • os g app crm: exit 1, tree unchanged.
  • os g flow into a wired config without requires: exit 1, tree unchanged.

The "cannot tell" row keeps the #20197 control: in a config that does not load, os g dashboard sales still generates, exit 0.

Two fixes in generate.ts, same class, in place

Both are the card's defect class, a scaffold that never reaches the stack or is refused once it does. Both are mechanical, both sit in this claim's file, and both are covered by this card's gates.

  • The view container's name is its object key, prefix included. The server registers a views container under that key and refuses one whose name disagrees. The #20197 census pinned the view's own name as unprefixed because no os validate gate judged it; that assertion is updated, and the reason is written into the pin.
  • Barrel membership is asked of the compiler (barrelExportsBinding), not by indexContent.includes(binding). Measured: after os g view order_line, os g view order found order inside orderLine and exported nothing. The new export {}; barrels would have made that bite os g dashboard port.

Docs

content/docs/deployment/cli.mdx, os generate section:

  • The four verdicts, and that os g never edits the config.
  • A Collected as column in the types table.
  • A view's name is its object key.
  • The example block now binds every scaffold to the object it generates first. os g action approve / os g app crm would now be refused in an os init project.

"Typical Workflow": step 3 is now os g flow opportunity. As written, os g flow lead_qualification now counted (1 Flows) but os validate warned the flow "targets object 'my_crm_lead_qualification', which this stack does not define … the flow will never fire". With opportunity, only the draft-status advisory remains. Step 4 ("Validate everything") is true as written: measured on 4173b2067, exit 0, 4 Objects, 1 Flows.

Changeset

.changeset/20215-generate-scaffolds-reach-stack.md is a patch for @objectstack/cli. It is a bug fix in a released package, Clause-②: no as claimed, the same shape #20197 landed its os g refusals under. It states what os init and os g now write and say that they did not before. The pending namespace-prefix note this PR falsified is corrected in place instead (next section), so this changeset carries no supersession paragraph.

A pending release note corrected in place (DELIBERATE CORRECTION)

This PR rewrites two sentences of .changeset/20197-generate-object-namespace-prefix.md, another card's PENDING release note. This PR makes both sentences false, and both notes compile into the same release. Commit a03756d5e carries that correction alone. Commit da4aca641 then drops the supersession paragraph this PR's own changeset carried, because the sentences it pointed at no longer exist.

node scripts/check-empty-changeset.mjs --base origin/main is red on this PR by design. It names that one file, "present on the merge base and CHANGED by this PR", and this is its DELIBERATE CORRECTION class: "your change may have made this PENDING release note false, and you rewrote it in the same stroke. Remedy: do NOT restore it -- say so on the PR and get it confirmed; restoring it from the base would put the false sentence back." The confirmation is the same-head contract review (seat answer 5860440515; claim 5859284846 amended to name this file). The precedent is PR #20284.

Line 11, One namespace source., last sentence:

  • Before: "dashboard and skill scaffolds name no object and never read the config."
  • After: "dashboard and skill scaffolds name no object, so a config that does not load does not stop them, but os g loads the config after every write, theirs included, to report whether the scaffold reaches the stack."

Line 12, Unchanged:, second sentence:

  • Before: "A view's, action's, flow's, dashboard's, app's and skill's own name, and an action's flow target, are written as before."
  • After: "An action's, flow's, dashboard's, app's and skill's own name, and an action's flow target, are written as before; a view's own name now equals the object key it binds to, prefix included."

Nothing else in that file moved: git diff --word-diff of a03756d5e shows these two sentences only (2 insertions, 2 deletions).

Readings for this round on head eedad4d37, after merging origin/main a78f731ad:

  • check-empty-changeset exit 1, naming only the file above.
  • The 95 derived families all ran. --ran reports "95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN", and every exit is 0 except that one.
  • pnpm lint exit 0.
  • node scripts/check-issue-citations.mjs --base origin/main exit 0 (18 citations resolve).
  • CLI typecheck and the five per-PR scaffold pins: 81/81.
  • The nightly chains: 17/17.

Pins

  • packages/cli/test/generate-scaffold-wiring.test.ts (unit, per-PR) covers:
    • the roster (every stack key is a key the stack schema declares; itemName is the emitted name);
    • the app/plugin templates (each barrel imported, wired, written; requires declared; the emitted project loads with every key a list);
    • barrel membership, the reach reader and the wiring lines;
    • os init keeping an author's barrel.
  • packages/cli/test/generate-stack-reach.test.ts (spawns the CLI, integration tier, per-PR, NOT .e2e) covers:
    • "refused", with the tree byte-identical, for an action with no object and a flow in a requires-less stack, plus a control where the same action generates once the object exists;
    • "not wired", for a pre-fix config (config byte-identical) and for no config;
    • "cannot run";
    • os g dashboard port against the export {}; barrel.
  • packages/cli/test/generate-scaffolds-reach-stack.e2e.test.ts (nightly) is triage's pin: os init -t app, then os g of every type, then os validate exits 0 with 2 Objects, 1 Apps, 1 Views, 1 Dashboards, 1 Actions, 1 Flows. os compile's artifact carries every generated item, the skill included (os validate's summary has no skills row).

Verification

Round 0 readings, on head e33889d77 unless noted (patch round 1's readings on eedad4d37 are in the DELIBERATE CORRECTION section):

  • pnpm --filter @objectstack/cli typecheck (tsc plus the test layer): exit 0.
  • pnpm lint (whole repo, not narrowed): exit 0.
  • CLI unit project: 230 files, 3296 tests, all pass on 01a556a52 (after merging origin/main). The only later commit touches one integration-tier test file.
  • CLI integration project, in two batches: 58 files, 485 pass, 1 skipped (not in a file this PR touches), on 01a556a52. generate-stack-reach.test.ts passes 7/7 on e33889d77.
  • OS_TEST_TIERS=nightly, generate-scaffolds-reach-stack.e2e.test.ts plus the existing generate-object-namespace-prefix.e2e.test.ts: 17/17.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands): 94 families, each run with its exit code recorded. --ran reports "94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN", all exit 0. On 01a556a52, three gates first refused with exit 3 (a prerequisite: packages outside the CLI closure had no dist). They were re-run to exit 0 after building.
  • node scripts/check-issue-citations.mjs --base origin/main: exit 0 (27 citations resolve).
  • Merged origin/main at 6ac33a57d (which carries PR feat(spec,metadata-protocol): a stored filter the record-filter conversion leaves as stored is a TODO in os migrate meta --stored, not silence (#17321) #20244's cli.mdx edit, a disjoint range), with a clean merge. The five commits origin/main gained since touch no packages/cli or cli.mdx path.

Ablations

Each ablation was committed first, mutated through scripts/ablation-replace.mjs (the anchor must hit, and the landing is proven by blob hash), run, and restored. Every restore was proven: the blob equals HEAD's (init.ts 3770e16c, generate.ts 3a92cfa4) and git diff HEAD is empty. All four ran on e33889d77, and every direction was red.

mutation per-PR guards nightly chain
revert the wiring (the templates wire objects only) 9 failed 8 failed (every os g prints wiring lines; counts; artifact)
revert row 2 (delete the template's requires line) 4 failed 3 failed (os g flow refused; counts; artifact)
view name back to the unprefixed stem 3 failed (incl. the #20197 census) n/a
barrel check back to includes 1 failed (os g dashboard port) n/a

Acceptance notes

  • The npm create objectstack starter is not wired. packages/create-objectstack/src/templates/blank/objectstack.config.ts imports ./src/objects/index.js alone and declares requires: ['automation']. It is read-only for this card. On that road (the north-star road starts there), os g view now says "not wired" with the lines, and os validate still counts 0 until the starter wires its barrels. Reported, not edited.
  • packages/spec/prompts/create-new-project.md (read-only here) lists flows/, dashboards/ and reports/ in its project tree, but its config sample wires objects, actions and apps only.
  • cli.mdx about line 741 (the "Your First App" fixture callout, outside this claim's ranges) says the walkthrough's os generate commands would make the summary gain "my_app_customer and a Logic: row". In the create-objectstack starter those scaffolds are not wired, and os generate action approve binds to no declared object.
  • The pending .changeset/20197-generate-object-namespace-prefix.md had two sentences this PR makes false. On the seat's answer (A), they are corrected in place, as the DELIBERATE CORRECTION section above describes; check-empty-changeset stays red for that class by design.
  • os validate's summary counts no skills (collectMetadataStats has no skills member), so the chain pin holds the skill through the compiled artifact.
  • Hand-wiring an empty barrel with Object.values hits TS2322 for the map-supported keys. The cause is MetadataCollectionInput's map branch in packages/spec, read-only here. The template avoids it with exportsOf.
  • Two runtime/validate disagreements the boot measurement surfaced are handed to the seat in the dev report rather than fixed here:
    • os validate passes a views container whose name disagrees with its object key, which os serve refuses;
    • defineStack's trigger-capability rule accepts triggers without automation, and the server then never runs the flow.

Generated by Claude Code

os init's app and plugin configs now import every barrel os generate
writes into (derived from the generator roster) and declare the
capabilities the flow scaffold needs; os g loads the config after writing
and reports whether the item reached the stack, refusing and rolling back
a write that makes a loading config stop loading. The view scaffold's
container name now equals the object key it binds to, and barrel
membership is asked of the compiler instead of a substring test.

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
…to-validate chain

The init templates read barrels through a typed exportsOf helper: with an
empty barrel, Object.values took its element type from defineStack's map
branch and a fresh project failed its own tsc.

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
…efore the control

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
…hat validates what it generated

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 31 documentable anchor(s).

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json a78f731add67eab50b3e969e8ad46e44d195ab12.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: objectstack.config.ts (literal, 32 pages)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 25 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json a78f731add67eab50b3e969e8ad46e44d195ab12 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1da143eef47aea1eb9398c58c72abd1a4270e9ff — the merge of head eedad4d37ccf16ab1d28d8c3ba07e80175f20894 into base a78f731add67eab50b3e969e8ad46e44d195ab12, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1da143eef47aea1eb9398c58c72abd1a4270e9ff && git checkout 1da143eef47aea1eb9398c58c72abd1a4270e9ff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a78f731add67eab50b3e969e8ad46e44d195ab12 eedad4d37ccf16ab1d28d8c3ba07e80175f20894 && git checkout -B drift-repro a78f731add67eab50b3e969e8ad46e44d195ab12 && git merge --no-ff eedad4d37ccf16ab1d28d8c3ba07e80175f20894

node scripts/docs-audit/affected-docs.mjs --json a78f731add67eab50b3e969e8ad46e44d195ab12

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs a78f731add67eab50b3e969e8ad46e44d195ab12 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…refix note this PR falsifies

The pending note said dashboard and skill scaffolds never read the config,
and that a view's own name is written as before. With this PR os g loads
the config after every write to report whether the scaffold reaches the
stack, and a view's name equals the object key it binds to. Corrected in
place (the DELIBERATE CORRECTION class of check-empty-changeset.mjs), both
notes compiling into the same release.

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
…ce-prefix note is corrected in place

Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: eedad4d37ccf16ab1d28d8c3ba07e80175f20894

① Derived judgments

  • The route (the templates wire every barrel; os g never edits the config). Correct.
    • It is the right reading of triage 5855744374: 「the claimant chooses between wiring the barrel on os g and importing every barrel in the templates; measure which keeps a hand-edited config safe; a loud not-wired line is the floor」.
    • init.ts SCAFFOLD_WIRED_BARRELS is derived from GENERATOR_SCAFFOLD_TARGETS, and both the app and plugin configContent render renderWiredImports() + renderWiredStackKeys().
    • generate.ts runMetadataGeneration only calls measureStackReach (utils/scaffold-wiring.ts), which runs findConfigPath + loadConfig + authoringRuleUnionStack, the same fold that validate.ts:350 counts with. It never touches config text.
    • Driven from the head's source in a scratch worktree (tsx bin/run-dev.js):
      • a fresh os init my-app -t app --no-install;
      • os g object|view|action|flow|dashboard|app|skill order_line: every run exits 0, printing Reaches the stack: … carries it in \KEY` as 'NAME'`;
      • config sha1 716a96aa before and after;
      • os validate exits 0 with Data: 2 Objects, UI: 1 Apps 1 Views 1 Dashboards 1 Actions, Logic: 1 Flows;
      • the os compile artifact carries all seven names, including skills: ['order_line'].
  • Hand-edited configs: honest for every claimed shape. Driven:
    • (b3) an objects-only pre-fix config: os g view/flow exit 0 with Not wired, printing import * as views from './src/views'; / views: Object.values(views), (the flow also prints requires: ['automation', 'triggers'],). The config sha is unchanged, and validate counts 0 Apps/0 Flows.
    • (b2) reordered imports, an extra node:path import, and defineStack fed from spread variables: Reaches, the sha is unchanged, and validate counts 1 Views 1 Flows.
    • (b4) objectstack.config.mjs: Reaches, the sha is unchanged, and the items are counted.
    • (c) no config: Not wired: there is no objectstack.config.{ts,js,mjs} here, the lines are printed, and the file is written.
    • (d) the plugin template: Reaches, the sha is unchanged, and validate counts 1 Views/1 Flows.
    • Triggers-only requires: Reaches, plus It will not run yet … requires: ['triggers', 'automation'],.
    • A broken config: os g dashboard generates and exits 0 with cannot be told; os g view is refused (the [finding] os generate object NAME in an os init -t app project writes name: 'NAME' with no namespace prefix, so the next os validate refuses the object the CLI just generated #20197 control).
  • Byte-identity on refusal. Correct.
    • os g action approve into a wired project, and os g flow into a requires-less wired project, both exit 1, and the full-tree sha1 listing is identical before and after. generate.ts records createdDir, barrelBefore and barrelWritten, and unwinds them in the refusal branch.
    • The barrel line is only ever appended AFTER the scaffold's writeFileSync, so "barrel updated, scaffold missing" cannot arise from the refusal path.
    • The plain I/O catch (printError + exit 1) does not unwind, but that shape is pre-existing on main.
  • Row 2. Correct.
    • requires: ['automation', 'triggers'] is SCAFFOLD_WIRED_REQUIRES, the union of GENERATORS.flow.requires (FLOW_SCAFFOLD_REQUIRES).
    • stack.zod.ts validateTriggerCapability refuses a record_change flow without triggers. An absent requires counts as omitting it, which the exit-1 drive above confirms.
    • format.ts:1285 is the boot warning for flows without automation.
    • Both tokens are in PLATFORM_CAPABILITY_TOKENS.
    • Nothing to install:
      • capability-preflight.ts makeProviderResolver resolves from the host dir OR the CLI's own module graph (createRequire(import.meta.url)), and @objectstack/service-automation and @objectstack/trigger-record-change are @objectstack/cli dependencies.
      • Measured: os validate printed "Capability … not installed" only while those CLI deps had no dist in my partial build, and went silent once they were built.
    • The cost (a flow-less project mounts automation + triggers) is stated in the template comment.
  • exportsOf and export {};. Correct.
    • A fresh os init -t app project passes tsc --noEmit -p . with exit 0.
    • Ablating exportsOf(x) → Object.values(x) in the emitted config gives exactly 4× TS2322, at the actions/flows/dashboards/apps lines (the MAP_SUPPORTED_FIELDS keys, metadata-collection.zod.ts:61). That reproduces the PR's stated measurement. The populated project also typechecks.
    • Empty barrels:
      • generate-scaffold-wiring.test.ts asserts that every non-object key loads as [];
      • scaffold-emission-typechecks.test.ts, which runs all templates through their own tsconfig, passed here 41/41 with the modified pins.
    • stackKey = singularToPlural(type):
      • PLURAL_TO_SINGULAR maps views/actions/flows/dashboards/apps/objects/skills one-to-one, with no duplicate singular, so Object.fromEntries cannot shadow;
      • the roster pin checks each key against ObjectStackDefinitionSchema.shape.
  • The two extra generate.ts fixes, against the bounded in-place exemption. .claude/agents/os-dev.md §3 sets four conditions: ① the same defect class, ② mechanical with a pinned shape, ③ no other claim on the file, and ④ the same gate family. It also owes the claim-surface amendment and a mention in the PR body.
    • View name: ObjectQL.registerMetadataCollections (engine.ts:6480-6510) throws when a container's name differs from the key derived from object. So in a namespaced project, the old scaffold is refused at boot exactly once src/views is wired, which is row 2's class.
      • The fix is objectNameFor(name, namespace), the derivation already used for object:. It is pinned by the census ('view.name (its object key)': PREFIXED) and by the roster pin.
      • Nothing reads the old name:
        • the runtime always registered the container under the derived key (toRegister = … {...item, name: itemName});
        • getViewsByObject filters expanded items by object;
        • ViewSchema.name is grammar-free;
        • no doc sample shows the old name.
    • Barrel membership: on main, it was indexContent.includes(toCamelCase(name)). Measured on the head, os g view order after order_line now appends export { default as order }. barrelExportNames walks named exports only.
    • Claim 5859284846 was amended in place (updated 22:30Z) to name both fixes, and the PR body names them with evidence. The conditions hold, and both fixes are correct.
  • The pins.
    • Run on the head: generate-scaffold-wiring.test.ts (unit tier) and generate-stack-reach.test.ts (a spawn file that vitest-tiers classifies as integration, per-PR) → 40/40. The .e2e chain is nightly by name.
    • The read-only fence forbids mutating the checkout, so each ablation was read against the pins rather than run:
    • Observation, not a defect: only the nightly chain proves that os validate counts. No per-PR pin invokes os validate. The per-PR spawn pin covers the reach reader through the same fold, and I measured the count directly above.

② Semver level

@objectstack/cli patch, Clause-②: no, no arm: correct.

  • No key is added to any published payload, and no published accept set moves:
    • packages/cli/src/index.ts is untouched; it re-exports only the command classes;
    • TEMPLATES and SCAFFOLD_WIRED_* are unreachable from the exports map, so per contract-review.md they are shipped bytes, not a published surface;
    • packages/spec is not touched.
  • The scaffold-output changes (a prefixed view name, new barrels, requires) are generated project content.
  • The new exit 1 (a write that stops a loading config from loading) is the same class as [finding] os generate object NAME in an os init -t app project writes name: 'NAME' with no namespace prefix, so the next os validate refuses the object the CLI just generated #20197's os g refusals, which shipped as patch with no arm. It is not the minor (narrowing) class of .changeset/19120-* / 19417-*, which shrank the accept set of a published API door (POST /api/v1/packages) and of the protocol install primitive.
  • The WHICH LEVEL rule (pr-automation.yml) raises to minor only for a widening of a public surface, which does not occur here.

③ Boundary flags

  • Deliberate correction of .changeset/20197-generate-object-namespace-prefix.md. Commit a03756d5e is a word-diff of 2 insertions and 2 deletions, and nothing else in the file moved (verified).

    • Line 11, last sentence.
      • Before: "dashboard and skill scaffolds name no object and never read the config."
      • After: "dashboard and skill scaffolds name no object, so a config that does not load does not stop them, but os g loads the config after every write, theirs included, to report whether the scaffold reaches the stack."
      • (a) Made false by THIS PR: yes. runMetadataGeneration now calls readProjectNamespace() unconditionally, and measureStackReach after every write.
      • (b) True of the combined release: yes, measured. os g dashboard sales into a throwing config generates (exit 0) and prints does not load, so whether … reaches its stack cannot be told. A wired project prints Reaches the stack for dashboard and skill.
      • (c) See below.
    • Line 12, second sentence.
      • Before: "A view's, action's, flow's, dashboard's, app's and skill's own name, and an action's flow target, are written as before."
      • After: "An action's, flow's, dashboard's, app's and skill's own name, and an action's flow target, are written as before; a view's own name now equals the object key it binds to, prefix included."
      • (a) Made false by THIS PR: yes. The view generator's name moved from toSnakeCase(name) to objectNameFor(name, namespace).
      • (b) True of the combined release: yes. The diff touches no other scaffold's name and not the action target (all itemNames are pinned equal to the emitted names), and the drive shows views carrying my_app_order_line.
    • (c) Nothing else in the note is now false. The prefix, no-double-prefix, one-source (the refusal on a non-loading config for object-naming types is retained) and --help sentences all still hold. Line 9's "That covers …" enumeration is now non-exhaustive, since a view's name is also a prefixed object name, but line 12 states it, so it is not false.
    • Deliberate correction of .changeset/20197-generate-object-namespace-prefix.md confirmed: 2 sentence(s) judged.
  • The PR's own changeset after dropping the supersession paragraph (da4aca641, 2 deletions): complete and true. Every remaining statement matches the diff and the drive:

    • the wired imports + exportsOf;
    • export {}; barrels, never overwritten;
    • the requires pair, with its two failure modes;
    • the four os g outcomes;
    • refusal + unwind;
    • dashboard and skill reading the config;
    • the view name;
    • the compiler-asked barrel step;
    • the flow header;
    • earlier projects keeping their config.

    Nit only: the barrel is export {}; plus a four-line comment, while the changeset and PR body say "containing only export {};".

  • Files outside the amended claim's surface: none. The 11 PR files are:

    • init.ts and generate.ts;
    • the new utils/scaffold-wiring.ts, a helper the wiring needs;
    • five packages/cli/test/* files;
    • cli.mdx, at the os generate section (:1403-:1470) and Typical Workflow (:2197-:2205, which is the :2156-:2175 range shifted);
    • .changeset/20215-*.md;
    • the 20197 note the amended claim names.
  • PR body vs diff: the route table, the refusal rows, the "cannot tell" control, the exportsOf TS2322 measurement, the view and barrel fixes, the docs list and the ablation table all correspond to the diff and to what I measured. The (b5) create-objectstack blank reading is consistent with wiringLines printing the whole requires list. Clause-②: no is on its own line, as check-changeset-no-major.mjs requires.

  • CI on head eedad4d37: 43 check-runs, all completed.

Implemented-by: claude/issue-20215-generate-scaffolds-reach-stack
Reviewed-by: session_01UYBdGBzWSrAMzpW8ah3GbP

Independence: INDEPENDENT AGENT (fed the card, the triage direction and the PR only; not the dispatch order or the seat's conclusions)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Check Changeset is red by design on this head: a confirmed DELIBERATE CORRECTION

domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · written 2026-09-28T00:30Z

  • The gate: Check Changeset (.github/workflows/pr-automation.yml, job changeset-check), which runs scripts/check-empty-changeset.mjs --base "$MERGE_BASE".

  • The reason: this PR rewrites two sentences of .changeset/20197-generate-object-namespace-prefix.md, a PENDING release note that PR fix(cli): os generate gives object names the manifest namespace prefix #20214 added. This PR makes both sentences false:

    • after every write, os g now loads the config, dashboard and skill included;
    • a view's own name now equals its object key.

    That is the gate's DELIBERATE CORRECTION class: 「do NOT restore it -- say so on the PR and get it confirmed」 and 「this gate stays red either way」.

  • The confirmation: the at-tier contract review on this same head (eedad4d3), record 5861268261, names the note and judges both rewritten sentences: 「Deliberate correction of .changeset/20197-generate-object-namespace-prefix.md confirmed: 2 sentence(s) judged.」 Under landing-operations.md, that record is the confirmation.

  • Why it does not block the queue: pr-automation.yml triggers on pull_request only, with no merge_group, and Check Changeset is not a required context. Every other check on this head is success or a rostered expected skip.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 00:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit c5dcb3b Sep 28, 2026
43 of 45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20215-generate-scaffolds-reach-stack branch September 28, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants