Skip to content

Commit 915f9cc

Browse files
committed
docs(automation): correct the "hook write checks are advisory" claims this rule falsifies
Three statements went stale the moment a hook-body write check began to gate, and none of them was reachable by the docs drift check: `packages/lint/src/ index.ts` yields no anchor, and a page that states a rule by its INPUTS shares no token with a diff that changed the EMITTER. - hooks.mdx listed "readonly targets" as a flow-only `os validate` check and said a hook body's write set is checked "only as an advisory warning". - hook-bodies.mdx called the whole write side "advisory and literal-only". Existence stays advisory for its own reason (the answer can depend on a package the build cannot see); writability gates because both halves of that judgement are declared in the stack being checked. The pages now say which is which. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
1 parent f2e79c0 commit 915f9cc

2 files changed

Lines changed: 11 additions & 4 deletions

File tree

‎content/docs/automation/hook-bodies.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Static validation around a hook is asymmetric, and it is worth knowing exactly w
175175

176176
- **Checked — read side.** `hook.condition` is validated at build time against the target object's fields by the expression validator (`@objectstack/lint`), including array-valued `hook.object` targets. A condition referencing a nonexistent field fails the lint.
177177
- **Checked — capability side.** `body.capabilities` gates which `ctx` APIs the body may call at all; the sandbox throws on an undeclared call.
178-
- **Checked — write side, advisory and literal-only.** Since [#4271](https://github.com/objectstack-ai/objectstack/issues/4271), `body.source` is **parsed** (never executed, never type-checked) and the field names it writes are resolved against the target object's declarations. An unknown field raises `hook-body-write-unknown-field` — a **warning** carrying a did-you-mean suggestion, which never blocks a build. Action bodies get the same check on their `ctx.api` writes (`action-body-write-unknown-field`). Both run under `os validate`, `os lint` and `os compile`.
178+
- **Checked — write side, literal-only.** Since [#4271](https://github.com/objectstack-ai/objectstack/issues/4271), `body.source` is **parsed** (never executed, never type-checked) and the field names it writes are resolved against the target object's declarations. An unknown field raises `hook-body-write-unknown-field` — a **warning** carrying a did-you-mean suggestion, which never blocks a build. Action bodies get the same check on their `ctx.api` writes (`action-body-write-unknown-field`). Both run under `os validate`, `os lint` and `os compile`. The *existence* question is advisory like this because the answer can depend on a package the build cannot see; the separate *writability* question — [writing a `readonly` field through `ctx.api`](#writing-a-readonly-field) — **does** gate, because both halves of that judgement are declared in the stack being checked.
179179
- **Checked — writes to a system column the object has no storage for.** Since [#8663](https://github.com/objectstack-ai/objectstack/issues/8663), a write to an injected system column is no longer exempted on the strength of its NAME alone. The registry injects `owner_id` / `organization_id` / the audit family onto an ADR-0015 [`external` object](/docs/data-modeling/external-datasources) exactly as onto a local one, but the remote database owns that schema and no column exists behind them. Writing one raises `hook-body-write-unprovisioned-anchor` (or `action-body-write-unprovisioned-anchor` / `flow-node-write-unprovisioned-anchor` on the other two surfaces) — a **warning** on all three, including the flow-node rule that otherwise gates, because the claim is about a remote schema the build cannot see. A column you **declare** yourself is untouched: on a federated object a declared `owner_id` maps a remote column you vouch for. Why it matters more than an ordinary typo: an undeclared name is refused upstream by the engine's own write-path validator (`INVALID_FIELD`), whereas the injected anchor is in the registered schema and passes it — so it is the one payload key that reaches the remote database raw, where a SQL remote aborts the **whole statement** with an untyped `no such column` and takes the correctly named fields of the same payload with it.
180180
- **Checked — writes that reach nothing at all.** Since [#4345](https://github.com/objectstack-ai/objectstack/issues/4345), an action body assigning to `ctx.record` raises `action-record-write-discarded`, also a warning. This one is **not** a field-resolution question: an action's `ctx.record` is a snapshot the runtime never writes back, so the assignment is discarded whether or not the field is declared — see [Signature conventions](#signature-conventions) below.
181181

@@ -211,7 +211,7 @@ An unknown field is **not** caught at runtime, and it does not fail quietly eith
211211

212212
Neither outcome is the one you wanted, and the advisory warning is the earliest signal you get.
213213

214-
Because the checking is advisory and literal-only:
214+
Because the existence check is advisory, and every write-side check here is literal-only:
215215

216216
- **Treat `hook-body-write-unknown-field` as a build failure by convention.** It does not gate, but the rule is tuned for near-zero false positives — in practice a warning is a real typo.
217217
- **Check by hand what the parser cannot see.** Computed keys, spreads, aliased input and dynamic object names are invisible to the rule; for an array or `"*"` hook, every field must exist on every target.

‎content/docs/automation/hooks.mdx‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,19 @@ Two structural reasons to prefer the flow when either could work:
3535
`update_record` node's `fields` is structural config that `os validate`
3636
checks — readonly targets, template dialects, declared expression slots. A
3737
hook body's write set is checked too, but only for the literal patterns a
38-
parser can recognise and only as an advisory warning (see
38+
parser can recognise (see
3939
[Hook & Action Bodies](/docs/automation/hook-bodies#write-set-checking)).
4040
Field existence is checked on both surfaces, at different strengths: a hook
4141
body gets a warning, an `update_record` node a **gating error**
4242
(`flow-node-write-unknown-field`) — its `fields` is a literal map next to a
43-
literal `objectName`, so nothing could have mis-read it.
43+
literal `objectName`, so nothing could have mis-read it. **Writability** is
44+
now gated on both: writing a `readonly` field through `ctx.api` is
45+
`hook-api-update-readonly-field`, the hook-side sibling of the flow node's
46+
`flow-update-readonly-field` (see
47+
[Writing a `readonly` field](/docs/automation/hook-bodies#writing-a-readonly-field)) —
48+
the two questions differ because a `readonly` declaration and a literal
49+
`ctx.api` update are both visible in the stack, while a field's existence may
50+
depend on a package the build cannot see.
4451
- **A flow reviews as data.** The node graph diffs field-by-field and renders
4552
in the Console designer with per-node run history; a hook body reviews as
4653
code only.

0 commit comments

Comments
 (0)