Repository navigation
Commit eab7236
Part of #15953
Clause-②: yes
Slice 1 of the card (hotcrm epic step 5c): the classification table,
plus ONE derived proof family end to end. #15953 remains open for the
remaining D and R rows.
## The classification table
Posted on the card before any implementation: comment 6115219467
(#15953 (comment)).
It has 14 rows: the 13 hotcrm files the audit's platform-semantics
sentence names
(`docs/audits/2026-09-hotcrm-handwritten-test-split.md:187-192`, files
read off its Appendix A), plus the ADR-0104 reference-shape check from
hotcrm's `test/helpers/hook-harness.ts`. Each row is classed D / R / K,
with a destination, a reason, and the residue of any file whose
describes split. hotcrm deleted that helper and its differential test in
hotcrm `9451b6de` (2026-10-08), so that row was read at `9451b6de^`.
## The family this slice implements: reference-value shape (ADR-0104 D1)
Row 14, which the claim preferred. The property is "every
client-writable reference field refuses a value its declared stored
shape refuses". It holds for any app, so it is derived, and the table
classes it D.
- **Derivation** (`deriveReferenceShapeCases`). It takes every field of
the app's objects whose type is in `REFERENCE_VALUE_TYPES` (`lookup`,
`master_detail`, `user`, `tree`), reading the objects through the same
collection reader as the CRUD family, so a multi-package artifact is
covered. Fields declared `readonly` or `system` are returned `excluded`
with the reason. The promise is read from the spec contract
`valueSchemaFor(field, 'stored')`. Three probes: a boolean, a number,
and an embedded record object (the expanded read shape written back). A
multi-valued field gets each probe as an array element. A probe that the
stored schema accepts is dropped and named, so it is never reported as a
hole.
- **The engine is asked; nothing is re-derived.** Each probe is written
on insert and on update through two doors: the REST data routes as the
signed-in caller, and the engine's own write as the system principal
(the handle's `hooks.run` with `{ system: true }`).
- **Attribution is the control.** A refusal counts only when the engine
names the probed field in `fields[]`. A refusal that names anything else
is `unattributed`, so an engine that refuses every write cannot read as
proven.
- **Field statuses:** `proven`, `holes` (a probe was admitted), `failed`
(a door faulted), `inconclusive` (the reason is reported), `excluded`.
`hardFailures` = holes + failed.
- **New exports** on `@objectstack/verify`: `runReferenceShapeProofs`,
`deriveReferenceShapeCases`, `referenceShapeProbeValue`,
`formatReferenceShapeReport`, `REFERENCE_SHAPE_PROBE_ID`, and nine
types. Changeset: `@objectstack/verify` minor.
## `os verify --reference-shapes`
What I added to `packages/cli/src/commands/verify.ts`:
- a boolean flag `--reference-shapes` (default false), opt-in like
`--rls`;
- the family runs on a fresh boot of its own (a door that admits a probe
lands a row, and the base records would collide with another phase on a
unique field);
- `hardFailures` adds `referenceShapes.summary.hardFailures`, so the
exit status reads it;
- the `--json` document carries `referenceShapes` when the flag is
given;
- the text face prints `formatReferenceShapeReport`.
Without the flag, the run and its document are unchanged. Changeset:
`@objectstack/cli` minor (a new flag and a new optional document key).
**Why opt-in rather than default:** the family finds a live engine
defect on `multiple: true` reference fields (see the finding below).
`examples/app-showcase` declares one (`showcase_project.team_members`,
`Field.user` with `multiple: true`). `Dogfood Verify CLI` runs `os
verify --rls` on the showcase in CI, so a default-on family would turn
that job red the moment this lands. The engine gap is #22908, whose
direction is to make the family default in the PR that lands that
repair. The changeset, the verify README and the `os verify` section of
`content/docs/deployment/cli.mdx` say why it is opt-in.
## Negative control and ablation
- **Negative control (permanent tests).** In
`packages/verify/src/reference-shape.test.ts`, the same metadata under
`OS_ALLOW_LAX_VALUE_SHAPES=1` (ADR-0104's declared escape hatch) is
reported with holes on all three probed fields, with `hardFailures` 3.
It runs on a stack of its own, because a lax admission revokes the
stack's ADR-0104 attestation. Through the CLI, in
`packages/cli/test/verify-reference-shapes.test.ts`, the same posture
makes the run exit 1 with `hardFailures` 1. An attribution control (a
stack double that refuses every write while naming another field) reads
`inconclusive`, never `proven`.
- **Ablation (one-off, at `777263207d`).** In
`packages/objectql/src/validation/record-validator.ts`, the
reference/structured-JSON refusal condition
(`valueShapeStrictEffective(valueStrict)`) was ANDed with an
always-false marker term, via `scripts/ablation-replace.mjs` (anchor 1
to 0, blob `c30d7977f3bb` to `ae3ed2be190c`). Then `pnpm --filter
@objectstack/objectql build`, and `scripts/ablation-dist-preflight.mjs`
found the marker present in 4 dist files. Result:
`reference-shape.test.ts` went RED (1 failed, 4 passed): the positive
test reported `rs_contact.account` ADMITTED on 8 of 12 writes.
- **Restore.** `git checkout HEAD -- ABS_PATH`, proven by the blob
matching HEAD and an empty `git diff HEAD`. Then a rebuild, and the
preflight with `--absent` reported the marker absent from all 14 dist
files and a clean tree. Result: GREEN (5 passed). The script carried an
EXIT/INT/TERM trap. No permanent ablation file was left behind.
## First consumer's evidence: hotcrm
Read-only clone of objectstack-ai/hotcrm `main` `1d7148b`, outside the
worktree. Nothing was written to hotcrm.
- **`os verify` itself is not achievable against hotcrm on this tree.**
hotcrm pins `@objectstack/*` 17.7.0, and this tree is protocol 18. `os
verify --reference-shapes --json` on hotcrm's config stops at the
author-time stage, which refuses 118 `expression-invalid` findings (the
protocol-18 retirement of the brace-template dialect in flow value
slots), so the runtime stage never runs. `bootStack` on this tree
refuses the package with `ProtocolIncompatibleError` (it targets
`^17.7.0`).
- **What I ran instead** (a scratch script, not shipped): the family
from this branch's build, over hotcrm's shipped artifact
(`objectstack.config.ts`, both packages), in two readings.
- **Reading 1, hotcrm's pinned engine** (its installed
`@objectstack/verify` 17.7.0 `bootStack`, REST door only, because 17.7.0
has no system overload on `hooks.run`): 56 reference fields (51 lookup,
5 master_detail, none multi-valued). 18 proven, 25 inconclusive, 13
excluded (`owner_id`, `system`), 0 holes. Probes: 258, of which 147
refused, 0 admitted, 21 unattributed, 90 not run.
- **Reading 2, this tree's engine, both doors**, over the snapshot `os
migrate meta --from 17 --out` writes from the same config, with only the
declared protocol range moved to `^18`: 23 proven, 20 inconclusive, 13
excluded, 0 holes. Probes: 516, of which 366 refused, 0 admitted, 0
unattributed, 150 not run.
- **Differential against the retired pin.** The pin's verdict function
(`referenceValueShapeError` at hotcrm `9451b6de^`) was applied to the
same fields and probe values. Every probe the pin says is refused, and
that a door could run, was refused with the field named: 93 probe kinds
in reading 1 and 114 in reading 2, with 0 contradictions in both. The 13
fields the pin skipped (`system`) are the 13 the family excludes. The
remaining probes are inconclusive or not run because the CRUD
derivation's synthesized base record does not satisfy some of hotcrm's
own rules (date-ordering validations, a required relation whose target
could not be created, a hook refusing a task). That is reported per
field, never counted as proven.
## Finding (not fixed here: no engine change in this slice)
On `9fdb12752c`, a `multiple: true` `lookup` or `user` field admits
wrong-shape elements. The spec contract says `valueSchemaFor({ type:
'lookup', multiple: true }, 'stored')` is an array of record-id strings,
and the family reports holes:
- **REST** (signed-in admin): `POST` / `PATCH` with `[{ id }]` answer
201 / 200 and store the embedded object. `[false]` and `[42]` are
refused, but as `reference_not_found` (the existence probe), not as a
shape error.
- **System door:** `[false]`, `[42]` and `[{ id }]` are all admitted and
stored verbatim.
The cause, read in the source: the record validator's multi-value branch
returns early for `lookup` / `user` / `file` / `image` elements
("reference integrity is handled elsewhere"). The engine's
reference-existence check skips object elements and skips system writes
entirely. Recorded in the report for the PM to file.
## Patch round (head `f8a821d054`)
CI on `777263207d` was red in two places, both caused by this PR's new
exports. Neither was in the locally derived gate list.
- **`Test Core (3/6)`**, `@objectstack/core#test:repo`,
`second-object-read-exposure.pin.test.ts`:
`packages/verify/src/reference-shape.ts` calls the reference-target
arbiter and was unclassified. Measured: the family uses the answer only
to pick an id for its embedded-record probe, and the id comes from a
record the family itself created through the REST door. It writes probe
values to the SOURCE object (through the REST data routes, which judge
that object's exposure, and through the engine's system write) and reads
no record of the target. So the pin's `CALLERS` table gains one
`not-a-served-read` row; the pin's logic and existing rows are
unchanged. `pnpm --filter @objectstack/core exec vitest run --project
repo --maxWorkers=2
src/security/second-object-read-exposure.pin.test.ts`: RED at
`777263207d` (1 failed, 3 passed), GREEN at `0598a8d13d` (4 passed). The
whole core `repo` project at the new head: 6 files, 83 tests passed.
None of its 6 files names `packages/verify`.
- **`Lint & Repo Gates`**, step "@objectstack/verify stand-in erasure
guard" (`pnpm check:verify-stand-in`): `runReferenceShapeProofs` and
`formatReferenceShapeReport` were unclassified.
`scripts/check-verify-stand-in-erasure.mjs` gains two `NOT_A_STAND_IN`
rows, the gate's own prescribed remedy, on the footing of
`runCrudVerification` and `formatReport`: one takes the `VerifyStack`
that `bootStack` returns, the other formats a report this package
produced. The gate is now OK: "2 stand-in check(s) guarded, 9
candidate(s) explicitly exempt, 10 call site(s) reached, 0 asserted
driver arguments". This file was outside the claimed surface; it is
declared in the report. The CI steps after step 14 of that job were
skipped on `777263207d`, so they first run on this head.
- **Docs:** the verify README names the family (intro bullet, CLI line,
programmatic snippet, the status table, API), and the `os verify`
section of `content/docs/deployment/cli.mdx` names the flag, the
`referenceShapes` key, and why the flag is opt-in.
## Tests and gates (code at `777263207d`; gates and lint at
`f8a821d054`)
- `pnpm --filter @objectstack/verify test`: 32 files, 250 tests passed.
- `pnpm --filter @objectstack/verify typecheck`: exit 0; test layer 0
files, 0 errors.
- `pnpm --filter @objectstack/cli typecheck`: exit 0. The new test file
is in the `tsconfig.test.json` program (`--listFilesOnly`, 1 hit).
- `pnpm --filter @objectstack/cli exec vitest run --project unit
--maxWorkers=2`: 283 files, 4269 tests passed.
- `--project integration` on the new spawn test: 3 passed. The new file
is integration tier, so it was run locally; the rest of the integration
tier is declared to CI.
- Gates at `f8a821d054`: `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` (no paths) derived 117 commands; the
docs page and the scripts file added 52 families to the 65 derived at
`777263207d`. 114 exit 0. Three exit 3 and are NOT MEASURED, not red,
because each reads built output of packages outside this closure: `pnpm
check:dual-build-cjs-loads`, `pnpm check:i18n-coverage` and `pnpm
--filter @objectstack/spec run check:skill-examples` (the client SDK has
no dist). `pnpm check:verify-stand-in` and `pnpm
check:pm-dispatch-gates` exit 0. `--ran` reconciliation: 117 derived,
114 run, 3 NOT-MEASURED, 0 UNRUN.
- Lint at `f8a821d054`, a proven narrowing: `eslint --no-inline-config
--format json` over the 7 touched source files reports 7 files, 0
errors, 0 warnings. All 7 are in the config's population
(`--print-config` resolves for each). `eslint.config.mjs` enables no
type-aware linting (no `parserOptions.project`, no typed rules), so this
diff cannot move the verdict of any untouched file.
## Acceptance notes
- Remaining rows for the next slices, all recorded on the card. D:
deleteBehavior (rows 1-3), unique scope (6-7), parent-derived write
reach (12), the static detail-section rule (13), and the D residues of
rows 9 and 11. R: rows 8 and 10, the R residues of rows 3, 4, 5, 7 and
12, and row 9, which is already pinned upstream by the spec
temporal-conformance matrix and the driver suites.
- `checkReadCoercion` and `checkDateBucketParity` were classified
against row 9: neither covers a datetime FILTER window.
- `--reference-shapes` stays opt-in in this PR. The engine gap it would
fail on is #22908, which records making the family default in the PR
that lands that repair.
- Gate-derivation gap, reported in the PR's report: deriving gates for
`packages/verify/src` paths names neither `pnpm check:verify-stand-in`
nor `@objectstack/core`'s `repo` project. Both turned red here.
- Not merged with `origin/main`: the later commits touch nothing this PR
derives from, according to the derivation header at `f8a821d054`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7dbb89d commit eab7236
10 files changed
Lines changed: 974 additions & 12 deletions
File tree
- .changeset
- content/docs/deployment
- packages
- cli
- src/commands
- test
- core/src/security
- verify
- src
- scripts
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1922 | 1922 | | |
1923 | 1923 | | |
1924 | 1924 | | |
| 1925 | + | |
1925 | 1926 | | |
1926 | 1927 | | |
1927 | 1928 | | |
| |||
1943 | 1944 | | |
1944 | 1945 | | |
1945 | 1946 | | |
1946 | | - | |
| 1947 | + | |
| 1948 | + | |
| 1949 | + | |
| 1950 | + | |
| 1951 | + | |
| 1952 | + | |
| 1953 | + | |
| 1954 | + | |
1947 | 1955 | | |
1948 | 1956 | | |
1949 | 1957 | | |
| |||
1970 | 1978 | | |
1971 | 1979 | | |
1972 | 1980 | | |
1973 | | - | |
1974 | | - | |
| 1981 | + | |
| 1982 | + | |
1975 | 1983 | | |
1976 | 1984 | | |
1977 | 1985 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
| 19 | + | |
18 | 20 | | |
19 | 21 | | |
| 22 | + | |
20 | 23 | | |
21 | 24 | | |
22 | 25 | | |
| |||
79 | 82 | | |
80 | 83 | | |
81 | 84 | | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
82 | 88 | | |
83 | 89 | | |
84 | 90 | | |
85 | 91 | | |
86 | 92 | | |
87 | | - | |
| 93 | + | |
88 | 94 | | |
89 | 95 | | |
90 | 96 | | |
91 | 97 | | |
92 | 98 | | |
| 99 | + | |
93 | 100 | | |
94 | 101 | | |
95 | 102 | | |
| |||
101 | 108 | | |
102 | 109 | | |
103 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
104 | 115 | | |
105 | 116 | | |
106 | 117 | | |
| |||
178 | 189 | | |
179 | 190 | | |
180 | 191 | | |
| 192 | + | |
181 | 193 | | |
182 | 194 | | |
183 | 195 | | |
| |||
278 | 290 | | |
279 | 291 | | |
280 | 292 | | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
281 | 308 | | |
282 | 309 | | |
283 | 310 | | |
| |||
293 | 320 | | |
294 | 321 | | |
295 | 322 | | |
296 | | - | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
297 | 329 | | |
298 | 330 | | |
299 | 331 | | |
300 | 332 | | |
301 | 333 | | |
302 | | - | |
| 334 | + | |
303 | 335 | | |
304 | 336 | | |
305 | 337 | | |
| 338 | + | |
306 | 339 | | |
307 | 340 | | |
308 | 341 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
221 | 221 | | |
222 | 222 | | |
223 | 223 | | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
224 | 232 | | |
225 | 233 | | |
226 | 234 | | |
| |||
0 commit comments