Repository navigation
feat(types): NamedListView mirrors spec 18's list-view requiredPermissions — no client gate #215
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |