Skip to content

docs(ci-cd-pipeline): pin lint.yml's step list, and make the Lint section true - #8422

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-8015-lint-section-command-parity
Sep 7, 2026
Merged

baozhoutao merged 1 commit into
mainfrom
claude/issue-8015-lint-section-command-parity

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #8015

The centre of the card is the pin's scope

scripts/__tests__/ci-cd-pipeline-doc.test.ts already held exactly the right rule — every
first-party command a job runs must be named where this page documents that job, and the
documentation may not credit it with one the job does not run
— and it was scoped to ci.yml
alone
. lint.yml was covered only by the workflow-INVENTORY pin, which requires a ## heading
naming the file and asserts nothing about the heading's contents. So its step list could drift
indefinitely underneath a green test file.

That gap was demonstrated rather than argued: on the neighbouring card (#8006), a sentence in
this same document was replaced with an obvious falsehood and the eleven test files that read the
page were re-run — 404/404 green, check-doc-links exit 0. Nothing looked at this page's contents
beyond the ci.yml table.

Re-derived on today's tree, not reused from the card

The card measured 28cfff4 and listed three undescribed commands. On 4b4d35a7d there are
four — check-bash32-floor.mjs landed in lint.yml at 4922b83fc (#8016) after the card was
written, which is the drift rate this pin exists to absorb.

Parsed with the same rule the pin uses (run: values only, never the surrounding YAML):

commands
lint job runs (7) scripts/check-lint-coverage.mjs, scripts/check-entry-guard.mjs, scripts/check-upstream-port-parity.mjs, scripts/check-bash32-floor.mjs, pnpm lint, scripts/check-cross-repo-closer-outcome.mjs, pnpm check
page named, before (3) scripts/check-lint-coverage.mjs, pnpm lint, pnpm check
missing (4) scripts/check-entry-guard.mjs, scripts/check-upstream-port-parity.mjs, scripts/check-bash32-floor.mjs, scripts/check-cross-repo-closer-outcome.mjs
phantom (0) —

Positive control for the parser, so the four is a reading and not a broken matcher: the same
matcher run over the same section found the three commands the page did name, and found 12 run:
steps in the job of which the environment ones (corepack enable, pnpm --version, pnpm install --frozen-lockfile, pnpm --filter '@object-ui/cli...' build, the inline should_run shell block)
correctly drop out by naming nothing this repository owns. A matcher that saw nothing would have
reported the page's three as missing too.

How the pin generalises (rather than being copied)

jobKeys, jobBlock, runSteps, firstPartyCommands, undocumentedCommands and
phantomCommands move to module scope, together with the rule's docblock. The new entry point
is:

commandParity(workflowFile, jobKey, documentation, label?)

The two sources differ only in where the prose lives — one table cell per job for ci.yml, one
whole ## section for lint.yml's single job — so that is the only thing a caller supplies. The
ci.yml block keeps its own history and its own failure messages and is now three lines long; the
new lint.yml block is the same shape.

Reading the whole Lint section rather than only its bullet list is deliberate: the paragraphs
below the bullets discuss pnpm lint and pnpm check too, and a rule that read only the list would
call those phantoms.

A companion assertion pins that lint.yml still defines exactly one job, so a second job cannot
arrive undocumented and unpinned at the same time.

Ablation — both directions, on the committed tree

Each leg mutates a committed file, proves the mutation reached disk before any result is read
(on-disk git hash-object differs from the HEAD blob, plus a marker count), and restores by
state (git diff HEAD empty), never by exit code. The script carries a trap … EXIT INT TERM
with absolute paths.

HEAD blob content/docs/guide/ci-cd-pipeline.md = 1c7afb0c285d9ddb17278d33591940f142ff93bd
HEAD blob .github/workflows/lint.yml           = 405826570dd7c3ea05aa4672877d544e0895e21c

===== BASELINE (committed implementation, unmutated) =====
  vitest exit = 0
 Test Files  1 passed (1)
      Tests  49 passed (49)

===== LEG A: lint.yml gains a step the page does not name =====
  marker count before = 0
  on-disk hash = 69a872dc3b4ac4ef3848238adf8de5c230519813 (HEAD 405826570dd7c3ea05aa4672877d544e0895e21c)
  marker "ABLATION-A" count = 1 (expected 1)
  LANDED
  vitest exit = 1
 Test Files  1 failed (1)
      Tests  1 failed | 48 passed (49)
AssertionError: .github/workflows/lint.yml runs commands that the "## Lint (`lint.yml`)" section of
content/docs/guide/ci-cd-pipeline.md does not name:
  - lint.yml `lint`: scripts/check-doc-links.mjs
  RESTORED (git diff HEAD is empty)

===== LEG B: the page names a command the lint job does not run =====
  marker count before = 0
  on-disk hash = b165b44dd50531b839381642dfc804d0e30b464e (HEAD 1c7afb0c285d9ddb17278d33591940f142ff93bd)
  marker "ABLATION-B" count = 1 (expected 1)
  LANDED
  vitest exit = 1
 Test Files  1 failed (1)
      Tests  1 failed | 48 passed (49)
AssertionError: the "## Lint (`lint.yml`)" section of content/docs/guide/ci-cd-pipeline.md names
commands that .github/workflows/lint.yml does not run:
  - lint.yml `lint`: scripts/check-doc-links.mjs
  RESTORED (git diff HEAD is empty)

===== FINAL STATE =====
  vitest exit = 0
 Test Files  1 passed (1)
      Tests  49 passed (49)

Commands run (real output)

All vitest runs went through the shared verify lock.

$ pnpm exec vitest run scripts/__tests__/ci-cd-pipeline-doc.test.ts
 Test Files  1 passed (1)
      Tests  49 passed (49)
os-verify-lock: VERDICT command-exit 0

$ pnpm exec vitest run <the 16 test files that read this document>
 Test Files  16 passed (16)
      Tests  489 passed (489)
os-verify-lock: VERDICT command-exit 0

$ pnpm type-check:scripts                        → exit 0
$ node scripts/check-doc-links.mjs               → exit 0  "Links are valid across 17 scan roots."
$ node scripts/check-control-bytes.mjs           → exit 0  "scanned 6662 tracked text file(s)"
$ node scripts/check-doc-fence-languages.mjs     → exit 0
$ node scripts/check-shell-escape-residue.mjs    → exit 0
$ node scripts/check-governed-queue-guard.mjs --test <both changed paths>
    ✅ NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched.

Changeset — measured, not guessed:

$ node scripts/check-changeset-presence.mjs
Compared the working tree with 4b4d35a7d (merge-base with origin/main): 2 file(s) changed,
0 of them published source of a package the release covers, 0 of them a manifest whose published
contract moved, 0 under a package changesets ignores, 0 changeset(s) added.
✅  No source or published contract of a released package changed in this range, so no changeset is owed.

So no changeset, and no skip-changeset label — this page's own pins deny that label is wired to
anything here.

ESLint, narrowed with the narrowing proven (pnpm lint is turbo run lint, which walks
packages; scripts/ has no package.json, so the changed file is outside that surface — it was
linted directly anyway):

  • population read from eslint's own config, not guessed: eslint.config.js declares no
    parserOptions.project and no projectService, so linting is not type-aware and no verdict on
    an untouched file can move because of this diff;
  • file count read from --format json: 1 file linted, 0 errors, 0 warnings;
  • the other changed file is .md, outside files: ['**/*.{ts,tsx}'] entirely.

Control-byte self-scan beyond the gate: grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over both
changed files → no matches.

Out of scope, filed not fixed

#8420 — every other workflow section on this page is unpinned the same way, with two verified
instances (vi-mock-specifiers.yml's section omits the blocking check-vi-mock-inherit.mjs;
shadcn-check.yml's omits all three of its commands). Deliberately not a rider here: a naive
whole-page extension flags 24 of 34 sections, most of them cross-referencing prose rather than
defects, and this document is under concurrent edit today.

Not touched, as the card directs: the Skill Guide Paths sentence (#8006, landed) and the
main@6422aa891 baseline sentence (#7965 ruled it correct, since it declares its measurement
point).

🤖 Generated with Claude Code


Generated by Claude Code

…tion true

`scripts/__tests__/ci-cd-pipeline-doc.test.ts` already held the right rule --
every first-party command a job runs must be named where this page documents
that job, and the documentation may not credit it with one the job does not
run -- but the rule was scoped to `ci.yml`'s job table alone. `lint.yml` was
covered only by the workflow-INVENTORY pin, which requires a `##` heading
naming the file and nothing about its contents, so its step list could drift
indefinitely underneath it.

It had. Measured on 4b4d35a, the `## Lint (lint.yml)` section named three of
the seven first-party commands the `lint` job runs. The four it omitted --
`check-entry-guard.mjs`, `check-upstream-port-parity.mjs`,
`check-bash32-floor.mjs` and `check-cross-repo-closer-outcome.mjs` -- are all
blocking gates in a REQUIRED check, missing from the page whose job is to tell
a contributor what can block them. Three had been unlisted since they landed;
`check-bash32-floor.mjs` (PR #8016) arrived while the card was open, which is
the drift rate this pin exists to absorb.

The rule is GENERALISED rather than copied: `jobKeys`, `jobBlock`, `runSteps`,
`firstPartyCommands` and the two direction finders move to module scope, and
`commandParity(workflowFile, jobKey, documentation)` takes whatever text
documents a job. `ci.yml` supplies one table cell per job; `lint.yml` supplies
one whole `##` section for its single job. A companion assertion pins that
`lint.yml` still defines exactly one job, so a second one cannot arrive
undocumented and unpinned at the same time.

Both directions are demonstrated by ablation on the committed tree: a step
added to `lint.yml` and not to the page fails the first assertion; a command
named on the page that the job does not run fails the second.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

ACCEPT — domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R46, reviewed 2026-09-07T22:25Z against GitHub and the tree.

⭐ The card said three undescribed commands; there are four — and the fourth is the argument

Re-derived independently by this seat from .github/workflows/lint.yml on origin/main, the lint job's first-party commands are exactly seven:

pnpm check · pnpm lint · scripts/check-bash32-floor.mjs · scripts/check-cross-repo-closer-outcome.mjs
scripts/check-entry-guard.mjs · scripts/check-lint-coverage.mjs · scripts/check-upstream-port-parity.mjs

The page named three. ⇒ Four were missing, not three: check-bash32-floor.mjs landed in lint.yml at 4922b83fc (#8016) after the card was written.

⭐ That is not a correction to file away — it is the card's thesis proving itself while the card sat open. A prose-only fix would have been stale before it merged. The drift rate is the evidence for pinning rather than patching, and this dispatch's insistence that the pin's scope is the centre of the card is vindicated by it rather than by argument.

Two further premise corrections, both reported rather than absorbed: the card says eleven test files read this document — there are 17, 16 vitest-reachable, all run (489/489 green); and objectui's repo lint is turbo run lint per package, not eslint ., so scripts/ is outside that surface entirely (linted directly anyway).

The generalisation is the deliverable, not the four bullets

commandParity(workflowFile, jobKey, documentation, label?) is hoisted to module scope with its helpers, and both consumers now call one rule: ci.yml's job table and lint.yml's whole section. ⛔ Nothing was copied — the dev's reasoning is that the two sources differ only in where the prose lives (one table cell per job vs. one ## section), so the documentation carrier is the only parameter. The ci.yml block shrank to three lines plus its messages.

⭐ Two judgements inside that are worth naming:

  • The unit reads the whole section, not just the bullet list — the paragraphs below the bullets discuss pnpm lint and pnpm check too, and a rule reading only the list would call those phantoms. A subtler scope choice than it looks.
  • The vacuity floors (ran > 4, named > 4) are controls on the matcher, not a ratchet on the gate count — so removing a gate from lint.yml doesn't force a test edit, which is the shape that quietly teaches people to weaken gates.

Both directions demonstrated, not asserted

  • A — inserted an unnamed command into lint.yml: RED, naming scripts/check-doc-links.mjs as run-but-unnamed.
  • B — added a bullet naming a command the job doesn't run: RED, naming it as credited-but-not-run.

Each with the mutation proved on disk before any result was read (marker count plus a blob hash differing from HEAD) and restored by state (git diff HEAD empty), under a trap with absolute paths. ⭐ Direction B is the one that matters most and is usually skipped: without it, "just name more commands" passes, and a page claiming a guardrail CI does not run is worse for the reader than an omission — the #3451 shape.

⛔ The refusal to widen is as valuable as the fix

The dev measured the temptation: a naive whole-page extension flags 24 of 34 sections, and most are not defects — cross-referencing prose, an intentional placeholder, and sections that write pnpm check:* aliases where the workflow deliberately calls node scripts/… directly for pre-install placement. ⇒ Filed as #8420 rather than swept in, on the repository's own reasoning that a gate that cries wolf gets switched off rather than fixed.

⭐ And one small thing that shows the same care: the dev deliberately did not write scripts/check-skills-paths.mjs's full path into the Lint section, because that would credit lint.yml with a gate that runs in skills-paths.yml — then added a parenthetical saying why, so the next editor doesn't "helpfully" complete it into a phantom.

⚠️ #8420 was filed with no labels; this seat added finding. ⛔ No domain:* — triage's to produce. It also carries a real contract question the dev correctly declined to answer: whether pnpm check:* aliases and node scripts/… direct calls are the same command, given they differ in pre-install placement.

Changeset none owed, by the gate's verdict. Not governed surface.

Next: CI converged all green (28 checks, read 22:23Z) → flip → post-flip guard → SQUASH → queue → landing probe (all seven commands named in the Lint section on re-fetched origin/main, and commandParity at module scope with both consumers calling it; control: the ci.yml job-table assertions still present and still passing — the generalisation must not have loosened the case it came from) → LANDED here and on #8015, which closes via Fixes.


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review September 7, 2026 22:24
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 7, 2026
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Armed: PR #8422 flipped ready at 2026-09-07T22:24:10Z on head db0d1d45b after CI converged all green (28 checks, Live E2E excluded, read 22:23:23Z); post-flip Governed Surface Queue Guard completed/success at 22:24:29Z; auto-merge SQUASH enabled; queued (added_to_merge_queue at 22:24:35Z).

Landing probe on merge, on re-fetched origin/main: all seven first-party commands the lint job runs are named in the Lint section (independently re-derived by this seat from the workflow: pnpm check, pnpm lint, check-bash32-floor.mjs, check-cross-repo-closer-outcome.mjs, check-entry-guard.mjs, check-lint-coverage.mjs, check-upstream-port-parity.mjs), and commandParity sits at module scope with both consumers calling it.

Control: the ci.yml job-table assertions still present and still passing. That is the control a generalisation needs — the rule was lifted out of the case it came from, so the risk is loosening that case while adding the new one. A green lint.yml pair beside a weakened ci.yml pair would read as success.

⭐ Worth re-reading at landing: the card named three undescribed commands and there are four — check-bash32-floor.mjs landed in lint.yml (#8016) while the card sat open. The page went stale during the life of the card that reported it stale, which is the argument for the pin rather than the prose.

Then LANDED here and on #8015, which closes via Fixes. — domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr.


Generated by Claude Code

Merged via the queue into main with commit 6e82faa Sep 7, 2026
31 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-8015-lint-section-command-parity branch September 7, 2026 22:40
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

LANDED — merged into main at 2026-09-07T22:40:43Z as 6e82faa3c (squash), read 22:40:59Z. #8015 closed completed via Fixes.

Content probe on re-fetched origin/main (tip 6e82faa3c, read 22:43Z):

  1. All seven first-party commands are named in the Lint section — check-lint-coverage, check-entry-guard, check-upstream-port-parity, check-bash32-floor, check-cross-repo-closer-outcome (1 each), pnpm lint and pnpm check (2 each, the bullets plus the prose below them). The four that were missing are in.
  2. commandParity is at module scope (:662) with BOTH consumers calling it — ci.yml at :885 via docJobRows().map(...), lint.yml at :977. One rule, two sources; ⛔ nothing copied.
  3. Control — the ci.yml job-table describe is still present. ⚠️ This is the control a generalisation specifically needs: the rule was lifted out of the case it came from, so the failure mode is a green new pair beside a quietly weakened original. It is intact.

⭐ The reading worth keeping: the card named three undescribed commands and there were four — check-bash32-floor.mjs entered lint.yml via #8016 while this card sat open. The page went stale during the life of the card that reported it stale. No prose-only fix could have outrun that; the pin is the only thing that does.

⛔ Not closed by this: #8420 — every other workflow section on this page is unpinned against what its job runs, with two verified instances (vi-mock-specifiers.ymls section omits the blocking check-vi-mock-inherit.mjs; shadcn-check.ymls names none of its three). It was correctly not swept in: a naive whole-page extension flags 24 of 34 sections and most are cross-referencing prose or deliberate placeholders, and this repository's own reasoning is that a gate that cries wolf gets switched off rather than fixed. It also carries a real contract question — whether pnpm check:* aliases and node scripts/… direct calls are the same command, given they differ in pre-install placement — which the dev correctly declined to answer.


Generated by Claude Code

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

Labels

Projects

None yet

2 participants