Repository navigation
Commit 3d91885
Part of #22032
Clause-②: no (narrowing)
This is pass 1 of #22032: the validation-rule predicates. Passes 2 to 4
stay fenced, and the card stays open for them: the field-rule slots,
option `visibleWhen`, and the object's action predicates.
## What changes
**The object save door gives the build's verdict on a validation rule's
predicates.** `formulas.mdx` says "the same `validateExpression`
validator backs `os build` and metadata registration". PR #22031
(#22019) made that true for formula fields and fenced every other
object-borne pass off the door by name. So an object whose
`validations[].condition` was `sqrt(record.amount) > 1`, or a bare
`amount > 1`, still saved with a 200, while `os build` refused both at
error.
- **The change is in the fence, not the registry.**
`runStackExpressionPasses` (`packages/lint/src/validate-expressions.ts`)
no longer empties the validation-rule loop on an `object` write. On that
write the rule now runs two passes, each at the build's own position in
the walk:
- the field-formula pass (unchanged);
- the validation-rule pass. It judges each rule's `condition`, with the
relationship-traversal checks, and a `conditional` rule's `when`. It
also runs the #4763 null-guard gate over every predicate the rule
carries, including those in the nested `then` and `otherwise` rules.
- **No registry change.** The `validateStackExpressions` entry already
declared `object` after PR #22031, so
`runtimeAuthoringRulesFor('object')` already dispatched it.
`runtime-gate.ts` is untouched. In `authoring-rules.ts` only comments
move: the entry's comment and the
`AuthoringRuleContext.runtimeWriteType` docblock. The latter is the one
line that reaches a built `.d.ts`.
- **The fence flag is renamed** from `fieldFormulasOnly` to
`objectWrite`. The old name would have been false. Its docblock
(`StackExpressionOptions.runtimeWriteType`) now names the two admitted
passes and the three fenced ones.
- **The door's verdict is the build's finding.** The door's 422 issue
and `runAuthoringRules('build', …)` give the same rule
(`expression-invalid`), location (`object 'fx_rule' · validation
'amount_rule'`), 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-validation-writes.test.ts` (new,
8 tests). It covers:
- LIT refusals: `sqrt(record.amount) > 1`, a bare `amount > 1`, a
`conditional` rule's `when`, and the null-guard gate's reach into the
nested `then` and `otherwise` predicates;
- CONTROL: valid, guarded predicates, at the top level and nested, 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 rule is not this write's
to answer for.
- **The fence (enumeration pin):** in
`packages/lint/src/runtime-gate.object-formula-writes.test.ts`. The
fenced body now carries one site for each of the three passes still
fenced: a `requiredWhen`, an option `visibleWhen`, and an action
`visible`. Before this PR it carried no option `visibleWhen`, so it
named only two of the three. The body also carries the lifted
validation-rule site. The build flags all four sites. The object door
flags the lifted site alone. On an object write,
`runStackExpressionPasses` returns exactly the build's own findings for
the admitted passes.
- **Protocol door:** a new #22032 block in
`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`,
through the real `saveMetaItem` and `publishMetaItem`:
- (a) both card bodies are refused with a 422 `INVALID_METADATA`
carrying the build's located finding, and nothing lands;
- (a) the same refusal on a draft's promotion;
- (b) a valid, guarded `condition` (`record.amount != null &&
record.amount > 100`) still saves, and the row lands;
- (d) for each refused body, the door and `os build` give the same
finding, compared on rule, where, path, message and hint.
- **The roster comment** in `runtime-gate.object-writes.test.ts` is
reworded. The roster itself does not move.
## Reverse verification (one-off, from committed HEAD `fcc1ae0c`)
- **What was mutated.** The fence was put back on the validation-rule
loop with `scripts/ablation-replace.mjs`: `for (const rule of
recordsOf(validations))` became `for (const rule of objectWrite ? [] :
recordsOf(validations))`. The anchor was hit once, 1 → 0, and the blob
went `55ee216f3d48` → `61fe6c01789b`.
- **Rebuild and dist proof.** `@objectstack/lint` was rebuilt.
`ablation-dist-preflight` found the planted marker in 4 built files.
- **The lint suites (source): red as predicted, 7 failed and 10
passed.** Red: the 4 LIT tests, PARITY, and the two fence tests. Green:
CONTROL, the differential, the registry test, the build-flags-each-site
test, and the six #22019 formula-door tests.
- **The protocol block (dist-mediated): red as predicted, 4 failed and 1
passed.** Red: (a) for both bodies, (a) on promotion, and (d). Green:
(b).
- **Restore.** The tool restored the file: blob `55ee216f3d48` 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 17 passed, and the protocol file 79
passed.
## Measurements
- **Corpus first (H4): the stop condition was not met.** Every object
this tree ships was judged by the build's validation-rule pass before
the door changed. That is every `*.object.ts` under `packages/**` and
`examples/**`, plus the two `app-multi-package` sub-stacks: 118 objects
in 17 groups.
- It was judged at the raw shape and at the `ObjectSchema.parse` shape.
- After the change it was judged again by the door's own function
(`runRuntimeAuthoringRules`, type `object`, with the object's own group
as the context).
- 21 rules carry 13 predicates on 10 objects:
- examples: 11 predicates on 7 objects (`app-crm` 3 on 2, `app-showcase`
6 on 4, `app-todo` 2 on 1);
- platform: `plugin-security` 2 on 2 (`sys_position`,
`sys_user_position`), plus one rule on `sys_user` that carries no
predicate.
- Result: 0 build errors and 0 build warnings; 0 door errors and 0 door
advisories.
- The card's two bodies, run as a positive control in the same harness,
gave 2 build errors and 2 door errors.
- **H1: confirmed.** The fence is
`StackExpressionOptions.runtimeWriteType`
(`validate-expressions.ts:1178`). It is consulted in
`runStackExpressionPasses` (`:1222` at base, `:1233` at head). At base
the validation-rule loop read `fieldFormulasOnly ? [] :
recordsOf(validations)` (`:1859`). The lift removes that guard. Passes 2
to 4 keep theirs: the field walk's early `continue`, and the action
loop.
- **H2: confirmed. No registry change.** The entry declares
`runtimeTypes: ['flow', 'action', 'hook', 'object']`
(`authoring-rules.ts:602` at base). `runtimeAuthoringRulesFor('object')`
(`runtime-gate.ts:550`) already dispatched it.
- **H3: confirmed.** The door's verdict equals the build's finding. This
is pinned key by key through the real save path for both card bodies. At
the lint level it is pinned for four bodies, including `when` and the
nested null-guard sites.
## Clause-② (measured)
- **Accept set: narrowing.** An object write in publish mode answered
200 for a validation rule whose predicate the validator refuses. It now
answers 422. This covers the active save, the draft promotion and the
package draft publish, which share the one gate.
- **Built entry declarations.** In `@objectstack/lint`, one doc comment
changes (`AuthoringRuleContext.runtimeWriteType`).
`StackExpressionOptions` and `runStackExpressionPasses` are not in the
built declarations. No exported signature moves.
- **Changeset.**
`.changeset/22032-object-save-door-validation-predicates.md` covers
`@objectstack/lint` and `@objectstack/metadata-protocol`: `minor`,
BREAKING, `fix(lint)!`. It gives the remedy, and its ADR-0087
disposition is `not-required (no-migration-prescription)`. It follows
#22019's changeset form.
## Tests and gates (all at `fcc1ae0c`)
- **Package tests:**
- `@objectstack/lint`: 121 files, 5646 tests passed.
- `@objectstack/metadata-protocol`: 219 files passed and 3 skipped;
28055 tests passed and 19 skipped.
- `typecheck` passed for both. The lint run includes its test-typecheck.
`--listFiles` shows both new and edited test files inside the tsc
program.
- **Consumer readings.** These are the files in the door's consumer
radius that carry validation-rule fixtures or drive the object save
door, run against a rebuilt closure:
- `@objectstack/rest`: `meta-object-extension-property-classes`,
`meta-object-materialization-agreement`,
`meta-object-overlay-extension-fold`, `meta-object-owd-gate` and
`meta-publish-package-scope`. 5 files, 71 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 validation predicate through the publish
door. That was found by grepping every test with `validations` against
the door's entry points.
- **Gates.**
- `dispatch-gates.mjs --commands` was derived at this head: 63 commands,
all exit 0. `--ran` reconciles: 63 derived, 63 run, 0 NOT-MEASURED, 0
UNRUN.
- `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET. It
passed after a full turbo build.
- `check:type-check-debt` was cut off by the batch's time cap and passed
when re-run alone (92s).
- The artifact-roster block (34 non-self-test families) and the 11
declared wide-population families were also run: 45 commands, all exit
0.
- **ESLint, narrowed to the 6 touched TypeScript files**
(`--no-inline-config`): 6 files, 0 errors and 0 warnings.
- The count is read from `--format json`.
- Each file is matched by `eslint.config.mjs` (`--print-config`), and no
file was reported as ignored.
- `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move the verdict on any
untouched file.
## Acceptance notes
- **A gap that both doors share.** The build's validation-rule pass
judges only the top-level `condition` and `when` of each rule. A nested
`then` or `otherwise` rule's `condition` gets the null-guard gate and
nothing else.
- Measured through the real `saveMetaItem` at this head: a `conditional`
rule whose `then.condition` is `sqrt(record.amount) > 1`, and whose
`otherwise.condition` is a bare `amont > 1`, saves with a 200, and the
row lands. The same predicate at the top level is refused with a 422.
- The door now mirrors the build exactly, so this is the build's gap. It
is outside this card. It is reported on the card for the seat to file.
- **Docs.** `formulas.mdx` was not edited. Its promise now holds at the
object save door for formula fields and validation-rule predicates. The
"Build-time validation" section could name the object save door: that is
a docs addition, not a false line.
- **Contract review.** Triage's grade asks for a contract review for
each pass. It is not attached here. It is the seat's, from an isolated
subagent at the contract-review tier.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent d4680d2 commit 3d91885
7 files changed
Lines changed: 450 additions & 58 deletions
File tree
- .changeset
- packages
- lint/src
- metadata-protocol/src
Lines changed: 29 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
309 | 309 | | |
310 | 310 | | |
311 | 311 | | |
312 | | - | |
313 | | - | |
314 | | - | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
315 | 316 | | |
316 | 317 | | |
317 | 318 | | |
| |||
588 | 589 | | |
589 | 590 | | |
590 | 591 | | |
591 | | - | |
| 592 | + | |
592 | 593 | | |
593 | | - | |
| 594 | + | |
594 | 595 | | |
595 | 596 | | |
596 | 597 | | |
597 | 598 | | |
598 | 599 | | |
599 | 600 | | |
600 | 601 | | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
601 | 616 | | |
602 | 617 | | |
603 | 618 | | |
| |||
Lines changed: 42 additions & 21 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
26 | 28 | | |
27 | 29 | | |
28 | 30 | | |
| |||
112 | 114 | | |
113 | 115 | | |
114 | 116 | | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
119 | 124 | | |
120 | 125 | | |
121 | 126 | | |
| |||
127 | 132 | | |
128 | 133 | | |
129 | 134 | | |
130 | | - | |
131 | | - | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
132 | 141 | | |
133 | 142 | | |
134 | 143 | | |
135 | | - | |
136 | | - | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
137 | 153 | | |
138 | 154 | | |
139 | 155 | | |
140 | 156 | | |
141 | | - | |
142 | | - | |
143 | | - | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
144 | 160 | | |
145 | 161 | | |
146 | 162 | | |
147 | | - | |
| 163 | + | |
148 | 164 | | |
149 | 165 | | |
150 | 166 | | |
151 | | - | |
| 167 | + | |
| 168 | + | |
152 | 169 | | |
153 | 170 | | |
154 | 171 | | |
| |||
157 | 174 | | |
158 | 175 | | |
159 | 176 | | |
160 | | - | |
161 | | - | |
162 | | - | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
163 | 184 | | |
164 | 185 | | |
0 commit comments