Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/validation-variants-strict.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@objectstack/spec': minor
---

The six `validation` rule variants reject unknown keys, each against its own key set, and the type can finally represent its ADR-0010 protection envelope.

`validation` was the registered metadata type this campaign kept deferring: a `z.lazy()` discriminated union whose six variants each `.extend()` a shared base, so the one-call `strictObject` conversion the other types took does not apply.

**Why not just close the base.** `.extend()` inherits strictness, so closing `BaseValidationSchema` alone would have rejected unknown keys correctly — but the error map closes over the key list it was *built* with, which for the base is only the shared keys. A typo of a variant's own key (`transtions` for `transitions`, `formuIa` for `formula`) would have been rejected with no rename offered, which is the difference between a fixable error and a confusing one. The union discriminates on `type`, so an author is always on exactly one variant and that variant's full key set is the right candidate list. The base is now a named shape spread into six `strictObject` calls, each with the aliases that fit its own vocabulary.

**The envelope.** `validation` is a registered metadata type, so `MetadataPlugin`'s loader stamps `_packageId` / `_provenance` on it and `getMetaItemLayered` → `saveMetaItem` round-trips a body carrying them — and the schema could not represent them, so they were dropped on every parse. Declared once in the shared shape, so all six variants inherit it, and the type comes off the debt list in `kernel/metadata-type-schemas.test.ts` (that list carries a reverse pin, so removing an entry is forced rather than optional).

Authoring impact: a key none of the variants declares is now rejected instead of silently discarded — it was already being ignored, so no working behavior changes. The rejection names which variant it landed on ("this state-machine validation rule"), echoes the key, and suggests the closest declared one from that variant's full set.
84 changes: 84 additions & 0 deletions content/docs/references/data/validation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ const result = ConditionalValidation.parse(data);
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'conditional'` | ✅ | |
| **when** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). e.g. P`record.type == 'enterprise'` |
| **then** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| [ConditionalValidation](#conditionalvalidation)` | ✅ | Validation rule to apply when condition is true |
Expand All @@ -169,6 +176,13 @@ const result = ConditionalValidation.parse(data);
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'cross_field'` | ✅ | |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
| **fields** | `string[]` | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
Expand All @@ -191,6 +205,13 @@ const result = ConditionalValidation.parse(data);
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'format'` | ✅ | |
| **field** | `string` | ✅ | |
| **regex** | `string` | optional | |
Expand All @@ -214,6 +235,13 @@ const result = ConditionalValidation.parse(data);
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'json_schema'` | ✅ | |
| **field** | `string` | ✅ | JSON field to validate |
| **schema** | `Record<string, any>` | ✅ | JSON Schema object definition |
Expand All @@ -236,6 +264,13 @@ const result = ConditionalValidation.parse(data);
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'script'` | ✅ | |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). If TRUE, validation fails. e.g. P`record.amount < 0` |

Expand All @@ -257,6 +292,13 @@ const result = ConditionalValidation.parse(data);
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | ✅ | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'state_machine'` | ✅ | |
| **field** | `string` | ✅ | State field (e.g. status) |
| **transitions** | `Record<string, string[]>` | ✅ | Map of `{ OldState: [AllowedNewStates] }` |
Expand Down Expand Up @@ -288,6 +330,13 @@ This schema accepts one of the following structures:
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'script'` | ✅ | |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). If TRUE, validation fails. e.g. P`record.amount < 0` |

Expand All @@ -310,6 +359,13 @@ This schema accepts one of the following structures:
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'state_machine'` | ✅ | |
| **field** | `string` | ✅ | State field (e.g. status) |
| **transitions** | `Record<string, string[]>` | ✅ | Map of `{ OldState: [AllowedNewStates] }` |
Expand All @@ -334,6 +390,13 @@ This schema accepts one of the following structures:
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'format'` | ✅ | |
| **field** | `string` | ✅ | |
| **regex** | `string` | optional | |
Expand All @@ -358,6 +421,13 @@ This schema accepts one of the following structures:
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'cross_field'` | ✅ | |
| **condition** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL) comparing fields. e.g. P`record.end_date > record.start_date` |
| **fields** | `string[]` | ✅ | Fields involved. Only fields[0] is read (labels which field the violation attaches to); the rest are advisory. Shares script’s evaluation path. |
Expand All @@ -381,6 +451,13 @@ This schema accepts one of the following structures:
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'json_schema'` | ✅ | |
| **field** | `string` | ✅ | JSON field to validate |
| **schema** | `Record<string, any>` | ✅ | JSON Schema object definition |
Expand All @@ -404,6 +481,13 @@ This schema accepts one of the following structures:
| **tags** | `string[]` | optional | Categorization tags (e.g., "compliance", "billing") |
| **severity** | `Enum<'error' \| 'warning' \| 'info'>` | optional | |
| **message** | `string` | ✅ | Error message to display to the user |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
| **_packageId** | `string` | optional | Owning package machine id. |
| **_packageVersion** | `string` | optional | Owning package version. |
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'conditional'` | ✅ | |
| **when** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). e.g. P`record.type == 'enterprise'` |
| **then** | `[ValidationRule](#validationrule)` | ✅ | Validation rule to apply when condition is true |
Expand Down
5 changes: 3 additions & 2 deletions docs/audits/2026-07-unknown-key-strictness-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts).
| `notification.zod.ts` / `offline.zod.ts` / `report.zod.ts` | 3 ea | authorable (p) | |
| `sharing.zod.ts` | 2 | authorable (p) | public-sharing config |

### `data/` — 160 sites
### `data/` — 165 sites

| File | Sites | Class | Note |
|---|---|---|---|
Expand All @@ -262,7 +262,8 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts).
| `hook.zod.ts` / `hook-body.zod.ts` | 6+2 | mixed | **strict as of #4001 data step** for the AUTHORING shapes: `HookSchema` (+ `retryPolicy`) and both body branches (`ExpressionBodySchema` / `ScriptBodySchema`). `HookContextSchema` and its `session` / `provenance` / `user` blocks are the RUNTIME shape the engine hands a handler — they stay tolerant, and must: strictness there would make an engine-internal enrichment (as `provenance` was in #3712) a breaking change for anyone parsing a context they were given. The file's old blanket `authorable (p)` was too wide — verification split it |
| `mapping.zod.ts` | 3 | authorable (p) | |
| `external-catalog.zod.ts` | 4 | wire (p) | |
| `field-value.zod.ts` / `seed.zod.ts` / `validation.zod.ts` | 1 ea | mixed (p) | |
| `validation.zod.ts` | 6 | authorable | **strict as of #4001 batch 3b** — a `z.lazy()` discriminated union, so the one-call conversion does not apply: each of the six variants builds its own `strictObject` from a shared `BASE_VALIDATION_SHAPE`. Closing the base alone would have rejected correctly but suggested from the SHARED keys only, so a typo of a variant's own key (`transtions` → `transitions`) would get no rename. Site count 1 → 6 because the six variants are now object sites in their own right. The ADR-0010 envelope lives in the shared shape, so all six inherit it |
| `field-value.zod.ts` / `seed.zod.ts` | 1+1 | mixed (p) | `seed` is strict (registered-types batch) |

### `automation/` — 88 sites

Expand Down
Loading
Loading