Repository navigation
Commit 3f80f17
fix(metadata-protocol): a cel-dated seed row reaches every hook in its temporal field's stored form (#22353)
Part of #22301
Clause-②: no
This PR carries stage 1's **item 9** only: a cel-dated seed row reaches
every hook in the stored form of its temporal field, so `bootStack`
stores the rows `objectstack dev` stores. **Item 1** (the handle mounts
what `requires` names) was implemented and measured on this branch, and
it is held back: as built, it stops every showcase boot in the dogfood
gate. How the handle treats an app's own `plugins` array goes to a
decision first (details below). Items 2 to 8 stay on the card for later
stages.
## Item 9: what the two boots handed the hook
Measured on `origin/main` 28bff18, one `bootStack` of a two-object
app: a `date` field and a `datetime` field, seeded with `daysFromNow(1)`
/ `daysFromNow(2)` as cel expressions and with literal ISO values.
| hook form | saw for the cel `date` value | saw for the literal row |
|---|---|---|
| in-process `handler` (a source config, what `bootStack` boots) | a JS
`Date` object, `2026-10-09T00:00:00.000Z` | `'2026-10-10'` (string) |
| sandboxed `body` (the compiled artifact `objectstack dev` boots) |
`'2026-10-09T00:00:00.000Z'` (string, a full instant on a `date` field)
| `'2026-10-10'` (string) |
| stored by the SQL driver | `'2026-10-09'` | `'2026-10-10'` |
So the two boots hand the hook different values. The app's hook is not
the divergence. hotcrm's `campaign_validation` reads the dates only when
they are strings, so it accepted the rows under `objectstack dev` and
refused them under `bootStack`. The divergence starts one step earlier:
`resolveSeedRecord` evaluates the expression to the `Date` the cel
stdlib returns (ADR-0053 D1), and the seed loader handed that `Date` to
the engine as-is. Neither hook form then saw the value the column
stores.
**Where it lands, and why:**
`packages/metadata-protocol/src/seed-loader.ts` (a `domain:engine` file;
the claim names it for this case). Right after resolution, each `Date`
on a `date` / `datetime` / `time` field is put into its stored form by
`temporalStorageForm` (`@objectstack/core`). That is the one rule the
drivers apply on write, so the loader carries no second copy of it. A
non-temporal field, an undeclared field and an Invalid Date are handed
over unchanged. `packages/verify` and `packages/formula` are not
touched: the verify boot's seed path is the shared loader, and
`resolveSeedRecord` has no field types to work with.
**What changes:** both hook forms now see the stored form
(`'2026-10-09'` for a `date`). A body hook on a `date` field sees
`2026-10-09` where it saw `2026-10-09T00:00:00.000Z`. Stored values do
not change on any driver, because each driver already normalises a
declared temporal field on write: `driver-memory` through the same
`temporalStorageForm`, `driver-sql` and the two drivers that extend it
(`driver-sqlite-wasm`, `driver-turso`) on their own write path, and
`driver-mongodb` through its `coerceTemporalValue`. Changeset:
`@objectstack/metadata-protocol` patch.
**Pin:** `packages/verify/src/harness.seed-temporal-form.test.ts` boots
through the real handle. It asserts that the cel rows are stored, that
the handler and the body each see exactly the stored value, that a hook
requiring string dates (hotcrm's shape) lets the row through, and that
the literal control rows are unchanged.
**Ablation**, at `e83540e29`, through `scripts/ablation-replace.mjs`:
- **Mutation leg.** The call was swapped for a no-op call carrying a
marker. `metadata-protocol` was rebuilt, and `ablation-dist-preflight`
found the marker in 2 built files. Result: 4 failed / 1 passed. The
control passed. The guarded hook logged `[SeedLoader] Failed to write
stf_guarded record #0 (name=guarded-cel): both dates are required`,
which is hotcrm's failure reproduced.
- **Restore leg.** Blob `097d4adee50f` equals HEAD, and `git diff HEAD`
is empty. After the rebuild the marker is absent from all 24 built files
and the tree is clean. Result: 5/5 passed.
- A first mutation attempt (a bare `void` statement) did not build. The
unused helper failed the d.ts pass, and the preflight found no marker.
That run's reading is void and is not counted above.
## Item 1: implemented, measured, held
What was built and measured, kept in this branch's history at
`ef5396ca2` and reverted at `b3186ec18`:
- **H1, the home: `@objectstack/core`.** The measured graph:
`@objectstack/cli` depends on `@objectstack/verify`, so the CLI cannot
be the home, and an `@objectstack/cli` subpath would close a cycle. Both
packages already depend on `@objectstack/core`, which owns the package
ordering (`resolveArtifactPackageOrder`) that the `requires` reader is
built on. `CAPABILITY_PROVIDERS`, `CapabilitySpec` and
`providesCapability` moved there. So did the reader
`stackDeclaredCapabilities` and its rule (`resolveStackCollection`, from
`packages/cli/src/utils/stack-collections.ts`).
`Serve.CAPABILITY_PROVIDERS` and `Serve.providesCapability` became
handles over the core declarations, pinned by identity (`toBe`), and the
CLI drift tests were re-pointed. There was no second copy.
- **H2, the reader.** `bootStack` read `requires` through
`stackDeclaredCapabilities`. Pins covered the top level, the package
bodies, the top level winning over the bodies, and a control. A caller's
instance won (`extraPlugins`), and hard dependencies came along
(`triggers` brought `job` and `queue`). Every provider package in the
table became a dependency of `@objectstack/verify`, with no cycle
(measured over deps, devDeps and peers). The pins were 7/7 green. With
the mount removed by ablation, 4 went red, while the control,
explicit-wins and manifest cases stayed green. Afterwards the blob
equalled HEAD. The CLI unit and integration pins over the moved code
were 260/260 and 11/11.
- **The blocker, measured.** `pnpm --filter @objectstack/dogfood exec
vitest run --project shared-showcase` failed 13 of 13 files at
bootstrap: `connector instance 'showcase_status_api' declares provider
'rest', but no provider factory is registered`. The showcase declares
`requires: ['automation', …]`. Automation's start materializes the app's
connector instances (ADR-0097). Their providers sit in the showcase's
own `plugins` array, which `objectstack serve` mounts and `bootStack`
never has. An uncommitted experiment that also mounted the app's
`plugins` instances moved the refusal one layer: `providerConfig.spec
'./src/system/connectors/status-openapi.json' … resolved to
'/tmp/os-dogfood-run-…'`. `serve` anchors automation's `packageRoot` at
the config's directory, and the handle only knows `hostRoot`. 117
dogfood files boot the showcase directly, and 11 of them pass a
connector plugin. For hotcrm, which carries no `plugins` array, the same
implementation boots green.
The open question is how the handle treats an app's own `plugins` array:
mount it as `serve` does, keep it the caller's, or put the whole
`requires` mounting behind an option. It is in the card report with each
option's cost.
## Tests (at `e83540e29`)
- `pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2`: 19
files / 138 tests passed.
- `pnpm --filter @objectstack/metadata-protocol exec vitest run
--maxWorkers=2`: 223 passed, 3 skipped files; 28,338 passed, 19 skipped
tests.
- `typecheck` for `@objectstack/verify` and
`@objectstack/metadata-protocol`: green, with test-layer debt unchanged.
- Gates: `dispatch-gates --commands` derived 62 commands on this tree.
All 62 were run and exited 0. `--ran` reports 62 derived, 62 run, 0
NOT-MEASURED and 0 UNRUN.
- Lint, narrowed: `eslint --no-inline-config` over the 2 changed
TypeScript files (the population `eslint.config.mjs`'s `**/*.{ts,…}` /
`packages/**/*.{ts,…}` objects match), counted from `--format json`: 2
files, 0 errors, 0 warnings. The config enables no type-aware linting,
so this diff cannot move the verdict of any untouched file. The full
`pnpm lint` is CI's.
- NOT MEASURED locally: the full dogfood suite and the integration
tiers. Both are CI's.
## Acceptance notes
- `bootStack` constructs its `AnalyticsServicePlugin` without the app's
`analyticsCubes`, and `serve` passes them. This is a boot-parity gap in
the same family as item 1, named in the card report for a later stage.
- A literal JS `Date` written into a source-config seed now reaches
hooks in stored form. The same seed compiled to an artifact arrives as a
JSON ISO string, which the loader leaves unchanged. Cel values agree in
both boots, but literal `Date`s still differ by form. No producer of
that shape was measured.
- The engine's private `resolveNowDefault` carries its own date / time /
instant table beside `temporalStorageForm`. The two agree today. This is
an observation, with no carrier.
## Patch round (contract review FAIL `6069291830` → seat order
`6069311153` → `792bcc71d`)
Written into this body by the `domain:spec` seat 2 at 2026-10-08T21:30Z.
The comments before it on this PR are read: the docs-drift bot's, and
the contract review this round answers.
- The changeset's sentence "The in-memory driver now stores the string
instead of a `Date` object" was false, because `driver-memory` already
normalises every declared temporal field on write (`toStorageForms` →
`coerceTemporalValue` → `temporalStorageForm`). It is replaced with:
stored values do not change on any driver, because each driver already
normalises a declared temporal field on write. The dev read
`driver-memory` and `driver-mongodb` before writing it, and the seat
confirmed that `driver-sqlite-wasm` and `driver-turso` extend
`SqlDriver`. The "What changes" line above is corrected the same way.
- One changeset commit (+1 / -1), no code. The 20 changeset/text gate
families, plus `check-changeset-fixed.mjs` and `check:empty-changeset`,
all exit 0. `main` was not merged (the push was accepted).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 54d32e5 commit 3f80f17
3 files changed
Lines changed: 202 additions & 1 deletion
File tree
- .changeset
- packages
- metadata-protocol/src
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | | - | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
27 | 30 | | |
28 | 31 | | |
29 | 32 | | |
| |||
440 | 443 | | |
441 | 444 | | |
442 | 445 | | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
443 | 479 | | |
444 | 480 | | |
445 | 481 | | |
| |||
1248 | 1284 | | |
1249 | 1285 | | |
1250 | 1286 | | |
| 1287 | + | |
| 1288 | + | |
1251 | 1289 | | |
1252 | 1290 | | |
1253 | 1291 | | |
| |||
| 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 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
0 commit comments