Skip to content

Commit 45cd354

Browse files
os-helpclaude
andauthored
docs(deployment): give the "gate" table its fourth column — the runtime publish surface (#7442) (#7458)
The table documented three CLI commands. Since #4463 P1 there is a fourth door: `saveMetaItem` runs the same `AUTHORING_RULES` table on a `state: 'active'` write, and `publishMetaItem` runs it on the draft→active promotion. A reader of the old table concluded a Studio save passes no gate — false, and exactly the mental model that leads someone to add a redundant check or skip a real one. Each cell measured against the code rather than assumed. `runtimeGatedTypes()` returns `['flow']` and six registry rules declare `surfaces: ['cli', 'runtime-publish']`, so the column is `✓ᶠ` (runs, flow writes only) on those rows and `—` everywhere else; the Zod row is a plain `✓` because the per-type `safeParse` runs on every write. Findings measured live through the real dispatch path: `expression-invalid`, `approval-expression-invalid`, `flow-trigger-unknown-event`, `flow-runas-unscoped`, `filter-empty-combinator`, `flow-node-write-unknown-field` refuse; `flow-template-unknown-field`, `flow-draft-status-ambiguous`, `approval-expression-no-empty-policy` ride back as advisories. An `object` / `view` / `dashboard` write runs zero rules. Four rows added so the column can be honest: two runtime-gated rules had no row at all (empty filter combinators #5330, the flow half of the reference-integrity suite), flow trigger readiness moved out of the "Advisory:" row it had outgrown (#5762 promoted it to gating), and the runtime-only platform-schedule organization refusal (#6285) shows the column is not a pure subset in the other direction either. Dropped `visibility aliases` from the advisory row — `visibility-alias-deprecated` was retired by #6318. Also documents what #7435 landed: advisories reach the author on the save response, not only a server log. Closing prose now says what the fourth door does and does not guarantee — the wiring test requires every runtime-publish rule to run on `os build` too, so the door narrows types, never verdicts. Adjacent citation drift fixed in the same pass: `AUTHORING_RULES` lives in `packages/lint/src/authoring-rules.ts`, not `packages/cli/src/lint/`. Fixes #7442 Claude-Session: https://claude.ai/code/session_01XAdrnFmbvpLodAR88aoagw Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent b61afc1 commit 45cd354

1 file changed

Lines changed: 91 additions & 38 deletions

File tree

content/docs/deployment/validating-metadata.mdx

Lines changed: 91 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -350,53 +350,106 @@ another package defines.
350350

351351
`os validate`, `os build` (alias of `os compile`) and `os lint` run the **same**
352352
author-time rules, from one table — `AUTHORING_RULES` in
353-
`packages/cli/src/lint/authoring-rules.ts`:
354-
355-
| | `os validate` | `os build` | `os lint` |
356-
|---|---|---|---|
357-
| Protocol schema (Zod) ||||
358-
| CEL / predicate validation (ADR-0032) ||||
359-
| List-view navigation modes (ADR-0053) ||||
360-
| View container shape ||||
361-
| Widget-binding integrity (ADR-0021) ||||
362-
| Dashboard action/route references (ADR-0049) ||||
363-
| Filter placeholder resolvability (#3574) ||||
364-
| Object & action name references (#3583) ||||
365-
| Page-component field bindings (#3583) ||||
366-
| React page block field bindings — §10 (#4340) ||||
367-
| Chart bindings outside dashboards (#3583) ||||
368-
| Navigation vs. granted access (ADR-0090 D6) ||||
369-
| SDUI scoped styling (ADR-0065) ||||
370-
| JSX / React page source parses (ADR-0080/0081) ||||
371-
| Approval-node approvers (ADR-0090 D3) ||||
372-
| Security posture (ADR-0090 — e.g. every custom object declares `sharingModel`) ||||
373-
| Organization-axis red lines (ADR-0105 D6) ||||
374-
| Autonumber `{field}` interpolation ||||
375-
| View references — form targets, view-key collisions (#2554) ||||
376-
| Flow authoring anti-patterns (#1874) ||||
377-
| Advisory: flow trigger wiring, record titles, semantic field pointers (ADR-0085), seed replay/state safety, capability references, liveness, visibility aliases ||||
378-
| Package docs — flatness, prefixes, links (ADR-0046) ||||
379-
| Undeclared authoring keys — every metadata collection (#3786) and the stack's own top-level keys (#4167) ||||
380-
| Naming, labels, data-model conventions, i18n coverage ||||
381-
| Emits `dist/objectstack.json` ||||
353+
`packages/lint/src/authoring-rules.ts`.
354+
355+
There is a fourth door, and it is not a command. Every metadata **write**
356+
Studio's designer, `PUT /api/v1/meta/*`, an MCP/AI author — lands in
357+
`saveMetaItem`, and since #4463 a write going **`state: 'active'`** runs that
358+
same table before it persists. So does the draft→active promotion
359+
(`publishMetaItem`), because otherwise saving `?mode=draft` and then publishing
360+
would be the bypass. Draft saves themselves are deliberately never gated: a
361+
draft is allowed to be half-finished, and it cannot execute until it is
362+
published.
363+
364+
For someone authoring in Studio that door is not one of four — it is the **only**
365+
one. `sys_metadata` overlay rows are not in any config file, so there is no
366+
`os lint` they could have run instead.
367+
368+
| | `os validate` | `os build` | `os lint` | runtime publish |
369+
|---|---|---|---|---|
370+
| Protocol schema (Zod) |||||
371+
| CEL / predicate validation (ADR-0032) |||| ✓ᶠ |
372+
| List-view navigation modes (ADR-0053) |||||
373+
| View container shape |||||
374+
| Widget-binding integrity (ADR-0021) |||||
375+
| Dashboard action/route references (ADR-0049) |||||
376+
| Filter placeholder resolvability (#3574) |||||
377+
| Empty filter combinators — `$and: []`, `$or: []`, `$not: {}` (#5330) |||| ✓ᶠ |
378+
| Object & action name references (#3583) |||||
379+
| Flow reference integrity — node writes, template paths, read-only writes (#3583) |||| ✓ᶠ |
380+
| Page-component field bindings (#3583) |||||
381+
| React page block field bindings — §10 (#4340) |||||
382+
| Chart bindings outside dashboards (#3583) |||||
383+
| Navigation vs. granted access (ADR-0090 D6) |||||
384+
| SDUI scoped styling (ADR-0065) |||||
385+
| JSX / React page source parses (ADR-0080/0081) |||||
386+
| Approval-node approvers (ADR-0090 D3) |||| ✓ᶠ |
387+
| Security posture (ADR-0090 — e.g. every custom object declares `sharingModel`) |||||
388+
| Organization-axis red lines (ADR-0105 D6) |||||
389+
| Platform-schedule `create_record` organization (#6285) |||| ✓ᶠ |
390+
| Autonumber `{field}` interpolation |||||
391+
| View references — form targets, view-key collisions (#2554) |||||
392+
| Flow authoring anti-patterns (#1874) |||| ✓ᶠ |
393+
| Flow trigger readiness — a flow that looks armed and never launches (#5762) |||| ✓ᶠ |
394+
| Advisory: record titles, semantic field pointers (ADR-0085), seed replay/state safety, capability references, liveness |||||
395+
| Package docs — flatness, prefixes, links (ADR-0046) |||||
396+
| Undeclared authoring keys — every metadata collection (#3786) and the stack's own top-level keys (#4167) |||||
397+
| Naming, labels, data-model conventions, i18n coverage |||||
398+
| Emits `dist/objectstack.json` |||||
399+
400+
**`✓ᶠ` means the rule runs at that door for `flow` writes.** `flow` is the only
401+
metadata type any rule declares there today — #4463 shipped P1 as one type and
402+
four rule families, and widening is a data edit nobody has made yet. So an
403+
object, view, page or dashboard save is checked by the schema parse and by
404+
nothing else, and the `` cells above are `` for two different reasons: some
405+
rules read a stack-wide collection a one-item write does not carry (pages,
406+
dashboards, navigation, permission sets), and some parse authored source through
407+
`typescript`, which the kernel boot path must never load.
408+
409+
Both halves of a gate's behaviour are on this door. Gating findings **refuse**
410+
the write, as the same `422 invalid_metadata` envelope a schema failure
411+
produces — `issues[]` carrying `rule`, `path`, `where`, `message` and `hint`, so
412+
Studio can point at the offending field. Advisory findings never block: they
413+
ride back on the save response under `advisories`, which is the channel a Studio
414+
or MCP/AI author can actually read — server logs are not. A clean save carries no
415+
`advisories` key at all. `OS_ALLOW_UNLINTED_METADATA_WRITES=1` degrades a
416+
refusal to a loud log for a migration window, so rows written before the gate
417+
existed stay re-savable; it converts refusals, and never promotes them into
418+
`advisories`.
382419

383420
So `os validate` is the fast inner-loop check (no artifact), `os build` is what
384421
you run when you need the deployable artifact, and `os lint` adds its own style
385422
rubric on top. **Any rule that can fail a build runs on all three**, so a green
386423
`os lint` means the build's gates are green too, and a stack cannot be published
387424
through the one command that happens to skip a check.
388425

389-
Two rows are deliberately not universal, and both are one-directional (neither
390-
lets a stack through a gate another command enforces): the Zod parse and the
391-
undeclared-key diff need the pre-parse tier and the schema, which only the two
392-
commands that parse actually have; and `os lint`'s own rubric — snake_case
393-
names, missing labels, data-model conventions — is a lint verdict, not a publish
394-
gate. `os build` has never rejected a camelCase object name.
426+
The fourth door does not weaken that, because it is held to the CLI's verdicts
427+
rather than to its own: a test fails if a rule runs at the runtime publish gate
428+
but not on `os build` — the two publish verbs must not disagree. What that
429+
column narrows is which *types* it judges, never which *verdict* it reaches. The
430+
one deliberate exception is the platform-schedule row (#6285), runtime-only by ruling:
431+
both of its inputs are facts about the **deployment** (the organization this
432+
write lands in, and whether this deployment walls organizations), and a build
433+
machine's environment is a false signal for them — so `os build` must not judge
434+
it at all.
435+
436+
Two rows are deliberately not universal across the three commands, and both are
437+
one-directional (neither lets a stack through a gate another command enforces):
438+
the Zod parse and the undeclared-key diff need the pre-parse tier and the schema,
439+
which only the two commands that parse actually have; and `os lint`'s own
440+
rubric — snake_case names, missing labels, data-model conventions — is a lint
441+
verdict, not a publish gate. `os build` has never rejected a camelCase object
442+
name.
395443

396444
That invariant is enforced, not merely documented. Each rule declares its command
397445
coverage as data, and a CLI test fails if a rule that can emit `error` runs on
398446
fewer than all three, if a narrowed rule carries no written reason, or if any
399-
command reaches for a rule directly instead of going through the registry.
447+
command reaches for a rule directly instead of going through the registry. The
448+
fourth door is declared the same way, in the same entry, and checked by the same
449+
test: every rule says whether the runtime publish gate runs it — naming the
450+
metadata types it inspects when it does, and giving a written reason when it does
451+
not. There is no third option, so a rule cannot end up at that door, or off it,
452+
by nobody's decision.
400453

401454
The enforcement exists because the contract drifted four separate times, and the
402455
last audit (#4409) found 23 of 26 rules running on some strict subset of the
@@ -431,8 +484,8 @@ see [the gate in action](/docs/getting-started/build-with-claude-code#4-the-gate
431484
for the bare-reference example verbatim.
432485

433486
<Callout type="info">
434-
`os lint` runs every gate above **plus** its own style rubric (snake_case
435-
naming, required labels, namespace prefixes, data-model patterns, translation
487+
`os lint` runs every rule the three commands share **plus** its own style rubric
488+
(snake_case naming, required labels, namespace prefixes, data-model patterns, translation
436489
coverage). It does not replace `os validate` — it never parses against the Zod
437490
schema, so a schema error is `os validate`'s verdict to give — but a rule that
438491
can fail the build fails `os lint` too.

0 commit comments

Comments
 (0)