Skip to content

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

Draft
objectstack-fleet[bot] wants to merge 12 commits into
mainfrom
claude/issue-22301-item1-one-composition
Draft

objectstack-fleet[bot] wants to merge 12 commits into
mainfrom
claude/issue-22301-item1-one-composition

Conversation

@objectstack-fleet

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 at both of the dispatch's stop conditions. Draft, not for review. Item 1 · stage 2 of #22301 under ruling 6070767186 (A, one composition rule). The composition is built and pinned, and the full dogfood suite was measured at it. Both stop conditions were reached, so nothing more was built, and the consumers the instance rule breaks were not changed. The decision request is in the os-dev-report on #22301.

What this branch carries

  1. The requires half (ef5396ca2, re-applied onto 6a53564b9 with no conflict; the moved table was compared line for line with main's Serve.CAPABILITY_PROVIDERS and is identical). The token-to-provider table and its exact identity match move to @objectstack/core (capability-providers.ts), beside the package-owned collection reader (stack-collections.ts). Serve's statics are handles over them.
  2. The app's own plugins array (packages/verify/src/harness.ts). It is mounted by serve's rule for an entry, which now has one home: materializeStackPlugin in @objectstack/core (stack-plugins.ts). A string is a package specifier, a plain bundle is wrapped into AppPlugin, and an instance is itself. serve's boot loop reads it from there, and each boot injects its own loader. It sits in serve's slot, after the harness's services and before the route surfaces.
  3. The caller wins by identity. An extraPlugins instance (or security / analytics) with the same name as an app plugin replaces it, and the app's instance never runs. The app's plugins count as held for the requires resolver (serve's "an explicit instance wins"), and their hard dependencies are searched like a provider's.
  4. hostRoot is the app's root. It is the automation service's packageRoot, now also when automation: true asks for the service, and it is the root a string entry resolves from.
  5. Loud failure. An entry that cannot be loaded or registered fails the boot, and the error names plugins[i] and the remedy.
  6. The instance rule. A second live bootStack of one configuration object is refused with RESOURCE_CONFLICT / 409 (a standard catalog code, so no ledger row). So is a copy that carries a mounted app-plugin instance. The claim is released on stop() and on a failed boot.
  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 (H4 below).
  8. Dogfood. test/shared-showcase.ts boots with hostRoot = examples/app-showcase.

PM hypotheses, measured

  • H1, holds. serve's rule (serve.ts boot loop): config.plugins || [], plus devPlugins under --dev; entries in array order; string → host-anchored import; no-init object → AppPlugin. Duplicates are not removed: the kernel's last-one-wins contract (plugin-registration.ts) supersedes by name. A failure is logged as ✗ Failed to load plugin and the boot continues. packageRoot is path.dirname(configPath), for automation only. The entry rule is now shared from @objectstack/core. Each boot keeps its own loading: serve keeps its relative-specifier refusal and diagnostic wrapper, the handle keeps createHostImporter(hostRoot). devPlugins is not read, because serve (not dev) does not mount it.
  • H2, measured. "Same configuration" can only key on object identity: bootStack receives an object, and hostRoot defaults to the cwd, which every app booted in one process shares. A spread copy shares the instances, so the plugin instances are a second key. bootStackOnce (handle.test.ts, same keys → same boot) and boot→stop→boot stay green. The rule breaks measured consumers: stop condition 2, below.
  • H3. Every direct showcase boot in packages/qa/dogfood was found: 117 files call bootStack(showcaseStack, …), and 14 go through getSharedShowcase(). None of the 11 connector-passing files needs a change for the caller-first rule. Their connectors share the app's names, so the app's copies are skipped, not mounted twice. 9 of the 11 pass. showcase-declarative-endpoints fails for a different reason (see OTHER below). packaged-activation-ledger-reach fails at its first, connector-less boot, which runs before its chdir.
  • H4, measured. CI sets no OS_CLOUD_URL: there are 0 hits in .github/. turbo 2.11.5 runs in strict env mode, and @objectstack/dogfood#test declares only OS_TEST_TIERS / OS_TEST_SHARD, so a CI-level value would not reach the task anyway. The showcase reads the value with resolveCloudUrl() when its module is imported. The marketplace plugins call the network only inside route handlers, not at boot.

Stop condition 1: the full dogfood suite is not green, and the fix reaches beyond the helper and the 11 files

These are the same commands CI's leg runs (OS_TEST_SHARD=k/3, vitest run in packages/qa/dogfood, --maxWorkers=2), under the lock. Shards 1 and 2 ran at 662e101f4 and shard 3 at 09dcac304. The test inputs did not change between those commits.

shard files tests
1/3 40 failed, 36 passed (76) 310 passed, 253 skipped (563)
2/3 32 failed, 44 passed (76) 15 failed, 347 passed, 179 skipped (541)
3/3 36 failed, 39 passed, 1 skipped (76) 3 failed, 347 passed, 327 skipped (677)

108 of 228 files fail. Grouped by first error:

102 files: OPENAPI-PACKAGEROOT. Each boots the showcase directly with no hostRoot. The showcase now gets automation from its requires and its connectors from its plugins, so at start the automation service materializes showcase_status_openapi, whose spec is a package-relative file. Under the per-file temporary cwd that file does not resolve, and the boot refuses: failed to read providerConfig.spec './src/system/connectors/status-openapi.json' … resolved to '/tmp/os-dogfood-run-…/src/system/connectors/status-openapi.json': ENOENT. That is the composition behaving as serve would from the wrong root. The 13 files that go through the helper, which passes hostRoot, boot. The 102 files:

account-oauth-tokens-not-serialized action-params-contract activity-withheld-update
admin-credential-lifecycle admin-identity-audit-trail admin-platform-admin-standing
admin-route-nonadmin-refusal api-key-hash-not-serialized api-key-owner-revoke api-key-revoke-lifecycle armed
audit-log-admin-search audit-log-internal-fields auth-session-audit-trail bearer-lane-password-change
business-unit-and-user-delete-federated-fixture dashboard-designer-roundtrip
datasource-meta-door-reaches-admin-door datasource-restore-code-wins delegated-admin-invite
delegation-of-duty discovery-auth-families external-import-code-datasource-namespace
external-import-destructive-remedy external-import-saves-like-meta external-validate-sees-runtime-save
external-validate-start-ordering federated-anchor-provenance federated-phantom-share-grant
federated-rls-injectors federated-sweep-projections field-zoo-roundtrip identity-admin-fields-org-peer
install-local-listing-not-loaded install-local-listing-sample-data install-local-no-active-organization
install-local-purge-sample-data install-local-reseed-intact-baseline install-local-sample-data-not-loaded
invitation-ledger-row-scope me-apps-and-everyone-baseline membership-actor-attribution
membership-decided-at-creation membership-ended-session-revoke membership-reconciler
membership-role-vocabulary meta-door-code-datasource meta-published-and-state-routes meta-types-create-seed
no-active-organization-write-refusal object-designer-field-reorder oidc-authorization-code-flow
oidc-authorize-env-gate org-admin-affordance-reach org-create-default-team org-scoped-sharing-rule-listing
organization-delete-federated-fixture organization-update-door owner-anchor-and-bulk-writes
packaged-activation-ledger-reach primary-bu-projection route-ledger-live-mount-parity
security-catalog-showcase semantic-roles session-token-not-serialized settings-config-change-audit
share-links-self-list sharing-rule-criteria-required sharing-rule-org-less-caller
showcase-bu-hierarchy-sharing showcase-client-liaison-fixtures showcase-crud-persona-matrix
showcase-d3-d4-capabilities showcase-d7-default-profile showcase-default-profile
showcase-demo-personas-loginable showcase-demo-personas-membership showcase-expand-crud-gate
showcase-external-autoconnect showcase-fls-read-mask-strip showcase-invoice-cbp
showcase-invoice-seed-isolation showcase-mcp-http-identity showcase-mcp-self-connection
showcase-object-extension-meta-read showcase-object-extension-scalar-divergence
showcase-permission-projection showcase-permission-seeding showcase-public-form-redirect
showcase-public-form-walled-intake showcase-public-form-withdrawal-layers showcase-public-form-withdrawal
showcase-public-form showcase-scope-depth-fallback showcase-scope-depth-write showcase-scope-depth
single-tenant-identity-create storage-growth temporal-storage-e2e two-factor-backup-code-reveal
two-factor-lockout view-container-cross-package-default

3 files: INSTANCE-RULE (RESOURCE_CONFLICT). Each runs two concurrent boots of one fixture config, one inside the organization and one outside it: parent-derived-write-refusal-not-visible, write-door-unreadable-is-not-found and predicate-write-unreadable-not-matched. armed (two concurrent showcase boots) belongs here too. Its first error is currently masked by OPENAPI-PACKAGEROOT.

3 files: OTHER. Each pins the old composition:

  • showcase-anonymous-deny-surfaces (shared): no @objectstack/service-automation is installed on this boot: expected 200 to be 501. The showcase's requires: ['automation'] is now honoured.
  • schedule-sweep-organization-scope: the sweep did not bind — registered jobs: (none). The fixture declares requires: ['automation', 'triggers', 'messaging'], so the real TimeRelativeTriggerPlugin is now mounted beside the trigger the test registers by hand with a fake job service.
  • showcase-declarative-endpoints: TypeError: Converting circular structure to JSON from JSON.stringify(showcaseStack). Once mounted, the config's own plugin instances (ConnectorSlackPlugin, RuntimeConfigPlugin) hold kernel references. This is the module-level-instance hazard the instance rule exists for, observed directly.

Stop condition 2: the instance rule breaks measured consumers

  • packages/verify/src/handle.test.ts: reports the walled posture … other stack (a posture-only boot of handleFixtureStack while the file's bootStackOnce boot is live), and bootStack (unshared) still returns a distinct stack.
  • packages/verify/src/harness.app-default-profile.test.ts: … a fresh member holds the declared grants and lets a suite opt OUT …. Each boots the same config again, and the earlier stacks are kept live until afterAll.
  • packages/qa/dogfood: the 3 inside/outside files above, plus armed.

None of these was changed. Every fixture except the showcase carries no plugins array.

Tests (at c9a2c6123 unless noted)

  • @objectstack/verify (vitest run --maxWorkers=2, at 662e101f4): 21 files, 154 tests. 150 passed, and 4 failed, all instance-rule consumers above. The new pins pass: harness.one-composition.test.ts 9/9 and harness.required-providers.test.ts 7/7.
  • @objectstack/core --project local: 82 files, 2224 tests passed, including stack-plugins.test.ts. Typecheck exit 0 for core, verify and cli (test-typecheck debt unchanged).
  • @objectstack/cli --project unit: 270 files, 3969 tests passed, including serve-capability-identity / -vocabulary and the two serve-config-plugin-* source pins. Integration: serve-mcp-capability-collision.e2e (nightly tier; it serves a config with a plugins entry through the shipped bin) 3/3.
  • Ablations, one per behaviour, through scripts/ablation-replace.mjs in wrap mode. Each restore was proven by blob == HEAD and an empty git diff HEAD. The suites import source, so no dist leg was needed.
    • app plugins never read: 6 red of 9
    • caller precedence removed: 1 red, the precedence pin
    • config-identity key removed: 1 red, the two-concurrent-boots pin
    • release on stop() removed: 1 red, the re-boot pin. The first attempt was void because the replacement was a substring of the anchor (x1 to x1), so it was redone.
    • plugin-copy guard removed: 1 red, the copy pin
    • core bundle wrap removed: 2 red of 5
  • Gates: dispatch-gates --commands derived 87 commands; all 87 were run, all exit 0. check:dual-build-cjs-loads first exited 3 (eight unrelated packages had no dist), and after building them it read 107 published require entry point(s) across 66 package(s) load. --ran reports 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN.
  • Lint: eslint --no-inline-config --format json over the 15 changed TS files gave 15 files, 0 errors, 0 warnings. The population is the **/*.{ts,…} and packages/**/*.{ts,…} config objects. No parserOptions.project is set, so the diff cannot move an untouched file's verdict. The full pnpm lint is CI's.

Acceptance notes

  • serve logs a plugin it cannot mount and boots on, while the handle refuses, by the ruling. The two now differ only in that.
  • The showcase's declarative MCP stdio connector resolves ./scripts/mcp-fixture.mjs against the process cwd, while its openapi file ref resolves against packageRoot. That is two anchors for app-relative paths. No producer was measured where they diverge under serve. Carrier: none.
  • Two CLI source pins read the boot loop's literal await Serve.importConfigPlugin(plugin, hostRoot). The injected loader keeps that spelling, so the pins remain true.

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 fdfdd7e76da8fc206849a5aae7018633f5893b9e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1306a597b96cd5ae3d4e82ebf7300bfccccd98c3 — the merge of head 08929776c764ac608cbc14ee2eaff300ce57c269 into base fdfdd7e76da8fc206849a5aae7018633f5893b9e, 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 1306a597b96cd5ae3d4e82ebf7300bfccccd98c3 && git checkout 1306a597b96cd5ae3d4e82ebf7300bfccccd98c3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fdfdd7e76da8fc206849a5aae7018633f5893b9e 08929776c764ac608cbc14ee2eaff300ce57c269 && git checkout -B drift-repro fdfdd7e76da8fc206849a5aae7018633f5893b9e && git merge --no-ff 08929776c764ac608cbc14ee2eaff300ce57c269

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

⚠️ 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 fdfdd7e76da8fc206849a5aae7018633f5893b9e → 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>

This branch has not been deployed

No deployments
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