Skip to content

Commit 66e0df6

Browse files
committed
chore(changeset): the validations[] parent refusal is a breaking lint minor; the warning entry stops promising 19.0
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz
1 parent 65d699d commit 66e0df6

3 files changed

Lines changed: 45 additions & 2 deletions

File tree

‎.changeset/24720-validation-rule-parent.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ A `validations[]` rule's predicates — a `script` or `cross_field` rule's `cond
1010

1111
The build's expression rule (`validateStackExpressions`) now reports such a predicate at `warning`, located where the rule's other findings are (`object 'NAME' · validation 'RULE'`, `… when`, or the nested rule's label). The message names the slot, the rule and the root. The object save door runs the same pass and lists the same finding among its advisories: the same rule id (`expression-invalid`), location and message.
1212

13-
**Nothing new is refused in 18.x.** A warning fails neither `os build` nor `os validate`, and a publish-mode object save that carries such a rule is still admitted. No ADR-0087 conversion can rewrite a predicate that reads `parent`, so under the post-GA rule for such narrowings only this warning ships in 18.x. The refusal, the same finding at `error`, is planned for 19.0. It will also cover a rule marked `active: false`, a rule whose `events` a stack never fires, and a `conditional` branch whose `when` is never true, because the verdict judges the rule as written.
13+
**Nothing new is refused by this change.** A warning fails neither `os build` nor `os validate`, and a publish-mode object save that carries such a rule is still admitted. No ADR-0087 conversion can rewrite a predicate that reads `parent`, so this change ships the warning first. The refusal, the same finding at `error`, follows in its own breaking entry (#24957). It also covers a rule marked `active: false`, a rule whose `events` a stack never fires, and a `conditional` branch whose `when` is never true, because the verdict judges the rule as written.
1414

1515
**Remedy.** To lock or require a field on the header's state, write the predicate on that field's own `readonlyWhen` or `requiredWhen`, where `parent` is bound. For any other check, read a column the detail object declares, and denormalise the header value you need onto the detail.
1616

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
fix(lint)!: `os build` and the object save door refuse a `validations[]` rule whose predicate reads `parent` (#24957)
6+
7+
Clause-②: no (narrowing: `os build` and the object save door refuse a `validations[]` rule whose predicate reads `parent`, which the server's rule evaluator never binds)
8+
9+
A `validations[]` rule's predicates are a `script` or `cross_field` rule's `condition`, a `conditional` rule's `when`, and the same slots on the rules nested in its `then` / `otherwise`. The server evaluates them against `record` and `previous` only. The master-detail header is bound as `parent` for a field's own `readonlyWhen` and `requiredWhen` alone, on an object with exactly one `master_detail`. A rule that reads `parent`, such as `parent.status == 'paid'`, faults (`Unknown variable: parent`) on every write it judges, and the server refuses that write as unevaluable, the seed's own rows at boot included.
10+
11+
The build's expression rule (`validateStackExpressions`) reported such a predicate at `warning` (#24720). It now refuses it at `error`, at the same location (`object 'NAME' · validation 'RULE'`, `… when`, or the nested rule's label), with the same rule id (`expression-invalid`). The message no longer says it is a warning. The object save door runs the same pass, so its verdict is the build's finding.
12+
13+
**BREAKING — what moves for consumers.**
14+
15+
- `os build`, `os validate` and `os lint` fail on a `validations[]` rule whose predicate reads `parent`. The rule is judged as written, so this also covers a rule marked `active: false`, a rule whose `events` a stack never fires, and a `conditional` branch whose `when` is never true.
16+
- An object write in publish mode that carries such a rule answered 200, with the finding among its advisories. It now answers `422 INVALID_METADATA`, with an `expression-invalid` issue located at the rule. This covers `PUT /api/v1/meta/object/:name` (and `saveMetaItem` in publish mode), the promotion of a draft (`POST /api/v1/meta/object/:name/publish`, `publishMetaItem`), and a package draft publish (`publishPackageDrafts`).
17+
18+
**Remedy.** To lock or require a field on the header's state, write the predicate on that field's own `readonlyWhen` or `requiredWhen`, where `parent` is bound. To refuse a whole write on the header's state (for example a new line under a paid invoice), use a lifecycle hook (`beforeInsert` / `beforeUpdate` / `beforeDelete`) that reads the header. For any other check, read a column the detail object declares, and denormalise the header value you need onto the detail. Saving the object as a draft (`mode: 'draft'`) is still allowed, because drafts are never gated; publishing that draft is judged.
19+
20+
**Unchanged.**
21+
22+
- The runtime is unchanged: a `validations[]` rule still binds `record` and `previous` and no `parent`, and such a rule already refused every write it judged. If the runtime comes to bind `parent` there, this refusal is withdrawn in that same change.
23+
- `parent` on a field's own `readonlyWhen` / `requiredWhen` is still accepted with no finding, and so is a `record` column spelled like the root (`record.parent_code`).
24+
- Stored rows are not migrated, and they are not refused on read. An object stored before this change keeps loading until it is next saved, and that save is judged.
25+
- `OS_ALLOW_UNLINTED_METADATA_WRITES=1` still turns a refusal into a logged write.
26+
- No example, template or package object in this repository authors `parent` in a `validations[]` rule.
27+
- No public export or signature moves.
28+
29+
<!-- adr-0087: not-required (no-migration-prescription) a refusal at `os build` and at the object save door of a validations[] rule predicate that reads `parent`, which the server's rule evaluator never binds: no authorable key, spelling, export or stored shape moves, and no stored row is read, rewritten or converted. A stored object carrying such a rule keeps loading until it is next saved, and the repair is the author's rewrite of the predicate (a field rule, a lifecycle hook, or a denormalised column), which no ledger entry can derive. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers this verdict (not already-registered); and the change is a build and door verdict, not a declaration (not runtime-interface-only or type-surface-only). -->

‎packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4023,7 +4023,7 @@ describe('runtime authoring gate on VIEW writes that name no package — the who
40234023
* `INVALID_METADATA` carrying the build's located finding — and nothing
40244024
* lands;
40254025
* (b) the same body still saves as a DRAFT (drafts are never gated), and the
4026-
* draft's promotion is refused;
4026+
* draft's promotion and a package's draft publish are refused;
40274027
* (c) control: `parent` on the field's own `readonlyWhen` saves, and so does a
40284028
* rule reading `record`.
40294029
*
@@ -4105,6 +4105,20 @@ describe('runtime authoring gate on OBJECT writes — a validations[] predicate
41054105
expect(lineRows(rows).map((r) => r.state)).toEqual(['draft']);
41064106
});
41074107

4108+
it("(b) a PACKAGE's draft publish of the same body is refused — nothing goes live", async () => {
4109+
const { protocol, rows } = hostWithHeader();
4110+
await expect(
4111+
saveLine(protocol, line([rule(PARENT)]), { mode: 'draft', packageId: 'app.fx' }),
4112+
).resolves.toMatchObject({ success: true });
4113+
4114+
const res = await protocol.publishPackageDrafts({ packageId: 'app.fx' });
4115+
4116+
expect(res.outcome, JSON.stringify(res)).toBe('refused');
4117+
expect(res.publishedCount).toBe(0);
4118+
expect(res.failed).toEqual([expect.objectContaining({ type: 'object', name: 'fx_line', code: 'INVALID_METADATA' })]);
4119+
expect(lineRows(rows).map((r) => r.state)).toEqual(['draft']);
4120+
});
4121+
41084122
it('(c) control: `parent` on the field\'s own `readonlyWhen`, and a rule reading `record`, save and land', async () => {
41094123
for (const body of [
41104124
line([], { note: { type: 'text', label: 'Note', readonlyWhen: PARENT } }),

0 commit comments

Comments
 (0)