Skip to content

Commit 600b1e2

Browse files
docs(deployment): the four-doors matrix names rule families and doors, so the input each door hands them is written as its own axis (#18872)
Fixes #18815 Clause-②: no Docs-only. One file, +69 / -0: `content/docs/deployment/validating-metadata.mdx`. ## The gap, restated at the scope it is actually at The "one gate, four doors" table has exactly two axes: its **rows** are rule **families**, its **columns** are the four doors. A check mark says that door runs that family. It cannot say what that door **hands** the family to judge — and that input is where the three CLI doors have drifted three separate times. So the two-pass shape they run (the union fold over the artifact's collections, then the same table once per `packages[]` entry) was documented nowhere, for any door, across the three landings that wired it. This PR adds a third axis to the page as its own `###` section, plus two short scoping paragraphs: one above the table naming what the table's two axes are, one under the `one-directional` parenthetical saying which scope that sentence is written at. ## The card's gating claim, re-derived rather than relayed The card rests on "all three doors now run the per-package pass". Verified on the branch base `fb2bccfc96` by reading the call sites, not by trusting the card: | door | call site | wired by | |---|---|---| | `os build` | `packages/cli/src/commands/compile.ts:482` | #16611 | | `os validate` | `packages/cli/src/commands/validate.ts:421` | #18677 (PR #18769, merged 2026-09-17T21:18:28Z) | | `os lint` | `packages/cli/src/commands/lint.ts:722` | #18778 (PR #18813, merged 2026-09-17T23:32:18Z) | All three reach the one loop, `runPerPackageAuthoringRules` at `packages/cli/src/utils/artifact-packages.ts:189`. Both PRs are merged and are ancestors of this branch's base. **The successor condition holds.** Measured end to end as well, not only read: `validate-per-package-authoring-parity`, `lint-per-package-authoring-parity` and `union-fold-command-parity` (spawned, real binaries) — 3 files, 15 tests, exit 0. ## The seat's premise probe, re-run structurally The dispatch handed one grep (`per-package | union fold | union-folded | stack tier` => 0 hits, lit control `os build|os validate|os lint` => 25). Re-run as a structural read rather than a keyword miss, and widened to the whole docs tree: - on the page, every occurrence of "tier" is a **rule** tier — `pre-parse tier` (:519), `react tier` (:335), `tier findings` (:63), the ADR-0049 `tier programme` (:241). None is a stack shape. - across `content/docs/**`: zero hits for the per-package pass or the union fold in that sense (the 13 `per-package` hits are changelogs, doc-book grouping, capability prefixes, one-app-per-package ADRs), against a lit control of **64** files naming the three commands. Same probe over the published `skills/` catalog: zero, lit control 14. So the gap is real and is repo-wide, not just table-shaped. ## Falsification — the card's own word for the axis is already taken, and for a DIFFERENT axis The card and the dispatch both name the second axis the **stack TIER**. That phrase is already spent in this repo, on something else: - `packages/lint/src/authoring-rules.ts:1671` — *"The stack tiers a command has in hand when it runs the registry"*, documenting `AuthoringRuleRun`, whose members are `normalized` and `parsed`; - `:215` — *"Which tier of the stack a rule reads"*, for the same two; - `:213` — and `AuthoringRuleTier` is a third sense again, `'gating' | 'advisory'`. Those are not near-synonyms of the axis this card is about; they are **orthogonal to it**. `runPerPackageAuthoringRules` hands BOTH stack tiers the same per-package stack (`artifact-packages.ts:230-231`, `normalized: asStack, parsed: asStack`), and `lint.ts:653` says the mirror thing for the other pass — *"Both tiers are handed the UNION-FOLDED stack"*. A page that wrote "stack tier" for the union-vs-per-package axis would therefore have created a **new** collision, in the very module the table cites as its source, of exactly the class this card exists to close. ⇒ the page names the axis for what it is — the **input** each door hands the rule table — and names the two passes what the code already calls them: **the union fold** and **the per-package walk**. The card's *requirement* is met (family and input are kept apart, explicitly, at the one paragraph where they were conflated); its *wording* is deliberately not adopted. Flagging it rather than quietly diverging. ## Also written out, because it is a third thing again The fourth door is on **neither** pass. A runtime write is one item, so the publish gate evaluates differentially (context alone, then with the item grafted in, #4463) and narrows its resolution context to the written item's **package closure** (#9612) — which changes what a rule can *resolve*, never what it judges, and iterates no `packages[]`. "Runs per package" therefore names one thing at the three CLI doors and another at this one, and the `runtime publish` column says neither. Sourced from `packages/lint/src/runtime-gate.ts:208-259`, not from the card, which explicitly did not assert what that row should say. ## Evidence - **Derived gate families** — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **39**; all 39 run, exit codes recorded and reconciled: `--ran` => *"39 derived famil(ies) accounted for — 39 run, 0 NOT-MEASURED (a DERIVED zero — all 39 recorded an exit code and none of them is 3)"*. Five first reported `PREREQUISITE NOT MET` (exit 3 / exit 1, nothing measured) and were re-run to exit 0 after building `@objectstack/spec`, `@objectstack/formula`, `@objectstack/lint` and `@objectstack/client-react`: `check:doc-formula-expressions`, `check:doc-security-posture`, `spec check:docs`, `spec check:skill-examples`, `check:docs-transcript-drift`. - **`pnpm lint`** — the family `dispatch-gates.mjs` does not name. Run **whole**, not narrowed: `eslint . --no-inline-config`, **exit 0**, 82s, at `777cd9aeca`. - **MDX compiles** — `@mdx-js/mdx` 3.1.1 `compile()` on the edited page, exit 0. - **Anchors** — `pnpm check:doc-anchors` exit 0; the new heading adds `#what-each-door-hands-the-rule-table` and renames nothing, so the existing inbound links to `#the-one-gate-four-doors` (`cli.mdx:650`, `deployment/index.mdx:153`) are untouched. - **Control bytes** — `grep -naP` over the edited file: no hits; `pnpm check:nul-bytes` exit 0. - **Publishing surface, measured with controls both ways** — 83 manifests, 70 non-private. Manifests whose `files[]` mentions `content`: **0**. Positive control, `dist`: **70 of 70**. Manifests with no `files[]` at all (whole dir ships): **0**. `content/docs` is at the repo root, outside every package directory, and the one manifest that names a `content/docs` path at all does so in its `description` string (`plugin-webhooks`), not in `files[]`. ⇒ nothing published moves ⇒ `skip-changeset`, applied to this PR. ## Acceptance notes *Out of scope, noted and not filed:* - `content/docs/getting-started/examples.mdx` §"A project is a multi-package artifact" is the page that teaches `packages[]` to authors and would be the natural second home for a one-line pointer at the per-package pass. It does **not** enumerate the doors and contradicts nothing here, so triage's escalation condition ("a second page that cannot express the axis ⇒ p1 plus a structural card") is **not** met — measured, not assumed. Successor: none; noted for whoever next edits that section. - `content/docs/deployment/cli.mdx` carries the doors in two places (`:649`, `:668`, `:1485`) and defers to this page's matrix by link for the detail. Those three statements are true at both passes now, so nothing there is falsified by this PR and nothing was edited there. Successor: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 36f13bb commit 600b1e2

1 file changed

Lines changed: 69 additions & 0 deletions

File tree

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,12 @@ For someone authoring in Studio that door is not one of four — it is the **onl
382382
one. `sys_metadata` overlay rows are not in any config file, so there is no
383383
`os lint` they could have run instead.
384384

385+
The table below has exactly two axes: its **rows** are rule **families**, its
386+
**columns** are the four doors. A `✓` means that door runs that family — it does
387+
not say what that door hands the family to judge. That input is a third thing,
388+
orthogonal to both, and no cell here can carry it; it is written out in
389+
[What each door hands the rule table](#what-each-door-hands-the-rule-table).
390+
385391
| | `os validate` | `os build` | `os lint` | runtime publish |
386392
|---|---|---|---|---|
387393
| Protocol schema (Zod) | ✓ | ✓ | — | ✓ |
@@ -522,6 +528,14 @@ rubric — snake_case names, missing labels, the best-practice data-model sweep
522528
is a lint verdict, not a publish gate. `os build` has never rejected a camelCase
523529
object name.
524530

531+
Read that parenthetical at the scope it is written at. It is a claim about these
532+
**rows**: no row is `✓` at one door and `—` at another in a direction that lets a
533+
stack through. The same sentence about the **input** each door hands the table is
534+
a *different* claim, it has been false three times, and the two read identically
535+
in prose — so the input claim is stated
536+
[separately below](#what-each-door-hands-the-rule-table) rather than folded in
537+
here.
538+
525539
The declared-unique row is the one that looks like that rubric and is not. Its
526540
ADR-0120 D5 rules register for `os validate` and `os build` only, because
527541
`os lint` reaches them through its own data-model sweep and a second
@@ -546,6 +560,61 @@ artifact for stacks the other two refuse. A flow whose expression approver did
546560
not parse built and published green; only `os lint` stopped it, and CI usually
547561
runs the other two.
548562

563+
### What each door hands the rule table
564+
565+
Everything above this heading is one axis: which rule **family** runs at which
566+
**door**. This is the other one — the **input** each door hands those families to
567+
judge. They are independent: a door can run every rule in the registry and still
568+
certify a project clean, if what it hands them is an empty stack. A `✓` in the
569+
matrix answers the first question only.
570+
571+
**The three CLI commands run the whole table twice on every run.**
572+
573+
1. **The union fold.** One stack, every package's collections seen together.
574+
Under the ADR-0130 D4 artifact shape a project's definitions may live entirely
575+
in `packages[]` with nothing at the top level, so the fold fills only the
576+
collections the top level does not carry, and returns a stack that already
577+
carries them unchanged. This is the only pass that can see a finding which
578+
**spans** packages. Until it was wired at all three doors (#17069, #17528),
579+
such a project was judged as if it declared nothing — `os validate` and
580+
`os lint` exited 0 on a stack `os build` refused.
581+
2. **The per-package walk.** The same table again, once per entry of `packages[]`:
582+
each package's body re-read as the stack it was assembled from, handed the
583+
artifact's own `packages[]` as **resolution context** so a sibling package's
584+
object still resolves, while the collections being judged stay that package's
585+
alone. It is de-duplicated against the union run, so what it reports is exactly
586+
the set the union could not see, and every finding it raises carries a
587+
`package '<id>' —` prefix in its `where`. A project with no `packages[]` skips
588+
it: one package by definition, already judged whole by the union run.
589+
590+
The second pass is not the first one in a stricter mood — it is a different
591+
question. A rule asking *"does this stack's app navigation point at an object
592+
this stack defines?"* is satisfied by the union for a package that carries
593+
neither half; per package it is not. Because the artifact registers **per
594+
package** (ADR-0130 D4/D5), the per-package answer is the one the runtime will
595+
live with.
596+
597+
**That pass reached the three doors one at a time, and every gap was false-clean
598+
in the author's favour.** `os build` has run it since #16611, `os validate` since
599+
#18677 and `os lint` since #18778; until each of those landed, the door in
600+
question ran the union fold and stopped. Symbol presence is not coverage here:
601+
`os lint` imported the per-package seams for two cards before it ran this pass,
602+
because it walks `packages[]` for a duplicate-name advisory of its own — and that
603+
advisory is `os lint`'s own rubric, not this table.
604+
605+
**The fourth door is on neither pass, and the package narrowing it does have is a
606+
third thing again.** A runtime write is one *item*, not a stack, so the publish
607+
gate builds a per-write snapshot — the written item plus the live registry's
608+
objects as resolution context — and runs the table twice over it: once on the
609+
context alone, once with the item grafted in, attributing to the write only the
610+
findings the item **added** (#4463). Its package dimension narrows that context to
611+
the written item's **package closure** — the written package, its declared
612+
dependencies, and unconditionally both platform/system objects and rows carrying
613+
no package provenance (#9612); a provenance it cannot read narrows nothing at all.
614+
That changes what a rule can **resolve**, never what it judges, and nothing at
615+
this door walks `packages[]`. So "runs per package" names one thing at the three
616+
CLI doors and another one here, and the `runtime publish` column says neither.
617+
549618
A clean run walks the registry and reports timing:
550619

551620
```text transcript=os-validate

0 commit comments

Comments
 (0)