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 |
|
…el, so `os g` scaffolds reach the stack (objectstack-ai#20333) (objectstack-ai#20363) Fixes objectstack-ai#20333 Clause-②: no ## Summary `npm create objectstack`'s blank starter imported `./src/objects` alone, so everything `os g view|action|flow|dashboard|app|skill` wrote was never loaded and `os validate` counted 0 of it. The starter now wires the seven generator barrels `os init` wires since PR objectstack-ai#20329, in the lines `os init` renders: `exportsOf` over `export {};` barrels, and `requires: ['automation', 'triggers']`. The copy is bound to the CLI's single source (`SCAFFOLD_WIRED_BARRELS` / `SCAFFOLD_WIRED_REQUIRES`, derived from `GENERATOR_SCAFFOLD_TARGETS`) by a parity pin, so it is not a second wiring rule. A per-PR pin drives `npm create objectstack` → `os g object` (control) → `os g flow` → `os validate` and reads `Logic: 1 Flows`. ## What changed - `packages/create-objectstack/src/templates/blank/objectstack.config.ts`: imports every wired barrel, declares the `exportsOf` helper, and hands each barrel to its stack key. The objects import changes from `'./src/objects/index.js'` to `'./src/objects'`, the extensionless form `os init` renders, which the parity pin compares verbatim; the template's `moduleResolution: bundler` resolves the directory index, and a fresh scaffold type-checks. It carries `requires: ['automation', 'triggers']`. `automation` was already there for the three connector plugins, and its comment keeps that reason. - Six new `src/{views,actions,flows,dashboards,apps,skills}/index.ts` barrels, byte-identical to what `os init` writes. - Two pins in `packages/cli/test/` (below). The CLI is the only package that can call the renderer, and it already depends on `create-objectstack`. - `packages/cli/package.json` gains `@objectstack/connector-{rest,openapi,mcp}` as devDependencies (lockfile +9 lines, one importer block). They exist only so the scaffolded project the chain pin builds under the CLI's `node_modules` can resolve the blank config's connector imports, and so CI builds them in `@objectstack/cli#test`'s closure. - `scripts/cross-package-test-inputs.mjs` and `turbo.json` declare the blank config and `src/**` as inputs of `@objectstack/cli#test`, with a witness for the barrel glob the scan cannot name. - Docs this change made false (see below), and a `create-objectstack` patch changeset. ## Why a static copy, and what binds it `create-objectstack` cannot import the roster. The dependency edge runs the other way, and the npx entry must not pull the CLI's closure: the boundary `scripts/sync-scaffold-emission-policy.mjs` already documents. Measured options: - **Generate at build time.** The roster is computed from the `GENERATORS` literal in `generate.ts`. Reading it at `create-objectstack`'s build would need either text-parsing that literal, or evaluating the CLI's source before the CLI's own dependencies are built, which is a build-order cycle. - **Parity pin over a static copy.** Chosen as the least machinery. `create-objectstack-wiring-parity.test.ts` reads every expected line off the CLI: the barrel import lines, the `exportsOf` line and the stack-key lines of `TEMPLATES.app.configContent`, the `requires` tokens as a superset of `SCAFFOLD_WIRED_REQUIRES`, and each empty barrel byte for byte from `TEMPLATES.app.srcFiles`. A generator added to the roster, a renderer change or a hand edit of the template reddens it (ablations A1 to A3). ## Measured before and after, through the real commands The on-ramp's real `bin/` scaffolded `my-app --skip-install --skip-skills` into a directory where the config's imports resolve, then this repo's CLI ran. | step | `origin/main` `c74de10a9` | this branch | |:---|:---|:---| | `os g object order_line` (control) | exit 0, reaches the stack | exit 0, reaches the stack | | `os g flow order_line` | exit 0, **Not wired** | exit 0, reaches the stack | | `os validate` | exit 0, `Data: 2 Objects`, `Logic: 0 Flows` | exit 0, `Data: 2 Objects`, `Logic: 1 Flows` | - **`exportsOf` is required here too.** A fresh starter type-checks (`tsc --noEmit`, 6.0.3, exit 0). The same starter with `Object.values` on the empty barrels fails with 4 x TS2322 (actions, flows, dashboards, apps). After generating the object and the flow it still type-checks. - **`requires` boots.** `os dev --fresh` on a random port: the flow-less fresh starter was healthy after about 22s, `/api/v1/ready` answered 200, and `AutomationServicePlugin` and the record-change, schedule, time-relative and api trigger plugins loaded, resolved through the CLI's own dependencies. With the generated flow it was healthy after about 24s and reported `Flows: 1 flow(s) 1 bound to triggers`. Neither boot printed "not enabled" or "NOT installed". - **Census.** `src/templates/` holds one starter, `blank`, which is also the registry's only entry. ## Pins - `packages/cli/test/create-objectstack-wiring-parity.test.ts` (unit, per-PR): 20 cases, described above. - `packages/cli/test/create-objectstack-stack-reach.test.ts` (integration, per-PR, not `.e2e`): the chain above, with item names read off the generator roster. It asserts the exit codes, the named subjects, the absence of the wiring lines and of a `requires` line from `os g flow`, and the `Data: 2 Objects` / `Logic: 1 Flows` counts. No prose is pinned. ## Ablations Each ran after the fix was committed. Mutations went through `scripts/ablation-replace.mjs` in wrap mode, which verified the anchor count and the blob change and restored with blob equal to HEAD and an empty `git diff HEAD`. - **A1, the wiring reverted** (the `flows: exportsOf(flows),` line deleted, then `create-objectstack` rebuilt). `ablation-dist-preflight --absent` confirmed the line was gone from `dist/`. Chain pin: 2 failed, 2 passed. The control and the scaffold stayed green, and `os g flow` printed the wiring lines while validate read no `Logic: 1 Flows`. Parity pin: 1 failed, 19 passed, on the stack-key comparison. Direction: red. - **A1 restore.** Rebuilt; `ablation-dist-preflight` found the marker present in `dist/templates/blank/objectstack.config.ts`, and the whole tree was clean. Chain pin 4/4, parity pin 20/20. - **A2, a barrel dropped from the template** (the `skills` key deleted): parity 1 failed, 19 passed. Red. - **A3, one barrel's bytes drifted from what `os init` writes** (`views/index.ts` reworded): parity 1 failed, 19 passed. Red. - After A2 and A3 the whole tree was clean, and parity was 20/20. ## Verification Patch round 1, at HEAD `702a27775` (origin/main `a88a1bb39` merged at `df0c0c846`): the 124 derived gates, `check-issue-citations --base origin/main` and `check:scaffold-emission-policy` all exited 0 on the first pass (`--ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN), including `check:doc-anchors`, `check:docs-audit-scope` and `check-affected-docs`; `pnpm lint` exited 0; the parity pin 20/20, the chain pin 4/4, and `create-objectstack` 16 files, 232 passed. Round 0: all of the following ran at HEAD `d50d46fe0` (origin/main `26daf0b03` merged). - `pnpm --filter create-objectstack test`: 16 files, 232 passed. `typecheck`: exit 0. - `pnpm --filter @objectstack/cli typecheck`: exit 0, including `check:test-typecheck`, whose ledger is unchanged. - CLI `unit` project: 231 files, 3316 passed. - CLI `integration`: this chain pin plus `generate-stack-reach.test.ts`, 2 files, 11 passed. - `pnpm lint`: exit 0 over the whole repo, not narrowed. - `node scripts/check-issue-citations.mjs --base origin/main`: exit 0. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN. Three gates first exited 3 with PREREQUISITE NOT MET (`check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n-coverage`) and exited 0 after a full build. - `pnpm check:scaffold-emission-policy`: exit 0. ## Docs this change made false, and a surface note These published lines described an objects-only starter and are corrected in place: - the blank starter's `README.md` Layout, plus its app remedy, which now says to export the file from `src/apps/index.ts`; - the shipped `AGENTS.md` rule 3, which prescribed `Object.values()` (measured TS2322 on the now-empty barrels); - the package `README.md` tree; - `content/docs/getting-started/your-first-project.mdx`: its section-2 tree and config block; - `content/docs/getting-started/build-with-claude-code.mdx` (patch round 1): step 3 said the agent wires the action, view and app through `actions:` / `views:` / `apps:` keys in `defineStack()`. It now says each file is exported from its directory's barrel (`src/actions/index.ts`, `src/views/index.ts`, `src/apps/index.ts`), which the starter's config already hands to `defineStack()`, matching the shipped `AGENTS.md` rule 3. A sweep of `content/docs/` found no other sentence telling a starter author to add a collection key; - `content/docs/deployment/cli.mdx`. In `cli.mdx`, the `os generate` section's "Not wired" example named "the `npm create objectstack` starter", and its first-app walkthrough ran `os generate action approve`. On the wired starter that action is refused with exit 1: "Action 'approve' references object 'my_app_approve' which is not defined in objects". The walkthrough now runs `object customer`, then `flow customer`, then `action customer`, measured `UI: 1 Actions` and `Logic: 1 Flows`. Its fixture callout now names the extra action. `content/docs/**` and `packages/create-objectstack/README.md` were outside the claim's first file surface; the seat amended the claim in place to name them. They are edited under the agent contract's rule that a published line this change makes false is repaired in the same PR. PR objectstack-ai#20341 edits `cli.mdx` around lines 1619 to 1690, disjoint from these hunks; PR objectstack-ai#20258 edited lines 1 to 7 of `your-first-project.mdx` and `build-with-claude-code.mdx`, has since landed, and merged into this branch without conflict. `skills/objectstack-platform/SKILL.md` line 192 says the template declares `requires: ['automation']`. That is now stale, but `skills/**` is a governed Tier H surface, so it is **not** edited here; the seat files it for the skills lane once this PR lands. ## Acceptance notes - Byte-identical barrels inherit the article slip in `init.ts`'s `renderEmptyWiredBarrel` ("a action", "a app"). `init.ts` is read-only here. Whoever next edits that renderer carries it, and the parity pin will then require the starter to follow. - The old walkthrough's `os generate flow onboarding` also bound its flow to an undeclared object. That was not silent: `os dev` warned "the flow will never fire". The new walkthrough binds to the object it creates. - Measured in patch round 1, on a scaffolded starter holding the Build with Claude Code step-3 files: exporting each from its barrel, with the config untouched, gives `os validate` exit 0 with `Data: 2 Objects 6 Fields` and `UI: 1 Apps 1 Views 1 Actions`, the page's step-4 counts. Adding `actions:` / `views:` / `apps:` keys beside the wired ones instead still validates (the later key wins), but the starter's `tsc --noEmit` fails with 3 x TS1117, and a later `os g view customer` then reports Not wired, while the barrel-wired project reports it reaches the stack. - The `requires` pair is PR objectstack-ai#20329's shape. The standing family cards for the rest of that seam are objectstack-ai#20331 and objectstack-ai#20332, both named on objectstack-ai#20215. --- _Generated by [Claude Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… without automation (objectstack-ai#20332) (objectstack-ai#20365) Fixes objectstack-ai#20332 Clause-②: no (narrowing) **BREAKING** (`@objectstack/spec` `minor`): a published accept set narrows. No export, no error code and no accepted shape is added. The refusal reuses `STACK_TRIGGER_CAPABILITY_REQUIRED` (`status: 422`). ## What changes `validateTriggerCapability` (`packages/spec/src/stack.zod.ts`) refused an auto-launched flow only when `requires` lacked `'triggers'`. Its docblock said the trigger "is installed by ONE token, `requires: ['triggers']`". That is false. Every trigger plugin installs its trigger into the automation service at `kernel:ready`, and without that service it warns and installs nothing. No runtime's resolver turns `triggers` into `automation`. So a stack with `requires: ['triggers']` and a `record_change` flow passed `os validate`, booted, and never fired the flow. Triage direction `5861188312`, verbatim: 「**The contract choice, decided here: refuse, do not imply.**」 The refusal now has three arms, one line per offending flow, on the same code, header and `issues` shape. Each prescription is the whole fix for the `requires` it was given: | `requires` (auto-launched flow present) | Before | After | |:---|:---|:---| | `['automation', 'triggers']` | accepted | accepted (any order) | | `['automation']` | refused: add `'triggers'` | **unchanged, byte for byte** | | `['triggers']` | **accepted** (the defect) | refused: add `'automation'` | | `[]` or absent | refused: add `['triggers']` | refused: add `['automation', 'triggers']` | | any, with no auto-launched flow, or only `obsolete` / `invalid` flows | accepted | accepted | ### The refusal text, quoted (for review) New arm, `requires: ['triggers']`: ```text flow 'task_fanout' declares a 'record_change' trigger but `requires` does not include 'automation' — 'triggers' installs the 'record_change' trigger into the automation service, and without it no 'record_change' trigger would be registered, so the flow would never auto-launch. Add 'automation' to requires: ['automation', 'triggers'] (@objectstack/service-automation runs the flow; @objectstack/trigger-* only fires it). ``` Changed arm, neither token: ```text flow 'task_fanout' declares a 'record_change' trigger but `requires` does not include 'automation' or 'triggers' — no 'record_change' trigger would be registered, so the flow would never auto-launch. Add requires: ['automation', 'triggers'] (record_change/schedule/time_relative/api ship in @objectstack/trigger-* and install into @objectstack/service-automation — 'triggers' alone installs nothing). ``` Unchanged arm, `requires: ['automation']`: ```text flow 'task_fanout' declares a 'record_change' trigger but `requires` does not include 'triggers' — no 'record_change' trigger would be registered, so the flow would never auto-launch. Add requires: ['triggers'] (record_change/schedule/time_relative/api ship in @objectstack/trigger-*). ``` ### A choice made here: the neither-token message names both tokens The dispatch left this open and suggested keeping today's message. Analysed on the four axes: - **Real business need (measured).** Every real producer that declares an auto-launched flow declares both tokens: `examples/app-showcase`, `examples/app-todo`, the `os init` template, and `os g flow`'s own output. The CLI boot banner already prescribes `requires: ['automation', 'triggers']`. No producer uses `['triggers']` alone. - **Long-term soundness.** One prescription that satisfies the contract. The message states the real install fact, the pair. - **Keeping AI authors from writing it wrong.** Keeping the old message would make an author who follows it literally land on `['triggers']`, and the new arm would refuse them a second time. A prescription that leads to another refusal is a trap. The pin `each prescription, applied literally once, is ACCEPTED` holds the property. - **No scope growth.** No new code path, code or export. Only the text of one arm changes. No pin anywhere parsed the old text for this case. ## Measurements (dispatch Zone 2) **§1 Reproduce on both runtimes.** - **Framework CLI**, at this branch with the pre-fix early exit (`if (hasTriggers) return errors;`) built into `packages/spec/dist`. The mutation was held by `scripts/ablation-replace.mjs` and confirmed in `dist/` by `scripts/ablation-dist-preflight.mjs`. On a probe project with `requires: ['triggers']` and one `record_change` flow: - `os validate` exited **0**. - `os serve --dev` reached `Server is ready` and printed `⚠ Flows: 1 flow(s) declared but the automation engine is not enabled — they will never run. Add requires: ['automation', 'triggers'] to objectstack.config.ts`. - It also printed four warns: `RecordChangeTriggerPlugin` / `ScheduleTriggerPlugin` / `TimeRelativeTriggerPlugin` / `ApiTriggerPlugin: automation service not available — … trigger NOT installed`. - After the restore leg (source equal to HEAD, spec rebuilt, marker absent from all 222 dist files), `os validate` on the same probe exits **1** with the new message. - **cloud** `origin/main` `96eb092`, read only: - `packages/objectos-runtime/src/capability-loader.ts` `resolveCapabilityDependencies` pulls `queue`, `job` and `messaging` for `triggers`, never `automation`. So the resolver does not imply it. - The hosted hosts force-mount both tokens on every tenant environment, whatever the artifact declares: `apps/objectos/hosted-slate.ts` `HOSTED_FORCED_REQUIRES`, and `apps/objectos-ee` `defaultRequires`. There a `['triggers']` stack runs, but so does a stack with no `requires` at all. The published arm has refused that second stack since it landed. The refusal judges the stack's own declaration, which is portable, and not one host's floor. - The fork condition ("`triggers` pulls `automation` in by itself") holds on neither resolver. **§2 Which kinds need `automation`.** All four. The boot above printed all four "NOT installed" warns. Each plugin's `kernel:ready` hook resolves `automation` and returns if it is missing: `packages/triggers/trigger-record-change/src/plugin.ts:47`, `trigger-schedule/src/plugin.ts:72`, `trigger-schedule/src/time-relative-plugin.ts:67`, `trigger-api/src/plugin.ts:62`. No kind stays accepted. **§3 Shape and door.** - One arm beside the existing one, same class and code. - `os validate` reaches it through `loadConfig`, which evaluates the author's `defineStack()` call (step 1), and not through its own stack parse (step 2). It is not a separate door. - Measured through the real CLI (`tsx bin/run-dev.js validate`) at this branch: `['triggers']` exits 1, `[]` exits 1, `['automation', 'triggers']` exits 0. **§4 Producer census** (expected 0 producers; 0 found; three test stacks): - `examples/**`: - `app-showcase` and `app-todo` declare both. `os validate` exits 0 on each at this branch. - `app-crm` declares `['ui', 'automation']` and has no auto-launched flow; `os validate` exits 0. - `app-multi-package` and `embed-objectql` declare no `requires`. - `packages/create-objectstack/src/templates/**`: `blank` declares `['automation']`. It is out of this arm's reach. - `os init` / `os g` on current `main` (PR objectstack-ai#20329 landed as `c5dcb3ba07`): `init` declares both. `os g flow` into a `['triggers']` project was the "cannot run" answer and is now a refusal (see below). - `packages/**` test stacks with `triggers` alone and an auto-launched flow: - `packages/cli/test/generate-object-namespace-prefix.test.ts`: converted to the pair. - `packages/qa/dogfood/test/fixtures/override-composite-fixture.ts`: converted. Its boot mounts both explicitly; `verify`'s harness does not read `requires`. - `packages/lint/src/authoring-rule-input-tier.test.ts:93`: **not converted.** `packages/lint` is fenced read-only for this dispatch. See "Owed, and fenced". - cloud `main`: six `requires` literals with `triggers` and no `automation`, all loader and publish-route token-list tests. No `defineStack`, no flows, so none is affected. - hotcrm: NOT MEASURED. It is not a repository in this container. **§5 The one-token sentence**, restated at every site that repeated it: - the `validateTriggerCapability` and `StackTriggerCapabilityRequiredError` docblocks; - `automation/flow-trigger-kind.ts`; - the error-code ledger comment; - the two spec test comments; - `content/docs/automation/flows.mdx` and `content/docs/permissions/capabilities.mdx`. `@objectstack/lint` carries no trigger-capability rule of its own; it shares only `resolveFlowTriggerKind`. There is no asymmetry to report. ## Pins (table-driven: requires × kind × status) `packages/spec/src/stack-requires.test.ts`, new block. Each refusal is asserted as its envelope (`code`, `status: 422`, one `issues` entry per flow) plus the prescription text: - `['triggers']` refused for `record_change`, `schedule`, `time_relative` and `api`; - `['automation', 'triggers']` accepted for all four, and in any order (the control); - `[]` and absent: the pair named, and `Add requires: ['triggers']` asserted absent; - `['automation']`: today's `issues` entry asserted with `toEqual`; - no auto-launched flow (none, a `screen` flow, a hand-launched `autolaunched` flow) unaffected; - `obsolete` / `invalid` unaffected, while `draft` / `active` are refused; - four flows give four issues; - each prescription, applied once, is accepted. **Ablation**, committed first and restored by blob hash: - **Leg A:** the old early exit. `7 failed | 23 passed`. - **Leg B:** the neither case given the old message. `3 failed | 27 passed`. - **Restored:** `30 passed`, blob `4c0bfced0f02` equal to HEAD. - Direction as predicted (turns red). **`os g` pin flipped** (`packages/cli/test/generate-stack-reach.test.ts`, landed with PR objectstack-ai#20329). `os g flow order_line` into a `['triggers']` project was pinned as "cannot run", exit 0. The written flow now stops the config from loading, so the write is refused. The pin is now: exit 1, the project tree byte-identical, the flow file absent, stdout names `does not include 'automation'` and prints `requires: ['automation', 'triggers']`, and no `Created`. Result: `7 passed`. ## Tests and gates The branch head is `94e32023d4`: a merge of `origin/main` `6704717188` made through `scripts/pm/os-regen-merge.sh`. It touched `error-code-ledger.zod.ts` on both sides, and both edits survive. Every reading below was taken at that head unless it names `45cfbaafee`, the last commit before the merge. The merge brought no change to any file those older readings depend on. - `@objectstack/spec` at `94e32023d4`: - `vitest run --project local`: **554 files, 16439 passed, 1 todo**. - `typecheck` exit 0. - `check:generated`: every artifact up to date, after a rebuild. - `@objectstack/cli` at `45cfbaafee`: - `generate-object-namespace-prefix` and `generate-scaffold-wiring` (unit): **52 passed**. - `generate-stack-reach` (integration): **7 passed**. - `@objectstack/lint`, the consumer suite, at `45cfbaafee`: **1 failed | 4303 passed**. The one failure is the fenced fixture described below. Its new message, re-read at `94e32023d4`, is the refusal it should be. This is expected, and owed. - dogfood: the fixture module loads (`requires = ["automation","triggers"]`). The boot pin itself is left to CI's Dogfood Regression Gate. - `os validate` on the examples at `45cfbaafee`: `app-todo`, `app-showcase` and `app-crm` each exit 0. - Gates: `dispatch-gates --commands` at `94e32023d4` derives 112. All 112 ran, each with its exit code recorded, and `--ran` reconciles them: **112 run, 0 NOT-MEASURED, 0 UNRUN**. - Exit 0: 111. This includes every `@objectstack/spec` `check:*` in the list (`api-surface`, `authorable-surface`, `docs`, `error-code-provenance`, `liveness`, `skill-examples`), `check:type-check-debt`, `check:dual-build-cjs-loads`, and the root `check:*` and `node scripts/*` set. - Exit 1, red by design: `check-empty-changeset`. See the next section. ## Deliberate correction of a pending release note `.changeset/20215-generate-scaffolds-reach-stack.md` (from PR objectstack-ai#20329, unreleased) says two things this PR makes false: - "Without `automation`, the server loads the flow and never runs it." - that `os g` reports **cannot run** for a flow in a stack whose `requires` lacks `automation`. Both sentences are corrected in place. `content/docs/deployment/cli.mdx` gets the same correction. `check-empty-changeset` stays red on this, by design ("DELIBERATE CORRECTION … say so on the PR and get it confirmed"). **This needs a person's confirmation.** Restoring the file from base would put the false sentences back into the next release. ## Owed, and fenced (not in this PR) These are outside the dispatch's fence, so this PR does not touch them: 1. `packages/lint/src/authoring-rule-input-tier.test.ts:93`: `requires: ['triggers']` becomes `['automation', 'triggers']`, and its comment names the pair. Until then `@objectstack/lint`'s suite is red on that one test. 2. `packages/cli/src/commands/init.ts` (the emitted config comment, `:649–652`, and the `SCAFFOLD_WIRED_REQUIRES` docblock) and `packages/cli/src/commands/generate.ts` (the emitted flow-file header, `:366–369`, and its docblock). Each says that without `automation` "the server loads the flow and never runs it". After this change the config stops loading. ## Acceptance notes - The `cannot run` branch of `os g`'s reach report has no scaffold that reaches it now. The flow scaffold's only tokens are the pair, and a stack missing either is refused. This is dead for today's generators. It is noted and not filed. Carrier: the next PR to touch `packages/cli/src/commands/generate.ts`. - `os validate` on a config that exports a plain object instead of calling `defineStack()` skips every `defineStack` cross-field refusal, this one included. Measured: a plain-object probe with `requires: ['triggers']` and a `record_change` flow exits 0 at this head. This is the whole refusal family's door, not this arm's, so it is reported to the seat in the dev report and not filed here. --- _Generated by [Claude Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
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