From f9aed274c1d53a56981a0ea6392f498d938150f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 20:53:49 +0000 Subject: [PATCH 1/3] feat(tooling): pin the fence vocabulary so no spelling hides a TypeScript block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 collects 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. Adds `scripts/check-doc-fence-languages.mjs`, which reads block BODIES with #5867's own binding triage classifier rather than pinning a list of languages — so `txt`, `console`, `raw` or a bare fence with no info string at all cannot reopen the gap. Two failure modes are reported apart, because only the first can be auto-classified: a KNOWN unhighlighted spelling (#5867's population, mechanical remedy, baselineable) and a spelling nobody has thought of (a human's call, never baselined; zero in the tree today). `KNOWN_UNHIGHLIGHTED_TS_FENCES` is #5867's remaining population per file — 92 files, 128 blocks — shrink-only in the shape objectui#6133 landed for `KNOWN_HAND_TYPED_GUARDS`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- .github/workflows/doc-fence-languages.yml | 85 +++ content/docs/guide/ci-cd-pipeline.md | 49 ++ package.json | 1 + .../check-doc-fence-languages.test.ts | 141 ++++ scripts/check-doc-fence-languages.mjs | 602 ++++++++++++++++++ 5 files changed, 878 insertions(+) create mode 100644 .github/workflows/doc-fence-languages.yml create mode 100644 scripts/__tests__/check-doc-fence-languages.test.ts create mode 100644 scripts/check-doc-fence-languages.mjs diff --git a/.github/workflows/doc-fence-languages.yml b/.github/workflows/doc-fence-languages.yml new file mode 100644 index 0000000000..052e411739 --- /dev/null +++ b/.github/workflows/doc-fence-languages.yml @@ -0,0 +1,85 @@ +name: Doc Fence Languages + +# Why this is its own workflow rather than a step in `ci.yml` or `lint.yml`: the +# defect it exists for arrives in a DOCS-ONLY pull request, and that is exactly +# the shape both of those workflows skip — their "does this change need a full +# run" diff excludes `content/**` and `'**/*.md'`, which is the entire surface +# this gate reads. `doc-component-types.yml`'s header records the reasoning in +# full; this is the sixth instance of the same shape, and `control-bytes.yml`'s +# header names the consequence of getting it wrong: a gate that cannot see a +# markdown-only change "rebuilds the hole it exists to close". +# +# Hence: no `paths` and no `paths-ignore` here, deliberately. +# `scripts/__tests__/check-doc-fence-languages.test.ts` fails if either is ever +# added, and fails too if a second workflow starts running the same script — one +# gate, one home. +# +# It needs no install and no build. The script reads the checkout with `node:fs` +# only — the same 222 documents `check-doc-snippet-types` covers — and it +# deliberately re-implements that gate's document walk rather than importing it, +# because that gate imports `typescript` and an install-gated docs check is one +# a docs-only pull request skips. The copy is held to the original by the pin +# test above, which imports BOTH walks and compares them. Keep this job +# install-free: the moment it needs `pnpm install` it stops being cheap enough +# to run unfiltered, and the filter is the hole. + +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 could 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: doc-fence-languages-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + doc-fence-languages: + name: Doc Fence Language Check + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: '22.x' + + # `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: Check that no fence spelling hides a TypeScript block + run: | + node scripts/check-doc-fence-languages.mjs --self-test + node scripts/check-doc-fence-languages.mjs diff --git a/content/docs/guide/ci-cd-pipeline.md b/content/docs/guide/ci-cd-pipeline.md index 179d166db1..0f5ce042d4 100644 --- a/content/docs/guide/ci-cd-pipeline.md +++ b/content/docs/guide/ci-cd-pipeline.md @@ -32,6 +32,7 @@ one has its own section below. | `skills-paths.yml` | Skill Guide Path Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a path stated in a `skills/` guide does not exist | | `doc-component-types.yml` | Doc Component Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a `content/docs/**.mdx` snippet teaches a `type` nothing registers | | `doc-snippet-types.yml` | Doc Snippet Type Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a covered documentation snippet no longer compiles against the packages' built types | +| `doc-fence-languages.yml` | Doc Fence Language Check | Push / PR to `main`, `develop` — **no path filter**; merge-queue builds; manual | **Yes** — when a TypeScript block sits under a fence the snippet gate does not read | | `performance-budget.yml` | Bundle Analysis | Push / PR touching `packages/**`, `apps/console/**`, `pnpm-lock.yaml` | **Yes** — the console entry gzip budget | | `live-e2e.yml` | Live E2E (informational) | PR to `main`, `develop` (code paths); nightly cron `30 6 * * *`; manual | No — informational lane, `continue-on-error` | | `labeler.yml` | Auto Label PRs | PR `opened`, `synchronize`, `reopened` | No | @@ -673,6 +674,54 @@ at the harness. Either fix what the snippet teaches, or — if the block is genu it with a reason. Run it locally with `pnpm check:doc-snippets` (after building the packages it names: `pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter)`). +## Fence Languages (`doc-fence-languages.yml`) + +**Triggers:** Push and PR to `main`/`develop`, merge-queue builds, plus manual dispatch — **no path +filter**, for the same reason as the two sections above. It appears in the checks list as **Doc Fence +Language Check**. + +Runs `scripts/check-doc-fence-languages.mjs`. It answers the question the gate above cannot ask about +itself: *is every TypeScript block in the documentation actually fenced as TypeScript?* +`check-doc-snippet-types` reads `ts` / `tsx` / `typescript` fences and nothing else, so a TypeScript +block fenced any other way is invisible to it — +[#5867](https://github.com/objectstack-ai/objectui/issues/5867), whose remediation lane collected its +population from ```plaintext fences only. + +**`plaintext` is not the only spelling of an unhighlighted fence.** +[#6135](https://github.com/objectstack-ai/objectui/issues/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. Widening the lane's derivation once would fix that +block; it would not stop a sixth spelling reopening the identical gap. + +**So it reads bodies, not a list of languages.** No enumeration of allowed fence languages is on the +enforcement path — an enumeration is the thing that rots, and it rots silently. Every fence's body is +put to #5867's own binding triage classifier (*a block whose first line starts with `import` / +`export` / `interface` / `type X =` / `const x: T` is code*), quoted rather than extended. `txt`, +`console`, `raw`, or a bare fence with no info string at all therefore cannot hide a block. + +**Two failure modes, because only one can be auto-classified.** A *known* spelling of an +unhighlighted fence (`plaintext`, `text`, `plain`, `txt`, no info string) is #5867's population and +its remedy is mechanical, so it is the only mode the baseline describes. Any *other* spelling might +be a sixth synonym or a real highlighter language — that is a human's call, so it is reported +separately and can **never** be baselined. + +**The baseline is #5867's remaining population.** `KNOWN_UNHIGHLIGHTED_TS_FENCES` maps a path to the +number of hidden blocks it carries, ⛔ **shrink-only** in the shape +[#6133](https://github.com/objectstack-ai/objectui/issues/6133) landed for +`KNOWN_HAND_TYPED_GUARDS`: a file not in the map that carries one fails, a file carrying more than +its number fails, and a file carrying fewer fails as *stale* and names itself. Every #5867 batch now +lowers these numbers in the same pull request that re-fences the blocks, so the lane's arithmetic +lives in the repository instead of being re-derived by hand in each handback. + +**`--self-test` runs first.** It drives the real scanner over fixture sources — including a +`text`-fenced, a `txt`-fenced and an info-string-less TypeScript block — and pins the shrink-only +baseline in every direction it can move. A scanner whose recogniser is broken reports a clean tree, +which is why the probe runs before the verdict. + +**If it fails:** each line is `file:line ````. Re-fence the +block ```ts (or ```tsx) and fix whatever `check-doc-snippets` then reports, then lower the file's +number. Run it locally with `pnpm check:doc-fences`; it needs no install and no build. + ## Link Checking (`check-links.yml`) **Trigger:** Weekly cron (`17 4 * * 0` — Sundays, off the top of the hour, when the scheduled-run diff --git a/package.json b/package.json index a3c86872dc..ec7439ee77 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "check:skills-paths": "node scripts/check-skills-paths.mjs", "check:doc-types": "node scripts/check-doc-component-types.mjs", "check:doc-snippets": "node scripts/check-doc-snippet-types.mjs", + "check:doc-fences": "node scripts/check-doc-fence-languages.mjs", "check:eager-closure": "node scripts/check-eager-closure-budget.mjs", "check:entry-guard": "node scripts/check-entry-guard.mjs", "cli": "node packages/cli/dist/cli.js", diff --git a/scripts/__tests__/check-doc-fence-languages.test.ts b/scripts/__tests__/check-doc-fence-languages.test.ts new file mode 100644 index 0000000000..1fdd8bfbf5 --- /dev/null +++ b/scripts/__tests__/check-doc-fence-languages.test.ts @@ -0,0 +1,141 @@ +import { describe, expect, it } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse as parseYaml } from 'yaml'; + +import { census, listDocuments as fenceDocuments, TS_FENCE_LANGUAGES as GUARD_TS_FENCES } from '../check-doc-fence-languages.mjs'; +import { listDocuments as snippetDocuments, TS_FENCE_LANGUAGES as GATE_TS_FENCES } from '../check-doc-snippet-types.mjs'; + +const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); +const GUARD = 'scripts/check-doc-fence-languages.mjs'; +const WORKFLOW = 'doc-fence-languages.yml'; + +/** + * objectui#6135. `check-doc-snippet-types` reads `ts` / `tsx` / `typescript` + * fences, so a TypeScript block fenced any other way is invisible to it, and + * objectui#5867's remediation lane collected its population from `plaintext` + * fences only — one spelling of an unhighlighted fence out of several. A + * ```text block opening `interface FileUploadSchema {` was outside both. + * + * `check-doc-fence-languages.mjs` closes that by reading BODIES rather than + * pinning a list of languages. This file pins the two things its correctness + * rests on and cannot check about itself: + * + * 1. **The scan surface really is the gate's.** The guard re-implements + * `listDocuments` so it needs no `pnpm install` — the gate imports + * `typescript`, and an install-gated docs check is one a docs-only pull + * request skips, which is the hole `doc-component-types.yml`'s header + * records. A copy that is never compared is a copy free to drift, and the + * drift direction is silent: a document the guard stops walking is a + * document nothing reports on. So both walks are imported and compared. + * 2. **The wiring.** A gate nobody runs is indistinguishable from a gate that + * passes (`entry-guard-wiring.test.ts` records that lesson for its own + * subject). The alias, the workflow, the self-test leg and the + * unfiltered triggers are asserted here. + * + * Deliberately NOT asserted: the size of the baseline or the number of blocks it + * carries. Those move with every objectui#5867 batch, and a hand-copied + * enumeration in a test drifts by construction — the lesson `lint-workflow.test.ts` + * records for this repository at length. The guard's own output is the honest + * place for those numbers. + */ +describe('check-doc-fence-languages: the scan surface is check-doc-snippet-types’s', () => { + it('walks exactly the documents the snippet gate walks', () => { + expect(fenceDocuments(ROOT)).toEqual(snippetDocuments(ROOT)); + }); + + it('…and that is a non-empty set, so the comparison is not vacuous', () => { + expect(fenceDocuments(ROOT).length).toBeGreaterThan(100); + }); + + it('treats exactly the snippet gate’s fence languages as already-covered', () => { + expect([...GUARD_TS_FENCES].sort()).toEqual([...GATE_TS_FENCES].sort()); + }); +}); + +describe('check-doc-fence-languages: non-vacuity, through the shipped module', () => { + const TS_BODY = 'interface FileUploadSchema {\n accept?: string;\n}'; + const doc = (info: string, body: string = TS_BODY) => [ + { rel: 'probe.mdx', source: ['```' + info, body, '```'].join('\n') }, + ]; + const modes = (info: string, body?: string) => + census(doc(info, body)).sites.map((s: { mode: string }) => s.mode); + + // The three spellings the 2026-08-24 ruling on objectui#6135 named by hand. + it.each(['text', 'txt', ''])('a TypeScript block fenced %o is found', (info) => { + expect(modes(info)).toEqual(['synonym']); + }); + + it('names the file and the fence line, rather than only counting', () => { + const [site] = census(doc('text')).sites; + expect(site).toMatchObject({ rel: 'probe.mdx', line: 1, language: 'text', mode: 'synonym' }); + }); + + it.each(['ts', 'tsx', 'typescript'])('the same block fenced %o is not a finding', (info) => { + expect(modes(info)).toEqual([]); + }); + + it('a spelling nobody has thought of is the OTHER failure mode', () => { + expect(modes('console')).toEqual(['unknown']); + }); + + it('prose under an unhighlighted fence is not a finding — the classifier is quoted, not widened', () => { + expect(modes('plaintext', 'Upload a file, then press Save.')).toEqual([]); + }); +}); + +describe('check-doc-fence-languages is wired, not merely present', () => { + const workflow = parseYaml(fs.readFileSync(path.join(ROOT, '.github/workflows', WORKFLOW), 'utf8')); + const steps: Array> = workflow.jobs['doc-fence-languages'].steps; + const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(GUARD)); + + it('the guard script exists', () => { + expect(fs.existsSync(path.join(ROOT, GUARD))).toBe(true); + }); + + it('package.json aliases it, and the alias points at the script that exists', () => { + const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); + expect(pkg.scripts['check:doc-fences']).toContain(GUARD); + }); + + it('the workflow runs it — one step, both legs', () => { + expect(gateSteps).toHaveLength(1); + const run = gateSteps[0].run as string; + expect(run).toContain(`node ${GUARD} --self-test`); + expect(run.split('\n').some((l) => l.trim() === `node ${GUARD}`)).toBe(true); + }); + + it('one gate, one home — no other workflow runs the same script', () => { + const dir = path.join(ROOT, '.github/workflows'); + const others = fs + .readdirSync(dir) + .filter((f) => f.endsWith('.yml') && f !== WORKFLOW) + .filter((f) => fs.readFileSync(path.join(dir, f), 'utf8').includes(GUARD)); + expect(others).toEqual([]); + }); + + it('has no paths filter — the defect it catches arrives in a docs-only pull request', () => { + // `on` parses as the boolean `true` in YAML 1.1; `yaml` gives back `on`. + const on = workflow.on ?? workflow[true as unknown as string]; + expect(Object.keys(on)).toContain('pull_request'); + expect(on.pull_request).not.toHaveProperty('paths'); + expect(on.pull_request).not.toHaveProperty('paths-ignore'); + }); + + it('subscribes to the merge queue, so it cannot stall one if it becomes required', () => { + const on = workflow.on ?? workflow[true as unknown as string]; + expect(Object.keys(on)).toContain('merge_group'); + }); + + it('needs no install — nothing in the job runs pnpm', () => { + const runs = steps.map((s) => (typeof s.run === 'string' ? s.run : '')).join('\n'); + expect(runs).not.toContain('pnpm'); + }); + + it('its self-test passes — the half that makes a green scan mean something', () => { + const out = execFileSync('node', [GUARD, '--self-test'], { cwd: ROOT, encoding: 'utf8' }); + expect(out).toMatch(/check-doc-fence-languages self-test: \d+ cases pass/); + }); +}); diff --git a/scripts/check-doc-fence-languages.mjs b/scripts/check-doc-fence-languages.mjs new file mode 100644 index 0000000000..6910352a1f --- /dev/null +++ b/scripts/check-doc-fence-languages.mjs @@ -0,0 +1,602 @@ +#!/usr/bin/env node +/** + * A fenced block whose BODY is TypeScript must be fenced `ts` / `tsx` / + * `typescript`. No spelling of an unhighlighted fence may hide one from + * `check-doc-snippet-types`. + * + * Run: node scripts/check-doc-fence-languages.mjs (also `pnpm check:doc-fences`) + * node scripts/check-doc-fence-languages.mjs --self-test + * node scripts/check-doc-fence-languages.mjs --list + * Exit: 0 = no TypeScript block sits under a non-TypeScript fence beyond the + * declared, shrink-only debt below. 1 = one does, or the debt list is + * stale. + * + * ## The hole this closes (objectui#6135, measured, not predicted) + * + * `check-doc-snippet-types` compiles `ts` / `tsx` / `typescript` fences and + * nothing else, so a TypeScript block fenced any other way is invisible to it — + * that is objectui#5867, and its remediation lane collects the population by + * walking ```plaintext fences. + * + * ⚠️ `plaintext` is not the only spelling of an unhighlighted fence, and until + * this file landed nothing pinned the vocabulary. Re-running objectui#5867's own + * derivation on `origin/main` `bfdb9f906` with the fence-language set widened by + * `text` and `plain`, and nothing else changed, moved the population by exactly + * one block: + * + * plaintext 127 blocks / 91 files + * plaintext + text + plain 128 blocks / 92 files + * …+ txt + no info string at all 128 blocks / 92 files + * + * The block the wider set adds is `content/docs/components/form/file-upload.mdx:27`, + * a ```text fence opening `interface FileUploadSchema {`. It had been outside the + * lane's population — and outside the gate — the whole time, for no reason other + * than how its fence is spelled. + * + * Widening the lane's derivation once fixes that one block. It does NOT stop a + * sixth spelling reopening the identical gap tomorrow, and the 2026-08-24 ruling + * on objectui#6135 (route "A then C") is explicit that the durable half is the + * point: "A alone chases spellings … Fixing the lane's arithmetic without pinning + * the vocabulary means this card gets refiled under a different fence name." + * + * ## Why this reads BODIES rather than pinning a list of languages + * + * The obvious shape — enumerate the allowed fence languages and ban the rest — + * was measured and rejected here, because the enumeration IS the thing that + * rots. It has to be extended every time a page picks up a new highlighter + * language, each extension is a place to get it wrong, and the failure direction + * is silent: a spelling nobody added is a spelling nobody notices. + * + * Reading the body removes the list from the load-bearing path entirely. The + * question asked of every fence is triage's, not this file's: + * + * a block whose first line starts with `import` / `export` / `interface` / + * `type X =` / `const x: T` IS code + * + * — objectui#5867's binding triage ruling, 2026-08-24. ⛔ That classifier is + * quoted here, not extended: this gate widens WHICH FENCES are examined, never + * what counts as code. Those are two different edges and only the second was + * ruled. A block that fails the classifier is prose and this gate says nothing + * about it, whatever its fence says. + * + * The consequence is the property the ruling asked for: `txt`, `console`, `raw`, + * `output`, a bare ``` with no info string at all — none of them is named + * anywhere in the enforcement path, and every one of them fails the moment it + * carries a TypeScript body. + * + * ## The two failure modes, because only one of them can be auto-classified + * + * SYNONYM the fence is a KNOWN spelling of an unhighlighted block — + * `plaintext`, `text`, `plain`, `txt`, or no info string at all + * (`UNHIGHLIGHTED_SPELLINGS`). The gate knows exactly what this is: + * objectui#5867's population, one block of it. The remedy is + * mechanical — re-fence it `ts` or `tsx` — so it is the only mode a + * baseline entry can describe. + * + * UNKNOWN the fence names something else — a spelling nobody has thought of. + * The gate CANNOT auto-classify it: `raw` might be a sixth synonym of + * an unhighlighted fence, or a real highlighter language whose block + * happens to open with `import`. Deciding which is a human's call, so + * this mode ⛔ can never be baselined and fails on sight. There are + * ZERO of them in the tree today, which is what makes "never + * baselined" affordable rather than aspirational. + * + * `UNHIGHLIGHTED_SPELLINGS` therefore chooses a MESSAGE, never a verdict. Adding + * a spelling to it moves a finding from UNKNOWN to SYNONYM; it does not excuse + * the block, because SYNONYM findings still have to be inside a shrink-only + * baseline that no supported route adds to. There is no edit to this file that + * makes a new TypeScript-under-a-non-TypeScript-fence block pass. + * + * ## The baseline, and why it is the lane's population rather than a permit + * + * ⛔ SHRINK-ONLY, in the shape objectui#6133 landed for + * `KNOWN_HAND_TYPED_GUARDS`: `path -> number of SYNONYM blocks the file carried + * when this gate landed`. A count rather than a bare path, for that card's third + * reason, which is the one that matters — a path-only baseline silently accepts a + * SECOND hidden block being smuggled into an already-owed file. + * + * • a file NOT in the map that carries one fails — a new hidden block cannot land; + * • a file IN the map carrying MORE than its number fails; + * • a file carrying FEWER fails as STALE and names itself, the remedy being to + * lower or delete the line. No supported route raises a number. + * + * The map is not a debt list this gate invented. It IS objectui#5867's remaining + * population, per file, machine-readable and in the repository — 92 entries and + * 128 blocks at the commit this landed on, reconciling exactly with that card's + * own derivation. Every batch of that lane now lowers these numbers in the same + * pull request that re-fences the blocks, which is what stops the lane's + * arithmetic from being a figure re-derived by hand in each handback and trusted + * by the next dispatch. When the last entry goes, so does the map, and the rule + * above stands alone. + * + * ## What it reads, and what it deliberately does not + * + * The scan surface is `check-doc-snippet-types`'s, exactly: every `.mdx` and + * `.md` under `content/docs`, plus every `packages//README.md`. It is + * re-implemented here rather than imported so this gate needs NO install — that + * gate imports `typescript`, and an install-gated docs check is one that a + * docs-only pull request skips, which is the shape objectui#5174 and + * `doc-component-types.yml`'s header both record as the hole. The copy is not + * left to drift: `scripts/__tests__/check-doc-fence-languages.test.ts` imports + * BOTH walks and fails if they ever return different document lists, and pins + * this file's TypeScript-fence set against the gate's own `TS_FENCE_LANGUAGES`. + * + * ⛔ It compiles nothing. Whether a block that reaches a `ts` fence then passes + * `--strict` is `check-doc-snippet-types`'s question; this gate only makes sure + * the block is asked. + */ + +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import { dirname, join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { isEntrypoint } from './invoked-as.mjs'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); + +// ── The scan surface — kept identical to check-doc-snippet-types by test ───── + +const DOCS_ROOT = 'content/docs'; +const PACKAGES_DIR = 'packages'; +const DOC_EXTENSIONS = ['.mdx', '.md']; + +/** Every document in the scan set, in a stable order. */ +export function listDocuments(root = repoRoot) { + const out = []; + const walk = (dir) => { + for (const entry of readdirSync(dir).sort()) { + const p = join(dir, entry); + if (statSync(p).isDirectory()) walk(p); + else if (DOC_EXTENSIONS.some((ext) => entry.endsWith(ext))) out.push(relative(root, p).split(sep).join('/')); + } + }; + const docsRoot = join(root, DOCS_ROOT); + if (existsSync(docsRoot)) walk(docsRoot); + const pkgDir = join(root, PACKAGES_DIR); + if (existsSync(pkgDir)) { + for (const entry of readdirSync(pkgDir).sort()) { + const readme = join(pkgDir, entry, 'README.md'); + if (existsSync(readme)) out.push(relative(root, readme).split(sep).join('/')); + } + } + return out; +} + +// ── The vocabulary: one set gates, one set only labels ─────────────────────── + +/** Fences `check-doc-snippet-types` already compiles. Pinned against that gate's + * own export by the test, so a change there cannot leave this one behind. */ +export const TS_FENCE_LANGUAGES = new Set(['ts', 'tsx', 'typescript']); + +/** + * Known spellings of an UNHIGHLIGHTED fence, including the empty info string. + * This set picks the failure MODE and its remedy text; it never decides whether + * a block fails. See the header: adding a spelling here moves a finding from + * UNKNOWN to SYNONYM and nothing else. + */ +export const UNHIGHLIGHTED_SPELLINGS = new Set(['plaintext', 'text', 'plain', 'txt', '']); + +// ── Triage's classifier, quoted rather than extended ───────────────────────── + +/** + * objectui#5867's binding triage ruling (2026-08-24): a block whose FIRST LINE + * starts with `import` / `export` / `interface` / `type X =` / `const x: T` is + * code. + * + * "First line" is read literally — line 1 of the body, no leading whitespace + * tolerated — and that is a measurement rather than a preference. A variant that + * trims first agrees with this one on all 127 `plaintext` blocks and on the + * `text` one, and disagrees on exactly one block in the tree: + * `content/docs/guide/architecture-overview.md:123`, an ASCII-art plugin + * lifecycle diagram whose first line is an INDENTED `import 'plugin-kanban'` + * inside a box drawing. It is not TypeScript, and the strict reading is the one + * that says so. + */ +export function classifiesAsTypeScript(body) { + const line = body.split('\n')[0]; + return ( + /^import\b/.test(line) || + /^export\b/.test(line) || + /^interface\s/.test(line) || + /^type\s+[A-Za-z_$][\w$]*\s*(<[^=]*>)?\s*=/.test(line) || + /^const\s+[A-Za-z_$][\w$]*\s*:/.test(line) + ); +} + +// ── Fence scanning ─────────────────────────────────────────────────────────── + +/** + * EVERY fenced block in one document, with its info-string language. Fences are + * matched by their own run length — the same walk `check-doc-snippet-types` + * uses — so a ```` ```` ```` wrapper containing ``` does not confuse it. This + * one filters nothing: the whole point is to see the fences that gate cannot. + */ +export function scanFences(source) { + const lines = source.split('\n'); + const blocks = []; + for (let i = 0; i < lines.length; i++) { + const open = /^([ \t]*)(`{3,})(.*)$/.exec(lines[i]); + if (!open) continue; + const ticks = open[2]; + let close = lines.length; + for (let j = i + 1; j < lines.length; j++) { + const c = /^[ \t]*(`{3,})[ \t]*$/.exec(lines[j]); + if (c && c[1].length >= ticks.length) { + close = j; + break; + } + } + const info = open[3].trim(); + blocks.push({ + fenceLine: i + 1, + language: (info.split(/\s+/)[0] || '').toLowerCase(), + body: lines.slice(i + 1, close).join('\n'), + }); + i = close; + } + return blocks; +} + +/** `null` when the block is nothing to this gate; otherwise its failure mode. */ +export function classifyFence(block) { + if (TS_FENCE_LANGUAGES.has(block.language)) return null; + if (!classifiesAsTypeScript(block.body)) return null; + return UNHIGHLIGHTED_SPELLINGS.has(block.language) ? 'synonym' : 'unknown'; +} + +/** + * Every hidden TypeScript block across a set of `{ rel, source }` documents. + * Pure, so the self-test drives it over fixture sources rather than the tree. + */ +export function census(documents) { + const sites = []; + for (const { rel, source } of documents) { + for (const block of scanFences(source)) { + const mode = classifyFence(block); + if (!mode) continue; + sites.push({ + rel, + line: block.fenceLine, + language: block.language, + mode, + head: (block.body.split('\n')[0] || '').trim().slice(0, 72), + }); + } + } + const observed = new Map(); + for (const s of sites.filter((s) => s.mode === 'synonym')) observed.set(s.rel, (observed.get(s.rel) ?? 0) + 1); + return { sites, observed, unknown: sites.filter((s) => s.mode === 'unknown') }; +} + +// ── The debt: objectui#5867's remaining population, per file ───────────────── + +/** + * ⛔ SHRINK-ONLY. `path -> number of SYNONYM-mode blocks the file carried when + * this gate landed` — objectui#5867's remaining population, per file, at + * `bfdb9f906`: 92 files, 128 blocks. The rationale, and why a COUNT rather than + * a bare path, is in the header; the shape is objectui#6133's. + * + * The remedy for every line is the same, which is the property that makes a debt + * list safe: re-fence the block ```ts (or ```tsx), fix whatever + * `check-doc-snippet-types` then reports, and lower the number here — deleting + * the line when it reaches 0. That is objectui#5867's batches, and no entry + * records a judgement anyone has to re-make. + * + * ⚠️ ONE entry is different in kind and is labelled rather than left to be + * re-derived. `content/docs/components/form/file-upload.mdx` is the block this + * card surfaced, and it is NOT merely blocked — objectui#6138 measured that a + * block declaring its OWN `interface` and then being checked against it compiles + * VACUOUSLY, and `file-upload.mdx:27` opens `interface FileUploadSchema {`. Its + * line comes down when objectui#6138 rules on what such a block should be, not + * when someone re-fences it; re-fencing it today would add a block that reports + * green while checking nothing. + * + * @type {Map} + */ +export const KNOWN_UNHIGHLIGHTED_TS_FENCES = new Map([ + ['content/docs/blocks/block-schema.mdx', 10], + ['content/docs/components/basic/button-group.mdx', 1], + ['content/docs/components/basic/html.mdx', 1], + ['content/docs/components/basic/icon.mdx', 1], + ['content/docs/components/basic/image.mdx', 1], + ['content/docs/components/basic/navigation-menu.mdx', 1], + ['content/docs/components/basic/pagination.mdx', 1], + ['content/docs/components/basic/separator.mdx', 1], + ['content/docs/components/basic/sidebar.mdx', 1], + ['content/docs/components/basic/text.mdx', 1], + ['content/docs/components/complex/carousel.mdx', 1], + ['content/docs/components/complex/data-table.mdx', 1], + ['content/docs/components/complex/filter-builder.mdx', 1], + ['content/docs/components/complex/filter-ui.mdx', 1], + ['content/docs/components/complex/resizable.mdx', 1], + ['content/docs/components/complex/scroll-area.mdx', 1], + ['content/docs/components/complex/sort-ui.mdx', 1], + ['content/docs/components/complex/table.mdx', 1], + ['content/docs/components/complex/view-switcher.mdx', 1], + ['content/docs/components/data-display/alert.mdx', 1], + ['content/docs/components/data-display/avatar.mdx', 1], + ['content/docs/components/data-display/badge.mdx', 1], + ['content/docs/components/data-display/breadcrumb.mdx', 1], + ['content/docs/components/data-display/kbd.mdx', 1], + ['content/docs/components/data-display/list.mdx', 1], + ['content/docs/components/data-display/statistic.mdx', 1], + ['content/docs/components/data-display/tree-view.mdx', 1], + ['content/docs/components/disclosure/accordion.mdx', 1], + ['content/docs/components/disclosure/collapsible.mdx', 1], + ['content/docs/components/disclosure/toggle-group.mdx', 1], + ['content/docs/components/feedback/empty.mdx', 1], + ['content/docs/components/feedback/loading.mdx', 1], + ['content/docs/components/feedback/progress.mdx', 1], + ['content/docs/components/feedback/skeleton.mdx', 1], + ['content/docs/components/feedback/sonner.mdx', 1], + ['content/docs/components/feedback/spinner.mdx', 1], + ['content/docs/components/feedback/toast.mdx', 1], + ['content/docs/components/feedback/toaster.mdx', 1], + ['content/docs/components/form/button.mdx', 1], + ['content/docs/components/form/calendar.mdx', 1], + ['content/docs/components/form/checkbox.mdx', 1], + ['content/docs/components/form/combobox.mdx', 1], + ['content/docs/components/form/command.mdx', 1], + ['content/docs/components/form/date-picker.mdx', 1], + ['content/docs/components/form/file-upload.mdx', 1], + ['content/docs/components/form/form.mdx', 1], + ['content/docs/components/form/input-otp.mdx', 1], + ['content/docs/components/form/input.mdx', 1], + ['content/docs/components/form/label.mdx', 1], + ['content/docs/components/form/radio-group.mdx', 1], + ['content/docs/components/form/select.mdx', 1], + ['content/docs/components/form/slider.mdx', 1], + ['content/docs/components/form/switch.mdx', 1], + ['content/docs/components/form/textarea.mdx', 1], + ['content/docs/components/layout/aspect-ratio.mdx', 1], + ['content/docs/components/layout/card.mdx', 1], + ['content/docs/components/layout/container.mdx', 1], + ['content/docs/components/layout/flex.mdx', 1], + ['content/docs/components/layout/grid.mdx', 1], + ['content/docs/components/layout/page.mdx', 1], + ['content/docs/components/layout/stack.mdx', 1], + ['content/docs/components/layout/tabs.mdx', 1], + ['content/docs/components/navigation/header-bar.mdx', 1], + ['content/docs/components/overlay/alert-dialog.mdx', 1], + ['content/docs/components/overlay/context-menu.mdx', 1], + ['content/docs/components/overlay/dialog.mdx', 1], + ['content/docs/components/overlay/drawer.mdx', 1], + ['content/docs/components/overlay/dropdown-menu.mdx', 1], + ['content/docs/components/overlay/hover-card.mdx', 1], + ['content/docs/components/overlay/menubar.mdx', 1], + ['content/docs/components/overlay/popover.mdx', 1], + ['content/docs/components/overlay/sheet.mdx', 1], + ['content/docs/components/overlay/tooltip.mdx', 1], + ['content/docs/core/report-schema.mdx', 11], + ['content/docs/fields/auto-number.mdx', 2], + ['content/docs/fields/object.mdx', 2], + ['content/docs/layout/app-shell.mdx', 4], + ['content/docs/layout/page-header.mdx', 3], + ['content/docs/layout/sidebar-nav.mdx', 5], + ['content/docs/plugins/plugin-calendar.mdx', 1], + ['content/docs/plugins/plugin-charts.mdx', 1], + ['content/docs/plugins/plugin-chatbot.mdx', 2], + ['content/docs/plugins/plugin-dashboard.mdx', 3], + ['content/docs/plugins/plugin-editor.mdx', 1], + ['content/docs/plugins/plugin-form.mdx', 2], + ['content/docs/plugins/plugin-gantt.mdx', 1], + ['content/docs/plugins/plugin-grid.mdx', 2], + ['content/docs/plugins/plugin-kanban.mdx', 1], + ['content/docs/plugins/plugin-map.mdx', 1], + ['content/docs/plugins/plugin-markdown.mdx', 1], + ['content/docs/plugins/plugin-timeline.mdx', 1], + ['content/docs/plugins/plugin-view.mdx', 2], +]); + +/** Split observed counts against the baseline. Both directions are failures. */ +export function reconcile(observed, baseline) { + const fresh = []; + for (const [rel, count] of observed) { + const owed = baseline.get(rel) ?? 0; + if (count > owed) fresh.push({ rel, count, owed }); + } + const stale = []; + for (const [rel, owed] of baseline) { + const count = observed.get(rel) ?? 0; + if (count < owed) stale.push({ rel, count, owed }); + } + return { fresh: fresh.sort((a, b) => (a.rel < b.rel ? -1 : 1)), stale: stale.sort((a, b) => (a.rel < b.rel ? -1 : 1)) }; +} + +// ── Verdict ────────────────────────────────────────────────────────────────── + +const REMEDY_SYNONYM = + `\n That block is TypeScript by objectui#5867's triage classifier, under a` + + `\n fence check-doc-snippet-types does not read. Re-fence it \`\`\`ts (or` + + `\n \`\`\`tsx) and fix what that gate then reports.` + + `\n` + + `\n KNOWN_UNHIGHLIGHTED_TS_FENCES is SHRINK-ONLY: adding a line, or raising` + + `\n a number, is not a supported way to make this pass.`; + +const REMEDY_UNKNOWN = + `\n A TypeScript body under a fence language this gate has never seen. It` + + `\n cannot auto-classify that, so it will not guess — and ⛔ this mode is` + + `\n never baselined. Two remedies, and the choice is yours to state:` + + `\n` + + `\n • the block is TypeScript -> re-fence it \`\`\`ts / \`\`\`tsx;` + + `\n • the fence is another spelling of an UNHIGHLIGHTED block` + + `\n -> add the spelling to` + + `\n UNHIGHLIGHTED_SPELLINGS *and* re-fence` + + `\n the block. Adding the spelling alone` + + `\n changes the message, never the verdict.`; + +export function analyze(documents, baseline = KNOWN_UNHIGHLIGHTED_TS_FENCES) { + const { sites, observed, unknown } = census(documents); + return { sites, observed, unknown, ...reconcile(observed, baseline) }; +} + +function readTree(root) { + return listDocuments(root).map((rel) => ({ rel, source: readFileSync(join(root, rel), 'utf8') })); +} + +function list() { + const { sites } = census(readTree(repoRoot)); + for (const s of sites) console.log(`${s.rel}:${s.line} [${s.language || 'no info string'}] ${s.mode} ${s.head}`); + console.log(`${sites.length} hidden TypeScript block(s).`); + return 0; +} + +function main() { + const documents = readTree(repoRoot); + const { sites, observed, unknown, fresh, stale } = analyze(documents); + const byFile = new Map(); + for (const s of sites) byFile.set(s.rel, [...(byFile.get(s.rel) ?? []), s]); + + if (unknown.length) { + console.error(`❌ check:doc-fences — ${unknown.length} TypeScript block(s) under an UNKNOWN fence language:\n`); + for (const s of unknown) console.error(` ${s.rel}:${s.line} \`\`\`${s.language} — ${s.head}`); + console.error(REMEDY_UNKNOWN); + return 1; + } + + if (fresh.length) { + const total = fresh.reduce((n, f) => n + (f.count - f.owed), 0); + console.error(`❌ check:doc-fences — ${total} TypeScript block(s) under an unhighlighted fence, beyond the baseline:\n`); + for (const f of fresh) { + for (const s of byFile.get(f.rel) ?? []) console.error(` ${s.rel}:${s.line} \`\`\`${s.language || '(no info string)'} — ${s.head}`); + if (f.owed) console.error(` (${f.rel} is baselined at ${f.owed}; it now carries ${f.count}. The baseline only shrinks.)`); + } + console.error(REMEDY_SYNONYM); + return 1; + } + + if (stale.length) { + console.error(`❌ check:doc-fences — ${stale.length} stale KNOWN_UNHIGHLIGHTED_TS_FENCES entry/entries:\n`); + for (const s of stale) console.error(` ${s.rel} — baselined at ${s.owed}, now carries ${s.count}`); + console.error( + `\n Good news, and the list has to say so: ${stale.some((s) => s.count === 0) ? 'delete the zero lines' : 'lower the numbers'} in` + + `\n KNOWN_UNHIGHLIGHTED_TS_FENCES in scripts/check-doc-fence-languages.mjs` + + `\n (delete the entry when it reaches 0). This map is objectui#5867's` + + `\n remaining population; a stale line is that number drifting away from` + + `\n the tree, which is the whole reason it is kept here rather than` + + `\n re-derived by hand in each handback.`, + ); + return 1; + } + + const owed = [...KNOWN_UNHIGHLIGHTED_TS_FENCES.values()].reduce((a, b) => a + b, 0); + console.log( + `✅ check:doc-fences — every TypeScript block in ${documents.length} document(s) is fenced ts/tsx/typescript, ` + + `except ${KNOWN_UNHIGHLIGHTED_TS_FENCES.size} declared file(s) carrying ${owed} block(s) of objectui#5867's ` + + `remaining population (⛔ SHRINK-ONLY). No unknown fence spelling hides one.`, + ); + return 0; +} + +// ── Self-test — a guard that has never been shown to fail is not a guard ───── + +/** + * Drives the real scanner over fixture sources. The three probes the 2026-08-24 + * ruling on objectui#6135 named by hand are the first three cases, and they are + * here rather than only in a throwaway mutation so that the day this gate stops + * seeing them is a red CI run rather than nothing at all. + */ +export function selfTest() { + const cases = []; + const t = (name, ok, detail) => cases.push({ name, ok, detail }); + + const TS_BODY = 'interface FileUploadSchema {\n accept?: string;\n}'; + const doc = (info, body = TS_BODY) => [{ rel: 'probe.mdx', source: ['```' + info, body, '```'].join('\n') }]; + const modeOf = (info, body) => census(doc(info, body)).sites[0]?.mode ?? 'none'; + const named = (info) => { + const { sites } = census(doc(info)); + return sites.length === 1 && sites[0].rel === 'probe.mdx' && sites[0].line === 1; + }; + + // ── the three spellings the ruling named, each reddening AND named ──────── + t('a ```text-fenced TypeScript block is a SYNONYM finding', modeOf('text') === 'synonym'); + t('a ```txt-fenced TypeScript block is a SYNONYM finding', modeOf('txt') === 'synonym'); + t('a TypeScript block with NO info string is a SYNONYM finding', modeOf('') === 'synonym'); + t('…and each is reported with its file and fence line', named('text') && named('txt') && named('')); + t('```plain and ```plaintext are the same finding', modeOf('plain') === 'synonym' && modeOf('plaintext') === 'synonym'); + + // ── corrected blocks go green ──────────────────────────────────────────── + t('the SAME block fenced ```ts is not a finding', modeOf('ts') === 'none'); + t('…fenced ```tsx is not a finding', modeOf('tsx') === 'none'); + t('…fenced ```typescript is not a finding', modeOf('typescript') === 'none'); + + // ── a spelling nobody has thought of is the OTHER mode, never baselined ── + t('a TypeScript block fenced ```console is an UNKNOWN finding', modeOf('console') === 'unknown'); + t('…so is ```raw, and ```output', modeOf('raw') === 'unknown' && modeOf('output') === 'unknown'); + t( + 'an UNKNOWN finding fails even when its file is baselined', + (() => { + const { unknown } = analyze(doc('console'), new Map([['probe.mdx', 99]])); + return unknown.length === 1; + })(), + ); + + // ── the classifier is quoted, not widened ──────────────────────────────── + t('a prose block under any of those fences is NOT a finding', modeOf('text', 'Upload a file, then press Save.') === 'none'); + t('a bare object literal is NOT a finding', modeOf('plaintext', '{\n "accept": "image/*"\n}') === 'none'); + t('a comment-opening block is NOT a finding', modeOf('plaintext', '// the shape a slot receives\nfoo();') === 'none'); + t( + 'every limb of triage’s classifier fires', + ['import x from "y";', 'export const a = 1;', 'interface A {}', 'type A = B;', 'const a: A = b;'].every( + (b) => modeOf('plaintext', b) === 'synonym', + ), + ); + t( + 'an INDENTED first line is not code — the ASCII-diagram reading', + modeOf('', " import 'plugin-kanban'\n │\n ▼") === 'none', + ); + + // ── the fence walk is the gate's: run length, not a bare ``` ───────────── + t( + 'a ```` block is closed by its OWN run length, not by an inner ```', + (() => { + const s = ['````text', 'import a from "b";', '```', 'still inside', '````'].join('\n'); + const { sites } = census([{ rel: 'p.mdx', source: s }]); + return sites.length === 1 && sites[0].line === 1 && sites[0].mode === 'synonym'; + })(), + ); + t( + 'a ```ts fence QUOTED inside a ```` block is that block’s body, not a block', + census([{ rel: 'p.mdx', source: ['````plaintext', '```ts', 'import a from "b";', '```', '````'].join('\n') }]).sites.length === 0, + ); + t( + 'an info string with attributes still reads its language', + modeOf('text title="schema.ts"') === 'synonym' && modeOf('ts title="schema.ts"') === 'none', + ); + + // ── the baseline moves in exactly one direction ────────────────────────── + const two = [{ rel: 'a.mdx', source: ['```text', TS_BODY, '```', '```plain', TS_BODY, '```'].join('\n') }]; + t('a file not in the map that carries one is FRESH', analyze(two, new Map()).fresh.length === 1); + t('a file carrying MORE than its number is FRESH', analyze(two, new Map([['a.mdx', 1]])).fresh.length === 1); + t('a file carrying exactly its number is clean', (() => { + const r = analyze(two, new Map([['a.mdx', 2]])); + return r.fresh.length === 0 && r.stale.length === 0; + })()); + t('a file carrying FEWER is STALE', analyze(two, new Map([['a.mdx', 3]])).stale.length === 1); + t('a baselined file that no longer carries any is STALE', analyze([], new Map([['a.mdx', 1]])).stale.length === 1); + + // ── the map describes the SYNONYM mode only ───────────────────────────── + t( + 'UNHIGHLIGHTED_SPELLINGS carries the empty info string', + UNHIGHLIGHTED_SPELLINGS.has('') && UNHIGHLIGHTED_SPELLINGS.has('text') && UNHIGHLIGHTED_SPELLINGS.has('txt'), + ); + t('no TS fence language is also an unhighlighted spelling', [...TS_FENCE_LANGUAGES].every((l) => !UNHIGHLIGHTED_SPELLINGS.has(l))); + + const failed = cases.filter((c) => !c.ok); + for (const c of failed) console.error(` ✗ ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); + if (failed.length) { + console.error(`✗ check-doc-fence-languages self-test: ${failed.length} of ${cases.length} case(s) failed.`); + return 1; + } + console.log( + `✓ check-doc-fence-languages self-test: ${cases.length} cases pass — a TypeScript block fenced text, txt, plain or ` + + `with no info string at all is found and NAMED, the same block fenced ts/tsx/typescript is not, an unrecognised ` + + `spelling is the second failure mode and is never baselined, triage's classifier is quoted rather than widened, ` + + `and the shrink-only baseline is pinned in every direction it can move.`, + ); + return 0; +} + +if (isEntrypoint(import.meta.url)) { + const argv = process.argv; + process.exit(argv.includes('--self-test') ? selfTest() : argv.includes('--list') ? list() : main()); +} From 71297422bea64ca5f9857d1539cec6c7831c67ba Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 20:57:20 +0000 Subject: [PATCH 2/3] fix(tooling): classify the new Doc Fence Language check in the Dependabot gate `scripts/dependabot-merge-gate.mjs` partitions every check a pull request produces into required and optional buckets, and an unclassified blocking check is one the gate would let a Dependabot merge past. `doc-fence-languages.yml` carries no trigger-level path filter, so every pull request produces it. Found by `scripts/__tests__/dependabot-merge-gate.test.ts`, which is the pin that exists for exactly this. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- scripts/dependabot-merge-gate.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/dependabot-merge-gate.mjs b/scripts/dependabot-merge-gate.mjs index a85a31a714..e41bb2e300 100644 --- a/scripts/dependabot-merge-gate.mjs +++ b/scripts/dependabot-merge-gate.mjs @@ -125,6 +125,7 @@ import { pathToFileURL } from 'node:url'; * changeset-presence.yml Changeset Declaration * doc-component-types.yml Doc Component Type Check * doc-snippet-types.yml Doc Snippet Type Check + * doc-fence-languages.yml Doc Fence Language Check * * The four shards are spelled out individually on purpose. A single `Test` * entry, or any pattern match, would be satisfied by whichever shard happened @@ -146,6 +147,7 @@ export const REQUIRED_CONTEXTS = Object.freeze([ 'Changeset Declaration', 'Doc Component Type Check', 'Doc Snippet Type Check', + 'Doc Fence Language Check', ]); /** From 62b2639f88afd93f92c00ebd7219754338a45545 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 21:07:26 +0000 Subject: [PATCH 3/3] fix(tooling): lower the fence baseline for #5867 batch 4 and #6137 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `check:doc-fences` went red on its FIRST CI run, reporting nine of its own entries as STALE — `block-schema.mdx` baselined at 10 now carries 0, and the same for `fields/auto-number`, `fields/object` and six `plugins/` pages. Nothing was wrong with the tree: #6136 (#5867 batch 4) and #6137 merged while this branch was open and re-fenced 23 blocks across those 9 files. That is the shrink-only baseline doing exactly what it is for, unstaged, on its first run, with the remedy printed. The remedy is followed literally: the nine zero lines are deleted and nothing else moves. The population is RE-DERIVED on the merged base rather than subtracted by hand — 105 blocks / 83 files, and the diff against the old map is nine deletions with no line added and no number raised. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe --- scripts/check-doc-fence-languages.mjs | 37 ++++++++++++--------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/scripts/check-doc-fence-languages.mjs b/scripts/check-doc-fence-languages.mjs index 6910352a1f..72e3d52fbe 100644 --- a/scripts/check-doc-fence-languages.mjs +++ b/scripts/check-doc-fence-languages.mjs @@ -20,13 +20,19 @@ * * ⚠️ `plaintext` is not the only spelling of an unhighlighted fence, and until * this file landed nothing pinned the vocabulary. Re-running objectui#5867's own - * derivation on `origin/main` `bfdb9f906` with the fence-language set widened by - * `text` and `plain`, and nothing else changed, moved the population by exactly - * one block: + * derivation on `origin/main` with the fence-language set widened by `text` and + * `plain`, and nothing else changed, moves the population by exactly one block. + * Measured twice, because objectui#5867's batches were landing underneath this + * card while it was written — the delta is stable, the totals are not: * - * plaintext 127 blocks / 91 files - * plaintext + text + plain 128 blocks / 92 files - * …+ txt + no info string at all 128 blocks / 92 files + * at bfdb9f906 at 273537957 + * plaintext 127 / 91 104 / 82 + * plaintext + text + plain 128 / 92 105 / 83 + * …+ txt + no info string at all 128 / 92 105 / 83 + * + * (blocks / files. The 23 blocks across 9 files that left between those two + * readings are objectui#5867 batch 4, PR #6136, plus PR #6137 — which is exactly + * what the SHRINK-ONLY baseline below is for, and it caught the drift itself.) * * The block the wider set adds is `content/docs/components/form/file-upload.mdx:27`, * a ```text fence opening `interface FileUploadSchema {`. It had been outside the @@ -101,8 +107,8 @@ * lower or delete the line. No supported route raises a number. * * The map is not a debt list this gate invented. It IS objectui#5867's remaining - * population, per file, machine-readable and in the repository — 92 entries and - * 128 blocks at the commit this landed on, reconciling exactly with that card's + * population, per file, machine-readable and in the repository — 83 entries and + * 105 blocks at the commit this landed on, reconciling exactly with that card's * own derivation. Every batch of that lane now lowers these numbers in the same * pull request that re-fences the blocks, which is what stops the lane's * arithmetic from being a figure re-derived by hand in each handback and trusted @@ -184,8 +190,8 @@ export const UNHIGHLIGHTED_SPELLINGS = new Set(['plaintext', 'text', 'plain', 't * * "First line" is read literally — line 1 of the body, no leading whitespace * tolerated — and that is a measurement rather than a preference. A variant that - * trims first agrees with this one on all 127 `plaintext` blocks and on the - * `text` one, and disagrees on exactly one block in the tree: + * trims first agrees with this one on every `plaintext` block in the tree and on + * the `text` one, and disagrees on exactly one block in the tree: * `content/docs/guide/architecture-overview.md:123`, an ASCII-art plugin * lifecycle diagram whose first line is an INDENTED `import 'plugin-kanban'` * inside a box drawing. It is not TypeScript, and the strict reading is the one @@ -272,7 +278,7 @@ export function census(documents) { /** * ⛔ SHRINK-ONLY. `path -> number of SYNONYM-mode blocks the file carried when * this gate landed` — objectui#5867's remaining population, per file, at - * `bfdb9f906`: 92 files, 128 blocks. The rationale, and why a COUNT rather than + * `273537957`: 83 files, 105 blocks. The rationale, and why a COUNT rather than * a bare path, is in the header; the shape is objectui#6133's. * * The remedy for every line is the same, which is the property that makes a debt @@ -293,7 +299,6 @@ export function census(documents) { * @type {Map} */ export const KNOWN_UNHIGHLIGHTED_TS_FENCES = new Map([ - ['content/docs/blocks/block-schema.mdx', 10], ['content/docs/components/basic/button-group.mdx', 1], ['content/docs/components/basic/html.mdx', 1], ['content/docs/components/basic/icon.mdx', 1], @@ -367,24 +372,16 @@ export const KNOWN_UNHIGHLIGHTED_TS_FENCES = new Map([ ['content/docs/components/overlay/sheet.mdx', 1], ['content/docs/components/overlay/tooltip.mdx', 1], ['content/docs/core/report-schema.mdx', 11], - ['content/docs/fields/auto-number.mdx', 2], - ['content/docs/fields/object.mdx', 2], ['content/docs/layout/app-shell.mdx', 4], ['content/docs/layout/page-header.mdx', 3], ['content/docs/layout/sidebar-nav.mdx', 5], ['content/docs/plugins/plugin-calendar.mdx', 1], - ['content/docs/plugins/plugin-charts.mdx', 1], ['content/docs/plugins/plugin-chatbot.mdx', 2], ['content/docs/plugins/plugin-dashboard.mdx', 3], - ['content/docs/plugins/plugin-editor.mdx', 1], - ['content/docs/plugins/plugin-form.mdx', 2], ['content/docs/plugins/plugin-gantt.mdx', 1], - ['content/docs/plugins/plugin-grid.mdx', 2], ['content/docs/plugins/plugin-kanban.mdx', 1], ['content/docs/plugins/plugin-map.mdx', 1], - ['content/docs/plugins/plugin-markdown.mdx', 1], ['content/docs/plugins/plugin-timeline.mdx', 1], - ['content/docs/plugins/plugin-view.mdx', 2], ]); /** Split observed counts against the baseline. Both directions are failures. */