Repository navigation
Commit 21914c3
Fixes #22908
Clause-②: yes (narrowing)
## What changed
`packages/objectql/src/validation/record-validator.ts`, the multi-value
branch of `validateOne`. It returned `null` early for `lookup` / `user`
/ `file` / `image` ("reference integrity is handled elsewhere"), so an
element of a `multiple: true` field was never parsed against its stored
form. Those types now fall through to the ADR-0104 value-shape arm that
a single value goes through. That arm parses the whole array with
`valueSchemaFor(def, 'stored')`, which is already `z.array(element)` for
a multi-valued field (`ReferenceIdValueSchema` for `lookup` / `user`,
`FileReferenceIdValueSchema` for `file` / `image`). The result is the
same refusal (`invalid_type`, message key `invalid_value_shape`), under
the same posture, with the same warn-first sink. No second shape rule
was added, as triage 6115825886 binds.
- One predicate, `isValueShapeJudgedType`, now answers "does the
ADR-0104 arm judge this type" in all three places that ask it: the arm,
the multi-value branch that hands an element to it, and
`valueShapeViolation` (the scan's predicate). Before this, the scan and
the branch could disagree about which types those are.
- `valueShapeDetail` names the index of the wrong element on a
multi-valued field (`at index 1: ...`), so a refusal names the field and
the index. The write path and `os migrate value-shapes` share this one
reading. `repeater` / `vector` details are unchanged.
- System principal: the shape check is the validator's, which already
runs for an `isSystem` write, so the system door now refuses too.
`assertReferencesResolve` is untouched (read-only): its `isSystem`
latitude on whether an id EXISTS stays as #4551 left it, and a test pins
that.
- `file` / `image` with `multiple: true`: measured and given their own
stored form, the opaque `sys_file` id, under the media posture
(`mediaValueShapeStrict` / the file-references flag). A verified media
flag does not open reference elements, and the value-shapes flag does
not open media elements (pinned).
One changeset, `.changeset/22908-multi-reference-element-shape.md`:
`@objectstack/objectql` `minor`, BREAKING (accept-set narrowing),
ADR-0087 `not-required (no-migration-prescription)`, FROM / TO stated.
## Premise, checked on `origin/main` `018b077fbf` before any edit
- At the validator: with `valueShapeStrict: true`, `{ team: [{ id: 'x'
}], watchers: [false] }` on a multi-valued `lookup` / `user` was
ADMITTED, while the single-valued control `{ owner: { id: 'x' } }` was
refused `invalid_type`.
- At claim, read-only (triage's item). Does ADR-0104's value-shapes scan
report a stored multi-valued element of the wrong shape? Yes.
`scanValueShapes` over rows `team: [{ id }]`, `team: [false]`,
`watchers: [42]` reported `team` count 2 and `watchers` count 1, plus a
single-valued control row, with `blocking` 4. So no gap card is owed.
But at BASE the scan counted values the strict write path admitted,
which contradicts the scan's own header ("count exactly what strict mode
would reject"). This change closes that disagreement, and a test pins
the two in agreement. The media half has its own evidence:
`backfill-file-references.ts` converts arrays element by element.
## Pins (tests beside the code)
-
`packages/objectql/src/validation/multi-reference-element-shape.test.ts`
(validator level): for `lookup` / `user` with `multiple: true`, the
three bad elements (boolean, number, embedded record) are refused on
insert and update, naming the field and index 0. The wrong element at
index 2 is named as index 2. CONTROLS: the single-valued rows refuse the
same three with no index; a well-formed id array and `[]` are accepted;
a non-array is still the array refusal. A readonly multi-valued
reference is judged by its shape, as a readonly single value is.
Warn-first admits the element and reports it to the sink (`value-shape`
gate for references, `media` for files). The gate split holds, and the
`OS_ALLOW_LAX_VALUE_SHAPES` hatch still works. `file` / `image` elements
are held to the `sys_file` id form. The scan predicate and the strict
write path agree on every probe.
- `packages/objectql/src/engine-multi-reference-element-shape.test.ts`
(engine write path, strict posture): the three bad elements on `lookup`
and `user` with `multiple: true`, through a caller's context (the engine
call the REST data routes make) and the system principal, on insert and
update. Each is refused as a `ValidationError` with `code`
`VALIDATION_FAILED` and `fields[0]` `{ field, code: 'invalid_type' }`,
and nothing is stored or changed. CONTROLS: the single-valued rows are
unchanged on both doors; well-formed id arrays are accepted on both
doors; and a well-formed id that names no row is `reference_not_found`
for the caller but still written by the system principal (the `isSystem`
existence latitude is kept).
## The real door: PR #22907's reference-shape family (one-off, not
committed)
`runReferenceShapeProofs` from `@objectstack/verify` (a real
`bootStack`, the REST data routes as the signed-in caller, and
`hooks.run` with `{ system: true }`) over a fixture with `accounts`
(lookup, multiple), `watchers` (user, multiple), and single-valued
`account` / `assignee` controls. It ran as a scratch test file that was
deleted afterwards. It resolves `@objectstack/objectql` through its
`exports`, so through the built `dist/`.
- Fixed build: 4 fields, 4 proven, 0 holes; 48 probes, 48 refused
`invalid_type` (REST answers `400`), 0 admitted, 0 unattributed.
- Ablated build (below): the two multi-valued fields read `holes`, 8 of
12 admitted each. That reproduces the card's measurement: REST embedded
record admitted `201` / `200`; REST boolean / number refused only as
`reference_not_found` `400`; system door all admitted. The single-valued
controls stay proven. Summary: 2 proven, 2 holes, 32 refused, 16
admitted.
## Ablation (one-off; `scripts/ablation-replace.mjs` in WRAP mode, plus
an EXIT/INT/TERM trap)
- Mutation: the early return restored as `if (isValueShapeJudgedType(t)
&& String('ABLATION-22908-MARKER')) return null;` ahead of the option
arm. Anchor 1 to 0, blob `671387d31cbd` to `7cc631fd7095`. On disk:
marker 1, the early-return line 1.
- objectql pins (src-resolved): 43 failed, 10 passed (53). The 10 that
pass are the controls.
- `pnpm --filter @objectstack/objectql build`, then
`scripts/ablation-dist-preflight.mjs`: the marker is present in 4 dist
files. The real-door reading is above.
- Restore: `git checkout HEAD -- ABS_PATH` by the tool. Blob after
restore equals HEAD (`671387d31cbd`), `git diff HEAD` is empty, `git
status` is clean. Rebuilt; `--absent` preflight: the marker is absent
from all 14 dist files. objectql pins 53/53 pass; the real-door reading
is 48/48 refused.
- First attempt was a no-op: the tool refused before the child ran,
because my replacement still contained the anchor (count 1 to 1). It
restored to the HEAD blob and took no reading. The second attempt is the
one reported.
## Local verification
All read at the final head, `55cf89916e`, after the last commit.
- `pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2`:
405 files passed, 7911 tests passed (through `os-verify-lock`, `VERDICT
command-exit 0`).
- `pnpm --filter @objectstack/objectql typecheck` (`tsc --noEmit` and
`check:test-typecheck`): exit 0, test layer held at 40 files / 234
errors / 65 pinned signatures, with no new signature. Both new test
files are in the `tsconfig.test.json` program (`--listFiles`);
`tsconfig.json` excludes tests.
- Gates: the 69 commands derived at dispatch for the declared surface
all exit 0. Re-derived on the real diff (`dispatch-gates --commands`,
change set from the merge base): 66 families, all inside the 69.
`dispatch-gates --ran` with recorded exit codes: 66 derived, 66 run, 0
NOT-MEASURED, 0 UNRUN (a derived zero). The 3 extra are engine-surface
families: `check:error-status-conformance`,
`check:stack-collection-maps`, `check:swallow-census-controls`. One gate
went red during development, `check:objectql-double-limit`, which was
right: the new engine test's stub `find` ignored the caller's `limit`.
It now applies the bound after the filter.
- The dist-reading gates (`check:dts-closure`,
`check:dual-build-cjs-loads`, `check:lean-entry-closure`,
`check:published-files`, `check:sourcemap-no-sources-content`,
`check-dts-emitted --self-test`) ran after `pnpm --filter
@objectstack/objectql build`.
- Lint, narrowed and proven: eslint's own config
(`calculateConfigForFile`) applies to all 3 changed `.ts` files, and
none is ignored. `eslint --no-inline-config --format json` over them
gave 3 files, 0 errors, 0 warnings. The config enables no type-aware
linting (no `parserOptions.project`, no typed rules), so this diff
cannot move the verdict on any untouched file. The repo-wide `pnpm lint`
is CI's.
- Consumers: no export, spec key or response shape changes, so no
import-side suites were run. A grep of downstream writers of
multi-valued arrays (`plugin-approvals` attachments, `service-storage`
gallery, `rest` and `runtime` fixtures, the showcase seed) found id
elements only. CI runs the full set.
## Acceptance notes (observations; not filed)
- The spec's authoring-time default-value gate
(`actionableValueIssueMessage`,
`packages/spec/src/data/default-value-shape.ts`) is a deliberately
separate reading. It does not name the index for a multi-valued literal
default. That is polish, not a defect class; carrier: none.
- `assertReferencesResolve`'s note "already-expanded objects are
skipped" now describes a path reachable only under warn-first. On a
strict deployment the validator refuses an expanded element first, for
both cardinalities, as it already did for a single value.
- Not measured: what the console (`objectui`) writes into a multi-valued
lookup / user / file / image on save. No `objectui` checkout was
available here. If a console editor wrote expanded records back as
elements, a strict deployment would now refuse that save. The
single-valued field has refused that shape under the same posture all
along.
- ⛔ Not folded here, per triage: making `os verify --reference-shapes`
the default. The one-off reading above is the input for that separate
call. `examples/app-showcase` itself was not run through the CLI.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent e5687cb commit 21914c3
4 files changed
Lines changed: 490 additions & 31 deletions
File tree
- .changeset
- packages/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 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 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 | + | |
0 commit comments