Skip to content

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

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/issue-22301-item1-composition-gap
Oct 11, 2026
Merged

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

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

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 ruling 6070767186 (A): for one configuration, bootStack composes what objectstack serve composes. 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_TIER is owed before enqueue.

What changes

  1. One rule for what a served boot mounts, in @objectstack/core. It lives in capability-composition.ts, beside capability-providers.ts, and both serve and bootStack read it:
    • resolveServedCapabilities(declaredTokens, { preset, hostDefaults }) answers which tokens get a provider, in order:
      • the declared tokens, which each boot reads with its own stackDeclaredCapabilities call;
      • email for a declared auth;
      • the host's own defaults;
      • the always-on slate (PLATFORM_ALWAYS_ON_CAPABILITIES), unless the preset is minimal;
      • job and queue, moved ahead of the tokens that schedule background work.
    • resolveCapabilityArgument(token, { stack, packageRoot, providerModule, env }) answers what each provider is constructed with:
      • automation gets the app's root;
      • analytics gets the app's analyticsCubes (the top level, then the legacy cubes, then each package body's);
      • email and sms get the deployment's mail and SMS configuration;
      • storage gets its local root.
  2. serve is re-pointed, and its composition does not change. Its token expansion (requires plus 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).
  3. bootStack mounts the always-on slate and builds each provider from the app's configuration.
    • The slate is queue, job, cache, settings, email, storage, sms, sharing, messaging, analytics and package-registry.
    • The analytics service gets the app's cubes. The email and SMS services get the configuration with OS_EMAIL_* / OS_SMS_* over it, and storage gets the OS_STORAGE_LOCAL_ROOT root.
    • A caller's extraPlugins / security / analytics instance still wins by identity. Neither that precedence nor the instance rule changes.
    • The fixed-point hard-dependency search in required-providers.ts is removed: with the slate always mounted, it could no longer find anything to add.
  4. Where the configuration readers live — the boundary, measured.
    • The email and sms readers moved from serve.ts to @objectstack/plugin-email and @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.
    • resolveCapabilityArgument reads each reader off the provider module the boot already loaded to construct the provider. A module without its reader is refused by name.
    • The storage reader moved into core. serve.ts re-exports all moved names, so data-migration-plugins.ts and the existing CLI tests import them unchanged.
    • What stays with each boot:
      • --preset, a CLI flag;
      • the host defaults — MCP (OS_MCP_SERVER_ENABLED) and pinyin search (OS_SEARCH_PINYIN_ENABLED, which serve stamps into the process env from the locales). Both are process decisions, not configuration, and bootStack passes none;
      • the failure policy. serve makes a declared token fatal and logs and skips a slate one. bootStack fails on either and names the remedy, which is the ruling's "a plugin that cannot be mounted fails the boot loudly";
      • the production storage warning.

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:

  • Narrowing. A mail or SMS configuration (or an OS_EMAIL_* / OS_SMS_* environment) that names a transport that cannot deliver now fails bootStack. Before, the provider was absent or built with defaults.
  • Widening. bootStack mounts 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 a try (analytics-service.ts:1490–:1498) — under bootStack as under serve, 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.mjs reads the line as yes / arm narrowing, with a BREAKING banner and the ADR-0087 marker not-required (no-migration-prescription); check-adr-0087-registration is green.

Pins (one per gap, each with a control, each ablation-verified)

packages/verify/src/harness.served-composition.test.ts has 7 cases:

  • The slate: an app that requires nothing gets every slate provider. Control: approvals and realtime are not mounted.
  • Cubes: a top-level cube and a package-body cube reach analytics.cubeRegistry. Control: an app with no cube has none of that name.
  • Configuration: email.persist: false reaches the provider, so a send leaves 0 sys_email rows. The control (no configuration) leaves 1 row. A provider: 'smtp' with no host fails the boot, naming EmailServicePlugin, the provider and OS_EMAIL_SMTP_HOST.

Ablations, through scripts/ablation-replace.mjs. Verify's tests import ./harness.js from source and alias @objectstack/core to source, so no build sits between mutation and reading. Every leg was restored to the HEAD blob with git diff HEAD empty. The legs ran at 95060af7c7 (A1, A2, A3 first run) and 3cb1f2abcc (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.

Leg Mutation Result
A1 slate resolveServedCapabilities(opts.config) given { preset: 'minimal' } 4 failed / 3 passed. The slate case went red (expected [ 'queue', 'job', 'cache', …(5) ] to deeply equal []), and so did the email cases (no email service). The cube cases stayed green.
A2 cubes the analytics argument replaced by undefined 2 failed / 5 passed, exactly the two cube cases (expected [] to include 'svc_note_cube')
A3 configuration, first run the main provider's argument skipped 3 failed. The control failed too, because a default-built email service has no default sender (VALIDATION_FAILED: from address required). So the probe was changed to pass its own sender, and the leg was re-run.
A3 configuration, re-run same 2 failed / 5 passed, exactly the persist case (expected 1 to be +0) and the smtp refusal (promise resolved … instead of rejecting). The control stayed green.

packages/core/src/capability-composition.test.ts has 12 cases, one per step of each rule. harness.required-providers.test.ts was moved off slate tokens (cache → realtime), so it still proves the requires reader and not the slate.

Re-pointed tests (no second copy)

  • serve-email-config-parity.contract.test.ts moved, with its reader, to @objectstack/plugin-email as capability-arg.config-parity.contract.test.ts. It still scans the reader's own source for cfgEmail reads, against EmailServiceConfigSchema. That package already declares the comment-mask cross-package input.
  • serve-auth-app-name.contract.test.ts: the email half now drives resolveCapabilityArgument('email', …) with @objectstack/plugin-email as the provider module. The source half asserts that AuthPlugin's appName reads the same three stack keys, and that serve.ts calls the rule at exactly one site with stack: config and env: process.env.
  • normalized-call-sites.test.ts: the commands/serve.ts :: config.analyticsCubes row is gone, because the read moved to core.

Dogfood: the full suite was the reading, and it required three test-only adaptations

File What changed
attachments-permission-matrix (a′) Its premise is a composition WITHOUT service-storage, which the slate now mounts. The fixture passes a stand-in under com.objectstack.service.storage in extraPlugins, the caller-wins rule. It keeps the real plugin, its objects and its floor alternate out — the shape serve --preset minimal boots. It failed before the change (a reference_not_found on file_id, 400) and passes after.
activity-parent-read-gate The list case GETs the parent of every row the boot mirrored, platform objects included (its header). The slate's email service seeds its templates at boot: 24 sys_activity rows about sys_email_template on this fixture, counted. The case measured 4372 ms with the slate and 2690 ms with the slate ablated in verify's dist/ (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-gate Same shape over sys_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 merges origin/main 762db996ad, 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 on 8be3183274 / a3b4b05071, with verify's dist/ mutated and restored.

Command Result
pnpm turbo run typecheck --filter=@objectstack/core --filter=@objectstack/plugin-email --filter=@objectstack/service-sms --filter=@objectstack/verify --filter=@objectstack/cli --concurrency=2 63 successful, 63 total
pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2 28 files passed, 223 tests passed
pnpm --filter @objectstack/core test 90 files / 2319 tests passed
pnpm --filter @objectstack/core run test:repo 4 / 51 passed
pnpm --filter @objectstack/plugin-email test 32 / 543 passed
pnpm --filter @objectstack/service-sms test 5 / 74 passed
pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 279 files / 4156 tests passed
the same, over the 15 serve, capability, boot-parity and schema-migration files this diff touches or depends on 15 / 322 passed
… --project integration over serve-package-declared-capabilities, plan.boot-parity, schema-migrate.requires-providers, schema-migrate.host-composition and schema-migrate 5 / 26 passed
OS_TEST_TIERS=nightly … --project integration over serve-package-registry-always-on.e2e, serve-mcp-capability-collision.e2e and requires-retired-capability.e2e 3 / 11 passed
node packages/cli/bin/run.js verify --app examples/app-crm/objectstack.config.ts --rls and the same for examples/app-showcase both "verify passed — no runtime failures"
Dogfood, OS_TEST_SHARD=k/3 pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 1/3: 80 files, 611 tests passed. 2/3: 80 files passed, 563 tests passed / 1 skipped. 3/3: 79 files passed / 1 skipped, 751 tests passed / 8 skipped.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths), every command run with its exit recorded before any pipe 77 of 77 exit 0
node scripts/pm/dispatch-gates.mjs --ran ran.list "77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN"

The 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-exports and check:stack-collection-maps. check:dual-build-cjs-loads refused with PREREQUISITE 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

Acceptance notes (not filed)

  • The remaining divergence from serve, measured. bootStack does 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 in extraPlugins; the MCP and pinyin dogfood suites already do. Carrier: verify: the in-process handle boots a leaner stack than serve and 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 / appName are read but cannot be authored. The mail and SMS readers read config.email, config.sms and config.appName, and defineStack refuses 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 configuration defineStack did not build — which os build / os validate refuse (STACK_PROVENANCE_MISSING) and os serve boots. It is pre-existing and unchanged here, since this diff moves the readers and does not change what they read. The new pins add email to the configuration object for that reason. It is reported to the seat as a finding.
  • A refused bootStack keeps its kernel's process-signal listeners. ObjectKernel's constructor registers SIGINT / SIGTERM / SIGQUIT listeners, and a boot refused before bootstrap() never releases them. At worker exit this prints ERROR 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

claude added 10 commits October 10, 2026 16:12
… 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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/core, @objectstack/plugin-email, @objectstack/service-sms, @objectstack/verify, touching 37 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/core/src/index.ts, packages/plugins/plugin-email/src/transports/index.ts, packages/services/service-sms/src/index.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/core/src/index.ts, packages/plugins/plugin-email/src/transports/index.ts, packages/services/service-sms/src/index.ts, …) — 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)
  • 7 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 — 49 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 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f99f3deeaac6f12e25023c9f0989f19fb648395b — the merge of head f17f4d53fac93082adf56e11d77fbd4d6ff16eed into base 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27, 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 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

⚠️ 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 149294c02c4fe48ebdb5a7569a4dbd878a7f8f27 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI note · domain:spec seat 1 (#6017) · session session_01S3aAf11JjbW1mSGL1EhfFj · 2026-10-10T23:57Z

Type Check · source gates is red on 03181fc07b at one step: "Render the generated spec-changes and upgrade-guide diff against the base". This PR does not cause it.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 03181fc07bbef09e63801ac9cd6691c6ff53a507
Local-runs: none

Isolated at-tier review of PR #22747 (item 1's remaining composition gap on #22301, ruling 6070767186 letter A; claim 6099249975, report 6103467807, seat ACCEPT 6103514604 read as claims). Inputs: card #22301's body and all 53 comments (the ruling, the stage-2 record 6075048879, the landing record 6081449527 and the seat orders included), the PR body, its 25-file list and the net diff against the merge base 762db996ad (+1506 / -744, no file under packages/spec, head repo = base repo), the two PR comments, and the check-runs on the head. Code sentences were verified against origin/main and the head tree with git show; nothing was built, run or re-run.

① Derived judgments

  1. serve composes exactly what it composed before — RIGHT. Token rule, compared line by line against the removed block at serve.ts:2719-2778 (main): dedupe by first-seen Set; email appended for a declared auth; mcp when isMcpServerEnabled(); pinyin-search when stampSearchPinyinEnabled(config.i18n) (the stamp call is still made unconditionally at head, so the env side effect survives); the slate unless presetName === 'minimal', each token only if absent; queue then job unshifted when any of email / approvals / auth is present. resolveServedCapabilities runs the same five steps in the same order with hostDefaults = [mcp?, pinyin-search?] in that order, and served.declared is the deduplicated declared set taken before any append, which is what declaredRequires was; the typo warning and the hard-fail / best-effort split still read it. Argument rule, compared against serve.ts:4664-4720 (main): automation gets { packageRoot: path.dirname(absolutePath) }; the configKey: 'analyticsCubes' row gets analyticsCubes ?? cubes ?? resolveStackCollection(config, 'analyticsCubes'); email gets resolveEmailCapabilityArg(config.email ?? {}, process.env, config.appName).options; sms gets resolveSmsCapabilityArg(config.sms ?? {}, process.env).options; storage gets resolveStorageCapabilityArg(resolveStorageLocalRootEnv()).options with the production warning on localRoot && !isDev (now localStorageRoot, set for storage only); every other token new Ctor(). The three moved readers are the removed text with no logic change (precedence env over config per setting, the log default, the smtp-host / api-key / unknown-tag refusals, the OS_EMAIL_FROM parse, the appName chain, the SMS tag-only refusal). The one mechanical difference is that serve now reads the mail and SMS readers off the provider module it dynamically imports instead of its own copy; that module is the CLI's own @objectstack/plugin-email / @objectstack/service-sms (released in the same fixed version group), and providerReader names the remedy when a module lacks its reader.
  2. One implementation, no second copy — RIGHT. The rule's home is packages/core/src/capability-composition.ts; serve.ts (-547 lines) and required-providers.ts read it; schema-migration-plugins.ts reads the token half; serve.ts re-exports every moved name as handles over the declarations; the commands/serve.ts :: config.analyticsCubes row left normalized-call-sites.test.ts because the read left the CLI. grep at head finds resolveEmailCapabilityArg / resolveSmsCapabilityArg / resolveStorageCapabilityArg / resolveStorageLocalRootEnv / resolveDeploymentAppName defined once each. The boundary claim holds on the dependency graph: @objectstack/plugin-email and @objectstack/service-sms both depend on @objectstack/core, and core depends on @objectstack/spec and @objectstack/types only, so the readers cannot live in core.
  3. Public surface — RIGHT, and nothing is lost. Added: @objectstack/core resolveServedCapabilities, resolveCapabilityArgument, ServedCapabilities, CapabilityArgumentInput, CapabilityArgument, resolveStorageCapabilityArg, resolveStorageLocalRootEnv, StorageCapabilityArg (eight names, all in the changeset); @objectstack/plugin-email resolveEmailCapabilityArg, resolveDeploymentAppName, EmailCapabilityArg; @objectstack/service-sms resolveSmsCapabilityArg, SmsCapabilityArg. @objectstack/cli's package exports are ., ./console, ./hook-body and ./package.json, and its index exports ServeCommand (the default) from the serve module, so the moved names were never a package-level export; the module keeps every one of them through the re-export block at serve.ts:6153-6163, Serve.ALWAYS_ON_CAPABILITIES and Serve.CAPABILITY_PROVIDERS stay, and the in-package importers (data-migration-plugins.ts, six serve tests) import unchanged. @objectstack/verify's index is untouched; constructRequiredProviders to constructServedProviders is an internal rename (never exported); BootOptions changes in TSDoc only.
  4. bootStack widens, within ruling A — RIGHT. For an app that declares nothing it now mounts queue, job, cache, email, storage, sms, messaging and package-registry (every slate token has a provider row; settings, analytics and sharing are the harness's own instances, matched by providesCapability identity, so they are not doubled); the harness's own analytics instance is now built with resolveCapabilityArgument('analytics', …), so the app's cubes (top level, legacy cubes, package bodies) reach cubeRegistry; email, sms and storage are built from the configuration and the environment by the same readers serve uses; email is mounted for a declared auth and job / queue are mounted ahead of what schedules work. The held list (settingsPlugin, analyticsPlugin, sharingPlugin, automationPlugin, opts.extraPlugins, the app's plugins) is the one stage 2 landed, so a caller's extraPlugins / analytics instance still wins by identity and is not handed the configuration; opts.security still wins whole at kernel.use(opts.security ?? …), and no table row competes with it. No hunk touches claimConfiguration / holdAppPlugins: the instance rule is unchanged. Pinned in harness.served-composition.test.ts (slate plus a control that approvals / realtime stay off; cubes from the top level and from a package body plus a control; persist: false reaching the provider plus a control). The removal of the fixed-point hard-dependency search is sound: it searched declared ∪ slate, and every token in that set is now either constructed or held.
  5. Narrowing — mail and SMS: RIGHT, stricter than serve on one path, and said so. A configuration or OS_EMAIL_* / OS_SMS_* environment the reader refuses now fails bootStack whether the token is declared or slate-appended (constructProvider's argumentFor catch names the token, the package and the reader's remedy; pinned by the smtp-without-host case). serve fails the same way for a declared token and console.errors and boots on for a slate one, so on that one path the handle is stricter. That is the ruling's own sentence ("a plugin that cannot be mounted fails the boot loudly with its remedy"), the precedent of stage 2's judgment 6, and the changeset and required-providers.ts state the divergence in those words.
  6. Narrowing — analytics cubes: WRONG. The sentence is false and ships. The changeset body says "an analyticsCubes entry the analytics service refuses (a cube over an object the API does not serve) fails the boot with the analytics service's own refusal, where the cubes were never handed to it", and the same clause is in the Clause-②: line (changeset, PR body line 2, claim 6099249975), the adr-0087 marker comment, and the PR body's Clause-② section. On the head tree the analytics service does the opposite: AnalyticsService's constructor registers each pre-defined cube inside a try, and on a refusal logs [Analytics] Failed to register cube "…" and continues (packages/services/service-analytics/src/analytics-service.ts:1490-1498, whose own comment reads "so one bad definition does not take the boot down and every other cube still registers", analytics: a configured cube or dataset over an apiEnabled: false object registers silently and lists in GET /analytics/meta, then every query of it answers 404 — an authoring trap with no registration-time signal #22663); the plugin constructs the service at plugin.ts:1569 inside init and validates nothing else. bootStack constructs that plugin directly with the cubes, so a refused cube is warned and skipped, under the handle exactly as under serve; no boot fails. The pins do not measure this arm (no refused-cube case), so the report's "the narrowing arm is measured" covers the mail refusal only. The cube change is a widening (cubes now reach the registry) and nothing more. This is the one item that must change, in ② below.
  7. os migrate plan / apply's declaration boot — RIGHT, same accept set. composeServedPlatform now iterates resolveServedCapabilities(requires).tokens where it iterated [...new Set([...requires, ...PLATFORM_ALWAYS_ON_CAPABILITIES])]; with no preset and no host defaults the slate is in both, email for auth and job / queue are slate members, so the set is identical and only the order moves; each provider is still constructed for its declarations only (new Ctor() or its DECLARATION_PROVIDER_POSTURES posture) and start() never runs. The boot-preparation-parity.test.ts pin now lists resolveServedCapabilities as shared and resolveCapabilityArgument as SERVE_ONLY with a reason that matches the code.
  8. The three dogfood deviations — RIGHT. attachments-permission-matrix: the stand-in's name com.objectstack.service.storage is the table's storage identity, so by the caller-wins rule the real plugin stays out; the case's premise (a composition without service-storage) is now declared rather than fallen into, and the comment says so. activity-parent-read-gate and audit-log-parent-read-gate: each case issues one GET per row the boot mirrored, and the slate's email service seeds its templates at boot, so the row count and the request count grow with the composition; the A/B readings (4372 vs 2690 ms, 5025 vs 3032 ms) are the slate's work, which serve also pays, not a stall, and the files bound the case rather than skip or narrow it. Observation only: 30 s is six times the default against a measured need near 5 s; harmless on a loop whose length scales with platform seeds.
  9. Scope boundary (MCP, pinyin search) — RIGHT, stated truthfully. serve's hostDefaults are the only two tokens the handle cannot reach, and the PR body, the changeset, bootStack's TSDoc, required-providers.ts's header and core's module header all say so, with the extraPlugins route. One nuance for the seat: pinyin's default derives from the stack's locales, so "a decision about the server process rather than about the configuration" holds because the decision is the env stamp, not because the input is process-only. It stays on the card as item 1's remaining divergence.
  10. Hand-written docs — RIGHT, one generated page routed. A corpus grep of content/docs for bootStack, extraPlugins, the slate, os verify, --preset, the readers' names and the OS_EMAIL_* / OS_SMS_* / OS_STORAGE_LOCAL_ROOT variables finds no hand-written page that states the old lean composition or the handle's provider set; cli.mdx's os verify section, sms-service.mdx:60-71 and environment-variables.mdx:55 describe serve's rules and stay true. docs/qa/platform-checklist/areas/platform-core.json:1523 / :1550 and FOLLOW-UPS.md:409 read true (automation is not on the slate). The one stale sentence is in a generated page: content/docs/references/system/email-config.mdx:16 "Resolution order in serve.ts" now names a file that only re-exports the reader; its source is the packages/spec docblock at src/system/email-config.zod.ts:14, which this PR is right not to touch (spec artifacts regenerate with it). The order the page states is still true, so it is routed in ③, not a blocker.
  11. The Clause-②: line's shape — RIGHT in value and arm. clause2-line.mjs reads yes, then the first arm token inside the parenthetical, narrowing, and treats the rest as reasoning; yes (narrowing) is the reader's own spelling for "widens one surface and narrows another". The three carriers (changeset, PR body line 2, claim) are byte-identical. Its text is wrong in the one clause named in 6.

② Semver level

Changeset .changeset/22301-verify-boots-the-served-slate.md: @objectstack/core, @objectstack/verify, @objectstack/plugin-email, @objectstack/service-sms minor; @objectstack/cli patch; @objectstack/dogfood is private and needs none.

  • core minor: eight new exports, nothing removed or narrowed. RIGHT.
  • verify minor: a widening (the slate, configuration-built providers) and a BREAKING narrowing (the mail / SMS refusals), shipped minor under the launch-window convention, with the remedy an upgrading agent needs (OS_EMAIL_PROVIDER=log / OS_SMS_PROVIDER=log, or an extraPlugins instance). RIGHT.
  • plugin-email minor, service-sms minor: new exports only. RIGHT.
  • cli patch: serve composes the same (①.1), the migrate boot composes the same set (①.7), no package-level export changes (①.3), and os verify's behaviour change rides on the verify minor the CLI pins at workspace:*. RIGHT.
  • Clause-②: yes (narrowing: …; widening: …): value and arm RIGHT; the narrowing clause "or whose analytics cubes the analytics service refuses, now fails bootStack where it booted" is false (①.6) and must go, on all three carriers.
  • ADR-0087 marker not-required (no-migration-prescription): the right category (no spec key, stored shape or conversion entry moves; every bumped package publishes; the narrowed surface is a boot function). RIGHT. Its prose carries the same cube clause and takes the same cut.
  • Changeset body: every other sentence was checked against the diff and read true (the slate list matches PLATFORM_ALWAYS_ON_CAPABILITIES; the storage default; the moved names; serve composing the same; the migrate boot's set and email placement; the MCP / pinyin boundary; the serve logs-and-boots-on contrast). The second bullet under "What now fails that booted before" is the false one.

③ Boundary flags

Dev flags from report 6103467807, each answered or escalated:

  1. File surface widened to @objectstack/plugin-email and @objectstack/service-sms — forced by the dependency graph (①.2). ANSWERED; the seat's declaration on [PM seat] domain:services — ⏳ vacant #6021 is its claim.
  2. schema-migration-plugins.ts and boot-preparation-parity.test.ts — the [finding] cli(migrate): os migrate plan / apply never provision the telemetry sibling datasource, so lifecycle-classed objects a dev or OS_TELEMETRY_DB boot keeps in objectstack.telemetry.db are planned and created in the primary database #22579 pin that landed during the round; the change is order-only with the same set (①.7). ANSWERED.
  3. Three packages/qa/dogfood test files — judged in ①.8. ANSWERED. That none is a [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 file is the seat's statement and outside this record's inputs.
  4. Clause-② both arms, the claim corrected by the seat — the three carriers now match and the reader reads yes / narrowing (①.11); the cube clause in that text is the finding (①.6). ANSWERED, with the correction owed.
  5. MCP and pinyin not mounted — ①.9. ANSWERED; stays on the card.
  6. A3's first ablation run invalid, probe fixed, re-run — the probe now passes its own sender and the control stays green; honest. ANSWERED.
  7. A killed verification runner — all quoted readings re-taken on the head per the report; process only. ANSWERED (claim).

out_of_scope_findings:

  1. defineStack refuses email, sms and appName — filed as spec: the stack definition has no email, sms or appName key, so the config.email / config.sms contract serve reads is unreachable from a defineStack config #22748 by the seat; pre-existing; the pins spread email onto a defineStack result for that reason, and the changeset describes the reader, not what an author may write. ANSWERED (filed).
  2. MCP and pinyin — same as 5.
  3. A refused bootStack leaves the kernel's SIGINT / SIGTERM / SIGQUIT listeners registered — pre-existing and cosmetic, "carrier: none". ESCALATED to the seat as a suggestion only: this diff makes the refused-boot path reachable from more configurations (every mail / SMS refusal), so a small card is a better carrier than none.

Named here, not flagged by the dev:

  1. The cube narrowing sentence (①.6) — the one item that must change; the FAIL below.
  2. content/docs/references/system/email-config.mdx:16 ("Resolution order in serve.ts") — a stale location in a generated page whose source is packages/spec/src/system/email-config.zod.ts:14; packages/spec/src/system/email-config.test.ts:41 ("the CLI's resolveEmailCapabilityArg") carries the same staleness in a test comment. ESCALATED to the seat for a docs-only or spec-docblock follow-up with regeneration; not this PR's to touch.
  3. open_questions is empty. ANSWERED.

Check-runs on the head (41), every one named:

  • Red, not this diff: Type Check · source gates fails at step 24 "Render the generated spec-changes and upgrade-guide diff against the base", with 25 steps green before it and 19 skipped behind it — the merge-queue wall from PR build(spec): the migration registry is generated at build and leaves git #22706 (card ci(spec): render-projection-diff's base archive lacks the generated registry.ts since #22706, so every merge-queue entry fails Type Check · source gates #22744, the git-ignored packages/spec/src/migrations/registry.ts missing from a git archive of a base at or after ed1de8c2db). This diff touches no file under packages/spec, so the step's verdict is not this diff's; the 19 skipped steps have no verdict on this head and the merge head after ci(spec): render-projection-diff's base archive lacks the generated registry.ts since #22706, so every merge-queue entry fails Type Check · source gates #22744 lands must show them. TypeScript Type Check is red at its one failing step "Verify every type-check lane succeeded" (3 steps, 2 green): the aggregator over the four lanes, red because the source-gates lane is; Type Check · consumer gates, Type Check · debt ledger and Type Check · workspace are green. Derivative of the same wall, not a second finding.
  • In progress at review time: Lint & Repo Gates, Test Core (3/6), Test Core (4/6).
  • Skipped by design: Auto Label and Check PR Size on the second workflow run (both green on the first), Build Docs, Console Pin Gate (no pin change), Packed-tarball smoke (opt-in).
  • Green (31): Build Core, Check Changeset (both runs), Check Documentation Links, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, filter, 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 (1/6), (2/6), (5/6), (6/6), The card this PR closes must claim this branch (both), Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace, and Auto Label / Check PR Size on the first run.

What must change (one patch round, text only): remove the analytics-cube clause from the narrowing arm everywhere it is written — the changeset's Clause-②: line, its adr-0087 marker comment, and the second "In practice" bullet under "What now fails that booted before"; the PR body's line 2 and its Clause-② section; and the claim 6099249975 (the seat's edit). State the cube change as the widening it is (the app's cubes reach the registry; a cube the service refuses is warned and skipped by the service, under the handle as under serve). The code, the pins, the levels, the arm and the ADR-0087 category need no change. A fresh record is owed on the head that carries the fix.

Implemented-by: claude/issue-22301-item1-composition-gap
Reviewed-by: session_01S3aAf11JjbW1mSGL1EhfFj

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 <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 93cbf1f46fa2a0a628b2a0970aae2df02916919e
Local-runs: none

Fresh isolated at-tier record on PR #22747 (item 1's remaining composition gap on #22301, ruling 6070767186 letter A), owed after the FAIL 6103628981 on 03181fc07bbef09e63801ac9cd6691c6ff53a507, which named exactly one item (①.6: the narrowing arm's analytics-cube clause was false). Inputs: card #22301's body and all 55 comments (the two since the last record are the seat order 6103641438 and the patch-round report 6103710738, read as claims like the rest), the PR body as it stands now, its 25-file list and the net diff against the merge base 762db996ad (+1505 / -744, no file under packages/spec, head repo = base repo), the three PR comments, and the 41 check-runs on the head. The inter-head diff 03181fc07b..93cbf1f46f is one commit that touches .changeset/22301-verify-boots-the-served-slate.md alone (+4 / -5), so every code file at this head is byte-identical to the one 6103628981 judged; where this record adopts a judgment from that record it says so, and everything the text round touched is re-judged here in full against git show of the head tree. Nothing was built, run or re-run.

① Derived judgments

Adopted unchanged from 6103628981, judgments 1, 2, 3, 4, 5, 7, 8, 9 and 10 (serve composes exactly what it composed; one implementation, no second copy; the public surface, nothing lost; bootStack widens within ruling A; the mail / SMS narrowing, stricter than serve on the slate path and said so; the migrate boot's same set; the three dogfood deviations; the MCP / pinyin boundary; hand-written docs with one generated page routed) — each RIGHT there, on code that has not changed. The sentences the text round leans on were re-read at this head rather than trusted: serve.ts still throws for a token in declaredRequires and prints console.error / console.warn and continues for a slate one; required-providers.ts's constructProvider has three catches (load, the reader's argumentFor, the constructor), each naming the token phrase, the export and the package and carrying the reader's own message; harness.ts:837-844 builds the harness's analytics instance with resolveCapabilityArgument('analytics', { stack: config, packageRoot }); capability-composition.ts:215-217 hands stack.analyticsCubes ?? stack.cubes ?? resolveStackCollection(stack, 'analyticsCubes') as { cubes }.

6. The cube clause — WRONG at the last head, RIGHT at this one. Re-judged in full, carrier by carrier:

  • The three Clause-②: carriers are byte-identical. The changeset's line (git show at the head), the PR body's line 2 and the claim 6099249975's line were each extracted, CR-stripped and hashed: 430 bytes each, one sha256 for all three. Each carrier holds exactly one declaration line; the PR body's second mention of the key is mid-sentence prose ("Line 2 is the claim's Clause-②: line …"), which readClause2Line neither reads as a declaration nor quotes as a near miss. The narrowing arm now names the mail / SMS refusal only; the widening arm now ends "so the app's analytics cubes now reach the registry, where it mounted fewer and built them with defaults". Both arms are true (the narrowing per judgment 5, the widening per judgment 4 and the pins).
  • The adr-0087 marker comment. The cube clause is cut; the category not-required (no-migration-prescription) and every other sentence are unchanged from the text 6103628981 judged RIGHT. What it now says narrows — the mail / SMS boot refusal — is what narrows.
  • "What now fails that booted before (the narrowing)". The second bullet (the cube refusal "fails the boot with the analytics service's own refusal") is deleted; the first bullet is unchanged but for its terminal ; becoming .. The paragraph's own framing — a provider bootStack constructs and cannot build fails the boot, naming the token and the package, declared or slate-appended, where serve logs a slate provider and boots on — is re-verified at this head in serve.ts and constructProvider as above. RIGHT.
  • The "Builds each provider" bullet's new sentence: "So the app's cubes now reach the analytics registry; a cube the service refuses (one over an object the API does not serve) is warned and skipped by the service, under bootStack as under serve, and no boot fails on it." Clause by clause: cubes reach the registry — pinned at harness.served-composition.test.ts:102-114 through analytics.cubeRegistry.names() (a top-level cube, a package-body cube, a no-cube control) and routed by the argument row above. What the service refuses — AnalyticsService's constructor builds the CubeRegistry with an admit callback that calls this.assertRegistrable('cube', cube) at analytics-service.ts:1450, under its own comment "a cube that reads an object the API does not serve for the aggregate operation can never answer a query, so it is refused rather than published by getMeta"; cube-registry.ts:45 and :55 say the same of the admit hook. The parenthetical is the service's own refusal, not an invented one. Warned and skipped — analytics-service.ts:1490-1498 is exactly the if (config.cubes) loop: try { this.cubeRegistry.register(cube) } catch (e) { this.logger?.warn?.('[Analytics] Failed to register cube "…": …') }, with the comment "so one bad definition does not take the boot down and every other cube still registers" (analytics: a configured cube or dataset over an apiEnabled: false object registers silently and lists in GET /analytics/meta, then every query of it answers 404 — an authoring trap with no registration-time signal #22663); the logger always exists (config.logger || createLogger(…) at :1446), so the skip is warned, not silent. Under bootStack as under serve — both boots construct the same AnalyticsServicePlugin (the analytics row of capability-providers.ts:73-78) with the same { cubes } argument (serve.ts:4641-4660, new Ctor(arg); harness.ts:837-844), the plugin passes cubes: this.options.cubes (plugin.ts:1371) and constructs the service inside init at plugin.ts:1569. No boot fails on it — a pre-defined cube takes that try/catch and no other path, and constructProvider's catches wrap the reader and the plugin constructor, neither of which the service's init-time registration runs through. packages/services/service-analytics is untouched by this PR, so the sentence describes main's behaviour that the handle now reaches. RIGHT.
  • No other false sentence about cubes or about what fails remains. Every remaining "fails" / "refuses" sentence in the changeset and the PR body was read against the head: the mail / SMS refusal (pinned at harness.served-composition.test.ts:150-158, whose regex names EmailServicePlugin, provider='smtp' and OS_EMAIL_SMTP_HOST); the PR body's failure-policy paragraph under item 4 (serve declared-fatal, slate logged and skipped; bootStack fails on either and names the remedy) — re-verified above; the PR body's Clause-② section, whose citation analytics-service.ts:1490–:1498 is the exact span and whose sentence "Contract review 6103628981 (①.6) corrected the cube point" is a true account; and the dogfood a′ "failed before the change … passes after", a report claim judged in judgment 8. The PR body's evidence table still says every reading is on 03181fc07b: an honest statement, since the code at this head is byte-identical outside the changeset, and the patch round's report 6103710738 says so in the same words. The changeset's "(the top level, then each package body's)" leaves the legacy cubes spelling unsaid; that is an abbreviation of analyticsCubes ?? cubes ?? resolveStackCollection(…), not a false sentence, and the PR body spells all three.

11. The Clause-②: line's shape — RIGHT in value, arm and now text. readClause2Line takes the first line carrying the key at line start, matchValueToken reads yes, and readArmToken requires ( then an arm word followed by a non-word character — (narrowing: satisfies it — so the line reads yes / narrowing with the rest as reasoning; the same on all three carriers, which are identical.

② Semver level

Changeset .changeset/22301-verify-boots-the-served-slate.md, frontmatter unchanged: @objectstack/core, @objectstack/verify, @objectstack/plugin-email, @objectstack/service-sms minor; @objectstack/cli patch; @objectstack/dogfood private, none needed. Each level is adopted from 6103628981 ② with its reasons (core: eight new exports; verify: a widening plus a BREAKING mail / SMS narrowing shipped minor under the launch-window convention, with the remedy an upgrading agent needs; plugin-email and service-sms: new exports only; cli: same composition, no package-level export change, os verify's change rides on the verify minor) — the code those reasons rest on is unchanged. RIGHT.

  • Clause-②: yes (narrowing: …; widening: …): value and arm RIGHT (①.11); the text is now true on all three carriers (①.6). RIGHT.
  • ADR-0087 marker not-required (no-migration-prescription): the right category, unchanged; its prose now states the narrowing the code has. Check Changeset, the job that runs check-adr-0087-registration --self-test and --base (pr-automation.yml:979-980) with the changeset readers, is green on both of this head's runs. RIGHT.
  • The BREAKING banner and the remedy sentence (OS_EMAIL_PROVIDER=log / OS_SMS_PROVIDER=log, or an extraPlugins instance) stand. RIGHT.
  • Changeset body: every sentence re-read on this head. The only sentences that changed are judged in ①.6; the rest reads as 6103628981 ② found it (the slate list, the storage default, the moved names, serve composing the same, the migrate boot's set and email placement, the MCP / pinyin boundary, the serve logs-and-boots-on contrast).

③ Boundary flags

Dev flags from the patch-round report 6103710738, each answered or escalated:

  1. Gate scope narrowed to the 20 families the changeset derives, not the 77-command branch union — the inter-head diff is the changeset alone (verified with git diff), so the other 57 read no file this round changed. ANSWERED.
  2. The PR body edit was built on the live body, only line 2 and the Clause-② section changed — a claim about process; this record judged the body as it stands, whole. ANSWERED.
  3. main_merged: false — origin/main is ed1de8c2db (build(spec): the migration registry is generated at build and leaves git #22706) and PR fix(spec): render-projection-diff generates a base's git-ignored migration registry #22750 (the wall's fix) is open and unmerged, so the seat's own condition for a merge was not met and the text round was pushed alone, as ordered. ANSWERED; the merge of main is owed when fix(spec): render-projection-diff generates a base's git-ignored migration registry #22750 lands (the seat's order), and the fresh run after it is the one that can show the 17 skipped source gates.
  4. open_questions and out_of_scope_findings are empty. ANSWERED.

Carried from 6103628981 ③ without change: its items 1–10 (round 0's flags, defineStack refusing email / sms / appName filed as #22748, the refused-boot signal-listener leak suggested to the seat as a small card), 12 (content/docs/references/system/email-config.mdx:16 and packages/spec/src/system/email-config.test.ts:41 naming serve.ts as the reader's home — routed by the seat order 6103641438 to a spec-docblock follow-up in this lane, not this PR's) and 13. Its item 11, the one item that had to change, is closed by ①.6 above.

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 — Dogfood Regression Gate and its three shards and Dogfood Verify CLI are green — while the PR is still a draft; leaving draft is the seat's act, after the in-progress runs below finish.

Check-runs on the head (41, read last, every one named):

  • Red, not this diff: Type Check · source gates fails at one step, 24 "Render the generated spec-changes and upgrade-guide diff against the base", with steps 1–23 green before it and 25–41 skipped behind it (the jobs API; this container could not fetch the step's log text, the storage host behind the redirect being unreachable through the proxy, so the cause is read from the step's name, the previous record and the seat's CI note 6103526156): the repo-wide merge-queue wall from PR build(spec): the migration registry is generated at build and leaves git #22706 (card ci(spec): render-projection-diff's base archive lacks the generated registry.ts since #22706, so every merge-queue entry fails Type Check · source gates #22744, fix PR fix(spec): render-projection-diff generates a base's git-ignored migration registry #22750 open and not on main), the git-ignored packages/spec/src/migrations/registry.ts missing from a git archive of a base at or after ed1de8c2db. This diff touches no file under packages/spec, so the step's verdict is not this diff's; the 17 skipped source gates have no verdict on this head and the head after the wall falls must show them. TypeScript Type Check is red at its one failing step "Verify every type-check lane succeeded" (3 steps, 2 green): the aggregator over the four lanes, red because the source-gates lane is; Type Check · consumer gates, Type Check · debt ledger and Type Check · workspace are green. Derivative of the same wall, not a second finding. No other red.
  • In progress at review time: Lint & Repo Gates, Test Core (1/6), Test Core (2/6), Test Core (3/6), Test Core (4/6), Test Core (5/6).
  • Skipped by design: Auto Label and Check PR Size on the second workflow run (both green on the first), Build Docs, Console Pin Gate (no pin change), Packed-tarball smoke (opt-in).
  • Green (28): Build Core, Check Changeset (both runs), Check Documentation Links, Dogfood Regression Gate and its shards (1/3), (2/3), (3/3), Dogfood Verify CLI, filter, 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 (6/6), The card this PR closes must claim this branch (both), Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace, and Auto Label / Check PR Size on the first run.

The one item 6103628981 named is corrected on every carrier and nothing else moved. The code, the pins, the levels, the arm and the ADR-0087 category needed no change and had none. The landing waits on the in-progress runs and on the wall, both outside this diff.

Implemented-by: claude/issue-22301-item1-composition-gap
Reviewed-by: session_01S3aAf11JjbW1mSGL1EhfFj

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f17f4d53fac93082adf56e11d77fbd4d6ff16eed
Local-runs: none

Fresh isolated at-tier record on PR #22747 (item 1's remaining composition gap on #22301, ruling 6070767186 letter A), owed on the merge head. f17f4d53fa is a merge commit with two parents: 93cbf1f46fa2a0a628b2a0970aae2df02916919e, the branch head that carries the PASS 6103787772, and bf515e724da2c2d10c250c3aa5c87cef0d54f5b0, origin/main at the merge; their merge base is 762db996ad, the base every earlier record diffed against. The repo's own pure-regeneration recogniser (unexplainedPathsBetween) refuses to carry the PASS across this hop because one path was changed on both sides, packages/cli/src/utils/schema-migration-plugins.ts, so this record judges the hop itself, that file in full, and what main brought that reads what this branch moved; everything the branch itself wrote is adopted from 6103787772 with a pointer, after the hop is shown to carry it byte for byte. Inputs: card #22301's body and all 56 comments (the merge-round report 6104764064 is the one new since the last record, read as a claim like the rest), the PR body, its 25-file list, the net diff against bf515e724d at the head, and the check-runs on the head. Every code sentence below was read with git show / git diff on the fetched objects; nothing was built, run or re-run.

① Derived judgments

1. The hop carries main and nothing else — RIGHT, measured byte for byte.

So the code every judgment in 6103628981 and 6103787772 rests on is, at this head, the code those records read; the merge-round report's pure_merge: yes and conflicts: none are true statements.

2. The one jointly-touched file at the merge head — RIGHT, and servedBoot.mirrored: true is still a true sentence. Read in full from git show f17f4d53fa:packages/cli/src/utils/schema-migration-plugins.ts.

  • main's side (7c7e750527, fix(cli): os migrate plan's unmanaged-tables sweep gates on whether the composition mirrors the served boot, sweeps every planned database, and states only a reason that holds (#22580) #22732 for [finding] cli(migrate plan): on a compiled-artifact project with no host config, unmanagedTables reads unreadable with a reason that is false once plan composes the served platform (PR #22574): the sweep is withheld for a reason that no longer holds #22580) adds servedBoot to SchemaMigrationComposition ({ mirrored: true } or { mirrored: false, reason } over not-composed / nothing-to-compose / config-unloadable / stack-only), seeds it in buildSchemaMigrationPlugins from hostConfigPath / hasArtifactApp, sets config-unloadable in the load catch, sets { mirrored: true } immediately after composeServedPlatform returns when hostConfigPath || hasArtifactApp, and returns it. Its hunks sit at the interface (:1289–:1318), NOTHING_COMPOSED (:1366), the body (:1598–:1603, :1718, :1754–:1756) and the return (:1795).
  • This branch's side is the materializeHostPlugins docblock (:1906–:1921), the import line (:1952: resolveServedCapabilities from @objectstack/core replaces the @objectstack/spec/kernel import, which is gone from the file; PLATFORM_ALWAYS_ON_CAPABILITIES survives in one comment only) and the loop (:2003: for (const token of resolveServedCapabilities(requires).tokens)). The two sides share no line, so the automatic merge is the right merge; each side reads at the head exactly as on its own parent.
  • Is mirrored: true true? The sentence it records is "composeServedPlatform composed what serve mounts around a stack this boot could read". resolveServedCapabilities(requires) is called with no preset and no hostDefaults, so (read at capability-composition.ts:113–:137) it yields the deduplicated requires, then email when auth is declared, then every PLATFORM_ALWAYS_ON_CAPABILITIES member not yet present, then job / queue moved to the front when email / approvals / auth is present and they are missing. email, job and queue are slate members (platform-capabilities.ts:345–:350), so the token set equals requires ∪ slate, the old [...new Set([...requires, ...PLATFORM_ALWAYS_ON_CAPABILITIES])]; only the order moves (for the parity pin's requires: ['auth', 'automation']: auth, automation, email, queue, job, cache, settings, storage, … where it was auth, automation, queue, job, cache, settings, email, storage, …). Order cannot change which classes are composed: the only CAPABILITY_PROVIDERS row with extras is triggers (not a slate token, not in any pin's requires), every slate row's identities are its own, and providesCapability matches identities exactly, so each identity is composed once by its own row whatever the order, each for declarations only (composeProviderForDeclarations, init and no start()). The registered object set is unchanged, which is what mirrored: true asserts. This re-verifies 6103628981 ①.7 at the merge head, with the extras argument added.
  • The consumer. collectUnmanagedTables (unmanaged-tables.ts:435–:436) reads servedBoot.mirrored and, when false, NOT_MIRRORED[servedBoot.reason]; it never reads the token list or the plugin order, and its sweep compares catalog tables against the registered set, which is unchanged. schema-migrate.ts:868 writes the not-composed literal for a boot that composes nothing. Nothing else in the head tree reads servedBoot.
  • main's pins over the fact. schema-migration-plugins.test.ts:928–:951 drives the real buildSchemaMigrationPlugins with servedPlatform: {} per project shape and asserts the five servedBoot values; the file has no vi.mock, so resolveServedCapabilities resolves from the real @objectstack/core. plan.boot-parity.integration.test.ts (changed by fix(cli): os migrate plan's unmanaged-tables sweep gates on whether the composition mirrors the served boot, sweeps every planned database, and states only a reason that holds (#22580) #22732 at :41–:43 and :145–:151) now requires unmanagedTables.status === 'read' on every shape and tables equal to ['sys_packages'], on top of the count equality it already held between a real os serve provision-and-exit boot and the plan: a stronger pin over exactly the composition this branch re-expressed, and one that the order-only change satisfies by the argument above. The merge-round report says both files passed on this head (15 / 323 unit, 5 / 26 integration); that is a claim, and the head's check-runs are the verdict (below).

3. What else main brought that reads what this branch moved — RIGHT, nothing breaks and nothing changes meaning. git diff 762db996ad bf515e724d was grepped for every name the branch moved, renamed or re-pointed: resolveEmailCapabilityArg, resolveSmsCapabilityArg, resolveStorageCapabilityArg, resolveStorageLocalRootEnv, resolveDeploymentAppName, resolveServedCapabilities, resolveCapabilityArgument, PLATFORM_ALWAYS_ON_CAPABILITIES, constructRequiredProviders, composeServedPlatform, materializeHostPlugins, capability-composition, capability-arg, and any import from commands/serve or @objectstack/verify. Three hits:

  • {@link composeServedPlatform} in main's own TSDoc in the joint file, and composeServedPlatform: true as a bootSchemaStack option in unmanaged-tables.integration.test.ts — both name a function this branch kept under its name.
  • import { bootStack, type VerifyStack } from '@objectstack/verify' in packages/qa/dogfood/test/second-object-exposure.dogfood.test.ts (3b5475a9a4, fix(metadata-protocol,service-analytics)!: a read that follows a lookup asks the target object its declared exposure — $expand and the dataset label passes (#22661) #22735), called as bootStack(secondObjectExposureStack as never, { security: secondObjectExposureSecurity() }). BootOptions.security?: SecurityPlugin is at harness.ts:198, unchanged by this branch (6103628981 ①.3: BootOptions changes in TSDoc only), and opts.security still wins whole at kernel.use. Under this branch the boot mounts the slate around that fixture (seven sox_* objects, no requires, no cubes); what the test measures — $expand through the list, single-record and query routes, the export door, and the dataset label and sort-key passes through /analytics/dataset/query — runs on the harness's own analytics instance, which it mounted before this branch and still does (now built with { cubes } resolving to the fixture's none), and on sox_* reads the slate's sys_* providers never touch. signIn / signUp with the slate mounted are what every dogfood file already exercised on the branch's green shards at 93cbf1f46f. The compile question is settled by the type: the call site passes an option the type declares. The behaviour verdict is the dogfood shards' on this head: (3/3) green, (1/3) and (2/3) in progress at review time (which shard holds this file is not readable from the check-runs API).
  • main touched nothing under packages/verify, packages/plugins/plugin-email, packages/services/service-sms, packages/spec/src/kernel, packages/core outside security/ and utils/import-runner* (plus vitest.repo-tests.json), and not commands/serve.ts, boot-preparation-parity.test.ts or any other of the branch's 24 remaining files. So no other main-side change has a neighbour in this diff. Its two other dogfood edits (me-apps-and-everyone-baseline, showcase-fls-read-mask-strip) move permission-set authoring from a system-context insert to the data door (feat(spec,core)!: positions declare their permissionSets; the authorization resolver reads the security catalog and the activation ledger #22723) and read nothing about composition.

4. Everything the branch itself wrote — adopted from 6103787772 ① (which adopted 6103628981 ① items 1, 2, 3, 4, 5, 7, 8, 9 and 10 and re-judged 6 and 11), each RIGHT there. The pointer is honest because item 1 above shows every one of the 25 files byte-identical to what those records read: serve composes exactly what it composed; one implementation, no second copy; the public surface, nothing lost; bootStack widens within ruling A; the mail / SMS narrowing, stricter than serve on the slate path and said so; the cube clause as a widening, true on every carrier; the migrate boot's same set (re-verified here in item 2); the three dogfood deviations; the MCP / pinyin boundary; hand-written docs with one generated page routed; the Clause-②: line's shape.

② Semver level

Adopted from 6103787772 ② with its reasons: @objectstack/core, @objectstack/verify, @objectstack/plugin-email, @objectstack/service-sms minor; @objectstack/cli patch; @objectstack/dogfood private, none needed. The changeset is byte-identical across the hop (item 1), so the frontmatter, the Clause-②: yes (narrowing: …; widening: …) line (value yes, arm narrowing, one sha256 on all three carriers), the ADR-0087 marker not-required (no-migration-prescription) and the BREAKING banner with its remedy are the text that record judged RIGHT. The merge adds no package to the diff and moves no packages/**/src/** file beyond the 25. Check Changeset, the job that runs the changeset readers and the level axis against the base, is green on this head. RIGHT.

③ Boundary flags

Dev flags from the merge-round report 6104764064, each answered:

  1. conflicts: none and pure_merge: yes — measured in ①.1: 266 of 267 hop paths blob-identical to main, the one joint file read in full, the net delta content-identical to the branch's own, no conflict markers. ANSWERED.
  2. "Dogfood and the full cli unit tier were not re-run this round, and are left to CI on the merge head" — the head's check-runs below: Dogfood Regression Gate (3/3) and Dogfood Verify CLI green; (1/3), (2/3) and the six Test Core shards in progress at review time. The merge-round readings quoted for this head (verify 28 / 223, core 91 / 2333, the 15 unit and 5 integration CLI files) are claims; the landing waits on the runs. ANSWERED, pending the runs.
  3. The seat order 6103641438's merge condition ("if origin/main carries PR fix(spec): render-projection-diff generates a base's git-ignored migration registry #22750, merge it") — 052a5e153c (fix(spec): render-projection-diff generates a base's git-ignored migration registry #22750) is in the hop, so the merge was ordered and is the right act; bfc15d275b (feat(spec,core)!: positions declare their permissionSets; the authorization resolver reads the security catalog and the activation ledger #22723), named in the report, is in the hop too. ANSWERED.
  4. open_questions and out_of_scope_findings are empty; files_changed is empty, which ①.1 confirms. ANSWERED.

Carried from 6103787772 ③ without change: its items 1–4 (the patch round's flags) and everything it carried from 6103628981 ③ — items 1–10 (round 0's flags, defineStack refusing email / sms / appName filed as #22748, the refused-boot signal-listener leak suggested to the seat as a small card), 12 (content/docs/references/system/email-config.mdx:16 and packages/spec/src/system/email-config.test.ts:41 naming serve.ts as the reader's home, routed by the seat order to a spec-docblock follow-up in this lane) and 13.

Named here, not flagged by the dev:

  1. main's fix(cli): os migrate plan's unmanaged-tables sweep gates on whether the composition mirrors the served boot, sweeps every planned database, and states only a reason that holds (#22580) #22732 made plan.boot-parity.integration.test.ts strict about the unmanaged sweep running on every shape (①.2). It is an integration-tier file; whether per-PR CI runs that tier is not readable from the check-runs API, and the merge-round report's local 5 / 26 is a claim. Not a finding: the order-only argument in ①.2 holds without it, and main's own unit pins over servedBoot run in the unit tier. Observation for the seat.
  2. The merge-round report quotes core as 91 files / 2333 tests where round 0 quoted 90 / 2319: main's hop adds two files under packages/core/src/security — second-object-read-exposure.pin.test.ts, listed in vitest.repo-tests.json (the repo-tests project, 4 files to 5), and position-binding-conversion.test.ts (the unit project, 90 files to 91). Consistent; not a finding.

Check-runs on the head (32 at review time, read last, every one named):

  • Red: none. Type Check · source gates is green, every one of its 41 gate steps and 4 teardown steps success — step 24 "Render the generated spec-changes and upgrade-guide diff against the base", the queue wall the two earlier records named as not this diff's, now passes against a base that carries fix(spec): render-projection-diff generates a base's git-ignored migration registry #22750. The wall is down on this head as expected.
  • In progress at review time (12): Dogfood Regression Gate (1/3), Dogfood Regression Gate (2/3), Lint & Repo Gates, Temporal Conformance (live PG + MySQL), Test Core (1/6), Test Core (2/6), Test Core (3/6), Test Core (4/6), Test Core (5/6), Test Core (6/6), Type Check · consumer gates, Type Check · workspace. The TypeScript Type Check aggregator over the four lanes had not been created yet when the list was read, as it follows the lanes; the earlier records' count of 41 runs includes it and a second workflow run's Auto Label / Check PR Size, none of which exists on this head yet.
  • Skipped by design (3): Build Docs, Console Pin Gate (no pin change), Packed-tarball smoke (opt-in).
  • Green (17): Auto Label, Build Core, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate (3/3), Dogfood Verify CLI, filter, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, The card this PR closes must claim this branch, Type Check · debt ledger, Type Check · source gates.

Nothing must change. The merge carries main and only main; the one file both sides touched merges into a true servedBoot over an unchanged object set; the branch's 25 files are the files the PASS 6103787772 judged, byte for byte. The landing waits on the twelve in-progress runs, which is the seat's watch and not this record's.

Implemented-by: claude/issue-22301-item1-composition-gap
Reviewed-by: session_01S3aAf11JjbW1mSGL1EhfFj

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 03:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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