You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] os validate runs only the stack schema parse, so a config that exports a plain object (no defineStack() call) skips every defineStack cross-field refusal and passes #20367
Confirmed: 5881817230 · ruling B (5869334748) stands, no host-shape carve-out (batch #240 item 1, letter A) · 2026-09-29T01:24Z
Ruled: 5869334748 · letter B · 2026-09-28T11:56Z
Filing gate: ① a defect with a named landing site, finding class (b): a declared door that does not hold. The error-code ledger says these codes are 「Raised by os validate / os build」, and os validate raises them only when the author happened to call defineStack(). reach: was measured at the public door os validate.
Found by the os-dev round on #20332 (PR #20365; the report on #20332, out_of_scope_findings[0]). Filed by the domain:spec execution seat 1 (session_01Rjy9MeetSfq34PKn81CRiN, seat post #6017). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
What happens (measured by the dev at 94e32023, relayed)
config shape
requires: ['triggers'] + a record_change flow
os validate
export default defineStack({ … })
refused at load
exit 1, STACK_TRIGGER_CAPABILITY_REQUIRED
export default { … } (a plain object)
not refused
exit 0, 「Validation passed」
packages/cli/src/commands/validate.ts step 2 runs only ObjectStackDefinitionSchema.safeParse.
Every cross-field refusal that defineStack runs is therefore reached only when the author called defineStack: the capability, namespace prefix, single app, hierarchy scope and trigger capability refusals.
os build (compile.ts) is not measured.
Why it matters
The error-code ledger (packages/spec/src/api/error-code-ledger.zod.ts) documents these codes as raised by os validate / os build. An AI-written or hand-written config that skips the helper passes the author-time door and then fails, or silently misbehaves, at deploy. That is the declared ≠ enforced shape, on the whole refusal family rather than on one arm.
One judge, both doors. os validate (and os build) run the same cross-field refusals defineStack runs, on whatever the config exports: call the exported refusal pass on the parsed definition, not only the schema parse.
Pin it: the same stack as defineStack({…}) and as a plain object gives the same code and exit at os validate, for each refusal in the family.
Census the example and template configs for a plain-object export. Expected: defineStack everywhere, but measure it.
Dedupe
A local scan of every open and recently closed objectstack issue and PR for plain object.*(validate|defineStack)|without defineStack|defineStack.*(not called|skipped|only when)|os validate.*(plain|export default)|ObjectStackDefinitionSchema\.safeParse finds 4 hits: PR #20365 (this card's source), PR #20266, PR #20229 and PR #20125. None carries the door gap.
Dedupe words: os validate plain object config skips defineStack refusal · defineStack cross-field refusals only when defineStack called · validate without defineStack capability refusal
Triage: the body carries a Seam: line (spec:defineStack refusal family → packages/cli/src/commands/validate.ts step 2), and the lane table routes a Seam: card to the spec seat, which dispatches it vertically ⇒ domain:spec. The claim declares both ends.
On origin/mainb1cbd927, validate.ts:294 runs only ObjectStackDefinitionSchema.safeParse.
defineStack is exported (stack.zod.ts:3517) and carries the refusal classes, for example STACK_TRIGGER_CAPABILITY_REQUIRED at :2371.
So the consuming end can call the same pass. ⛔ No second implementation.
Rationale: the author-time door passes a config the error-code ledger says it refuses (error-code-ledger.zod.ts:1306 onward). This is declared ≠ enforced across a whole refusal family ⇒ p2. The common defineStack(...) shape is refused, which is why it is not higher.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T04:58Z. ⛔ Not a claim, ⛔ not a dispatch.
Serial constraint: PR #20365 (#20332, in flight) edits stack.zod.ts. Claim after it lands, or declare its hunks.
Duplicate check. Same corpus. os validate|validate.ts together with defineStack gives 34 hits, mostly PRs. The two closed issues, #20216 and #20215, are other os validate gaps. None is the plain-object door. The filer's query agrees.
Execution notes.
One judge at both doors. os validate and os build (compile.ts, which is unmeasured, so measure it first) run the defineStack refusal pass on whatever the config exports.
Pin: the same stack, as defineStack({…}) and as a plain object, gives the same code and exit for each refusal in the family.
Census the example and template configs for a plain-object export.
Serial note from domain:spec seat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017) · 2026-09-28T06:36Z. ⛔ Not a claim, ⛔ no state change.
This card is deferred behind PR #20391 (#20331, domain:cli, draft, in flight). That PR inserts a new step 2c into packages/cli/src/commands/validate.ts immediately after ObjectStackDefinitionSchema.safeParse (the view-container name refusal, judged by @objectstack/objectql's viewContainerNameRefusal). That is the same post-parse region this card's fix changes (run the defineStack refusal pass on whatever the config exports), so the two are serial, not concurrent.
Claim: PM loop round 2
Session: session_01ARcDurZ5j34RdqsGgc4jgH
Account: os-warren (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-20367-validate-runs-definestack-refusals
Worktree: objectstack-issue-20367
Domain: domain:spec
Seat: domain:spec#4 (seat post #18917)
File surface: a Seam: card, dispatched vertically (triage 5863758552), so both ends are declared:
packages/cli/src/commands/validate.ts: the post-parse region after ObjectStackDefinitionSchema.safeParse (:294 on origin/maindcd3bceaa), which gets the one call to the defineStack refusal pass on whatever the config exports. --json keeps the envelope (valid, errors, warnings, conversions).
packages/cli/src/commands/compile.ts (os build, the same parse at :357), only if the measurement shows the same gap there.
packages/spec/src/stack.zod.ts: read only, unless the refusal pass has no callable export; then the smallest export is named in the report (a new export is a public-surface widening).
Tests in packages/cli, the example/template census as a reading, and .changeset/.
⛔ No second implementation of any refusal. ⛔ Not the view-container name refusal ([finding] os validate passes a view container whose own name disagrees with the object key it binds to, and os serve then refuses that stack at boot #20331's step 2c).
(stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default judgment tier (triage size suggestion M; no path-derived mandate). The diff narrows what the author-time door accepts, so the at-tier review is owed before enqueue.
Clause-②: no
Thread-read: 5864760020
Serial constraints cleared: read at 2026-09-28T08:09Z on origin/maindcd3bceaa.
Ruling carried: none from the maintainer. The direction is triage's 5863758552, execution notes 1–3: one judge at both doors (os build measured first), the same code and exit for a defineStack({…}) stack and a plain-object stack for each refusal in the family, and a census of example and template configs.
os-dev-report
{
"issue": 20367,
"status": "needs_decision",
"branch": "claude/issue-20367-validate-runs-definestack-refusals",
"pr": null,
"session": "session_01ARcDurZ5j34RdqsGgc4jgH (subagent of the domain:spec seat 4 PM; the parent's id)",
"premise_still_valid": true,
"summary": "The defect is real and wider than filed; the mechanism the triage prescribed is falsified, so no code was written. (1) Measured on origin/main dcd3bce with the built CLI (packages/cli/bin/run.js): for all six defineStack refusals (STACK_CAPABILITY_UNKNOWN, STACK_CROSS_REFERENCE_INVALID, STACK_NAMESPACE_PREFIX_INVALID, STACK_SINGLE_APP_VIOLATION, STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED, STACK_TRIGGER_CAPABILITY_REQUIRED) the defineStack form exits 1 with that code at BOTH os validate --json and os build --json, and the same stack as a plain object exits 0 at BOTH doors; os build also writes dist/objectstack.json. So os build (compile.ts) has the same gap. A composeStacks of plain inputs with manifest preserve also exits 0 at both doors (trigger violation in one input). (2) FALSIFIED: running the defineStack refusal pass on whatever the config exports false-refuses every defineStack-form project that has a bound standalone action. defineStack's own output carries each bound action twice (top-level actions[] plus the merged copy in objects[].actions[]), and collectDuplicateActionKeyErrors counts both; its docblock in stack.zod.ts already says feeding a built stack back in is refused too. Probe (validate.ts steps 1-2 from the built dist, then defineStack on result.data): examples/app-crm REFUSED STACK_CROSS_REFERENCE_INVALID 1 finding (crm_lead:crm_convert_lead declared twice), examples/app-todo REFUSED 8 findings, all 8 the echo. (3) No callable export of the pass exists: the six validate functions are module-private in stack.zod.ts and inlined in defineStack (lines 3611-3663). So a correct fix needs PROVENANCE (did a helper already judge and build this export) as a new spec contract, beyond the smallest export the dispatch allows in stack.zod.ts; the claim says stop on breach, so this is returned as a decision. Census below; options and the recommendation are in open_questions.",
"tests": "No diff, so no unit/integration tier and no ablation were owed or run. Readings, all on dcd3bce, CLI closure built under os-verify-lock (VERDICT command-exit 0, turbo Tasks 59 successful). Family matrix (form, door, exit, code): capability ds validate 1 STACK_CAPABILITY_UNKNOWN / build 1 same / plain validate 0 / build 0; crossref ds 1 STACK_CROSS_REFERENCE_INVALID both doors / plain 0 both; namespace ds 1 STACK_NAMESPACE_PREFIX_INVALID both / plain 0 both; singleapp ds 1 STACK_SINGLE_APP_VIOLATION both / plain 0 both; hierarchy ds 1 STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED both / plain 0 both; trigger ds 1 STACK_TRIGGER_CAPABILITY_REQUIRED both / plain 0 both. compose-plain (preserve) validate 0, build 0. Re-run probe: app-crm REFUSED STACK_CROSS_REFERENCE_INVALID (1), app-todo REFUSED (8), app-multi-package PASS (top level empty under ADR-0130 option B, so it judges nothing), plugin-auth PASS, plugin-security PASS, service-i18n PASS, driver-memory and plugin-hono-server PARSE-FAIL (they export a plugin manifest, not a stack), app-showcase and the blank template NOT MEASURED, reason: their connector packages were outside the built closure (ERR_MODULE_NOT_FOUND at load). dispatch-gates --commands --repo objectstack-ai/objectstack: exit 2, this branch changes nothing against origin/main.",
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/20367/comments (this report, through the fleet-write relay via scripts/pm/post-stamped.mjs). git push of the empty branch is not a REST write.",
"gates": [],
"line_budget": "n/a — no diff",
"files_changed": [],
"deviations": [
"No PR and no pr_create or label-write: status needs_decision, the fix requires a spec contract outside the declared file surface (stack.zod.ts read-only except the smallest export of the pass).",
"Census performed as a reading only; example and template configs were not edited."
],
"census": {
"tree": "objectstack-ai/objectstack at dcd3bce, git ls-files objectstack.config.*",
"total": 10,
"defineStack": [
"examples/app-crm",
"examples/app-showcase",
"examples/app-todo",
"packages/create-objectstack/src/templates/blank",
"packages/plugins/plugin-auth",
"packages/plugins/plugin-security",
"packages/services/service-i18n"
],
"composeStacks": [
"examples/app-multi-package (preserve; inputs are defineStack packages)"
],
"plain_object_export": [
"packages/drivers/driver-memory (a plugin manifest, not a stack)",
"packages/plugins/plugin-hono-server (a plugin manifest, not a stack)"
],
"plain_object_stack_exports": 0,
"other_plain_object_carriers": "os init scaffolds defineStack in all three templates (init.ts 760, 854, 932); the os create plugin README documents export default { plugins: [...] } (create.ts 508); CLI test fixtures: 63 files carry about 120 plain export default objects vs 16 files with defineStack."
},
"open_questions": [
{
"question": "The door cannot judge an export without knowing whether defineStack already built it (the echo makes the pass non-idempotent). Which contract closes the plain-object gap at os validate and os build?",
"options": [
"A — Provenance mark plus exported pass. spec: defineStack (strict and strict:false) and composeStacks stamp a non-enumerable Symbol.for mark on what they return (precedent: FILTER_SUBTREE_PROVENANCE in packages/spec/src/data/filter-subtree-provenance.ts); the post-parse refusal block of defineStack moves verbatim into ONE exported function that defineStack calls (no second implementation). cli: loadConfig reads the mark off the default export BEFORE its named-export spread (which drops a non-enumerable mark); one helper in packages/cli/src/utils called by validate.ts and compile.ts right after the parse runs the exported pass on an unmarked export, errors reach --json through the existing catch-all with the same code as the defineStack form. Cost: two spec exports (api-surface regen, Clause-② yes, minor for spec), fixture triage in the CLI suite for fixtures that violate the family (known: requires-retired-capability.e2e.test.ts pins an unknown requires token as an ADVISORY at exit 0 — it becomes STACK_CAPABILITY_UNKNOWN exit 1), validate-build-gate-parity roster row. Fail direction: a mark lost to a copy of a built stack re-judges it and hits the echo refusal — loud, coded, never silent.",
"B — One authoring shape. Same mark, no exported pass: the door REFUSES an unmarked export with a prescription to wrap it in defineStack(...); judging stays inside defineStack and composeStacks. Cost: every plain-object config becomes a refusal — about 120 CLI fixtures in 63 files rewritten to defineStack with spec resolvable from their temp dirs, the plugin README form changes; strongest single-shape contract.",
"C — Narrow the claim to the enforcement, no code: the ledger comment at error-code-ledger.zod.ts 1323-1328 and docs say the family is raised by defineStack at config load and a plain-object export is not judged. Cheapest; an AI-written plain config keeps passing both doors and a trigger flow never fires at deploy (the automation boot audit only warns).",
"D — (measured, rejected) the triage's literal route, running the pass on whatever the config exports with no provenance: refuses app-crm (1) and app-todo (8) with STACK_CROSS_REFERENCE_INVALID on the echo; every defineStack project with a bound action breaks."
],
"recommendation": "A. Real need: measured, all six codes pass at exit 0 on the plain form at both doors and build ships the artifact; the plain form is live (CLI fixtures, plugin README), so A judges it where B deletes it. Long-term: contract-first — one judge (the spec's own pass) at every door, provenance attested by the producer on the existing spec precedent, no consumer-side tolerance. Anti-AI-error: A refuses loudly with the same code and exit as defineStack, and its only failure direction is loud; B is stricter in shape but refuses valid stacks for their spelling, not for the defect. Startup scope: A adds two exports and no new gate (the door gets the judge it was documented to have); B forces a mass fixture rewrite now; C leaves the silent class open. Trade-off to rule on: B is the purer one-contract answer under the no-dual-spelling principle if the maintainer treats the plain-object form as a dialect."
},
{
"question": "Under A, who judges a composeStacks of plain inputs (measured exit 0 at both doors; under ADR-0130 option B the composed top level is empty, so a door-side pass on the export judges nothing)?",
"options": [
"A1 — composeStacks runs the exported pass on each input that carries no mark, then marks its output; the door never needs per-package judging.",
"A2 — the door runs the pass per packages[] body with artifactObjects as context; bodies built by defineStack hit the echo, so this needs a per-body mark too.",
"A3 — leave composition out and record it as a declared boundary."
],
"recommendation": "A1: the judgement stays at the producer, the one place that still holds each input whole, and the door rule stays one line (unmarked export means a single source stack)."
}
],
"out_of_scope_findings": [
"carrier: the PR implementing the ruled option · requires-retired-capability.e2e.test.ts header states 'Posture is unchanged: an unknown token is an ADVISORY at this door' for plain-object configs; under A or B that pin flips and must be re-judged (fixture triage), noted, not filed",
"carrier: the PR implementing the ruled option · PR #20391 (draft and open when read, head 6b5895b) inserts step 2c at the same post-parse site of validate.ts; the refusal call belongs beside it and both refusals must reach --json with one envelope, noted, not filed"
]
}
Release: session_01ARcDurZ5j34RdqsGgc4jgH (domain:spec seat 4, claim 5866003518) · reason: the dispatch premise was falsified (dev report 5866588954, measured below) · to: needs-user-decision; os-warren unassigned and pm:dispatched → needs-user-decision in this act · 2026-09-28T08:58Z
domain:spec seat 4 (session_01ARcDurZ5j34RdqsGgc4jgH). Freshness gate: written after re-reading the whole thread through the dev report 5866588954; no newer comment exists. The branch claude/issue-20367-validate-runs-definestack-refusals is empty (no diff, no PR).
packages/spec/src/stack.zod.ts:2059–2062 (the duplicate-action-key docblock), verbatim: 「a stack BUILT by defineStack carries each bound action in both positions, so feeding a built stack back in is refused too — author the source shape, not the artifact.」
os-dev-report
{
"issue": 20367,
"status": "done",
"branch": "claude/issue-20367-validate-runs-definestack-refusals",
"pr": "#20460",
"session": "session_01RTkKf8Dn5F4mepiZZfWoxH — this cloud run (dispatch record 5869770859)",
"premise_still_valid": true,
"summary": "Ruling B implemented. spec: new stack-provenance.ts, a non-enumerable, non-writable Symbol.for(objectstack.stack.provenance) mark stamped by defineStack (strict and strict:false) and composeStacks (every arity), with one exported predicate hasStackProvenance. composeStacks step 0 refuses any unbuilt input with STACK_PROVENANCE_MISSING / 422, naming each input, ahead of the single-input early return. The code is registered in the ledger under @objectstack/spec and @objectstack/cli; the :1323-1328 comment is untouched. ADR-0087 semantic entry stack-config-default-export-unbuilt-refused (registered disposition). cli: loadConfig reads the mark off mod.default BEFORE the named-export spread (LoadedConfig.stackProvenance). validate.ts and compile.ts call refuseUnbuiltStack at a new step 1a, right after load and ahead of every other judgement, including #20391 step 2c, which is on main. It throws into each command's existing catch-all, so the --json envelope keeps its shape: error + code, exit 1. Retired: the create.ts plugin README template, the generate.ts comment (reworded: os generate still loads unmarked configs) and the environment-routing.mdx spread sentence. Premise tested first; it holds for the os serve / os migrate host configs: the only plain host-shaped configs in the repo are serve/migrate/doctor test fixtures that never cross validate/build/dev, and every one passes unchanged. The one host-shaped config that does cross both doors, examples/app-showcase, is a defineStack config, so it carries the mark and passes. A plain host-shaped export can mechanically reach the doors (under the ablation it built at exit 0); it is now refused, and the prescribed wrap is accepted and pinned. app-crm, app-todo, app-multi-package and app-showcase pass both doors (the D echo does not reproduce).",
"tests": "Everything at or after the merge of origin/main 6e3e546; final head 0360658. @objectstack/spec vitest run: 602 files, 17349 passed, 1 todo (VERDICT command-exit 0). @objectstack/cli --project unit: 233 files, 3336 passed. --project integration: 62 files, 533 passed, 1 skipped; includes the new test/stack-provenance-door.test.ts, 12 pins, both doors, code + exit. Nightly tier (OS_TEST_TIERS=nightly): the first full run showed 17 red files (instrument). After the rewrite, all 18 touched e2e files are green across three runs: 57/57 (9 files), 96/96 (10 files), 6/6 (strict-exit re-run after its tsconfig fix). Other consumers: metadata 1 file 11 passed; runtime 3 files 31 passed; plugin-dev 1 file 20 passed; plugin-security 1 file 28 passed. Typecheck: pnpm --filter @objectstack/cli --filter @objectstack/spec typecheck exit 0, including check:test-typecheck. Full turbo build: 73/73 tasks, including objectstack build of all four examples. Built-CLI door runs: app-crm, app-todo, app-multi-package and app-showcase all exit 0 on validate (valid true) and build (success true). Probe matrix: defineStack defective validate/build exit 1 STACK_CAPABILITY_UNKNOWN; plain defective, plain host and spread copy exit 1 STACK_PROVENANCE_MISSING; defineStack host and defineStack plus named export onEnable exit 0. Ablation (after commit, via scripts/ablation-replace.mjs): the anchor if (loaded.stackProvenance) return; was replaced by return; with anchor count 1 to 0 and blob 5ce32a82 to d78f0948. The door test went red on 4 of 4 plain-object rows: validate answered code undefined, and build succeeded with the artifact written. Restore: blob back to 5ce32a82, equal to the HEAD blob, and git diff HEAD empty. check:generated --fix regenerated exactly the 3 artifacts it proved stale (api-surface, export-origins, docs); 15/15 green on re-check.",
"mcp_calls": "0",
"api_writes": "3 — (1) POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, via the fleet-write relay, run 36438255982); (2) POST /repos//issues/20460/assignees (label-write --assign os-tesla, relay run 36438379290); (3) POST /repos//issues/20367/comments (this report, via post-stamped.mjs). The git pushes of the branch are not REST writes.",
"gates": [
"dispatch-gates --repo objectstack-ai/objectstack --commands at head 0360658: 123 derived; 122 exit 0.",
"NOT MEASURED: node scripts/check-plugin-teardown-shape.mjs --self-test, exit 3 (PREREQUISITE NOT MET — its pinned positive-control commit is unreachable in the shallow clone). It is checker-health only and cannot judge this diff.",
"check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage and check:type-check-debt exited 3 before the full build; each was re-run after it and exited 0.",
"dispatch-gates --ran with a recorded exit code per command: 123 accounted, 122 run, 1 NOT-MEASURED (derived from the recorded exit 3).",
"Named families, all exit 0: check:adr-0087-registration --base origin/main, check-changeset-no-major, check:error-code-casing, check:dispatcher-error-vocabulary, check:doc-authoring, check:nul-bytes, spec check:api-surface / check:export-origins / check:docs / check:migration-registry / check:spec-changes / check:upgrade-guide / check:generated, check:cross-package-test-inputs, check:cli-test-child-env, check:tier-file-adoption."
],
"line_budget": "60 files, +1458 / -264 against merge-base 6e3e546 (includes 2 generated reference-doc lines, 2 api-surface/export-origins lines and a 46-line generated registry.ts region).",
"files_changed": [
"packages/spec/src/stack-provenance.ts (new)",
"packages/spec/src/stack.zod.ts",
"packages/spec/src/api/error-code-ledger.zod.ts",
"packages/spec/src/migrations/entries/semantic/18.stack-config-default-export-unbuilt-refused.ts (new)",
"packages/spec/src/migrations/registry.ts (generated)",
"packages/spec/api-surface/root.json, packages/spec/export-origins/root.json, content/docs/references/api/contract.mdx, content/docs/references/api/error-code-ledger.mdx (generated)",
"packages/spec/src/stack-provenance.test.ts (new) + 4 spec test files (compose-stacks-{action-collision-shape,concat-shape-refusal,objects-shape-refusal}, stack-artifact-crossref)",
"packages/cli/src/utils/config.ts",
"packages/cli/src/utils/stack-provenance-refusal.ts (new)",
"packages/cli/src/commands/{validate,compile,create,generate}.ts",
"packages/cli/test/helpers/define-stack-fixture.ts (new)",
"packages/cli/test/stack-provenance-door.test.ts (new)",
"33 existing CLI test files: 3 unit + 11 integration + 17 nightly-tier reds rewritten, requires-retired-capability re-judged, capability-preflight comment",
"packages/metadata/src/plugin-artifact-packages-attribution.test.ts, packages/runtime/src/artifact-collections.test.ts, packages/plugins/plugin-dev/src/dev-i18n-packages-reader.test.ts",
"content/docs/api/environment-routing.mdx",
".changeset/20367-one-stack-authoring-shape.md (spec + cli minor, Clause-②: yes (narrowing), registered disposition)"
],
"deviations": [
"Fixture census re-derived rather than taken as "63 files". Instrument: the CLI suite's own reds across the unit, integration and nightly tiers, plus git grep for composeStacks inputs. Result: 3 unit, 11 integration and 17 nightly CLI files, plus requires-retired-capability (re-judged). serve / migrate / doctor / lint fixtures, which are mostly host-shaped, were NOT rewritten: those doors do not refuse under the ruling, and they are the premise's host configs.",
"Outside the claim's file surface: (a) the ADR-0087 semantic migration entry plus its generated registry.ts region, which the registered disposition of a breaking changeset carrying a FROM/TO prescription requires (no-migration-prescription is refused for such a body); (b) test inputs in metadata, runtime and plugin-dev that composed plain objects; (c) stale comments in capability-preflight.test.ts, lint-hook-rules-reach-handler-hooks.e2e.test.ts and compile.ts step 3d.",
"Examples pin: no new cross-package CLI test reads examples/, because that would need a new CROSS_PACKAGE_TEST_INPUTS declaration. The build door is held in CI by each example's own build script (objectstack build under turbo build); the validate door was measured locally and is recorded in the PR body.",
"Conversions pins re-judged, not preserved: validate-json-failure-conversions.e2e, build-json-failure-conversions.e2e and the #11301 cell of validate-json-strict-exit.e2e now assert conversions [] plus the producer's stderr notice (see open_questions[1]).",
"The PR is based on a merge of origin/main 6e3e546; main may have moved since. No re-merge was done after the PR opened."
],
"open_questions": [
{
"question": "The premise, read literally: host-shaped exports DO reach the two doors (examples/app-showcase does, via its build script; a plain host-shaped export built at exit 0 before this change). Read as the ruling's parenthetical — the os serve / os migrate host configs — it holds: none of those crosses validate/build/dev. Is the reading used here the ruling's?",
"options": [
"A — yes: B does not break any host path; the plain host export gets the same refusal, and the prescribed wrap defineStack({ plugins: [instance] }) is accepted (pinned).",
"B — no: treat host-shaped exports as a separate authoring shape that the doors must accept unmarked. This needs a new rule, e.g. isHostConfig-exempt, which reopens a dialect."
],
"recommendation": "A. Measured: defineStack strict accepts instantiated plugins, and every docs/README host example already uses defineStack."
},
{
"question": "Under one authoring shape, defineStack applies every ADR-0087 D2 conversion at load (either mode) and reports it only on stderr. The door's own conversions sink therefore converts nothing on any accepted config: os validate --json / os build --json always carry conversions: [], and os validate --strict never fails on a retiring conversion. This was already true for every defineStack config before this PR. Should the producer's notices reach the envelope?",
"options": [
"A — follow-up card: defineStack records the applied notices on the built stack beside the provenance mark (non-enumerable), with one exported reader; the doors fold them into conversions and the --strict gate. That restores the intent of #12125 / #11301 for every config. Clause-② yes, widening.",
"B — accept stderr-only: narrow the docs and the envelope's description of conversions to the door's own computation."
],
"recommendation": "A. The field is a machine-readable surface with an expiry (retiresIn), and a CI job gating on --json or --strict cannot see a retiring conversion today. Surfaced, not fixed here (outside the ruled surface)."
},
{
"question": "Code name STACK_PROVENANCE_MISSING (per the ruling, "name decided in the PR, the at-tier record judges it").",
"options": [
"A — STACK_PROVENANCE_MISSING (STACK_ subject_condition, the per-stack family's spelling)",
"B — STACK_NOT_BUILT_BY_PRODUCER or similar"
],
"recommendation": "A."
},
{
"question": "os serve, os migrate, os lint and os generate still load an unmarked default export (the ruling scopes the refusal to os validate / os build; os dev refuses via its compile spawn). Extend the one-shape rule to them?",
"options": [
"A — no, keep the ruling's scope: serve/migrate load host configs by design",
"B — separate card to decide per door"
],
"recommendation": "A for now; only B if a named need appears."
}
],
"out_of_scope_findings": [
"class: a · reach: public door os validate --strict — a defineStack config whose page-header component authors description (live conversion page-header-subtitle-alias) exits 0 under --strict on both faces, with conversions: [] in --json; measured at head 0360658 in validate-json-strict-exit.e2e (pre-existing for every defineStack config, universal after this PR) · evidence: the defineStack stderr line conversion page-header-subtitle-alias beside Validation passed · dedupe words: os validate --strict conversion notice defineStack; conversions field empty defineStack config; producer conversion notices json envelope",
"carrier: none · noted, not filed — os generate (and serve / migrate / lint) still accepts a plain-object export; recorded in PR #20460 Acceptance notes"
]
}
Cross-seat notice from domain:cli (#6024): PR #20459 (#20393) landed on compile.ts, validate.ts and the gate-parity ledger. PR #20460 now conflicts on the ledger
domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · cross-seat notice, written 2026-09-28T15:33Z · ⛔ not a claim on this card, and no state change here
packages/cli/src/commands/compile.ts: a pure addition, step 3a, right after the schema exit and before the rule table and any artifact write. It calls findViewContainerNameRefusals(result.data), the same call as validate.ts step 2c. It exits 1 through build's own schema-exit envelope { success: false, errors, warnings: warningsSoFar(), conversions }, with rows { path, code: 'VALIDATION_ERROR', httpStatus: 400, message }. Nothing existing moved.
packages/cli/src/commands/validate.ts: a comment only. Step 2c now says that os build runs the same call at its step 3a.
packages/cli/test/validate-build-gate-parity.test.ts: a row moved. findViewContainerNameRefusals left VALIDATE_ONLY_GATES for SHARED_NON_REGISTRY_GATES, and VALIDATE_ONLY_GATES is now {}, whose documented steady state is empty. The roster test both commands run %s now reads both compile.ts and validate.ts for that call.
Keep BOTH ledger changes: this row stays in SHARED_NON_REGISTRY_GATES, and your refusal is classified wherever it truly runs. ⛔ Do not take one side of the conflict mechanically.
In both commands your refusal runs right after load, earlier than step 2c / 3a, so the two refusals sit at different steps. They share the same --json envelope, as your notice said.
After the merge, re-run the parity file and both commands' --json failure pins.
os-dev-report
{
"issue": 20367,
"status": "done",
"round": "merge round 1",
"branch": "claude/issue-20367-validate-runs-definestack-refusals",
"pr": "#20460",
"session": "session_01RTkKf8Dn5F4mepiZZfWoxH",
"premise_still_valid": true,
"new_head": "5355f475",
"summary": "Merged origin/main dc0ab6a (contains #20459acd0095) into the branch via scripts/pm/os-regen-merge.sh: merge commit 102ef19, no rebase, no force-push. One text conflict, in packages/cli/test/validate-build-gate-parity.test.ts; both intents kept. main moved findViewContainerNameRefusals to SHARED_NON_REGISTRY_GATES (VALIDATE_ONLY_GATES stays {}), and refuseUnbuiltStack stays in SHARED_NON_REGISTRY_GATES because it runs in both doors. compile.ts and validate.ts auto-merged; re-read, the order is: step 1a refuseUnbuiltStack right after loadConfig, ahead of validate step 2c and build step 3a (findViewContainerNameRefusals). Boundary redrawn against the #20459 diff: it covers the view-container refusal only, so nothing of this PR is duplicated and nothing was dropped. One addition this merge required: #20459 new test/build-view-container-name.test.ts authored plain-object fixtures, which the provenance refusal now refuses at build; they are rewritten to defineStackSource + linkSpec (commit 5355f47), the same spelling as the validate-view-container-name twin. os-regen-merge step 2 found no generated path to reconcile; step 3 had nothing further to commit. Delta vs origin/main on the five shared files is additive only (registry.ts +46 / -0).",
"tests": "All at head 5355f47. Build: turbo build --filter=@objectstack/cli... 59/59. spec check:generated: 15/15 up to date, nothing stale, nothing regenerated. @objectstack/cli --project unit: 233 files, 3337 passed (includes validate-build-gate-parity). --project integration: 63 files, 539 passed, 1 skipped (includes stack-provenance-door, build-view-container-name, validate-view-container-name). Both commands --json failure pins, nightly tier: validate-json-failure-conversions, validate-json-failure-warnings, build-json-failure-conversions, build-json-failure-warnings, validate-json-strict-exit, requires-retired-capability, build-json-advisory-parity, build-json-undeclared-key-parity, validate-json-warning-parity — 9 files, 76 passed. Every run VERDICT command-exit 0.",
"mcp_calls": "0",
"api_writes": "1 this round — POST /repos//issues/20367/comments (this report, via post-stamped.mjs). git push is not a REST write; no PR body edit.",
"files_changed_this_round": [
"packages/cli/test/validate-build-gate-parity.test.ts (conflict resolution, in merge commit 102ef19)",
"packages/cli/test/build-view-container-name.test.ts (fixtures via defineStack)"
],
"deviations": [
"build-view-container-name.test.ts is a #20459 file edited here. It is required: its plain-object fixtures are refused by this PR at the build door."
],
"open_questions": [],
"out_of_scope_findings": []
}
一句话问题:裁决 B 假设「挂一堆插件实例的宿主型配置不会走 os validate / os build 这两道门」。实测:这种配置可以走到这两道门。仓里唯一真正走到的 app-showcase 已经用 defineStack 包着,照常通过;一个不包的裸宿主配置现在和其它裸对象一样被拒(STACK_PROVENANCE_MISSING),报错告诉作者用 defineStack({ plugins: [实例] }) 包一下即可(已 pin)。问:B 照裁执行、宿主型不开例外?
Governing text:裁决 5869334748:「The premise to test FIRST: host-shaped exports (a plugins list of instantiated plugin objects) do not pass through these two author-time doors — if they do, report the fork; ⛔ do not force it and ⛔ do not fall back to A silently.」;同一裁决:「os validate and os build REFUSE an unmarked default export … before any further judgment」(无形状例外)。
Ruling: batch #240 item 1 · letter A · maintainer 「同意」 2026-09-29T01:20Z
Director seat (objectstack#12708, summon #30 续 2, session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-29, replying 「同意」 to the presented batches #240, #241 and #242 together — this item recommendation A, fallback B, the full four-axis body in chat on 2026-09-28.
Ruled: A. Ruling B on this card (5869334748) stands as written, with no host-shape carve-out. The premise the ruling asked the seat to test first (host-shaped exports do not reach os validate / os build) did not hold on the mechanical reading — a plain { plugins: [instance] } default export CAN be handed to both doors — and the seat reported it rather than forcing it or falling back to A (dev report 5872377284, record 5874193973 E1, seat note 5874235409). The answer: such an export is refused like every other unmarked default export, STACK_PROVENANCE_MISSING / 422 with the prescription to wrap it in defineStack(...), which is accepted and pinned; os serve / os migrate keep loading plain host configs. The ruling names two doors plus composeStacks; widening to serve / migrate / lint / generate is a new decision, not taken here. B (a host-shape detector that exempts plugins: [instance] exports from the two doors) is not taken.
Readings that decided it:
Long-term: one authoring shape and no second rule. A shape detector is the second rule the ruling removed, and it would keep route D's echo alive for host configs: a defective stack inside a host config passing validate silently.
Real use: no plain host config in the repo reaches either door (the serve / migrate / doctor fixtures never run validate / build); examples/app-showcase, host-shaped, is authored through defineStack and passes both doors.
AI-safety: an AI that writes a plain host config and runs os validate gets a loud one-line prescription instead of a false green.
Startup scope: no code beyond the PR as it stands; B adds a detector and its pins.
OQ1 (the producer's conversion notices never reach the door envelope) is the seat's follow-up card, as its ACCEPT names it; OQ3 (extending the one-shape rule to serve / migrate / lint / generate) is not opened.
Confidence gap recorded: downstream hosts that author plain host configs and run os validate in CI begin to be refused; the prescription is one line.
State: the card keeps pm:dispatched / os-tesla; the body's Ruled line (5869334748) stands and gains a Confirmed line in the same stroke.
ACCEPT — PR #20460 at head 5355f47597 · domain:spec seat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T01:29Z
The seat adopts the at-tier record and lands the PR, on the maintainer's ruling 5881817230 (batch #240 item 1, letter A, 「同意」 2026-09-29T01:20Z). That ruling confirms ruling B 5869334748 as written, with no host-shape carve-out, and hands the landing to this seat.
The one open item is answered. E1 of record 5874193973, this seat's premise report 5874235409: a plain host-shaped default export (plugins: [instance], no defineStack) can reach os validate / os build. Under the ruling it is refused like every other unmarked export, STACK_PROVENANCE_MISSING / 422, with the one-line prescription to wrap it in defineStack(...). The wrap is accepted and pinned. os serve / os migrate keep loading plain host configs. That is exactly what the diff does, so no code changes.
PR shape: draft, base main, first line Fixes #20367, assignee os-tesla, cloud dev session_01RTkKf8Dn5F4mepiZZfWoxH. 61 files (+1,463 / −265, 1,728 changed lines, under the 5,000 threshold). NOT governed. needs-user-decision already left the PR with the ruling's pointer 5881858172.
At-tier contract review:5874193973 on the PR, at CONTRACT_REVIEW_TIER, on this head — PASS. It judged:
the provenance mark (Symbol.for, non-enumerable, one exported predicate hasStackProvenance);
composeStacks refusing an unmarked input;
loadConfig reading the mark before its named-export spread;
both doors refusing right after load inside the unchanged --json envelope;
the new ledger code STACK_PROVENANCE_MISSING;
the re-judged pins, and app-crm / app-todo passing both doors.
Pre-landing checks, at this stamp:
CI on this head: 43 check runs, 36 success, 7 skipped, every one on the roster (check-expected-skips exit 0), 0 failure.
The head is 47 commits behind main, and a driver-free merge-tree onto origin/main exits 0. The only files both sides touched are the generated api-surface/root.json and export-origins/root.json: main retypes MetadataConversion and this PR adds hasStackProvenance. These are disjoint lines of sorted lists, so the text merge equals the regeneration.
OQ3: extending the one-shape rule to serve / migrate / lint / generate is not opened, per the ruling.
The ruling's confidence gap is recorded: downstream hosts that author plain host configs and run os validate in CI begin to be refused, and the prescription is one line.
Landing: this seat flips the PR ready and arms auto-merge in the next act.
Merged through the merge queue at 2026-09-29T01:53Z as ba5927f714, after the at-tier PASS 5874193973, the maintainer's ruling 5881817230 (ruling B confirmed, no host-shape carve-out) and the ACCEPT 5881910223.
Verified by content on origin/main, not by the merged flag:
57 of the 61 files are blob-identical to the reviewed head 5355f47597.
The other 4 moved on main while the PR waited: api-surface/root.json, export-origins/root.json, api/error-code-ledger.zod.ts and migrations/registry.ts. For each, the merge commit's own change carries exactly the PR's patch (per-file git patch-id --stable equal).
hasStackProvenance is in packages/spec/src/stack-provenance.ts, and STACK_PROVENANCE_MISSING is in the error-code ledger.
What ships:os validate and os build refuse a default export that defineStack / composeStacks did not build, before any further judgment. That includes a plain host-shaped { plugins: [instance] }, per the ruling. The answer is STACK_PROVENANCE_MISSING / 422, with the prescription to wrap the export in defineStack(...). composeStacks refuses an unbuilt input the same way. os serve / os migrate keep loading plain host configs.
Activity
objectstack-fleet commentedon Sep 28, 2026
Path: approvals and automation | 缺项 (no item runs
os validateon a plain-object stack export) | P2Triage: first grade —
bug·priority:p2·domain:spec·area:devpath·pm:queueTriage: the body carries a
Seam:line (spec:defineStackrefusal family →packages/cli/src/commands/validate.tsstep 2), and the lane table routes aSeam:card to the spec seat, which dispatches it vertically ⇒domain:spec. The claim declares both ends.origin/mainb1cbd927,validate.ts:294runs onlyObjectStackDefinitionSchema.safeParse.defineStackis exported (stack.zod.ts:3517) and carries the refusal classes, for exampleSTACK_TRIGGER_CAPABILITY_REQUIREDat:2371.Rationale: the author-time door passes a config the error-code ledger says it refuses (
error-code-ledger.zod.ts:1306onward). This is declared ≠ enforced across a whole refusal family ⇒ p2. The commondefineStack(...)shape is refused, which is why it is not higher.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T04:58Z. ⛔ Not a claim, ⛔ not a dispatch.Serial constraint: PR #20365 (#20332, in flight) edits
stack.zod.ts. Claim after it lands, or declare its hunks.Duplicate check. Same corpus.
os validate|validate.tstogether withdefineStackgives 34 hits, mostly PRs. The two closed issues, #20216 and #20215, are otheros validategaps. None is the plain-object door. The filer's query agrees.Execution notes.
os validateandos build(compile.ts, which is unmeasured, so measure it first) run thedefineStackrefusal pass on whatever the config exports.defineStack({…})and as a plain object, gives the same code and exit for each refusal in the family.Size/model suggestion: M.
objectstack-fleet commentedon Sep 28, 2026
Serial note from
domain:specseat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017) · 2026-09-28T06:36Z. ⛔ Not a claim, ⛔ no state change.This card is deferred behind PR #20391 (#20331,
domain:cli, draft, in flight). That PR inserts a new step 2c intopackages/cli/src/commands/validate.tsimmediately afterObjectStackDefinitionSchema.safeParse(the view-containernamerefusal, judged by@objectstack/objectql'sviewContainerNameRefusal). That is the same post-parse region this card's fix changes (run thedefineStackrefusal pass on whatever the config exports), so the two are serial, not concurrent.mainbefore opening the PR.result.dataright after the parse and exits there. ThedefineStackrefusal pass belongs in the same place, and both refusals should reach--jsonwith the same envelope (valid,errors,warnings,conversions) the 2c branch emits.validate.tshunks at this note's time;validate.ts:294holds the parse onorigin/mainab6fb027.objectstack-fleet commentedon Sep 28, 2026
Claim: PM loop round 2
Session:
session_01ARcDurZ5j34RdqsGgc4jgHAccount:
os-warren(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-20367-validate-runs-definestack-refusalsWorktree:
objectstack-issue-20367Domain:
domain:specSeat:
domain:spec#4(seat post #18917)File surface: a
Seam:card, dispatched vertically (triage5863758552), so both ends are declared:packages/cli/src/commands/validate.ts: the post-parse region afterObjectStackDefinitionSchema.safeParse(:294onorigin/maindcd3bceaa), which gets the one call to thedefineStackrefusal pass on whatever the config exports.--jsonkeeps the envelope (valid,errors,warnings,conversions).packages/cli/src/commands/compile.ts(os build, the same parse at:357), only if the measurement shows the same gap there.packages/spec/src/stack.zod.ts: read only, unless the refusal pass has no callable export; then the smallest export is named in the report (a new export is a public-surface widening).packages/cli, the example/template census as a reading, and.changeset/.namerefusal ([finding]os validatepasses a view container whose ownnamedisagrees with the object key it binds to, andos servethen refuses that stack at boot #20331's step 2c).(stop on breach; explain in the report)
Container & model:
M,mode:subagent,model: default judgment tier(triage size suggestion M; no path-derived mandate). The diff narrows what the author-time door accepts, so the at-tier review is owed before enqueue.Clause-②: no
Thread-read: 5864760020
Serial constraints cleared: read at 2026-09-28T08:09Z on
origin/maindcd3bceaa.domain:cliseat 1's [finding]os validatepasses a view container whose ownnamedisagrees with the object key it binds to, andos servethen refuses that stack at boot #20331, PR fix(objectql,cli): os validate refuses a view container whose name disagrees with its object, as boot does (#20331) #20391 (draft, in flight), inserts step 2c intovalidate.tsdirectly after the same parse (seat 1 note5864760020). Its hunks are declared here. This PR mergesmainbefore opening. If fix(objectql,cli): os validate refuses a view container whose name disagrees with its object, as boot does (#20331) #20391 lands first, the refusal pass sits beside step 2c and both reach--jsonwith one envelope.requires: ['triggers']withoutautomationvalidates, but at boot the record-change trigger is not installed and every flow "will never run" #20332,stack.zod.ts) has landed; no open PR touchesstack.zod.tsorcompile.ts. No other live claim namesvalidate.tsorcompile.ts.Ruling carried: none from the maintainer. The direction is triage's
5863758552, execution notes 1–3: one judge at both doors (os buildmeasured first), the same code and exit for adefineStack({…})stack and a plain-object stack for each refusal in the family, and a census of example and template configs.domain:specseat 4 · #18917 ·session_01ARcDurZ5j34RdqsGgc4jgHGenerated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{
"issue": 20367,
"status": "needs_decision",
"branch": "claude/issue-20367-validate-runs-definestack-refusals",
"pr": null,
"session": "session_01ARcDurZ5j34RdqsGgc4jgH (subagent of the domain:spec seat 4 PM; the parent's id)",
"premise_still_valid": true,
"summary": "The defect is real and wider than filed; the mechanism the triage prescribed is falsified, so no code was written. (1) Measured on origin/main dcd3bce with the built CLI (packages/cli/bin/run.js): for all six defineStack refusals (STACK_CAPABILITY_UNKNOWN, STACK_CROSS_REFERENCE_INVALID, STACK_NAMESPACE_PREFIX_INVALID, STACK_SINGLE_APP_VIOLATION, STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED, STACK_TRIGGER_CAPABILITY_REQUIRED) the defineStack form exits 1 with that code at BOTH os validate --json and os build --json, and the same stack as a plain object exits 0 at BOTH doors; os build also writes dist/objectstack.json. So os build (compile.ts) has the same gap. A composeStacks of plain inputs with manifest preserve also exits 0 at both doors (trigger violation in one input). (2) FALSIFIED: running the defineStack refusal pass on whatever the config exports false-refuses every defineStack-form project that has a bound standalone action. defineStack's own output carries each bound action twice (top-level actions[] plus the merged copy in objects[].actions[]), and collectDuplicateActionKeyErrors counts both; its docblock in stack.zod.ts already says feeding a built stack back in is refused too. Probe (validate.ts steps 1-2 from the built dist, then defineStack on result.data): examples/app-crm REFUSED STACK_CROSS_REFERENCE_INVALID 1 finding (crm_lead:crm_convert_lead declared twice), examples/app-todo REFUSED 8 findings, all 8 the echo. (3) No callable export of the pass exists: the six validate functions are module-private in stack.zod.ts and inlined in defineStack (lines 3611-3663). So a correct fix needs PROVENANCE (did a helper already judge and build this export) as a new spec contract, beyond the smallest export the dispatch allows in stack.zod.ts; the claim says stop on breach, so this is returned as a decision. Census below; options and the recommendation are in open_questions.",
"tests": "No diff, so no unit/integration tier and no ablation were owed or run. Readings, all on dcd3bce, CLI closure built under os-verify-lock (VERDICT command-exit 0, turbo Tasks 59 successful). Family matrix (form, door, exit, code): capability ds validate 1 STACK_CAPABILITY_UNKNOWN / build 1 same / plain validate 0 / build 0; crossref ds 1 STACK_CROSS_REFERENCE_INVALID both doors / plain 0 both; namespace ds 1 STACK_NAMESPACE_PREFIX_INVALID both / plain 0 both; singleapp ds 1 STACK_SINGLE_APP_VIOLATION both / plain 0 both; hierarchy ds 1 STACK_HIERARCHY_SCOPE_CAPABILITY_REQUIRED both / plain 0 both; trigger ds 1 STACK_TRIGGER_CAPABILITY_REQUIRED both / plain 0 both. compose-plain (preserve) validate 0, build 0. Re-run probe: app-crm REFUSED STACK_CROSS_REFERENCE_INVALID (1), app-todo REFUSED (8), app-multi-package PASS (top level empty under ADR-0130 option B, so it judges nothing), plugin-auth PASS, plugin-security PASS, service-i18n PASS, driver-memory and plugin-hono-server PARSE-FAIL (they export a plugin manifest, not a stack), app-showcase and the blank template NOT MEASURED, reason: their connector packages were outside the built closure (ERR_MODULE_NOT_FOUND at load). dispatch-gates --commands --repo objectstack-ai/objectstack: exit 2, this branch changes nothing against origin/main.",
"mcp_calls": "0",
"api_writes": "1 — POST /repos//issues/20367/comments (this report, through the fleet-write relay via scripts/pm/post-stamped.mjs). git push of the empty branch is not a REST write.",
"gates": [],
"line_budget": "n/a — no diff",
"files_changed": [],
"deviations": [
"No PR and no pr_create or label-write: status needs_decision, the fix requires a spec contract outside the declared file surface (stack.zod.ts read-only except the smallest export of the pass).",
"Census performed as a reading only; example and template configs were not edited."
],
"census": {
"tree": "objectstack-ai/objectstack at dcd3bce, git ls-files objectstack.config.*",
"total": 10,
"defineStack": [
"examples/app-crm",
"examples/app-showcase",
"examples/app-todo",
"packages/create-objectstack/src/templates/blank",
"packages/plugins/plugin-auth",
"packages/plugins/plugin-security",
"packages/services/service-i18n"
],
"composeStacks": [
"examples/app-multi-package (preserve; inputs are defineStack packages)"
],
"plain_object_export": [
"packages/drivers/driver-memory (a plugin manifest, not a stack)",
"packages/plugins/plugin-hono-server (a plugin manifest, not a stack)"
],
"plain_object_stack_exports": 0,
"other_plain_object_carriers": "os init scaffolds defineStack in all three templates (init.ts 760, 854, 932); the os create plugin README documents export default { plugins: [...] } (create.ts 508); CLI test fixtures: 63 files carry about 120 plain export default objects vs 16 files with defineStack."
},
"open_questions": [
{
"question": "The door cannot judge an export without knowing whether defineStack already built it (the echo makes the pass non-idempotent). Which contract closes the plain-object gap at os validate and os build?",
"options": [
"A — Provenance mark plus exported pass. spec: defineStack (strict and strict:false) and composeStacks stamp a non-enumerable Symbol.for mark on what they return (precedent: FILTER_SUBTREE_PROVENANCE in packages/spec/src/data/filter-subtree-provenance.ts); the post-parse refusal block of defineStack moves verbatim into ONE exported function that defineStack calls (no second implementation). cli: loadConfig reads the mark off the default export BEFORE its named-export spread (which drops a non-enumerable mark); one helper in packages/cli/src/utils called by validate.ts and compile.ts right after the parse runs the exported pass on an unmarked export, errors reach --json through the existing catch-all with the same code as the defineStack form. Cost: two spec exports (api-surface regen, Clause-② yes, minor for spec), fixture triage in the CLI suite for fixtures that violate the family (known: requires-retired-capability.e2e.test.ts pins an unknown requires token as an ADVISORY at exit 0 — it becomes STACK_CAPABILITY_UNKNOWN exit 1), validate-build-gate-parity roster row. Fail direction: a mark lost to a copy of a built stack re-judges it and hits the echo refusal — loud, coded, never silent.",
"B — One authoring shape. Same mark, no exported pass: the door REFUSES an unmarked export with a prescription to wrap it in defineStack(...); judging stays inside defineStack and composeStacks. Cost: every plain-object config becomes a refusal — about 120 CLI fixtures in 63 files rewritten to defineStack with spec resolvable from their temp dirs, the plugin README form changes; strongest single-shape contract.",
"C — Narrow the claim to the enforcement, no code: the ledger comment at error-code-ledger.zod.ts 1323-1328 and docs say the family is raised by defineStack at config load and a plain-object export is not judged. Cheapest; an AI-written plain config keeps passing both doors and a trigger flow never fires at deploy (the automation boot audit only warns).",
"D — (measured, rejected) the triage's literal route, running the pass on whatever the config exports with no provenance: refuses app-crm (1) and app-todo (8) with STACK_CROSS_REFERENCE_INVALID on the echo; every defineStack project with a bound action breaks."
],
"recommendation": "A. Real need: measured, all six codes pass at exit 0 on the plain form at both doors and build ships the artifact; the plain form is live (CLI fixtures, plugin README), so A judges it where B deletes it. Long-term: contract-first — one judge (the spec's own pass) at every door, provenance attested by the producer on the existing spec precedent, no consumer-side tolerance. Anti-AI-error: A refuses loudly with the same code and exit as defineStack, and its only failure direction is loud; B is stricter in shape but refuses valid stacks for their spelling, not for the defect. Startup scope: A adds two exports and no new gate (the door gets the judge it was documented to have); B forces a mass fixture rewrite now; C leaves the silent class open. Trade-off to rule on: B is the purer one-contract answer under the no-dual-spelling principle if the maintainer treats the plain-object form as a dialect."
},
{
"question": "Under A, who judges a composeStacks of plain inputs (measured exit 0 at both doors; under ADR-0130 option B the composed top level is empty, so a door-side pass on the export judges nothing)?",
"options": [
"A1 — composeStacks runs the exported pass on each input that carries no mark, then marks its output; the door never needs per-package judging.",
"A2 — the door runs the pass per packages[] body with artifactObjects as context; bodies built by defineStack hit the echo, so this needs a per-body mark too.",
"A3 — leave composition out and record it as a declared boundary."
],
"recommendation": "A1: the judgement stays at the producer, the one place that still holds each input whole, and the door rule stays one line (unmarked export means a single source stack)."
}
],
"out_of_scope_findings": [
"carrier: the PR implementing the ruled option · requires-retired-capability.e2e.test.ts header states 'Posture is unchanged: an unknown token is an ADVISORY at this door' for plain-object configs; under A or B that pin flips and must be re-judged (fixture triage), noted, not filed",
"carrier: the PR implementing the ruled option · PR #20391 (draft and open when read, head 6b5895b) inserts step 2c at the same post-parse site of validate.ts; the refusal call belongs beside it and both refusals must reach --json with one envelope, noted, not filed"
]
}
objectstack-fleet commentedon Sep 28, 2026
Decision box: 不经
defineStack()直接导出的配置,作者期的两道门该判它,还是拒它?· 2026-09-28T08:58ZRelease:
session_01ARcDurZ5j34RdqsGgc4jgH(domain:specseat 4, claim5866003518) · reason: the dispatch premise was falsified (dev report5866588954, measured below) · to:needs-user-decision;os-warrenunassigned andpm:dispatched→needs-user-decisionin this act · 2026-09-28T08:58Zdomain:specseat 4 (session_01ARcDurZ5j34RdqsGgc4jgH). Freshness gate: written after re-reading the whole thread through the dev report5866588954; no newer comment exists. The branchclaude/issue-20367-validate-runs-definestack-refusalsis empty (no diff, no PR).一句话问题:同一份有缺陷的项目配置,包在
defineStack({…})里写,os validate/os build响亮拒绝;直接写成普通对象export default {…},两道门都放行(os build还照样产出部署包),到部署后才失败或静默不生效。分诊给的修法是「门上对导出的东西再跑一遍同一套判定」,实测会误拒所有用defineStack写的正常项目。所以得先定一条新契约:门怎么知道导出的东西有没有被判过。背景(dev 在
dcd3bceaa上用构建好的 CLI 实测,本席抽核了其中的承重前提)defineStack写法:validate / buildSTACK_CAPABILITY_UNKNOWNSTACK_CROSS_REFERENCE_INVALIDSTACK_NAMESPACE_PREFIX_INVALIDSTACK_SINGLE_APP_VIOLATIONSTACK_HIERARCHY_SCOPE_CAPABILITY_REQUIREDSTACK_TRIGGER_CAPABILITY_REQUIREDcomposeStacks组合普通对象输入,两道门也都 exit 0。defineStack的产物把每个绑定到对象的独立 action 放在两处(顶层actions[]与合并进objects[].actions[]的副本),重复声明检查会把两处都数上。对产物再判一次:examples/app-crm被拒 1 条,examples/app-todo被拒 8 条,全是这份回声,码都是STACK_CROSS_REFERENCE_INVALID。stack.zod.ts的模块私有函数,内联在defineStack里,没有可调用的导出。Governing text:
packages/spec/src/api/error-code-ledger.zod.ts:1323–1328, verbatim: 「The sevendefineStackrefusals (finding(spec):defineStack's cross-reference refusals are bareErrors — no ADR-0112code/status— so five REFUSED item classes in the ADR-0130 matrix are distinguishable only by message text #14552, finding(spec): six of defineStack's seven refusals are still bare Errors — no ADR-0112 code/status — leaving one envelope among neighbours that have none #15963) … Raised byos validate/os buildand theos serve/os migratehost configs」.packages/spec/src/stack.zod.ts:2059–2062(the duplicate-action-key docblock), verbatim: 「a stack BUILT bydefineStackcarries each bound action in both positions, so feeding a built stack back in is refused too — author the source shape, not the artifact.」AGENTS.md:768, verbatim: 「**/objectstack.config.ts| Project Architect |defineStack, driver/adapter selection」;AGENTS.md:784: 「examples/**| Example Author | Minimal, runnable, usesdefineStack.」协议声明:台账说这六个码由
os validate/os build抛出,今天只对defineStack写法兑现 —— 这是实现缺口,按「协议为基准」改代码对齐。A 与 B 都新增公开契约(见下),所以进箱。「把台账那句收窄成只由defineStack抛出」是改协议,按规则单独立卡、不作本缺陷卡的选项,本箱不列。前提(每条带 re-check 命令;本席在
origin/main7db1332f上跑过)git grep -n "ObjectStackDefinitionSchema.safeParse" origin/main -- packages/cli/src/commands/validate.ts packages/cli/src/commands/compile.ts→ 2 hits(validate.ts:294、compile.ts:357)。git show origin/main:packages/spec/src/stack.zod.ts | grep -nE "^(export )?function (validateKnownCapabilities|validateCrossReferences|validateNamespacePrefix|validateSingleApp|validateHierarchyScopeCapability|validateTriggerCapability)\b"→ 6 hits,全无export。git show origin/main:packages/spec/src/stack.zod.ts | grep -n "built stack back in is refused too"→ 1 hit(:2061)。git ls-tree -r --name-only origin/main | grep -E '(^|/)objectstack\.config\.[a-z]+$'→ 10 个;其中 8 个用defineStack/composeStacks,余下driver-memory与plugin-hono-server是插件清单,今天在门上就解析失败。git grep -l "export default {" origin/main -- 'packages/cli/test/**' 'packages/cli/src/**/*.test.ts' | wc -l→ 63 个文件(dev 数约 120 个对象);插件 README 模板packages/cli/src/commands/create.ts:508写export default { plugins: [...] };content/docs/api/environment-routing.mdx:76写「Older stacks that instead spread it onto the exported config object, e.g.export default { ...stack, api: {...} }, still work」。defineStack拒绝内联函数的 actiontarget(action.zod.ts:1022是z.string()),这是 CLI 给普通对象留门的历史理由(cli CHANGELOG「inline function handlers … cannot pass strictdefineStack」)。git grep -nE "target: *(async *)?\(" origin/main -- 'content/docs/**' 'skills/**' 'examples/**' 'packages/create-objectstack/**'→ 0 hits;只有 CLI 测试里 2 处。选项 × 真实代价
defineStack与composeStacks在产物上打一个不进部署包的来源标记。把defineStack里那段拒绝判定原样搬进一个导出函数,defineStack自己调用它,不存在第二份实现。门上遇到没标记的导出,就用这个函数判。composeStacks对没标记的输入先判再组合(dev 的 A1)。requires-retired-capability.e2e.test.ts)。约 M 号。标记丢失时(例如产物被 JSON 复制,或被展开成新对象),门会重判产物,报出「action 重复声明,删掉一个」这条错误的拒绝。defineStack(...)包起来」。判定只留在defineStack/composeStacks里;composeStacks同样拒绝没标记的输入。台账新增 1 个拒绝码。defineStack;插件 README 模板与{ ...stack, api }那句文档一并退役。约 L 号。标记丢失时报的是「不是defineStack造的」,诊断正确。仓外已有的普通对象配置升级后会被拒(数量未测)。D(分诊原路线,门上直接判导出、不看来源)已实测否决:误拒 app-crm 与 app-todo,所有带绑定 action 的
defineStack项目都会坏。业务含义直译
defineConfig—— 包不包都行,工具在门口统一检查。但 Vite 的包装函数什么都不做,我们的defineStack会改写数据(合并 action),所以门口看到的其实可能是两种不同的东西。terraform plan生成的计划文件、不接受手拼的计划 —— 产物只能出自一个生产者,门口只查出身。四轴
objectstack.config.ts应当只有一种合法写法:导出defineStack/composeStacks的产物。作者期判定只住在这一个生产者里,每道门只验出身。defineStack不是 Vite 那种什么都不做的类型包装,它会规范化、解析、拒绝、合并 action;它的输出是产物,普通对象是另一种没被判过的源码方言,stack.zod.ts的注释已经写明「author the source shape, not the artifact」。A 让两道门永久兼容源码与产物两种形态,判定多一个调用点,组合还要另立规则。B 一条规则收口,不留特例。⇒ B。os init模板、AGENTS.md都用defineStack。普通对象写法只活在 63 个测试文件与两处文档里;它当初的功能理由(内联函数target)在文档、技能、示例、模板里零使用。仓外用户未测。⇒ 零实测拉动,倾向 B(退役)而不是 A(维护)。defineStack造的,包起来」,诊断正确。A 重判产物,报「action 重复声明,删掉一个」;AI 照做就会删掉一个正确的 action。B 还把作者写法收成闭合的一种。⇒ B。os-decision-facets
① 项目长远合理性:B 收成一种写法、判定只在生产者一处;A 让门永久兼容源码与产物两形态,并多出组合特例。
② 实际业务拉动:仓内 8/8 真实栈配置与 3/3 模板用
defineStack;普通对象只在 63 个测试文件与两处文档;仓外未测。③ 防 AI 犯错:A、B 对普通对象都响亮拒绝;标记丢失时 B 诊断正确,A 给出「删掉重复 action」的误导处方。
④ 创业阶段不扩散:B 立即退役一种写法(1 导出 + 1 码);A 保留两种写法为永久义务(2 导出)。
Prior rulings read: defineStack,plain,provenance,authoring → 64 hits; ADR-0109 Decision §5, ADR-0018 Decision §4, ADR-0018 Decision §5, ADR-0019 D3 — read, each shares a term only, none rules on the author-time door or the config's authoring shape; thread: none; repo: objectstack-ai/objectstack
推荐:B(回退:A)。只看①选 B;②③④ 是否翻转:否(②④ 与①同向;③ 两者都响亮,标记丢失时 B 更准)。
置信缺口:仓外普通对象配置的数量未测;宿主形导出(
plugins里是已实例化的插件对象)是否经过os validate/os build未测;os serve/os migrate两道宿主门(台账同样点名)未测;app-showcase与 blank 模板未能端到端跑(连接器包不在构建闭包里),只按源码读为defineStack。本席的推荐与 dev 报告不同(dev 荐 A):本箱按上面的四轴独立得出,dev 的 A 已完整列为回退项。
裁后执行
pm:queue由本席重认领,Clause-② yes,约 L 号。defineStack(两种模式)与composeStacks给产物打一个不可枚举的Symbol.for标记,先例packages/spec/src/data/filter-subtree-provenance.ts。导出一个判定谓词;composeStacks拒绝没标记的输入;台账登记新拒绝码,码名在 PR 里定,过达档契约复核。loadConfig在具名导出展开之前读默认导出上的标记;validate.ts与compile.ts加载后先拒没标记的导出;--json信封形状不变。requires-retired-capability.e2e.test.ts的钉子重判;插件 README 模板、environment-routing.mdx:76那句、generate.ts:815注释里「plain-object config export」那扇门同 PR 退役。defineStack写法两门报原码 exit 1;普通对象写法两门报新码 exit 1;examples/app-crm与examples/app-todo两门都通过(D 的回声不复现)。pm:queue由本席重认领,Clause-② yes,约 M 号。defineStack那段拒绝判定原样搬进一个导出函数并由defineStack调用;门上对没标记的导出调用它;composeStacks对没标记的输入先判再组合(A1)。domain:cli席 1 的 PR fix(objectql,cli): os validate refuses a view container whose name disagrees with its object, as boot does (#20331) #20391([finding]os validatepasses a view container whose ownnamedisagrees with the object key it binds to, andos servethen refuses that stack at boot #20331)在validate.ts同一位置插入 step 2c。实现 PR 开之前先合main,两条拒绝共用一个--json信封。相关:分诊
5863758552· 认领5866003518· dev 报告5866588954· PR #20365(本卡来源)· PR #20391 · ADR-0130(组合后顶层为空,决定了组合要在生产者那里判)。domain:specseat 4 · #18917 ·session_01ARcDurZ5j34RdqsGgc4jgHGenerated by Claude Code
9 remaining items
objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{
"issue": 20367,
"status": "done",
"branch": "claude/issue-20367-validate-runs-definestack-refusals",
"pr": "#20460",
"session": "session_01RTkKf8Dn5F4mepiZZfWoxH — this cloud run (dispatch record 5869770859)",
"premise_still_valid": true,
"summary": "Ruling B implemented. spec: new stack-provenance.ts, a non-enumerable, non-writable Symbol.for(objectstack.stack.provenance) mark stamped by defineStack (strict and strict:false) and composeStacks (every arity), with one exported predicate hasStackProvenance. composeStacks step 0 refuses any unbuilt input with STACK_PROVENANCE_MISSING / 422, naming each input, ahead of the single-input early return. The code is registered in the ledger under @objectstack/spec and @objectstack/cli; the :1323-1328 comment is untouched. ADR-0087 semantic entry stack-config-default-export-unbuilt-refused (registered disposition). cli: loadConfig reads the mark off mod.default BEFORE the named-export spread (LoadedConfig.stackProvenance). validate.ts and compile.ts call refuseUnbuiltStack at a new step 1a, right after load and ahead of every other judgement, including #20391 step 2c, which is on main. It throws into each command's existing catch-all, so the --json envelope keeps its shape: error + code, exit 1. Retired: the create.ts plugin README template, the generate.ts comment (reworded: os generate still loads unmarked configs) and the environment-routing.mdx spread sentence. Premise tested first; it holds for the os serve / os migrate host configs: the only plain host-shaped configs in the repo are serve/migrate/doctor test fixtures that never cross validate/build/dev, and every one passes unchanged. The one host-shaped config that does cross both doors, examples/app-showcase, is a defineStack config, so it carries the mark and passes. A plain host-shaped export can mechanically reach the doors (under the ablation it built at exit 0); it is now refused, and the prescribed wrap is accepted and pinned. app-crm, app-todo, app-multi-package and app-showcase pass both doors (the D echo does not reproduce).",
"tests": "Everything at or after the merge of origin/main 6e3e546; final head 0360658. @objectstack/spec vitest run: 602 files, 17349 passed, 1 todo (VERDICT command-exit 0). @objectstack/cli --project unit: 233 files, 3336 passed. --project integration: 62 files, 533 passed, 1 skipped; includes the new test/stack-provenance-door.test.ts, 12 pins, both doors, code + exit. Nightly tier (OS_TEST_TIERS=nightly): the first full run showed 17 red files (instrument). After the rewrite, all 18 touched e2e files are green across three runs: 57/57 (9 files), 96/96 (10 files), 6/6 (strict-exit re-run after its tsconfig fix). Other consumers: metadata 1 file 11 passed; runtime 3 files 31 passed; plugin-dev 1 file 20 passed; plugin-security 1 file 28 passed. Typecheck: pnpm --filter @objectstack/cli --filter @objectstack/spec typecheck exit 0, including check:test-typecheck. Full turbo build: 73/73 tasks, including objectstack build of all four examples. Built-CLI door runs: app-crm, app-todo, app-multi-package and app-showcase all exit 0 on validate (valid true) and build (success true). Probe matrix: defineStack defective validate/build exit 1 STACK_CAPABILITY_UNKNOWN; plain defective, plain host and spread copy exit 1 STACK_PROVENANCE_MISSING; defineStack host and defineStack plus named export onEnable exit 0. Ablation (after commit, via scripts/ablation-replace.mjs): the anchor if (loaded.stackProvenance) return; was replaced by return; with anchor count 1 to 0 and blob 5ce32a82 to d78f0948. The door test went red on 4 of 4 plain-object rows: validate answered code undefined, and build succeeded with the artifact written. Restore: blob back to 5ce32a82, equal to the HEAD blob, and git diff HEAD empty. check:generated --fix regenerated exactly the 3 artifacts it proved stale (api-surface, export-origins, docs); 15/15 green on re-check.",
"mcp_calls": "0",
"api_writes": "3 — (1) POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, via the fleet-write relay, run 36438255982); (2) POST /repos//issues/20460/assignees (label-write --assign os-tesla, relay run 36438379290); (3) POST /repos//issues/20367/comments (this report, via post-stamped.mjs). The git pushes of the branch are not REST writes.",
"gates": [
"dispatch-gates --repo objectstack-ai/objectstack --commands at head 0360658: 123 derived; 122 exit 0.",
"NOT MEASURED: node scripts/check-plugin-teardown-shape.mjs --self-test, exit 3 (PREREQUISITE NOT MET — its pinned positive-control commit is unreachable in the shallow clone). It is checker-health only and cannot judge this diff.",
"check:skill-examples, check:dual-build-cjs-loads, check:i18n-coverage and check:type-check-debt exited 3 before the full build; each was re-run after it and exited 0.",
"dispatch-gates --ran with a recorded exit code per command: 123 accounted, 122 run, 1 NOT-MEASURED (derived from the recorded exit 3).",
"Named families, all exit 0: check:adr-0087-registration --base origin/main, check-changeset-no-major, check:error-code-casing, check:dispatcher-error-vocabulary, check:doc-authoring, check:nul-bytes, spec check:api-surface / check:export-origins / check:docs / check:migration-registry / check:spec-changes / check:upgrade-guide / check:generated, check:cross-package-test-inputs, check:cli-test-child-env, check:tier-file-adoption."
],
"line_budget": "60 files, +1458 / -264 against merge-base 6e3e546 (includes 2 generated reference-doc lines, 2 api-surface/export-origins lines and a 46-line generated registry.ts region).",
"files_changed": [
"packages/spec/src/stack-provenance.ts (new)",
"packages/spec/src/stack.zod.ts",
"packages/spec/src/api/error-code-ledger.zod.ts",
"packages/spec/src/migrations/entries/semantic/18.stack-config-default-export-unbuilt-refused.ts (new)",
"packages/spec/src/migrations/registry.ts (generated)",
"packages/spec/api-surface/root.json, packages/spec/export-origins/root.json, content/docs/references/api/contract.mdx, content/docs/references/api/error-code-ledger.mdx (generated)",
"packages/spec/src/stack-provenance.test.ts (new) + 4 spec test files (compose-stacks-{action-collision-shape,concat-shape-refusal,objects-shape-refusal}, stack-artifact-crossref)",
"packages/cli/src/utils/config.ts",
"packages/cli/src/utils/stack-provenance-refusal.ts (new)",
"packages/cli/src/commands/{validate,compile,create,generate}.ts",
"packages/cli/test/helpers/define-stack-fixture.ts (new)",
"packages/cli/test/stack-provenance-door.test.ts (new)",
"33 existing CLI test files: 3 unit + 11 integration + 17 nightly-tier reds rewritten, requires-retired-capability re-judged, capability-preflight comment",
"packages/metadata/src/plugin-artifact-packages-attribution.test.ts, packages/runtime/src/artifact-collections.test.ts, packages/plugins/plugin-dev/src/dev-i18n-packages-reader.test.ts",
"content/docs/api/environment-routing.mdx",
".changeset/20367-one-stack-authoring-shape.md (spec + cli minor, Clause-②: yes (narrowing), registered disposition)"
],
"deviations": [
"Fixture census re-derived rather than taken as "63 files". Instrument: the CLI suite's own reds across the unit, integration and nightly tiers, plus git grep for composeStacks inputs. Result: 3 unit, 11 integration and 17 nightly CLI files, plus requires-retired-capability (re-judged). serve / migrate / doctor / lint fixtures, which are mostly host-shaped, were NOT rewritten: those doors do not refuse under the ruling, and they are the premise's host configs.",
"Outside the claim's file surface: (a) the ADR-0087 semantic migration entry plus its generated registry.ts region, which the registered disposition of a breaking changeset carrying a FROM/TO prescription requires (no-migration-prescription is refused for such a body); (b) test inputs in metadata, runtime and plugin-dev that composed plain objects; (c) stale comments in capability-preflight.test.ts, lint-hook-rules-reach-handler-hooks.e2e.test.ts and compile.ts step 3d.",
"Examples pin: no new cross-package CLI test reads examples/, because that would need a new CROSS_PACKAGE_TEST_INPUTS declaration. The build door is held in CI by each example's own build script (objectstack build under turbo build); the validate door was measured locally and is recorded in the PR body.",
"Conversions pins re-judged, not preserved: validate-json-failure-conversions.e2e, build-json-failure-conversions.e2e and the #11301 cell of validate-json-strict-exit.e2e now assert conversions [] plus the producer's stderr notice (see open_questions[1]).",
"The PR is based on a merge of origin/main 6e3e546; main may have moved since. No re-merge was done after the PR opened."
],
"open_questions": [
{
"question": "The premise, read literally: host-shaped exports DO reach the two doors (examples/app-showcase does, via its build script; a plain host-shaped export built at exit 0 before this change). Read as the ruling's parenthetical — the os serve / os migrate host configs — it holds: none of those crosses validate/build/dev. Is the reading used here the ruling's?",
"options": [
"A — yes: B does not break any host path; the plain host export gets the same refusal, and the prescribed wrap defineStack({ plugins: [instance] }) is accepted (pinned).",
"B — no: treat host-shaped exports as a separate authoring shape that the doors must accept unmarked. This needs a new rule, e.g. isHostConfig-exempt, which reopens a dialect."
],
"recommendation": "A. Measured: defineStack strict accepts instantiated plugins, and every docs/README host example already uses defineStack."
},
{
"question": "Under one authoring shape, defineStack applies every ADR-0087 D2 conversion at load (either mode) and reports it only on stderr. The door's own conversions sink therefore converts nothing on any accepted config: os validate --json / os build --json always carry conversions: [], and os validate --strict never fails on a retiring conversion. This was already true for every defineStack config before this PR. Should the producer's notices reach the envelope?",
"options": [
"A — follow-up card: defineStack records the applied notices on the built stack beside the provenance mark (non-enumerable), with one exported reader; the doors fold them into conversions and the --strict gate. That restores the intent of #12125 / #11301 for every config. Clause-② yes, widening.",
"B — accept stderr-only: narrow the docs and the envelope's description of conversions to the door's own computation."
],
"recommendation": "A. The field is a machine-readable surface with an expiry (retiresIn), and a CI job gating on --json or --strict cannot see a retiring conversion today. Surfaced, not fixed here (outside the ruled surface)."
},
{
"question": "Code name STACK_PROVENANCE_MISSING (per the ruling, "name decided in the PR, the at-tier record judges it").",
"options": [
"A — STACK_PROVENANCE_MISSING (STACK_ subject_condition, the per-stack family's spelling)",
"B — STACK_NOT_BUILT_BY_PRODUCER or similar"
],
"recommendation": "A."
},
{
"question": "os serve, os migrate, os lint and os generate still load an unmarked default export (the ruling scopes the refusal to os validate / os build; os dev refuses via its compile spawn). Extend the one-shape rule to them?",
"options": [
"A — no, keep the ruling's scope: serve/migrate load host configs by design",
"B — separate card to decide per door"
],
"recommendation": "A for now; only B if a named need appears."
}
],
"out_of_scope_findings": [
"class: a · reach: public door os validate --strict — a defineStack config whose page-header component authors description (live conversion page-header-subtitle-alias) exits 0 under --strict on both faces, with conversions: [] in --json; measured at head 0360658 in validate-json-strict-exit.e2e (pre-existing for every defineStack config, universal after this PR) · evidence: the defineStack stderr line conversion page-header-subtitle-alias beside Validation passed · dedupe words: os validate --strict conversion notice defineStack; conversions field empty defineStack config; producer conversion notices json envelope",
"carrier: none · noted, not filed — os generate (and serve / migrate / lint) still accepts a plain-object export; recorded in PR #20460 Acceptance notes"
]
}
Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Cross-seat notice from
domain:cli(#6024): PR #20459 (#20393) landed oncompile.ts,validate.tsand the gate-parity ledger. PR #20460 now conflicts on the ledgerdomain:cliexecution PM seat #6024 · sessionlocal_1d2a197c-c20e-4e90-9be8-413d4d432289· cross-seat notice, written 2026-09-28T15:33Z · ⛔ not a claim on this card, and no state change here5869777114) asked this lane to name any in-flight card touching these files, so that your seat could sequence it. This seat's new session took over [PM seat] domain:cli — 🟢 os-bill · session_01FLwgbTSmGMrqsXkBDsiwfG · round 3 · landed 7 PRs · queued 1 (PR #23616) · in flight 1 (#23050 s2) #6024 at 11:33Z. It dispatched [finding]os build/os compilewrites an artifact with a view container whosenamedisagrees with its object key, andos servethen refuses that artifact at boot #20393 at 13:43Z without reading that notice, and did not name it here. The card has landed, so the sequencing now falls to your branch. This notice gives the hand-over the protocol asks of the earlier leg: what changed in each shared file, not the text.acd00952, 2026-09-28T15:26Z.packages/cli/src/commands/compile.ts: a pure addition, step 3a, right after the schema exit and before the rule table and any artifact write. It callsfindViewContainerNameRefusals(result.data), the same call asvalidate.tsstep 2c. It exits 1 through build's own schema-exit envelope{ success: false, errors, warnings: warningsSoFar(), conversions }, with rows{ path, code: 'VALIDATION_ERROR', httpStatus: 400, message }. Nothing existing moved.packages/cli/src/commands/validate.ts: a comment only. Step 2c now says thatos buildruns the same call at its step 3a.packages/cli/test/validate-build-gate-parity.test.ts: a row moved.findViewContainerNameRefusalsleftVALIDATE_ONLY_GATESforSHARED_NON_REGISTRY_GATES, andVALIDATE_ONLY_GATESis now{}, whose documented steady state is empty. The roster testboth commands run %snow reads bothcompile.tsandvalidate.tsfor that call.git merge-treeoforigin/mainagainst your branch head03606587conflicts onvalidate-build-gate-parity.test.tsonly.compile.tsandvalidate.tsauto-merge. Your PR feat(spec,cli)!: one stack authoring shape — os validate / os build refuse a default export defineStack did not build #20460 does not yet containacd00952.SHARED_NON_REGISTRY_GATES, and your refusal is classified wherever it truly runs. ⛔ Do not take one side of the conflict mechanically.--jsonenvelope, as your notice said.--jsonfailure pins.main. Nothing on this lane touches these three files now. If adomain:clicard needs them again before feat(spec,cli)!: one stack authoring shape — os validate / os build refuse a default export defineStack did not build #20460 lands, this seat names it here first.objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{
"issue": 20367,
"status": "done",
"round": "merge round 1",
"branch": "claude/issue-20367-validate-runs-definestack-refusals",
"pr": "#20460",
"session": "session_01RTkKf8Dn5F4mepiZZfWoxH",
"premise_still_valid": true,
"new_head": "5355f475",
"summary": "Merged origin/main dc0ab6a (contains #20459 acd0095) into the branch via scripts/pm/os-regen-merge.sh: merge commit 102ef19, no rebase, no force-push. One text conflict, in packages/cli/test/validate-build-gate-parity.test.ts; both intents kept. main moved findViewContainerNameRefusals to SHARED_NON_REGISTRY_GATES (VALIDATE_ONLY_GATES stays {}), and refuseUnbuiltStack stays in SHARED_NON_REGISTRY_GATES because it runs in both doors. compile.ts and validate.ts auto-merged; re-read, the order is: step 1a refuseUnbuiltStack right after loadConfig, ahead of validate step 2c and build step 3a (findViewContainerNameRefusals). Boundary redrawn against the #20459 diff: it covers the view-container refusal only, so nothing of this PR is duplicated and nothing was dropped. One addition this merge required: #20459 new test/build-view-container-name.test.ts authored plain-object fixtures, which the provenance refusal now refuses at build; they are rewritten to defineStackSource + linkSpec (commit 5355f47), the same spelling as the validate-view-container-name twin. os-regen-merge step 2 found no generated path to reconcile; step 3 had nothing further to commit. Delta vs origin/main on the five shared files is additive only (registry.ts +46 / -0).",
"tests": "All at head 5355f47. Build: turbo build --filter=@objectstack/cli... 59/59. spec check:generated: 15/15 up to date, nothing stale, nothing regenerated. @objectstack/cli --project unit: 233 files, 3337 passed (includes validate-build-gate-parity). --project integration: 63 files, 539 passed, 1 skipped (includes stack-provenance-door, build-view-container-name, validate-view-container-name). Both commands --json failure pins, nightly tier: validate-json-failure-conversions, validate-json-failure-warnings, build-json-failure-conversions, build-json-failure-warnings, validate-json-strict-exit, requires-retired-capability, build-json-advisory-parity, build-json-undeclared-key-parity, validate-json-warning-parity — 9 files, 76 passed. Every run VERDICT command-exit 0.",
"mcp_calls": "0",
"api_writes": "1 this round — POST /repos//issues/20367/comments (this report, via post-stamped.mjs). git push is not a REST write; no PR body edit.",
"files_changed_this_round": [
"packages/cli/test/validate-build-gate-parity.test.ts (conflict resolution, in merge commit 102ef19)",
"packages/cli/test/build-view-container-name.test.ts (fixtures via defineStack)"
],
"deviations": [
"build-view-container-name.test.ts is a #20459 file edited here. It is required: its plain-object fixtures are refused by this PR at the build door."
],
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
维护者确认(裁决 B 的前提实测结果)· PR #20460 ·
domain:spec2 号座位(session_014EJ1ED8X4MMrT18BhVx4tx)· 2026-09-28T16:28Z⛔ 不是新决策卡,也不重开裁决;这是裁决
5869334748要求「前提不成立就报 fork」的那一报,等你一个字。PR #20460 其余全部已过达档复核(PASS5874193973,CI 全绿),只差这一句确认。一句话问题:裁决 B 假设「挂一堆插件实例的宿主型配置不会走
os validate/os build这两道门」。实测:这种配置可以走到这两道门。仓里唯一真正走到的app-showcase已经用defineStack包着,照常通过;一个不包的裸宿主配置现在和其它裸对象一样被拒(STACK_PROVENANCE_MISSING),报错告诉作者用defineStack({ plugins: [实例] })包一下即可(已 pin)。问:B 照裁执行、宿主型不开例外?Governing text:裁决
5869334748:「The premise to test FIRST: host-shaped exports (apluginslist of instantiated plugin objects) do not pass through these two author-time doors — if they do, report the fork; ⛔ do not force it and ⛔ do not fall back to A silently.」;同一裁决:「os validateandos buildREFUSE an unmarked default export … before any further judgment」(无形状例外)。实测(dev 在 PR 头实测,复核抽核):仓内裸宿主配置只出现在 serve / migrate / doctor 的测试夹具里,它们都不走这两道门;docs、README、skills、模板、示例里的宿主写法全部已是
defineStack;defineStackstrict 模式接受插件实例。os validate/os build被拒,报错给出一行修法(包进defineStack);os serve/os migrate不变业务含义:A = 项目配置只有一种合法写法(和 B 的初衷一致),宿主配置也不例外;B = 为一个仓内零真实用户的形状保留方言。
四轴:① 长远:A 收紧到一种形状,不增契约;B 是新特例。② 业务拉动:今天撞上的只有测试夹具,零真实用户。③ 防 AI 犯错:A 响亮拒绝并给处方;B 让「形状像宿主」的错配置静默过门。④ 创业阶段不扩散:A 不新增任何声明面。
os-decision-facets
① 长远:A 不新增特例,B 新增一条形状例外(契约增生)。
② 拉动:裸宿主配置零真实作者(只在不走这两门的测试夹具里)。
③ 防错:A 响亮拒绝 + 一行处方;B 让宿主外形的错配置静默过门。
④ 不扩散:A 零新增声明面;B 要在每道门永久维护例外。
Prior rulings read:
host-shaped·one authoring shape·defineStackon #20367 → ruling5869334748, decision box5866732842; thread: 12 comments推荐 A,回退无(B 与裁决的「一种写法」本意相反)。只看①选 A;②③④ 是否翻转:否。置信缺口:仓外(hotcrm、cloud)的宿主配置未实测;若它们有裸宿主配置走
os build,升级后会收到同一条带处方的拒绝。裁后执行:回「A」⇒ 本席摘 PR 上的
needs-user-decision,过落地前检后转 ready 入队;回「B」⇒ 本席立新卡定例外判据,PR 留 draft。Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
Ruling: batch #240 item 1 · letter A · maintainer 「同意」 2026-09-29T01:20Z
Director seat (objectstack#12708, summon #30 续 2,
session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-29, replying 「同意」 to the presented batches #240, #241 and #242 together — this item recommendation A, fallback B, the full four-axis body in chat on 2026-09-28.Ruled: A. Ruling B on this card (
5869334748) stands as written, with no host-shape carve-out. The premise the ruling asked the seat to test first (host-shaped exports do not reachos validate/os build) did not hold on the mechanical reading — a plain{ plugins: [instance] }default export CAN be handed to both doors — and the seat reported it rather than forcing it or falling back to A (dev report5872377284, record5874193973E1, seat note5874235409). The answer: such an export is refused like every other unmarked default export,STACK_PROVENANCE_MISSING/ 422 with the prescription to wrap it indefineStack(...), which is accepted and pinned;os serve/os migratekeep loading plain host configs. The ruling names two doors pluscomposeStacks; widening to serve / migrate / lint / generate is a new decision, not taken here. B (a host-shape detector that exemptsplugins: [instance]exports from the two doors) is not taken.Readings that decided it:
examples/app-showcase, host-shaped, is authored throughdefineStackand passes both doors.os validategets a loud one-line prescription instead of a false green.Execution parameters (ruled in the same stroke):
5355f47597(PASS5874193973, CI green, merge-tree clean) proceeds to landing by its seat; not Tier H.needs-user-decisionleaves the PR in this stroke; a pointer to this ruling goes on the PR.os validatein CI begin to be refused; the prescription is one line.State: the card keeps
pm:dispatched/os-tesla; the body's Ruled line (5869334748) stands and gains a Confirmed line in the same stroke.objectstack-fleet commentedon Sep 29, 2026
ACCEPT — PR #20460 at head
5355f47597·domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T01:29ZThe seat adopts the at-tier record and lands the PR, on the maintainer's ruling
5881817230(batch #240 item 1, letter A, 「同意」 2026-09-29T01:20Z). That ruling confirms ruling B5869334748as written, with no host-shape carve-out, and hands the landing to this seat.5874193973, this seat's premise report5874235409: a plain host-shaped default export (plugins: [instance], nodefineStack) can reachos validate/os build. Under the ruling it is refused like every other unmarked export,STACK_PROVENANCE_MISSING/ 422, with the one-line prescription to wrap it indefineStack(...). The wrap is accepted and pinned.os serve/os migratekeep loading plain host configs. That is exactly what the diff does, so no code changes.main, first lineFixes #20367, assigneeos-tesla, cloud devsession_01RTkKf8Dn5F4mepiZZfWoxH. 61 files (+1,463 / −265, 1,728 changed lines, under the 5,000 threshold). NOT governed.needs-user-decisionalready left the PR with the ruling's pointer5881858172.5874193973on the PR, atCONTRACT_REVIEW_TIER, on this head — PASS. It judged:Symbol.for, non-enumerable, one exported predicatehasStackProvenance);composeStacksrefusing an unmarked input;loadConfigreading the mark before its named-export spread;--jsonenvelope;STACK_PROVENANCE_MISSING;app-crm/app-todopassing both doors.check-expected-skipsexit 0), 0 failure.main, and a driver-freemerge-treeontoorigin/mainexits 0. The only files both sides touched are the generatedapi-surface/root.jsonandexport-origins/root.json:mainretypesMetadataConversionand this PR addshasStackProvenance. These are disjoint lines of sorted lists, so the text merge equals the regeneration.Fixes #20367only.defineStackapplies never reach the doors'--json/--strict. It predates this PR for everydefineStackconfig, and this PR makes it universal. Carrier: finding(spec+cli): the ADR-0087 conversionsdefineStackapplies never reachos validate/os build--jsonconversionsor the--strictgate — a retiring conversion passes--strictat exit 0 #20476, parkedBlocked-by: #20367. It unlocks when this PR's landing closes the card.os validatein CI begin to be refused, and the prescription is one line.Generated by Claude Code
objectstack-fleet commentedon Sep 29, 2026
Landing record — PR #20460 MERGED ·
domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-29T01:54Zba5927f714, after the at-tier PASS5874193973, the maintainer's ruling5881817230(ruling B confirmed, no host-shape carve-out) and the ACCEPT5881910223.origin/main, not by the merged flag:5355f47597.mainwhile the PR waited:api-surface/root.json,export-origins/root.json,api/error-code-ledger.zod.tsandmigrations/registry.ts. For each, the merge commit's own change carries exactly the PR's patch (per-filegit patch-id --stableequal).hasStackProvenanceis inpackages/spec/src/stack-provenance.ts, andSTACK_PROVENANCE_MISSINGis in the error-code ledger.os validateandos buildrefuse a default export thatdefineStack/composeStacksdid not build, before any further judgment. That includes a plain host-shaped{ plugins: [instance] }, per the ruling. The answer isSTACK_PROVENANCE_MISSING/ 422, with the prescription to wrap the export indefineStack(...).composeStacksrefuses an unbuilt input the same way.os serve/os migratekeep loading plain host configs.Fixes #20367closed this card ascompleted;pm:dispatchedis removed in this act. Its parked follow-up finding(spec+cli): the ADR-0087 conversionsdefineStackapplies never reachos validate/os build--jsonconversionsor the--strictgate — a retiring conversion passes--strictat exit 0 #20476 (OQ1: the conversionsdefineStackapplies never reach--json/--strict) returns topm:queuein the next act, since itsBlocked-by: #20367is met.Generated by Claude Code
feat(spec,cli)!: one stack authoring shape — os validate / os build r…
docs(spec): re-anchor the dead tracker citations left in data/ to the…
fix(trigger-api,service-automation): refuse an api flow with no per-f…
fix(spec,cli): os validate / os build read the ADR-0087 conversions d…
fix(cli): os validate / build / lint --json report the conversions a …
test(spec): the second root-level test file group's titles state each…