Repository navigation
Commit 7098aca
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).
- **`os migrate plan`'s declaration boot reads the same token rule.**
The boot-preparation parity pin that landed on `main` during this round
(#22579, `boot-preparation-parity.test.ts`) holds every step `serve`
runs to a shared function the migrate boot runs too; the merge made it
red. `composeServedPlatform` (`schema-migration-plugins.ts`) now expands
its tokens through `resolveServedCapabilities` instead of its own
`[...requires, ...PLATFORM_ALWAYS_ON_CAPABILITIES]` — the same set; a
declared `auth` now places `email` after the declared tokens, as `serve`
does. The pin lists `resolveServedCapabilities` under `composition`, and
`resolveCapabilityArgument` as `SERVE_ONLY` with its reason: that boot
constructs each provider for its declarations only, with a measured
posture per token, and runs no `start()`.
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
- `@objectstack/plugin-email` and `@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/core`
cannot import either package.
- `packages/cli/src/utils/schema-migration-plugins.ts` and
`boot-preparation-parity.test.ts`: the #22579 pin that landed on `main`
during this round, which the merge turned red.
- Three `packages/qa/dogfood` test files, not `bootShowcase`: the full
dogfood run's red. None is one of #22615's files (#22615 touched no
dogfood file).
- Three CLI test files whose prose or source scan named the moved code.
## 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:
#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](https://claude.ai/code/session_01S3aAf11JjbW1mSGL1EhfFj)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e865300 commit 7098aca
25 files changed
Lines changed: 1505 additions & 744 deletions
File tree
- .changeset
- packages
- cli
- src
- commands
- utils
- test
- core/src
- plugins/plugin-email/src
- transports
- qa/dogfood/test
- services/service-sms/src
- transports
- verify/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
44 | 46 | | |
45 | 47 | | |
46 | 48 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
22 | 26 | | |
23 | 27 | | |
24 | 28 | | |
25 | 29 | | |
26 | 30 | | |
27 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
28 | 34 | | |
29 | 35 | | |
30 | 36 | | |
| |||
58 | 64 | | |
59 | 65 | | |
60 | 66 | | |
| 67 | + | |
61 | 68 | | |
62 | | - | |
63 | | - | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
64 | 75 | | |
65 | 76 | | |
66 | 77 | | |
| |||
80 | 91 | | |
81 | 92 | | |
82 | 93 | | |
83 | | - | |
| 94 | + | |
84 | 95 | | |
85 | 96 | | |
86 | 97 | | |
| |||
90 | 101 | | |
91 | 102 | | |
92 | 103 | | |
93 | | - | |
94 | | - | |
95 | | - | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
96 | 114 | | |
97 | 115 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
29 | | - | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
| |||
0 commit comments