Skip to content

feat(core,cli,verify): bootStack composes what serve composes — item 1 stage 2 of #22301 (HELD at stop conditions) - #22381

Merged
objectstack-fleet[bot] merged 20 commits into
mainfrom
claude/issue-22301-item1-one-composition
Oct 9, 2026
Merged

objectstack-fleet[bot] merged 20 commits into
mainfrom
claude/issue-22301-item1-one-composition

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Part of #22301
Clause-②: yes (narrowing: a second live boot of the same configuration is refused, where it booted; widening: bootStack mounts the app's requires providers and plugins, and @objectstack/core exports the provider table)

Status: held draft. This is item 1 · stage 2 of #22301 under ruling 6070767186 (A: one composition rule, with the instance rule), patched per seat order 6071972897 (A and A) and the seat's CI note. Contract review PASS at 08929776c (6075048879). The merge round (seat note 6075062726) merged main three times, at 3054516ef, b9222dc70 and 2b61f2d9d, and carries the two text fixes. Contract review PASS at 8d985cf8f (6079992475). Merge round 2 (seat order 6080029242) merged main at c512c255c and swept the one direct showcase boot it brought. Head: 749197628.

What this branch carries

  1. The requires half. The token-to-provider table, its exact identity match and the package-owned collection reader move to @objectstack/core (capability-providers.ts, stack-collections.ts). Serve's statics are handles over them.
  2. The app's own plugins array is mounted by serve's entry rule, which now has one home: materializeStackPlugin in @objectstack/core (stack-plugins.ts). A string entry is a package specifier, a plain bundle is wrapped into AppPlugin, and an instance is itself. serve's boot loop reads the rule from there, and each boot injects its own loader.
  3. The caller wins by identity. An extraPlugins, security or analytics instance with the same name replaces the app's plugin, and the app's instance never runs. The app's plugins count as held for the requires resolver, and their hard dependencies are searched like a provider's.
  4. hostRoot is the app's root. It is the automation packageRoot, whether automation comes from requires or from automation: true, and it is the root a string entry resolves from. os verify now passes dirname(configPath) as hostRoot, the way serve anchors to its project. CI's Dogfood Verify CLI runs os verify from the repository root, and there the showcase's ./src/system/connectors/status-openapi.json resolved against the root (ENOENT). That one caller in packages/cli/src/commands/verify.ts is fixed.
  5. Loud failure. An entry that cannot be loaded or registered fails the boot, and the error names plugins[i].
  6. The instance rule, as ruled, with both keys. A second live bootStack of one configuration object is refused, and so is a copy that carries a mounted app-plugin instance. The refusal is RESOURCE_CONFLICT / 409. The refusal names three remedies: stop() first; bootStackOnce; or, to keep two stacks live, a configuration built again (its builder called once more, or a fresh module instance). A { ...config } spread is not one. Measured at 08929776c, a boot keeps live references into a configuration's nested definitions, and writes into one:
    • packages/objectql/src/registry.ts:2600 runs applySystemFields. That returns the authored schema itself (:651, :772, :876), or a new fields container whose values are the authored field objects (:780).
    • :2750 stores definition: { ...schema, name: fqn }, a shallow copy.
    • packages/objectql/src/engine.ts:7044 and :7216 write objDef.name = name into a map-form objects entry in place.
  7. OS_CLOUD_URL=off is stated in bootStack's docs and the changeset. packages/qa/dogfood declares it per project in its vitest env.
  8. The merge round's text fixes.
    • bootStackOnce's TSDoc now states the instance rule for a second options key. While the first boot of a config is live, a second opts key on that config is refused (RESOURCE_CONFLICT / 409), and the memo drops that key. To keep two stacks live, the second one needs a configuration built again.
    • docs/qa/platform-checklist/areas/platform-core.json, item platform-core.activation-ledger-registration-home, is now at revision 4. Its fixtures.requires, step 1 and harness source entry restate the no-automation composition as the dogfood suite builds it: a showcase-derived configuration that does not declare automation (packaged-activation-ledger-reach, showcaseWithoutAutomation). Revision 1's history entry keeps its text and gains a "superseded at revision 4" note. FOLLOW-UPS.md says the same.
  9. The cold-boot case in declared-position-provenance carries its boot's budget. The case reboots the showcase inside the it(), under vitest's 5000 ms default. On a loaded shard it timed out at 5225 ms; on quiet ones it read 2809, 2736 and 3752 ms. It now carries the 300_000 budget that beforeAll gives the same start(). It was the only in-case boot in the swept files without a budget. The other 12 already carried one.

The swept surface (packages/qa/dogfood/test/**, packages/verify consumers)

  • One owner for the showcase's test root. The new test/showcase-boot.ts exports bootShowcase(opts, config = showcaseStack), with hostRoot = examples/app-showcase. There is no inline hostRoot and no per-file cwd change. 122 files call it, at 133 call sites (the helper's own definition is not counted). That includes shared-showcase.ts and every file that landed on main during the rounds:
  • Tests that pinned the old composition now read the served one:
    • showcase-anonymous-deny-surfaces expects automation mounted, so a member gets 200.
    • schedule-sweep-organization-scope passes a stand-in under com.objectstack.trigger.time-relative in extraPlugins. The fixture's requires stays as authored.
    • showcase-declarative-endpoints serializes a copy without plugins.
    • Two more were masked in round 1 by the packageRoot failure:
  • Instance-rule consumers boot a configuration of their own, built again:
    • verify/handle.test.ts uses a fresh module instance of handle.fixture.js.
    • verify/harness.app-default-profile.test.ts calls an in-file builder.
    • parent-derived-write-refusal-not-visible, write-door-unreadable-is-not-found and predicate-write-unreadable-not-matched call per-boot builders, backed by buildAttCase … buildCmtReadonly builders added to fixtures/attachments-fixture.ts / comments-fixture.ts.
    • armed boots its second showcase stack from a fresh module instance, so it never shares plugin instances.

Measured at 8d985cf8f

measurement result
Dogfood 1/3 (OS_TEST_SHARD=1/3 pnpm turbo run test --filter=@objectstack/dogfood, VITEST_MAX_WORKERS=3 from vitest-worker-cap.mjs) 78 passed (78) files · 575 passed (575) tests
Dogfood 2/3 78 passed (78) files · 557 passed, 1 skipped (558) tests
Dogfood 3/3 77 passed, 1 skipped (78) files · 706 passed, 8 skipped (714) tests. The skipped file is rls-multitenant, skipped by its own describe.skipIf. The diff adds 0 skip lines.
CI's Dogfood Verify CLI step, run from the repo root as CI runs it crm and showcase both print ✓ verify passed — no runtime failures
CLI integration test/verify-host-root.test.ts 2/2 passed
@objectstack/verify 23 files / 187 tests passed
@objectstack/core --project local 84 files / 2237 tests passed
@objectstack/cli --project unit 274 files / 4038 tests passed
typecheck core, verify, cli, dogfood exit 0; test-typecheck debt unchanged
bootStackOnce TSDoc claim, one-off probe (not committed; harness.ts is unchanged since) with the first key's boot live, a second opts key on the same config is refused with RESOURCE_CONFLICT / 409. After stop(), the same second key boots, so the memo dropped it
gates: dispatch-gates --commands (89) all 89 exit 0. --ran: 89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN
check:pm-dispatch-gates ✓ dispatch-gates self-test: 2011 cases pass., exit 0
check:platform-checklist (derived for the docs/qa edit) exit 0: 16 areas, 279 items. Every symbol anchor resolves, including the new packaged-activation-ledger-reach.dogfood.test.ts#showcaseWithoutAutomation
eslint --no-inline-config over the 147 changed TS files 0 errors, 0 warnings. The config enables no type-aware linting, so the diff cannot change the verdict on a file it does not touch
CI at 8d985cf8f 36 check runs: 34 success, 2 skipped. Lint & Repo Gates is success, with 196 of 198 steps success, PM dispatch-gates self-test among them; the 2 skipped steps are Unmeasured-gate-tail reporter self-test and Report how many gates never ran. All three Dogfood Regression Gate shards, Dogfood Verify CLI and all six Test Core shards are success

The ablations (A1 to A7) were not re-run. Every line they cut is byte-identical between 08929776c and 8d985cf8f. The cut files are verify/src/harness.ts, core/src/stack-plugins.ts and cli/src/commands/verify.ts. Over those files, git diff 08929776c 8d985cf8f shows only the 8 new TSDoc lines in harness.ts.

The ablations as measured at 08929776c, through scripts/ablation-replace.mjs, each restored with blob == HEAD and git diff HEAD empty:

ablation red
app plugins never read 7 of 10
caller precedence removed the precedence pin
config-identity key removed the two-concurrent-boots pin
release on stop() removed the re-boot pin
plugin-copy guard removed the copy pin
core bundle wrap removed 2 of 5
os verify's hostRoot removed the verify-host-root pin, with the app's plugins[0] ('@fixture/verify-host-root-plugin') could not be loaded

#22422 is fixed on main (27a8b33de), and this branch contains it. The PM dispatch-gates self-test is green here and in CI.

Merge round 2, measured at 749197628. Measured lightly, per the order: the swept file, the shard that holds it, the derived gates and check:pm-dispatch-gates. CI answers the rest.

measurement result
position-environment-write-through alone (vitest run --project isolated) 1 file, 6 passed (6). The backfill case reboots twice after stop() and passes (4001 ms), so the instance rule releases on each stop(). The run printed 0 ENOENT lines, and nothing was written under examples/app-showcase
Dogfood 3/3, the shard that holds it (OS_TEST_SHARD=3/3, CI's command, VITEST_MAX_WORKERS=3) 77 passed, 1 skipped (78) files · 706 passed, 8 skipped (714) tests. The file passes 6/6 (backfill 6062 ms); the skipped file is rls-multitenant's own describe.skipIf
gates: dispatch-gates --commands (89, the same set as at 8d985cf8f) all 89 exit 0. --ran: 89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN
check:pm-dispatch-gates ✓ dispatch-gates self-test: 2011 cases pass., exit 0
eslint --no-inline-config on the swept file 0 errors, 0 warnings
tree-wide sweep count 0 direct showcase boots outside bootShowcase; the 4 remaining text matches are comments, plus the helper's own call
CI at 749197628 36 check runs: 34 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in))

Shards 1/3 and 2/3 and the verify, core and CLI tiers were not re-run locally; the merge changed none of this branch's files. It did bring main's own changes to packages/core/src/security (#22441), and CI's Test Core covers those.

Serial

main moved three times while the round measured, and each window was read against this branch's surface.

The other boots main added in these windows each boot a configuration once and stop it, or reboot only after stop():

  • dogfood: security-catalog-cold-boot-environment-holder, flow-runas, schedule-acting-organization, flow-door-elevated-start, managed-content-sealed;
  • verify: automation-trigger-elevated-door, action-flow-elevated-door.

All of them are green in the runs above. main has since moved to f66c440de (#22446, #22462, #22461). Those commits change two of this branch's swept files, showcase-anonymous-deny-surfaces (new API-description cases) and showcase-public-form (a SYSTEM read-back of the created id). git merge-tree --write-tree HEAD origin/main reports no conflict, and the would-be merged tree has 0 direct showcase boots: showcase-public-form keeps bootShowcase. That combination has not been measured here; the merge group measures it.

A dogfood file added later that boots the showcase directly will fail at boot (ENOENT), and the failure names the file. It should call bootShowcase.

Acceptance notes

  • hotcrm. A read-only read of 08cfa20 was done; its suite was not run against this branch. Six of its test files keep one boot of artifact live and boot it a second time. Under the instance rule they will be refused, with the remedy, once hotcrm takes this release:

    • case-assignment:766
    • contact-email-tenant-scope:134
    • flow-escalation-ownerless-case:58
    • flow-scheduled:887
    • hooks-runtime-service:583
    • hooks-runtime:272

    None of them needs hostRoot: hotcrm has no plugins array, and vitest's cwd is the app root.

  • When os verify runs from the repository root, the showcase's MCP stdio connector cannot spawn ./scripts/mcp-fixture.mjs, because that path resolves against the process cwd, not packageRoot. The connector is registered degraded with an ERROR line, and the verify still passes. The underlying difference is two anchors for app-relative paths. Filed by the seat as connector-mcp: a declarative stdio transport resolves its relative command against the process cwd, while the same app's OpenAPI file ref resolves against the package root — showcase_mcp_tools boots degraded from any other directory #22423.

  • activity-parent-read-gate › 'list: every row returned …' runs close to vitest's 5000 ms default. On this branch's shard runs it read 2328, 3696, 3770, 3305 and 2925 ms. Twice, on a box at load average 5 to 7 on 4 vCPU, it hit 5020 ms (at 91aff2e23 and 4a606ff8b). The file is outside this diff, and its fixture declares no requires and no plugins, so bootStack composes it exactly as before. This is an observation, not filed. Carrier: none.


Generated by Claude Code

claude added 5 commits October 8, 2026 23:23
…es names, by serve's own reader and table

Re-applies ef5396c (reverted at b3186ec pending the item-1 ruling) onto
current main. The `requires` token -> provider table and its exact identity
match move from the `Serve` command to `@objectstack/core`, beside the
package-owned collection reader `os serve` reads `requires` with (moved there
from the CLI's utils, which re-export it). `Serve.CAPABILITY_PROVIDERS` and
`Serve.providesCapability` become handles over the core declarations.

`@objectstack/verify`'s `bootStack` then constructs the providers the app's
`requires` names, skips any provider the boot already holds, and mounts the
always-on providers a mounted provider hard-depends on.

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
…'s entry rule; the instance rule

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
…helper anchors hostRoot; the dogfood run declines the marketplace

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 9, 2026
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/core, @objectstack/dogfood, @objectstack/verify, touching 58 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/core/src/index.ts, packages/qa/dogfood/vitest.config.ts, packages/verify/package.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via RESOURCE_CONFLICT (literal, a string literal in instanceRuleRefusal))
  • content/docs/api/error-handling-server.mdx (via RESOURCE_CONFLICT (literal, a string literal in instanceRuleRefusal))
  • content/docs/automation/webhooks.mdx (via com.objectstack.service.messaging (literal, a string literal in CAPABILITY_PROVIDERS; a string literal in messaging))
  • content/docs/deployment/cli.mdx (via os verify (command, read off packages/cli/src/commands/verify.ts))
  • content/docs/deployment/validating-metadata.mdx (via analyticsCubes (literal, a string literal in CAPABILITY_PROVIDERS; a string literal in analytics))
  • content/docs/getting-started/quick-start.mdx (via analyticsCubes (literal, a string literal in CAPABILITY_PROVIDERS; a string literal in analytics))
  • content/docs/permissions/record-view-auditing.mdx (via com.objectstack.audit (literal, a string literal in CAPABILITY_PROVIDERS; a string literal in audit))
  • content/docs/protocol/kernel/index.mdx (via com.objectstack.audit (literal, a string literal in CAPABILITY_PROVIDERS; a string literal in audit))
  • content/docs/protocol/kernel/lifecycle.mdx (via pluginName (symbol, a top-level function))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via BootOptions (symbol, a top-level interface))
  • content/docs/releases/v17/17-0.mdx (via bootStack (symbol, a top-level function))
  • content/docs/releases/v17/17-4.mdx (via os verify (command, read off packages/cli/src/commands/verify.ts))
  • content/docs/releases/v17/17-5.mdx (via analyticsCubes (literal, a string literal in CAPABILITY_PROVIDERS; a string literal in analytics))
  • content/docs/releases/v17/17-6.mdx (via RESOURCE_CONFLICT (literal, a string literal in instanceRuleRefusal), analyticsCubes (literal, a string literal in CAPABILITY_PROVIDERS; a string literal in analytics), os verify (command, read off packages/cli/src/commands/verify.ts))
  • content/docs/releases/v17/17-7.mdx (via RESOURCE_CONFLICT (literal, a string literal in instanceRuleRefusal), analyticsCubes (literal, a string literal in CAPABILITY_PROVIDERS; a string literal in analytics), os verify (command, read off packages/cli/src/commands/verify.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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

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

Which tree this was computed on

This run read content/docs from f574862bb9c87ada4e2800df737e716d8e5b1131 — the merge of head 749197628fc07c97865c2389e3d4f6cb002ca7bf into base c512c255c5c62467a697619c0643d5a329e20f10, which is what actions/checkout gives a pull_request run. Not the PR head.

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

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

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

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

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

claude added 2 commits October 9, 2026 00:50
…rs of the instance rule boot a configuration built again

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
…op a doubled helper import

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
claude added 3 commits October 9, 2026 01:37
…onfiguration that does not declare automation

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
…em1-one-composition

# Conflicts:
#	packages/qa/dogfood/test/permission-set-discard-overlay-eligibility.dogfood.test.ts
…s through bootShowcase

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 08929776c764ac608cbc14ee2eaff300ce57c269
Local-runs: none

Isolated at-tier review of PR #22381 (item 1 stage 2 of #22301, ruling 6070767186 letter A, seat order 6071972897 A and A). Inputs: card #22301 body and all 16 comments, the PR body, its file list and the net diff against the merge base 00bee2724 (149 files, +2503 / -892, 0 governed paths, head repo = base repo), and the check-runs on the head. Nothing built, run or re-run. Read at 2026-10-09T05:39Z.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged:

  1. @objectstack/core public surface widens by eleven exports — CAPABILITY_PROVIDERS, CapabilitySpec, CapabilityIdentities, providesCapability (capability-providers.ts); stackDeclaredCapabilities, resolveStackCollection, declaredPackageEntries, stackPackageBodies, collectFromPackageBodies (stack-collections.ts); materializeStackPlugin, StackPluginLoaders (stack-plugins.ts). The table and the identity match are the text removed from serve.ts, and serve-capability-identity.test.ts pins Serve.CAPABILITY_PROVIDERS and Serve.providesCapability as the SAME objects, so no second copy can pass. Home judged RIGHT: @objectstack/cli depends on @objectstack/verify, both depend on core, and none of the 19 provider packages verify now pins names verify, cli or dogfood as a dependency (@objectstack/organizations names verify as a devDependency only), so no cycle and no new package edge.
  2. @objectstack/cli public surface unchanged. The Serve statics are handles over the core declarations; src/utils/stack-collections.ts re-exports the three names it exported before (declaredPackageEntries, resolveStackCollection, stackDeclaredCapabilities) and its previously private helpers moved without becoming CLI exports. One text drift, judged immaterial: the defensive fail-loud message in declaredPackageEntries now reads "the stack collection readers" instead of "the CLI package readers"; no test pins the string. Observation: the static keeps the mutable Record type over core's Readonly table; no test or caller mutates it. RIGHT.
  3. serve's plugins loop now reads materializeStackPlugin with its own loaders — a string entry is still host-anchored through the literal Serve.importConfigPlugin(plugin, hostRoot) (the two source pins hold), an object without init is still wrapped into AppPlugin with the same fall-through to the bare bundle when @objectstack/runtime cannot be imported, an instance is still itself. Behaviour-preserving; stack-plugins.test.ts pins each shape and which loader it reaches. RIGHT.
  4. bootStack accept-set WIDENS (ruling A): (a) the providers the app's requires names, read by stackDeclaredCapabilities (top-level list, else each package body's — the reader serve uses), resolved through the same table and the same exact-identity "held" rule, where held = the harness's settings, analytics and sharing services, the opts.automation instance, every extraPlugins entry and every app plugin (required-providers.ts); (b) the app's own plugins array by the shared entry rule, a string entry resolved from hostRoot through the same host importer the multi-tenant import uses; (c) hostRoot is the automation packageRoot for both requires: ['automation'] and automation: true, the anchor serve passes as path.dirname(absolutePath). Pinned in harness.required-providers.test.ts (top level / package bodies / top level wins / control / explicit wins / hard dependencies triggers brings job and queue) and harness.one-composition.test.ts. RIGHT.
    What stays each boot's own is declared in required-providers.ts and the changeset, and is named here as the remaining composition gap, not a defect of this diff: providers are constructed with defaults (no email / sms / storage configuration, no analyticsCubes — the folded later stage), and the always-on slate (queue, job, cache, settings, email, storage, sms) is mounted only where a mounted plugin hard-depends on it, where serve force-appends it to every app. Within the ruling's letter (what requires names and what plugins holds).
  5. Caller precedence by identity. extraPlugins, security and analytics instances win over an app plugin of the same name (the kernel's identity — a class-name match would drop every bundle, since each becomes an AppPlugin), and over a requires provider they ARE (exact name or class name). Pinned (caller runs, app never inits; a stand-in under com.objectstack.service.approvals keeps the real provider out); ablation A2 red in the report. RIGHT.
  6. NARROWING — loud failure. An app plugins entry that cannot be loaded or registered fails the boot naming plugins[i] with the remedy; serve logs and boots on. The divergence is the ruling's own sentence ("a plugin that cannot be mounted fails the boot loudly with its remedy"). Pinned (a package the app root cannot resolve, the message names the entry and the hostRoot). RIGHT.
  7. NARROWING — the instance rule, both keys as ordered. claimConfiguration keys on the configuration object (a WeakSet) and holdAppPlugins on the mounted app-plugin instances (a second WeakSet), so a { ...config } copy carrying a live instance is refused too. The refusal is RESOURCE_CONFLICT / 409, a standard-catalog member (errors.zod.ts). The claim is taken synchronously before the first await (two boots started together: exactly one refused — pinned), released on any boot failure and at stop() (idempotent; the release runs after the best-effort close and shutdown). bootStackOnce shares the one promise, so it takes no second claim, and a failed shared boot drops both its memo and its claim. The remedy names the three routes, the third spelled "built again" on the measured nested-definition references the PR body cites by file and line (registry.ts shallow copy with authored field objects; engine.ts in-place objDef.name write). RIGHT, as ruled and as the seat's Q2 = A ordered.
  8. Registration order differs from serve (verify: providers in the extraPlugins slot, app plugins after the sharing service; serve: app plugins before the resolver's providers). Immaterial: ObjectKernel.use only registers, init order is resolvePluginOrder over declared dependencies, and the one order-sensitive case — a same-name tie — resolves the way serve's rule does (the app's instance supersedes a boot-composed one). RIGHT, observation only.
  9. os verify passes hostRoot: dirname(absolutePath) on both boots — the fix for the round-1 Dogfood Verify CLI red (the showcase's ./src/system/connectors/status-openapi.json resolved against the repository root). Pinned by a spawned run from a foreign cwd (verify-host-root.test.ts); ablation A7 red. Side effect stated in the changeset: --multi-tenant resolves the organizations package from the app's directory, as serve does. RIGHT.
  10. @objectstack/verify dependency closure grows by 19 workspace packages so every table row is importable from verify; pinned by the table-versus-manifest test; lockfile updated accordingly. RIGHT.
  11. OS_CLOUD_URL=off is a real declined spelling (cloud-url.ts: off, none, local, disabled), stated in bootStack's docs and the changeset; dogfood sets it per vitest project (the seat-accepted deviation). RIGHT.
  12. The dogfood sweep. One owner, test/showcase-boot.ts, with hostRoot = examples/app-showcase spelled fileURLToPath(new URL('../../../../examples/app-showcase/', import.meta.url)) — a path to a package dogfood already declares (@objectstack/example-showcase), so the read is graph-visible. 120 files call bootShowcase; no inline hostRoot, no chdir added; no bootStack(showcaseStack remains outside comments. Four mechanical files sampled: import swap plus call swap only. The five OTHER files re-pin the served composition with a premise each (hasPlugin('com.objectstack.service-automation') then 200; a stand-in under com.objectstack.trigger.time-relative in extraPlugins with the fixture's requires as authored; a plugin-free serialisable copy; hasPlugin('package-service') then 400 MISSING_REQUIRED_FIELD; a showcase-derived configuration without automation and without the connector plugins that hard-depend on it). The three dogfood instance-rule consumers and the two verify consumers boot configurations BUILT AGAIN (fixture builders, a fresh module instance of handle.fixture.js); armed boots a fresh module instance. RIGHT, and Dogfood Regression Gate is green on all three shards at this head.
  13. Hand-written docs. No content/docs page states the old lean composition (deployment/cli.mdx, deployment/validating-metadata.mdx and a corpus grep for bootStack / extraPlugins read clean; the only hit is a release page, read-only). The drift bot's rows are advisory. Nothing goes false. RIGHT.
  14. One doc gap, not false but incomplete: bootStackOnce's TSDoc still says a different opts object "is a different stack" without saying that, on a live configuration, that second stack is now refused by the instance rule. The behaviour is documented on bootStack and in the changeset; a one-line TSDoc follow-up is owed, not a blocker.

② Semver level

Changeset .changeset/22301-verify-boots-what-serve-boots.md: @objectstack/core: minor, @objectstack/verify: minor, @objectstack/cli: patch; the body carries Clause-②: yes (narrowing) and an adr-0087 marker with category not-required (no-migration-prescription). @objectstack/dogfood is private: true and needs none.

  • core minor: eleven new exports, nothing removed or narrowed. RIGHT.
  • verify minor: a widening (requires providers, app plugins, hostRoot as packageRoot) and a BREAKING narrowing (the instance rule; an unloadable or unregistrable app entry fails the boot; a provider that refuses to start fails the boot), shipped minor under the launch-window convention check-changeset-no-major.mjs enforces. The body carries its migration in the form this change admits: the three remedies, why a { ...config } spread is not one, and the offline switch. RIGHT.
  • cli patch: its own diff is a fix (hostRoot) plus a refactor to handles with no surface change; os verify's composition change rides on the verify minor the CLI pins at workspace:*. RIGHT.
  • ADR-0087 disposition not-required (no-migration-prescription): the only honest category. Nothing authorable or stored moves — no spec key, no stored shape, no conversion entry — so registered / already-registered have no id to name, unpublished is false (all three bumped packages publish), and runtime-interface-only would need bootStack unreferenced in code. The remedy prose is a consumer-code remedy, not a FROM → TO rewrite, which is the shape that category's own refusal reads. The step "Require an ADR-0087 disposition on a declared-breaking changeset" is green. RIGHT.
  • Clause-②: line. The PR body's line is Clause-②: yes (narrowing: … ; widening: …). The reader (scripts/pm/clause2-line.mjs, readArmToken) takes the first arm token, narrowing, and leaves the rest of the parenthetical as reasoning, so the PR declares yes with arm narrowing — the same declaration the changeset carries and the claim 6070970729 made. Check Changeset is green on both runs. One arm read; RIGHT. A bare yes (narrowing) would be the cleaner spelling, and no reader misreads this one.

③ Boundary flags

Dev flags (reports 6071911155, 6074859576) and the PR body's acceptance notes, each answered or escalated:

  1. hotcrm — six test files keep one boot of artifact live and boot it again; refused with the built-again remedy once hotcrm takes this release. Read-only at hotcrm 08cfa20; the suite is NOT MEASURED against this branch, with the reason the seat order's own "if it cannot" clause accepts. Carrier: the changeset's remedy paragraph (it ships as CHANGELOG text an upgrading agent greps), the PR acceptance notes and the seat's round report. ANSWERED — the ruling took the instance rule knowing the consumer.
  2. docs/qa/platform-checklist/areas/platform-core.json (two items) and FOLLOW-UPS.md describe the no-automation composition as bootStack(showcaseStack) with automation omitted, which this diff makes false. Acceptance note; the seat names "the next edit of that area's checklist" as the carrier. ANSWERED with one reservation, ESCALATED to the seat: a checklist item is a procedure the checklist runner executes, so until rewritten it is a trap for that runner — a finding card is the safer carrier than the next edit. Internal QA docs, non-governed, not blocking.
  3. MCP stdio connector anchors its relative command on the process cwd while the OpenAPI ref anchors on packageRoot — filed as connector-mcp: a declarative stdio transport resolves its relative command against the process cwd, while the same app's OpenAPI file ref resolves against the package root — showcase_mcp_tools boots degraded from any other directory #22423 (open). ANSWERED.
  4. analyticsCubes parity — a later stage on this card, folded by the stage-1 report. ANSWERED.
  5. Stage-2 open questions Q1 / Q2 — seat order 6071972897 answered A and A; implemented as ordered (one helper, no inline hostRoot, no cwd change; both keys kept, consumers built again, the third remedy from a read with file and line). The final report's open_questions is empty. ANSWERED.
  6. PR body not PATCHed by the dev (role file) — the seat applied the proposed body; the live body is the proposed text. ANSWERED.
  7. Surface beyond the order's literal list (showcase-boot.ts, two more OTHER files, two fixture builders, verify.ts and its test) — inside the amended surface or the seat's CI note; 0 governed paths; 3395 changed lines, under 5000. ANSWERED.
  8. Two PID files at the container's filesystem root — environment hygiene outside the repository, not a contract matter; left for the seat.
  9. Named here, not flagged by the dev as such: the remaining divergence in ①.4 (the always-on slate not force-mounted; providers constructed with defaults) and the bootStackOnce TSDoc gap in ①.14. ESCALATED to the card as later-stage notes; neither contradicts the ruling.

Checks on 08929776c, every one named:

  • Red: Lint & Repo Gates — one step, "PM dispatch-gates self-test", 1 of 2011 cases, on security-catalog-cold-boot-environment-holder.dogfood.test.ts:108, a file this diff does not touch; red on main; tracked as ci(pm): check:pm-dispatch-gates is red on main — its live-tree case reads mkdtempSync(join(process.cwd(), …)) in a #22365 dogfood test as an unresolved temp base #22422. Judged: not this diff. Its consequence on this head is the material part: every Lint step after it (about 150, from "ROOT_DIR_WATCH_HINTS declarations are literals" through "Duration-shaped spec keys carry their unit", "Cross-package test inputs", "Changeset-family gate self-tests", "Startup registry-verdict guard" and "workspace manifest dependency graph has no cycle" included) is skipped because a prior step failed — they carry no if:, and only the if: failure() reporter ran. Those families have NO check-run verdict on this head. The only reading for them is the dev's local battery (87 derived commands, all exit 0, report 6074859576), which this record does not adopt as a gate verdict. So this head cannot enqueue as it stands; the seat's plan — merge main once ci(pm): check:pm-dispatch-gates is red on main — its live-tree case reads mkdtempSync(join(process.cwd(), …)) in a #22365 dogfood test as an unresolved temp base #22422 lands, and a fresh record on the merge head unless the merge is pure regeneration — is the right one, and the merge head's Lint run must be read in full, not only its first 35 steps, before enqueue.
  • Skipped by design: Console Pin Gate (no pin change), Packed-tarball smoke (opt-in), and Auto Label / Check PR Size on the second workflow run (both green on the first).
  • Green: Build Core, Build Docs, Check Changeset (both runs), Check Documentation Links, Check PR Size, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue (both), No other open PR may claim the same single-writer path (both), Part-of PR must not also close its card (both), Spec property liveness, Temporal Conformance (live PG + MySQL), Test Core and its six shards, The card this PR closes must claim this branch (both), Type Check · consumer gates / debt ledger / source gates / workspace, TypeScript Type Check, Validate Package Dependencies, filter; the Vercel status is success.
  • Pending: none — every check-run on the head had completed when read.

The verdict below is on the contract at this head: the diff executes ruling A with the instance rule as ordered, the changeset says what the diff publishes at the right levels, and every flag is answered or carried. It does not vouch for the Lint families that never ran here; those are the merge head's to show green.

Implemented-by: claude/issue-22301-item1-one-composition
Reviewed-by: session_01DhTqaEHqPVSVnAkjG3jywn

VERDICT: PASS

claude added 2 commits October 9, 2026 06:24
…ition, as #22301 now has them

The bootStackOnce TSDoc states that a second options key on a configuration
whose first boot is still live is refused by the instance rule. The
platform-core activation-ledger item (revision 4) and FOLLOW-UPS restate the
no-automation composition as the dogfood suite builds it: a showcase-derived
configuration that does not declare automation.

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…ℹ glyph (objectstack-ai#22409)

Fixes objectstack-ai#22255

Clause-②: no

## What changes

The startup banner's `Flows:` section prints one line per unbound-flow
class (trigger type, reason), and every class printed as a yellow `⚠`. A
class whose reason is the deployment's scheduled-work sentence (flows
left unbound only because package-authored scheduled work is off, the
documented default) now prints **dim, under `ℹ`**. It keeps its count,
trigger type, first sentence and flow list. Every other reason keeps its
yellow `⚠`: a missing trigger, a binding failure, anything else. The
unknown-target-object and shadowed-flow lines are untouched.

This applies the routing of record (triage `6057431712`), which executes
the maintainer's direction on objectstack-ai#22160, quoted verbatim: 「预期中的降级记 info」.

- `packages/cli/src/utils/format.ts`: a new
`isDeploymentPolicyClass(reason)` and the class loop in
`printAutomationSummary`. Only the glyph and the color depend on the
class. The line's text, the class order and the records handed back to
*Boot diagnostics* (`restatedAbove`) are unchanged.
- ⛔ Not changed: the automation plugin's binding, the scheduled-work
switch, `@objectstack/service-automation`'s own log level, `serve.ts`
and `packages/spec`.

## How the class is told apart: identity, not prose

`isDeploymentPolicyClass` is `reason === SCHEDULED_WORK_DISABLED_REASON`
(`@objectstack/types`). Measured on `origin/main` at `11d119ab1`:

- The engine records `scheduledWorkDisabledReason(policy)` from the
policy reading that refused the bind (`activateFlowTrigger`).
`describeUnboundReason` reads that record back for
`getTriggerBindingAudit`, and `collectAutomationSummary` passes it
through unchanged.
- For every policy the environment resolves, that function returns the
constant byte for byte: `resolveScheduledWorkPolicy` never sets
`hostDisabledReason` (pinned in `env.test.ts`).
- The test file's real-producer leg boots the real
`AutomationServicePlugin` on a `LiteKernel` and reads the summary
through `collectAutomationSummary`. Its schedule line is now asserted to
start with `ℹ`, so the identity is pinned against what the producer
actually records. If the producer is reworded, the line goes back to
`⚠`, which is the loud direction.
- The check does not call
`scheduledWorkDisabledReason(resolveScheduledWorkPolicy())`. On that
reading the call returns the same constant by construction, and the
resolver throws on an unrecognised `OS_TENANCY_POSTURE`, which a printer
must not do.
- No structured kind was needed. So neither `serve.ts` (held by PR
objectstack-ai#22381) nor the spec contract is touched.

## Pins (`format.boot-warning-classes.test.ts`)

- **Real boot** (eight scheduled flows, switch off): the one schedule
line now starts with ` ℹ 8 flows declare `.
- **Policy class:** prints dim (opening SGR `ESC[2m`, no yellow) under
`ℹ`, with its flows and first sentence. Controls: a missing trigger and
a binding failure keep a yellow (`ESC[33m`) `⚠`.
- **⛔ Identity, not words:** three reasons that only quote the policy
all keep `⚠`. They are a binding failure carrying the policy's first
sentence, the whole sentence inside a longer record (the schedule
trigger's own refusal shape), and the first sentence alone.
- **`restatedAbove` is the same under both glyphs:** a policy flow, a
missing-trigger flow and a binding-failure flow, each with its producer
audit record. Every flow is named once, and no *Boot diagnostics* block
prints.
- The existing first-sentence pin moves from `⚠` to `ℹ`.

## Evidence (head `62c86cba9`, branched from `11d119ab1`)

**Public door.** `objectstack dev --seed-admin --fresh -p 38421` on
`examples/app-showcase`, with the CLI built from this head. The Flows
section, SGR stripped and the shared sentence cut to an ellipsis:

```text
  Flows:   30 flow(s) 20 bound to triggers (record_change, schedule, time_relative, api) · 7 draft
  ℹ 1 flow declares a 'time_relative' trigger but is NOT bound — disabled by deployment policy — … : showcase_task_due_reminder
  ℹ 1 flow declares a 'schedule' trigger but is NOT bound — disabled by deployment policy — … : showcase_scheduled_digest
      reasons cut to their first sentence — --log-level debug prints each flow's full reason
  Seeds:   com.example.showcase 132 rows

  ℹ Boot diagnostics — 1 informational (2 more already listed above):
```

Both class lines open with the dim SGR (`ESC[2m`) in the raw capture,
and *Boot diagnostics* still withholds the two restated records. That
boot printed two other `⚠` lines, both about this worktree having no
console build (`packages/console/dist` and the SDUI manifest). They come
from the environment, not from this change.

**Tests and gates**, all at `62c86cba9`:

| Check | Result |
|:---|:---|
| `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2` | 270 files, 3971 tests passed, lock `VERDICT
command-exit 0` |
| `format.boot-warning-classes.test.ts` alone | 21 of 21 passed |
| `pnpm --filter @objectstack/cli typecheck` | exit 0 (`tsc --noEmit`,
then `check:test-typecheck` over `tsconfig.test.json`) |
| `pnpm lint` (full: `eslint . --no-inline-config`) | exit 0, no
findings |
| derived gate families | `dispatch-gates --ran`: 64 derived, 64 run, 0
NOT-MEASURED, 0 UNRUN (a derived zero, every exit code recorded) |

The integration layer is declared to CI: the diff reaches no integration
file and no spawn entry.

**Ablation.** Run on the committed fix with
`scripts/ablation-replace.mjs`. In each leg the anchor went from 1 hit
to 0, and the restore brought the blob back to `5804f510beed`, equal to
HEAD, with `git diff HEAD` empty.

- **Leg 1**, identity forced to `false`: 3 failed, 18 passed. The
failures are the real-boot `ℹ` pin, the first-sentence pin and the
dim/`ℹ` pin.
- **Leg 2**, identity replaced by `reason.includes('disabled by
deployment policy')`: 1 failed, 20 passed. The failure is the identity
pin.

The test imports `./format.js`, which vitest resolves to
`src/utils/format.ts`, so neither leg depended on a `dist` rebuild.

## Acceptance notes

- **Host-injected policy.** A per-kernel `ScheduledWorkPolicy` that
carries its own `hostDisabledReason` keeps `⚠`. That sentence is the
host's, not the documented default, and the printer cannot know it. For
this banner the case is dormant: `git grep` finds 0 non-test producers
of `hostDisabledReason:` in the tree, against a positive control of 4
hits in test files. Telling that class apart would need a structured
kind on the audit row (the spec contract plus `serve.ts`), which this PR
does not add.
- **Contract wording.** `FlowRuntimeState.reason` in the spec contract
says consumers render the reason and do not parse it. An equality check
against the exported producer constant is not parsing, and no prose
match was added.
- **Stale prose this change makes inaccurate, left unedited** (outside
the claim's file surface):
- `docs/qa/platform-checklist/areas/platform-core.json`
acceptance[2].verify describes the policy lines as `⚠ … disabled by
deployment policy`. Its clause (fail on the misauthored `⚠` classes)
still holds, and is now easier to apply.
- The still-pending
`.changeset/22073-boot-warning-one-line-per-class.md` quotes the class
with `⚠`. This PR's changeset states the move to `ℹ`, and the two would
ship in the same release.
- **Line order is unchanged** (first-seen). A dim policy line can still
print above a `⚠` class: the one-line-per-class order belongs to the
earlier ruling and was not re-ruled.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01BmsuLyUeuG5CNpZFMH1jzS)_

Co-authored-by: Claude <noreply@anthropic.com>
claude added 4 commits October 9, 2026 07:30
…em1-one-composition

# Conflicts:
#	packages/qa/dogfood/test/showcase-object-extension-scalar-divergence.dogfood.test.ts
…s budget

The case reboots the showcase inside the it(), under vitest's 5000 ms default.
bootShowcase now composes the showcase as os serve does, and that reboot read
2809 ms quiet and timed out at 5225 ms on a loaded shard. It now carries the
300_000 budget beforeAll gives the same start(), as every other in-case boot
in the swept files already does.

Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn
Co-authored-by: Claude <noreply@anthropic.com>
…through bootShowcase

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8d985cf8f677c049e55775db4113222fdcf0d78c
Local-runs: none

Isolated at-tier review of PR #22381 at the merge-round head (item 1 stage 2 of #22301, ruling 6070767186 letter A, seat order 6071972897 A and A, merge-round note 6075062726). Inputs: card #22301's body and all 18 comments; the PR body, its 152-file list and the net diff against the merge base 2b61f2d9d (+2539 / -903, 3442 changed lines, 0 governed paths, head repo = base repo, draft, auto-merge not armed); the check-runs on the head; and main fetched into a private ref for the sweep questions. Nothing built, run or re-run. Read at 2026-10-09T11:30Z.

① Derived judgments

The round's delta, measured. The branch-side hunks of the net diff at this head (vs 2b61f2d9d) were compared file by file with the net diff the standing PASS 6075048879 judged at 08929776c (vs 00bee2724, 149 files). 146 files are hunk-identical, serve.ts, pnpm-lock.yaml and every core, verify and cli source among them (main's console not-built mount auto-merged outside the plugin loop). Three files are new and three differ, and they are exactly the five changes the round names: the bootStackOnce TSDoc, the two docs/qa/platform-checklist restatements, the declared-position-provenance budget, the storage-unclaimed-download sweep and the showcase-object-extension-scalar-divergence resolution. Nothing else moved. Each judged:

  1. bootStackOnce TSDoc (text fix a) — true to the code. bootStack is async, and claimConfiguration throws the RESOURCE_CONFLICT / 409 refusal synchronously inside it, so the call returns a rejected promise; bootStackOnce stores that promise under the second opts key, and the catch handler it attaches to every stored boot deletes that key from the memo map on rejection, so a later caller boots again; the claim is released at stop(). The eight added lines say exactly that and name the built-again remedy. The dev's one-off probe corroborates; it is not adopted as a gate verdict and does not need to be. RIGHT. This closes the prior record's ①.14 gap.
  2. The checklist restatements (text fix b) — true to the code, and the right lifecycle shape. The live fields a runner executes — fixtures.requires (:1446), step 1 and the harness.ts#bootStack source entry — now describe the no-automation composition as the suite builds it, and the new anchor packaged-activation-ledger-reach.dogfood.test.ts#showcaseWithoutAutomation resolves to a builder that takes the automation token out of the showcase's requires, drops the plugins that depend on com.objectstack.service-automation, and boots through bootShowcase; the file's first test is still the anti-vacuity control. FOLLOW-UPS.md:404 says the same and states why the old spelling no longer stages it. The item moves revision 3 to 4 with a history entry, which is the README's Change rule ("edit the fields, bump revision, append a history entry") and what the validator checks (revision equals the last history entry). Keeping revision 1's entry with a bracketed "superseded at revision 4" note is correct: a history entry is a record of what that revision did, not a procedure a runner follows, the ledger is append-only (the README makes deletion a review-time offence), the original text is preserved, and the note stops a reader taking it for the current shape. Rewriting the historical text, the seat's one-line alternative, is not needed. check:platform-checklist is a Lint & Repo Gates step, green on this head. docs/qa/** is not a governed surface. RIGHT. This closes the prior record's ③.2 escalation.
  3. declared-position-provenance budget — the cold-boot case reboots the showcase inside its it(); it now carries 300_000, the same budget the file's beforeAll gives the same start(), with the reason in a comment. Test-only, inside the amended surface. RIGHT.
  4. storage-unclaimed-download sweep — import swap plus call swap, security and extraPlugins kept as fix(service-storage)!: downloading a file with no attachments scope and no field owner requires a signed-in caller #22439 wrote them. RIGHT.
  5. showcase-object-extension-scalar-divergence resolution — feat(metadata-protocol)!: managed content is sealed — OS_METADATA_WRITABLE no longer opens an item a managed package ships (ADR-0131 D6, #15206 S2) #22401's boot() helper is kept as main landed it, with bootStack(showcaseStack, { re-pointed to bootShowcase({ (+3 / -2); the showcaseStack import stays because the file still reads it at :172, :186 and :243; the second boot follows a stop(), so the instance rule holds (the file is green in the dogfood gate). RIGHT.
  6. Sweep completeness at this head. Over the whole tree, bootStack(showcaseStack / bootStackOnce(showcaseStack appears in no code: only in comments, CHANGELOGs and the two checklist files (where it is named to say it is NOT the composition). Every dogfood test that imports @objectstack/example-showcase, the ten that import onEnable beside it and boot a derived configuration included, boots through bootShowcase; shared-showcase.ts does too. 121 files import the helper, 132 call sites, the body's count. Every remaining direct bootStack( in dogfood passes a fixture stack (crmStack, fixtureStack, probeApp, builders), never the showcase. bootShowcase names hostRoot once, examples/app-showcase, and overrides a caller's. COMPLETE.
  7. main since the last merge. main is at d87dff67c, six first-parent commits past the base (3ca71b6e0, 591948347, c7a3234a7, 33ae4bff0, 587bd9699, d87dff67c; the brief's 587bd9699 plus one). 39 files, none in the PR's 152; no added line contains bootStack, bootStackOnce, showcaseStack, bootShowcase, extraPlugins, hostRoot, CAPABILITY_PROVIDERS, materializeStackPlugin, stackDeclaredCapabilities, @objectstack/example-showcase or @objectstack/verify; packages/core is touched only under src/security/ (request-grants memo), not index.ts or the three moved modules; no dogfood, verify or cli file. So none of the six adds a showcase boot or an instance-rule consumer, and none touches this surface. They do touch .claude/** (agent and pm-dispatch skill files), .github/workflows/ci.yml and scripts/partition-test-shards.mjs (ci(test-shards): slice the CLI 3 ways at plain weight under a density cap #22456, CLI shard slicing): main's own landings, outside the PR's delta, and a merge adds none of them to it. A re-merge is not owed for correctness; the queue rebuilds on current main either way.
  8. The 146 unchanged files are judged by the standing PASS 6075048879 on byte-identical hunks, re-affirmed here on the load-bearing points: the eleven @objectstack/core exports arrive through three export * lines; the three moved modules import nothing from cli or verify (stack-collections.ts imports only ./artifact-packages.js); verify's 17 new workspace:* dependencies include none that lists verify, cli or dogfood as a runtime dependency, so no cycle; the instance rule's claim is taken before the first await, released on any boot failure and at stop(), with both keys (configuration identity, mounted app-plugin instances) and the three-remedy text. The remaining composition gap the prior record named in its ①.4 (always-on slate not force-appended, providers built with defaults, analyticsCubes) is unchanged and stays a later stage.

② Semver level

Changeset .changeset/22301-verify-boots-what-serve-boots.md is byte-identical to the judged head: @objectstack/core: minor, @objectstack/verify: minor, @objectstack/cli: patch; Clause-②: yes (narrowing) in the body; the ADR-0087 marker not-required (no-migration-prescription) with its category closure. The round's additions publish nothing new: the TSDoc rides inside verify's published dist under the verify minor; the checklist files and dogfood tests are unpublished (@objectstack/dogfood is private). Not skip-changeset. RIGHT.

Clause-②: line. The PR body carries Clause-②: yes (narrowing: …; widening: …); the reader (scripts/pm/clause2-line.mjs) takes the first token inside the parenthetical as the arm, so the PR declares yes with arm narrowing, the same declaration the changeset and the claim 6070970729 carry. Check Changeset is green on both workflow runs. RIGHT.

③ Boundary flags

Merge-round report 6079800949: open_questions is empty. Its deviations and findings, each:

  1. Three merges of main, not one — each through os-regen-merge.sh, never a rebase; the only conflict is the scalar-divergence file, resolved as judged in ①.5; pnpm-lock.yaml hunks unchanged. ANSWERED.
  2. Revision 1's history entry annotated rather than rewritten — correct, ①.2. ANSWERED.
  3. Surface beyond the two text fixes (the budget, the sweep) — inside packages/qa/dogfood/test/**, the surface the seat order amended into the claim, and what an enqueue-able merge head needs. ANSWERED.
  4. PR body not patched by the dev — the seat applied the delta; the live body reads "Measured head: 8d985cf8f" with the merge-round table and the three-window serial read. ANSWERED.
  5. activity-parent-read-gate near its 5000 ms budget under load — outside the diff, its fixture declares no requires and no plugins, so this PR does not change its composition; green on all three shards here. An observation, carrier none; recorded, not blocking.
  6. hotcrm's six double-boot files — unchanged from the judged head; carrier the changeset's remedy paragraph (ships as CHANGELOG text) and the seat's round report. ANSWERED.
  7. connector-mcp: a declarative stdio transport resolves its relative command against the process cwd, while the same app's OpenAPI file ref resolves against the package root — showcase_mcp_tools boots degraded from any other directory #22423 (MCP stdio transport anchored on the process cwd) — filed, unchanged. ANSWERED.
  8. Two PID files at the container's filesystem root — environment hygiene outside the repository, unchanged, for the seat. Not a contract matter.
  9. From the prior record: ①.14 (TSDoc gap) and ③.2 (checklist as a runner trap) are CLOSED by this round; ①.4's later-stage notes stand.

Checks on 8d985cf8f, every one named. 43 check-runs: 39 success, 4 skipped, 0 failure, 0 pending; the combined commit status (Vercel) is success.

  • Red: none.
  • Pending: none — every check-run had completed when read.
  • Skipped by design: Auto Label and Check PR Size on the second workflow run (both success on the first), Console Pin Gate (no pin change), Packed-tarball smoke (opt-in).
  • Green, the seven required contexts: Lint & Repo Gates, TypeScript Type Check, Test Core (and its six shards), Dogfood Regression Gate (and its three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard.
  • Green, the rest: Build Docs, Check Changeset (both runs), Check Documentation Links, Check PR Size, Dogfood Verify CLI, Flag docs affected by code changes, No other open PR may claim the same issue (both), No other open PR may claim the same single-writer path (both), Part-of PR must not also close its card (both), Spec property liveness, The card this PR closes must claim this branch (both), Type Check · consumer gates / debt ledger / source gates / workspace, Validate Package Dependencies, filter.
  • Lint & Repo Gates read in full, as the prior record required of the merge head: job 113766125203, 198 steps, 196 success, 2 skipped (Unmeasured-gate-tail reporter self-test and Report how many gates never ran, the if: failure() reporters, skipped because nothing failed). Step 35 PM dispatch-gates self-test is success (ci(pm): check:pm-dispatch-gates is red on main — its live-tree case reads mkdtempSync(join(process.cwd(), …)) in a #22365 dogfood test as an unresolved temp base #22422's fix 27a8b33de is in this head), so the roughly 150 families that had no verdict at 08929776c now carry one.

The verdict is on the contract at this head: the delta is the judged one plus the five named changes, each right; the two text fixes are true to the code and the checklist change is in its lifecycle's shape; the sweep is complete against the merged tree and against main as it stands; the changeset says what the diff publishes at the right levels; every flag is answered or carried; and every check-run on the head has a conclusion, none red.

Implemented-by: claude/issue-22301-item1-one-composition
Reviewed-by: session_01DhTqaEHqPVSVnAkjG3jywn

VERDICT: PASS

claude added 2 commits October 9, 2026 11:34
…ot goes through bootShowcase

bootShowcase passes the showcase's own directory as hostRoot, so the file no
longer changes the process cwd to reach it. Its databaseFile option and its two
reboots after stop() are unchanged.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 749197628fc07c97865c2389e3d4f6cb002ca7bf
Local-runs: none

Isolated at-tier review of PR #22381 at the merge-round-2 head (item 1 stage 2 of #22301: ruling 6070767186 letter A; seat order 6071972897 A and A; merge-round note 6075062726; round-2 order 6080029242). Inputs: card #22301's body and all 20 comments; the PR body, its 153-file list and the net diff against the merge base c512c255c (+2542 / -913, 3455 changed lines, 0 governed paths, head repo = base repo, draft, auto-merge not armed); the 43 check-runs on the head; main fetched into a private ref for the sweep and combination questions. Nothing built, run or re-run. Read at 2026-10-09T12:23Z.

① Derived judgments

The head, measured against the standing PASS. This head is 8d985cf8f (PASS 6079992475) plus the merge commit 6b1607767 (parents 8d985cf8f and c512c255c) plus one commit, 749197628. Read three ways:

  • The merge is the clean union. git diff --name-only 6b1607767 c512c255c is exactly the 152 files of the judged net diff, and git diff --name-only 6b1607767 8d985cf8f is exactly the 79 files main moved in the window 2b61f2d9d..c512c255c; the two sets do not intersect, so no file was reconciled by hand. The seven merge=os-regen paths in that window (five content/docs pages, packages/spec/authorable-surface/data.json, packages/spec/liveness/state-counts/object.md) are blob-equal to c512c255c at this head, so no regeneration was owed and none was invented.
  • The branch side is carried verbatim: for every one of the 152 judged files, the added and removed lines of git diff c512c255c 749197628 hash equal to those of git diff 2b61f2d9d 8d985cf8f. 0 differ.
  • The net diff at this head is those 152 files plus exactly one: packages/qa/dogfood/test/position-environment-write-through.dogfood.test.ts (+3 / -10). Nothing else moved. The 152 are judged by the standing PASS on byte-identical hunks and are not re-judged here.

So the PR's delta is unchanged apart from the one swept file. The one commit, judged:

  1. The sweep (749197628) — right. feat(plugin-security)!: under single, Setup positions are written through to the environment ledger, and row-only positions are backfilled once (ADR-0131 D3, C2 stage S7) #22388 landed the file on main with bootStack(showcaseStack, { databaseFile: db }) at :71, a process.chdir(SHOWCASE_DIR) in beforeAll, its restore in afterAll, and the fileURLToPath import and SHOWCASE_DIR constant that served the chdir. The commit replaces the boot with bootShowcase({ databaseFile: db }) (the helper spreads the caller's options and sets hostRoot = examples/app-showcase, so the databaseFile option is handed through unchanged), drops the chdir pair, prevCwd, the constant, its comment and the now-unused import, and changes nothing else: the six cases, the databaseFile SQLite path (absolute, under tmpdir()), the two reboots in the backfill case and the 300_000 budgets are as main wrote them. RIGHT.
  2. Dropping the cwd change is safe for what the file reads. The file's own reads are the REST door (apiAs), the engine (find / insert / delete on the SQLite file at an absolute path) and createSecurityCatalogReader over the registry and the metadata service; none is cwd-relative. The one cwd-anchored read the chdir existed for — the file's own comment said package-relative refs resolve against the cwd — is the showcase's status-openapi.json, which the automation service now reads from packageRoot = hostRoot (harness.ts :959, :1011), the anchor bootShowcase names. Two side effects, both improvements: the boot's .objectstack/data now lands in the file's per-file temporary cwd (dogfood: five files boot the showcase in the package directory and leave its federated fixture database behind, so a later showcase boot's federated state depends on shard order #21914's design) instead of inside examples/app-showcase, and the showcase's MCP stdio connector — whose fixture path is cwd-anchored, connector-mcp: a declarative stdio transport resolves its relative command against the process cwd, while the same app's OpenAPI file ref resolves against the package root — showcase_mcp_tools boots degraded from any other directory #22423 — registers degraded as it does in every other swept file; the file uses no MCP tool. The per-file cwd is stable across the file's three boots, so the backfill's upgraded-deployment shape (rows and the sys_migration verdict carried in the SQLite file across stop() / start()) is unchanged. RIGHT.
  3. The instance rule holds across the two reboots after stop(). bootStack takes the claim synchronously (claimConfiguration, harness.ts :695) and releases it on a failed boot (:699) and in stop() after the kernel is shut down (:1287); release() drops the configuration key from LIVE_CONFIGURATIONS and every held app-plugin instance from LIVE_APP_PLUGINS. The file boots in beforeAll, and in the backfill case runs stop() then start() twice, so each re-boot of the same showcaseStack object and the same module-level plugin instances finds both sets empty and claims again. Nothing in the file keeps two kernels live. The gate agrees: Dogfood Regression Gate (3/3), the shard that holds the file, is success on this head. RIGHT.
  4. Tree-wide sweep complete at this head. In code, bootStack(showcaseStack / bootStackOnce(showcaseStack appears nowhere; the remaining text matches are comments (shared-showcase.ts:5, audit-log-admin-search:20, platform-objects/src/plugin.ts:60), CHANGELOG entries and the two checklist files that name the spelling to say it is not the composition. Of the 27 dogfood tests that import the showcase configuration, only test/showcase-boot.ts calls bootStack; shared-showcase.ts boots through bootShowcase (:86). Every other direct bootStack( in dogfood passes a fixture (crmStack, fixtureStack, builders, …), never the showcase. bootShowcase has 122 calling files and 133 call sites excluding the helper's own definition — the body's count. COMPLETE.
  5. main since this merge, read for the queue's combination. main is at f66c440de, three first-parent commits past c512c255c (fix(rest): the API-description endpoints refuse an anonymous caller (#22430) #22446, fix(rest): an anonymous public-form submit answers the created id, not the stored row (#22437) #22462, fix(auth): plugin-auth re-dispatch and vendor-call doors stop renewing a cookie session in-process (#22398) #22461; 32 files). Two of them change files in this PR's delta, showcase-anonymous-deny-surfaces and showcase-public-form, and nothing else in the window touches the PR's 153.
    • They combine without conflict. The hunks are disjoint: on showcase-anonymous-deny-surfaces, the PR edits one comment block and the member case at :495–:540 (501 becomes 200 with a hasPlugin('com.objectstack.service-automation') assertion), while main adds a header comment near :50, the API_DESCRIPTION_ENDPOINTS constant near :150, three cases near :665 and two rows in the uniform-deny table near :800; on showcase-public-form, the PR swaps the two import lines and the bootStack(showcaseStack, { line, while main inserts after the import block (SYS), after let stack (ql) and after the boot's closing }); (ql = await …), each separated from the PR's lines by an unchanged line. A git merge-tree of this head with f66c440de on the fetched refs (a git read, nothing built or run; the regen-driver caveat does not reach these two hand-written files) yields a tree with no conflict in which showcase-public-form keeps bootShowcase beside main's landedUnder read-back and showcase-anonymous-deny-surfaces carries both the 200 case and the API-description cases; GitHub's own mergeable: true on the PR says the same.
    • The new cases are not composition-dependent, so they are likely to hold. fix(rest): the API-description endpoints refuse an anonymous caller (#22430) #22446's cases drive GET /openapi.json and GET /docs, which @objectstack/rest's registerOpenApiEndpoints mounts on every boot, anonymously (401, the shared rest-flat body) and as a member (200); which app plugins are mounted does not reach them, and the file's boot is getSharedShowcase(), already on the served composition in this PR. fix(rest): an anonymous public-form submit answers the created id, not the stored row (#22437) #22462's change reads the submitted showcase_inquiry row back through a SYSTEM findOne by the answered id and asserts name, status ('new'), source ('web') and the three unforged anchors; those are stamped by the app's own insert hook (src/data/hooks/index.ts:111), present in both compositions, and the only showcase flow that names showcase_inquiry is showcase_inquiry_purge, a type: 'screen' flow with no record trigger, filtered to status: 'closed'. fix(auth): plugin-auth re-dispatch and vendor-call doors stop renewing a cookie session in-process (#22398) #22461 changes plugin-auth's in-process re-dispatch doors, which the harness mounts under either composition. The window's other new boots (public-form-submit-answer, public-form-read-back-masking, zero-set-masking) boot fixture configurations that declare no requires and no plugins, which this PR composes exactly as before; the would-be merged tree has 0 direct showcase boots. This is a reading; the queue's rebuilt generation is where the joint verdict is measured (Multi-agent discipline §10), and a re-merge is not owed for correctness.
  6. One observation, outside this round's order (not blocking). Three sibling files of the same cold-boot family — declared-position-provenance (:108), automation-authoring-doors-durable (:130) and email-template-overlay-survives-boot (:99) — still process.chdir(SHOWCASE_DIR) in beforeAll while booting through bootShowcase. The chdir lines predate this PR on main; the PR swept their boots and left them. bootShowcase's own header says not to chdir into the showcase (the boot writes .objectstack/data there, the cross-file state dogfood: five files boot the showcase in the package directory and leave its federated fixture database behind, so a later showcase boot's federated state depends on shard order #21914 removed), and this round's commit shows the chdir is redundant once hostRoot is named. The three files are green on this head and at the judged one. Carried to the seat in ③.

② Semver level

The changeset .changeset/22301-verify-boots-what-serve-boots.md is byte-identical to the judged head: @objectstack/core: minor, @objectstack/verify: minor, @objectstack/cli: patch; Clause-②: yes (narrowing) in its body; the ADR-0087 marker not-required (no-migration-prescription) with its category closure. This round's only addition is a test file in @objectstack/dogfood, which is private, and the merge brings main's own landings, which this PR does not publish; so what the diff publishes is unchanged and the levels stand — core's new exports at minor, verify's composition change and its two narrowings at minor under the convention the standing records accepted, and os verify's hostRoot fix in @objectstack/cli at patch. Not skip-changeset. RIGHT.

Clause-②: line. The PR body carries Clause-②: yes (narrowing: …; widening: …); the reader (scripts/pm/clause2-line.mjs) takes the first token inside the parenthetical as the arm, so the PR declares yes with arm narrowing — the same declaration the changeset and the claim 6070970729 carry. Check Changeset is success on both workflow runs. RIGHT.

③ Boundary flags

Round-2 report 6080581208: open_questions is empty. Its deviations and findings, each:

  1. The gate battery and check:pm-dispatch-gates ran unlocked while a sibling's build held the lock — local measurement hygiene; nothing in this record rests on that run, since the head's check-runs carry every gate verdict (Lint & Repo Gates success, its PM dispatch-gates self-test step 35 success). ANSWERED.
  2. Not merged again after main moved to f66c440de — per the order's scope (one file, c512c255c's window). The newer window is read in ①.5: no conflict, no direct showcase boot, the new cases composition-independent; the combination is measured by the queue. ANSWERED.
  3. Out-of-scope finding: the fix(rest): the API-description endpoints refuse an anonymous caller (#22430) #22446 cases are unmeasured under the served composition — answered by the reading in ①.5; carrier: the merge group, as the report says. ANSWERED.
  4. Carried unchanged from the standing records: hotcrm's six double-boot files (carrier: the changeset's remedy paragraph, which ships as CHANGELOG text, and the seat's round report); connector-mcp: a declarative stdio transport resolves its relative command against the process cwd, while the same app's OpenAPI file ref resolves against the package root — showcase_mcp_tools boots degraded from any other directory #22423 (filed); the two PID files at the container's filesystem root (environment hygiene, for the seat); activity-parent-read-gate near its budget (an observation, carrier none; the file's composition is unchanged by this PR); analyticsCubes parity (a later stage of this card). ANSWERED.
  5. New, from this review (①.6): the three sibling files that keep a pre-existing chdir(SHOWCASE_DIR) the helper's header forbids. Test-only, green, outside the round-2 order; ESCALATED to the seat as a later test-only sweep (one commit, three files), not a blocker on this head.
  6. The PR body's "CI at 749197628" row counts 36 check-runs at report time; a second PR Automation run on the same head (the pull_request event after the body edit) has since added seven, so the live count is 43. Not a defect; noted so the next reader is not surprised.

Checks on 749197628, every one named. 43 check-runs: 39 success, 4 skipped, 0 failure, 0 pending — every run had completed when read; the combined commit status (Vercel) is success.

  • Red: none.
  • Pending: none.
  • Skipped by design: Auto Label and Check PR Size on the second PR Automation run (both success on the first), Console Pin Gate (no pin change), Packed-tarball smoke (opt-in).
  • Green, the seven required contexts: Lint & Repo Gates (job 113800731789: 198 steps, 196 success, 2 skipped — Unmeasured-gate-tail reporter self-test and Report how many gates never ran, the on-failure reporters), TypeScript Type Check, Test Core (and its six shards), Dogfood Regression Gate (and its three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard.
  • Green, the rest: Build Docs, Check Changeset (both runs), Check Documentation Links, Check PR Size (first run), Dogfood Verify CLI, Flag docs affected by code changes, No other open PR may claim the same issue (both), No other open PR may claim the same single-writer path (both), Part-of PR must not also close its card (both), Spec property liveness, The card this PR closes must claim this branch (both), Type Check · consumer gates / debt ledger / source gates / workspace, Validate Package Dependencies, filter.

The verdict is on the contract at this head: the delta is the judged one plus one swept file, the sweep is right and complete, the cwd drop is safe for what the file reads, the instance rule holds across its reboots, the changeset says what the diff publishes at the right levels, every flag is answered or carried, and every check-run on the head has a conclusion, none red.

Implemented-by: claude/issue-22301-item1-one-composition
Reviewed-by: session_01DhTqaEHqPVSVnAkjG3jywn

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 9, 2026 12:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 9, 2026 12:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit 97610a5 Oct 9, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22301-item1-one-composition branch October 9, 2026 13:02
os-tesla pushed a commit that referenced this pull request Oct 9, 2026
…nt protocol, ^18

#22381 (97610a5) added test/verify-host-root.test.ts with a current-app
fixture pinned to engines.protocol '^17'. Under PROTOCOL_VERSION 18.0.0 the
runtime handshake refuses that app, so `os verify` exits 1 before the pin's
subject (the app-dir anchoring) is ever reached. The fixture now declares
'^18', the same sweep rule this branch applies to every current-app fixture.

Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants