Repository navigation
Commit 99aeee6
fix(cli)!: os validate and os build refuse a seed record value the seed load would refuse, in the load's words (#27600)
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBKxmDhctefSL1Rt755zqeNc)_
Fixes #27259
Claim: 6496966334 (amended 6499269644)
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none edited. No skill teaches that `os validate` passes a
seed value the load refuses, and none teaches a `{ value, currency }`
seed. Governed hits that list what `os validate` checks stay true as
non-exhaustive lists:
`skills/objectstack-platform/references/operations.md:13`,
`skills/objectstack-data/SKILL.md:453`,
`skills/objectstack-ui/SKILL.md:284`. The seed teaching
(`skills/objectstack-data/references/seeds.md`,
`skills/objectstack-formula/SKILL.md:270` on `cel` seed values) stays
true: an expression value is left to the load. Named, not edited.
ADR-0087: `not-required (no-migration-prescription)`, in the changeset.
No key of any schema moves; the refusal moves earlier, from the seed
load to the authoring commands, in the load's own words, and every row
it names was already dropped at load.
**`Metadata-protocol: narrowed`, per the claim amendment.** A seed value
`os validate` and `os build` newly refuse is a value refused at the
authoring door, so the kind is `narrowed` and both packages the diff
moves take `minor`. The measurement beside it: what the runtime stores
does not move. The record validator refused these values at seed load
before this PR and refuses the same values after it; the refusal only
moves earlier. The changeset carries the BREAKING banner.
## 维护者速读
- **改了什么:** `os validate` 和 `os build`
现在用种子加载时同一个记录校验器检查每条种子记录的字段值;加载会拒收的值(比如货币字段写成 `{ value: 10, currency:
'USD' }`)在校验时就报错,错误码和文案与加载时一字不差,并定位到 `data[0].records[4].budget`。
- **为什么改:** 以前作者自己的检查说"通过"(exit 0),这条种子要等到启动加载时才被丢弃,只在启动日志里留一行错误。
- **风险与回滚:** 收窄 `os validate`/`os build`
的接受集;运行时接受与拒收的集合不变(这些行加载时本来就被拒)。仓内四个示例应用 873 个种子值全部照常通过。回滚 = revert 本
PR。
- **先看:**
`packages/cli/src/utils/seed-record-values.ts`(哪些值交给判定、哪些留给加载)、`packages/objectql/src/validation/record-validator.ts`
的
`validateInsertValues`(插入路径的同一套归一化与校验调用)、`packages/cli/test/seed-record-values-load-parity.test.ts`(与真实种子加载逐行对拍)。
- **两分钟验证:** `pnpm --filter @objectstack/cli exec vitest run
test/seed-record-values-load-parity.test.ts` → 1 个用例绿:校验时拒收的行号 =
真实加载拒收的行号(0、3、4、5、6),且加载日志里的句子以校验给出的句子结尾。
- **需要你做的:** 无,席位落地。
## What changes
**Before, measured on `origin/main` `ad3a640e`.** A scratch copy of
`examples/app-todo` with `budget: Field.currency({ label: 'Budget' })`
on `todo_task` and one seed row `budget: { value: 10, currency: 'USD'
}`:
```text
$ npx objectstack validate → "✓ Validation passed (277ms)", exit 0
$ npx objectstack dev --fresh → ERROR [SeedLoader] Failed to write todo_task record #4 (subject=Fix Server bug):
Budget is a currency amount: write it as a bare number (ADR-0104), such as 10,
not an object such as { value, currency }. A currency field declares its currency
in currencyConfig: the fixed mode's defaultCurrency, or under the dynamic mode
(the default) the tenant default currency
"Seeds: com.example.todo 7 ok / 1 error"
```
**After, same copy, head `d914d366` and later.**
```text
$ npx objectstack validate → exit 1
✗ Seed record values the seed load refuses (1 issue)
✗ data[0].records[4].budget: Budget is a currency amount: write it as a bare number (ADR-0104), such as 10,
not an object such as { value, currency }. … the tenant default currency (invalid_number)
$ npx objectstack build → exit 1, the same two lines
budget: 10 → "✓ Validation passed", exit 0
budget: '10' → "✓ Validation passed", exit 0 (the load writes the number 10)
budget: -5 → "✓ Validation passed", exit 0
budget: 'abc' → exit 1, "data[0].records[4].budget: Budget must be a number (invalid_number)"
```
- **`@objectstack/objectql`: `validateInsertValues(objectSchema, row,
options?)`, new export.** The insert write's verdict on the values a row
supplies: the insert door's two rewrites (`normalizeBlankTypedValues`,
`normalizeNumericStringValues`), the multi-value wrap
(`normalizeMultiValueFields`, scope `'include'`), then the same
`validateRecordInScope(…, 'insert', 'include', …)` call
`ObjectQL.insert()` makes. It judges only the declared fields the row
supplies with a non-missing value, so `required` (which a default or a
hook may still satisfy) and undeclared names are left to the load. ⛔ One
judge: nothing in it re-states a value rule; it composes the functions
the insert path already calls, in the insert path's order.
- **`@objectstack/cli`: `judgeSeedRecordValues(stack)`
(`utils/seed-record-values.ts`).** Owns only the WALK. The field map is
the runtime registry's: a driverless `ObjectQL` (the shape
`authoring-filter-judge.ts` measured) holding the stack's objects,
picklists and picklist extensions, folded across packages
(`authoringRuleUnionStack`). Datasets are read where they sit
(`datasetsOf`, now exported from `bundle-seed-files.ts`): top level,
`manifest.data`, each `packages[i].manifest.data`. Each issue is `{
location, object, field, code, message }`, with the validator's code and
sentence.
- **What the walk does not hand to the judge**, each a value the load
rewrites before the write or one a deployment decides: a value holding
an expression envelope (the load evaluates it) or a `seedFile(path)`
(the load stores the file and writes its id; `collectSeedFiles` judges
the path); a reference, file or structured-JSON field (the load resolves
a reference's natural key to an id, and the ADR-0104 shape check refuses
only on a deployment whose migration evidence says so); a field bound to
a picklist the stack does not declare (`judgePicklistReferences` reports
it); a dataset whose object the stack does not declare or the registry
refuses.
- **Wiring.** `os validate` step 3d-bis, right after the seed-file gate;
`os build` step 4a-0, right before it. Both exit 1 with the issues;
`--json` carries them as `errors` (validate) and `issues` (build), the
shapes each command's seed-file exit already uses.
`test/validate-build-gate-parity.test.ts` holds the call in
`SHARED_NON_REGISTRY_GATES`.
- **Two exports made visible inside `packages/cli`:** `objectsOf` /
`registrationCopy` (`authoring-filter-judge.ts`) and `datasetsOf` /
`DatasetLike` (`bundle-seed-files.ts`), so the registry build and the
dataset walk are each written once. No published CLI surface changes.
## Shipped-seed reading (the narrowing)
Every seed this repository ships, run through the new check on head
`98d28ec0` (`os validate` in each app, plus a read-only probe counting
the values the walk hands the judge):
| App | Datasets | Records | Values | Judged | Left to load: expression
or seed file | Left: reference, file, structured | Object not in stack |
Newly refused |
|:---|---:|---:|---:|---:|---:|---:|---:|---:|
| `examples/app-todo` | 1 | 8 | 39 | 32 | 7 | 0 | 0 | **0** |
| `examples/app-crm` | 5 | 28 | 185 | 137 | 17 | 31 | 0 | **0** |
| `examples/app-showcase` | 19 | 132 | 969 | 704 | 101 | 135 | 29
(`sys_business_unit`) | **0** |
| `examples/app-multi-package` | 0 | 0 | 0 | 0 | 0 | 0 | 0 | **0** |
All four apps answer `✓ Validation passed`, exit 0, as before. The
showcase's `field-zoo` dataset covers every field type and passes. No
platform object, plugin or service in `packages/**` ships seed datasets
(`git grep` for `defineSeed(` and `records: [` outside tests: none).
Dogfood boots these examples and is green on `603ecf33`. HotCRM fixtures
are not in this repository and were not read.
## Tests
- `packages/objectql/src/validation/record-validator.test.ts`, a new
`[#27259]` block, 7 cases: a currency object is refused with exactly the
fields `validateRecord(…, 'insert')` gives (`invalid_number`, the
lesson, the label); a bare number passes; a numeric string, a blank and
a scalar on a multi-value field pass; an omitted or null required value
is not judged (CONTROL: `validateRecord` on insert refuses the
omission); an undeclared name gets no answer (CONTROL); a readonly
value's shape is judged, as on the insert path; the row is never
mutated.
- `packages/cli/src/utils/seed-record-values.test.ts`, 11 cases: the
currency object refused at `data[0].records[1].budget` with the lesson;
a bare number passes; rewritten values pass; an unreadable number, an
unknown option, an unreadable date, a value below `min` and a bad
multi-value element are refused with their codes; a picklist-bound field
is judged against the stack's picklist; an expression, a seed file, a
reference, a file and a field on a picklist declared elsewhere are left
to the load; `required` is not judged; CONTROL: an undeclared field and
a dataset for an object outside the stack get no answer; a package
body's dataset is located at
`packages[1].manifest.data[0].records[0].budget`; the stack is not
mutated.
- `packages/cli/test/seed-record-values-load-parity.test.ts`
(integration tier by its `new ObjectQL(` signal), 1 case: seven rows
through the check and through a real `SeedLoaderService` load on an
`ObjectQL` engine over SQLite `:memory:`. The rows the check refuses are
exactly the rows the load refuses (0, 3, 4, 5, 6), each load message
ends with the check's sentence, and the two rows it passes are written.
- `packages/cli/test/validate-build-gate-parity.test.ts`:
`judgeSeedRecordValues` joins `SHARED_NON_REGISTRY_GATES`.
`packages/cli/test/normalized-call-sites.test.ts`: a `resolved` row for
the walk's folded read.
## Acceptance notes
- The CONTROL with an unknown field: on a `defineSeed` dataset,
`defineSeed` itself already refuses an undeclared field when the config
loads (`✗ defineSeed('todo_task'): unknown field(s) in records —
bogus_field.`, exit 1). That answer is byte-identical before and after
(diffed, timing lines aside). On a plain dataset the new check gives no
answer for the name.
- A seed dataset for a platform object (`sys_business_unit` in the
showcase) is not judged: the stack does not declare the object, so `os
validate` has no field map for it. The runtime judges it at load as
before.
- A readonly field's value is judged for shape on the insert path; the
check judges it the same way. The `required` check on a missing value,
validation rules, reference resolution and permissions stay the load's.
A supplied empty array on a required multi-value field is not missing:
the check refuses it `required`, as the load's insert path does.
- Known limit, hooks: a `beforeInsert` hook runs on a seed write before
the record validator, and the authoring check runs no hooks. A stack
whose hook rewrites a seed value into an accepted shape loads clean but
is refused by `os validate`. The walk's header names this.
- Known limit, coverage: a dataset whose object is declared only under
`manifest.objects`, outside `packages[]`, has no field map in the folded
stack and is not judged (no false refusal). The walk's header names
this.
## Verification
<details><summary>Package suites</summary>
- `pnpm --filter @objectstack/objectql test` on `98d28ec0` (after it,
objectql source moved only in a docblock, `a44c5261`) exit 0: Test Files
485 passed (485), Tests 9124 passed (9124). `pnpm --filter
@objectstack/objectql typecheck` exit 0.
- `pnpm --filter @objectstack/cli exec vitest run --project unit` on
`603ecf33`: VERDICT command-exit 0, Test Files 350 passed (350), Tests
5066 passed (5066). (On `98d28ec0` it caught
`normalized-call-sites.test.ts`: the walk's folded read had no row;
added in `603ecf33`.) `pnpm --filter @objectstack/cli typecheck` exit 0.
The parity file (integration tier): 1 passed on `603ecf33`.
- The integration tier beyond the new parity file is declared to CI.
</details>
<details><summary>Ablation (each through scripts/ablation-replace.mjs,
restored blob == HEAD, git diff HEAD empty)</summary>
- `packages/cli/src/utils/seed-record-values.ts`: the
`validateInsertValues(...)` call replaced by `void row;` (anchor 1 → 0,
blob 1e84757 → 1748ccd5): `seed-record-values.test.ts` 4 failed, 7
passed. Restored.
- `packages/objectql/src/validation/record-validator.ts`: the
`normalizeMultiValueFields(objectSchema, row, 'include');` line deleted
(anchor 1 → 0, blob dc438b6 → 16c92c87): the `[#27259]` block 2 failed,
5 passed (the rewrite case and the no-mutation case). Restored.
</details>
<details><summary>Dogfood (Clause-② narrowing arm), head
603ecf3</summary>
`pnpm --filter '@objectstack/dogfood^...' build && pnpm --filter
@objectstack/dogfood test` under the lock: VERDICT command-exit 0, Test
Files 350 passed | 3 skipped (353), Tests 2817 passed | 20 skipped
(2837).
</details>
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 65 derived families, the same list on
`603ecf33` and on `8e7f125e`. All 65 run serially as `CMD :: exit N` on
`603ecf33`; the 9 changeset and closing-keyword families re-run on
`8e7f125e` after the changeset's kind changed (all exit 0). `--ran`: 65
accounted, 64 run, 1 NOT-MEASURED, 0 UNRUN.
- 64 exit 0.
- `pnpm check:dual-build-cjs-loads` exit 3, PREREQUISITE NOT MET (needs
a full build: no `dist` for `embedder-openai`, `knowledge-ragflow`,
`service-cluster-redis`); CI runs it.
- `GITHUB_EVENT_NAME=pull_request node
scripts/check-changeset-no-major.mjs --base origin/main --event` with
this body: exit 0 (`Clause-②: no (narrowing)`, `Metadata-protocol:
narrowed`, minor).
- A first run on `98d28ec0` caught `check:driver-memory-census` exit 1:
the parity test bound the frozen `@objectstack/driver-memory`. It now
loads through `@objectstack/driver-sql` on SQLite `:memory:`, and the
gate is green on `603ecf33`.
- PR check-runs: in progress at the time of writing.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufziV1z63WH7moz8f5TS)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 54c2496 commit 99aeee6
13 files changed
Lines changed: 601 additions & 6 deletions
File tree
- .changeset
- packages
- cli
- src
- commands
- utils
- test
- objectql/src
- validation
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
| |||
1084 | 1085 | | |
1085 | 1086 | | |
1086 | 1087 | | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| 1095 | + | |
| 1096 | + | |
| 1097 | + | |
| 1098 | + | |
| 1099 | + | |
| 1100 | + | |
| 1101 | + | |
| 1102 | + | |
| 1103 | + | |
1087 | 1104 | | |
1088 | 1105 | | |
1089 | 1106 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
| 29 | + | |
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
| |||
804 | 805 | | |
805 | 806 | | |
806 | 807 | | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
807 | 833 | | |
808 | 834 | | |
809 | 835 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
| 60 | + | |
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
69 | | - | |
| 69 | + | |
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
83 | | - | |
| 83 | + | |
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| |||
91 | 91 | | |
92 | 92 | | |
93 | 93 | | |
94 | | - | |
| 94 | + | |
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
| |||
| 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 | + | |
0 commit comments