Repository navigation
feat(core,verify,cli): bootStack mounts the always-on slate and builds each provider from the app's configuration — item 1 gap of #22301 - #22747
Conversation
… providers from the configuration Part of the item-1 composition gap; not yet built or tested. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <[email protected]>
…uration Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <[email protected]>
…name Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <[email protected]>
…e question Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <[email protected]>
…case is bounded by its work The always-on slate mounts service-storage and the email service on every bootStack. The (a') attachments case keeps the real storage plugin out with a stand-in under its identity; the activity list case, which requests the parent of every row the boot mirrored, gets an explicit 30 s bound. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <[email protected]>
…em1-composition-gap
The always-on slate's email service seeds its templates at boot, and the case requests the record of every ledgered row: 5025 ms (timed out) with the slate, 3032 ms with it ablated. An explicit 30 s bound, as the activity sibling has. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <[email protected]>
…em1-composition-gap
Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <[email protected]>
…okens, and the migrate boot reads it too The boot-preparation parity pin (#22579) holds every step serve runs to a shared function the migrate boot runs as well. Reading `requires` stays each boot's `stackDeclaredCapabilities` call; `resolveServedCapabilities` expands what it read, and `os migrate plan`'s declaration boot now expands its tokens through it instead of its own requires-plus-slate copy. The argument rule is serve-only for that boot, which builds providers for their declarations alone. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <[email protected]>
📓 Docs Drift CheckThis PR changes 5 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 49 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 f99f3deeaac6f12e25023c9f0989f19fb648395b && git checkout f99f3deeaac6f12e25023c9f0989f19fb648395b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27 f17f4d53fac93082adf56e11d77fbd4d6ff16eed && git checkout -B drift-repro 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27 && git merge --no-ff f17f4d53fac93082adf56e11d77fbd4d6ff16eed
node scripts/docs-audit/affected-docs.mjs --json 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27
|
|
CI note ·
|
Contract reviewServed-tier: Isolated at-tier review of PR #22747 (item 1's remaining composition gap on #22301, ruling ① Derived judgments
② Semver levelChangeset
③ Boundary flagsDev flags from report
Named here, not flagged by the dev:
Check-runs on the head (41), every one named:
What must change (one patch round, text only): remove the analytics-cube clause from the narrowing arm everywhere it is written — the changeset's Implemented-by: VERDICT: FAIL |
…rned and skipped, never a failed boot Contract review 6103628981 (1.6): AnalyticsService registers each cube in a try, warns on a refusal and continues, so the narrowing arm's cube clause was false. The Clause-2 line now reads as the claim's corrected line, and the adr-0087 marker and the narrowing list carry the mail and SMS refusal alone. Claude-Session: https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj Co-authored-by: Claude <[email protected]>
Contract reviewServed-tier: Fresh isolated at-tier record on PR #22747 (item 1's remaining composition gap on #22301, ruling ① Derived judgmentsAdopted unchanged from 6. The cube clause — WRONG at the last head, RIGHT at this one. Re-judged in full, carrier by carrier:
11. The ② Semver levelChangeset
③ Boundary flagsDev flags from the patch-round report
Carried from Observation for the seat, not a finding: the PR body's own draft condition ("does not leave draft until the full dogfood suite has run on CI") is met on this head's check-runs — Check-runs on the head (41, read last, every one named):
The one item Implemented-by: VERDICT: PASS |
…em1-composition-gap
Contract reviewServed-tier: Fresh isolated at-tier record on PR #22747 (item 1's remaining composition gap on #22301, ruling ① Derived judgments1. The hop carries
So the code every judgment in 2. The one jointly-touched file at the merge head — RIGHT, and
3. What else
4. Everything the branch itself wrote — adopted from ② Semver levelAdopted from ③ Boundary flagsDev flags from the merge-round report
Carried from Named here, not flagged by the dev:
Check-runs on the head (32 at review time, read last, every one named):
Nothing must change. The merge carries Implemented-by: VERDICT: PASS |
Part of #22301
Clause-②: yes (narrowing: a configuration whose mail or SMS settings, or the OS_EMAIL_* / OS_SMS_* environment, name a transport that cannot deliver, now fails bootStack where it booted; widening: bootStack mounts the always-on slate objectstack serve mounts for every app and hands each provider the app's configuration, so the app's analytics cubes now reach the registry, where it mounted fewer and built them with defaults)
Item 1 of #22301: the remaining composition gap. The stage-2 contract review on PR #22381 recorded it (
6075048879, judgment 4). It falls under ruling6070767186(A): for one configuration,bootStackcomposes whatobjectstack servecomposes. Claim:6099249975. Items 6 and 7 are not addressed here, and #22301 remains open for them.Status: draft. This PR does not leave draft until the full dogfood suite has run on CI. The contract review at
CONTRACT_REVIEW_TIERis owed before enqueue.What changes
@objectstack/core. It lives incapability-composition.ts, besidecapability-providers.ts, and bothserveandbootStackread it:resolveServedCapabilities(declaredTokens, { preset, hostDefaults })answers which tokens get a provider, in order:stackDeclaredCapabilitiescall;emailfor a declaredauth;PLATFORM_ALWAYS_ON_CAPABILITIES), unless the preset isminimal;jobandqueue, moved ahead of the tokens that schedule background work.resolveCapabilityArgument(token, { stack, packageRoot, providerModule, env })answers what each provider is constructed with:automationgets the app's root;analyticsgets the app'sanalyticsCubes(the top level, then the legacycubes, then each package body's);emailandsmsget the deployment's mail and SMS configuration;storagegets its local root.serveis re-pointed, and its composition does not change. Its token expansion (requiresplus the slate) and its per-token argument block are replaced by the two calls. The precedence, the order and the declared/best-effort split are unchanged. Evidence:serve-package-declared-capabilities(spawned) and the three capability e2e files pass on this branch (below).os migrate plan's declaration boot reads the same token rule. The boot-preparation parity pin that landed onmainduring this round ([finding] cli(migrate):os migrate plan/applynever provision the telemetry sibling datasource, so lifecycle-classed objects a dev orOS_TELEMETRY_DBboot keeps inobjectstack.telemetry.dbare planned and created in the primary database #22579,boot-preparation-parity.test.ts) holds every stepserveruns to a shared function the migrate boot runs too; the merge made it red.composeServedPlatform(schema-migration-plugins.ts) now expands its tokens throughresolveServedCapabilitiesinstead of its own[...requires, ...PLATFORM_ALWAYS_ON_CAPABILITIES]— the same set; a declaredauthnow placesemailafter the declared tokens, asservedoes. The pin listsresolveServedCapabilitiesundercomposition, andresolveCapabilityArgumentasSERVE_ONLYwith its reason: that boot constructs each provider for its declarations only, with a measured posture per token, and runs nostart().bootStackmounts the always-on slate and builds each provider from the app's configuration.queue,job,cache,settings,email,storage,sms,sharing,messaging,analyticsandpackage-registry.OS_EMAIL_*/OS_SMS_*over it, and storage gets theOS_STORAGE_LOCAL_ROOTroot.extraPlugins/security/analyticsinstance still wins by identity. Neither that precedence nor the instance rule changes.required-providers.tsis removed: with the slate always mounted, it could no longer find anything to add.emailandsmsreaders moved fromserve.tsto@objectstack/plugin-emailand@objectstack/service-sms. Each must refuse exactly what its package's transports cannot build, so it reads that package's transport vocabulary. Both packages depend on@objectstack/core, so core cannot import them.resolveCapabilityArgumentreads each reader off the provider module the boot already loaded to construct the provider. A module without its reader is refused by name.serve.tsre-exports all moved names, sodata-migration-plugins.tsand the existing CLI tests import them unchanged.--preset, a CLI flag;OS_MCP_SERVER_ENABLED) and pinyin search (OS_SEARCH_PINYIN_ENABLED, whichservestamps into the process env from the locales). Both are process decisions, not configuration, andbootStackpasses none;servemakes a declared token fatal and logs and skips a slate one.bootStackfails on either and names the remedy, which is the ruling's "a plugin that cannot be mounted fails the boot loudly";Clause-② — line 2 is the claim's corrected line, and the changeset carries it byte for byte
Line 2 is the claim's
Clause-②:line as the seat corrected it (6099249975), and the changeset carries the same line byte for byte. Both arms are measured:OS_EMAIL_*/OS_SMS_*environment) that names a transport that cannot deliver now failsbootStack. Before, the provider was absent or built with defaults.bootStackmounts the always-on slate and hands each provider the app's configuration, so the app's analytics cubes now reach the registry. A cube the service refuses is warned and skipped by the service —AnalyticsService's constructor registers each cube inside atry(analytics-service.ts:1490–:1498) — underbootStackas underserve, and no boot fails on it.Contract review
6103628981(①.6) corrected the cube point: an earlier revision listed the cube under the narrowing.scripts/pm/clause2-line.mjsreads the line asyes/ armnarrowing, with a BREAKING banner and the ADR-0087 markernot-required (no-migration-prescription);check-adr-0087-registrationis green.Pins (one per gap, each with a control, each ablation-verified)
packages/verify/src/harness.served-composition.test.tshas 7 cases:approvalsandrealtimeare not mounted.analytics.cubeRegistry. Control: an app with no cube has none of that name.email.persist: falsereaches the provider, so a send leaves 0sys_emailrows. The control (no configuration) leaves 1 row. Aprovider: 'smtp'with no host fails the boot, namingEmailServicePlugin, the provider andOS_EMAIL_SMTP_HOST.Ablations, through
scripts/ablation-replace.mjs. Verify's tests import./harness.jsfrom source and alias@objectstack/coreto source, so no build sits between mutation and reading. Every leg was restored to the HEAD blob withgit diff HEADempty. The legs ran at95060af7c7(A1, A2, A3 first run) and3cb1f2abcc(A3 re-run). That was before the token rule's parameter became the declared tokens (03181fc07b, which changes its input, not what it answers), so the A1 anchor reads as it was then.resolveServedCapabilities(opts.config)given{ preset: 'minimal' }expected [ 'queue', 'job', 'cache', …(5) ] to deeply equal []), and so did the email cases (no email service). The cube cases stayed green.undefinedexpected [] to include 'svc_note_cube')VALIDATION_FAILED: from address required). So the probe was changed to pass its own sender, and the leg was re-run.expected 1 to be +0) and the smtp refusal (promise resolved … instead of rejecting). The control stayed green.packages/core/src/capability-composition.test.tshas 12 cases, one per step of each rule.harness.required-providers.test.tswas moved off slate tokens (cache→realtime), so it still proves therequiresreader and not the slate.Re-pointed tests (no second copy)
serve-email-config-parity.contract.test.tsmoved, with its reader, to@objectstack/plugin-emailascapability-arg.config-parity.contract.test.ts. It still scans the reader's own source forcfgEmailreads, againstEmailServiceConfigSchema. That package already declares the comment-mask cross-package input.serve-auth-app-name.contract.test.ts: the email half now drivesresolveCapabilityArgument('email', …)with@objectstack/plugin-emailas the provider module. The source half asserts that AuthPlugin'sappNamereads the same three stack keys, and thatserve.tscalls the rule at exactly one site withstack: configandenv: process.env.normalized-call-sites.test.ts: thecommands/serve.ts :: config.analyticsCubesrow is gone, because the read moved to core.Dogfood: the full suite was the reading, and it required three test-only adaptations
attachments-permission-matrix(a′)com.objectstack.service.storageinextraPlugins, the caller-wins rule. It keeps the real plugin, its objects and its floor alternate out — the shapeserve --preset minimalboots. It failed before the change (areference_not_foundonfile_id, 400) and passes after.activity-parent-read-gatesys_activityrows aboutsys_email_templateon this fixture, counted. The case measured 4372 ms with the slate and 2690 ms with the slate ablated in verify'sdist/(preflight present / absent both green). It timed out once at the 5000 ms default in shard 1. It gets an explicit 30 s bound.audit-log-parent-read-gatesys_audit_log: 5025 ms (timed out) with the slate, 3032 ms ablated. It gets the same bound.Evidence (commands, verbatim counts)
Every reading below is on head
03181fc07b(git rev-parse --short HEAD), which mergesorigin/main762db996ad, with every package rebuilt from that tree first (pnpm turbo run build --filter=@objectstack/dogfood^... --filter=@objectstack/cli... --filter=@objectstack/example-crm...: 63 successful). The A/B timings are the exception: they were taken on8be3183274/a3b4b05071, with verify'sdist/mutated and restored.pnpm turbo run typecheck --filter=@objectstack/core --filter=@objectstack/plugin-email --filter=@objectstack/service-sms --filter=@objectstack/verify --filter=@objectstack/cli --concurrency=2pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2pnpm --filter @objectstack/core testpnpm --filter @objectstack/core run test:repopnpm --filter @objectstack/plugin-email testpnpm --filter @objectstack/service-sms testpnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2… --project integrationoverserve-package-declared-capabilities,plan.boot-parity,schema-migrate.requires-providers,schema-migrate.host-compositionandschema-migrateOS_TEST_TIERS=nightly … --project integrationoverserve-package-registry-always-on.e2e,serve-mcp-capability-collision.e2eandrequires-retired-capability.e2enode packages/cli/bin/run.js verify --app examples/app-crm/objectstack.config.ts --rlsand the same forexamples/app-showcaseOS_TEST_SHARD=k/3 pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths), every command run with its exit recorded before any pipenode scripts/pm/dispatch-gates.mjs --ran ran.listThe roster gates for the directories this diff writes in also ran, each exit 0:
check:authz-resolver,check:error-code-casing,check:filter-alias-parity,check-changeset-fixed,check:published-readme-exportsandcheck:stack-collection-maps.check:dual-build-cjs-loadsrefused withPREREQUISITE NOT MET(exit 3) until the eight packages it reads (studio,client-react, …) were built. It ran after that and is among the 77.Deviations from the claim's file surface, each forced by a measurement
@objectstack/plugin-emailand@objectstack/service-sms(src, their index exports, the moved parity contract): the email and SMS readers depend on each package's transport vocabulary, and@objectstack/corecannot import either package.packages/cli/src/utils/schema-migration-plugins.tsandboot-preparation-parity.test.ts: the [finding] cli(migrate):os migrate plan/applynever provision the telemetry sibling datasource, so lifecycle-classed objects a dev orOS_TELEMETRY_DBboot keeps inobjectstack.telemetry.dbare planned and created in the primary database #22579 pin that landed onmainduring this round, which the merge turned red.packages/qa/dogfoodtest files, notbootShowcase: the full dogfood run's red. None is one of [finding] metadata(residual): a residual top-level object is listed by the metadata door under manifest.id while the data door answers 404, and the boot's warning says every door reports it #22615's files ([finding] metadata(residual): a residual top-level object is listed by the metadata door under manifest.id while the data door answers 404, and the boot's warning says every door reports it #22615 touched no dogfood file).Acceptance notes (not filed)
serve, measured.bootStackdoes not mount MCP or pinyin search. These are host-process defaults decided by env, and the pinyin one stamps the process env from the stack's locales. A test process boots many stacks, so a stamp from one boot would outlive it. A suite that needs either passes the provider inextraPlugins; the MCP and pinyin dogfood suites already do. Carrier: verify: the in-process handle boots a leaner stack thanserveand has no door for eight things an app's tests need (requires[] capabilities, system/predicate update, the form door, user-less triggers, …), measured by hotcrm#2013 #22301, item 1.email/sms/appNameare read but cannot be authored. The mail and SMS readers readconfig.email,config.smsandconfig.appName, anddefineStackrefuses all three keys (STACK_SCHEMA_INVALID, "Unrecognized key(s) on this stack definition", measured on this tree). So they reach a provider only from a configurationdefineStackdid not build — whichos build/os validaterefuse (STACK_PROVENANCE_MISSING) andos serveboots. It is pre-existing and unchanged here, since this diff moves the readers and does not change what they read. The new pins addemailto the configuration object for that reason. It is reported to the seat as a finding.bootStackkeeps its kernel's process-signal listeners.ObjectKernel's constructor registers SIGINT / SIGTERM / SIGQUIT listeners, and a boot refused beforebootstrap()never releases them. At worker exit this printsERROR Shutdown failed … Kernel not running, observed after this file's refused-boot case. The behaviour is pre-existing: every refused boot path shares it. It is cosmetic. Carrier: none.Generated by Claude Code