fix(cli): generated scaffolds reach the stack, or os g says they do not (#20215) - #20329
objectstack-fleet[bot] merged 11 commits into
Conversation
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>
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>
…nerate-scaffolds-reach-stack
…'s text Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 25 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 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
|
…nerate-scaffolds-reach-stack
…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>
…nerate-scaffolds-reach-stack
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: Independence: INDEPENDENT AGENT (fed the card, the triage direction and the PR only; not the dispatch order or the seat's conclusions) VERDICT: PASS |
|
Fixes #20215
Clause-②: no
os init(theappandplugintemplates) now wires every barrelos generatewrites into, and declares the capabilities the flow scaffold runs on. After writing,os gloads 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 grefuses and takes the write back out.os gnever edits a config.Premise, re-measured on
origin/main6a6a17b6before any editI built the CLI's dependency closure at
6a6a17b6, ranos init my-app -t app --no-install, generated each of the seven types asorder_line, and then ranos validate:os gTip: Run objectstack validate to check your configos validateData: 2 Objects 5 Fields·UI: 0 Apps·Logic: 0 FlowsRow 2 reproduces. With all six barrels wired by hand,
os validateexits 1 with "flow 'order_line_flow' declares a 'record_change' trigger butrequiresdoes not include 'triggers'". Addingrequires: ['triggers']gives exit 0,UI: 1 Apps 1 Views 1 Dashboards 1 Actions,Logic: 1 Flows.Booting that hand-wired project with
os serve --devmeasured two more facts:views:container from manifest 'com.example.my-app': the container's ownnameis 'order_line', which disagrees with the object key it binds to, 'my_app_order_line' … dropname, or set it to 'my_app_order_line'".os validatehad passed it. So wiringsrc/viewsalone would have turned "the view is silently absent" into "the server does not boot" on the road's next step.triggersis not enough for a flow to run. Withrequires: ['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 printedFlows: 1 flow(s) 1 bound to triggers (record_change, schedule, time_relative, api) · 1 draft.Row 1: the route, measured
The route is:
os initimports every generator's barrel, andos greports whether its file reached the stack without ever editing the config.The floor is exact for every config shape. After writing,
os gloads the config through the sameloadConfigthatos validateuses, folds it the way the counter does (authoringRuleUnionStack), and looks for the item's metadatanameunder the stack key (singularToPlural(type)). It never parses the config's text, so a reordered config, variables,.mjsandpackages[]are all read the same way.os g object,os g viewandos g flow(order_line) were run in each shape. The config hash is sha1, taken before and after the three runs:os g view/os g flowsaidos validateos init -t appfbea6b0e→fbea6b0e1 Views,1 FlowsdefineStackfed from variables (const ui = {…}; const stack = {…, ...ui})os initconfig (./src/objectsonly)requiresfor the flow)0 Apps,0 Flowsobjectstack.config.mjscreate-objectstackblankshape (./src/objects/index.js,requires: ['automation'])requires: ['automation', 'triggers'],0 Flowsos init -t pluginThese rows were measured on
cae468f49. The wiring-advice text in (b5) is fromc21f96460.Why not "
os gedits the config". That route would be a config editor, a capability the CLI has nowhere today:os initonly 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 nodefineStackobject 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 aneeds_decision. The editor route was not built, so its column is analysis, NOT MEASURED.Empty barrels.
os initwrites anindex.tscontaining onlyexport {};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 validateexits 0 on a fresh project:UI: 0 Apps,Logic: 0 Flows.os compileexits 0.tsc --noEmitexits 0, measured by the existingscaffold-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, notObject.values.Object.values(emptyBarrel)does not type-check againstdefineStackfor the keys that also accept a name-keyed map. With no export to infer from, TypeScript takes the element type from the map branch, whosenameis optional. Measured: TS2322 onactions,flows,dashboardsandappsof a fresh project, whileviewsandskills, 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 emittednameby a pin, with and without a namespace).Row 2: the template declares what the flow needs
Every template that wires
src/flowsdeclaresrequires: ['automation', 'triggers']. The list is derived as the union of the generators' ownrequires, which today is the flow scaffold's pair. The flow scaffold's header also states the pair.I chose this over "
os g flowaddstriggerstorequires" because adding torequiresis the same config editor. It includesautomationas 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 flowwarns and prints the wholerequireslist to use.What
os gsays nowrequireslacks a token the scaffold runs onrequireslist is printed"Refused" is what the wired barrels make reachable. Measured on
c21f96460in a fresh project:os g action approvewithout anapproveobject: exit 1 withdefineStack'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 flowinto a wired config withoutrequires: exit 1, tree unchanged.The "cannot tell" row keeps the
#20197control: in a config that does not load,os g dashboard salesstill generates, exit 0.Two fixes in
generate.ts, same class, in placeBoth 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.
nameis its object key, prefix included. The server registers a views container under that key and refuses one whosenamedisagrees. The#20197census pinned the view's ownnameas unprefixed because noos validategate judged it; that assertion is updated, and the reason is written into the pin.barrelExportsBinding), not byindexContent.includes(binding). Measured: afteros g view order_line,os g view orderfoundorderinsideorderLineand exported nothing. The newexport {};barrels would have made that biteos g dashboard port.Docs
content/docs/deployment/cli.mdx,os generatesection:os gnever edits the config.nameis its object key.os g action approve/os g app crmwould now be refused in anos initproject."Typical Workflow": step 3 is now
os g flow opportunity. As written,os g flow lead_qualificationnow counted (1 Flows) butos validatewarned the flow "targets object 'my_crm_lead_qualification', which this stack does not define … the flow will never fire". Withopportunity, only the draft-status advisory remains. Step 4 ("Validate everything") is true as written: measured on4173b2067, exit 0,4 Objects,1 Flows.Changeset
.changeset/20215-generate-scaffolds-reach-stack.mdis apatchfor@objectstack/cli. It is a bug fix in a released package,Clause-②: noas claimed, the same shape#20197landed itsos grefusals under. It states whatos initandos gnow 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. Commita03756d5ecarries that correction alone. Commitda4aca641then 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/mainis 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 answer5860440515; claim5859284846amended to name this file). The precedent is PR #20284.Line 11, One namespace source., last sentence:
dashboardandskillscaffolds name no object and never read the config."dashboardandskillscaffolds name no object, so a config that does not load does not stop them, butos gloads the config after every write, theirs included, to report whether the scaffold reaches the stack."Line 12, Unchanged:, second sentence:
name, and an action's flowtarget, are written as before."name, and an action's flowtarget, are written as before; a view's ownnamenow equals the object key it binds to, prefix included."Nothing else in that file moved:
git diff --word-diffofa03756d5eshows these two sentences only (2 insertions, 2 deletions).Readings for this round on head
eedad4d37, after mergingorigin/maina78f731ad:check-empty-changesetexit 1, naming only the file above.--ranreports "95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN", and every exit is 0 except that one.pnpm lintexit 0.node scripts/check-issue-citations.mjs --base origin/mainexit 0 (18 citations resolve).Pins
packages/cli/test/generate-scaffold-wiring.test.ts(unit, per-PR) covers:itemNameis the emittedname);app/plugintemplates (each barrel imported, wired, written;requiresdeclared; the emitted project loads with every key a list);os initkeeping an author's barrel.packages/cli/test/generate-stack-reach.test.ts(spawns the CLI, integration tier, per-PR, NOT.e2e) covers:os g dashboard portagainst theexport {};barrel.packages/cli/test/generate-scaffolds-reach-stack.e2e.test.ts(nightly) is triage's pin:os init -t app, thenos gof every type, thenos validateexits 0 with2 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
e33889d77unless noted (patch round 1's readings oneedad4d37are 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.unitproject: 230 files, 3296 tests, all pass on01a556a52(after mergingorigin/main). The only later commit touches one integration-tier test file.integrationproject, in two batches: 58 files, 485 pass, 1 skipped (not in a file this PR touches), on01a556a52.generate-stack-reach.test.tspasses 7/7 one33889d77.OS_TEST_TIERS=nightly,generate-scaffolds-reach-stack.e2e.test.tsplus the existinggenerate-object-namespace-prefix.e2e.test.ts: 17/17.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands): 94 families, each run with its exit code recorded.--ranreports "94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN", all exit 0. On01a556a52, three gates first refused with exit 3 (a prerequisite: packages outside the CLI closure had nodist). They were re-run to exit 0 after building.node scripts/check-issue-citations.mjs --base origin/main: exit 0 (27 citations resolve).origin/mainat6ac33a57d(which carries PR feat(spec,metadata-protocol): a stored filter the record-filter conversion leaves as stored is a TODO inos migrate meta --stored, not silence (#17321) #20244'scli.mdxedit, a disjoint range), with a clean merge. The five commitsorigin/maingained since touch nopackages/cliorcli.mdxpath.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.ts3770e16c,generate.ts3a92cfa4) andgit diff HEADis empty. All four ran one33889d77, and every direction was red.objectsonly)os gprints wiring lines; counts; artifact)requiresline)os g flowrefused; counts; artifact)nameback to the unprefixed stem#20197census)includesos g dashboard port)Acceptance notes
npm create objectstackstarter is not wired.packages/create-objectstack/src/templates/blank/objectstack.config.tsimports./src/objects/index.jsalone and declaresrequires: ['automation']. It is read-only for this card. On that road (the north-star road starts there),os g viewnow says "not wired" with the lines, andos validatestill counts 0 until the starter wires its barrels. Reported, not edited.packages/spec/prompts/create-new-project.md(read-only here) listsflows/,dashboards/andreports/in its project tree, but its config sample wiresobjects,actionsandappsonly.cli.mdxabout line 741 (the "Your First App" fixture callout, outside this claim's ranges) says the walkthrough'sos generatecommands would make the summary gain "my_app_customerand aLogic:row". In thecreate-objectstackstarter those scaffolds are not wired, andos generate action approvebinds to no declared object..changeset/20197-generate-object-namespace-prefix.mdhad 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-changesetstays red for that class by design.os validate's summary counts no skills (collectMetadataStatshas no skills member), so the chain pin holds the skill through the compiled artifact.Object.valueshits TS2322 for the map-supported keys. The cause isMetadataCollectionInput's map branch inpackages/spec, read-only here. The template avoids it withexportsOf.os validatepasses a views container whosenamedisagrees with its object key, whichos serverefuses;defineStack's trigger-capability rule acceptstriggerswithoutautomation, and the server then never runs the flow.Generated by Claude Code