Repository navigation
Commit f2a45db
Part of #22032
Clause-②: no (narrowing)
This is pass 2 of #22032: the field-rule slots. Passes 3 and 4 stay
fenced, and the card stays open for them: option `visibleWhen`, and the
object's action predicates.
## What changes
**The object save door gives the build's verdict on a field's rule
slots.** `formulas.mdx` says "the same `validateExpression` validator
backs `os build` and metadata registration". After pass 1 the object
door judged formula fields and validation-rule predicates, and fenced
the field-rule slots off by name. So an object whose field carried a
bare `requiredWhen: 'amount > 1'` (the card's measured body) still saved
with a 200, while `os build` refused it at error.
- **The change is in the fence, not the registry.** In
`runStackExpressionPasses` (`packages/lint/src/validate-expressions.ts`)
the field walk no longer starts with `if (objectWrite) {
judgeFieldFormula(fname, f); continue; }`. On an object write the walk
now runs, at the build's own position:
- the four slots (`requiredWhen`, `readonlyWhen`, `conditionalRequired`,
`visibleWhen`) as `record`-scoped predicates, with the root verdict;
- the `parent` gate (a `readonlyWhen` / `requiredWhen` reading `parent`
on an object without exactly one `master_detail`);
- the null-guard check over `requiredWhen`;
- the refusal of a `requiredWhen` / `readonlyWhen` read through a
reference field;
- the formula pass, unchanged.
- **Pass 3 stays fenced by its own guard.** H1 held: the removed
`continue` also skipped the per-option `visibleWhen` loop, which sits
inside the same iteration. That loop now reads `(objectWrite ? [] :
recordsOf(f.options))`, the same guard shape as the flow, action,
sharing-rule and hook loops. Deleting the `continue` alone would have
lifted passes 2 and 3 together.
- **No registry change (H3 re-verified).** The
`validateStackExpressions` entry declares `runtimeTypes: ['flow',
'action', 'hook', 'object']` (`authoring-rules.ts`), and
`runtimeAuthoringRulesFor('object')` (`runtime-gate.ts`) dispatches it.
`runtime-gate.ts` is untouched.
- **Docblocks made true (H2).**
`StackExpressionOptions.runtimeWriteType` now names three admitted
passes and two fenced ones. `AuthoringRuleContext.runtimeWriteType` in
`authoring-rules.ts`, the one line that reaches a built `.d.ts`, names
the field-rule-slot pass. The function-head comment, the
`judgeFieldFormula` docblock, the registry entry's measurement comment
and the object roster comment in `runtime-gate.object-writes.test.ts`
move with it.
- **The door's verdict is the build's finding (H4).** The door's 422
issue and `runAuthoringRules('build', …)` give the same rule
(`expression-invalid`), location (`object 'fx_field' · field 'name'
requiredWhen`), path, message and hint. The pins compare these key by
key.
- **No code change in `packages/metadata-protocol`.** Only its test file
gains the door-level pins.
## Pins
- **Lint door:**
`packages/lint/src/runtime-gate.object-field-rule-writes.test.ts` (new,
12 tests).
- LIT, one refused body per slot and per gate: a bare `requiredWhen`, an
unregistered function in `readonlyWhen` and in `visibleWhen`, a bare
`conditionalRequired`, the root verdict (`current_user`), the `parent`
gate, the `requiredWhen` null guard, and the traversal refusal
(`record.account.name`). Each is located at the slot and asserted on its
named subject.
- CONTROL: valid predicates on every slot, and a `parent`-scoped detail
with its master stored beside it, are clean at the door and at the
build.
- PARITY: for each refused body, the door's findings equal the build's.
- The differential: a stored sibling's broken field rules are not this
write's to answer for.
- **The fence (enumeration pin):** in
`packages/lint/src/runtime-gate.object-formula-writes.test.ts`. The
fenced sites are now passes 3 and 4 only (an option `visibleWhen`, an
action `visible`). The lifted sites are the validation rule and the
`requiredWhen`. The build flags all four; the object door flags the two
lifted sites, in the build's order, and `runStackExpressionPasses` on an
object write returns exactly the build's findings for the admitted
passes.
- **Protocol door:** a new pass-2 block in
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`,
through the real `saveMetaItem`, `publishMetaItem` and
`publishPackageDrafts`:
- (a) a bare `requiredWhen`, an unregistered function in `visibleWhen`,
and a `parent` read with no master in `readonlyWhen` are each refused on
an active save with a 422 `INVALID_METADATA` carrying the build's
located finding, and nothing lands;
- (a) the card's body is refused on a draft's promotion, and on a
package draft publish (`outcome: 'refused'`, `failed` naming the object
with `INVALID_METADATA`, the row left a draft); the draft saves
themselves still succeed;
- (b) valid predicates on the three slots still save, and the row lands
active;
- (d) for each refused body, the door and `os build` give the same
finding on rule, where, path, message and hint.
## Reverse verification (one-off, from committed HEAD `ab17f41aa`)
- **What was mutated.** `scripts/ablation-replace.mjs --hold` put the
fence back at the head of the field loop: `const ablationFence22032 =
objectWrite; if (ablationFence22032) { judgeFieldFormula(fname, f);
continue; }`. The anchor was hit once, 1 to 0, and the blob went
`758396f8e4e4` to `0f007b9ed735`. The script carried `trap restore EXIT
INT TERM`.
- **Rebuild and dist proof.** `@objectstack/lint` was rebuilt, and
`ablation-dist-preflight` found the marker in 4 built files.
- **Lint suites (source): 11 failed, 18 passed, as predicted.** Red: the
8 LIT tests, PARITY, and the two fence tests that assert the lifted
sites. Green: the registry test, CONTROL, the differential, the
build-flags-each-site test, the six formula-door tests and the eight
pass-1 tests.
- **Protocol pass-2 block (dist-mediated): 6 failed, 1 passed, as
predicted.** Red: the three (a) saves, (a) on promotion, (a) on package
publish, and (d). Green: (b).
- **Restore.** The tool restored the file: blob `758396f8e4e4` equals
HEAD, and `git diff HEAD` is empty. Lint was rebuilt, and `--absent`
found the marker gone from all 14 built files with a clean tree. Both
suites went green again: lint 29 of 29, and the protocol file 86 of 86.
## Measurements
- **Corpus first: the stop condition was not met.** Every object this
tree ships was judged before the door changed: every `*.object.ts` under
`packages/**` and `examples/**` (111 files), plus the two
`app-multi-package` sub-stacks. That is 118 objects in 18 groups, at the
raw shape and at the `ObjectSchema.parse` shape, each with its own group
as context.
- 9 field-rule slots on 8 fields of 3 objects: `showcase_invoice` 4
(`issued_on.requiredWhen`, `tax_rate.readonlyWhen`,
`paid_on.requiredWhen`, `paid_on.visibleWhen`), `showcase_invoice_line`
4 (`product`, `quantity` and `unit_price` `readonlyWhen` read `parent`;
`description.requiredWhen`), and `sys_permission_set.name.readonlyWhen`.
- At base `bafb58bb0`: 0 build errors and 0 build warnings for the pass
(raw, parsed, and through `runAuthoringRules('build')`), and 0 door
findings.
- At head: 0 door errors and 0 door advisories over every object,
through `runRuntimeAuthoringRules` with type `object`, at both shapes.
- The card's body, as a positive control in the same harness: 1 build
error and 1 door error.
- **Which doors newly answer 422 (H5).** The active publish save, a
draft's promotion, and a package draft publish, measured through the
real methods above. A draft save stays ungated, measured by the same
pins.
- **`conditionalRequired` cannot reach this gate through the save
door.** Measured through the real `saveMetaItem` with a scratch test
that was deleted afterwards: the per-type spec step refuses it as a key
retired in protocol 17 (422 `INVALID_METADATA`, on a draft save and on a
publish save) before the gate runs. The lint pin judges it because the
build does.
## Clause-② (measured)
- **Accept set: narrowing.** An object write in publish mode answered
200 for a field-rule slot the validator refuses. It now answers 422 on
the three doors above.
- **Built entry declarations.** In `@objectstack/lint` one doc comment
moves (`AuthoringRuleContext.runtimeWriteType`).
`StackExpressionOptions` and `runStackExpressionPasses` are not in the
built declarations. No exported signature moves.
- **Changeset.** `.changeset/22032-object-save-door-field-rule-slots.md`
covers `@objectstack/lint` and `@objectstack/metadata-protocol`:
`minor`, `fix(lint)!`, BREAKING, with the remedy, and ADR-0087
`not-required (no-migration-prescription)`. `.changeset/pre.json` is
absent on `origin/main` `bafb58bb0`, so `minor` with the BREAKING
banner, as pass 1.
## Tests and gates (all at `ab17f41aa`)
- `@objectstack/lint`: 122 files, 5658 tests passed; `typecheck` exit 0,
its test-typecheck included (`--listFiles` shows the three touched lint
test files in the `tsconfig.test.json` program).
- `@objectstack/metadata-protocol`: 221 files passed and 3 skipped;
28234 tests passed and 19 skipped; `typecheck` exit 0 (`--listFiles`
shows the door test file in the program).
- **Consumer readings, against a rebuilt `rest^...` and `objectql^...`
closure.**
- `@objectstack/rest`: every `meta-object-*` file and
`meta-publish-package-scope`, 11 files, 197 tests passed.
- `@objectstack/objectql`: `save-meta-response-conformance`,
`publish-meta-response-conformance` and `plugin.integration`, 3 files,
67 tests passed.
- No other test fixture saves a field-rule slot through a save door:
every test file carrying one of the four slot keys was grepped against
the door entry points, and the hits are the lint and metadata-protocol
files above, whose suites are green.
- **Gates.** `dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 63 commands at this head. All 63 end
at exit 0, and `--ran` reconciles 63 derived, 63 run, 0 NOT-MEASURED, 0
UNRUN, with an exit code recorded for each.
- `check-plugin-teardown-shape.mjs --self-test` first exited 3: its
pinned fixture commit was outside this shallow clone. After `git fetch
--depth=1` of that one commit it exited 0.
- `check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT MET: no
full build). After `pnpm build` (72 tasks, 71 cached) it exited 0.
- **ESLint, narrowed to the 6 touched TypeScript files**
(`--no-inline-config --format json`): 6 files, 0 errors and 0 warnings.
Each file is matched by `eslint.config.mjs` (`--print-config`), none was
ignored, and the config enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move the verdict on any
untouched file.
## Acceptance notes
- **A master save can be refused for a stored detail's field rule.**
Measured through the real `saveMetaItem` with a scratch test that was
deleted afterwards. The stored detail `fx_detail` has one
`master_detail` to `fx_master`, and a `readonlyWhen` reading
`parent.acct.name`, where `acct` is a lookup on the master. Re-saving
the master with only its label changed answers 422, with the issue
located at `object 'fx_detail' · field 'qty' readonlyWhen`. Without the
detail, the same save resolves.
- Cause: the gate's differential baseline leaves out the written
object's stored self, so a context finding that needs the written object
present is charged to that write. The traversal refusal's `parent`
holder needs the master's field types.
- The property is not new to this pass. `validateObjectFieldRefs` on
`main` does the same: re-saving a master is refused for a stored detail
whose lookup's `lookupColumns` names a column the master lacks.
- The corpus has no such detail. The detail is already refused by `os
build` and faults at runtime. The changeset states the consequence and
the remedy. It is reported on the card for the seat.
- **Boundary.** The door judges a detail's `parent.REF.FIELD` read only
when the master is in the write's context; a detail saved without its
master in the package closure gets no traversal verdict there, where the
build, holding the whole stack, gives one. The door stays a subset of
the build.
- **Out of this PR.** The nested `then` / `otherwise` predicate gap
(#22042) is not addressed here; it is serial behind this pass.
`formulas.mdx` could name the object save door: a docs addition, not a
false line.
- **Contract review.** Triage's grade asks for one per pass. It is the
seat's, from an isolated subagent at the contract-review tier, and is
not attached here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent a959493 commit f2a45db
7 files changed
Lines changed: 456 additions & 50 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
309 | 309 | | |
310 | 310 | | |
311 | 311 | | |
312 | | - | |
313 | | - | |
| 312 | + | |
| 313 | + | |
314 | 314 | | |
315 | 315 | | |
316 | 316 | | |
| |||
613 | 613 | | |
614 | 614 | | |
615 | 615 | | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
616 | 631 | | |
617 | 632 | | |
618 | 633 | | |
| |||
Lines changed: 174 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 | + | |
0 commit comments