Commit be55fd2
fix(objectql): a seed row keeps its authored created_at on insert, as the replay already does (#21661)
Fixes #21646
Clause-②: no
- An authored seed value that was silently dropped on insert is now
kept, as the replay already keeps it.
- No accepted input, key, export or error code is added or removed.
- No contract surface is touched.
## What changed
`sys_stamp_audit_insert` (`packages/objectql/src/plugin.ts`,
`applyToRecord`) now keeps an authored `created_at` (`?? now`) on a seed
write (`ExecutionContext.seedReplay`), as it already did under
`preserveAudit`:
```ts
record.created_at = preserveAudit || seedReplay ? (record.created_at ?? now) : now;
```
That one ternary is the only change to the stamp. `updated_at`,
`created_by`, `updated_by`, `tenant_id` and the whole update stamp
behave exactly as before. `preserveAudit` is not touched.
`SEED_WRITE_EXECUTION_CONTEXT` is read, not edited, so the seed context
does not gain `preserveAudit`. A bare `isSystem` context, REST and every
other caller still stamp now.
## Where the hook reads `seedReplay` (H1, measured: one detail of the
suggested route changed)
- **The hook session has no `seedReplay`.** The stamp reads its flags
from `hookCtx.session`. `buildSession` (`engine.ts`) builds that object
one field at a time: `userId`, `organizationId`, `positions`,
`accessToken`, `isSystem`, `actor`, `skipTriggers`, `skipAutomations`
and `preserveAudit`. It never copies `seedReplay`, so a branch on
`session.seedReplay` would read `undefined` on every seed write.
- **The flag is read from the options bag.** The `beforeInsert`
envelope's `input.options` is the caller's own options bag. The
HookContext `input` PHASE contract in
`packages/spec/src/data/hook.zod.ts` says so: a `before*` handler reads
the caller's bag. `engine.insert` is the only `beforeInsert` dispatch
site, and it sets `opCtx.context = options?.context`. So
`hookCtx.input.options.context.seedReplay` is the seed context exactly
as the seeder built it. The new helper `isSeedReplay` reads it there.
- **Not taken: copying `seedReplay` into `buildSession`.** That would
create a key the engine produces but no contract declares. Fixing that
would need a new key on `HookContextSchema.session` in `packages/spec`,
which is a contract surface this card's Clause-② says it does not touch.
This builtin would be the key's only reader.
- **Trust is the same on both routes.** `seedReplay` is a
`NonEntryExecutionContextField`
(`packages/core/src/security/assemble-execution-context.ts`), so no
transport entry point builds it from a request. The session is also
built from that same context object.
- **`skipTriggers` does not skip the audit hooks.** `triggerHooks` reads
only `session.skipAutomations`, and only to skip hooks bound from
metadata. `skipTriggers` gates flow dispatch, never the code-registered
audit hooks, and the seed context carries no `skipAutomations`. Pin 2
shows this: an unauthored seed row is stamped by the hook at boot.
## H2: `created_by` (measured, no change)
The seed context carries no `userId`. The stamp assigns `created_by` and
`updated_by` only inside `if (session?.userId)`, on both events, so it
writes neither on a seed write. Seed writes are `isSystem`, so the
readonly strip does not run on either path. The result:
- an authored `created_by` is kept on the insert and on the replay
update;
- an unauthored `created_by` stays absent.
Both paths already behaved the same before this change. A companion test
records the measurement, labelled as green on both sides of the change.
## H3: three readers, one context (measured)
| reader | call | covered by |
| --- | --- | --- |
| `SeedLoaderService` | `engine.insertMany` / `insert` / `update` with
`{ context: SEED_WRITE_EXECUTION_CONTEXT }` | pins 1, 2 and 4, through
`load()` |
| `AppPlugin` replaying a stack's `data[]` | main path: `new
SeedLoaderService(ql, …).load()`; both fallbacks: `ql.insert(object,
record, SEED_WRITE_OPTIONS)` per row | pin 1 (loader) and pin 1's
single-row case |
| `@objectstack/verify` `seed()` | `ql.insert(object, rows, { context:
SEED_CONTEXT })` with an array, where `SEED_CONTEXT =
SEED_WRITE_EXECUTION_CONTEXT` | pin 1's array case |
All three reach the stamp with `seedReplay` set. There is no producer to
fix. The other `new SeedLoaderService(…)` sites (package install in
`protocol.ts`, `runtime/src/domains/packages.ts`, and the other
`app-plugin.ts` sites) are the same loader.
## H4: `cel` values (measured)
`SeedLoaderService` evaluates every Expression envelope
(`resolveSeedRecord`) before it decides insert or update. The insert and
the update therefore both receive the evaluated instant, which is a
`Date` for ``cel`daysAgo(5)` ``. Pin 1 reads it back as
`2026-09-28T00:00:00.000Z` on both boots, the value the card measured on
its replay.
## H5: the warning (measured)
`preserveAuditIgnoredOnInsertWarning` is emitted only from the
non-`isSystem` branch of `engine.insert`'s create-side strip, and only
when `preserveAudit` was requested. A seed write is `isSystem` and has
no `preserveAudit`, so the warning cannot fire for it, before or after
this change. Pin 4 checks both halves. Two seed boots produce no line
with the warning's lead clause. A non-system `{ userId, preserveAudit:
true }` create produces exactly
`preserveAuditIgnoredOnInsertWarning('seed_case', ['run_at'])`. The
expected line comes from the producer function, not from a copy of its
text.
## Tests
New: `packages/objectql/src/plugin-audit-seed-created-at.test.ts`. It
boots a real `ObjectKernel` with `ObjectQLPlugin`, so the audit hooks
are bound the way a booted app binds them. It runs the real
`SeedLoaderService.load()` twice over one store-backed stub driver: a
fresh boot, then a replay. `Date` is faked to two instants on the same
UTC day.
- **Pin 1.** An authored `created_at` is kept on the fresh boot (INSERT)
and on the replay (UPDATE). Row A is ``cel`daysAgo(5)` `` and reads
`2026-09-28T00:00:00.000Z`; row B is `2026-09-01T12:00:00.000Z`. The
replay is a real update (`totalUpdated: 3`) and moves `updated_at` to
the second boot. A second case covers the call shapes of the other two
readers.
- **Pin 2.** A seed row with no `created_at` is stamped at boot, and the
replay leaves that stamp alone.
- **Pin 3.** A non-seed system insert (`{ isSystem }` and `{ isSystem,
skipTriggers }`) and a REST insert (the protocol's `createData`, the
door `POST /api/v1/data/OBJECT` uses) are all stamped now.
- **Pin 4.** The `preserveAudit` insert warning is unchanged, as
described under H5.
- **Companion.** The `created_by` measurement from H2.
Pre-fix reading, the same file against unfixed `plugin.ts`: `Tests 2
failed | 4 passed (6)`. Both pin 1 cases fail with `expected
'2026-10-03T12:00:00.000Z' to be '2026-09-28T00:00:00.000Z'`, which is
the card's table: the boot instant replaced the authored value.
Readings at `e5a2555da6` (the head after merging `origin/main`
`6ec54f00ba`), unless a different commit is named:
- **The pin file and its four nearest neighbours**, run with `pnpm
--filter @objectstack/objectql exec vitest run --maxWorkers=2`:
`plugin-audit-seed-created-at`, `plugin-audit-created-at-create-side`,
`plugin-audit-created-by-create-side`, `engine-seed-required-deferral`
and `seed-loader-org-stamp`. Result: `Test Files 5 passed (5)`, `Tests
24 passed (24)`.
- **The whole local project** (`vitest run --project local
--maxWorkers=2`): `Test Files 370 passed (370)`, `Tests 7439 passed
(7439)`.
- **The repo project** (`--project repo`, one file): `Tests 5 passed
(5)`, read at `2a8264570c`.
- **`pnpm --filter @objectstack/objectql typecheck`**: exit 0. This
covers `tsc` over src and scripts, plus `check:test-typecheck` over the
test layer, which reported `40 file(s) / 234 error(s) / 65 pinned
signature(s) held`. The ledger is unchanged. `--listFilesOnly` shows
that program includes the new test file.
## Acceptance notes
- **Under the seed context, a malformed authored `created_at` is now
stored on the first insert.** Before this change it was stored only on
the replay update. Measured through `SeedLoaderService.load()` over the
kernel's engine: a literal `'yesterday'` on an author-declared
`readonly` datetime, and on `created_at`, is stored verbatim with no
error. The same literal on a non-readonly datetime is refused (`must be
a valid datetime (ISO-8601)`) and reported as a seed error. A raw
``cel`…` `` envelope, which only a writer that skips `resolveSeedRecord`
can send (`AppPlugin`'s two fallbacks, `verify.seed()`), is stored the
same way. On the update path the envelope was already stored for
`created_at` before this change. The cause is outside this card: a
system-context write does not check the value shape of `readonly`
fields. That is reported to the seat as a separate finding, not fixed
here.
- Two descriptions of `preserveAudit` still say "symmetric with how
`created_at` / `created_by` (already) behave on insert": the
`ExecutionContext.preserveAudit` TSDoc and the
`HookContext.session.preserveAudit` TSDoc. That has not been true since
`created_at` and `created_by` stopped being client-preferred on an
ordinary insert. This is older drift, outside this card, and not fixed
here.
- The `seedReplay` TSDoc in `execution-context.zod.ts` lists what the
flag exempts, which is only the `state_machine` rule. It does not
mention that the audit stamp now keeps an authored `created_at` under
it. Its statements are still true, so this change does not make them
false. The file is outside this card's surface.
- `content/docs/data-modeling/seed-data.mdx` already shows ``created_at:
cel`now()` `` in a seed record. That value is now kept on insert as well
as on replay. No doc text changes.
## Reverse verification
The fix was committed first. The reverse leg then reverted only the
stamp's `seedReplay` arm. It went through
`scripts/ablation-replace.mjs`, whose anchor must hit, and a shell
`trap` also ran `git checkout HEAD --` on the absolute path. Predicted
direction: pin 1 red, everything else green.
Observed at `e5a2555da6`, and identically at `2a8264570c`:
- **The mutation landed.** The anchor count went 1 → 0 and the reverted
form 0 → 1. The blob went `e34d4989074d` → `4645b78e8872`.
- **Only pin 1 went red.** Result: `Tests 2 failed | 4 passed (6)`. Both
pin 1 cases failed with `expected '2026-10-03T12:00:00.000Z' to be
'2026-09-28T00:00:00.000Z'` and `… to be '2026-09-01T12:00:00.000Z'`.
Pins 2, 3 and 4 and the companion stayed green.
- **The file was restored.** The blob after restore equals the HEAD blob
(`e34d4989074d`), `git diff HEAD` is 0 bytes, and `git status
--porcelain` is empty.
No `dist/` build is in this loop. The pin file imports `./plugin.js` by
relative path, so vitest reads the mutation from `src/`.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, run with no paths at `e5a2555da6`, derives
64 commands for this change set. That is the dispatch list's 50 plus 14
more:
- `check-adr-0087-registration` (twice);
- `check-empty-changeset` (twice);
- `release-rehearsal-clone --self-test`;
- `release-pending-publish --self-test`;
- `check:engine-double-contract`;
- `check:objectql-double-limit`;
- `check:objectui-changeset`;
- `check:pm-changeset-deadline-census`;
- `check:query-options-erasure`;
- `check:type-check-coverage`;
- `check:type-check-debt`;
- `check:where-matcher`.
All 64 ran at `e5a2555da6`, with each exit code captured before any
pipe: 64 × exit 0. The `--ran` reconciliation reports `64 derived
famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all
64 recorded an exit code and none of them is 3)`.
- `check:objectql-double-limit` failed on the first run because the new
stub driver's `find` ignored `limit`. `f135b5c542` fixes that: `find`
now applies `ast.limit` after the filter.
- NOT MEASURED locally: the families the derivation lists outside its
total, which run only in CI. These are shard attestation, test
completeness, the Test Core, Dogfood, Temporal Conformance and Build
Core jobs, and the workspace type-check lanes. The repository-wide `pnpm
lint` also runs only in CI.
- `origin/main` gained one more commit after the merge: `eea82af677`,
metadata-protocol's view-container save door. None of its files is in
this diff.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 55e6f14 commit be55fd2
3 files changed
Lines changed: 388 additions & 5 deletions
File tree
- .changeset
- packages/objectql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
Lines changed: 344 additions & 0 deletions
| 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 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
0 commit comments