Repository navigation
feat(core,cli,verify): bootStack composes what serve composes — item 1 stage 2 of #22301 (HELD at stop conditions) - #22381
objectstack-fleet[bot] wants to merge 12 commits into
Conversation
…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>
Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
… read it Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 47 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
…em1-one-composition
…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>
…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>
…ve does Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
Part of #22301
Clause-②: yes (narrowing: a second live boot of the same configuration is refused, where it booted; widening:
bootStackmounts the app'srequiresproviders andplugins, and@objectstack/coreexports 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 theos-dev-reporton #22301.What this branch carries
requireshalf (ef5396ca2, re-applied onto6a53564b9with no conflict; the moved table was compared line for line withmain'sServe.CAPABILITY_PROVIDERSand 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.pluginsarray (packages/verify/src/harness.ts). It is mounted byserve's rule for an entry, which now has one home:materializeStackPluginin@objectstack/core(stack-plugins.ts). A string is a package specifier, a plain bundle is wrapped intoAppPlugin, and an instance is itself.serve's boot loop reads it from there, and each boot injects its own loader. It sits inserve's slot, after the harness's services and before the route surfaces.extraPluginsinstance (orsecurity/analytics) with the samenameas an app plugin replaces it, and the app's instance never runs. The app's plugins count as held for therequiresresolver (serve's "an explicit instance wins"), and their hard dependencies are searched like a provider's.hostRootis the app's root. It is the automation service'spackageRoot, now also whenautomation: trueasks for the service, and it is the root a string entry resolves from.plugins[i]and the remedy.bootStackof one configuration object is refused withRESOURCE_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 onstop()and on a failed boot.OS_CLOUD_URL=offis stated inbootStack's docs and the changeset.packages/qa/dogfooddeclares it per project in its vitestenv(H4 below).test/shared-showcase.tsboots withhostRoot=examples/app-showcase.PM hypotheses, measured
serve's rule (serve.tsboot loop):config.plugins || [], plusdevPluginsunder--dev; entries in array order; string → host-anchored import; no-initobject →AppPlugin. Duplicates are not removed: the kernel's last-one-wins contract (plugin-registration.ts) supersedes byname. A failure is logged as✗ Failed to load pluginand the boot continues.packageRootispath.dirname(configPath), forautomationonly. The entry rule is now shared from@objectstack/core. Each boot keeps its own loading:servekeeps its relative-specifier refusal and diagnostic wrapper, the handle keepscreateHostImporter(hostRoot).devPluginsis not read, becauseserve(not dev) does not mount it.bootStackreceives an object, andhostRootdefaults 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.packages/qa/dogfoodwas found: 117 files callbootStack(showcaseStack, …), and 14 go throughgetSharedShowcase(). None of the 11 connector-passing files needs a change for the caller-first rule. Their connectors share the app'snames, so the app's copies are skipped, not mounted twice. 9 of the 11 pass.showcase-declarative-endpointsfails for a different reason (see OTHER below).packaged-activation-ledger-reachfails at its first, connector-less boot, which runs before itschdir.OS_CLOUD_URL: there are 0 hits in.github/. turbo 2.11.5 runs in strict env mode, and@objectstack/dogfood#testdeclares onlyOS_TEST_TIERS/OS_TEST_SHARD, so a CI-level value would not reach the task anyway. The showcase reads the value withresolveCloudUrl()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 runinpackages/qa/dogfood,--maxWorkers=2), under the lock. Shards 1 and 2 ran at662e101f4and shard 3 at09dcac304. The test inputs did not change between those commits.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 itsrequiresand its connectors from itsplugins, so at start the automation service materializesshowcase_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 asservewould from the wrong root. The 13 files that go through the helper, which passeshostRoot, boot. The 102 files: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-foundandpredicate-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'srequires: ['automation']is now honoured.schedule-sweep-organization-scope:the sweep did not bind — registered jobs: (none). The fixture declaresrequires: ['automation', 'triggers', 'messaging'], so the realTimeRelativeTriggerPluginis now mounted beside the trigger the test registers by hand with a fake job service.showcase-declarative-endpoints:TypeError: Converting circular structure to JSONfromJSON.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(aposture-onlyboot ofhandleFixtureStackwhile the file'sbootStackOnceboot is live), andbootStack (unshared) still returns a distinct stack.packages/verify/src/harness.app-default-profile.test.ts:… a fresh member holds the declared grantsandlets a suite opt OUT …. Each boots the same config again, and the earlier stacks are kept live untilafterAll.packages/qa/dogfood: the 3 inside/outside files above, plusarmed.None of these was changed. Every fixture except the showcase carries no
pluginsarray.Tests (at
c9a2c6123unless noted)@objectstack/verify(vitest run --maxWorkers=2, at662e101f4): 21 files, 154 tests. 150 passed, and 4 failed, all instance-rule consumers above. The new pins pass:harness.one-composition.test.ts9/9 andharness.required-providers.test.ts7/7.@objectstack/core--project local: 82 files, 2224 tests passed, includingstack-plugins.test.ts. Typecheck exit 0 for core, verify and cli (test-typecheck debt unchanged).@objectstack/cli--project unit: 270 files, 3969 tests passed, includingserve-capability-identity/-vocabularyand the twoserve-config-plugin-*source pins. Integration:serve-mcp-capability-collision.e2e(nightly tier; it serves a config with apluginsentry through the shipped bin) 3/3.scripts/ablation-replace.mjsin wrap mode. Each restore was proven by blob == HEAD and an emptygit diff HEAD. The suites import source, so no dist leg was needed.stop()removed: 1 red, the re-boot pin. The first attempt was void because the replacement was a substring of the anchor (x1tox1), so it was redone.dispatch-gates --commandsderived 87 commands; all 87 were run, all exit 0.check:dual-build-cjs-loadsfirst exited 3 (eight unrelated packages had nodist), and after building them it read107 published require entry point(s) across 66 package(s) load.--ranreports87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN.eslint --no-inline-config --format jsonover the 15 changed TS files gave 15 files, 0 errors, 0 warnings. The population is the**/*.{ts,…}andpackages/**/*.{ts,…}config objects. NoparserOptions.projectis set, so the diff cannot move an untouched file's verdict. The fullpnpm lintis CI's.Acceptance notes
servelogs a plugin it cannot mount and boots on, while the handle refuses, by the ruling. The two now differ only in that../scripts/mcp-fixture.mjsagainst the process cwd, while its openapi file ref resolves againstpackageRoot. That is two anchors for app-relative paths. No producer was measured where they diverge underserve. Carrier: none.await Serve.importConfigPlugin(plugin, hostRoot). The injected loader keeps that spelling, so the pins remain true.Generated by Claude Code