Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
297f2e5
[US-219] fix: front-load review contract coverage
rucka Sep 1, 2026
fa4fc10
[US-219] fix: prove external boundaries
rucka Sep 1, 2026
5b7d069
[#416] test: fixture-based RED suite for the llms.txt drift gate
rucka Sep 1, 2026
c7d6301
[#416] feat: the drift gate — the tracked llms.txt compared against i…
rucka Sep 1, 2026
42ca9e1
[#416] chore: wire llms-index:check into ci.yml and the root quality-…
rucka Sep 1, 2026
bebad61
[#416] fix: the drift gate runs unbuilt, diffs multisets, sorts by co…
rucka Sep 1, 2026
b4426d4
[#416] fix: the sparse-tree caution now conditions the call to action
rucka Sep 1, 2026
1022db1
[#416] fix: LF is pinned and a CRLF checkout is diagnosed, not dumped
rucka Sep 1, 2026
b332a41
[#416] fix: the CRLF exit is the recipe that was run, not the idiomat…
rucka Sep 1, 2026
a5235fa
[#416] fix: any carriage return is the terminator branch, and the dum…
rucka Sep 1, 2026
62fb750
[#416] fix: the documented gate chain is the executed one, and the CR…
rucka Sep 1, 2026
e58f21d
[#416] fix: point the stale byte-equality comment at the drift gate
rucka Sep 1, 2026
e6cd2ff
[#416] fix: an unreadable index is not an unreadable tree, and look-a…
rucka Sep 2, 2026
3969a4e
[#416] fix: the invisible class is Unicode's, and every invisible lin…
rucka Sep 2, 2026
d789dc3
[#416] fix: the invisible class is Unicode's rendering property, not …
rucka Sep 2, 2026
d99834e
[#416] fix: paths are POSIX everywhere, and the remedy is a command t…
rucka Sep 3, 2026
ade1461
[#416] fix: the index's producer is named once, and it is a command t…
rucka Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/llms-txt-deterministic-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@pair/pair-cli': patch
---

`.pair/llms.txt` is generated in a deterministic, locale-independent order. Entries were sorted with `localeCompare`, which passes no locale and resolves against the runtime's ICU default: the same tree produced a different file on a Node built with full ICU than on one built with `small-icu`, so the index's bytes were a property of the machine that ran `pair install` / `pair update` rather than of the knowledge base. Sorting now uses the strings' own code units, so every environment emits the same file.

Consequence for an existing project: the next `pair install` / `pair update` rewrites `.pair/llms.txt` in the new order — uppercase-first entries (`PRD.md`, `README.md`, `ADR-*`) sort before their lowercase siblings within each section. It is a one-time reordering of a generated file, with no entry added, removed or changed.

`.pair/llms.txt` also uses POSIX separators in every entry path, on every platform. The paths were built with `path.join`, which is bound to the host: on Windows the generated index read `- [Product Requirements Document (PRD)](.pair\\adoption\\product\\PRD.md)` — a link no markdown renderer and no agent resolves — so a Windows adopter's `pair install` / `pair update` produced an index whose 562 links were all broken for everyone else. The separator was also a sort key (`\\` is U+005C, `/` is U+002F), so entry order differed by platform too. File-system access is unchanged and still uses the platform separator.
79 changes: 62 additions & 17 deletions .claude/workflows/pair-implement-batch.js

Large diffs are not rendered by default.

159 changes: 155 additions & 4 deletions .claude/workflows/pair-implement-batch.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,25 @@ const SRC = readFileSync(new URL('./pair-implement-batch.js', import.meta.url),
'',
)
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor
const REVIEWED_HEAD = 'a'.repeat(40)

async function runWorkflow({ args, dispatch }) {
const calls = []
const agent = async (prompt, opts) => {
calls.push({ prompt, opts })
return dispatch(prompt, opts)
const result = await dispatch(prompt, opts)
// A real reviewer now returns the immutable revision it reviewed. Keep legacy
// fixtures concise while allowing focused tests to provide an invalid/missing
// value explicitly.
if (
opts.agentType === 'pair-reviewer' &&
result &&
typeof result === 'object' &&
String(result.verdict ?? '').trim() &&
result.reviewedHead === undefined
)
return { ...result, reviewedHead: REVIEWED_HEAD }
return result
}
// Mirrors the real primitive's contract: "a thunk that throws (or whose agent errors)
// resolves to null in the result array — the call itself never rejects". The earlier
Expand Down Expand Up @@ -105,7 +118,14 @@ test('valid contract: reviewer schema derives from contract.json (AC1) and cache
dispatch: stdDispatch({ contractResult: { status: 'cache-hit', contract } }),
})
const rev = calls.find(c => c.opts.agentType === 'pair-reviewer')
assert.deepEqual(rev.opts.schema, contract.schema)
assert.deepEqual(rev.opts.schema, {
...contract.schema,
properties: {
...contract.schema.properties,
reviewedHead: { type: 'string', pattern: '^[0-9a-f]{40}$' },
},
required: ['verdict', 'reviewedHead'],
})
assert.ok(rev.prompt.includes('Blocker'), 'severity vocabulary threaded from the contract')
assert.ok(rev.prompt.includes('Rework'), 'verdict vocabulary threaded from the contract')
assert.deepEqual(result.contracts, [{ name: 'code-review', status: 'cache-hit' }])
Expand Down Expand Up @@ -212,8 +232,16 @@ test('contract with usable schema but missing canonical vocabulary keys: prompt
dispatch: stdDispatch({ contractResult: { status: 'cache-hit', contract } }),
})
const rev = calls.find(c => c.opts.agentType === 'pair-reviewer')
// Schema is still enum-locked from the (structurally usable) contract...
assert.deepEqual(rev.opts.schema, contract.schema)
// Schema is still enum-locked from the (structurally usable) contract, with
// the orchestration-owned reviewed revision layered on top.
assert.deepEqual(rev.opts.schema, {
...contract.schema,
properties: {
...contract.schema.properties,
reviewedHead: { type: 'string', pattern: '^[0-9a-f]{40}$' },
},
required: ['verdict', 'reviewedHead'],
})
// ...but the prompt vocabulary text falls back to the documented defaults,
// since verdictOptions/severities (the canonical keys it's threaded from)
// are absent. In practice ensure-contract.mjs's validateContract now rejects
Expand Down Expand Up @@ -1002,6 +1030,129 @@ test('the fix step is likewise barred from deferring a finding into a new issue'
)
})

test('the fix step sweeps the bounded contract surface before re-review', async () => {
const finding = { location: 'x.ts:1', severity: 'Major', description: 'd', recommendation: 'r' }
let round = 0
const { calls } = await runWorkflow({
args: { stories: [STORY] },
dispatch: (prompt, opts) => {
if (opts.agentType === 'pair-contract-generator') return { status: 'cache-hit', contract: validContract() }
if (opts.agentType === 'pair-reviewer') return round++ === 0 ? { verdict: 'Rework', findings: [finding] } : { verdict: 'Approved', findings: [] }
if (opts.phase === 'Implement') return { gatesPassed: true, branch: 'b' }
if (opts.phase === 'PR') return { prNumber: 7 }
return { fixed: true }
},
})

const fix = calls.find(c => c.opts.label?.startsWith('fix:')).prompt
assert.match(fix, /CONVERGENCE SWEEP/, 'the fixer must make the bounded contract explicit')
assert.match(fix, /location is the starting point/i, 'a finding location is not the contract boundary')
assert.match(fix, /success\/failure/i, 'paired execution paths are checked together')
assert.match(fix, /every distributed representation/i, 'source and shipped representations are checked together')
assert.match(fix, /PROVISIONED ARTIFACT CONTRACT/, 'a provisioned command has an explicit end-to-end check')
assert.match(fix, /producer.*published identity.*consumer/i, 'the provisioner, artifact metadata and invocation are mapped together')
assert.match(fix, /clean temporary environment/i, 'the actual installed or built artifact is exercised')
assert.match(fix, /never stub.*boundary/i, 'a stub cannot stand in for the published command boundary')
assert.match(fix, /unrelated cleanup/i, 'the sweep stays bounded and is not scope creep')
assert.doesNotMatch(fix, /touch ONLY what each finding's location names/, 'line-only scope discipline would recreate the gap')
})

test('review and fix exhaust finite protocol states before another round', async () => {
const finding = { location: 'state.ts:1', severity: 'Major', description: 'd', recommendation: 'r' }
let round = 0
const { calls } = await runWorkflow({
args: { stories: [STORY] },
dispatch: (prompt, opts) => {
if (opts.agentType === 'pair-contract-generator') return { status: 'cache-hit', contract: validContract() }
if (opts.agentType === 'pair-reviewer') return round++ === 0 ? { verdict: 'Rework', findings: [finding] } : { verdict: 'Approved', findings: [] }
if (opts.phase === 'Implement') return { gatesPassed: true, branch: 'b' }
if (opts.phase === 'PR') return { prNumber: 7 }
return { fixed: true }
},
})
const review = calls.find(c => c.opts.agentType === 'pair-reviewer').prompt
const fix = calls.find(c => c.opts.label?.startsWith('fix:')).prompt
assert.ok(review.includes('CONTRACT INVENTORY (mandatory)'), 'the reviewer inventories a contract before reporting its first hole')
assert.ok(review.includes('finite decision table of every supported state'), 'a finite protocol/state space is exhausted in the same review')
assert.ok(review.includes('AUTHORITATIVE BOUNDARY PROOF (mandatory)'), 'the reviewer must prove externally-defined state semantics at the real boundary')
assert.ok(fix.includes('FINITE-STATE COMPLETENESS (mandatory when'), 'the fixer must preserve that complete state model')
assert.ok(fix.includes('Do not implement one newly discovered row at a time'), 'the next re-review is not used to discover ordinary variants serially')
assert.ok(fix.includes('A unit test of the function being changed cannot establish external semantics'), 'the fixer cannot infer external-tool behavior from its own unit tests')
})

test('re-review is anchored to the reviewed revision and checks only the fix delta plus prior findings', async () => {
const finding = { location: 'workflow.yml:4', severity: 'Major', description: 'd', recommendation: 'r' }
let round = 0
const { calls } = await runWorkflow({
args: { stories: [STORY] },
dispatch: (prompt, opts) => {
if (opts.agentType === 'pair-contract-generator') return { status: 'cache-hit', contract: validContract() }
if (opts.agentType === 'pair-reviewer')
return round++ === 0
? { verdict: 'Rework', findings: [finding] }
: { verdict: 'Approved', findings: [] }
if (opts.phase === 'Implement') return { gatesPassed: true, branch: 'b' }
if (opts.phase === 'PR') return { prNumber: 7 }
return { fixed: true }
},
})

const reviews = calls.filter(c => c.opts.agentType === 'pair-reviewer')
assert.match(reviews[0].prompt, /reviewedHead/i, 'every review returns the immutable head it covered')
assert.match(reviews[1].prompt, new RegExp(`git diff ${REVIEWED_HEAD}\\.\\.\\.origin/feat/#292-x --name-only`), 're-review inventories the fix delta, not the entire PR')
assert.match(reviews[1].prompt, new RegExp(`git diff ${REVIEWED_HEAD}\\.\\.\\.origin/feat/#292-x`), 're-review starts from the previous review baseline')
assert.match(reviews[1].prompt, /only if it is in this delta or a contract boundary changed by this delta/i, 'unchanged PR surface is not repeatedly re-audited')
})

test('a review without an immutable baseline cannot converge', async () => {
const { result, calls } = await runWorkflow({
args: { stories: [STORY] },
dispatch: (prompt, opts) => {
if (opts.agentType === 'pair-contract-generator') return { status: 'cache-hit', contract: validContract() }
if (opts.agentType === 'pair-reviewer') return { verdict: 'Approved', findings: [], reviewedHead: 'not-a-sha' }
if (opts.phase === 'Implement') return { gatesPassed: true, branch: 'b' }
if (opts.phase === 'PR') return { prNumber: 7 }
return { fixed: true }
},
})

assert.equal(result.batch[0].status, 'failed-review')
assert.equal(calls.filter(c => c.opts.agentType === 'pair-reviewer').length, 2, 'missing review evidence is retried once')
})

test('a review baseline must be lower-case like the review contract declares', async () => {
const { result } = await runWorkflow({
args: { stories: [STORY] },
dispatch: (prompt, opts) => {
if (opts.agentType === 'pair-contract-generator') return { status: 'cache-hit', contract: validContract() }
if (opts.agentType === 'pair-reviewer') return { verdict: 'Approved', findings: [], reviewedHead: 'A'.repeat(40) }
if (opts.phase === 'Implement') return { gatesPassed: true, branch: 'b' }
if (opts.phase === 'PR') return { prNumber: 7 }
return { fixed: true }
},
})

assert.equal(result.batch[0].status, 'failed-review')
})

test('accepted-findings key is collision-free for location and description pairs', async () => {
const { result } = await runWorkflow({
args: { stories: [STORY] },
dispatch: stdDispatch({
contractResult: { status: 'cache-hit', contract: validContract() },
review: {
verdict: 'Approved',
findings: [
{ location: 'a b', severity: 'Minor', description: 'c', nonActionable: true },
{ location: 'a', severity: 'Minor', description: 'b c', nonActionable: true },
],
},
}),
})

assert.equal(result.batch[0].acceptedFindings.length, 2)
})

// ── A run that drove nothing must not report success ───────────────────────
// Observed: two workflows were launched concurrently on a saturated machine, every
// implementer stalled past the supervisor's window, `parallel` returned six nulls,
Expand Down
13 changes: 13 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# LF is this repo's normal form in the index AND in the working tree, on every
# platform. CI is `ubuntu-latest` and no tracked text file carries a CR today; the
# pin keeps that true through a checkout made with `core.autocrlf=true` (git's
# Windows default), which would otherwise hand the working tree CRLF copies.
* text=auto eol=lf

# Not a preference here but a correctness constraint: `.pair/llms.txt` is generated
# with `\n` and compared BYTE FOR BYTE against a fresh generator run by
# `pnpm llms-index:check` (#416). Terminators rewritten on checkout make the gate red
# on an untouched tree, and regenerating cannot end that loop — the write lands as LF
# and the next checkout restores the CRs. See ADL
# `.pair/adoption/decision-log/2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md`.
.pair/llms.txt text eol=lf
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ jobs:
run: pnpm docs:staleness
- name: Run skills-conformance check
run: pnpm skills:conformance
# Same reason as the smoke-scenario step above, for the KB index (#416): the byte
# equality between `.pair/llms.txt` and its generator cannot be enforced by a unit
# test, because a KB-only change (a guideline added under `.pair/`) touches no
# package `turbo test` hashes — it would replay a cached PASS and the drift would
# ship. This step and the root `quality-gate` chain run the SAME command
# unconditionally, which is the whole parity point. Position in the job is free:
# the script runs `ts-node -T`, so it needs no build of @pair/content-ops (whose
# types the imported generator source references). Type-checking that source is
# `turbo ts:check`'s job, and THAT task carries the `^build` edge.
- name: Run llms-index drift check
run: pnpm llms-index:check
- name: Run duplication check
run: pnpm dup:check
# The batch engine ships as a product artifact (#219). Its dry-run suite runs inside
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Decision: Review re-checks use an immutable baseline and prove provisioned artifacts

## Date

2026-08-31

## Status

Active

## Category

Process Decision

## Context

PR #474 / story #217 ran on `89793d27`, which already required a convergence sweep. In round 4,
the fixer added a CLI installation step and retained a runner invocation, but tested a stub named
`pair` rather than the installed package's declared `pair-cli` bin. Round 5 therefore found the
new functional defect. The generic sweep named distributed representations but did not require an
end-to-end proof across installation, published identity, and invocation. Re-review also rescanned
the whole accumulated PR, so each fix expanded the next review surface.

## Decision

1. Every reviewer return includes the lower-case 40-character SHA it inspected (`reviewedHead`).
Missing or invalid evidence is retried once then fails closed; it never converges a PR.
2. The initial review remains complete. Each later re-review verifies prior findings plus
`git diff <reviewedHead>...origin/<branch>` and directly changed producer/consumer boundaries.
A new blocking finding must come from that delta or boundary; an unchanged surface is not
re-audited as a new fix round.
3. A fix touching an installed, built, published, named, or invoked artifact maps
`producer -> published identity -> consumer` and proves it in a clean temporary environment
using the real artifact. The exact boundary cannot be stubbed, aliased, or faked.

## Alternatives Considered

- **Keep full-PR scans on each round**: rejected — a growing diff makes a re-review another
independent first review and creates unbounded new scope.
- **Accept findings after a fixed round cap**: rejected — it hides genuine defects rather than
bounding their cause.
- **Only strengthen source-string tests**: rejected — #217 passed such a test while the published
CLI contract was broken.

## Consequences

- Re-review is bounded without downgrading Major or Minor findings.
- A caller resuming an older cycle has one fresh full review to establish a new baseline.
- Workflow authors must keep dataset source and root mirror byte-identical; tests cover both.

## Adoption Impact

- `.pair/adoption/tech/way-of-working.md`: records the review baseline and provisioned-artifact
proof convention.
- `packages/knowledge-hub/dataset/.workflows/pair-implement-batch.js` and its installed mirror:
enforce the convention.
- Both dry-run copies of `pair-implement-batch.test.mjs`: pin the schema, bounded re-review, and
real-artifact prompt requirements.
Loading