Skip to content

feat(types): NamedListView mirrors spec 18's list-view requiredPermissions — no client gate #215

feat(types): NamedListView mirrors spec 18's list-view requiredPermissions — no client gate

feat(types): NamedListView mirrors spec 18's list-view requiredPermissions — no client gate #215

Workflow file for this run

name: Repo Gates
# ── One workflow, one checkout, one setup (objectui#12214) ──────────────────
#
# This file is the fifteen one-check gate workflows that used to sit beside it,
# folded into ONE job under the two-speed doctrine (objectstack#22998): each of
# them paid its own runner, checkout and Node setup on every pull request, push
# and queue build, to run a check that mostly takes seconds. Nothing was
# weakened in the fold:
#
# - every check runs the same command, on the same events, with no path
# filter, exactly as it did in its own file;
# - every check keeps its name: each step is named `<check> — <step>`, where
# `<check>` is the job name the check used to report under;
# - every gate step runs `if: ${{ !cancelled() }}` (or on the install / build
# it needs), so one red check never hides another -- the job is red when
# any of them is, and the step list says which.
#
# ⚠️ What DID move: the check-run each gate used to produce. None of them was
# in the merge queue's required set (`node scripts/check-required-check-set.mjs`
# reads the live set; when this fold landed the ruleset named
# `Governed Surface Queue Guard`, `Changeset Declaration` and
# `Spec Main Shape Gate`, which is why those three workflows were NOT folded --
# the last has since left the set by objectui#12240's ruling and keeps its own
# workflow for its hourly run on `main`), so the queue is unchanged. The one in-tree reader
# of the old names, `scripts/dependabot-merge-gate.mjs`, now waits for the one
# context this job produces, `Repo Gates`.
#
# ── Why no `paths` and no `paths-ignore`, deliberately ──────────────────────
#
# The conclusion each folded header reached on its own, and which still holds
# for all of them together: the change most likely to trip these gates is a
# markdown-only, docs-only, skills-only or workflow-only change -- precisely the
# shape `ci.yml` and `lint.yml` decide inside their jobs to skip, and the shape
# their `push` triggers filter out. A gate living behind that switch "rebuilds
# the hole it exists to close" (objectui#3448, objectui#3523, objectui#4381).
# Their pin tests under `scripts/__tests__/` fail if a filter is ever added
# here, and each fails too if a second workflow starts running its script --
# one gate, one home, and this file is that home for all of them.
#
# ── Why the order is install-free gates, then install, then build ───────────
#
# Twelve of the gates read the checkout with `node:fs` and nothing else, so
# they run BEFORE `pnpm install`, which is what keeps them inside the
# population `scripts/check-pre-install-import-graph.mjs` derives and holds to
# an import graph free of `node_modules` -- the reason they were cheap enough
# to run unfiltered in the first place. Keep any check added to that group
# install-free.
#
# Three gates judge the PUBLISHED type surface and need built `dist/*.d.ts`:
# the README export check builds every package under `packages/`, and the two
# snippet gates each derive the closure they need with `--build-filter`. One
# build serves all three: the union of `./packages/*` and both derived
# filters. ⛔ It is never an unfiltered `turbo run build` -- the 2026-08-16
# ruling on objectui#4846 (recorded in `published-dist-gate.yml`) rejected a
# per-PR build of the whole workspace -- and a derived filter that names no
# package is refused at the step that derives it.
#
# ⛔ No population count is written in this header, and none may be added
# (objectui#7448): each gate prints its own census in its verdict line, and the
# pin tests that read this header fail if a count of documents, pages or files
# reappears here.
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
# Merge queue (objectui#3523 — see `ci.yml`'s trigger block for the full note
# and the measurements behind it). A required check that does not report on a
# queue build stalls the queue until the ruleset's 60-minute timeout fails it,
# so an unfiltered gate that can become required subscribes here from the
# start. `types:` is named although `checks_requested` is currently the only
# activity type GitHub defines for `merge_group`.
merge_group:
types: [checks_requested]
workflow_dispatch:
concurrency:
group: repo-gates-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
repo-gates:
name: Repo Gates
runs-on: ubuntu-latest
# The three gates that build used to carry 25 minutes each, in parallel;
# here they share one install and one build, after the install-free gates.
timeout-minutes: 30
steps:
# `submodules: true` is carried over from the README export check's own
# checkout, unchanged. The tree declares no submodule today (no
# `.gitmodules`), so it fetches nothing; it is kept so the fold changes
# no gate's inputs, not because any gate here needs it.
- name: Checkout code
uses: actions/checkout@v7
with:
submodules: true
# pnpm is on the path before any gate runs: `Docs Route Eager Closure
# Check` calls its gate through `pnpm check:docs-route-closure`, with no
# install.
- name: Enable Corepack and download the pinned pnpm
run: bash scripts/ci-setup-pnpm.sh
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
# ── The install-free gates ───────────────────────────────────────────
# A single raw U+0000 makes grep and ripgrep classify the whole file as
# binary and print no matching lines at all, so the file drops out of code
# search and out of every grep-based lint — with no error to say so. git
# will not warn either: it decides binary-ness from the first 8000 bytes
# only. Reads `git ls-files`, so no install is required.
- name: Control Byte Scan — Scan tracked text files for raw control bytes
if: ${{ !cancelled() }}
run: node scripts/check-control-bytes.mjs
# Reads `.github/workflows/` off the checkout, so no install is required.
# The gate refuses to report a verdict at all if its own census collapses:
# a scan that stops finding references renders identically to a clean
# repository, which is the one way a check like this lies.
- name: Action Ref Convention — Check action reference spelling against the declared convention
if: ${{ !cancelled() }}
run: node scripts/check-action-ref-convention.mjs
# Two legs, in this order and in one step, for the reason `lint.yml` runs
# the entry-guard gate the same way: a scan whose recogniser is broken
# reports a clean tree. The self-test pins the workflow parser and the
# graph walk against fixtures, so a green scan below means something.
#
# This step is itself in the population the gate derives — it runs a
# `scripts/` file before this job installs anything — so
# the gate walks its own import graph on every run. That is deliberate: a
# floor that exempted its own enforcer would be the first thing to rot.
- name: Pre-Install Import Graph Check — Check the pre-install gates' import graphs
if: ${{ !cancelled() }}
run: |
node scripts/check-pre-install-import-graph.mjs --self-test
node scripts/check-pre-install-import-graph.mjs
# objectui#5150: the `git commit -F -` example in AGENTS.md §9 shipped with
# its heredoc terminator wrapped in the single-quote-inside-single-quote
# shell escape. Copied verbatim it does not fail with a message — it HANGS,
# on a terminator that never matches, and a reader does not attribute a hung
# terminal to the document. objectui#5151 measured the full derived gate
# union against the replanted bytes: control-bytes, doc-links and both
# changeset gates all exited 0, none of them negligently — the residue is
# printable ASCII inside a code block and no scan surface reached it.
#
# ⛔ This gate checks an ENUMERATED LITERAL. It does NOT make fenced shell
# examples executable-by-construction, and nothing in this repository does;
# `bash -n` per block is objectui#5151's unbuilt "direction 1". The script
# header states the boundary and the test suite asserts it as a fact.
#
# GREEN AT REST — there is nothing to find on an ordinary day — so it prints
# its per-root census rather than a bare "OK", and it FAILS when a scan root
# does not resolve or the population collapses. A scan that silently finds
# nothing reads as coverage, which is this gate's own defect one level up.
- name: Shell Escape Residue Scan — Check for machine-produced shell-escape residue in fenced examples
if: ${{ !cancelled() }}
run: node scripts/check-shell-escape-residue.mjs
# A `vi.mock` whose relative specifier resolves to no file does NOT error.
# Vitest registers the mock against a module id nothing imports, the run
# proceeds with the real module everywhere, and the suite passes —
# identically to a correct one, with no warning and no smaller assertion
# count. objectui#5646: the one known instance (PR #5645) passed even with
# the code under test reverted to the shape the suite was written to
# catch, and only an ablation leg exposed it.
#
# This gate is GREEN AT REST — there are zero unresolvable specifiers in
# the tree and there should stay zero — so it prints its census rather
# than a bare "OK", and it FAILS if the population collapses to nothing.
# A scan that silently finds nothing reads as coverage, which is this
# gate's own defect one level up.
- name: Inert vi.mock Specifier Check — Check every relative vi.mock specifier resolves
if: ${{ !cancelled() }}
run: node scripts/check-vi-mock-specifiers.mjs
# The sibling property of the same call sites: a factory that HAND-LISTS
# the exports it returns freezes the mock's export surface. The next
# export any module in the file's import graph reads AT MODULE SCOPE then
# kills the file during COLLECTION -- `Test Files 3 failed | 546 passed`
# with `Tests 6694 passed`, ZERO failed assertions, because the tests in
# those files never ran (objectui#6768). It reads as flake to whoever
# sees it next, and the bill lands on whoever added the export.
#
# The recogniser is SEMANTIC, never a grep for `importOriginal`: that
# spelling mis-counted eleven correct files as broken AND missed one
# broken file entirely (objectui#6849). It asks whether the factory
# OBTAINS the real module -- a callback parameter under any name, or
# `vi.importActual` of the same specifier -- and SPREADS it.
- name: Inert vi.mock Specifier Check — Check every covered vi.mock factory inherits the real export surface
if: ${{ !cancelled() }}
run: node scripts/check-vi-mock-inherit.mjs
# Resolves every relative / `/docs/...` markdown link found in
# `content/docs/**` against the files actually on disk. Reads the checkout
# and nothing else, so no install is required. External URLs are a
# different problem with a different tool — Lychee, in `check-links.yml`,
# which sweeps both `content/docs/` and `docs/` on a weekly cron and on
# demand, and deliberately gates nothing (#3449 fixed its scope; #3213
# decided it stays off pull requests).
- name: Internal Docs Link Check — Check internal docs links
if: ${{ !cancelled() }}
run: node scripts/check-doc-links.mjs
# `check-doc-snippet-types` compiles `ts` / `tsx` / `typescript` fences and
# nothing else, so a TypeScript block fenced any other way is invisible to
# it — objectui#5867, whose remediation lane collected its population from
# ```plaintext fences only. `plaintext` is not the only spelling of an
# unhighlighted fence: objectui#6135 measured a ```text block opening
# `interface FileUploadSchema {` sitting outside the gate AND outside the
# lane that exists to close it, for no reason but how its fence is spelled.
#
# This gate reads block BODIES with objectui#5867's own triage classifier,
# so no list of languages is on the enforcement path and `txt`, `console`,
# `raw` or a bare fence with no info string at all cannot reopen the gap.
# Today's residue is declared, SHRINK-ONLY, and is that lane's remaining
# population per file.
#
# `--self-test` runs FIRST and is the half that stops the gate rotting into
# decoration: it drives the real scanner over fixture sources, including
# the three spellings named on objectui#6135, and pins the shrink-only
# baseline in every direction it can move. A scanner whose recogniser is
# broken reports a clean tree.
- name: Doc Fence Language Check — Check that no fence spelling hides a TypeScript block
if: ${{ !cancelled() }}
run: |
node scripts/check-doc-fence-languages.mjs --self-test
node scripts/check-doc-fence-languages.mjs
# A `<SchemaExample id="…" />` in a docs page resolves through
# `getExample(id)`, which THROWS on an unknown id rather than degrading —
# the honest runtime behaviour, and the reason a mistyped or stale id is a
# crash in the published docs rather than a blank tile. Nothing asked the
# question before render: the catalog's own suite resolves every registry
# ENTRY, which cannot see a page pointing at an id that is not there.
# Reads the checkout and nothing else, so no install.
- name: Doc Example Id Check — Check documented example ids against the catalog registry
if: ${{ !cancelled() }}
run: node scripts/check-doc-example-ids.mjs
# A `type` string in a `content/docs/**` code block is not rendered,
# not parsed and not compared against anything, so a snippet can name a
# component that does not exist and every check in the repo stays green —
# while a reader who copies it gets the renderer's red "Unknown component
# type" panel (OBJUI-001). That defect landed three times before this gate
# (objectui#4786 `stats-card`, objectui#4796 `plugin:grid` and
# `plugin:map`), each found by a human probe. The catalog side has had the
# equivalent ratchet since objectui#4616
# (`examples/schema-catalog/test/catalog-gallery-render.test.tsx`); this is
# the missing half. Reads the checkout and nothing else, so no install.
- name: Doc Component Type Check — Check documented component types against the registry
if: ${{ !cancelled() }}
run: node scripts/check-doc-component-types.mjs
# Same shape, same blind spot, one file further out: `.github/prompts/**`
# is markdown, so `ci.yml`'s type-check diff excludes it under `'**/*.md'`
# and a prompt-only PR starts that gate nowhere. The step above asks "does
# this type exist"; this one asks the stricter question a file read by an
# AI AUTHOR has to survive — "does it render, or is it answered only by the
# opt-in placeholder panel". A retired key passes the first and fails the
# second, which is the whole of objectui#8929. Reads the checkout only.
- name: Doc Component Type Check — Check prompt-taught component keys render
if: ${{ !cancelled() }}
run: node scripts/check-prompt-component-keys.mjs
# The guides under `skills/` are a direct input to every agent that writes
# code here, and their prose gives in-repo paths as coordinates. A dead one
# produces no compile error — just "file not found" from a Read, and a
# wasted lap re-locating a symbol that does exist (#3713 and #3730 were
# 13+ of these in one guide, both rounds found by eye). Reads the checkout
# and nothing else, so no install is required.
- name: Skill Guide Path Check — Check paths stated in the skill guides
if: ${{ !cancelled() }}
run: node scripts/check-skills-paths.mjs
# The self-test runs FIRST. A probe that cannot fail is not a probe: it
# plants an untaught token that must go red, a substring-only match that
# must go red, a `must_not_contain` entry that must NOT, and a baseline row
# that must suppress exactly its own row — so a gate broken into permanent
# green is caught here rather than by nobody.
- name: Skill Eval Token Check — Self-test the oracle and the whole-token rule, both directions
if: ${{ !cancelled() }}
run: node scripts/check-skill-eval-tokens.mjs --self-test
# Reads the checkout and nothing else, so no install is required.
- name: Skill Eval Token Check — Check every eval must_contain token against the guides
if: ${{ !cancelled() }}
run: node scripts/check-skill-eval-tokens.mjs
# Reads source only — no `node_modules`, no `dist`, no `.source`. The three
# halves (declarations, ledger, gauge) are all printed before any of them
# decides the exit code: exit 1 is a verdict about the registrar, exit 2
# says the gauge itself is not trustworthy and is never reported as the
# former.
- name: Docs Route Eager Closure Check — Weigh the docs route eager closure
if: ${{ !cancelled() }}
run: pnpm check:docs-route-closure
# ── The gates that read the built type surface ───────────────────────
- name: Install dependencies
id: install
if: ${{ !cancelled() }}
run: pnpm install --frozen-lockfile
# The filter comes from the gate, so it can never drift from what the
# marked fences import.
#
# ⛔ Do not fold this back into `echo "args=$(node …)" >> "$GITHUB_OUTPUT"`
# (objectui#6221). A command substitution contributes its STDOUT to the
# surrounding word and nothing else — the step's status is `echo`'s — so a
# gate that failed reads as a gate that named no packages, `args` is
# silently empty, and the build that reads it judges a closure nobody
# derived -- with no signal anywhere. Capture the status, then write the output.
- name: Skill Example Check — Derive the packages the marked examples import
id: skill-filter
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
run: |
status=0
args="$(node scripts/check-skill-examples.mjs --build-filter)" || status=$?
if [ "$status" -ne 0 ]; then
echo "::error::Could not derive the build filter: \`node scripts/check-skill-examples.mjs --build-filter\` exited $status. Refusing to continue — carrying on would build every package in the workspace instead of the ones the marked examples import." >&2
exit "$status"
fi
case "$args" in
*--filter=*) ;;
*)
echo "::error::The derived build filter names no package (got: '$args'). Refusing to continue -- see this workflow's header." >&2
exit 1
;;
esac
echo "args=$args" >> "$GITHUB_OUTPUT"
# The filter comes from the gate, so it can never drift from what the
# covered documents import. A package the snippets need but nothing built
# is reported by the gate as `unbuilt-package` — its own failure reason,
# never as a page full of broken imports.
#
# ⛔ Do not fold this back into `echo "args=$(node …)" >> "$GITHUB_OUTPUT"`
# (objectui#6221). A command substitution contributes its STDOUT to the
# surrounding word and nothing else — the step's status is `echo`'s — so a
# gate that failed reads as a gate that named no packages, `args` is
# silently empty, and the build that reads it judges a closure nobody
# derived -- with no signal anywhere. `set -o pipefail` is not the remedy and would
# not help; there is no pipe here. Capture the status, then write the
# output.
- name: Doc Snippet Type Check — Derive the packages the covered snippets import
id: snippet-filter
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
run: |
status=0
args="$(node scripts/check-doc-snippet-types.mjs --build-filter)" || status=$?
if [ "$status" -ne 0 ]; then
echo "::error::Could not derive the build filter: \`node scripts/check-doc-snippet-types.mjs --build-filter\` exited $status. Refusing to continue — carrying on would build every package in the workspace instead of the ones the covered snippets import." >&2
exit "$status"
fi
case "$args" in
*--filter=*) ;;
*)
echo "::error::The derived build filter names no package (got: '$args'). Refusing to continue -- see this workflow's header." >&2
exit 1
;;
esac
echo "args=$args" >> "$GITHUB_OUTPUT"
# One build for the three gates below: every package under `packages/`
# (the README export check reads each package's declared type entry), in
# union with the two closures the snippet gates derived above, so a
# derived filter that ever names a package outside `packages/` is still
# built. The derived filters are LISTS of `--filter=` words and must
# word-split, so they stay unquoted. A derive step that failed contributes
# nothing here and is red on its own; its gate then reports `unbuilt`
# rather than judging a tree it cannot see.
- name: Build every package the gates below read
id: build
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
env:
SKILL_FILTER: ${{ steps.skill-filter.outputs.args }}
SNIPPET_FILTER: ${{ steps.snippet-filter.outputs.args }}
run: pnpm exec turbo run build --filter='./packages/*' $SKILL_FILTER $SNIPPET_FILTER
# The self-test runs BEFORE the corpus, and after the build because its
# compiler legs need the same built tree. A probe that cannot fail is not a
# probe: it plants a fence that must go red and a marker that must be
# reported, so a harness broken into permanent green is caught here rather
# than by nobody.
- name: Skill Example Check — Self-test the marker convention, both directions
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node scripts/check-skill-examples.mjs --self-test
- name: Skill Example Check — Check the marked skill examples against the built types
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node scripts/check-skill-examples.mjs
# REPORT-ONLY (objectui#7864). Code a generator EMITS from a template
# literal under `packages/*/src/**` is compiled by nothing: `tsc` sees a
# string, `tsup` copies it through, and this gate's own scan surface stops
# at the authored pages — `content/docs`, the per-app docs trees, the
# package READMEs, the root `README.md` (objectui#7115), the top level of
# the root `docs/` tree (objectui#7856 card 1) and, recursively,
# `docs/adr/**` and `docs/audits/**` (objectui#7856 card 2, which brought
# them in LEDGER-FIRST: they are records, so their blocks are declared on
# `UNGATED_DOCS` rather than compiled). This
# step censuses that class through the same `compileSnippets()` the doc
# blocks go through, against the closure the step above just built.
#
# It runs BEFORE the blocking gate on purpose. The census is meant to be
# re-read on every run, and placed after it the number would be skipped on
# exactly the runs where the corpus moved. It exits 0 whatever it finds —
# one known member (objectui#7472) is an OPEN card — and non-zero only when
# the instrument itself is broken: a walk that collapsed, or a failed
# harness control. A check that runs, goes green and looked at nothing is
# the counterfeit the gate's own header is built against.
- name: Doc Snippet Type Check — Census the code emitted from template literals (report-only)
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node scripts/check-doc-snippet-types.mjs --emit-census
- name: Doc Snippet Type Check — Compile documentation snippets against the built types
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node scripts/check-doc-snippet-types.mjs
# objectui#8757. Same corpus discipline as the gate above: a block that
# cannot compile is either fixed or DECLARED in the script's
# `UNGATED_EXAMPLES` ledger with a written reason — ⛔ never made green by
# narrowing what the gate scans.
- name: Doc Snippet Type Check — Compile JSDoc `@example` blocks against the built types
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node scripts/check-doc-example-types.mjs
# objectui#5250 (the maintainer ruling's JSON-fence slice). Every `json` /
# `jsonc` node document in `content/docs` is judged through the STRICT
# authoring face — `StrictAnyComponentSchema`, read from the
# `@object-ui/types` the build step above produced (the snippet gate's
# closure carries it) — the face `objectui validate` judges with. A fence
# that is deliberately not a complete node document is declared on the
# line above it, and the declaration is re-judged on every run. Last of
# the three, for the reason the step above gives: a newly wired blocking
# gate must not mask an established one.
- name: Doc Snippet Type Check — Judge JSON doc fences through the strict authoring face
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node scripts/check-doc-json-fences.mjs
# A README in `packages/<pkg>/` teaching `import { X } from
# '@object-ui/<pkg>'` for an `X` the package does not export ships in the
# npm tarball and gives the reader TS2305 or a TypeError. One manual sweep
# (objectui#5043) found it in seven packages and recorded that as a LOWER
# bound, because the method could only see single-line imports.
#
# GREEN AT REST — zero drift in the tree when this landed and there should
# stay zero — so it prints its census rather than a bare "OK", and FAILS if
# the population collapses to nothing.
- name: README Export Check — Check every README self-import against the real export surface
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: pnpm check:readme-exports