Repository navigation
Commit fe5ef8c
fix(spec)!: defineSeed refuses a record key the target object does not have (#22294)
Fixes #22149
Clause-②: yes (narrowing: a misspelled seed key is refused where it
passed, while the record type now admits the injected system columns and
`InjectedSystemColumnName` is a new public export, which widen the
published surface)
## What changes
- **`defineSeed(obj, config)` checks every record key when it runs.** A
key must be a field `obj` declares or a system column the platform
injects on `obj`. That set is `resolveInjectedSystemColumns(obj).names`,
the per-object answer the registry's own `applySystemFields` reads.
Every unknown key is refused in one error naming the object, the record
index and the key, with a near-miss suggestion. The shape is the one
`ObjectSchema.create()` uses for an unknown object key:
```text
defineSeed('crm_case'): unknown field(s) in records — created_atx.
• records[0]: `created_atx` is not a field of `crm_case`. Did you mean
'created_at'?
```
- **The record type admits the injectable system columns.** The new
exported type `InjectedSystemColumnName` comes from
`packages/spec/src/data/injected-system-columns.ts`: a record literal
writing `created_at` or `owner_id` now passes `tsc`, where it used to be
refused. The type cannot evaluate an object's opt-outs, so the call
narrows it per object. `created_at` on a `systemFields: false` object,
or `owner_id` on an `ownership: 'org'` object, is refused when the call
runs.
- **The docblock and `content/docs/data-modeling/seed-data.mdx` say
exactly what each half enforces.** The compile-time half is TypeScript's
excess-property check on a record literal. The call-time half checks
every record.
**Which leg, named with the measurement (triage asked the claimant to
name it): both.** The type is exact wherever TypeScript's
excess-property check runs, but that cannot hold for every record or
object shape. So the check that holds for every record is the call-time
one. `os validate`, `os build` and boot reach it by evaluating the
config module, so no `packages/cli` edit was needed.
## Premises, measured
- **H1, the record type widens to `string` keys for an
`ObjectSchema.create()` object: FALSIFIED.** Compiled hotcrm's own
`crm_case` object (`src/service/objects/case.object.ts` at hotcrm
`99d290a`) against the published `@objectstack/spec` 17.7.0 tarball.
`keyof` its `fields` is 23 literal keys, and an inline `{ subject,
created_atx }` record is TS2353. What actually silences the check is the
shape of hotcrm's `cases` seed: its `records` array also spreads an
array typed as a string-keyed `Record` of unknown values. That spread
turns off excess-property checking for every inline record beside it.
Probe: inline misspelling plus that spread compiles clean, and the
control (inline misspelling plus a spread of narrowly typed rows) is
TS2353. The other holes measured on the source: records from a variable,
and an object typed `ServiceObject` or an `ObjectSchema.parse()` result,
whose keys are `string`. The same type refused `created_at` in a record
literal (TS2353 on the 17.7.0 types too), so hotcrm's legitimate
`created_at` compiled only through the spread hole.
- **H2, `os validate` evaluates the seed module: HOLDS.** `loadConfig`
evaluates the config through `bundleRequire`, which runs every
module-level `defineSeed` call. Measured on `examples/app-todo` through
the source CLI entry (`packages/cli/bin/run-dev.js validate`), with a
misspelled key `categroy` injected into a seed record. Before (the
call-time check disabled in spec's `dist`, marker proven present in 4
built files): exit 0, "Validation passed". After: exit 1,
`defineSeed('todo_task'): unknown field(s) in records — categroy`, with
"Did you mean 'category'?". Controls: the unmodified app exits 0, and
`created_at` injected into a record exits 0, "Validation passed". Every
injection was restored and hash-verified to the HEAD blob.
- **H3, one declared source for the system columns: HOLDS, for the
question the check asks.** `resolveInjectedSystemColumns(def).names`
(`packages/spec/src/data/injected-system-columns.ts`) is "every column
addressable on this object without being authored". It holds the
driver's `id`, plus `organization_id`, the four audit columns,
`owner_id` and `owning_business_unit_id` as the object's `systemFields`,
`tenancy`, `ownership` and `managedBy` select them. The registry's
`applySystemFields` consumes the same plan. No second list is written:
the type-level union is read off the same constants, and the function
now builds `names` as a Set of `InjectedSystemColumnName`, so a column
it starts adding without widening the union is a compile error. What it
answers is existence, not "the loader honours this value": see
Acceptance notes.
## Landing outside the claimed file surface
- `packages/spec/src/data/injected-system-columns.ts`: one exported type
plus the typed Set. The type half has to admit the system columns
without a hand-written list, and the declared source lives here.
- `content/docs/data-modeling/seed-data.mdx`: the page made the same
promise the docblock made (lines 7-9 and 48: TypeScript validates every
record key), quoted a stale error text, and showed a stale `records`
signature. Now it states both halves. Its CEL example writing
`created_at`, `owner_id` and `organization_id` inline compiles under the
new type.
- `packages/spec/api-surface/data.json` and `export-origins/data.json`:
regenerated by `check:generated --fix`, one added type entry each.
## Tests
Final gate union at `d6ab9ae9c`. The spec suites ran at `2e559a83d`. The
only commit since, `d6ab9ae9c`, touches only the changeset file.
- **New `packages/spec/src/data/define-seed-record-keys.test.ts`, 9
cases.** The docblock's own unknown-key example fails `tsc` (a
`@ts-expect-error` in the `tsconfig.test.json` program; the file is in
its 2283-file `--listFilesOnly` list) and is refused when it runs. Also
refused when they run: a misspelling beside the spread of untyped rows,
records from a variable, an object typed `ServiceObject`, and several
unknown keys collected into one error. The per-object plan refuses
`created_at` on `systemFields: false` and `owner_id` on `ownership:
'org'`, while `id` is still accepted. Controls: a declared-fields-only
seed passes and returns the parsed seed. The system-field control
passes: `created_at`, `id`, `owner_id` and `organization_id` pass `tsc`
and the call.
- **Ablation U1 (runtime check disabled by an early return): 7 refusal
cases red, 2 controls green.** The first U1 attempt was a no-op: the
replacement still contained the anchor, so the tool refused and
restored, and nothing ran. Re-anchored, the mutation landed (anchor 1 to
0).
- **Ablation T1 (`SeedRecord` admitting any key): the test file's
directive becomes TS2578 "Unused '@ts-expect-error' directive".** The
narrow program with the same `tsconfig.test.json` settings is clean on
HEAD.
- **Door ablation and its restore leg.** Described under H2. After
restoring, spec was rebuilt; the marker is absent from all 232 built
files, and the whole tree is clean against HEAD.
- **`pnpm --filter @objectstack/spec test`: 626 files, 18670 passed, 1
todo.** `test:repo`: 53 files, 903 passed. `typecheck`: exit 0 (src
`tsc`, scripts, and the test layer held by `test-typecheck-debt.json`).
- **Consumers.** The `defineSeed` callers were re-taken from the tree:
`examples/app-crm`, `examples/app-showcase`, `examples/app-todo` and
`packages/qa/dogfood`. `organizations` and `plugin-security` only
mention it in comments, and `spec/scripts/schema-index.test.ts` holds it
in a string fixture. Results:
- `example-todo`: 7 files, 238 passed; `typecheck` exit 0.
- `example-crm`: 5 files, 45 passed; `typecheck` exit 0.
- `example-showcase`: 33 files, 408 passed; `typecheck` exit 0.
- dogfood `seed-ownership-claim-dispatch.dogfood.test.ts`: 1 passed;
`typecheck` exit 0.
Each seed module is in its package's `tsc` program (`--listFilesOnly`).
The rest of the dogfood suite is declared to CI.
- **Records the narrowing refuses: none.** Every seed module was
evaluated against the rebuilt `dist`. app-crm passes (5 seeds, 28
records), app-showcase passes (19 seeds, 132 records) and app-todo
passes (1 seed, 8 records). In the same resolution context, a misspelled
record throws, which is the control. hotcrm at `99d290a`, with
`@objectstack/spec` resolved to this build: all 8 seed modules pass (354
records, including the `created_at` its case seeds author), and its
`crm_case` with `created_atx` is refused.
- **Gates.** `dispatch-gates --commands` derives 108 (the dispatch
list's 86, plus 22 docs families from the page edit, `check:generated`
and `check:skill-examples`). Run at `d6ab9ae9c`: 108 run, all exit 0.
`--ran` verdict: "108 derived famil(ies) accounted for — 108 run, 0
NOT-MEASURED (a DERIVED zero — all 108 recorded an exit code and none of
them is 3)". An earlier pass at `2e559a83d` had three non-zero results,
all resolved:
- `check-adr-0087-registration` exit 1: a FROM to TO table contradicted
the `no-migration-prescription` disposition. The remedy is now prose, as
precedent `22019` does.
- `check:skill-examples` and `check:dual-build-cjs-loads` exit 3:
PREREQUISITE NOT MET, unbuilt packages. After the prerequisites were
built, both exit 0.
- **Lint, a declared narrowing.** The repo sweep is CI's. ① Population:
the `eslint.config.mjs` `files` globs match only code extensions, so
this diff's lintable files are the 3 changed `.ts` files. The other 4
are `.md`, `.mdx` and `.json`. ② Count: eslint `--format json` reports 3
files, 0 errors, 0 warnings. ③ Invariance: the config enables no
type-aware linting (no `parserOptions.project`, no `projectService`), so
this diff cannot move the verdict on an untouched file.
## Changeset
`minor`, **BREAKING**, the launch-window grade for accept-set
narrowings. This is not a type-only change. The refusal is a call-time
verdict, reached at `os validate`, `os build` and boot. ADR-0087
disposition: `not-required (no-migration-prescription)`. No key,
spelling, export or stored shape moves; the only export change is an
added type. The repair is the author's edit of a misspelled key, which
no ledger entry can derive.
## Acceptance notes (noted, not filed)
- **`skills/objectstack-data/references/seeds.md` (governed, not edited
here).** Lines 5-6 say TypeScript checks every record's keys, which
holds only for record literals; the call checks every record. Line 54
gives the `records` type as a partial record over the object's field
keys. That is stale: the type is `SeedRecord`, which adds the injectable
system columns and narrows reference values. Its CEL example (lines
114-121) writes `created_at`, `owner_id` and `organization_id` inline;
it compiles under the new type.
- **Seeds not built with `defineSeed` get no authoring-time key check.**
That covers a plain seed literal in `defineStack({ data })`,
`SeedSchema.parse()`, and a runtime `seed` draft. For these, the
engine's declared-field door on insert (`undeclaredWriteFieldErrors`,
`INVALID_FIELD`) is what refuses an undeclared key. That is a code
reading, not measured here.
- **Existence is not honour.** The check accepts `updated_at` because
the column exists, but the insert audit stamp overwrites an authored
`updated_at` (only `created_at` is kept for a seed). That is a code
reading of objectql's audit binder: value semantics, not this card.
- **Injected lookup columns take `unknown` values.** For `owner_id`,
`created_by`, `updated_by`, `organization_id` and
`owning_business_unit_id` in a record literal, the value type is
`unknown` unless the object declares the field itself. So
`SeedFieldValue`'s natural-key narrowing does not apply to them. Their
definitions are not literally typed, so the narrowing cannot be derived
from the source without a second list.
- **The refusal carries no ADR-0112 `code`.** It is a plain `Error`, the
same as `ObjectSchema.create()`'s unknown-key refusal. A code would be a
new ledger entry, a naming decision not taken here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 79c35d4 commit fe5ef8c
7 files changed
Lines changed: 328 additions & 20 deletions
File tree
- .changeset
- content/docs/data-modeling
- packages/spec
- api-surface
- export-origins
- src/data
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
8 | | - | |
9 | | - | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
| |||
44 | 46 | | |
45 | 47 | | |
46 | 48 | | |
47 | | - | |
48 | | - | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
49 | 52 | | |
50 | 53 | | |
51 | 54 | | |
| |||
255 | 258 | | |
256 | 259 | | |
257 | 260 | | |
258 | | - | |
259 | | - | |
260 | | - | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
261 | 269 | | |
262 | 270 | | |
263 | 271 | | |
| |||
269 | 277 | | |
270 | 278 | | |
271 | 279 | | |
272 | | - | |
| 280 | + | |
273 | 281 | | |
274 | 282 | | |
275 | 283 | | |
276 | 284 | | |
277 | 285 | | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
278 | 302 | | |
279 | | - | |
| 303 | + | |
280 | 304 | | |
281 | 305 | | |
282 | 306 | | |
| |||
565 | 589 | | |
566 | 590 | | |
567 | 591 | | |
568 | | - | |
| 592 | + | |
569 | 593 | | |
570 | 594 | | |
571 | 595 | | |
572 | 596 | | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
573 | 601 | | |
574 | 602 | | |
575 | 603 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
400 | 400 | | |
401 | 401 | | |
402 | 402 | | |
| 403 | + | |
403 | 404 | | |
404 | 405 | | |
405 | 406 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
390 | 390 | | |
391 | 391 | | |
392 | 392 | | |
| 393 | + | |
393 | 394 | | |
394 | 395 | | |
395 | 396 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
75 | 93 | | |
76 | 94 | | |
77 | 95 | | |
| |||
144 | 162 | | |
145 | 163 | | |
146 | 164 | | |
147 | | - | |
| 165 | + | |
148 | 166 | | |
149 | 167 | | |
150 | 168 | | |
| |||
0 commit comments