Skip to content

[#416] feat: llms.txt drift gate - #475

Open
rucka wants to merge 17 commits into
mainfrom
feature/US-416-llms-txt-drift-gate
Open

[#416] feat: llms.txt drift gate#475
rucka wants to merge 17 commits into
mainfrom
feature/US-416-llms-txt-drift-gate

Conversation

@rucka

@rucka rucka commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

PR Information

PR Title: [#416] feat: llms.txt drift gate
Story/Epic: #416 — no parent epic (cross-cutting quality-gate coverage)
Type: Feature (tech-debt)
Priority: P2 (Could-Have)
Labels: user story, tech-debt, risk:green, pr-state:to-be-reviewed

Summary

What Changed

pnpm llms-index:check — a gate that regenerates .pair/llms.txt with the CLI's own generateLlmsTxt and requires byte equality with the committed file, printing the missing/extra lines and never writing the file. Wired into ci.yml and the root quality-gate.

Around it, three environment axes that would each make a byte-compared artifact machine-dependent are closed at the generator, plus the remedy the gate prints:

Axis Before Now
Collation localeCompare, no locale ⇒ runtime ICU default UTF-16 code-unit order
Line terminator nothing pinned; core.autocrlf=true rewrote all 570 lines .gitattributes (* text=auto eol=lf + the index by name); the gate diagnoses any \r
Path separator path.join.pair\knowledge\... on Windows posix.join for emitted paths; join for file-system access only
Printed remedy pair update — no such executable here pnpm llms-index:regen, a root script that exists

Why This Change

.pair/llms.txt is generated and tracked, and it is the index CLAUDE.md points every agent at. Nothing compared it to its generator, so adding a guideline without regenerating silently hid it from every agent — two independent misses are on record (story-local-markers.md; the two how-to guides dropped in #246 and still indexed ~5 months later).

A gate on a byte-compared artifact is only trustworthy if the artifact is byte-reproducible. Each axis above was measured as a full-file false positive on an untouched tree — collation: 458 of 560 entries move under ICU; terminator: 570 missing + 570 extra; separator: 562 missing + 562 extra — with a closing imperative that either cannot work or, obeyed, commits the damage.

Story Context

User Story: As a maintainer of the pair knowledge base, I want a quality gate that fails when .pair/llms.txt no longer matches what the generator would emit, so that the machine-readable KB index that CLAUDE.md points agents at cannot silently go stale.

Acceptance Criteria:

AC Covered by
1 — byte equality against generateLlmsTxt checkLlmsIndexDrift; compareIndex returns in-sync only on exact equality
2 — prints the missing/extra LINES formatDrift, multiset deltas (a duplicated line is drift a set view cannot see), with look-alike pairs rendered byte-escaped
3 — red on the PR, not later on main .github/workflows/ci.yml step llms-index:check
4 — same check locally and in CI root quality-gate chain + the same ci.yml step; gate:composition guards the chain
5 — names the regeneration command, never regenerates REGENERATION_COMMAND = 'pnpm llms-index:regen', asserted as a literal AND resolved against the real root package.json; the gate is handed a 3-method read-only fs slice, so "cannot write" is a type fact
6 — consumes #393's trailing-newline form expectations are always generateLlmsTxt's own output; the form is never re-litigated

Changes Made

Implementation Details

  • The gatepackages/dev-tools/src/quality-gates/llms-txt-drift-check.ts. Four outcomes as a discriminated union, because they have different remedies: in-sync, drift, broken-setup (no indexable section — an unfinished install, not a stale index), unreadable-index (the tracked file's own permission bits or a directory in its place). Reports the terminator (trackedCarriesCr), a BOM (trackedCarriesBom) and emptied sections, and puts the matching precondition in front of the closing imperative — a report that says "restore the tree first" and then closes with a bare "regenerate" delivers, in its own call to action, the damage the caution exists to prevent.
  • The remedypackages/dev-tools/src/quality-gates/llms-txt-regenerate.ts (pnpm llms-index:regen). The check's exact inverse: same generator, same tracked path, one file written. It runs the check first and refuses on broken-setup, unreadable-index, an unreadable tree, trackedCarriesCr and emptiedSections, printing the gate's own message — so obeying the printed advice can never be the thing that loses a section. On the WRITE_MODE_FORMATTERS offender list: a gate that ran its own remedy would hide the drift it exists to reveal.
  • Determinism at the generatorapps/pair-cli/src/registry/llms-generation.ts: code-unit ordering and posix.join for emitted paths. This is published-CLI behaviour, so it carries a patch changeset.
  • The read-only sliceLlmsSourceFs (3 read methods) replaces FileSystemService in generateLlmsTxt's signature; the writer declares its extra power as a separate two-method LlmsIndexSink.
  • #216's byte-equality guard was replaced, not duplicatedllms-index-conformance.test.ts keeps only its output-shape assertions.

Files Changed

  • Added: packages/dev-tools/src/quality-gates/llms-txt-drift-check.ts + .test.ts, packages/dev-tools/src/quality-gates/llms-txt-regenerate.ts + .test.ts, apps/pair-cli/src/registry/llms-generation.win32.test.ts, .gitattributes, .changeset/llms-txt-deterministic-order.md, 3 ADLs under .pair/adoption/decision-log/
  • Modified: apps/pair-cli/src/registry/llms-generation.ts, apps/pair-cli/src/registry/llms-index-conformance.test.ts, packages/dev-tools/src/quality-gates/pre-push-gate-composition.ts + .test.ts, packages/dev-tools/{package.json,tsconfig.json,README.md}, root package.json, .github/workflows/ci.yml, .pair/llms.txt, .pair/adoption/tech/way-of-working.md, DEVELOPMENT.md, apps/website/content/docs/contributing/development-setup.mdx

Services to Release

  • @pair/pair-cli (patch changeset): .pair/llms.txt is now emitted in a deterministic, locale-independent order and with POSIX separators on every platform. An adopter's next pair-cli install / pair-cli update rewrites the file once — uppercase-first entries sort before their lowercase siblings; a Windows adopter additionally stops shipping 562 backslash-separated, unresolvable links. Generated file, so a one-time diff, not a migration.

Testing

Test Coverage

  • Unit: 112 rows for the gate (terminator domain closed: LF / CRLF / mixed / \r\r\n / bare CR / CR+LF / stray CR at EOF; invisible-character rendering; the four outcomes), 11 for the regeneration command (write path, idempotence, BOM strip, and one row per refusal state), 53 for gate composition, 221 across the CLI registry.
  • E2E: pnpm --filter @pair/website e2e — 39 passed (a docs .mdx changed).
  • Manual/boundary probes: below — the parts a unit test cannot establish.

Test Results

pnpm quality-gate    ✅ exit 0 (full chain — Pre-merge tiering: disabled)
pnpm --filter @pair/website e2e   ✅ 39 passed
pnpm llms-index:check             ✅ .pair/llms.txt matches the generator

Testing Strategy

  • Happy path: a guideline added without regenerating ⇒ the gate names the exact missing line.
  • Edge cases: every outcome above, plus multiset (duplicated-line) drift, an empty tracked file, an absent one, order/whitespace-only difference, and look-alike pairs whose only difference is invisible.
  • Error handling: an unreadable KB tree and an unreadable index file are distinct outcomes with distinct recipes — conflating them sends a contributor to reinstall a knowledge base that is fine.
  • Cross-platform: llms-generation.win32.test.ts binds path to Node's own path.win32 (what require('path') returns on Windows) and asserts the generator emits the same bytes a POSIX machine emits, order included. On POSIX join and posix.join are the same function, so this is the only shape of test that can tell the fixed code from the broken code on a Linux CI.

Reviewer Guide

Boundary probes (run against the real producers, not stubs)

Claim Probe Result
A Windows run breaks the index Real path.win32 bound to the real generator over the real repo tree, output into the real compareIndex/formatReport before: 562 missing + 562 extra, 1140-line report, no caution naming the cause, closing on the bare imperative — after: in-sync
The printed remedy works Fresh git clone + pnpm install (nothing built), add a guideline, run the gate, type the printed command verbatim, re-run red → ✓ regenerated → green; git diff --stat = `.pair/llms.txt
The index's producer is pair-cli install / pair-cli update, and pair is not a binary pnpm pack of apps/pair-cli + its two workspace deps → npm i the three real tarballs into an empty npm init project → run the linked bin in an empty target dir packed manifest bin = {"pair-cli":"dist/cli.js"}; node_modules/.bin links pair-cli, no pair; which pair → not found. pair-cli install --offline --source packages/knowledge-hub/dataset wrote .pair/llms.txt (474 lines, sha1 733c39ed…); after tampering the file, pair-cli update restored it byte-identical. The retired remedy string pair update names no executable — which is why the gate prints pnpm llms-index:regen
Regenerating cannot fix a CR checkout Real script on the real repo with .pair/llms.txt converted to CRLF exit 1, file byte-identical, message = the gate's own with the rm + git checkout -- recipe
That recipe is the one that works core.autocrlf=true clone git add --renormalize stages nothing and leaves all 583 CR lines; rm + git checkout -- gives w/lf, 0 CRs, gate green — with core.autocrlf left at true (the eol=lf attribute overrides it)
Regenerating does fix a BOM Real regeneration on a BOM-prefixed index rewritten BOM-free, gate green — so that caution adds no precondition
The gate survives a fresh install pnpm install, no build, run the gate verdict returned (this is why it is ts-node -T: type-checking made the verdict depend on whether someone had built @pair/content-ops)

Testing the Changes

git checkout feature/US-416-llms-txt-drift-gate && pnpm install
pnpm llms-index:check                                  # green on an untouched tree
printf '# Probe\n' > .pair/knowledge/guidelines/probe.md
pnpm llms-index:check                                  # red, names the one missing line
pnpm llms-index:regen && pnpm llms-index:check         # green again, one file changed
rm .pair/knowledge/guidelines/probe.md && pnpm llms-index:regen

Documentation

Technical Decisions

ADL What it settles
2026-09-01-a-gate-imports-its-generator-by-source-and-gets-a-read-only-slice.md A gate imports the artifact-producing code by SOURCE path (costs paid explicitly: "composite": false, a @pair/content-ops devDependency that orders ts:check and resolves nothing, one hardcoded hop) and runs it transpile-only; code invoked by a gate takes the narrowest fs capability
2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md A tracked, byte-compared artifact is byte-reproducible on any machine that checks it out — three rules: content-derived order, eol=lf pin + terminator diagnosis, POSIX separators in emitted paths
2026-09-03-a-gate-names-a-remedy-it-can-run.md A gate's remedy names a command that exists for its audience, and that command refuses the states its own cautions warn about

Consumed, not re-decided: 2026-08-11-a-mirror-guard-compares-the-transform.md, 2026-07-31-pre-push-gate-is-check-only.md, 2026-08-20-a-user-facing-cli-fix-carries-its-changeset.md.

Docs updated: DEVELOPMENT.md + its byte-identical docs-site twin (development-setup.mdx), way-of-working.md (Quality Gates), packages/dev-tools/README.md.

One command family per statement: the adoption bullet and the gate's own header name the index's producer as pair-cli install / pair-cli update (the published bin is pair-cli), and pair update appears only where it is explicitly labelled the retired spelling. A file that credits a command with generating the index and denies that binary exists 100 words later re-teaches, in the project's source of truth for the gate, the exact string ADL 2026-09-03 retired.

Dependencies & Related Work

Related PRs

Follow-up Work

@rucka rucka added user story Work item representing a user story tech-debt Tracked technical debt (living backlog, R7.2 — never blocks a PR) risk:green Classification: low risk tier labels Sep 1, 2026
@rucka rucka self-assigned this Sep 1, 2026
@rucka rucka added the pr-state:to-be-reviewed PR state: awaiting review / gate label Sep 1, 2026
@rucka

rucka commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict

risk:green · cost:n/aCHANGES-REQUESTED — the gate's own comparison is not environment-independent: on a pristine checkout of this branch, LC_ALL=cs_CZ.UTF-8 (or et_EE.UTF-8) makes pnpm llms-index:check red with 0 missing / 0 extra, and the fix it names (pair update) reddens CI for everyone else.

Open findings: 5.

PR: #475 · Author: rucka · Reviewer: independent reviewer agent · Date: 2026-09-01 · Story: US-416 · Type: feature (tech-debt)

Classification matrix — per dimension
Dimension Tier Source Note
Service/domain criticality green Criticality Table packages/dev-tools = Low
Change/diff risk green diff footprint +673/-35; one new gate module + test + wiring; generator change is a type narrowing only
Business impact yellow subdomain class Integration & Process Standardization (Supporting) — KB integrity
Security relevance green /pair-capability-assess-security No untrusted input, no network, no secrets, read-only FS by type
Coupling balance green path heuristic New cross-package SOURCE import dev-tools → pair-cli, argued in an ADL; no new integration

Tier = max(assessed) with the default-artifact-downgrade on Business impact → green, confirming refinement (no raise).

Assessments

Security — Input validation

Verdict: green — the only inputs are repo-local file paths derived from __dirname; nothing untrusted reaches the gate.

Security — Output handling

Verdict: green — output is console text on a dev/CI TTY; no encoding surface.

Security — Authentication / Authorization

Verdict: green — no auth surface; the gate runs in CI with the repo checkout it already has.

Security — Introduced vulnerabilities

Verdict: green — 0 introduced, 0 pre-existing. The FileSystemServiceLlmsSourceFs narrowing strictly reduces capability (no writeFile/rm/chmod reachable from the gate).

Cost

Verdict: cost:n/a — capability not run; no paid provider, no infra, no runtime footprint beyond one extra CI step (~seconds).

Architecture (Coupling)

Verdict: not assessed (/pair-capability-assess-coupling unavailable until #263). Reviewer note: the new dev-tools → apps/pair-cli/src/... source reach-in is a real coupling increase, argued and bounded in the ADL, with a loud compile-time failure mode.

Bug fix — Red test before fix

Verdict: not applicable — not a bug fix (tech-debt/coverage story).

Details

Findings by severity

Critical (must fix before merge)

  • none.

Major (must fix before merge)

  • apps/pair-cli/src/registry/llms-generation.ts:55entries.sort((a, b) => a.path.localeCompare(b.path)) uses the ambient collation, so the "expected" index is a function of the machine's locale, and the gate this PR adds turns that into a red build on an unmodified tree. Verified on this branch's real corpus (4d3013cf), by re-implementing generateLlmsTxt in plain JS and diffing against the tracked .pair/llms.txt: LC_ALL=cs_CZ.UTF-8equal: false, first divergence at line 276 — Czech collation treats ch as a digraph sorting after h, so checkpoint-template.md moves after code-review-template.md; LC_ALL=et_EE.UTF-8equal: false as well (en_US, da_DK, sv_SE, tr_TRequal: true today). Concrete failure: a contributor with a Czech/Estonian locale (or any CI image with LANG set to one) runs pnpm llms-index:check on a clean checkout and gets ❌ ... is not what the generator emits, 0 missing line(s), 0 extra line(s), "the difference is their order", and the instruction to run pair update — which regenerates in their collation; committing that result flips the gate red for en-US CI. The two sides ping-pong and neither can make the gate green. The story's edge case names this exact class ("locale-dependent sort … must fail on a content difference, not on an environment artefact"), and the determinism test only re-runs the generator twice inside one process, so it cannot see it. Fix: pin the collation — a.path.localeCompare(b.path, 'en-US') (or a module-level Intl.Collator('en-US')). Verified zero-diff: with 'en-US' pinned, the generated index is byte-identical (66389 chars) to the tracked file under cs_CZ, et_EE and da_DK, so no regeneration of .pair/llms.txt is needed. Add a regression case that runs the generator under a second collation (or asserts the pinned comparator's order on names like checkpoint-* / code-*, aa-* / ab-*), otherwise the determinism AC stays same-process-only.

Minor (must fix before merge — same bar as Major, just lower impact)

  • packages/dev-tools/src/quality-gates/llms-txt-drift-check.ts:125hasIndexableSection returns true as soon as one ## heading exists, so "partially installed" is detected only in the degenerate case where nothing at all is indexable (the test at :168 hedges this in its own name). Concrete failure: a tree where .pair/adoption/** is present but .pair/knowledge/** is missing (sparse checkout, interrupted pair install, a bad rebase dropping the directory) is reported as drift with ~300 extra lines plus "Regenerate with pair update and commit the result" — and a contributor who follows that instruction commits a truncated index that the gate then passes green. That is the silent staleness the story exists to close, reached through the gate's own advice. Fix: treat a sectionDefs directory that the tracked index still references but that no longer exists on disk as broken-setup (or require every previously-indexed top-level section to be present), so a vanished section is an unfinished setup rather than drift.
  • package.json:32 / packages/dev-tools/package.json:17 — the gate is not runnable on its own from a fresh clone, which is the property the ADL uses to reject the @pair/content-ops alternative ("the root step is also runnable on its own — pnpm llms-index:check"). packages/content-ops/dist/ is gitignored (git check-ignore.gitignore:2:dist/), ts-node here type-checks (confirmed: pnpm --filter @pair/dev-tools exec ts-node -e "const x: number = 'a'"error TS2322, exit 1), and the gate's source import pulls llms-generation.ts — with its import type { FileSystemService } from '@pair/content-ops' — into that program. So pnpm install && pnpm llms-index:check (exactly the sequence in this PR's own Reviewer Guide, which omits a build) dies with a module-resolution error instead of a drift verdict. Inside pnpm quality-gate and ci.yml the ordering saves it, which is why the branch is green. Fix: make the root script self-sufficient (turbo build --filter @pair/content-ops && pnpm --filter @pair/dev-tools llms-index:check), or state the build prerequisite in the Reviewer Guide and next to the script.
  • apps/pair-cli/src/registry/llms-index-conformance.test.ts:35 — comment left stale by the move: "Byte equality above pins whatever the generator emits, so without this case the wrong output would be asserted correct" — there is no byte-equality case above (or anywhere) in this file any more, and the sentence is the justification for the cases that remain. A reader hunting the byte-equality guard is sent to a case that no longer exists. Fix: re-point it at packages/dev-tools/src/quality-gates/llms-txt-drift-check.ts.
  • packages/dev-tools/src/quality-gates/llms-txt-drift-check.ts:211main() has no try/catch, and void main() at :222 discards the promise. Concrete failure: readdir throws EACCES/EMFILE on a KB subdirectory (permission-mangled checkout, container volume) → node prints an unhandled-rejection stack trace instead of any llms-index: message. The exit code is still non-zero (fail-safe), but the contributor sees a crash from a gate whose entire AC-2/AC-5 contribution is an actionable message. Fix: wrap the body and print a broken-setup-shaped message naming the path that failed.

Questions (informational, never blocking)

  • none.
Positive feedback
  • The FileSystemServiceLlmsSourceFs narrowing is the strongest part of the change: "this gate cannot write the file it judges" stops being a promise and becomes unrepresentable, and existing callers stay untouched by structural typing.
  • broken-setup as a third variant of a discriminated union (not an ok/detail bag) makes "a missing tree is not a stale index" impossible for a caller to conflate.
  • Replacing Auto-development eligibility tag from refinement classification #216's byte-equality vitest case rather than duplicating it — and keeping its non-drift shape assertions with the reason spelled out — is exactly the "two guards that drift apart" avoidance the story asked for.
  • The ADL argues the two alternatives a reviewer actually reaches for (move the generator into @pair/content-ops; widen rootDir instead of dropping composite) with the specific compiler errors observed, not in the abstract.
Functionality & requirements (AC coverage)
AC Verdict Evidence
1 — regenerate + byte compare met checkLlmsIndexDrift :196-207; compareIndex returns in-sync only on actual === expected
2 — missing/extra lines, not "files differ" met formatReport :149-177; tests at :73 and :95 assert the exact line text
3 — red on the PR, not later on main met ci.yml pull_request trigger has no .pair/** path exclusion (only .changeset/**), and the step runs unconditionally rather than through cacheable turbo test
4 — wired into ci.yml + root quality-gate met same command via the root alias in both; husky pre-push reaches it through quality-gate
5 — names the regen command, never writes met REGENERATION_COMMAND; the mtime assertion at :120 is the right proof, and broken-setup correctly withholds the command
6 — consumes #393's trailing-newline form met independently regenerated the index from the branch's corpus: byte-identical to the tracked file (66389 chars), single trailing newline
Edge — non-deterministic/locale-dependent sort not met Major finding above
Edge — tree absent or partially installed partial Minor finding above; only the "no indexable section at all" case is distinguished
Edge — tracked file empty met test :134
Testing & quality gates
  • Coverage: 14 fixture-based cases, all on temp trees; the real corpus is never read by the suite, so a legitimately added guideline cannot redden it — the right split against the gate's job.
  • Gap: main() (exit-code path) is untested; determinism is asserted same-process only (see Major).
  • Gates: not re-run here (no node_modules in the review worktree). Independently verified the branch is green on the check that matters: a faithful re-implementation of generateLlmsTxt reproduces the tracked .pair/llms.txt byte for byte under the default locale.
Adoption compliance
  • Degradation level: 1 — decision recorded before/with the code.
  • ADL present for both new conventions (source-path import; narrowest FS capability), with alternatives and consequences; way-of-working.md § Quality Gates updated in the same PR, matching the "guard over repo-wide state ⇒ root gate step" rule it extends.
  • No new external dependency (@pair/content-ops is an existing workspace package), so no tech-stack.md change is owed. Gate placement matches ADR-014 / the gate-tooling ADL: logic in a tested module, thin require.main CLI, root script delegating via pnpm --filter.
Tech debt
  • Dropping composite on @pair/dev-tools removes the compiler's objection to any future cross-package reach-in from that package; the ADL records this and asks that a second one be argued. Surfaced, not blocking.
Documentation
  • Module header, tsconfig comment at the edit site, ci.yml step comment, ADL and way-of-working.md all updated. One stale comment (Minor above).
Performance & deployment
  • One extra CI step; a single generator walk of .pair/** plus one file read, no repeated walks.
  • Reversible: the gate is check-only and writes nothing; removing the two wiring lines fully rolls it back.

@rucka

This comment has been minimized.

rucka added a commit that referenced this pull request Sep 1, 2026
…depoint

Review round 1 on PR #475.

- `llms-index:check` was a bare `ts-node`, so it type-checked a source file whose
  first line imports @pair/content-ops TYPES from dist/. On a fresh `pnpm install`
  the gate produced none of its three outcomes — TS2307 + a ts-node stack, exit 1.
  Now `ts-node -T`; type-checking that source is ts:check's job, and turbo ts:check
  is the task carrying the ^build edge.
- compareIndex diffed through Sets, so a duplicated entry line (the shape a
  "keep both sides" merge of llms.txt produces) reported `0 missing / 0 extra` plus
  "the difference is their order or whitespace" — wrong, in the case that most needs
  the diff. Multiset deltas now.
- localeCompare with no locale sorted the index by the runtime's ICU default: 458 of
  560 entries move under a small-icu Node, so the gate went red on an untouched tree
  and its own message told the contributor to commit the churn. Codepoint order;
  .pair/llms.txt regenerated through writeProjectLlmsTxt.
- main() had no try/catch: an unreadable KB dir (chmod 000) escaped as an unhandled
  rejection instead of the broken-setup outcome. Caught, reported, exit 1.
- A tree missing ONE whole section clears the broken-setup guard and is reported as
  drift telling the contributor to regenerate — which deletes that section from the
  index. Caution line when a tracked heading has zero generated entries.
- dev-tools README Tools table: the gate had no row (plus the two stale ones).

ADLs: codepoint-sort rule recorded; the source-import ADL gains the -T rationale.
Bound re-review to immutable deltas and require a complete finite-state inventory before review or remediation.
rucka added a commit that referenced this pull request Sep 1, 2026
Round 2 of PR #475.

- formatDrift's closing paragraph branches on emptiedSections: a sparse tree
  reads "Once the tree is complete, regenerate with `pair update`", so the
  message no longer ends with the imperative the caution above it forbids.
- the catch-path test drives main() with an injected throwing readdir instead
  of an if-wrapped chmod probe that ran zero assertions under root; the chmod
  variant survives as an explicitly skipped case.
- ADL restated: the invariant is determinism, the comparator is UTF-16
  code-unit order (= codepoint for every BMP path). No comparator change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
@rucka
rucka force-pushed the feature/US-416-llms-txt-drift-gate branch from 4bc1f26 to a7de384 Compare September 1, 2026 16:20
rucka added a commit that referenced this pull request Sep 1, 2026
…depoint

Review round 1 on PR #475.

- `llms-index:check` was a bare `ts-node`, so it type-checked a source file whose
  first line imports @pair/content-ops TYPES from dist/. On a fresh `pnpm install`
  the gate produced none of its three outcomes — TS2307 + a ts-node stack, exit 1.
  Now `ts-node -T`; type-checking that source is ts:check's job, and turbo ts:check
  is the task carrying the ^build edge.
- compareIndex diffed through Sets, so a duplicated entry line (the shape a
  "keep both sides" merge of llms.txt produces) reported `0 missing / 0 extra` plus
  "the difference is their order or whitespace" — wrong, in the case that most needs
  the diff. Multiset deltas now.
- localeCompare with no locale sorted the index by the runtime's ICU default: 458 of
  560 entries move under a small-icu Node, so the gate went red on an untouched tree
  and its own message told the contributor to commit the churn. Codepoint order;
  .pair/llms.txt regenerated through writeProjectLlmsTxt.
- main() had no try/catch: an unreadable KB dir (chmod 000) escaped as an unhandled
  rejection instead of the broken-setup outcome. Caught, reported, exit 1.
- A tree missing ONE whole section clears the broken-setup guard and is reported as
  drift telling the contributor to regenerate — which deletes that section from the
  index. Caution line when a tracked heading has zero generated entries.
- dev-tools README Tools table: the gate had no row (plus the two stale ones).

ADLs: codepoint-sort rule recorded; the source-import ADL gains the -T rationale.
rucka added a commit that referenced this pull request Sep 1, 2026
Round 2 of PR #475.

- formatDrift's closing paragraph branches on emptiedSections: a sparse tree
  reads "Once the tree is complete, regenerate with `pair update`", so the
  message no longer ends with the imperative the caution above it forbids.
- the catch-path test drives main() with an injected throwing readdir instead
  of an if-wrapped chmod probe that ran zero assertions under root; the chmod
  variant survives as an explicitly skipped case.
- ADL restated: the invariant is determinism, the comparator is UTF-16
  code-unit order (= codepoint for every BMP path). No comparator change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
rucka and others added 9 commits September 1, 2026 21:25
Require real boundary evidence for externally-defined state and repair claims.\n\nRefs: #416
- 13 cases on self-contained temp KB trees: in-sync passes; un-indexed
  guideline listed as missing; deleted KB file listed as extra; empty and
  absent tracked file are drift not a crash; missing/partial tree is a
  distinct broken-setup outcome; failing run leaves the file byte-identical;
  generator and check both deterministic
- expected index is never hardcoded — asserted against generateLlmsTxt's
  own output, so #393's newline form is consumed, not re-litigated (AC6)
- Task: T-1 — Write failing fixture-based tests for the llms.txt drift check

Refs: #416
…ts generator

- new `llms-txt-drift-check` in dev-tools: generate → read tracked → byte
  compare; on drift it prints the missing/extra lines and names `pair update`,
  and a missing/partial KB tree is a distinct broken-setup outcome, not drift
- `generateLlmsTxt` now takes `LlmsSourceFs`, a read-only 3-method slice, and
  the gate's adapter has no write primitive — "this gate cannot write the file
  it judges" is a type fact, not a review promise
- the generator is imported by SOURCE path so the gate judges the working tree,
  never a stale dist/. That costs `composite: false` on @pair/dev-tools (an
  emit-time rootDir invariant on a package that emits nothing) and a
  @pair/content-ops devDep for the build-ORDER edge — both argued in the ADL,
  both repeated at their edit sites
- #216's byte-equality vitest guard is REPLACED, not duplicated: the
  conformance test keeps only its output-shape assertions
- Task: T-2 — Implement the llms.txt drift-check module

Refs: #416
…gate

- root `llms-index:check` alias + a step in the gate chain next to
  skills:conformance; ci.yml gets the SAME command as a named step, after
  `Run build` (ts-node type-checks the generator against content-ops' types)
- a unit test could not enforce this: a KB-only change touches nothing
  `turbo test` hashes, so it would replay a cached PASS — the #400 rule
- way-of-working records the gate: what it compares, that it names
  `pair update`, and that it never writes
- validated empirically: an un-indexed guideline exits 1 naming the missing
  line, leaves .pair/llms.txt byte-identical; the clean tree exits 0
- Task: T-3 — Wire the check into ci.yml + root quality-gate

Refs: #416
…depoint

Review round 1 on PR #475.

- `llms-index:check` was a bare `ts-node`, so it type-checked a source file whose
  first line imports @pair/content-ops TYPES from dist/. On a fresh `pnpm install`
  the gate produced none of its three outcomes — TS2307 + a ts-node stack, exit 1.
  Now `ts-node -T`; type-checking that source is ts:check's job, and turbo ts:check
  is the task carrying the ^build edge.
- compareIndex diffed through Sets, so a duplicated entry line (the shape a
  "keep both sides" merge of llms.txt produces) reported `0 missing / 0 extra` plus
  "the difference is their order or whitespace" — wrong, in the case that most needs
  the diff. Multiset deltas now.
- localeCompare with no locale sorted the index by the runtime's ICU default: 458 of
  560 entries move under a small-icu Node, so the gate went red on an untouched tree
  and its own message told the contributor to commit the churn. Codepoint order;
  .pair/llms.txt regenerated through writeProjectLlmsTxt.
- main() had no try/catch: an unreadable KB dir (chmod 000) escaped as an unhandled
  rejection instead of the broken-setup outcome. Caught, reported, exit 1.
- A tree missing ONE whole section clears the broken-setup guard and is reported as
  drift telling the contributor to regenerate — which deletes that section from the
  index. Caution line when a tracked heading has zero generated entries.
- dev-tools README Tools table: the gate had no row (plus the two stale ones).

ADLs: codepoint-sort rule recorded; the source-import ADL gains the -T rationale.
Round 2 of PR #475.

- formatDrift's closing paragraph branches on emptiedSections: a sparse tree
  reads "Once the tree is complete, regenerate with `pair update`", so the
  message no longer ends with the imperative the caution above it forbids.
- the catch-path test drives main() with an injected throwing readdir instead
  of an if-wrapped chmod probe that ran zero assertions under root; the chmod
  variant survives as an explicitly skipped case.
- ADL restated: the invariant is determinism, the comparator is UTF-16
  code-unit order (= codepoint for every BMP path). No comparator change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
Review round 3.

- changeset (patch, @pair/pair-cli): the comparator change rewrites every
  adopter's .pair/llms.txt on their next `pair update`; ADL
  2026-08-20-a-user-facing-cli-fix-carries-its-changeset.md requires the
  artifact in the PR that makes the change, release timing stays human.
- .gitattributes: `* text=auto eol=lf` + the index by name. Without it a
  `core.autocrlf=true` clone got 562 missing + 562 extra lines and advice
  that loops (regenerate writes LF, next checkout restores the CRs).
- the gate normalizes the terminator before diffing, flags `trackedUsesCrlf`,
  suppresses the order/whitespace sentence and conditions the call to action
  on "once the checkout is normalized to LF" — real drift stays visible.
- ADL: extended to the byte-reproducibility invariant (order + terminator)
  and gains the `## Adoption Impact` section the template requires.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
…ic one

Found by running the message's own advice on a `core.autocrlf=true` clone:
`git add --renormalize` is INERT when the index side is already LF (which is
what .gitattributes guarantees) — it staged nothing, all 583 CR-carrying
lines stayed on disk and the gate stayed red. `rm` + `git checkout --` is
what forces git to rewrite the file under the attribute; verified to take
the same clone from w/crlf to w/lf and the gate to exit 0.

Also: strip a RUN of trailing CRs, not one, so a doubled terminator
(what a hand-rolled s/\n/\r\n/ leaves on an already-CRLF file) reports as a
terminator mismatch instead of every line missing AND extra.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQJzGMhRqBRRboxMrRqFPP
…p figures are the measured ones

Terminator domain closed. The gate split on `\n` only, so a bare-CR file (classic
Mac; a hand-rolled `s/\n/\r/`, never git) collapsed into ONE segment: on the real
index 570 `missing` + 1 giant concatenated `extra`, no caution, and the closing
advice was the bare `pair update` the CRLF branch exists to avoid. Split is now
`/\r\n|\r|\n/`, the flag is `trackedCarriesCr` (set from any `\r`), and the caution
reads "carriage return (CRLF, or a bare CR)". One test row per terminator state:
LF, CRLF, mixed, doubled `\r\r\n`, bare CR, CR+LF, stray CR at EOF.

Figures: the ADL, the source comment and the test comment said 562 content lines /
~1124-line dump. Measured: 583 physical, 570 non-blank, 562 ENTRIES — and the
pre-normalization gate on a CRLF copy of the real index prints 570 + 570 over 1153
lines. Corrected to 570 / ~1140, matching way-of-working.md and the PR body.
@rucka
rucka force-pushed the feature/US-416-llms-txt-drift-gate branch from 3580a61 to a5235fa Compare September 1, 2026 19:48
… recipe touches one file

- DEVELOPMENT.md + its website twin enumerate the real `quality-gate` order
  (adds workflows:test, smoke-modes:check, llms-index:check) and name the
  llms-index remedy.
- `carriageReturnCaution` drops `git config core.autocrlf false`: the `eol=lf`
  attribute overrides autocrlf on its own (verified on a core.autocrlf=true
  clone — `rm` + `git checkout --` alone → w/lf, 0 CRs, gate green, config
  untouched), so the step rewrote repo-local config for every file to fix one.
  Test flipped first (RED), ADL rule 2 updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The "byte equality above" reference dangled: that guard moved to
llms-txt-drift-check.ts and no longer lives in this file.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rucka

This comment has been minimized.

…like lines show their bytes

- tracked-file read gets its own catch → `unreadable-index` outcome naming the FILE
  (EACCES / EISDIR) and `git checkout -- .pair/llms.txt` (verified on git 2.55 to
  replace a chmod-000 file, a directory, a dangling symlink); the KB-tree text is
  reserved for generator failures
- missing/extra pairs that differ only in invisible bytes (BOM, trailing/leading
  whitespace, NBSP, zero-width) render quoted with \uXXXX escapes + one caution;
  a leading BOM gets its own caution (`pair update` verified to rewrite without it)
- 16 new rows, one per state; 52 passed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfhvsS5rippi6aUbbGrf6F
…e shows its bytes

- ZERO_WIDTH is \p{Cf}+\p{Zl}+\p{Zp}+\p{Variation_Selector} (432 code points, Node 24),
  not a 7-entry hand list: bidi marks U+200E/F, U+061C, U+2066-9, U+202A-E, U+2061-4,
  U+180E, VS U+FE00-F now detected; astral members escaped as \u{XXXXX} via codePointAt
- an UNPAIRED line carrying an invisible char is escaped too (lone U+200B no longer
  prints as a blank line under `1 extra line(s):`)
- caution counts the quoted LINES, not distinct visible forms; wording names the classes
- one SPACE_LIKE_CLASS feeds key, rendering and the carries-invisible test
- runs of 2+ spaces collapse in the key and print as  … in the rendering

Rows: 13 BMP Cf/Zl/VS members, 3 astral, U+200E on a real tree, lone U+200B, unpaired
NBSP, count=3, doubled space, single space literal, 8 space-likes detected AND escaped,
astral emoji stays raw, unpaired plain line stays raw. Suite 52 -> 84.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfhvsS5rippi6aUbbGrf6F
…its format one

`\p{Cf}` misses 4036 Default_Ignorable code points (U+034F, the Hangul
fillers, reserved-but-ignorable blocks) — a CGJ look-alike pair printed as
two identical raw lines with no caution — and carries 32 code points a font
DOES draw (Arabic number signs, END OF AYAH, Kaithi/Egyptian format
controls), quoted under a caution claiming the terminal does not show them.

Class is now `\p{Default_Ignorable_Code_Point}\p{Zl}\p{Zp}` (VS ⊂ DI,
DI ∩ White_Space = ∅, measured on Node 24.15.0). One test row per
(range × general category) of DI \ Cf, one negative row per range of Cf \ DI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfhvsS5rippi6aUbbGrf6F
@rucka

This comment has been minimized.

…hat exists

Separator: entry paths built with posix.join; join stays for fs access. join is
platform-bound, so a Windows run emitted .pair\\knowledge\\... — unresolvable links
shipped by pair install/update, and a different sort key (\\ U+005C vs / U+002F). Real
path.win32 over the real index: 562 missing + 562 extra, no caution naming it.

Remedy: REGENERATION_COMMAND is pnpm llms-index:regen (llms-txt-regenerate.ts), the
check's exact inverse — one file, same generator, same tracked path — refusing on
every state whose caution says not to regenerate. pair update named no executable
here and pair-cli update without --source reverts the guideline that reddened the
gate. On the write-mode offender list.

Tests: AC-5 asserts the literal, not the constant against itself, plus the script
name resolved against the real root package.json.
…hat exists

`way-of-working.md`'s KB-index bullet credited `pair update` with generating
`.pair/llms.txt` and, 100 words later, denied that binary exists — re-teaching in
the adoption file the exact string ADL 2026-09-03 retired. The gate header carried
the same juxtaposition (producer at line 5, "no `pair` on `PATH`" at line 75).

Producer is now `pair-cli install` / `pair-cli update` in both cells; `pair update`
survives once, labelled as the retired spelling. Proved against the real artifact:
pnpm-packed pair-cli installed into an empty project links `pair-cli` and no `pair`,
and `pair-cli install`/`update` write (and restore, byte-identical) `.pair/llms.txt`.
@rucka

rucka commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

In response to the first review: #475 (comment)

Remediation — final synthesis (converged, entire cycle)

This cycle re-opened twice after prior convergences (comments #issuecomment-5499957423 and #issuecomment-5511252662, minimized below — each time a fresh independent re-review surfaced new findings, and this run found none). Full audit of every finding raised against this PR across all review runs and every fix round, rounds 0–8 carried over verbatim from the prior synthesis, rounds 9–11 from .pair/working/reviews/416.md, round 12 the current re-review (zero actionable, all dispositioned below).

round severity location resolution commit
0 Major apps/pair-cli/src/registry/llms-generation.ts:55localeCompare is locale-dependent (cs_CZ/et_EE reorder the tracked index) Comparator pinned to UTF-16 code-unit (codepoint) order, environment-independent; .pair/llms.txt regenerated 55047883
0 Minor packages/dev-tools/src/quality-gates/llms-txt-drift-check.ts:125hasIndexableSection only catches a tree with zero indexable sections, not one missing section emptiedSections added: a heading present in the tracked file with zero generated entries prints a "do not regenerate" caution ahead of the regen advice 55047883
0 Minor package.json:32 / packages/dev-tools/package.json:17llms-index:check dies with TS2307 on an unbuilt tree; PR's own Reviewer Guide fails as written Script switched to ts-node -T; verdict prints correctly on a clean, unbuilt checkout 55047883
0 Minor apps/pair-cli/src/registry/llms-index-conformance.test.ts:35 — stale comment: "Byte equality above" refers to a guard no longer in this file Comment repointed to llms-txt-drift-check.ts, the guard's real location e58f21db
0 Minor packages/dev-tools/src/quality-gates/llms-txt-drift-check.ts:211main() has no try/catch; an I/O error (e.g. EACCES) surfaces as an unhandled-rejection stack, not the broken-setup outcome main() wraps the body, prints formatUnreadableTree(...) with the underlying error, sets process.exitCode = 1 55047883
1 Major packages/dev-tools/package.json:17 (+ package.json:32, ci.yml:115) — gate dies with TS2307 standalone (unbuilt cross-package source import) ts-node -T; ts:check (turbo ^build edge) kept for the type check 55047883
1 Major llms-txt-drift-check.ts:104-118compareIndex diffs via Sets, losing line multiplicity; a duplicated entry surfaces as 0 missing / 0 extra + a false "order or whitespace" message Diff rewritten as multisets (per-line occurrence budget); duplicate now reports as extra 55047883
1 Minor llms-generation.ts:55 — determinism assertion runs the generator twice in one process, can't see locale drift Codepoint comparator (see round 0) + fixture asserting a pair where ICU and codepoint disagree (PRD.md vs context-map.md) 55047883
1 Minor llms-txt-drift-check.ts:211-223main() uncaught I/O error try/catch, broken-setup-shaped message, process.exitCode = 1; 3 main tests incl. real chmod 000 55047883
1 Minor llms-txt-drift-check.ts:125-127 — emptied section reported as ordinary drift DriftReport.emptiedSections; caution printed ahead of regen advice 55047883
1 Minor PR #475 description — AC numbering didn't match story #416 Renumbered against the story's AC list (PR body)
1 Minor packages/dev-tools/README.md:7-14 — Tools table missing llms-index:check row (+2 pre-existing stale rows) Row added; two stale rows fixed; count-based prose removed 55047883
2 Minor llms-txt-drift-check.ts:236-241 — closing paragraph always printed the bare regen imperative, even with an emptied section callToAction(treeLooksComplete) branch; precondition-gated when a section was emptied 4bc1f26b
2 Minor llms-txt-drift-check.test.ts:288-320 — catch-path test conditioned on uid (root skips assertions silently) Injected-error test always runs; chmod 000 variant calls ctx.skip(reason) instead of an empty body when the bit doesn't bind 4bc1f26b
2 Questions ADL …sorts-by-codepoint.md + llms-generation.ts:55 — comparator's invariant mischaracterized as a collation choice Restated as determinism; comparator named as UTF-16 code-unit order; propagated to way-of-working.md, comment, test name 4bc1f26b
3 Major llms-generation.ts:69 + .changeset/ — user-facing reordering shipped with no changeset .changeset/llms-txt-deterministic-order.md added (patch on @pair/pair-cli) 5ff87240
3 Minor ADL …sorts-by-codepoint.md:88-100 — Adoption Impact section missing Section appended; ADL title widened to cover the terminator axis 5ff87240
3 Minor PR #475 body — stale "Not this story" bullet Deleted, body rewritten against current head (PR body)
3 Questions llms-txt-drift-check.ts:115-117 — CRLF-checkout platform scope unresolved .gitattributes (* text=auto eol=lf) + gate strips trailing CR, prints terminator caution, gates its call-to-action on a normalized checkout 5ff87240, d3284372
3 (self-correction) 5ff87240's own printed recipe (git add --renormalize) didn't converge the loop it prescribed Recipe replaced with rm + git checkout --; proven to end the loop d3284372
4 Minor ADL / llms-txt-drift-check.ts:125 / test:242 — stale figures (562/~1124) didn't match measured 570/~1140 Figures corrected; ADL states the three quantities that must reconcile a5235fa0
4 Questions llms-txt-drift-check.ts:129-134,190 — terminator handling covered CRLF only, not bare CR / mixed / doubled \r LINE_TERMINATOR regex split on the full domain; flag renamed trackedCarriesCr; one test row per terminator variant a5235fa0
5 Minor DEVELOPMENT.md:124 — documented gate chain (9 of 12 steps) didn't match the executed quality-gate chain Enumeration updated to the executed chain/order in both DEVELOPMENT.md and its mdx twin 62fb750b
5 Questions llms-txt-drift-check.ts:263 — printed recipe's git config core.autocrlf false step unreachable/unjustified Dropped; recipe reduced to rm + git checkout --; RED-first test asserts its absence 62fb750b, e58f21db
6 Minor llms-txt-drift-check.ts:405-414 + :389-390 (tracked read inside the tree try) Tracked-file read moved to its own try/catch → new DriftReport kind unreadable-index (path, detail); formatReport prints could not read the tracked index <path> + error + git checkout -- .pair/llms.txt; formatUnreadableTree now sees generator failures only. Rows: EACCES (injected + real chmod-000, root-skipped), EISDIR, dangling symlink (→ absent-file drift, pinned), KB-tree EACCES control e6cd2ff9
6 Minor llms-txt-drift-check.ts:140-142 / :211-214 (a) trackedCarriesBom flag + byteOrderMarkCaution (no precondition: regeneration fixes it); (b) renderLines renders a missing/extra pair whose visibleForm matches as JSON.stringify + \uXXXX for BOM/zero-width/NBSP-class chars, plus one invisibleDifferenceCaution; visibly different lines stay raw. Rows: BOM, trailing space, leading space, NBSP, ZWSP, raw control, mixed (2 look-alikes + 1 real), BOM+CRLF, BOM-free flag, compareIndex BOM at start / not at start e6cd2ff9
6 Minor PR #475 description "Files Changed" Count kept at 18 — verified against git diff and gh pr view at both reviewed head and current head; enumeration = 6 added + 12 modified PR body (publish-pr)
7 Minor llms-txt-drift-check.ts:234 (ZERO_WIDTH) Class widened to [\p{Cf}\p{Zl}\p{Zp}\p{Variation_Selector}] with u; hex uses codePointAt, prints \u{XXXXX} above the BMP. Rows: 13 BMP members, 3 astral, real fixture tree, astral emoji stays raw 3969a4eb
7 Minor :268-273 (lookAlikeForms / renderLines) needsEscaping = paired || INVISIBLE.test(line) — an unpaired line carrying any class member is escaped and counted 3969a4eb
7 Minor :283-290 + :412 (caution count) Count = escaped lines across both lists; wording spells out the character classes 3969a4eb
7 Minor :242 vs :262 (SPACE_LIKE duplicated) One SPACE_LIKE_CLASS string builds SPACE_LIKE (+\t), SPACE_LIKE_ESCAPED, INVISIBLE; it.each over all 8 members asserts detected AND escaped 3969a4eb
7 Questions :251 (visibleForm, space runs) visibleForm collapses / {2,}/ → one space; escapeInvisible spells a run of 2+ spaces; single space stays literal 3969a4eb
8 Minor llms-txt-drift-check.ts:240 (ZERO_WIDTH_CLASS) Class is \p{Default_Ignorable_Code_Point}\p{Zl}\p{Zp}; JSDoc names DI as the class and lists DI \ Cf + Cf \ DI. Rows: one per range × general category of DI \ Cf (Mn/Lo/Cn/astral) d789dc31
8 Questions :240 / :329-333 (visible Cf under the caution) Tightened: \p{Cf} dropped (⊃ \p{Variation_Selector}, itself ⊂ DI) — DI's contract is "renders as nothing", keeping Cf would leave the caution's claim false for 32 members. Negative rows: one per range of Cf \ DI, all raw + no caution d789dc31
9 Major apps/pair-cli/src/registry/llms-generation.ts:45,51posix.join-mixed paths break under Windows path semantics Emitted entry paths built with posix.join; join kept for fs access only; llms-generation.win32.test.ts runs the generator against real path.win32 (5 rows: byte-equality with the POSIX doc, no backslash at any depth, sections found, separator-as-sort-key order); ADL …sorts-by-codepoint.md gains rule 3 d99834e2
9 Major packages/dev-tools/src/quality-gates/llms-txt-drift-check.ts:73REGENERATION_COMMAND named a command with no real executable (pair update) REGENERATION_COMMANDpnpm llms-index:regen; new llms-txt-regenerate.ts (+11-row suite), the check's exact inverse, refusing on broken-setup/unreadable-index/unreadable tree/trackedCarriesCr/emptiedSections; root + package scripts added; DEVELOPMENT.md, its mdx twin, way-of-working.md, dev-tools README moved with it; ADL 2026-09-03-a-gate-names-a-remedy-it-can-run.md d99834e2
9 Minor packages/dev-tools/src/quality-gates/llms-txt-drift-check.test.ts:124-132 — AC-5 row asserted the stale literal Asserts the literal pnpm llms-index:regen; two new rows — script name resolved against the real root package.json, REGENERATION_COMMAND never matching \bpair\b(?!-cli) d99834e2
10 re-dispatch of the round-9 finding set, authored against the round-8 head, not the branch head No code change — all three already fixed at d99834e2; re-verified line-by-line against the pushed head (evidence re-run in the log) d99834e2 (round 9)
11 Minor .pair/adoption/tech/way-of-working.md:88 (+ llms-txt-drift-check.ts:5-6, same contract cell, same fix) — producer clause named pair update/pair install then denied pair update exists, a self-contradiction Opening clause now names the real producer (pair-cli install/pair-cli update, writeProjectLlmsTxt); later clause records pair update as the retired spelling that names no executable; same fix applied to the gate module's header ade14617

Accepted / non-actionable

location severity disposition
ADL 2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md:72 (also llms-txt-drift-check.ts:246) Questions Historical record — "all 583 CR-carrying lines stay on disk" is a measurement scoped "at this commit" (ADL L46); figure diverges across later commits as expected for an intermediate-commit measurement, not an error. Non-blocking.
.github/workflows/ci.yml job secret-scan on head d33cc67c / scripts/smoke-tests/scenarios/pr-state-flow.sh @ 5e99d538 Questions Pre-existing on main since 5e99d538 — this PR's own commits scan clean; resolves independently of this diff.
apps/pair-cli/src/commands/update/handler.ts (untouched by PR) — behaviour of the printed remedy pair update (pre-round-9 remedy) Questions By convention at the time — superseded by round 9's fix, which replaced the remedy with pnpm llms-index:regen for exactly this reason (blast radius on unrelated files).
packages/dev-tools/tsconfig.json:19exclude: ["node_modules", "**/*.test.ts"] keeps the new 1261-line test file outside tsc --noEmit, checked only incidentally via vitest transpile Minor Matches the package's existing convention — exclude is unchanged by this PR; widening it pulls every dev-tools suite into ts:check as unrelated scope.
packages/dev-tools/src/quality-gates/llms-txt-regenerate.ts:141 — refusal prints check.message verbatim, itself the check's closing line, so the regenerator's own trailing sentence ("This check never writes .pair/llms.txt…") can read as "you ran the wrong command" Minor By design (ADL 2026-09-03-a-gate-names-a-remedy-it-can-run.md decision point 2: refusal prints the gate's OWN message, never a second wording); the very next line, Nothing was written to <path>., disambiguates. Concrete probe: real CRLF-converted .pair/llms.txt + real pnpm llms-index:regen → exit 1, file byte-identical.
packages/dev-tools/src/quality-gates/llms-txt-regenerate.ts:120 — sparse-tree refusal triggers on a whole-section loss only; a partial loss inside a surviving section regenerates without caution Questions Below severity floor — carried to the merge gate unfixed. Concrete probe: real repo, one file moved out of a non-emptying section → pnpm llms-index:regen prints ✓ regenerated, diff shows the missing file committed as a deletion. Legitimate by design (a real removal is byte-identical to a partial checkout); only the threshold statement is undocumented.
.pair/adoption/tech/way-of-working.md:88 (+ llms-txt-drift-check.ts:5-6) — "on every pair-cli install/update" is false when tally.ok === 0 (all registries fail) Questions Below severity floor — carried to the merge gate unfixed. Real call sites (install/handler.ts:423-425, update/handler.ts:239-241) gate the write on tally.ok > 0; quantifier pre-dates this delta (only the command name changed).
DEVELOPMENT.md:132 and :155 — still teach the retired pair update spelling (prescriptive remedy line + CLI command table), one paragraph below this PR's corrected line; real probe: which pair → not found, pair-cli publishes as the only bin Minor Below severity floor — carried to the merge gate unfixed. Out of the fix delta (d99834e2...ade14617) and unchanged surface in prior rounds; human call at merge: sweep to pair-cli in this PR or confirm "retired" is scoped to the gate's printed remedy only.

Final verdict: review clean — zero actionable findings on the latest independent re-review (round 12); every finding across all runs of this cycle (rounds 0–8 from the prior syntheses, rounds 9–11 from .pair/working/reviews/416.md, round 12 above) resolved in place or dispositioned above.

@rucka
rucka force-pushed the main branch 2 times, most recently from 7b55900 to adb9627 Compare September 8, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-state:to-be-reviewed PR state: awaiting review / gate risk:green Classification: low risk tier tech-debt Tracked technical debt (living backlog, R7.2 — never blocks a PR) user story Work item representing a user story

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant