Skip to content

Commit 164f344

Browse files
os-zhuangclaude
andauthored
docs: v17 docs sweep run 5 — rc.2 catch-up (#4881)
Three hand-written pages had drifted behind changes that landed in the 2bafe62..a2ebea2 window. - data-modeling/validation.mdx contradicted itself: the has(x) callout says an unevaluable predicate is rejected fail-closed, while the condition paragraph twelve lines later still taught the pre-17 "logged and skipped rather than blocking the write". #4649 reversed that. Rewritten to the shipped contract, including the total stored-or-payload record that makes the has() callout true in the first place. - automation/hooks.mdx had no coverage of the declarative condition gate while three changes landed on it, one breaking. Adds a section for #4775 (unevaluable condition aborts the operation), #4770 (evaluates against stored + payload, not the payload alone) and #4784 (previous is bound). - concepts/metadata-lifecycle.mdx did not list job, which #4509 closed to runtime creation and org override. Adds the row with its reasoning, and notes the retired standalone validation kind under ADR-0088. Filed #4880 for the areas[] documentation gap rather than guessing at the section shape. Docs-only; releases nothing. Claude-Session: https://claude.ai/code/session_01AaegKY1Y7GqTb8CKMm5GLC Co-authored-by: Claude <noreply@anthropic.com>
1 parent be25f97 commit 164f344

4 files changed

Lines changed: 91 additions & 2 deletions

File tree

.changeset/v17-docs-sweep-run-5.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
---
3+
4+
Docs-only: v17 docs sweep run 5 (rc.2 catch-up over the `2bafe62e..a2ebea2e`
5+
window). Three hand-written pages had drifted behind changes that landed in the
6+
window; the rest of the sweep's search surface came back clean.
7+
8+
- **`data-modeling/validation.mdx` contradicted itself.** The `has(x)` callout
9+
(added by #4763) says an unevaluable predicate is "rejected fail-closed", while
10+
the `condition` paragraph twelve lines later still taught the pre-17 behaviour —
11+
"logged and skipped rather than blocking the write". That is exactly what #4649
12+
reversed, and it is the sentence an upgrading author reads to decide whether
13+
their rules are enforcing anything. Rewritten to the shipped contract:
14+
`VALIDATION_FAILED` naming the rule and the offending key, `severity` still
15+
governing blocking, and the total stored-⊕-payload record that makes the
16+
`has()` callout true in the first place.
17+
18+
- **`automation/hooks.mdx` had no coverage of the declarative `condition` gate**
19+
one passing clause under wildcard hooks, and nothing else — while three changes
20+
landed on it in this window, one of them breaking. Adds a "The `condition` gate"
21+
section: an unevaluable condition now ABORTS the operation instead of silently
22+
skipping the hook (#4775), the condition evaluates against stored ⊕ payload
23+
rather than the write's payload alone (#4770), and `previous` is bound so a
24+
condition can express a transition (#4784) — including the upgrade note that
25+
`record.x == v` alone is now true on every update of an already-matching row.
26+
27+
- **`concepts/metadata-lifecycle.mdx` did not list `job`**, which is the page that
28+
explains the two-tier overlay/runtime-create gate and therefore where an author
29+
looks when "create job" disappears from Studio. Adds the row with #4509's
30+
reasoning (`handler` names a compiled-bundle function a runtime writer cannot
31+
reach), and notes that the standalone `validation` kind is gone under ADR-0088.
32+
33+
Releases nothing.

content/docs/automation/hooks.mdx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,46 @@ a hook can be, so review it as such:
9696
Where a wildcard is the honest answer, say so in `description` — it is the one
9797
place a reviewer can find out why the broad target was chosen.
9898

99+
## The `condition` gate
100+
101+
A declarative hook can carry a CEL `condition`, evaluated **before** the handler:
102+
the hook fires only when it is true. Three things about it changed in protocol 17,
103+
and the first is breaking.
104+
105+
**An unevaluable condition aborts the operation (#4775).** A condition the platform
106+
could not work out used to emit a `logger.warn` and `return false` — the hook simply
107+
did not fire. "The condition said no" and "the platform could not evaluate the
108+
condition" carry *opposite* risks depending on the hook: swallowed into a `before*`
109+
guard it silently lets the write through; swallowed into an audit hook it silently
110+
drops the record. They are now distinct outcomes, and the second **fails the write**.
111+
Hooks that have been getting by on that skip will start failing — that is how you
112+
find out they were never enforcing anything.
113+
114+
**The condition reads the record, not the payload (#4770).** It used to evaluate
115+
against `ctx.input.data` — only the fields the current write happened to carry — so
116+
`condition: "record.done == true"` did **not** run on the most ordinary updates there
117+
are (change the status, change the assignee), because `done` was not in the payload.
118+
It now evaluates against **stored ⊕ payload**: the prior record overlaid with this
119+
write's data, total over the object's declared fields (`null` for a declared field in
120+
neither), with the payload winning for the fields it carries. Undeclared or typo'd
121+
keys stay unresolvable — `record.stauts` is an error, not a silent `false`.
122+
123+
**`previous` is bound, so a condition can express a transition (#4784).** The scope
124+
was a single `{ record }` root, which made the published `previous` form
125+
(`previous.status != 'escalated' && record.status == 'escalated'`, and the legacy
126+
`OLD.x` / `ISCHANGED(x)` mappings) abort with `No such key: previous`. It is now
127+
bound alongside `record`, built by the same helper the validation side uses, so one
128+
CEL expression means one thing on both surfaces.
129+
130+
```ts
131+
// "after a task transitions to done" — not "whenever a done task is written"
132+
condition: P`previous.done != true && record.done == true`
133+
```
134+
135+
Because `record` now means the record's *state*, `record.done == true` alone is true
136+
on **every** update of an already-done row. If you wrote a condition under the old
137+
payload semantics expecting "the write that changed it", add the `previous` half.
138+
99139
## Before Hook
100140

101141
Mutate the incoming record before it is saved. The engine exposes the pending

content/docs/concepts/metadata-lifecycle.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,9 @@ In shared-database multi-tenancy, **most metadata types must not be per-org cust
113113
| `permission`, `position` || Per-org overlays are allowed; tenant-level controls layer on top. |
114114
| `object`, `field` || Defines the table schema. Overriding a packaged object/field would break existing data — but both set `allowRuntimeCreate: true`, so tenants *can* author brand-new objects and fields. |
115115
| `datasource` || Connection strings; multi-tenant isolation is enforced at a higher layer. (`allowRuntimeCreate: true` — the datasource wizard persists `origin: 'runtime'` rows.) |
116+
| `job` || **Also `allowRuntimeCreate: false` since protocol 17** (#4509). `JobSchema.handler` names a function in the compiled bundle's function table, which a runtime writer has no way to reach — so a job created in Studio or through `PUT /meta` parsed, saved, reported success and was never scheduled. The door is closed rather than bridged: `job` stays first-class through `*.job.ts` / `defineStack({ jobs, functions })`, where every schedule shape, `retryPolicy` and `timeout` does reach the scheduler. Existing rows are untouched — they were never scheduled — and `migrateStoredMetadata` reports them `skipped`. |
116117

117-
There is no `workflow` metadata type (per [ADR-0020](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0020-state-machine-converge-and-enforce.md), record state machines are a `state_machine` validation). The runtime gate is implemented in `OVERLAY_ALLOWED_TYPES` (derived from the registry) and enforced by `SysMetadataRepository.put()`.
118+
There is no `workflow` metadata type (per [ADR-0020](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0020-state-machine-converge-and-enforce.md), record state machines are a `state_machine` validation). Nor is there a standalone `validation` type any more — it was retired in protocol 17 under [ADR-0088](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0088-metadata-kind-admission-and-retirement.md) because `ValidationRuleSchema` carries no object-binding key, so a rule authored through that door could never say what it protected; author rules in the object's own `validations[]` instead. The runtime gate is implemented in `OVERLAY_ALLOWED_TYPES` (derived from the registry) and enforced by `SysMetadataRepository.put()`.
118119

119120
The gate is **two-tier**`allowOrgOverride: false` is not the same as "no runtime writes":
120121

content/docs/data-modeling/validation.mdx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,22 @@ Formula-based validation using expressions:
104104
}
105105
```
106106

107-
The `condition` is a **CEL** predicate and should evaluate to `true` when the data is **invalid**. A predicate that cannot be evaluated (parse error, unbound variable) is treated as a broken rule — it is logged and skipped rather than blocking the write.
107+
The `condition` is a **CEL** predicate and should evaluate to `true` when the data is **invalid**.
108+
109+
A predicate that cannot be evaluated (parse error, unbound variable, a comparison
110+
CEL has no overload for) **rejects the write** with `VALIDATION_FAILED`, naming the
111+
rule and — when the fault is a missing key — the key the predicate read and how to
112+
fix it. Until protocol 17 such a rule was logged at WARN and *skipped*, so the write
113+
went through while the rule stayed declared and enforced nothing; a validation exists
114+
to reject a write, and "the rule could not be checked" must never resolve to
115+
"allowed" (#4649). `severity` still governs blocking — an unevaluable `warning` /
116+
`info` rule is logged and does not throw.
117+
118+
The record a predicate reads is the stored row overlaid with this write's payload,
119+
**total over the object's declared fields** (`null` for a declared field present in
120+
neither), on update as well as insert — so a driver that stores only the columns it
121+
wrote no longer decides whether an expression is evaluable. That totality is also why
122+
`has()` is not a null guard: see the callout above.
108123

109124
### Uniqueness (use an index, not a validation rule)
110125

0 commit comments

Comments
 (0)