Skip to content

Gate the metadata-lifecycle overlay-whitelist table against DEFAULT_METADATA_TYPE_REGISTRY — both directions, and by AST not regex #11752

Description

@os-steve

Follow-up to #11664 / #11750. Filed by the devx PM seat; the #11750 dev raised it and was instructed not to build it in a docs-only PR.

What

Add a gate that asserts the "Overlay whitelist (shared-DB tenancy invariant)" table in content/docs/concepts/metadata-lifecycle.mdx against DEFAULT_METADATA_TYPE_REGISTRY (packages/spec/src/kernel/metadata-plugin.zod.ts).

Why this one is worth a gate

The section declares the registry to be the single machine-readable source — "The whitelist lives in one place: MetadataTypeRegistryEntry.allowOrgOverride" — and the table drifted from it anyway, on four types, undetected long enough to be caught by a human fact-checking a promo video (#11664, video-studio #4):

Type Table said Registry says
flow ✅ ❌ (rolled back #6283)
permission ✅ ❌ (rolled back #6483)
position ✅ ❌ (same rollback)
translation absent ✅

Nothing mechanical was watching a comparison that is entirely mechanical. #11750 corrected the table; it did not stop the next drift.

Two constraints, both established by measurement on #11750 — treat as binding

1. The gate MUST check both directions. translation was a false negative by omission — a type the registry marks allowOrgOverride: true that the table simply did not list. A table→registry gate ("every row I see agrees with the registry") passes on a table missing a whole row. Three of the four defects were findable from the table side; the fourth was findable only from the registry side. A one-directional gate would have shipped 3/4 and reported clean.

2. The gate MUST read the registry by AST, not regex. This is not a style preference — a same-line regex silently under-reads this exact file. Measured on origin/main @ 2a6122bd9:

  • grep -cE "^ \{ type: '" over the registry literal → 26
  • AST parse → 27

The missed entry is datasource, whose object literal opens { on its own line so type: and allowOrgOverride: land on separate lines. A regex-built gate would have been born with one type invisible to it — and invisible in the direction that matters, since the missing entry would never be reported as an omission from the table.

Two more shapes a regex gets wrong here, both present in the file today:

  • grep -c 'allowOrgOverride: true' = 6, but only 5 are entries; the sixth is prose inside a code comment at :789.
  • 33 lines match allowOrgOverride: false inside the registry region; only 22 are entries. The other 11 are comments discussing the flags.

The #11750 dev already wrote a throwaway AST comparator end-to-end and demonstrated it firing in both directions (exit 1 / VERDICT: 4 DIVERGENCE(S) before, exit 0 / VERDICT: CONSISTENT after), so the hard part is done and is known to work — this card is about making it a permanent, wired gate rather than re-deriving it.

Non-vacuity requirement

Standard for this repo, and there is a ready-made positive control: the pre-#11750 revision of metadata-lifecycle.mdx is a file the gate must reject with 4 divergences. Prove the gate red on it and green on main after #11750 lands. Do not accept a green reading on a gate that has not been shown able to go red.

Scope notes

  • The table is hand-maintained — verified on docs: correct the overlay-whitelist table against the registry (flow, permission, position, translation) #11750: no generator writes the page, no DO NOT EDIT marker, no existing gate compares it to the registry. So the gate is the new thing, not a fix to a generator.
  • Do not flip any registry flag to make things agree. scripts/adr-anchors/packages__spec__src__kernel__metadata-plugin.zod.ts.json states that a flag flip here is an ADR-0005 whitelist change requiring the ADR revised, not a file edit. The registry is the authority; the table is what gets corrected.
  • Sibling pages already carry the same facts correctly (content/docs/permissions/authorization.mdx:236, content/docs/automation/jobs.mdx:42). Whether the gate should also cover those is an open question, not a requirement — decide it on evidence, and say which way you went and why.
  • Wire it into the derived-gate family the way its neighbours are wired, and confirm the wiring by a live node scripts/pm/dispatch-gates.mjs derivation rather than by assuming the glob covers it.

Activity

  1. self-assigned this
    on Aug 24, 2026
  2. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor

    Claim: devx lane PM seat, session e2eac1a7-8000-5c95-9749-38aec2ace6fc, branch claude/issue-11752-overlay-whitelist-gate.

    Dispatching now. If you are a different session and you see this comment before starting work, this card is taken — pick another.


    Generated by Claude Code

  3. claude commented on Aug 24, 2026

    @claude
    Contributor
    {
      "issue": 11752,
      "status": "done",
      "branch": "claude/issue-11752-overlay-whitelist-gate",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11759",
      "premise_still_valid": true,
      "summary": "Added scripts/check-overlay-whitelist-table.mjs (+ package.json check:overlay-whitelist-table, + a lint.yml step in the docs-guard block): the overlay-whitelist table in content/docs/concepts/metadata-lifecycle.mdx is held to DEFAULT_METADATA_TYPE_REGISTRY on two legs, both counts printed even at zero, registry read by TypeScript AST via scripts/ts-parse.mjs. ORDERING: your assumption 1 is right about order and wrong about timing — PR #11750 is OPEN, not merged; main @ 2a6122bd9 predates it, so the gate is red on today's main BY CONSTRUCTION. That red is the correct answer and I used it as the live positive control instead of a synthetic one. To avoid landing a permanently-red gate the branch merges #11750's single commit (180a416ac) as a declared dependency; #11750 remains open and authoritative, and the merge collapses to nothing if it lands first. Assumptions 2 and 3 held: the registry region has no spread/computed/helper shape (clean AST walk), and multi-type cells parse correctly (controlled). Assumption 4 (siblings) I decided your way, on measurement rather than lean — see open_questions for the one call that is yours, not mine.",
      "tests": "All runs at final commit 1f6d4507e unless stated; exit codes captured before any pipe.\n\nNON-VACUITY, LIVE, BOTH DIRECTIONS (same gate binary, two real trees):\n  RED  @ 2a6122bd9 (main, pre-#11750): exit 1, 'VERDICT: 4 DIVERGENCE(S)'\n    LEG 1 table->registry: 3 — [mismatch] flow (mdx:111 vs zod.ts:827), permission (mdx:113 vs zod.ts:1027), position (mdx:113 vs zod.ts:1028)\n    LEG 2 registry->table: 1 — [missing-row] translation (zod.ts:981, allowOrgOverride: true, named nowhere in the table)\n  GREEN @ 1f6d4507e: exit 0 — 'leg 1 (table -> registry) 0 divergence(s) over 13 type(s) named in 8 row(s); leg 2 (registry -> table) 0 divergence(s) over 5 allowOrgOverride: true type(s) [view, dashboard, report, translation, email_template] out of 27 declared.'\n  Every zero above is stated alongside that red run, which is the same probe on the same code.\n\nSELF-TEST (embeds the positive control permanently): exit 0 — 'positive control reproduces the 4 known divergences (leg 1: flow, permission, position; leg 2: translation), the corrected table is green on both legs, and 21 structural/parser cases are refused.' The battery caught two real defects in my own first draft (a wrong fixture count; a control that fired duplicate+mismatch instead of mismatch) — both fixed, both anchors confirmed on disk by grep before re-running.\n\nAST vs REGEX, card's numbers reproduced on 2a6122bd9: grep -cE \"^  \\{ type: '\" = 26, AST (--list) = 27; missed entry is datasource @ registry:870. MEASURED FURTHER (scratch ablation, swapping only the registry-reading leg, no tree mutation so no restore leg): regex reader leg1=4 leg2=1 total=5 including a SPURIOUS 'leg1 [unknown-type] datasource'; AST reader leg1=3 leg2=1 total=4. See open_questions/summary — this refines the card.\n\nWIRING, live derivation (Zone 3 item 4), 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' at 1f6d4507e, stderr confirms 'objectstack-ai/objectstack at commit 1f6d4507e': gate matched on BOTH sources, not merely the scripts/** glob —\n  'pnpm check:overlay-whitelist-table [lint.yml] matched via content/docs/concepts/metadata-lifecycle.mdx => gate source 'content/docs/concepts/metadata-lifecycle.mdx'; scripts/check-overlay-whitelist-table.mjs => gate script 'scripts/check-overlay-whitelist-table.mjs''\n  So it is reachable whether the table drifts or the gate does. 33 families matched for this diff.\n\nGATE UNION at 1f6d4507e (the final commit; clean tree, nothing committed after) — 24/25 green:\n  overlay-whitelist-table 0, entry-guard 0, parse-guard 0, nul-bytes 0, pnpm-filter-targets 0,\n  required-contexts 0, aggregator-roster 0, step-collectors 0, workflow-status-fns 0,\n  type-check-coverage 0, shard-attestation 0, node-version 0, agent-test-spelling 0,\n  cross-package-inputs 0, doc-anchors 0, doc-authoring 0, doc-frontmatter 0, docs-audit-scope 0,\n  docs-redirects 0, role-word 0, section-landing-index 0, published-readme-links 0,\n  react-page-adapter 0, ci-failure-selftest 0\n  type-check-debt = 1, NOT this change: it refuses to measure without a built dependency closure ('56 workspace dependenc(ies) ... have no built type entry point on disk'), the fresh-worktree trap. lint.yml:3418-3422 satisfies that precondition one step earlier (turbo run build --filter './packages/*' ... immediately before 'pnpm check:type-check-debt'). Its non-re-measure leg printed green in the same run ('check-type-check-coverage: OK — 65/78 workspace packages type-checked'). This diff carries zero TypeScript into any tsc program: the four changed paths are a workflow, the private root package.json, one .mjs and one .mdx, and root tsconfig.json sets no allowJs.\n\nREPO-WIDE LINT: 'pnpm lint' (eslint . --no-inline-config) run IN FULL at 1f6d4507e, LINT_EXIT=0, 53s under the shared verify lock. No narrowing claimed, so no narrowing evidence owed.\n\nAll heavy runs went through scripts/pm/os-verify-lock.sh (one wait of 232s behind two other agents' builds; verdicts read from its VERDICT lines).",
      "open_questions": [
        {
          "question": "The branch currently carries #11750's commit so the gate can be green. That is a sequencing call you own, not a blocker: #11750 is open, non-draft, already skip-changeset labelled, and will very likely land first.",
          "options": [
            "A — leave the carry as is. If #11750 lands first the merge collapses to nothing and my PR is a pure gate diff. If mine lands first, #11750 becomes an empty PR someone must close.",
            "B — drop the merge commit and hold #11759 until #11750 lands, then merge main. Keeps the two PRs disjoint; costs a rebase round and leaves #11759 red until then.",
            "C — land #11759 red and rely on #11750 landing shortly after. Rejected: a red required gate wedges every open PR and the merge queue."
          ],
          "recommendation": "A. It is the only option that makes #11759 self-consistently green at every moment, the merge preserves #11750's authorship, and the collapse case is the likely one. C is unsafe and I did not consider it further."
        },
        {
          "question": "Sibling-page coverage (your assumption 4) — I went your way, but on measurement, and I want the number on the record rather than the lean.",
          "options": [
            "A — leave siblings out (what I did).",
            "B — extend the gate to prose assertions on authorization.mdx / jobs.mdx / agents.mdx / capabilities.mdx / email-template.mdx."
          ],
          "recommendation": "A. 'grep -rn allowOrgOverride content/docs/' hits 22 lines across 13 files; only ~5 are genuine '<type> is <bool>' assertions. The rest are the schema-field reference row, a code sample using an invented type, and sentences ABOUT the flag rather than about any type — metadata-lifecycle.mdx:121 itself is one. Covering them mechanically means a co-occurrence regex over English plus a per-site allowlist of ~17 exceptions to catch ~5 assertions: a worse gate than none, and the kind whose baseline gets bulk-updated to green. jobs.mdx:42 IS a table but a job-vs-flow one whose flag mention sits inside an English cell; no sibling page has a machine-shaped type->flag table. The count and the reasoning are in the script header so the next person re-deciding starts from the number."
        }
      ],
      "out_of_scope_findings": [
        "filed as #11763: metadata-lifecycle.mdx:119 (the sentence #11750 added under the table) carries two hand-kept registry-derived numbers — 'of the 27 types' and 'Those five' — that nothing checks. Both correct today (--list confirms 27 types, 5 overridable), so this is a finding, not a defect: label 'finding', no pm:queue, unassigned. Not folded into #11759 because the in-place-fix exemption fails condition 3 — that sentence was authored by PR #11750, still open, so another agent's in-flight work holds a live claim on the line, and asserting the number would couple my gate to that PR's final wording. Registering a 28th type would leave the page asserting '27' with every gate green."
      ]
    }

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions