diff --git a/.changeset/llms-txt-deterministic-order.md b/.changeset/llms-txt-deterministic-order.md new file mode 100644 index 000000000..03c933fec --- /dev/null +++ b/.changeset/llms-txt-deterministic-order.md @@ -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. diff --git a/.claude/workflows/pair-implement-batch.js b/.claude/workflows/pair-implement-batch.js index f95817702..50b7f7002 100644 --- a/.claude/workflows/pair-implement-batch.js +++ b/.claude/workflows/pair-implement-batch.js @@ -795,7 +795,7 @@ const MAX_FIX_ROUNDS = PIPELINE.maxFixRounds // truncated structured output) — and the contentless shape is the one this repo // actually measured on #432 (the machine slept mid-response), i.e. the retry // missed the exact incident it was written for while covering its rarer sibling. -// The review step therefore passes `hasVerdict`, the SAME predicate its +// The review step therefore passes `hasReviewEvidence`, the SAME predicate its // convergence guard uses, so "did not review" means one thing at both sites: the // transient gets its second chance, and a step that comes back contentless twice // still fails closed. @@ -812,6 +812,11 @@ async function agentRetry(prompt, opts, isUsable = r => !!r) { // ONE predicate, asked by the retry and by the convergence guard, so the two // cannot drift into disagreeing about what a dead reviewer is. const hasVerdict = r => !!r && !!String(r.verdict ?? '').trim() +const REVIEWED_HEAD_PATTERN = /^[0-9a-f]{40}$/ +// A review also has to identify the immutable PR revision it actually inspected. +// Without that baseline a later reviewer cannot distinguish the fix delta from the +// already-audited PR surface, which turns each re-review into another full scan. +const hasReviewEvidence = r => hasVerdict(r) && REVIEWED_HEAD_PATTERN.test(String(r.reviewedHead ?? '')) // ── Schemas (orchestration return-value contracts) ───────────────────────── // These are the compact values agents RETURN for control-flow — NOT the artifact @@ -854,6 +859,9 @@ const LOOSE_REVIEW_SCHEMA = { // Control flow keys on `nonActionable` + actionable count, never on specific // verdict strings. verdict: { type: 'string' }, + // Immutable full SHA of the PR head reviewed. This is workflow evidence, not + // part of the human-facing review template vocabulary. + reviewedHead: { type: 'string', pattern: '^[0-9a-f]{40}$' }, needsHumanDecision: { type: 'boolean' }, findings: { type: 'array', @@ -879,7 +887,7 @@ const LOOSE_REVIEW_SCHEMA = { }, }, }, - required: ['verdict'], + required: ['verdict', 'reviewedHead'], } const FIX_SCHEMA = { type: 'object', @@ -977,7 +985,17 @@ const contracts = STORIES.length ? await parallel(CONTRACT_SPECS.map((s) => () = const crContract = contracts.find((c) => c.name === 'code-review') // Schema the reviewer returns: template-derived when the contract is usable, // the loose skeleton otherwise. Control flow stays value-agnostic either way. -const REVIEW_SCHEMA = crContract?.schema ?? LOOSE_REVIEW_SCHEMA +const REVIEW_SCHEMA_BASE = crContract?.schema ?? LOOSE_REVIEW_SCHEMA +// Template contracts own human verdict/finding vocabulary. The orchestration-only +// baseline is layered on top so a template refresh cannot accidentally remove it. +const REVIEW_SCHEMA = { + ...REVIEW_SCHEMA_BASE, + properties: { + ...REVIEW_SCHEMA_BASE.properties, + reviewedHead: { type: 'string', pattern: '^[0-9a-f]{40}$' }, + }, + required: [...new Set([...(REVIEW_SCHEMA_BASE.required ?? []), 'verdict', 'reviewedHead'])], +} // Reviewer prompt vocabulary: `verdictOptions` and `severities` are CANONICAL, // required contract keys (ensure-contract.mjs's validateContract rejects any // contract missing either) — so whenever a contract IS present, both are @@ -1006,6 +1024,16 @@ const TEXT_SHAPE = '(specific inputs/state -> the wrong output or the loss that follows) and the EVIDENCE it is real ' + '(what you ran, what it printed). Cut narration, never evidence.' +const AUTHORITATIVE_BOUNDARY_PROOF = + 'AUTHORITATIVE BOUNDARY PROOF (mandatory): when a table row, equivalence, normalization or remediation depends on an external command, service, file format or runtime, name the exact real producer/consumer that defines it and run a minimal isolated end-to-end probe for every such claim. Keep rows distinct until that boundary proves them equivalent. A unit test of the function being changed cannot establish external semantics or prove that user-facing repair advice works: apply the advice in a clean temporary environment and verify the promised postcondition.' + +const CONTRACT_INVENTORY = + 'CONTRACT INVENTORY (mandatory): before reporting findings, map each changed observable contract to its authoritative producer, inputs, consumers and representations. A FIRST review inventories every changed contract; a re-review inventories only its fix delta and directly changed boundary. For a finite protocol, parser, configuration, state transition or command-output domain, build a finite decision table of every supported state plus its invalid/boundary pair, and probe the real behavior. Report every defect that table exposes now; do not leave ordinary rows for a later review. ' + + AUTHORITATIVE_BOUNDARY_PROOF + +const FINITE_STATE_COMPLETENESS = + 'FINITE-STATE COMPLETENESS (mandatory when a change parses, selects, snapshots, or branches on a finite protocol/state domain): identify the authoritative grammar or producer, make the complete decision table of supported states and invalid/boundary cases, then write and run a real test for every row before editing the canonical source. Do not implement one newly discovered row at a time and wait for re-review to name the next ordinary variant. ' + + AUTHORITATIVE_BOUNDARY_PROOF const SEVERITIES = (REVIEW_VOCAB?.severities ?? DEFAULT_SEVERITIES).join(', ') const VERDICTS = (REVIEW_VOCAB?.verdictOptions ?? DEFAULT_VERDICTS).join(', ') @@ -1048,20 +1076,28 @@ function baseOf(story) { return String(story.base ?? '').trim() || PIPELINE.baseBranch } -function wtClause(story) { +function wtClauseBase(story) { const base = baseOf(story) return `ISOLATION (mandatory): do ALL git/file work inside a dedicated worktree at \`${PIPELINE.worktreeRoot}/${story.id}\` — create-or-reuse it: \`git worktree add ${PIPELINE.worktreeRoot}/${story.id} -B ${story.branch} ${base}\` on first setup, or \`git worktree add ${PIPELINE.worktreeRoot}/${story.id} ${story.branch}\` if the branch already has commits; if the path already exists, just \`cd\` into it. NEVER modify the repo's main working tree and NEVER switch its branch.${base === PIPELINE.baseBranch ? '' : ` This story is STACKED on \`${base}\`: that branch is its base, so its commits are already in your history and must NOT be reverted, duplicated or re-implemented — only ADD your own work on top. When you open the PR, target \`${base}\` as the PR base branch, not \`main\`, so the diff shows only this story's change.`}` } +function wtClause(story) { + return `${wtClauseBase(story)} ${FINITE_STATE_COMPLETENESS}` +} + // Reviewer isolation: read-only inspection in a DETACHED throwaway worktree pinned // to the PR's pushed head. Detached HEAD never occupies the branch, so it can't // collide with the authoring worktree (which holds it) or with other stories' // reviewers in a parallel batch — and it never touches the main checkout's branch. -function revWtClause(story) { +function revWtClauseBase(story) { const p = `${PIPELINE.worktreeRoot}/${story.id}-review` return `ISOLATION (mandatory, read-only): NEVER switch the main checkout's branch. Inspect the code in a DETACHED throwaway worktree pinned to the PR's current pushed head: \`git worktree remove --force ${p} 2>/dev/null; git fetch origin -q; git worktree add --detach ${p} origin/${story.branch}\`, then \`cd ${p}\`. Read the code there (the untracked checkpoint is absent here — good, stay blind to it). When finished, remove it: \`git worktree remove --force ${p}\`.` } +function revWtClause(story) { + return `${revWtClauseBase(story)} ${CONTRACT_INVENTORY}` +} + // #373 finding 3: the escalate-flush shared block — supersede-the-prior-flush + the manual // out-of-band CONVENTION + the untracked-worktree-persistence note — is identical across BOTH // escalation prompts (MAX_FIX_ROUNDS + needsHumanDecision). Authored ONCE here so a future @@ -1216,6 +1252,7 @@ async function driveStory(story) { // before honouring it, so the escalation is deferred by a round rather than dropped. let humanDecisionPending = false let prevFindings = [] + let prevReviewedHead = null // ACCUMULATES across rounds — never reassigned. A finding accepted in round 0 (by-design, or // below the floor) is not re-raised by the round-1 reviewer, because round 1 only sees the // fixed code and has no memory of what the human was already told would be carried. So a @@ -1230,7 +1267,10 @@ async function driveStory(story) { const acceptedKeys = new Set() const accept = (findings) => { for (const f of findings) { - const key = `${f.location ?? ''}${f.description ?? ''}` + // Keep a collision-free delimiter without embedding an invisible raw NUL in the shipped + // JavaScript source. A readable space collapses `(location, description)` pairs such as + // (`"a b"`, `"c"`) and (`"a"`, `"b c"`), silently dropping one accepted finding. + const key = `${f.location ?? ''}\u0000${f.description ?? ''}` if (acceptedKeys.has(key)) continue acceptedKeys.add(key) accepted.push(f) @@ -1249,10 +1289,14 @@ async function driveStory(story) { // in-flight log AND no first-review comment already on the PR. Either signal makes // round-0 a SILENT re-review, so a PR never accrues a second first-review. const first = round === 0 && !isContinuation && !firstReviewPosted + // An initial/resumed-without-history review establishes the whole-PR baseline. + // Once a fix is in flight, even the file inventory must start at that baseline; + // otherwise the pacing loop invites a second full audit before its delta rule. + const reviewBase = prevFindings.length ? prevReviewedHead : baseOf(story) const review = await agentRetry( - `Independently review PR #${pr.prNumber} for story ${tag}, following ${SK.review}. ${revWtClause(story)} PACING (mandatory — this is what killed the previous four attempts at this review, measured): a supervisor kills any agent that goes 180 seconds without emitting a TEXT MESSAGE. Tool calls do NOT count as progress: the last stalled reviewer was calling \`sed\`/\`cat\` every ~5 seconds and was still killed, because it had not written a sentence in 200 seconds. So: after EVERY file you inspect, write ONE SHORT LINE of prose saying what you found or that it is clean — before moving to the next file. Never read two files in a row without speaking in between, and never go into a long silent analysis pass. Start by listing the changed files (\`git diff ${baseOf(story)}...origin/${story.branch} --name-only\`), say aloud the order you will take them, then go file by file, narrating as you go. Brevity is fine — one line is enough — but silence is fatal. Review ONLY from the story's acceptance criteria, the PR diff+description, and the code. Do NOT read ${BLIND_PATHS}, nor any checkpoint, handoff or working log under them — they are the author's private context and this review is independent and blind to it. Report EVERY finding regardless of severity (including minor/nit), using the ${REVIEW_TEMPLATE_LABEL} vocabulary: each finding = \`location\` (File:Line), \`severity\` ∈ {${SEVERITIES}}, \`description\` (the CONCRETE FAILURE CASE — inputs/state -> wrong output — not a retelling of the diff), \`recommendation\` (the change, in one or two lines); verdict ∈ {${VERDICTS}}. ${TEXT_SHAPE} DO NOT FILE NEW ISSUES. This is a hard rule, and it overrides any habit of deferring work to a follow-up card: a debt you find in this diff is resolved IN PLACE, in this same PR, within this story's scope. Never invoke ${SK.writeIssue}, never write \`Deferred to #\`, and never recommend "track this separately" — a finding parked in a fresh card is a finding nobody fixes, and it converts a reviewed PR into an unreviewed backlog. Set \`nonActionable: true\` ONLY if fixing it would be genuinely WRONG — byte-consistent with a source of truth, matching an existing convention, an ALREADY-EXISTING tracked story (cite its number; do not create one), or something that can only resolve after merge. Being outside this story's originally stated scope is NOT a reason: fix it here. Whenever you set \`nonActionable: true\`, ALSO set \`disposition\` with a concrete reason replacing the bare label (\`By convention …\` / \`Historical record\` / \`Already tracked in #\` / \`Resolves after merge\`); never leave "non-actionable" as the only explanation. If a finding is SO large that fixing it here would genuinely swamp the story, say so explicitly in \`description\` and leave it ACTIONABLE — the human decides at the merge gate whether to accept the bigger PR or carve it out; that decision is not yours to pre-empt by filing a card. ${first ? `This is the FIRST review: POST your full review report as a PR comment on #${pr.prNumber} (${REVIEW_TEMPLATE_LABEL} structure), and include the marker line \`${firstReviewMarker}\` VERBATIM as the first line of the comment body — it is an HTML comment (invisible in the rendered markdown, so no visible noise) that lets a later resume detect this first review by an EXACT substring match rather than a semantic reading (finding 1). Then return findings + verdict.` : prevFindings.length - ? `This is a RE-REVIEW: do NOT post any PR comment (the orchestrator synthesizes the cycle at the end). Return findings + verdict only. Verify these prior findings were genuinely resolved: ${JSON.stringify(prevFindings)}.` - : `This is a RE-REVIEW on a resumed in-flight cycle (round-0 of this run carries no prior findings): do a FRESH, independent full review pass. do NOT post any PR comment (the orchestrator synthesizes the cycle at the end). Return findings + verdict only.`} Return findings and a verdict.`, + `Independently review PR #${pr.prNumber} for story ${tag}, following ${SK.review}. ${revWtClause(story)} PACING (mandatory — this is what killed the previous four attempts at this review, measured): a supervisor kills any agent that goes 180 seconds without emitting a TEXT MESSAGE. Tool calls do NOT count as progress: the last stalled reviewer was calling \`sed\`/\`cat\` every ~5 seconds and was still killed, because it had not written a sentence in 200 seconds. So: after EVERY file you inspect, write ONE SHORT LINE of prose saying what you found or that it is clean — before moving to the next file. Never read two files in a row without speaking in between, and never go into a long silent analysis pass. Start by listing the changed files (\`git diff ${reviewBase}...origin/${story.branch} --name-only\`), say aloud the order you will take them, then go file by file, narrating as you go. Brevity is fine — one line is enough — but silence is fatal. Review ONLY from the story's acceptance criteria, the PR diff+description, and the code. Do NOT read ${BLIND_PATHS}, nor any checkpoint, handoff or working log under them — they are the author's private context and this review is independent and blind to it. Report EVERY finding regardless of severity (including minor/nit), using the ${REVIEW_TEMPLATE_LABEL} vocabulary: each finding = \`location\` (File:Line), \`severity\` ∈ {${SEVERITIES}}, \`description\` (the CONCRETE FAILURE CASE — inputs/state -> wrong output — not a retelling of the diff), \`recommendation\` (the change, in one or two lines); verdict ∈ {${VERDICTS}}. ${TEXT_SHAPE} DO NOT FILE NEW ISSUES. This is a hard rule, and it overrides any habit of deferring work to a follow-up card: a debt you find in this diff is resolved IN PLACE, in this same PR, within this story's scope. Never invoke ${SK.writeIssue}, never write \`Deferred to #\`, and never recommend "track this separately" — a finding parked in a fresh card is a finding nobody fixes, and it converts a reviewed PR into an unreviewed backlog. Set \`nonActionable: true\` ONLY if fixing it would be genuinely WRONG — byte-consistent with a source of truth, matching an existing convention, an ALREADY-EXISTING tracked story (cite its number; do not create one), or something that can only resolve after merge. Being outside this story's originally stated scope is NOT a reason: fix it here. Whenever you set \`nonActionable: true\`, ALSO set \`disposition\` with a concrete reason replacing the bare label (\`By convention …\` / \`Historical record\` / \`Already tracked in #\` / \`Resolves after merge\`); never leave "non-actionable" as the only explanation. If a finding is SO large that fixing it here would genuinely swamp the story, say so explicitly in \`description\` and leave it ACTIONABLE — the human decides at the merge gate whether to accept the bigger PR or carve it out; that decision is not yours to pre-empt by filing a card. ${first ? `This is the FIRST review: POST your full review report as a PR comment on #${pr.prNumber} (${REVIEW_TEMPLATE_LABEL} structure), and include the marker line \`${firstReviewMarker}\` VERBATIM as the first line of the comment body — it is an HTML comment (invisible in the rendered markdown, so no visible noise) that lets a later resume detect this first review by an EXACT substring match rather than a semantic reading (finding 1). Then return findings + verdict.` : prevFindings.length + ? `This is a RE-REVIEW: do NOT post any PR comment (the orchestrator synthesizes the cycle at the end). Verify these prior findings were genuinely resolved: ${JSON.stringify(prevFindings)}. The last complete review covered immutable head ${prevReviewedHead}. First inspect ONLY the fix delta with \`git diff ${prevReviewedHead}...origin/${story.branch} --name-status\`, then its directly changed producer/consumer contract boundaries. Do NOT re-audit the unchanged PR surface. A new finding is actionable only if it is in this delta or a contract boundary changed by this delta; otherwise report it as a Question for the human, not a new fix round.` + : `This is a RE-REVIEW on a resumed in-flight cycle (round-0 of this run carries no prior findings): do a FRESH, independent full review pass. do NOT post any PR comment (the orchestrator synthesizes the cycle at the end).`} Return findings, verdict, and \`reviewedHead\`: the lower-case 40-character SHA printed by \`git rev-parse origin/${story.branch}\` after your inspection.`, // effort was 'xhigh'. The measured cause of the repeated kills was NOT effort and NOT a // stuck command: transcript timing showed the reviewer issuing a tool call every ~5s // (97 events, mean gap 4.9s, max 49s — zero gaps over 180s) yet still killed, because @@ -1263,10 +1307,9 @@ async function driveStory(story) { // narration reliable, restoring 'xhigh' is legitimate: it costs review depth, which is // the whole point of this gate. Do not read this line as "xhigh causes stalls". withModel({ agentType: 'pair-reviewer', phase: 'Review', label: `rev:${tag} r${round}`, effort: 'high', schema: REVIEW_SCHEMA }), - // A review is USABLE only if it carries a verdict — the same predicate the guard below - // converges on. Without it the retry covered the dead reviewer (`null`) and skipped the - // contentless one (`{}`), which is the shape actually measured on #432. - hasVerdict, + // A review is USABLE only with a verdict and its immutable reviewed head. Without the + // latter, the next pass cannot be an evidence-bounded re-review. + hasReviewEvidence, ) // A DEAD reviewer is not a clean review. `agent()` returns null when the subagent // dies, and `review?.findings ?? []` then yields zero findings — which the @@ -1286,15 +1329,16 @@ async function driveStory(story) { // So the test is inverted: a VERDICT must be present. Absence of findings is not evidence // that a review happened; presence of a verdict is. Every real review emits one — it is a // required field of the contract schema — so this costs a genuine clean review nothing. - // `hasVerdict` is the SAME function `agentRetry` was given above: the contentless return is - // retried once like any other dead step, and only then does it land here. - if (!hasVerdict(review)) + // `hasReviewEvidence` is the SAME function `agentRetry` was given above: a contentless or + // unanchored return is retried once like any other dead step, then lands here. + if (!hasReviewEvidence(review)) // `acceptedFindings` travels on EVERY terminal arm, this one included. A card whose // reviewer dies mid-cycle otherwise reports the by-design and below-floor findings of // every earlier round as if none had been raised — and those are precisely the findings // the fixer never receives, so they are recoverable from nowhere else. AC4 says an // accepted finding always reaches the human; a failure is not an exception to that. return { story, prNumber: pr.prNumber, status: 'failed-review', round, acceptedFindings: accepted, reviewLog: cycleHasRemediation ? reviewLog : undefined } + const reviewedHead = String(review.reviewedHead).toLowerCase() const findings = review.findings ?? [] const allActionable = findings.filter((f) => !f.nonActionable) // Below the floor: still reported, still shown to the human, just not blocking. Marked @@ -1357,11 +1401,12 @@ async function driveStory(story) { round++ prevFindings = actionable + prevReviewedHead = reviewedHead cycleHasRemediation = true // FIX — implementer resumes checkpoint (if present) + resolves actionable findings. // Logs the round to the working review log INSTEAD of posting a per-round PR comment. const fix = await agentRetry( - `Resume story ${tag}. ${wtClause(story)} Read the checkpoint if present (${SK.checkpoint} $mode=resume); otherwise work from the PR diff + code. Resolve EVERY one of these actionable review findings on PR #${pr.prNumber} — including minor/nit, do not defer any: ${JSON.stringify(prevFindings)}. Fix them IN PLACE, in this PR: do NOT file a follow-up issue for any of them, do NOT invoke ${SK.writeIssue}, and do NOT leave a "tracked separately" note in lieu of the fix. If a finding turns out to be genuinely larger than this story, still fix what belongs here and say plainly in the working log what remains — the human decides at the merge gate, not a new card. Follow ${SK.implement} for the change itself (test-first where a finding describes a defect), verify with ${SK.verifyQuality} (tier-resolved — do not improvise a gate command), and record any decision a finding forces with ${SK.recordDecision}. Commit and push. Then re-invoke **${SK.publishPr}**: it is create-or-update and idempotent, and re-running it is what keeps the PR body, the classification tags and the \`pr-state:*\` label in sync with the NEW head commit instead of describing the pre-fix state. As in the open-PR step it will emit \`Review: review-dispatch-required\` rather than nesting — expected: this orchestrator drives the re-review. ${TEXT_SHAPE} Re-running it REWRITES the PR body, and this is the only step that does so once a cycle is under way: rewrite it to describe the CURRENT head, do not append a round-by-round history — a body that grows by one section per fix round is re-read in full by every later reviewer of this same cycle. Do NOT post a remediation PR comment; INSTEAD append this round to the working log \`${reviewLog}\` (create it if absent) as a COMPACT TABLE under a \`## Round N\` heading — one row per finding, columns \`severity | location | what changed | commit\`. One row, one line: no paragraph per finding, and do not restate the finding's description (its location identifies it). Add prose ONLY where a fix diverged from the recommendation, and then only the reason. Only for a genuine design disagreement set needsHumanDecision instead of forcing a fix. Do NOT merge.`, + `Resume story ${tag}. ${wtClause(story)} Read the checkpoint if present (${SK.checkpoint} $mode=resume); otherwise work from the PR diff + code. Resolve EVERY one of these actionable review findings on PR #${pr.prNumber} — including minor/nit, do not defer any: ${JSON.stringify(prevFindings)}. Fix them IN PLACE, in this PR: do NOT file a follow-up issue for any of them, do NOT invoke ${SK.writeIssue}, and do NOT leave a "tracked separately" note in lieu of the fix. If a finding turns out to be genuinely larger than this story, still fix what belongs here and say plainly in the working log what remains — the human decides at the merge gate, not a new card. CONVERGENCE SWEEP (mandatory): the finding location is the starting point, not the contract boundary. Before changing code, make a finite map of the same observable contract: the reported case and its paired success/failure path; any state transition or resume path the contract owns; and the canonical source plus every distributed representation of that behavior (generated asset, dataset, installed copy, or documented command). Change every map cell required for that one contract, then stop — do not use the sweep for unrelated cleanup, new behavior, or speculative hardening. For a generated/distributed artifact, resolve the canonical source from the asset registry, edit only that source, then run the declared generator/installer and inspect its output; never hand-edit a derived copy. PROVISIONED ARTIFACT CONTRACT (mandatory when a change installs, builds, publishes, names, or invokes an executable/package): map \`producer -> published identity -> consumer\` — for example installer/release step -> package manifest/bin/file/export -> workflow or user command. Prove the exact path in a clean temporary environment using the real built or installed artifact. Never stub, alias, or fake the exact producer, published identity, or consumer boundary; external effects may be isolated only after that boundary is crossed. For each logic defect, write a test that executes the real function/script against a real or realistic fixture and asserts output/side effects, never a source-string regex. Re-run the finding's evidence command and the mapped boundary cases before commit. Follow ${SK.implement} for the change itself: its TDD discipline and adoption-compliance phase are mandatory. Verify with ${SK.verifyQuality} (tier-resolved — do not improvise a gate command), and record any decision a finding forces with ${SK.recordDecision}. Commit and push. Then re-invoke **${SK.publishPr}**: it is create-or-update and idempotent, and re-running it is what keeps the PR body, the classification tags and the \`pr-state:*\` label in sync with the NEW head commit instead of describing the pre-fix state. As in the open-PR step it will emit \`Review: review-dispatch-required\` rather than nesting — expected: this orchestrator drives the re-review. ${TEXT_SHAPE} Re-running it REWRITES the PR body, and this is the only step that does so once a cycle is under way: rewrite it to describe the CURRENT head, do not append a round-by-round history — a body that grows by one section per fix round is re-read in full by every later reviewer of this same cycle. Do NOT post a remediation PR comment; INSTEAD append this round to the working log \`${reviewLog}\` (create it if absent) as a COMPACT TABLE under a \`## Round N\` heading — one row per finding, columns \`severity | location | what changed | commit\`. One row, one line: no paragraph per finding, and do not restate the finding's description (its location identifies it). Add prose ONLY where a fix diverged from the recommendation, and then only the reason. Only for a genuine design disagreement set needsHumanDecision instead of forcing a fix. Do NOT merge.`, withModel({ agentType: 'pair-implementer', phase: 'Review', label: `fix:${tag} r${round}`, effort: 'high', schema: FIX_SCHEMA }), ) // failed-fix: the fixer died mid-round; a partial working log may exist. Surface diff --git a/.claude/workflows/pair-implement-batch.test.mjs b/.claude/workflows/pair-implement-batch.test.mjs index b3360499b..cbaa5dbb4 100644 --- a/.claude/workflows/pair-implement-batch.test.mjs +++ b/.claude/workflows/pair-implement-batch.test.mjs @@ -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 @@ -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' }]) @@ -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 @@ -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, diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..5fd41b201 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ce65fcc4..94fdd7fb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.pair/adoption/decision-log/2026-08-31-review-baseline-and-provisioned-artifact-contract.md b/.pair/adoption/decision-log/2026-08-31-review-baseline-and-provisioned-artifact-contract.md new file mode 100644 index 000000000..79afd4533 --- /dev/null +++ b/.pair/adoption/decision-log/2026-08-31-review-baseline-and-provisioned-artifact-contract.md @@ -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 ...origin/` 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. diff --git a/.pair/adoption/decision-log/2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md b/.pair/adoption/decision-log/2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md new file mode 100644 index 000000000..aeef01ce7 --- /dev/null +++ b/.pair/adoption/decision-log/2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md @@ -0,0 +1,201 @@ +# Decision: a generated artifact that is tracked and byte-compared is byte-reproducible across environments — fixed entry order, pinned line endings + +## Date + +2026-09-01 + +## Status + +Active + +## Category + +Convention Adoption + +## Context + +`.pair/llms.txt` is generated by `generateLlmsTxt`, committed, and — since #416 — +compared byte for byte against a fresh generator run by `pnpm llms-index:check`. Its +entries were ordered with `a.path.localeCompare(b.path)`, which passes **no locale** +and therefore resolves against the runtime's ICU default. + +That makes the file's bytes a property of the machine, not of the content. Measured on +the committed index: **458 of 560 entries occupy a different position under ICU +collation than under codepoint order** — ICU sorts `.pair/adoption/product/context-map.md` +before `PRD.md` (case-insensitive-ish), codepoint order the reverse (`P` = 0x50, +`c` = 0x63). + +Concrete failure with the gate in place: a contributor on a Node built without full +ICU (`--with-intl=small-icu`, several distro and container builds) runs the gate on an +**untouched** tree, gets red, is told "regenerate with `pnpm llms-index:regen` and commit", does +so, and commits 458 reordered lines that flip back on the next machine. The gate's own +message drives the churn. + +The story's edge-case list names this failure verbatim ("Generator not deterministic +across environments … locale-dependent sort"), and the determinism test that was meant +to cover it ran the generator twice **in one process**, where the ICU default is a +constant — so it could not fail on the thing it named. + +**The same failure has a second axis: the line terminator.** The generator emits `\n`; +the bytes on disk are whatever git checked out. With `core.autocrlf=true` (git's default +on Windows) every one of the index's 570 content lines gains a `\r`, byte equality fails +on the whole file, and the gate reports 570 `missing` + 570 `extra` — a ~1140-line dump +that hides any real drift — closing with "regenerate and commit". Obeying it writes LF, +the next checkout restores the CRs, and the message cannot get the contributor out of the +loop. The repo had no `.gitattributes` at all, so nothing pinned the normal form. +(Figures measured on the committed index at this commit: 583 physical lines, 570 of them +non-blank — the deltas are computed over the non-blank lines. 562 is the ENTRY count, and +an earlier draft of this paragraph used it for both.) + +**And a third axis: the path separator.** `scanSection` built each emitted entry path +with `path.join`, which is bound to the platform — `.pair/knowledge/…` on POSIX, +`.pair\knowledge\…` on Windows. Measured with Node's real `path.win32` bound to the +generator against the real committed index: **all 562 entries change**, the gate reports +562 `missing` + 562 `extra` in a 1140-line report with no caution naming the cause, and +closes with the bare regenerate imperative. Obeying it commits an index in which every +markdown link is broken for every other platform and every agent. The separator is also +a SORT key — `\` is U+005C and `/` is U+002F — so `a/b.md` precedes `a5.md` on POSIX and +follows it on Windows: the determinism rule 1 buys on the collation axis is lost on this +one. It is an adopter-facing defect too, not only a contributor one: `generateLlmsTxt` is +what `pair-cli install`/`update` writes, so a Windows adopter's `.pair/llms.txt` ships +with broken links. + +## Decision + +**Any generated artifact that is both tracked and byte-compared must be byte-reproducible +on any machine that checks it out.** Three rules follow, one per axis of variation found: + +1. **Entry order is a function of the content alone** — not `localeCompare`, not + filesystem-walk order. +2. **The line terminator is pinned in `.gitattributes`** (`* text=auto eol=lf`, plus an + explicit `.pair/llms.txt text eol=lf` naming the gate that depends on it), so the + checkout cannot rewrite the bytes the gate compares. **And the gate reports a + CR-carrying tracked file as what it is**: it splits on the whole terminator set + (`\r\n`, bare `\r`, `\n`) before computing the missing/extra deltas (so a real drift + stays visible under any of them), states the terminator mismatch, and puts its call to + action behind the precondition "once the checkout is normalized to LF" — never the + bare regenerate imperative, which here is the one fix that provably cannot work. The trigger is + **any** `\r`, not `\r\n` alone: a bare-CR file (classic-Mac form — git never writes it, + a hand-rolled `s/\n/\r/` does) has the same cause, the same diagnosis and the same exit, + and under an `\n`-only split it collapses to a SINGLE segment — the whole index as one + unreadable `extra` line, no terminator caution, and the closing advice back to the bare + regenerate imperative this rule exists to prevent. The recipe it prints is the one that was + **run** against a `core.autocrlf=true` clone — `rm` + `git checkout --`, the one file + and nothing else: the idiomatic `git add --renormalize` is inert when the index side is + already LF (it stages nothing, all 583 CR-carrying lines stay on disk and the gate stays + red), and a `git config core.autocrlf false` step in front is unnecessary (the `eol=lf` + attribute overrides `autocrlf` by itself — with the config left at `true`, the two-step + recipe alone gave `w/lf`, 0 CRs, gate green) and would rewrite repo-local git config for + every file to fix one. +3. **A path that is EMITTED into the artifact is built with `posix.join`; `join` stays + for file-system access only.** The two are different jobs and sharing one call + conflated them. Windows' file APIs accept either separator, so reading the tree keeps + the platform's own `join` and nothing about the walk changes; the entry path is + content, and content does not vary by host. The invariant is the same one rule 2 + buys, which is why this is a rule of this ADL and not a new record: `/` is the + separator the artifact's consumers (markdown renderers, every agent resolving the + index, the drift gate's byte comparison) already assume. + +Platform scope, since the question is what makes rule 2 necessary: **LF is the repo's +normal form on every platform**, in the index and in the working tree. Windows is not +excluded as a development platform — it is supported by normalizing on checkout rather +than by teaching each gate to accept both terminators. + +**The invariant being bought is DETERMINISM across environments, not a particular +collation.** Any total order fixed by the string's own units qualifies; the property that +matters is that two machines running the same generator over the same tree emit the same +bytes. + +Realized here as JavaScript's default string relational comparison — +`a < b ? -1 : a > b ? 1 : 0` — applied to `scanSection` in +`apps/pair-cli/src/registry/llms-generation.ts`. That operator compares **UTF-16 code +units**, which is the same order as codepoint order for every character in the BMP, hence +for every path this or any adopter's KB has carried (all ASCII). The two orders diverge +only above U+FFFF: a non-BMP character is a surrogate pair (U+D800–U+DFFF) and therefore +sorts BELOW U+E000–U+FFFF, where codepoint order would put it above. Either order +satisfies the invariant — both are environment-independent — so the divergence is a +naming precision, not a defect, and no code change follows from it. A future artifact +citing this ADL should cite the invariant (determinism), and read "code unit" as the +implementation, not as a requirement of its own. + +`.pair/llms.txt` is regenerated in the same commit, since the order changes. + +The rule follows from what the artifact IS: a machine-read index whose consumer is an +agent and whose gate is byte equality. Human-friendly collation buys nothing a reader +of a 560-line link list will notice, and costs cross-environment reproducibility. + +An artifact rendered for humans and not byte-compared (a docs page, a report) is free +to collate — the rule is scoped to the byte-compared, tracked case. + +(The file's slug says `sorts-by-codepoint`, the shorthand this was first written under. +It is kept as the stable citation key — the normative statement is this section.) + +## Alternatives Considered + +- **Pin the locale: `localeCompare(b, 'en')`.** Still ICU-dependent — a small-icu Node + falls back to the root locale regardless of the argument, so the failure survives on + exactly the builds that produce it. Rejected. +- **Leave the comparator and assert the expected order in the fixture test.** The + reviewer's minimum. It converts a silent environment dependence into a red test on + the affected machine, which is better than nothing, but the tracked file's bytes + remain machine-dependent and the contributor still cannot run the gate there. + Rejected as the weaker half of a fix available in full. +- **Normalize in the gate instead** (sort both sides before comparing): the gate would + stop reporting a real ordering drift, and the committed file would still differ by + machine. Rejected — it hides the symptom in the one place built to reveal it. +- **Declare Windows out of scope and change nothing** (no `.gitattributes`, no CRLF + branch). Defensible on today's evidence — CI is `ubuntu-latest`, no doc claims Windows + — but the cost of being wrong is a contributor stuck in an unbreakable regenerate loop + on their first gate run, and the cost of being right is two lines of config. Rejected. +- **Normalize the separator in the GATE** (compare `\`-joined and `/`-joined paths as + equal): same defect as normalizing the order — the committed file's bytes would still + differ by machine, and the broken links would ship to adopters through + `pair-cli install`/`update`, which the gate never sees. Rejected. +- **Accept CRLF in the verdict** (compare normalized text, call it in-sync): drops AC1's + byte equality, and the tracked file's bytes would then legitimately differ by machine — + the very property this ADL exists to protect. Rejected. + +## Consequences + +- `.pair/llms.txt` changes order once, in this commit (70 lines move). Uppercase-first + entries (`PRD.md`, `README.md`, `ADR-*`) now sort before their lowercase siblings + inside each section. +- The index is now reproducible on any Node build, ICU or not — the property the + drift gate needs to be trustworthy. +- Every adopter's `.pair/llms.txt` is regenerated in the same new order by their next + `pair install` / `pair update`. It is a generated file, so this is a one-time diff, + not a migration. +- The fixture test asserts the order on the `PRD.md` / `context-map.md` pair — chosen + because ICU and codepoint disagree on it, so the assertion fails if the comparator + is ever reverted. +- The repo gains its first `.gitattributes`. It is inert on the current tree (no tracked + text file carries a CR) and on macOS/Linux clones; it changes what a + `core.autocrlf=true` clone puts in the working tree. +- The emitted entry path is separator-independent. Covered by + `apps/pair-cli/src/registry/llms-generation.win32.test.ts`, which binds `path` to + Node's own `path.win32` (what `require('path')` returns on Windows — `lib/path.js` + ends `module.exports = isWindows ? win32 : posix`) and asserts the generator emits the + same bytes a POSIX machine emits, including sibling order. On POSIX `join` and + `posix.join` are the same function, so this is the only shape of test that can + distinguish the fixed code from the broken code on a Linux CI. +- The drift report has one more branch: `trackedCarriesCr`. It suppresses the + order/whitespace sentence (one cause, one diagnosis) and adds "the checkout is + normalized to LF" to the call to action's preconditions, which now compose with the + sparse-tree one. The terminator domain is closed: LF, CRLF, mixed, doubled `\r\r\n`, + bare CR, CR mixed with LF, and a stray CR at EOF — one test row each. + +## Adoption Impact + +- `adoption/tech/way-of-working.md` — the Quality Gates entry for `llms-index:check` + states the index's order rule and now also the LF pin, both citing this ADL. +- **`@pair/pair-cli` is published (`0.4.3`, not private) and this changes its output**: + `generateLlmsTxt` is what `pair install` / `pair update` writes into every adopter's + `.pair/llms.txt`, so their next run reorders it. Per ADL + [2026-08-20-a-user-facing-cli-fix-carries-its-changeset.md](2026-08-20-a-user-facing-cli-fix-carries-its-changeset.md) + that is a user-facing behaviour change to a published package and it carries a **patch + changeset in this PR** (`.changeset/`), which is where the adopter's CHANGELOG entry + comes from. Release timing stays a human decision; authoring the changeset does not. +- No `tech-stack.md` change: no dependency enters or leaves. `.gitattributes` is repo + configuration, not a tool choice. +- No ADR: this is the byte-level contract of one generated artifact, not a boundary or a + pattern. diff --git a/.pair/adoption/decision-log/2026-09-01-a-gate-imports-its-generator-by-source-and-gets-a-read-only-slice.md b/.pair/adoption/decision-log/2026-09-01-a-gate-imports-its-generator-by-source-and-gets-a-read-only-slice.md new file mode 100644 index 000000000..0fc4182e8 --- /dev/null +++ b/.pair/adoption/decision-log/2026-09-01-a-gate-imports-its-generator-by-source-and-gets-a-read-only-slice.md @@ -0,0 +1,178 @@ +# Decision: a quality gate imports the generator it checks by SOURCE path, and the generator hands it a read-only file-system slice + +## Date + +2026-09-01 + +## Status + +Active + +## Category + +Convention Adoption + +## Context + +Story #416 adds the gate that keeps `.pair/llms.txt` equal to what `generateLlmsTxt` +emits. Refinement had already settled *where* the check lives — beside the sibling +gates in `packages/dev-tools/src/quality-gates/`, so `quality-gate` composition stays +uniform — and *what* it compares — the transform's output, per +[`2026-08-11-a-mirror-guard-compares-the-transform.md`](./2026-08-11-a-mirror-guard-compares-the-transform.md). + +What it did not settle is the mechanism, and the mechanism is where the two real +risks live: + +1. **How does `@pair/dev-tools` reach a generator that lives in `apps/pair-cli`?** + `@pair/pair-cli` publishes `dist/index.js` and exports only `version` from it; the + registry is reachable inside the app through its `#registry` subpath imports, which + are private to that package. There was no exported route to `generateLlmsTxt`. +2. **What stops the check from becoming a fixer?** The check-only rule + ([`2026-07-31-pre-push-gate-is-check-only.md`](./2026-07-31-pre-push-gate-is-check-only.md)) + is stated in prose, and the module that computes the expected index sits one import + away from `writeProjectLlmsTxt` — the function that would silently "fix" the drift + the gate exists to reveal. A guard whose only protection against writing is the + author's restraint is one autocomplete away from failing open. + +The DoD constraint framing both: **one** definition of the index format. Any answer +that copies the generator, or re-derives the format from the tracked file, is out. + +## Decision + +**Two rules, adopted together.** + +**1. A gate imports the artifact-producing code by SOURCE path, not through a package +entry point.** `llms-txt-drift-check.ts` does +`import { generateLlmsTxt } from '../../../../apps/pair-cli/src/registry/llms-generation'`. +The gate reads the same TypeScript the CLI compiles, so the generator has exactly one +definition and a change to it surfaces in the gate immediately — no rebuild, no +publish, no re-export whose only consumer is a dev script. + +Three mechanical consequences, all handled here rather than left to be rediscovered: + +- **`@pair/dev-tools` sets `"composite": false`.** This is what the source import + actually costs, and it was NOT free: with `composite` inherited from + `@pair/ts-config/base.json`, `tsc` rejected the import outright — `TS6059` (the + imported file "is not under `rootDir`") and `TS6307` ("not listed within the file + list of project"). Both are **emit-time invariants**, and `@pair/dev-tools` emits + nothing: it has no `build` script, its `ts:check` is `tsc --noEmit`, and no tsconfig + in the repo references it. `composite` there was inert cargo from the shared base, + enforcing `rootDir` on a project with no output. Turning it off for this one package + states the truth (a scripts package, not a build target) instead of bending `rootDir` + to admit the reach-in. The rationale is repeated at the edit site, in + `packages/dev-tools/tsconfig.json`, because that is where the next person meets it. +- `@pair/content-ops` is now a **devDependency of `@pair/dev-tools`**, though no + dev-tools source imports it. The source import pulls `llms-generation.ts` into + dev-tools' `tsc` program, and its sibling `writeProjectLlmsTxt` types against + `FileSystemService`. Without the declared dependency, turbo has no `^build` edge from + `@pair/dev-tools#ts:check` to `@pair/content-ops#build` and the two RACE — observed: + `dev-tools:ts:check` starting before `content-ops:build` finished, green only by + luck. The dependency is what makes the ordering a graph edge instead of a coin flip. +- `ts-node`/vitest compile the imported source directly because it is reached by a + relative path, not through `node_modules` (which `ts-node` ignores by default). +- **The gate's own script runs `ts-node -T` (transpile-only).** Type-checking the + imported source at GATE-RUN time made the gate's verdict depend on whether someone + had built a sibling package: `llms-generation.ts`'s first line imports + `@pair/content-ops` TYPES, which exist only in `dist/`. Reproduced on this branch by + moving `packages/content-ops/dist` aside — `pnpm llms-index:check` produced none of + its three outcomes and died with + `../../apps/pair-cli/src/registry/llms-generation.ts(1,40): error TS2307: Cannot find module '@pair/content-ops'` + plus a ts-node stack, exit 1. The same command with `-T` printed + `✓ llms-index: .pair/llms.txt matches the generator` on that same unbuilt tree. + Nothing is lost: type-checking that source belongs to `ts:check` in BOTH packages, + and `turbo ts:check` is the task that carries the `^build` edge (which is also why + the `@pair/content-ops` devDependency below stays — it orders `ts:check`, and only + `ts:check`). The rejected alternative was to make `llms-index:check` a turbo task + with `"dependsOn": ["^build"]`: it restores ordering but puts a repo-wide guard + behind turbo's cache, whose key is package-scoped — the stale-PASS trap this + repo's `turbo.json` already documents twice — so it would need `cache: false` or a + `$TURBO_ROOT$/.pair/**` inputs list, i.e. more machinery for a property `-T` + gives for free. + +**2. Code invoked BY a gate takes the narrowest file-system capability it needs.** +`generateLlmsTxt`'s parameter changed from `FileSystemService` (30 members, including +`writeFile`, `rm`, `chmod`) to `LlmsSourceFs` — a 3-method read-only interface +(`exists`, `readdir`, `readFile`) declared next to it. The gate passes its own +`readOnlyFileSystem` adapter, which has no write primitive to call. "This gate cannot +write the file it judges" becomes a type fact instead of a review promise. Existing +callers are untouched: `fileSystemService` satisfies the narrower interface +structurally. + +## Alternatives Considered + +- **Add an `exports` map to `@pair/pair-cli` and import `@pair/pair-cli/registry`**: + turns a dev-tooling need into a **public API change on a published package** (`"private": false`), + and binds the gate to `dist/` — so the check reports drift against the LAST BUILD, not + against the working tree. A generator edit without a rebuild would pass green. Rejected. +- **Leave the byte-equality guard where #216 put it** (`apps/pair-cli/src/registry/llms-index-conformance.test.ts`, + a vitest case over the real repo tree): it satisfies AC1 and nothing else. Its failure + is a raw string diff over a 400-line file — the experience AC2 rejects by name — it + names no regeneration command, and being a cacheable `turbo test` input that lives + entirely inside `apps/pair-cli`, a KB-only change replays a cached PASS. Replaced, not + duplicated (the file keeps its non-drift assertions about what the generator must index). +- **Move `generateLlmsTxt` into `@pair/content-ops`** (a workspace library both packages + already depend on) and import it as `@pair/content-ops/kb-index`: no `composite` change, + a clean package boundary, and genuinely the tidier long-term home for a + filesystem-scan-to-markdown transform. Rejected on the property that matters most to a + DRIFT gate: `@pair/content-ops` is consumed through `dist/`, so the gate would compare + `.pair/llms.txt` against the LAST BUILD of its generator. Inside `pnpm quality-gate` that + is safe (`turbo ts:check` pulls `^build` first), but the root step is also runnable on its + own — `pnpm llms-index:check` — and there it would silently judge the working tree with a + stale generator. A gate whose verdict depends on whether someone rebuilt is the failure + class this story exists to close. Worth revisiting only if the generator acquires a second + non-gate consumer. +- **Keep `composite` and widen `rootDir`** on `@pair/dev-tools` (e.g. `"rootDir": "../.."`): + makes the error go away while asserting something false — that this package emits, rooted + at the repo. It also leaves `TS6307` to be silenced separately. Rejected: the honest fix is + to stop claiming a non-emitting package is a build target. +- **Re-implement the index format in the gate**: two definitions of `llms.txt` that drift + apart — the exact failure the story exists to prevent. Rejected by DoD. +- **Keep `FileSystemService` and rely on the prose rule not to write**: cheaper by one + interface, and it leaves the strongest guarantee in the story (the gate never writes) + resting on nothing enforceable. +- **Move `writeProjectLlmsTxt` to its own module** so the generator carries no + `@pair/content-ops` type at all, dropping the devDependency: a genuinely cleaner split, + but it edits the install/update handlers and an existing test on a story whose whole + point is a gate. Deferred — the devDependency costs one line and no behaviour. + +## Consequences + +- The gate compiles the CLI's source; a compile error in `llms-generation.ts` now also + reddens `@pair/dev-tools#ts:check`. That is the intended coupling — one definition — + and it is confined to that one module. +- `@pair/dev-tools` gains a devDependency it does not import. **It is load-bearing for + build ORDER, not for resolution** — this ADL is where that non-obvious fact is + recorded, and the module header points here. +- **`@pair/dev-tools` no longer participates in project references.** Nothing referenced + it, so nothing breaks today; the cost lands the day this package acquires a `build` + script — whoever adds one must restore `composite` and, with it, deal with the source + import (most likely by taking the `@pair/content-ops` route rejected above, which by + then would have a second consumer to justify it). The tsconfig comment says so at the + edit site. +- Dropping `composite` removes the compiler's objection to ANY cross-package source + import from this package, not just this one. The boundary is now a convention rather + than a type error: `@pair/dev-tools` is where the repo's gates live, and a gate reading + the source it judges is its job — but a second reach-in should be argued, not assumed + legal because the first one was. +- The narrowing is the reusable half: the next gate that runs production code over a + tree it must not modify asks for a read-only slice rather than the whole service. +- The REMEDY the gate prints (`pnpm llms-index:regen`, ADL + [2026-09-03-a-gate-names-a-remedy-it-can-run.md](./2026-09-03-a-gate-names-a-remedy-it-can-run.md)) + reuses both halves of this decision — same source import, same `-T` — and declares its + extra power as a separate two-method `LlmsIndexSink` rather than widening + `LlmsSourceFs`. The type distinction between the checker and the writer is the point: + the check still has no write primitive to call, and the writer is the only module in + the folder that does. +- The relative import hardcodes a `../../../../apps/pair-cli/...` hop. Moving either + package breaks it loudly at compile time (not silently at runtime), which is the + acceptable failure mode; `repo-root.ts` already carries the folder's other hop count. + +## Adoption Impact + +- `adoption/tech/way-of-working.md` — the Quality Gates section gains the `llms-index` + gate (what it checks, the command, check-only). +- No `tech-stack.md` change: no new dependency enters the project. `@pair/content-ops` is + an existing workspace package, newly declared by one more workspace member. +- No ADR: this changes no service boundary or architectural pattern — it is the + convention for how a gate reaches the code it checks, and what capability that code is + handed. diff --git a/.pair/adoption/decision-log/2026-09-01-external-boundary-proof-prevents-false-equivalence.md b/.pair/adoption/decision-log/2026-09-01-external-boundary-proof-prevents-false-equivalence.md new file mode 100644 index 000000000..1fa09a35a --- /dev/null +++ b/.pair/adoption/decision-log/2026-09-01-external-boundary-proof-prevents-false-equivalence.md @@ -0,0 +1,54 @@ +# Decision: External boundary proof prevents false equivalence + +## Date + +2026-09-01 + +## Status + +Active + +## Category + +Process Decision + +## Context + +The finite-state inventory rule exposed #416's CRLF problem, but the fixer extended the repair to +bare CR by treating all carriage-return forms as equivalent. Its unit tests proved only the drift +checker’s classification. A minimal real Git probe showed the promised remedy was false: Git +normalizes CRLF to LF, but preserves a lone CR blob through checkout. The next review therefore +found a major regression: its advice forbade the only repair that actually worked. + +## Decision + +Whenever a decision-table row, equivalence, normalization or user-facing repair depends on an +external command, service, file format or runtime, identify its authoritative producer/consumer +and prove the claim with a minimal isolated end-to-end probe. Keep variants distinct until that +boundary demonstrates equivalence. Apply any proposed repair in the probe and verify its stated +postcondition. Unit tests of the changed function remain required but cannot substitute for this +boundary evidence. + +## Alternatives Considered + +- **Trust the implementation unit tests**: rejected — they cannot establish Git’s checkout + semantics or prove user guidance outside the function’s process. +- **Treat all syntactically similar inputs as one state**: rejected — the external producer can + distinguish them, as CRLF and lone CR demonstrate. +- **Defer external proof to re-review**: rejected — that makes the reviewer discover a repair + regression after the fixer has already committed it. + +## Consequences + +- A finite-state table may contain an external-boundary probe per row or equivalence class. +- Fix reports include command, observed result and postcondition for external repair claims. +- Re-review may still stop on increased findings; it remains the signal that the required proof + was absent or incorrect. + +## Adoption Impact + +- `.pair/adoption/tech/way-of-working.md`: extends Review Convergence with the boundary-proof + requirement. +- `packages/knowledge-hub/dataset/.workflows/pair-implement-batch.js` and installed mirror: + require reviewers and fixers to prove externally-defined semantics and repair advice. +- Both workflow test copies: pin the prompt requirement so it cannot silently disappear. diff --git a/.pair/adoption/decision-log/2026-09-01-review-contract-inventory-prevents-serial-findings.md b/.pair/adoption/decision-log/2026-09-01-review-contract-inventory-prevents-serial-findings.md new file mode 100644 index 000000000..ee73fa744 --- /dev/null +++ b/.pair/adoption/decision-log/2026-09-01-review-contract-inventory-prevents-serial-findings.md @@ -0,0 +1,58 @@ +# Decision: Review contract inventory prevents serial findings + +## Date + +2026-09-01 + +## Status + +Active + +## Category + +Process Decision + +## Context + +The bounded delta re-review correctly exposed defects introduced by fixes, but it found them one +ordinary state at a time. On #419, fixes moved from an already-dirty path, to an unsupported +`git status --porcelain` shape, to a silent skip of another normal shape. The reviewer had one +concrete failure case per round and the fixer repaired that example, not the finite input domain. +Stopping on a count increase is therefore useful evidence for human investigation, not a reason +to hide the increase. + +## Decision + +Before reporting or fixing a changed observable contract, the reviewer/fixer maps its authoritative +producer, inputs, consumers and distributed representations. For a finite protocol, parser, +configuration, state transition or command-output domain, it builds a decision table containing +every supported state and invalid/boundary pair, and probes/tests each row against real behavior. +The first review inventories the full PR surface; a re-review inventories only its fix delta and +directly changed boundary. A fixer may not implement one newly found normal row and wait for a +later re-review to reveal the next one. + +## Alternatives Considered + +- **Keep only the generic convergence sweep**: rejected — it names paired paths but did not make + a finite protocol domain explicit, so a scalar repro still drove a scalar fix. +- **Rescan the full PR on every re-review**: rejected — it reopens unchanged scope; the immutable + baseline/delta rule remains in force. +- **Ignore a rise in findings**: rejected — the monitor's stop is the deliberate signal that a + fix may have introduced a defect and requires investigation. + +## Consequences + +- The initial review may front-load more findings, but ordinary variants are no longer deferred to + later rounds. +- Fixes touching finite state domains carry a complete test matrix before code changes. +- A count increase remains a valid human stop condition; it now points to a specific missing + inventory rather than being treated as the convergence mechanism itself. + +## Adoption Impact + +- `.pair/adoption/tech/way-of-working.md`: records the inventory requirement under Review + Convergence. +- `packages/knowledge-hub/dataset/.workflows/pair-implement-batch.js` and its installed mirror: + require the inventory in reviewer/fixer prompts. +- Both dry-run copies of `pair-implement-batch.test.mjs`: pin the prompt path and finite-state + rule. diff --git a/.pair/adoption/decision-log/2026-09-03-a-gate-names-a-remedy-it-can-run.md b/.pair/adoption/decision-log/2026-09-03-a-gate-names-a-remedy-it-can-run.md new file mode 100644 index 000000000..b519b3212 --- /dev/null +++ b/.pair/adoption/decision-log/2026-09-03-a-gate-names-a-remedy-it-can-run.md @@ -0,0 +1,162 @@ +# Decision: a gate's remedy is a command its audience can run, and the remedy refuses the states its own caution warns about + +## Date + +2026-09-03 + +## Status + +Active + +## Category + +Tooling Preference + +## Context + +`pnpm llms-index:check` (#416) closed every drift report with **"Regenerate with +`pair update` and commit the result."** No `pair` executable exists for that message's +audience: + +- `which pair` → not found. +- No `pair` entry in any workspace `node_modules/.bin`; `apps/pair-cli/package.json` + declares `bin: { "pair-cli": "dist/cli.js" }`, and `README.md` invokes the CLI as + `npx @foomakers/pair-cli install`. +- No root `package.json` script named `pair`. + +The audience is exactly this repository's contributors — `@pair/dev-tools` is +`private: true` and the gate ships in no dataset — so there is no second reading under +which the string resolves. + +**Concrete failure.** A contributor adds `.pair/knowledge/guidelines/x.md`, +`pnpm quality-gate` goes red printing that sentence, they type it, and the shell answers +`command not found`. AC-5 of the story ("the failure message alone is enough to fix the +problem") is unmet at the last paragraph, which is the one a contributor scanning for +the fix acts on. + +**Resolving it to the nearest real binary is worse than the error.** `pair-cli update` +with no `--source` parses as `resolution: 'default', offline: false` +(`apps/pair-cli/src/commands/update/parser.ts`) and installs the **published** knowledge +base over `.pair/knowledge/**` — reverting the very guideline whose addition reddened +the gate. The PR's own probe table never ran the printed string; it ran +`pair-cli update --offline --source `, a different command. + +The same mistake is already on record: ADL +[2026-07-31-pre-push-gate-is-check-only.md](./2026-07-31-pre-push-gate-is-check-only.md), +"Resolved Decision (2026-08-05)" — *"The remedy was naming the wrong command … +`pair update` … resolves and installs the published KB; what a mirror divergence needs is +regeneration from the local dataset"*. Its part 2 is "the gate names that command in its +remedy, in all three places, replacing `pair update`". Printing `pair update` in a fourth +place, plus in `DEVELOPMENT.md` and its docs-site twin, re-opened a decision that was +already taken. + +**Why not simply reuse #419's command.** Story #419 owns that dedicated command +(`pnpm mirrors:regenerate`, `scripts/regenerate-mirrors.sh`), and it is an **unmerged +in-flight branch** — a gate cannot print a command that is not in the tree. It is also +oversized for this failure: by #419's own description it wraps +`pair-cli update --source --offline`, which rewrites `.pair/knowledge/**`, +`.claude/**`, `AGENTS.md`/`CLAUDE.md` and `.github/**`, **deletes** target-only files +under a `mirror`-behaviour registry (an untracked `.pair/knowledge/wip-draft.md` does not +survive the run), and requires a built CLI. Handing that blast radius to someone whose +only problem is a stale 562-line index is not a remedy, it is a trap. + +## Decision + +**A gate's printed remedy names a command that exists in this repository, and that +command refuses every state the gate's own cautions say not to regenerate on.** + +Realized as `pnpm llms-index:regen` → `pnpm --filter @pair/dev-tools llms-index:regen` → +`ts-node -T packages/dev-tools/src/quality-gates/llms-txt-regenerate.ts`. + +1. **It is the check's exact inverse, and nothing more.** The same `generateLlmsTxt` over + the same tree, written to the same `TRACKED_INDEX_PATH` constant the check reads. It + writes **one file**, `.pair/llms.txt`. Transpile-only for the same reason the check is + (`-T`): it compiles a source file from `apps/pair-cli`, and a remedy that dies with + `TS2307` on a fresh `pnpm install` is no remedy. +2. **It refuses rather than regenerates when the check's message carries a + precondition.** The report's call to action reads "Once the checkout is normalized to + LF …" / "Once the tree is complete …" precisely because on those states regenerating + either cannot work (git rewrites the terminators straight back) or destroys what the + caution protects (a sparse checkout's absent section, committed as a deletion). So the + command runs the CHECK first and writes only on the verdict "stale index, complete, + readable tree"; every other outcome is reported with the gate's **own message** — + never a second wording — and exits 1. The refusal set is closed over `DriftReport`: + `broken-setup`, `unreadable-index`, an unreadable tree, `trackedCarriesCr`, and + `emptiedSections`. `trackedCarriesBom` is deliberately NOT in it: regeneration is the + fix for a BOM. +3. **It is on the write-mode offender list.** `llms-index:regen` and + `llms-txt-regenerate` are entries in `pre-push-gate-composition`'s + `WRITE_MODE_FORMATTERS`, per the standing rule that adding a write script to this repo + means adding it to that list. The coupling is sharper here than for the other writers: + this script is the remedy the gate prints, and a gate that ran its own remedy would + silently fix the drift it exists to reveal. +4. **The assertion on the remedy is on the string's CONTENT, not on the constant.** The + suite's AC-5 test asserted `result.message).toContain(REGENERATION_COMMAND)` — the + constant compared against itself, green for any value it could hold, which is how + `pair update` passed a 110-case suite and five manual probe tables. It now asserts the + literal a contributor types, and a second test resolves that literal's script name + against the **real root `package.json`** (the same technique `pre-push-gate-composition` + uses for `pnpm format`), so a renamed or deleted script reddens here instead of in + someone's shell. + +**This does not pre-empt #419.** The two commands are complementary and differently +scoped: `mirrors:regenerate` realigns the dataset mirrors and is what the mirror-equality +guards name; `llms-index:regen` regenerates the one byte-compared index this gate +compares. When #419 merges, `mirrors:regenerate` will also rewrite `.pair/llms.txt` as a +side effect of running the real update transform — that is a superset, not a conflict, +and whether this gate should then point at it is a decision for that merge, not for this +one. + +## Alternatives Considered + +- **Point the remedy at `pair-cli update --offline --source packages/knowledge-hub/dataset` + (the reviewer's literal recommendation, wrapped in a root script).** Rejected on blast + radius, not on shape: by #419's own measurement that command deletes untracked + target-only files under the `mirror` registries and rewrites four trees. Telling a + contributor to run it because an index line is stale makes the advice unsafe to obey + blindly, which is the property the whole finding is about. It also needs a built CLI + (`turbo run build --filter=@pair/pair-cli...`), reintroducing the build dependency `-T` + was chosen to remove. +- **Wait for #419 and print `pnpm mirrors:regenerate`.** Rejected: it is unmerged, so the + gate would ship naming a command not in the tree — the same defect in a new spelling. +- **Keep a CLI-shaped string but spell it `pair-cli update`.** Rejected: `pair-cli` is not + on `PATH` either (it is a published `bin`, not a workspace one), and without `--source` + the command installs the published KB over the local one. +- **Drop the command from the message and describe the fix in prose.** Rejected: the + reason the command is named at all is that a report without one teaches contributors to + hand-edit the artifact, which is how it went stale twice. +- **Let the remedy regenerate unconditionally and rely on the printed caution.** Rejected: + the caution is a paragraph above the imperative, and the failure mode this whole gate + was shaped around is a contributor acting on the LAST paragraph. A command that + cheerfully commits the deletion of a section on a sparse checkout is the caution's own + damage, delivered by the fix. + +## Consequences + +- Two new root/package scripts (`llms-index:regen`), one new module + (`packages/dev-tools/src/quality-gates/llms-txt-regenerate.ts`) and its suite + (11 rows: the write path, idempotence, BOM strip, and one row per refusal state). +- `REGENERATION_COMMAND` is `pnpm llms-index:regen`. Every place that quoted the old + string moved with it: the gate's cautions and call to action, `DEVELOPMENT.md`, its + docs-site twin `development-setup.mdx`, the `way-of-working.md` gate entry, and the + suite's comments. +- `WRITE_MODE_FORMATTERS` grows by two entries, with a test asserting the check beside it + (`llms-index:check`) is NOT matched — the `:regen` suffix is the whole discriminator. +- The gate and its remedy can no longer disagree about a state: both read the same + `DriftReport`, and the refusal branches print the check's message verbatim. +- Verified end to end in the repo itself: with `.pair/llms.txt` deliberately drifted, + `pnpm llms-index:check` printed the message, the printed command was typed verbatim, + and the re-run gate went green with `git status` clean. + +## Adoption Impact + +- [way-of-working.md](../tech/way-of-working.md) — the "KB index drift" Quality Gates + entry names `pnpm llms-index:regen`, states that the remedy is on the write-mode + offender list, and records why it is neither `pair update` nor a mirror realignment. +- [DEVELOPMENT.md](../../../DEVELOPMENT.md) and its published twin + `apps/website/content/docs/contributing/development-setup.mdx` — the quality-gate + paragraph names the new command in both copies (the two are kept byte-identical for + that paragraph, per the pre-push ADL's consequence). +- `packages/dev-tools/README.md` — the tools table gains the `llms-index:regen` row. +- No `tech-stack.md` change: no dependency enters or leaves. +- No ADR: this is a remedy-naming and tooling convention, not a boundary or a pattern. diff --git a/.pair/adoption/tech/way-of-working.md b/.pair/adoption/tech/way-of-working.md index 3bbc3b8ed..3e76c3658 100644 --- a/.pair/adoption/tech/way-of-working.md +++ b/.pair/adoption/tech/way-of-working.md @@ -54,17 +54,38 @@ Resolution order, the split-tool routing and why the fallback is never the authe - When a bug fix or feature changes behavior covered by an existing CP, the corresponding test case MUST be updated. - **CP5's docs page list is machine-asserted against the filesystem** — `packages/knowledge-hub/src/conformance/docs-page-coverage.test.ts` compares it to `apps/website/content/docs/**/*.mdx`, so adding a docs page without listing it in CP5 fails CI ([ADL](../decision-log/2026-08-20-cp5-page-list-is-asserted-against-the-filesystem.md)). +## Review Convergence + +- **Baseline then delta:** the first review is complete and returns the immutable 40-character + head it inspected. A re-review verifies prior findings plus only the diff from that head and + directly changed producer/consumer boundaries; an unchanged PR surface does not create another + fix round. Missing or invalid review-head evidence fails closed, never converges a PR. +- **Provisioned artifact proof:** a fix that installs, builds, publishes, names, or invokes an + artifact maps `producer -> published identity -> consumer` and proves the real path in a clean + temporary environment. The exact boundary is never stubbed, aliased, or faked. See ADL + [2026-08-31-review-baseline-and-provisioned-artifact-contract.md](../decision-log/2026-08-31-review-baseline-and-provisioned-artifact-contract.md). +- **Contract inventory before a loop:** before reporting or fixing a changed contract, inventory + its authoritative producer, inputs, consumers and representations. A finite protocol, parser, + configuration or state transition gets a complete decision table of supported and + invalid/boundary states, with a real probe/test per row. When a row, equivalence, normalization + or repair depends on an external tool/service/format, prove it at that authoritative boundary; + an internal unit test cannot prove external semantics or that repair advice works. Re-review + applies the same rule only to its delta and changed boundary. See ADLs + [2026-09-01-review-contract-inventory-prevents-serial-findings.md](../decision-log/2026-09-01-review-contract-inventory-prevents-serial-findings.md) + and [2026-09-01-external-boundary-proof-prevents-false-equivalence.md](../decision-log/2026-09-01-external-boundary-proof-prevents-false-equivalence.md). + ## Quality Gates - `pnpm quality-gate` is the adopted project-level quality gate command. -- Quality gate includes: type checking (`ts:check`), testing (`test`), linting (`lint`), formatting and markdown lint in **check mode** (`format:check`), plus a guard that the gate stays check-mode (`gate:composition`) and the smoke-scenario mode guard (`smoke-modes:check`). +- Quality gate includes: type checking (`ts:check`), testing (`test`), linting (`lint`), formatting and markdown lint in **check mode** (`format:check`), plus a guard that the gate stays check-mode (`gate:composition`), the smoke-scenario mode guard (`smoke-modes:check`) and the KB-index drift gate (`llms-index:check`). - **`format:check`/`format` coverage is whole-repo, derived from git, not turbo's per-workspace scope** (#414): `scripts/format-lib/run-format.sh` lists every path `git ls-files --cached --others --exclude-standard` reports (extension-filtered), so "gitignored ⇒ never checked" is git's own rule — nested `.gitignore` files and the user's global `core.excludesFile` apply by construction, with no re-implementation in the wrappers. Coverage excludes almost nothing: root-level and non-workspace files (`.claude/**`, `.pair/adoption/**`, `qa/**`, `scripts/**`) are checked exactly like workspace files. **One documented exception**: third-party skills installed under `.claude/skills/` (any directory not matching the `pair-*` prefix, e.g. a marketplace skill) are never checked — their formatting is not this project's to maintain. An empty derived file set is treated as a broken wrapper (exit 2), never a silent pass — see `scripts/format-lib/git-tracked-paths.sh`. The per-package, glob-based invocation (`pnpm --filter prettier:check`/`mdlint:check`) is unaffected and still uses the wrappers' own `_ignore-args.sh`/`_ignore-file.sh` ignore assembly. -- **A guard whose only caller is a turbo task is not enforced.** `turbo ts:check test lint` are cacheable with package-scoped inputs, so a change OUTSIDE the guard's package replays a cached PASS and the guard never executes. A guard over repo-wide state therefore gets a thin CLI and a **root gate step** (`hygiene:check`, `smoke-modes:check`, `docs:staleness`, `skills:conformance`), which run unconditionally — a unit test alone is the enforcement point only for logic whose inputs live inside its own package (#400). +- **A guard whose only caller is a turbo task is not enforced.** `turbo ts:check test lint` are cacheable with package-scoped inputs, so a change OUTSIDE the guard's package replays a cached PASS and the guard never executes. A guard over repo-wide state therefore gets a thin CLI and a **root gate step** (`hygiene:check`, `smoke-modes:check`, `docs:staleness`, `skills:conformance`, `llms-index:check`), which run unconditionally — a unit test alone is the enforcement point only for logic whose inputs live inside its own package (#400). - **No step reachable from the gate writes files**: the gate reports, `pnpm format` / `pnpm lint:fix` fix deliberately. `gate:composition` enforces this through an **explicit offender list** — the two formatters, eslint autofix, and the repo's write scripts (`sync-version`, `test:perf`) — so **adding a new write-mode script to this repo means adding it to that list**; a differently named writer passes the guard green. See ADL [2026-07-31-pre-push-gate-is-check-only.md](../decision-log/2026-07-31-pre-push-gate-is-check-only.md). - **Pre-merge tiering**: `disabled` (default) — every PR runs the full pre-merge check suite. Set to `enabled` to opt into risk-tier-scoped pre-merge checks (lighter checks on lower-risk PRs) per [tier-aware-pipeline.md](../../knowledge/guidelines/infrastructure/cicd-strategy/tier-aware-pipeline.md); `/pair-capability-setup-gates` reads this flag before generating the pipeline. - **Review enforcement**: `disabled` (default) — the pair review **runs and publishes its verdict**, but nothing it says blocks a merge: `pair-review` and `pair-explicit-approval` are not required status checks, and the 🔴 explicit-approval rule is advisory. Set to `enabled` to make them required and the rule binding, per [pr-states.md](../../knowledge/guidelines/collaboration/project-management-tool/pr-states.md); `/pair-capability-setup-gates` reads this flag before touching branch protection, and `/pair-process-bootstrap` asks for it when no decision exists. Disabled is the default deliberately: a review that blocks by default turns a first install into a repository nobody can merge into — on a single-maintainer repo the 🔴 non-author approval is unobtainable outright. The tier requirements themselves (reviewer count, SLA, checklist depth, whether 🔴 needs explicit approval) are redefinable in this file; that the review **runs** is not. - **Coverage guardrail**: `enabled` — pair dogfoods its own capability: the [`Coverage guardrail` step](../../../.github/workflows/ci.yml) in CI sources [`coverage-gate.sh`](../../knowledge/assets/coverage-gate.sh), extracts the line-coverage % from each package's istanbul `coverage-summary.json`, and blocks a PR whose coverage drops below the human-committed baseline in [`tech/coverage-baseline.md`](./coverage-baseline.md) (maintaining/improving passes — not an absolute wall). The framework **default remains `disabled`** (the dataset template ships off); this line is pair's project-level opt-in only. See [coverage guardrail](../../knowledge/guidelines/infrastructure/cicd-strategy/tier-aware-pipeline.md#coverage-guardrail-opt-in-regression-gate-consumed-by-this-pipeline) + [config format](../../knowledge/assets/coverage-config-example.md); `/pair-capability-setup-gates` reads this flag before generating the pipeline. **Coverage baseline commit-back**: `disabled` — the separate, nested opt-in ratchet (#372, framework default also `disabled`): when `enabled`, a **push to the base branch** (never a PR run, never a fork) proposes a raised `baseline.` as a **bot pull request** from `chore/coverage-baseline-ratchet`, never a push to `main`, and requires a repo-scoped `COVERAGE_RATCHET_TOKEN` (`contents: write` + `pull requests: write`, no protection bypass) — without it the step warns and the gate's verdict is unchanged. It stays `disabled` here until story #234's branch protection is applied and that secret is provisioned (ADR-018 lands with that story, so it is not linked from here yet); see ADL [2026-07-30-coverage-ratchet-pr-not-push.md](../decision-log/2026-07-30-coverage-ratchet-pr-not-push.md). The step that runs it is the **shipped** KB asset `node .pair/knowledge/assets/coverage-ratchet.cjs` (ADR-023) — the same one an adopter's generated pipeline invokes, so this flag being `enabled` means the same thing here as anywhere else. - **Pair review required checks**: `pair-review` + `pair-explicit-approval` are the required status checks that make the judgment review unskippable (R5.7) and enforce the 🔴 explicit-human-approval rule (D10) — see [pr-states.md](../../knowledge/guidelines/collaboration/project-management-tool/pr-states.md) and [ADR-018](adr/adr-018-pr-state-flow-required-checks.md). Status on this repo: **not yet applied** — writing branch protection needs admin scope, so it is a deliberate human step; until applied, enforcement here is advisory (the documented degraded mode). **Ordering constraint** (applies in this order, or every merge stops): 1. provision the `pr-state:*` labels + add the `pair-explicit-approval` workflow (neither needs admin scope — this repo has not added the workflow yet, so the context does not report today); 2. confirm on a real PR that `pair-review` and `pair-explicit-approval` both report on the head commit, **and** that the approval context re-reports on that same head SHA after a review submission; 3. only then `PUT` the branch protection, keeping `enforce_admins` off until one PR has merged through it. The whole sequence (including the merge-block outcomes per tier) was executed on a throwaway repository — see `github-implementation.md` § "Verified on a throwaway repository" — so what remains here is applying it, not discovering whether it works. **This repo is single-maintainer**, so a 🔴 PR cannot satisfy `pair-explicit-approval` (GitHub rejects a self-approval): a second human reviewer account is a prerequisite for making that context required here — otherwise leave it out of the required list and keep the 🔴 rule advisory. The solo-maintainer alternative (a verified human approval token instead of a second account) is tracked as [#398](https://github.com/foomakers/pair/issues/398). **When the protection is written here, use the `checks` form with `app_id` pinned** for `pair-explicit-approval` (an unpinned status context is satisfiable by any push-access token, including the agent's); `pair-review` stays unpinned and is an anti-accident control, not an authorization control — see `github-implementation.md` § "What each context proves". +- **KB index drift (`llms-index:check`, #416):** `.pair/llms.txt` is generated by the CLI on every `pair-cli install` / `pair-cli update` (`writeProjectLlmsTxt`) **and** tracked — it is the index `CLAUDE.md` points every agent at — so it is compared against its generator, never trusted. The gate runs `generateLlmsTxt` over the repo tree and requires byte equality with the committed file; on a mismatch it prints the **missing and extra lines** and names `pnpm llms-index:regen`, and it **never writes** the file (a gate that fixed the drift would hide it). The remedy it names is a command that EXISTS and is on the write-mode offender list: `llms-index:regen` (`llms-txt-regenerate.ts`) is the check's exact inverse — same generator, same tracked path, one file written — and it REFUSES on every state whose caution says not to regenerate (CR-carrying checkout, sparse tree, unreadable index or tree). It is deliberately NOT the generating install command — `pair-cli update` without `--source` installs the **published** KB over `.pair/knowledge/**`, reverting the very guideline whose addition reddened the gate (and the retired spelling `pair update` names no executable at all: the published `bin` is `pair-cli`) — and not a mirror realignment (#419's `pnpm mirrors:regenerate` owns that, with a far larger blast radius): see ADL [2026-09-03-a-gate-names-a-remedy-it-can-run.md](../decision-log/2026-09-03-a-gate-names-a-remedy-it-can-run.md). It is a **guard over repo-wide state**, so per the bullet above it is a root gate step in `quality-gate` and a named step in `ci.yml`, not only a unit test. Mechanism — the gate imports the generator by SOURCE path (one definition of the index format, judged against the working tree and never a stale `dist/`, at the cost of `"composite": false` on the non-emitting `@pair/dev-tools`), runs it **transpile-only** (`ts-node -T`) so the verdict never depends on whether someone built `@pair/content-ops`, and is handed a read-only file-system slice so "cannot write" is a type fact: ADL [2026-09-01-a-gate-imports-its-generator-by-source-and-gets-a-read-only-slice.md](../decision-log/2026-09-01-a-gate-imports-its-generator-by-source-and-gets-a-read-only-slice.md). What it compares is the transform's output, per ADL [2026-08-11-a-mirror-guard-compares-the-transform.md](../decision-log/2026-08-11-a-mirror-guard-compares-the-transform.md); the index's entry order is fixed by the content (UTF-16 code-unit order, which is codepoint order for every BMP path), never by locale collation, and every emitted entry path uses POSIX separators (`posix.join`) so a Windows checkout produces the same bytes rather than 562 backslash-separated, unresolvable links, per ADL [2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md](../decision-log/2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md) — the invariant is determinism across environments, not a particular collation, and it covers the line terminator too: `.gitattributes` pins the repo (and the index by name) to `eol=lf`, and a checkout carrying **any** carriage return (CRLF, or the bare CR only a hand-rolled conversion writes) is reported as a terminator mismatch with the re-checkout that fixes it (`rm` + `git checkout --`, the recipe verified to rewrite the working tree — `git add --renormalize` is inert when the index is already LF), never as 570 missing plus 570 extra lines under advice that cannot end the loop. - **Gate & tooling code:** a gate's logic lives in a tested module in its owning package (white-box unit tests); scripts/CLIs are thin entrypoints and a root gate delegates (`pnpm --filter `). Scripts are never unit-tested — CLI-level checks go to smoke tests. See ADL [2026-07-13-gate-tooling-code-in-tested-modules.md](../decision-log/2026-07-13-gate-tooling-code-in-tested-modules.md). Gate/tooling packages are organized by bounded context, not one package per tool family — a new tool family sharing an existing package's bounded context is a new folder there, not a new package. See [ADR-014](adr/adr-014-tool-package-boundary-by-bounded-context.md). - **Conformance tests** (`packages/knowledge-hub/src/conformance/`): one test file per target KB artifact (a `SKILL.md`, guideline, or template), not per introducing story — a new story extends the matching file's `describe` block instead of adding a new story-named file. See ADL [2026-07-18-conformance-test-per-file-not-per-story.md](../decision-log/2026-07-18-conformance-test-per-file-not-per-story.md). - **Monorepo tooling gotchas** (e.g. `pnpm --filter` bypassing turbo's `dependsOn` graph on a fresh checkout): documented once, centrally, in `DEVELOPMENT.md`'s `Turbo Caching` section — affected packages' READMEs carry only a short pointer, not a full copy. See ADL [2026-07-18-workspace-gotcha-doc-placement.md](../decision-log/2026-07-18-workspace-gotcha-doc-placement.md). diff --git a/.pair/llms.txt b/.pair/llms.txt index 9ca1efe76..5a5861140 100644 --- a/.pair/llms.txt +++ b/.pair/llms.txt @@ -4,8 +4,9 @@ ## Adoption — Product -- [Context Map](.pair/adoption/product/context-map.md) - [Product Requirements Document (PRD)](.pair/adoption/product/PRD.md) +- [Context Map](.pair/adoption/product/context-map.md) +- [Subdomain Catalog Index](.pair/adoption/product/subdomain/README.md) - [Adoption & Guidelines (Supporting Subdomain)](.pair/adoption/product/subdomain/adoption-guidelines.md) - [Code & Documentation Generation (Core Subdomain)](.pair/adoption/product/subdomain/code-documentation-generation.md) - [Collaborative Workflow — Context](.pair/adoption/product/subdomain/collaborative-workflow.context.md) @@ -13,10 +14,10 @@ - [Development Tooling Standards (Generic Subdomain)](.pair/adoption/product/subdomain/development-tooling-standards.md) - [How To Knowledge (Supporting Subdomain)](.pair/adoption/product/subdomain/how-to-knowledge.md) - [Integration & Process Standardization (Supporting Subdomain)](.pair/adoption/product/subdomain/integration-process-standardization.md) -- [Subdomain Catalog Index](.pair/adoption/product/subdomain/README.md) ## Adoption — Tech +- [📋 Adopted Standards & Practices](.pair/adoption/tech/README.md) - [ADR-001: TTY Detection Pattern for CLI UX](.pair/adoption/tech/adr/adr-001-tty-detection-pattern.md) - [ADR-002: HTTP Range Requests for Download Resume](.pair/adoption/tech/adr/adr-002-http-range-resume.md) - [ADR-003: SHA256 Checksum Validation for File Integrity](.pair/adoption/tech/adr/adr-003-checksum-validation.md) @@ -43,13 +44,12 @@ - [ADR-023: The coverage-baseline ratchet ships as a GENERATED KB asset, not as a CLI command](.pair/adoption/tech/adr/adr-023-coverage-ratchet-ships-as-a-generated-kb-asset.md) - [Architecture](.pair/adoption/tech/architecture.md) - [Automation Policy — this project's delta](.pair/adoption/tech/automation.md) +- [Bounded Context Catalog (Grouped)](.pair/adoption/tech/boundedcontext/README.md) - [Development Collaboration Context](.pair/adoption/tech/boundedcontext/development-collaboration.md) - [Integration & Process Standardization Context](.pair/adoption/tech/boundedcontext/integration-process-standardization.md) - [Knowledge & Standards Context](.pair/adoption/tech/boundedcontext/knowledge-standards.md) -- [Bounded Context Catalog (Grouped)](.pair/adoption/tech/boundedcontext/README.md) - [`tech/coverage-baseline.md` — pair coverage guardrail config](.pair/adoption/tech/coverage-baseline.md) - [Infrastructure](.pair/adoption/tech/infrastructure.md) -- [📋 Adopted Standards & Practices](.pair/adoption/tech/README.md) - [`tech/risk-matrix.md`](.pair/adoption/tech/risk-matrix.md) - [Tech Stack](.pair/adoption/tech/tech-stack.md) - [UX/UI](.pair/adoption/tech/ux-ui.md) @@ -125,6 +125,12 @@ - [Decision: the CLI invocation name is `pair-cli`, not `pair`](.pair/adoption/decision-log/2026-08-25-cli-invocation-canonical-name-is-pair-cli.md) - [Decision: Post-merge cleanup covers local branches and worktrees; PR analyses retire at merge](.pair/adoption/decision-log/2026-08-25-post-merge-cleanup-covers-local-branches-pr-analyses-retire-at-merge.md) - [Decision: tier 1's `$approval` posture is unconditional, and tier 1 has no declaring composition site yet](.pair/adoption/decision-log/2026-08-28-tier1-approval-posture-is-unconditional-and-has-no-declaring-composition-site-yet.md) +- [Decision: Review re-checks use an immutable baseline and prove provisioned artifacts](.pair/adoption/decision-log/2026-08-31-review-baseline-and-provisioned-artifact-contract.md) +- [Decision: a generated artifact that is tracked and byte-compared is byte-reproducible across environments — fixed entry order, pinned line endings](.pair/adoption/decision-log/2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md) +- [Decision: a quality gate imports the generator it checks by SOURCE path, and the generator hands it a read-only file-system slice](.pair/adoption/decision-log/2026-09-01-a-gate-imports-its-generator-by-source-and-gets-a-read-only-slice.md) +- [Decision: External boundary proof prevents false equivalence](.pair/adoption/decision-log/2026-09-01-external-boundary-proof-prevents-false-equivalence.md) +- [Decision: Review contract inventory prevents serial findings](.pair/adoption/decision-log/2026-09-01-review-contract-inventory-prevents-serial-findings.md) +- [Decision: a gate's remedy is a command its audience can run, and the remedy refuses the states its own caution warns about](.pair/adoption/decision-log/2026-09-03-a-gate-names-a-remedy-it-can-run.md) ## How-To Guides @@ -140,6 +146,9 @@ ## Guidelines +- [📚 Technical Guidelines Knowledge Base](.pair/knowledge/guidelines/README.md) +- [Architecture](.pair/knowledge/guidelines/architecture/README.md) +- [Architectural Patterns](.pair/knowledge/guidelines/architecture/architectural-patterns/README.md) - [Clean Architecture Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/clean-architecture.md) - [Continuous Architecture Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/continuous-architecture.md) - [CQRS (Command Query Responsibility Segregation)](.pair/knowledge/guidelines/architecture/architectural-patterns/cqrs.md) @@ -147,106 +156,106 @@ - [Event Sourcing Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/event-sourcing.md) - [Hexagonal Architecture (Ports and Adapters)](.pair/knowledge/guidelines/architecture/architectural-patterns/hexagonal.md) - [Layered Architecture Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/layer-architecture.md) -- [Architectural Patterns](.pair/knowledge/guidelines/architecture/architectural-patterns/README.md) - [Transaction Script Pattern](.pair/knowledge/guidelines/architecture/architectural-patterns/transaction-script.md) +- [Decision Frameworks](.pair/knowledge/guidelines/architecture/decision-frameworks/README.md) - [📋 Decision Records Practice (Level 2)](.pair/knowledge/guidelines/architecture/decision-frameworks/adr-process.md) - [Decision Tracking Framework](.pair/knowledge/guidelines/architecture/decision-frameworks/decision-tracking.md) - [Evolution Strategy Framework](.pair/knowledge/guidelines/architecture/decision-frameworks/evolution-strategy.md) -- [Decision Frameworks](.pair/knowledge/guidelines/architecture/decision-frameworks/README.md) - [Technology Selection Framework](.pair/knowledge/guidelines/architecture/decision-frameworks/technology-selection.md) +- [Deployment Architecture Patterns](.pair/knowledge/guidelines/architecture/deployment-architectures/README.md) - [Desktop Self-Hosted Deployment](.pair/knowledge/guidelines/architecture/deployment-architectures/desktop-self-hosted.md) - [Hybrid Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/hybrid.md) - [Microservices Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/microservices.md) - [Modular Monolith Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/modular-monolith.md) -- [Deployment Architecture Patterns](.pair/knowledge/guidelines/architecture/deployment-architectures/README.md) - [Serverless Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/serverless.md) - [Structured Monolith Architecture](.pair/knowledge/guidelines/architecture/deployment-architectures/structured-monolith.md) +- [Design Patterns](.pair/knowledge/guidelines/architecture/design-patterns/README.md) - [Bounded Context Patterns and Implementation](.pair/knowledge/guidelines/architecture/design-patterns/bounded-contexts.md) - [Context Map Inline-Maintenance Guideline](.pair/knowledge/guidelines/architecture/design-patterns/context-map-maintenance.md) - [Coupling Balance](.pair/knowledge/guidelines/architecture/design-patterns/coupling-balance.md) - [Domain-Driven Design (DDD) Implementation Guide](.pair/knowledge/guidelines/architecture/design-patterns/domain-driven-design.md) - [System Integration Patterns](.pair/knowledge/guidelines/architecture/design-patterns/integration-patterns.md) - [Monorepo Architecture](.pair/knowledge/guidelines/architecture/design-patterns/monorepo.md) -- [Design Patterns](.pair/knowledge/guidelines/architecture/design-patterns/README.md) - [Repository Structure](.pair/knowledge/guidelines/architecture/design-patterns/repository-structure.md) - [Strategic Subdomain Definition Guide](.pair/knowledge/guidelines/architecture/design-patterns/strategic-subdomain-definition.md) - [System Design](.pair/knowledge/guidelines/architecture/design-patterns/system-design.md) - [Workspace Organization](.pair/knowledge/guidelines/architecture/design-patterns/workspace-organization.md) +- [LLM Integration Architecture](.pair/knowledge/guidelines/architecture/llm-integration/README.md) - [Agent Coordination and Communication Patterns](.pair/knowledge/guidelines/architecture/llm-integration/agent-coordination.md) - [AI Workflows and Agent Coordination](.pair/knowledge/guidelines/architecture/llm-integration/ai-workflows.md) - [Model Context Protocol (MCP) Development](.pair/knowledge/guidelines/architecture/llm-integration/mcp-development.md) - [Performance & Security for LLM Integration](.pair/knowledge/guidelines/architecture/llm-integration/performance-security.md) - [RAG Architecture Patterns](.pair/knowledge/guidelines/architecture/llm-integration/rag-architecture.md) -- [LLM Integration Architecture](.pair/knowledge/guidelines/architecture/llm-integration/README.md) - [Vector Databases for LLM Integration](.pair/knowledge/guidelines/architecture/llm-integration/vector-databases.md) +- [Project Architecture Constraints](.pair/knowledge/guidelines/architecture/project-constraints/README.md) - [Implementation Guidelines](.pair/knowledge/guidelines/architecture/project-constraints/deployment-constraints.md) - [Platform & Deployment Constraints](.pair/knowledge/guidelines/architecture/project-constraints/platform-constraints.md) -- [Project Architecture Constraints](.pair/knowledge/guidelines/architecture/project-constraints/README.md) - [Team & Development Constraints](.pair/knowledge/guidelines/architecture/project-constraints/team-constraints.md) -- [Architecture](.pair/knowledge/guidelines/architecture/README.md) +- [Code Design](.pair/knowledge/guidelines/code-design/README.md) +- [Code Organization](.pair/knowledge/guidelines/code-design/code-organization/README.md) - [Feature Architecture](.pair/knowledge/guidelines/code-design/code-organization/feature-architecture.md) - [File Structure](.pair/knowledge/guidelines/code-design/code-organization/file-structure.md) - [Naming Conventions](.pair/knowledge/guidelines/code-design/code-organization/naming-conventions.md) -- [Code Organization](.pair/knowledge/guidelines/code-design/code-organization/README.md) - [Workspace Structure](.pair/knowledge/guidelines/code-design/code-organization/workspace-structure.md) +- [Design Principles](.pair/knowledge/guidelines/code-design/design-principles/README.md) - [Design Rules](.pair/knowledge/guidelines/code-design/design-principles/design-rules.md) - [Error Handling](.pair/knowledge/guidelines/code-design/design-principles/error-handling.md) - [Functional Programming](.pair/knowledge/guidelines/code-design/design-principles/functional-programming.md) - [Mocking Strategy](.pair/knowledge/guidelines/code-design/design-principles/mocking-strategy.md) -- [Design Principles](.pair/knowledge/guidelines/code-design/design-principles/README.md) - [Service Abstraction](.pair/knowledge/guidelines/code-design/design-principles/service-abstraction.md) - [Service Factory](.pair/knowledge/guidelines/code-design/design-principles/service-factory.md) - [SOLID Principles](.pair/knowledge/guidelines/code-design/design-principles/solid-principles.md) +- [Framework Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/README.md) - [React Components](.pair/knowledge/guidelines/code-design/framework-patterns/components.md) - [Dependency Injection Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/dependency-injection.md) - [Fastify Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/fastify.md) - [React Hooks](.pair/knowledge/guidelines/code-design/framework-patterns/hooks.md) - [React & Next.js Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/react-nextjs.md) -- [Framework Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/README.md) - [Repository Pattern](.pair/knowledge/guidelines/code-design/framework-patterns/repository-pattern.md) - [Server Patterns](.pair/knowledge/guidelines/code-design/framework-patterns/server-patterns.md) - [Service Layer](.pair/knowledge/guidelines/code-design/framework-patterns/service-layer.md) - [State Management](.pair/knowledge/guidelines/code-design/framework-patterns/state-management.md) - [TypeScript](.pair/knowledge/guidelines/code-design/framework-patterns/typescript.md) -- [pnpm Package Management](.pair/knowledge/guidelines/code-design/package-management/pnpm.md) - [Package Management](.pair/knowledge/guidelines/code-design/package-management/README.md) +- [pnpm Package Management](.pair/knowledge/guidelines/code-design/package-management/pnpm.md) - [Shared Dependencies Management](.pair/knowledge/guidelines/code-design/package-management/shared-dependencies.md) - [Version Catalog Management](.pair/knowledge/guidelines/code-design/package-management/version-catalog.md) - [Workspace Configuration](.pair/knowledge/guidelines/code-design/package-management/workspace-config.md) +- [Quality Standards](.pair/knowledge/guidelines/code-design/quality-standards/README.md) - [Quality Automation](.pair/knowledge/guidelines/code-design/quality-standards/automation.md) - [Code Metrics](.pair/knowledge/guidelines/code-design/quality-standards/code-metrics.md) - [Test Coverage](.pair/knowledge/guidelines/code-design/quality-standards/coverage.md) - [ESLint](.pair/knowledge/guidelines/code-design/quality-standards/eslint.md) - [Linting Tools](.pair/knowledge/guidelines/code-design/quality-standards/linting-tools.md) - [Prettier Formatting](.pair/knowledge/guidelines/code-design/quality-standards/prettier-formatting.md) -- [Quality Standards](.pair/knowledge/guidelines/code-design/quality-standards/README.md) - [Shared Config Packages](.pair/knowledge/guidelines/code-design/quality-standards/shared-config-packages.md) - [Technical Debt Management](.pair/knowledge/guidelines/code-design/quality-standards/technical-debt.md) -- [Code Design](.pair/knowledge/guidelines/code-design/README.md) +- [Collaboration Guidelines](.pair/knowledge/guidelines/collaboration/README.md) +- [Collaboration Automation Framework](.pair/knowledge/guidelines/collaboration/automation/README.md) - [Automation Policy — `tech/automation.md`](.pair/knowledge/guidelines/collaboration/automation/automation-policy.md) - [Azure DevOps Automation](.pair/knowledge/guidelines/collaboration/automation/azure-devops-automation.md) - [Filesystem Automation](.pair/knowledge/guidelines/collaboration/automation/filesystem-automation.md) - [GitHub Automation](.pair/knowledge/guidelines/collaboration/automation/github-automation.md) -- [Collaboration Automation Framework](.pair/knowledge/guidelines/collaboration/automation/README.md) - [Decision Records: ADR, ADL, DDR, and Analysis-Log](.pair/knowledge/guidelines/collaboration/decision-records.md) +- [Estimation Framework](.pair/knowledge/guidelines/collaboration/estimation/README.md) - [AI-Assisted Estimation](.pair/knowledge/guidelines/collaboration/estimation/ai-assisted-estimation.md) - [Complexity-Based Estimation](.pair/knowledge/guidelines/collaboration/estimation/complexity-based-estimation.md) - [Forecast-Based Estimation](.pair/knowledge/guidelines/collaboration/estimation/forecast-based-estimation.md) - [Hybrid Estimation](.pair/knowledge/guidelines/collaboration/estimation/hybrid-estimation.md) -- [Estimation Framework](.pair/knowledge/guidelines/collaboration/estimation/README.md) - [Time-Based Estimation](.pair/knowledge/guidelines/collaboration/estimation/time-based-estimation.md) +- [Issue Management Framework](.pair/knowledge/guidelines/collaboration/issue-management/README.md) - [Azure DevOps Work Items](.pair/knowledge/guidelines/collaboration/issue-management/azure-devops-issues.md) - [Filesystem Issue Tracking](.pair/knowledge/guidelines/collaboration/issue-management/filesystem-issues.md) - [GitHub Issues](.pair/knowledge/guidelines/collaboration/issue-management/github-issues.md) - [Linear Issues](.pair/knowledge/guidelines/collaboration/issue-management/linear-issues.md) -- [Issue Management Framework](.pair/knowledge/guidelines/collaboration/issue-management/README.md) +- [Methodology Selection Framework](.pair/knowledge/guidelines/collaboration/methodology/README.md) - [Kanban Methodology](.pair/knowledge/guidelines/collaboration/methodology/kanban.md) - [Lean Methodology](.pair/knowledge/guidelines/collaboration/methodology/lean.md) - [Large-Scale Scrum (LeSS) Methodology](.pair/knowledge/guidelines/collaboration/methodology/less.md) -- [Methodology Selection Framework](.pair/knowledge/guidelines/collaboration/methodology/README.md) - [SAFe (Scaled Agile Framework)](.pair/knowledge/guidelines/collaboration/methodology/safe.md) - [Scrum Methodology](.pair/knowledge/guidelines/collaboration/methodology/scrum.md) - [Waterfall Methodology](.pair/knowledge/guidelines/collaboration/methodology/waterfall.md) +- [Project Management Tool Framework](.pair/knowledge/guidelines/collaboration/project-management-tool/README.md) - [Azure DevOps - Complete Implementation Guide](.pair/knowledge/guidelines/collaboration/project-management-tool/azure-devops-implementation.md) - [Canonical States & State Mapping](.pair/knowledge/guidelines/collaboration/project-management-tool/canonical-states.md) - [Definition of Ready & Definition of Done](.pair/knowledge/guidelines/collaboration/project-management-tool/definition-of-ready-and-done.md) @@ -254,19 +263,18 @@ - [GitHub Projects - Complete Implementation Guide](.pair/knowledge/guidelines/collaboration/project-management-tool/github-implementation.md) - [Linear - Complete Implementation Guide](.pair/knowledge/guidelines/collaboration/project-management-tool/linear-implementation.md) - [PR State Flow — gate ≠ review](.pair/knowledge/guidelines/collaboration/project-management-tool/pr-states.md) -- [Project Management Tool Framework](.pair/knowledge/guidelines/collaboration/project-management-tool/README.md) +- [Project Tracking Framework](.pair/knowledge/guidelines/collaboration/project-tracking/README.md) - [Azure DevOps Project Tracking](.pair/knowledge/guidelines/collaboration/project-tracking/azure-devops-tracking.md) - [Filesystem Project Tracking](.pair/knowledge/guidelines/collaboration/project-tracking/filesystem-tracking.md) - [GitHub Project Tracking](.pair/knowledge/guidelines/collaboration/project-tracking/github-tracking.md) -- [Project Tracking Framework](.pair/knowledge/guidelines/collaboration/project-tracking/README.md) -- [Collaboration Guidelines](.pair/knowledge/guidelines/collaboration/README.md) +- [Team Collaboration Framework](.pair/knowledge/guidelines/collaboration/team/README.md) - [Communication Protocols](.pair/knowledge/guidelines/collaboration/team/communication-protocols.md) - [Decision Making](.pair/knowledge/guidelines/collaboration/team/decision-making.md) -- [Team Collaboration Framework](.pair/knowledge/guidelines/collaboration/team/README.md) - [Remote Work](.pair/knowledge/guidelines/collaboration/team/remote-work.md) - [Role Responsibilities](.pair/knowledge/guidelines/collaboration/team/role-responsibilities.md) - [Scenarios](.pair/knowledge/guidelines/collaboration/team/scenarios.md) - [Standards](.pair/knowledge/guidelines/collaboration/team/standards.md) +- [Project Management Templates](.pair/knowledge/guidelines/collaboration/templates/README.md) - [Decision: [Decision Title]](.pair/knowledge/guidelines/collaboration/templates/adl-template.md) - [ADR: [Decision Title]](.pair/knowledge/guidelines/collaboration/templates/adr-template.md) - [Analysis Log: [Analysis Title]](.pair/knowledge/guidelines/collaboration/templates/analysis-log-template.md) @@ -282,87 +290,88 @@ - [Manual Test Case Template](.pair/knowledge/guidelines/collaboration/templates/manual-test-case-template.md) - [Manual Test Report Template](.pair/knowledge/guidelines/collaboration/templates/manual-test-report-template.md) - [Pull Request Template](.pair/knowledge/guidelines/collaboration/templates/pr-template.md) -- [Project Management Templates](.pair/knowledge/guidelines/collaboration/templates/README.md) - [[Subdomain Name] — Context](.pair/knowledge/guidelines/collaboration/templates/subdomain-context-template.md) - [[Subdomain Name] ([Classification] Subdomain)](.pair/knowledge/guidelines/collaboration/templates/subdomain-template.md) - [Task Template](.pair/knowledge/guidelines/collaboration/templates/task-template.md) - [User Story Template](.pair/knowledge/guidelines/collaboration/templates/user-story-template.md) - [Working Area Convention](.pair/knowledge/guidelines/collaboration/working-area.md) +- [🏗️ Infrastructure Knowledge Base](.pair/knowledge/guidelines/infrastructure/README.md) +- [� CI/CD Strategy Practice](.pair/knowledge/guidelines/infrastructure/cicd-strategy/README.md) - [CI/CD Artifacts Management](.pair/knowledge/guidelines/infrastructure/cicd-strategy/artifacts.md) - [GitHub Actions Implementation](.pair/knowledge/guidelines/infrastructure/cicd-strategy/github-actions-implementation.md) -- [� CI/CD Strategy Practice](.pair/knowledge/guidelines/infrastructure/cicd-strategy/README.md) - [Secrets Management](.pair/knowledge/guidelines/infrastructure/cicd-strategy/secrets-management.md) - [CI/CD Strategy](.pair/knowledge/guidelines/infrastructure/cicd-strategy/strategy.md) - [Tier-Aware Pre-Merge Pipeline](.pair/knowledge/guidelines/infrastructure/cicd-strategy/tier-aware-pipeline.md) +- [☁️ Cloud Providers Strategy Practice](.pair/knowledge/guidelines/infrastructure/cloud-providers/README.md) - [AWS Deployment Patterns](.pair/knowledge/guidelines/infrastructure/cloud-providers/aws-deployment.md) - [Cloud Cost Optimization Strategy](.pair/knowledge/guidelines/infrastructure/cloud-providers/cost-optimization.md) - [GCP Deployment Patterns](.pair/knowledge/guidelines/infrastructure/cloud-providers/gcp-deployment.md) - [Multi-Cloud Architecture Strategy](.pair/knowledge/guidelines/infrastructure/cloud-providers/multi-cloud.md) - [Cloud Provider Evaluation Framework](.pair/knowledge/guidelines/infrastructure/cloud-providers/provider-evaluation.md) -- [☁️ Cloud Providers Strategy Practice](.pair/knowledge/guidelines/infrastructure/cloud-providers/README.md) - [Vercel Deployment Patterns](.pair/knowledge/guidelines/infrastructure/cloud-providers/vercel-deployment.md) +- [☁️ Cloud Services Integration Practice](.pair/knowledge/guidelines/infrastructure/cloud-services/README.md) - [Cloud Compute Services](.pair/knowledge/guidelines/infrastructure/cloud-services/cloud-compute.md) - [Cloud Database Services](.pair/knowledge/guidelines/infrastructure/cloud-services/cloud-databases.md) - [Cloud DevOps Services](.pair/knowledge/guidelines/infrastructure/cloud-services/cloud-devops.md) - [Cloud Storage Services](.pair/knowledge/guidelines/infrastructure/cloud-services/cloud-storage.md) -- [☁️ Cloud Services Integration Practice](.pair/knowledge/guidelines/infrastructure/cloud-services/README.md) +- [🐳 Container Orchestration Practice](.pair/knowledge/guidelines/infrastructure/container-orchestration/README.md) - [Container Strategy](.pair/knowledge/guidelines/infrastructure/container-orchestration/container-strategy.md) - [Docker Compose Implementation](.pair/knowledge/guidelines/infrastructure/container-orchestration/docker-compose.md) - [Docker Implementation](.pair/knowledge/guidelines/infrastructure/container-orchestration/docker.md) - [Kubernetes Implementation](.pair/knowledge/guidelines/infrastructure/container-orchestration/kubernetes.md) -- [🐳 Container Orchestration Practice](.pair/knowledge/guidelines/infrastructure/container-orchestration/README.md) +- [🚀 Deployment Patterns Practice](.pair/knowledge/guidelines/infrastructure/deployment-patterns/README.md) - [🚀 Deployment Strategies](.pair/knowledge/guidelines/infrastructure/deployment-patterns/deployment-strategies.md) - [📊 Deployment Monitoring](.pair/knowledge/guidelines/infrastructure/deployment-patterns/monitoring.md) - [⚡ Deployment Performance Optimization](.pair/knowledge/guidelines/infrastructure/deployment-patterns/performance.md) -- [🚀 Deployment Patterns Practice](.pair/knowledge/guidelines/infrastructure/deployment-patterns/README.md) - [🔒 Deployment Security](.pair/knowledge/guidelines/infrastructure/deployment-patterns/security.md) +- [🌍 Environment Management Practice](.pair/knowledge/guidelines/infrastructure/environments/README.md) - [⚙️ Environment Configuration Management](.pair/knowledge/guidelines/infrastructure/environments/environment-config.md) - [🔄 Environment Consistency](.pair/knowledge/guidelines/infrastructure/environments/environment-consistency.md) - [💻 Local Development Environment](.pair/knowledge/guidelines/infrastructure/environments/local-development.md) - [🏭 Production Environment Management](.pair/knowledge/guidelines/infrastructure/environments/production-development.md) -- [🌍 Environment Management Practice](.pair/knowledge/guidelines/infrastructure/environments/README.md) - [🔍 Service Discovery Infrastructure](.pair/knowledge/guidelines/infrastructure/environments/service-discovery.md) - [🎭 Staging Environment Management](.pair/knowledge/guidelines/infrastructure/environments/staging-development.md) +- [🏗️ Infrastructure as Code Practice](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/README.md) - [🤖 Infrastructure Automation](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/automation.md) - [☁️ AWS CDK Implementation Guide](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/aws-cdk-implementation.md) - [📚 Infrastructure as Code Best Practices](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/iac-best-practices.md) - [🎯 Infrastructure Operational Excellence](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/operational-excellence.md) -- [🏗️ Infrastructure as Code Practice](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/README.md) - [🗄️ Infrastructure State Management](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/state-management.md) - [🏗️ Terraform Implementation Guide](.pair/knowledge/guidelines/infrastructure/infrastructure-as-code/terraform.md) -- [🏗️ Infrastructure Knowledge Base](.pair/knowledge/guidelines/infrastructure/README.md) -- [⚡ Performance Testing Infrastructure](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/performance-testing.md) - [🧪 Testing Infrastructure Practice](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/README.md) +- [⚡ Performance Testing Infrastructure](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/performance-testing.md) - [🗄️ Test Database Management](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/test-databases.md) - [🧪 Test Environment Management](.pair/knowledge/guidelines/infrastructure/testing-infrastructure/test-environments.md) +- [Observability Guidelines](.pair/knowledge/guidelines/observability/README.md) - [AI-Enhanced Observability](.pair/knowledge/guidelines/observability/ai-enhanced-observability.md) -- [Notification Strategies](.pair/knowledge/guidelines/observability/alerting/notifications.md) - [Alerting Guidelines](.pair/knowledge/guidelines/observability/alerting/README.md) +- [Notification Strategies](.pair/knowledge/guidelines/observability/alerting/notifications.md) - [Alerting Strategy](.pair/knowledge/guidelines/observability/alerting/strategy.md) - [Dashboards and Visualization](.pair/knowledge/guidelines/observability/dashboards-visualization.md) - [Distributed Tracing](.pair/knowledge/guidelines/observability/distributed-tracing.md) +- [Metrics Guidelines](.pair/knowledge/guidelines/observability/metrics/README.md) - [Application Monitoring Metrics](.pair/knowledge/guidelines/observability/metrics/application-monitoring.md) - [Business Metrics](.pair/knowledge/guidelines/observability/metrics/business-metrics.md) - [Custom Metrics](.pair/knowledge/guidelines/observability/metrics/custom-metrics.md) - [Feature Usage Metrics](.pair/knowledge/guidelines/observability/metrics/feature-usage.md) - [Performance Metrics](.pair/knowledge/guidelines/observability/metrics/performance-metrics.md) -- [Metrics Guidelines](.pair/knowledge/guidelines/observability/metrics/README.md) - [Metrics Strategy](.pair/knowledge/guidelines/observability/metrics/strategy.md) - [User Experience Metrics](.pair/knowledge/guidelines/observability/metrics/user-experience.md) -- [Proactive Monitoring](.pair/knowledge/guidelines/observability/observability-principles/proactive-monitoring.md) - [Observability Principles](.pair/knowledge/guidelines/observability/observability-principles/README.md) +- [Proactive Monitoring](.pair/knowledge/guidelines/observability/observability-principles/proactive-monitoring.md) - [Three Pillars of Observability](.pair/knowledge/guidelines/observability/observability-principles/three-pillars.md) - [Observability Tools](.pair/knowledge/guidelines/observability/observability-tools.md) - [Performance Analysis](.pair/knowledge/guidelines/observability/performance-analysis.md) - [Proactive Detection](.pair/knowledge/guidelines/observability/proactive-detection.md) -- [Observability Guidelines](.pair/knowledge/guidelines/observability/README.md) +- [Structured Logging Guidelines](.pair/knowledge/guidelines/observability/structured-logging/README.md) - [Contextual Information](.pair/knowledge/guidelines/observability/structured-logging/contextual-information.md) - [JSON Logging Standards](.pair/knowledge/guidelines/observability/structured-logging/json-logging.md) - [Log Levels](.pair/knowledge/guidelines/observability/structured-logging/log-levels.md) - [Logging Standards](.pair/knowledge/guidelines/observability/structured-logging/logging-standards.md) -- [Structured Logging Guidelines](.pair/knowledge/guidelines/observability/structured-logging/README.md) - [Sensitive Data Protection](.pair/knowledge/guidelines/observability/structured-logging/sensitive-data-protection.md) - [Workflow Integration](.pair/knowledge/guidelines/observability/workflow-integration.md) +- [Quality Assurance Framework](.pair/knowledge/guidelines/quality-assurance/README.md) +- [Accessibility Framework](.pair/knowledge/guidelines/quality-assurance/accessibility/README.md) - [Assistive Technology Integration](.pair/knowledge/guidelines/quality-assurance/accessibility/assistive-technology.md) - [automated-testing](.pair/knowledge/guidelines/quality-assurance/accessibility/automated-testing.md) - [Browser Extensions for Accessibility Testing](.pair/knowledge/guidelines/quality-assurance/accessibility/browser-extensions.md) @@ -378,7 +387,6 @@ - [Platform-Specific Accessibility](.pair/knowledge/guidelines/quality-assurance/accessibility/platform-specific.md) - [POUR Principles Implementation](.pair/knowledge/guidelines/quality-assurance/accessibility/pour-principles.md) - [React TypeScript Accessibility Patterns](.pair/knowledge/guidelines/quality-assurance/accessibility/react-typescript-patterns.md) -- [Accessibility Framework](.pair/knowledge/guidelines/quality-assurance/accessibility/README.md) - [ShadCN UI Accessibility Integration Guide](.pair/knowledge/guidelines/quality-assurance/accessibility/shadcn-ui-integration.md) - [Accessibility Testing Tools Framework](.pair/knowledge/guidelines/quality-assurance/accessibility/testing-tools.md) - [Accessibility Training Materials Framework](.pair/knowledge/guidelines/quality-assurance/accessibility/training-materials.md) @@ -391,6 +399,7 @@ - [Delivery Metrics](.pair/knowledge/guidelines/quality-assurance/delivery-metrics.md) - [Manual Testing Guidelines](.pair/knowledge/guidelines/quality-assurance/manual-testing.md) - [Manual Verification Framework](.pair/knowledge/guidelines/quality-assurance/manual-verification.md) +- [Performance Optimization Framework](.pair/knowledge/guidelines/quality-assurance/performance/README.md) - [Performance Benchmarking Framework](.pair/knowledge/guidelines/quality-assurance/performance/benchmarking.md) - [Cumulative Layout Shift (CLS) Optimization](.pair/knowledge/guidelines/quality-assurance/performance/cls.md) - [Performance Continuous Improvement Framework](.pair/knowledge/guidelines/quality-assurance/performance/continuous-improvement.md) @@ -407,24 +416,23 @@ - [Performance-First Development Framework](.pair/knowledge/guidelines/quality-assurance/performance/performance-first-development.md) - [⚡ Performance Fundamentals](.pair/knowledge/guidelines/quality-assurance/performance/performance-fundamentals.md) - [Performance Tools and Measurement](.pair/knowledge/guidelines/quality-assurance/performance/performance-tools.md) -- [Performance Optimization Framework](.pair/knowledge/guidelines/quality-assurance/performance/README.md) - [Performance Targets and Benchmarks Framework](.pair/knowledge/guidelines/quality-assurance/performance/targets-benchmarks.md) - [Performance Testing Strategies](.pair/knowledge/guidelines/quality-assurance/performance/testing-strategies.md) - [User-Centric Performance Framework](.pair/knowledge/guidelines/quality-assurance/performance/user-centric-performance.md) - [Quality Model](.pair/knowledge/guidelines/quality-assurance/quality-model.md) +- [Quality Monitoring Framework](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/README.md) - [Code Quality Monitoring](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/code-quality.md) - [Observability Requirements](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/observability-requirements.md) - [Performance Gates Implementation](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/performance-gates.md) -- [Quality Monitoring Framework](.pair/knowledge/guidelines/quality-assurance/quality-monitoring/README.md) +- [Quality Standards Framework](.pair/knowledge/guidelines/quality-assurance/quality-standards/README.md) - [Quality Assurance Checklist](.pair/knowledge/guidelines/quality-assurance/quality-standards/checklist.md) - [Code Review Standards](.pair/knowledge/guidelines/quality-assurance/quality-standards/code-review.md) - [Definition of Done](.pair/knowledge/guidelines/quality-assurance/quality-standards/definition-of-done.md) - [Quality Improvement Process# Quality Improvement Process](.pair/knowledge/guidelines/quality-assurance/quality-standards/improvement-process.md) - [Quality Gates Framework](.pair/knowledge/guidelines/quality-assurance/quality-standards/quality-gates.md) -- [Quality Standards Framework](.pair/knowledge/guidelines/quality-assurance/quality-standards/README.md) - [Quality Responsibility Matrix](.pair/knowledge/guidelines/quality-assurance/quality-standards/responsibility-matrix.md) - [Quality Verification Methods](.pair/knowledge/guidelines/quality-assurance/quality-standards/verification-methods.md) -- [Quality Assurance Framework](.pair/knowledge/guidelines/quality-assurance/README.md) +- [Security Framework](.pair/knowledge/guidelines/quality-assurance/security/README.md) - [AI-Enhanced Security Framework](.pair/knowledge/guidelines/quality-assurance/security/ai-enhanced-security.md) - [API Security Implementation](.pair/knowledge/guidelines/quality-assurance/security/api-security.md) - [🔐 Authentication and Authorization](.pair/knowledge/guidelines/quality-assurance/security/authentication-authorization.md) @@ -435,7 +443,6 @@ - [Dependency Security Management](.pair/knowledge/guidelines/quality-assurance/security/dependency-security.md) - [Dependency Security Testing Framework](.pair/knowledge/guidelines/quality-assurance/security/dependency-testing.md) - [Incident Response Framework](.pair/knowledge/guidelines/quality-assurance/security/incident-response.md) -- [Security Framework](.pair/knowledge/guidelines/quality-assurance/security/README.md) - [Risk-Based Security Framework](.pair/knowledge/guidelines/quality-assurance/security/risk-based-security.md) - [SAST Static Testing](.pair/knowledge/guidelines/quality-assurance/security/sast-static-testing.md) - [Secret Scanning — Deterministic CI Layer](.pair/knowledge/guidelines/quality-assurance/security/secret-scanning.md) @@ -452,15 +459,16 @@ - [Vulnerability Assessment](.pair/knowledge/guidelines/quality-assurance/security/vulnerability-assessment.md) - [Vulnerability Prevention Framework](.pair/knowledge/guidelines/quality-assurance/security/vulnerability-prevention.md) - [Web Application Security Framework](.pair/knowledge/guidelines/quality-assurance/security/web-app-security.md) -- [📚 Technical Guidelines Knowledge Base](.pair/knowledge/guidelines/README.md) +- [Technical Standards](.pair/knowledge/guidelines/technical-standards/README.md) +- [AI Development Standards](.pair/knowledge/guidelines/technical-standards/ai-development/README.md) +- [Agent Harness Framework](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/README.md) - [Claude Code](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/claude-code.md) - [opencode](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/opencode.md) - [pi](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/pi.md) -- [Agent Harness Framework](.pair/knowledge/guidelines/technical-standards/ai-development/agent-harness/README.md) - [AI Development Tools](.pair/knowledge/guidelines/technical-standards/ai-development/ai-tools.md) - [AI Development Documentation Standards](.pair/knowledge/guidelines/technical-standards/ai-development/documentation-standards.md) - [Model Context Protocol (MCP) Integration](.pair/knowledge/guidelines/technical-standards/ai-development/mcp-integration.md) -- [AI Development Standards](.pair/knowledge/guidelines/technical-standards/ai-development/README.md) +- [Skill Conventions — Shared KB References](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/README.md) - [Adoption-Informed Generation (decision log + ADR + context map)](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/adoption-informed-generation.md) - [Approval Rounds and the `$approval` Signal](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/approval-rounds.md) - [Graceful Degradation — Standard Bullets](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/graceful-degradation.md) @@ -468,105 +476,103 @@ - [Idempotency Convention](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/idempotency.md) - [Nested Sub-Documents (Progressive Disclosure)](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/nested-sub-documents.md) - [Output Format Shapes](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/output-shapes.md) -- [Skill Conventions — Shared KB References](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/README.md) - [`/pair-capability-record-decision` Invocation Contract](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/record-decision-contract.md) - [Resolution Cascade](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/resolution-cascade.md) - [Story-Local Acceptance-Criterion Markers — Banned](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/story-local-markers.md) - [Template Resolution](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/template-resolution.md) - [To-Issues Triage (Extend vs Create)](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/to-issues-triage.md) - [Way-of-Working / PM-Tool + Code-Host Resolution](.pair/knowledge/guidelines/technical-standards/ai-development/skill-conventions/way-of-working-pm-resolution.md) +- [Coding Standards](.pair/knowledge/guidelines/technical-standards/coding-standards/README.md) - [Error Handling Standards](.pair/knowledge/guidelines/technical-standards/coding-standards/error-handling.md) - [Internationalization and Localization (i18n/l10n)](.pair/knowledge/guidelines/technical-standards/coding-standards/i18n-localization.md) -- [Coding Standards](.pair/knowledge/guidelines/technical-standards/coding-standards/README.md) - [Technical Debt Management](.pair/knowledge/guidelines/technical-standards/coding-standards/technical-debt.md) - [Versioning Standards](.pair/knowledge/guidelines/technical-standards/coding-standards/versioning.md) +- [Deployment Workflow](.pair/knowledge/guidelines/technical-standards/deployment-workflow/README.md) - [Build Standards](.pair/knowledge/guidelines/technical-standards/deployment-workflow/build-standards.md) - [Deployment Automation](.pair/knowledge/guidelines/technical-standards/deployment-workflow/deployment-automation.md) -- [Deployment Workflow](.pair/knowledge/guidelines/technical-standards/deployment-workflow/README.md) - [Release Management](.pair/knowledge/guidelines/technical-standards/deployment-workflow/release-management.md) - [Deployment Strategy](.pair/knowledge/guidelines/technical-standards/deployment-workflow/strategy.md) -- [Development Environment Setup](.pair/knowledge/guidelines/technical-standards/development-tools/environment-setup.md) - [Development Tools Standards](.pair/knowledge/guidelines/technical-standards/development-tools/README.md) +- [Development Environment Setup](.pair/knowledge/guidelines/technical-standards/development-tools/environment-setup.md) - [Recommended Tools](.pair/knowledge/guidelines/technical-standards/development-tools/recommended-tools.md) - [Required Tools](.pair/knowledge/guidelines/technical-standards/development-tools/required-tools.md) - [Tool Configuration](.pair/knowledge/guidelines/technical-standards/development-tools/tool-configuration.md) - [Workflow Tools](.pair/knowledge/guidelines/technical-standards/development-tools/workflow-tools.md) - [Feature Flags](.pair/knowledge/guidelines/technical-standards/feature-flags.md) +- [Git Workflow Standards](.pair/knowledge/guidelines/technical-standards/git-workflow/README.md) - [Git Development Process](.pair/knowledge/guidelines/technical-standards/git-workflow/development-process.md) - [Git Quality Assurance Process](.pair/knowledge/guidelines/technical-standards/git-workflow/quality-assurance.md) -- [Git Workflow Standards](.pair/knowledge/guidelines/technical-standards/git-workflow/README.md) - [Version Control Standards](.pair/knowledge/guidelines/technical-standards/git-workflow/version-control.md) +- [Integration Standards](.pair/knowledge/guidelines/technical-standards/integration-standards/README.md) - [API Design Standards](.pair/knowledge/guidelines/technical-standards/integration-standards/api-design.md) - [Data Management Standards](.pair/knowledge/guidelines/technical-standards/integration-standards/data-management.md) - [External Services Integration](.pair/knowledge/guidelines/technical-standards/integration-standards/external-services.md) - [Integration Patterns](.pair/knowledge/guidelines/technical-standards/integration-standards/integration-patterns.md) -- [Integration Standards](.pair/knowledge/guidelines/technical-standards/integration-standards/README.md) -- [Technical Standards](.pair/knowledge/guidelines/technical-standards/README.md) +- [Technology Stack Standards](.pair/knowledge/guidelines/technical-standards/technology-stack/README.md) - [Technology Stack Conventions](.pair/knowledge/guidelines/technical-standards/technology-stack/conventions.md) - [Framework Selection Guidelines](.pair/knowledge/guidelines/technical-standards/technology-stack/framework-selection.md) -- [Technology Stack Standards](.pair/knowledge/guidelines/technical-standards/technology-stack/README.md) - [Technology Stack Standards](.pair/knowledge/guidelines/technical-standards/technology-stack/stack-standards.md) - [Technical Decisions Framework](.pair/knowledge/guidelines/technical-standards/technology-stack/tech-decisions.md) +- [🧪 Testing](.pair/knowledge/guidelines/testing/README.md) +- [♿ Accessibility Testing](.pair/knowledge/guidelines/testing/accessibility-testing/README.md) - [Automated Accessibility Testing](.pair/knowledge/guidelines/testing/accessibility-testing/automated-a11y.md) - [Manual Accessibility Testing](.pair/knowledge/guidelines/testing/accessibility-testing/manual-a11y.md) -- [♿ Accessibility Testing](.pair/knowledge/guidelines/testing/accessibility-testing/README.md) +- [🎭 End-to-End Testing](.pair/knowledge/guidelines/testing/e2e-testing/README.md) - [Cypress Testing](.pair/knowledge/guidelines/testing/e2e-testing/cypress.md) - [Playwright Testing](.pair/knowledge/guidelines/testing/e2e-testing/playwright.md) -- [🎭 End-to-End Testing](.pair/knowledge/guidelines/testing/e2e-testing/README.md) - [Test Scenarios](.pair/knowledge/guidelines/testing/e2e-testing/test-scenarios.md) +- [🔗 Integration Testing](.pair/knowledge/guidelines/testing/integration-testing/README.md) - [API Testing Strategy and Implementation](.pair/knowledge/guidelines/testing/integration-testing/api-testing.md) - [Database Testing Strategy and Implementation](.pair/knowledge/guidelines/testing/integration-testing/database-testing.md) -- [🔗 Integration Testing](.pair/knowledge/guidelines/testing/integration-testing/README.md) - [Service Integration](.pair/knowledge/guidelines/testing/integration-testing/service-integration.md) +- [⚡ Performance Testing](.pair/knowledge/guidelines/testing/performance-testing/README.md) - [Benchmarking](.pair/knowledge/guidelines/testing/performance-testing/benchmarking.md) - [Load Testing](.pair/knowledge/guidelines/testing/performance-testing/load-testing.md) -- [⚡ Performance Testing](.pair/knowledge/guidelines/testing/performance-testing/README.md) - [Stress Testing](.pair/knowledge/guidelines/testing/performance-testing/stress-testing.md) -- [🧪 Testing](.pair/knowledge/guidelines/testing/README.md) -- [CI Integration](.pair/knowledge/guidelines/testing/test-automation/ci-integration.md) - [🤖 Test Automation](.pair/knowledge/guidelines/testing/test-automation/README.md) +- [CI Integration](.pair/knowledge/guidelines/testing/test-automation/ci-integration.md) - [Test Reporting](.pair/knowledge/guidelines/testing/test-automation/test-reporting.md) +- [🎯 Testing Strategy](.pair/knowledge/guidelines/testing/test-strategy/README.md) - [Behavior Driven Development (BDD)](.pair/knowledge/guidelines/testing/test-strategy/bdd-behavior-driven-development.md) - [Coverage Strategy](.pair/knowledge/guidelines/testing/test-strategy/coverage-strategy.md) -- [🎯 Testing Strategy](.pair/knowledge/guidelines/testing/test-strategy/README.md) - [Test Driven Development (TDD)](.pair/knowledge/guidelines/testing/test-strategy/tdd-test-driven-development.md) - [Test Pyramid](.pair/knowledge/guidelines/testing/test-strategy/test-pyramid.md) - [Testing Philosophy](.pair/knowledge/guidelines/testing/test-strategy/testing-philosophy.md) +- [⚡ Unit Testing](.pair/knowledge/guidelines/testing/unit-testing/README.md) - [Jest Configuration](.pair/knowledge/guidelines/testing/unit-testing/jest-configuration.md) - [Mocking Strategies](.pair/knowledge/guidelines/testing/unit-testing/mocking-strategies.md) -- [⚡ Unit Testing](.pair/knowledge/guidelines/testing/unit-testing/README.md) - [Unit Testing Patterns](.pair/knowledge/guidelines/testing/unit-testing/test-patterns.md) - [Vitest Setup](.pair/knowledge/guidelines/testing/unit-testing/vitest-setup.md) +- [🎨 User Experience Guidelines](.pair/knowledge/guidelines/user-experience/README.md) - [Asset Collection](.pair/knowledge/guidelines/user-experience/asset-collection.md) - [Brand Alignment](.pair/knowledge/guidelines/user-experience/brand-alignment.md) - [CAT Tools (Computer-Assisted Translation)](.pair/knowledge/guidelines/user-experience/cat-tools.md) +- [Content Strategy](.pair/knowledge/guidelines/user-experience/content-strategy/README.md) - [Communication Design](.pair/knowledge/guidelines/user-experience/content-strategy/communication-design.md) - [Content Guidelines](.pair/knowledge/guidelines/user-experience/content-strategy/content-guidelines.md) - [Information Architecture](.pair/knowledge/guidelines/user-experience/content-strategy/information-architecture.md) -- [Content Strategy](.pair/knowledge/guidelines/user-experience/content-strategy/README.md) - [Translation Management](.pair/knowledge/guidelines/user-experience/content-strategy/translation-management.md) +- [Design Principles](.pair/knowledge/guidelines/user-experience/design-principles/README.md) - [Accessibility Integration](.pair/knowledge/guidelines/user-experience/design-principles/accessibility-integration.md) - [Color Contrast](.pair/knowledge/guidelines/user-experience/design-principles/color-contrast.md) - [🎯 Consistency Standards](.pair/knowledge/guidelines/user-experience/design-principles/consistency-standards.md) - [Layout Spacing](.pair/knowledge/guidelines/user-experience/design-principles/layout-spacing.md) -- [Design Principles](.pair/knowledge/guidelines/user-experience/design-principles/README.md) - [Typography](.pair/knowledge/guidelines/user-experience/design-principles/typography.md) - [👥 User-Centered Design](.pair/knowledge/guidelines/user-experience/design-principles/user-centered-design.md) +- [Design Systems](.pair/knowledge/guidelines/user-experience/design-systems/README.md) - [🧩 Component Libraries](.pair/knowledge/guidelines/user-experience/design-systems/component-libraries.md) - [🎨 Design Tokens](.pair/knowledge/guidelines/user-experience/design-systems/design-tokens.md) -- [Design Systems](.pair/knowledge/guidelines/user-experience/design-systems/README.md) - [System Architecture](.pair/knowledge/guidelines/user-experience/design-systems/system-architecture.md) - [Tailwind ShadCN Integration](.pair/knowledge/guidelines/user-experience/design-systems/tailwind-shadcn.md) - [Figma Workflows](.pair/knowledge/guidelines/user-experience/figma-workflows.md) +- [Interface Design](.pair/knowledge/guidelines/user-experience/interface-design/README.md) - [Component Design](.pair/knowledge/guidelines/user-experience/interface-design/component-design.md) - [Interaction Design](.pair/knowledge/guidelines/user-experience/interface-design/interaction-design.md) - [Layout Principles](.pair/knowledge/guidelines/user-experience/interface-design/layout-principles.md) -- [Interface Design](.pair/knowledge/guidelines/user-experience/interface-design/README.md) - [Responsive Principles](.pair/knowledge/guidelines/user-experience/interface-design/responsive-principles.md) - [UI Patterns](.pair/knowledge/guidelines/user-experience/interface-design/ui-patterns.md) - [Visual Standards](.pair/knowledge/guidelines/user-experience/interface-design/visual-standards.md) - [Markdown Templates](.pair/knowledge/guidelines/user-experience/markdown-templates.md) -- [🎨 User Experience Guidelines](.pair/knowledge/guidelines/user-experience/README.md) - [User Research](.pair/knowledge/guidelines/user-experience/user-research/README.md) - [🔬 Research Methods](.pair/knowledge/guidelines/user-experience/user-research/research-methods.md) - [Testing and Validation](.pair/knowledge/guidelines/user-experience/user-research/testing-validation.md) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 205eb2784..ec3d2ace8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -121,7 +121,7 @@ Before committing, always run: pnpm quality-gate ``` -This runs (in order): `ts:check`, `test`, `lint`, `format:check` (prettier + markdownlint, **check mode**), `gate:composition`, `hygiene:check`, `docs:staleness`, `skills:conformance`, `dup:check`. +This runs (in order): `ts:check`, `test`, `lint`, `workflows:test`, `format:check` (prettier + markdownlint, **check mode**), `gate:composition`, `hygiene:check`, `smoke-modes:check`, `docs:staleness`, `skills:conformance`, `llms-index:check`, `dup:check`. A red `llms-index:check` means `.pair/llms.txt` no longer matches its generator: run `pnpm llms-index:regen` and commit the result (the gate prints the missing/extra lines and never writes the file; the regen script writes that one file and refuses when the report says regenerating is the wrong move). The gate never formats. It is the pre-push hook, where the commits already exist: a write-mode formatter would rewrite the working tree without touching what is being pushed, so it only pollutes diff --git a/apps/pair-cli/src/registry/llms-generation.ts b/apps/pair-cli/src/registry/llms-generation.ts index 1db271608..e7b6e5e06 100644 --- a/apps/pair-cli/src/registry/llms-generation.ts +++ b/apps/pair-cli/src/registry/llms-generation.ts @@ -1,14 +1,65 @@ import type { FileSystemService } from '@pair/content-ops' -import { dirname, join } from 'path' +import type { Dirent } from 'fs' +import { dirname, join, posix } from 'path' import type { LogEntry } from '#diagnostics' +/** + * The READ-ONLY slice of a file system the index generator needs. + * + * Declared structurally instead of taking the whole `FileSystemService` because of + * who else runs this generator: the `.pair/llms.txt` drift gate (#416, in + * `@pair/dev-tools`) calls it to compute what the tracked index SHOULD be. A gate + * that could regenerate the file would silently fix the drift it exists to reveal + * (check-only gate, ADL 2026-07-31) — so it hands in an adapter that has no + * `writeFile` to call. The narrowing makes "this gate cannot write" a type fact + * rather than a review promise. + * + * `fileSystemService` satisfies it structurally, so every existing caller is + * unchanged. + */ +export interface LlmsSourceFs { + exists: (path: string) => Promise + readdir: (path: string) => Promise + readFile: (file: string) => Promise +} + interface LlmsEntry { title: string path: string } +/** + * Walk one section directory and return its entries, ordered. + * + * TWO `join`s, ON PURPOSE. `fullPath`/`entryPath` are FILE-SYSTEM paths and keep the + * platform's own separator (`join`); the `path` field is EMITTED into the index, so it + * is built with `posix.join`. Sharing one call conflated the two jobs and was a bug: + * `path.join` is bound to the platform, so a Windows run emitted + * `.pair\knowledge\...` — a link no markdown renderer and no agent resolves, shipped + * into every adopter's `.pair/llms.txt` by `pair install`/`update`. Measured on this + * repo's real index with Node's real `path.win32`: all 562 entries change, and the + * drift gate reports 562 missing + 562 extra with nothing naming the cause. + * + * ORDER IS A FUNCTION OF THE CONTENT, NOT of the runtime — the index is a TRACKED, + * byte-compared artifact (#416's drift gate). `localeCompare` with no locale argument + * uses the runtime's ICU default: measured on this repo's index, 458 of 560 entries sit + * in a different position under ICU collation (ICU puts + * `.pair/adoption/product/context-map.md` before `PRD.md`, this comparator the + * reverse), so on a Node built without full ICU the gate would go red on an untouched + * tree and send the contributor to regenerate environment-dependent churn. + * `<`/`>` compares UTF-16 CODE UNITS — codepoint order for every BMP path, i.e. every + * path a KB has carried; the two diverge only above U+FFFF, where a surrogate pair + * sorts below U+E000-U+FFFF. Both are environment-independent, which is the invariant. + * + * The separator belongs to that same invariant, and not only to the link form: it is a + * SORT KEY. `\` is U+005C, `/` is U+002F, so `a/b.md` sorts before `a5.md` on POSIX + * and after it on Windows. + * + * ADL `2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md`, rules 1 + * and 3. + */ async function scanSection( - fs: FileSystemService, + fs: LlmsSourceFs, baseDir: string, sectionPath: string, ): Promise { @@ -21,20 +72,20 @@ async function scanSection( for (const dirent of dirents) { const entryPath = join(fullPath, dirent.name) if (dirent.isDirectory()) { - const nested = await scanSection(fs, baseDir, join(sectionPath, dirent.name)) + const nested = await scanSection(fs, baseDir, posix.join(sectionPath, dirent.name)) entries.push(...nested) } else if (dirent.name.endsWith('.md') || dirent.name.endsWith('.mdx')) { const content = await fs.readFile(entryPath) const titleMatch = content.match(/^#\s+(.+)$/m) const title = titleMatch?.[1] ?? dirent.name.replace(/\.mdx?$/, '') - entries.push({ title, path: join(sectionPath, dirent.name) }) + entries.push({ title, path: posix.join(sectionPath, dirent.name) }) } } - return entries.sort((a, b) => a.path.localeCompare(b.path)) + return entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) } -export async function generateLlmsTxt(fs: FileSystemService, baseTarget: string): Promise { +export async function generateLlmsTxt(fs: LlmsSourceFs, baseTarget: string): Promise { const pairDir = join(baseTarget, '.pair') const sections: { heading: string; entries: LlmsEntry[] }[] = [] diff --git a/apps/pair-cli/src/registry/llms-generation.win32.test.ts b/apps/pair-cli/src/registry/llms-generation.win32.test.ts new file mode 100644 index 000000000..9dddf7a46 --- /dev/null +++ b/apps/pair-cli/src/registry/llms-generation.win32.test.ts @@ -0,0 +1,170 @@ +/** + * The THIRD environment axis of the same invariant `.gitattributes` (line terminator) + * and the code-unit comparator (collation) already close: `.pair/llms.txt` must be + * byte-reproducible on any machine that checks it out, and Windows is not excluded as + * a development platform (ADL + * `2026-09-01-a-byte-compared-generated-artifact-sorts-by-codepoint.md`). + * + * `path.join` is PLATFORM-BOUND. Building an entry path with it emitted + * `.pair/knowledge/...` on POSIX and `.pair\knowledge\...` on Windows — a link form no + * markdown renderer and no agent resolves, shipped into every adopter's `.pair/llms.txt` + * by `pair-cli install`/`update`, and undetectable by the drift gate, which reports it + * as 562 missing + 562 extra lines closing with the bare "regenerate and commit". + * + * WHY THIS FILE MOCKS `path`, and why that is not faking the boundary: Node's own + * `lib/path.js` ends with `module.exports = isWindows ? win32 : posix`, so on Windows + * `require('path')` IS `path.win32` — the object substituted below is Node's REAL win32 + * implementation, taken from the REAL `path` module of the running Node, not a + * hand-written stand-in. Running the generator against it is the only way a POSIX CI can + * execute the Windows row at all: on POSIX, `join` and `posix.join` are the same + * function, so no fixture can tell the fixed code from the broken code. + * + * The in-memory file system normalizes `\` to `/` before its lookup because the Win32 + * file APIs accept both separators interchangeably — a Windows machine finds + * `C:\repo\.pair\adoption` and `C:\repo/.pair/adoption` alike. Only the EMITTED ENTRY + * STRINGS are under test. + */ +import type { Dirent } from 'fs' +import * as pathModule from 'path' +import { describe, expect, it, vi } from 'vitest' + +vi.mock('path', async () => { + const actual = await vi.importActual('path') + return { ...actual.win32, win32: actual.win32, posix: actual.posix, default: actual.win32 } +}) + +import { generateLlmsTxt, type LlmsSourceFs } from './llms-generation' + +/** + * A read-only in-memory tree keyed by POSIX paths, tolerant of either separator on the + * way in — the tolerance the Win32 file APIs themselves have. + */ +function createFs(files: Record): LlmsSourceFs { + const normalize = (p: string) => p.replace(/\\/g, '/').replace(/\/+$/, '') + const paths = Object.keys(files).map(normalize) + + const childrenOf = (dir: string): { name: string; directory: boolean }[] => { + const prefix = `${dir}/` + const seen = new Map() + for (const p of paths) { + if (!p.startsWith(prefix)) continue + const rest = p.slice(prefix.length) + const slash = rest.indexOf('/') + const name = slash === -1 ? rest : rest.slice(0, slash) + seen.set(name, slash !== -1 || (seen.get(name) ?? false)) + } + return [...seen].map(([name, directory]) => ({ name, directory })) + } + + return { + exists: async path => { + const target = normalize(path) + return paths.includes(target) || paths.some(p => p.startsWith(`${target}/`)) + }, + readdir: async path => + childrenOf(normalize(path)).map( + entry => ({ name: entry.name, isDirectory: () => entry.directory }) as unknown as Dirent, + ), + readFile: async file => { + const content = files[normalize(file)] + if (content === undefined) throw new Error(`ENOENT: ${file}`) + return content + }, + } +} + +const TREE: Record = { + '/project/.pair/adoption/product/PRD.md': '# Product Requirements Document\n', + '/project/.pair/adoption/decision-log/2026-09-01-a-decision.md': '# A Decision\n', + '/project/.pair/knowledge/how-to/01-create-PRD.md': '# How to Create a PRD\n', + '/project/.pair/knowledge/guidelines/testing/README.md': '# Testing Guidelines\n', + // The separator-sensitive pair: `a/b.md` vs `a5.md` differ first at the separator, + // and `/` (0x2F) sorts BELOW `5` (0x35) while `\` (0x5C) sorts ABOVE it. + '/project/.pair/knowledge/guidelines/a/b.md': '# B\n', + '/project/.pair/knowledge/guidelines/a5.md': '# A5\n', + '/project/.pair/knowledge/guidelines/collaboration/templates/pr-template.md': '# PR Template\n', + '/project/.pair/knowledge/skills-guide.md': '# Skills Guide\n', +} + +/** + * The exact document the generator must emit for `TREE` — separators included. Written + * as a literal rather than derived from `posix.join`, so the assertion cannot be + * satisfied by the same platform-bound call it is meant to catch. + */ +const EXPECTED = [ + '# pair', + '', + '> AI-assisted development knowledge base for this project.', + '', + '## Adoption — Product', + '', + '- [Product Requirements Document](.pair/adoption/product/PRD.md)', + '', + '## Adoption — Decisions', + '', + '- [A Decision](.pair/adoption/decision-log/2026-09-01-a-decision.md)', + '', + '## How-To Guides', + '', + '- [How to Create a PRD](.pair/knowledge/how-to/01-create-PRD.md)', + '', + '## Guidelines', + '', + '- [B](.pair/knowledge/guidelines/a/b.md)', + '- [A5](.pair/knowledge/guidelines/a5.md)', + '- [PR Template](.pair/knowledge/guidelines/collaboration/templates/pr-template.md)', + '- [Testing Guidelines](.pair/knowledge/guidelines/testing/README.md)', + '', + '## Skills', + '', + '- [Skills Guide](.pair/knowledge/skills-guide.md)', + '', +].join('\n') + +describe('generateLlmsTxt on Windows (`path` bound to Node’s real win32 implementation)', () => { + it('is a smoke check that the win32 flavour is really in force', () => { + // Guards the whole file: if the mock stopped applying, every assertion below would + // pass on POSIX for the wrong reason. + expect(pathModule.win32.join('.pair/knowledge/guidelines', 'nested', 'a.md')).toBe( + '.pair\\knowledge\\guidelines\\nested\\a.md', + ) + }) + + it('emits the SAME bytes a POSIX machine emits for the same tree', async () => { + const result = await generateLlmsTxt(createFs(TREE), '/project') + + expect(result).toBe(EXPECTED) + }) + + it('emits no backslash in any entry path, at any nesting depth', async () => { + const result = await generateLlmsTxt(createFs(TREE), '/project') + + const entryPaths = [...result.matchAll(/^- \[[^\]]*\]\(([^)]*)\)$/gm)].map(m => m[1] ?? '') + expect(entryPaths.length).toBe(8) + expect(entryPaths.filter(p => p.includes('\\'))).toEqual([]) + }) + + it('still finds the sections: the file-system access keeps the platform separator', async () => { + // The fix is scoped to the EMITTED path. Reading the tree must keep using the + // platform's own `join`, or a real Windows machine would index nothing and the + // gate would report a broken setup instead of drift. + const result = await generateLlmsTxt(createFs(TREE), '/project') + + expect(result).toContain('## Guidelines') + expect(result).toContain('## Skills') + }) + + it('ORDERS by the POSIX path: the separator is a sort key, not only a link', async () => { + // Not a second spelling of the assertion above. The comparator is code-unit order + // over the emitted path, so the separator's own code point decides sibling order: + // `a/b.md` vs `a5.md` differ at `/` (0x2F) vs `5` (0x35) — `a/b.md` first — while + // `a\b.md` vs `a5.md` differ at `\` (0x5C) vs `5` — `a5.md` first. A Windows-built + // index would therefore also REORDER entries, not merely spell them differently, + // which is the determinism the ADL's code-unit comparator exists to buy. + const result = await generateLlmsTxt(createFs(TREE), '/project') + + const guidelines = result.slice(result.indexOf('## Guidelines'), result.indexOf('## Skills')) + const titles = [...guidelines.matchAll(/^- \[([^\]]*)\]/gm)].map(m => m[1]) + expect(titles).toEqual(['B', 'A5', 'PR Template', 'Testing Guidelines']) + }) +}) diff --git a/apps/pair-cli/src/registry/llms-index-conformance.test.ts b/apps/pair-cli/src/registry/llms-index-conformance.test.ts index 6f408cf21..e88264ffe 100644 --- a/apps/pair-cli/src/registry/llms-index-conformance.test.ts +++ b/apps/pair-cli/src/registry/llms-index-conformance.test.ts @@ -1,49 +1,41 @@ import { describe, it, expect } from 'vitest' -import { readFileSync } from 'fs' import { join } from 'path' import { fileSystemService } from '@pair/content-ops' import { generateLlmsTxt } from './llms-generation' -// `.pair/llms.txt` is GENERATED by `pair install` / `pair update` (generateLlmsTxt -// below), but it is also CHECKED IN — this repo dogfoods its own KB, so the index -// every agent reads is the committed file, not a build artifact. Nothing pinned the -// two together: `llms-generation.test.ts` runs the generator over in-memory -// fixtures, so a KB file added by hand (or one the generator would list and nobody -// added) drifts the committed index silently. That had already happened — the -// review of #216 found `story-local-markers.md` present in the KB tree and absent -// from the index on both the branch and `main`, invisible to every agent reading it. +// What this file guards, since #416: the generator's OUTPUT SHAPE against the real +// repo tree — the sections an adopter's index must carry. It no longer asserts the +// committed `.pair/llms.txt` equals that output. // -// This guard is the missing equality: run the REAL generator over the REAL repo -// tree and compare byte-for-byte with the committed file. It re-uses the production -// `generateLlmsTxt` (no parallel implementation), so a generator change and a KB -// change both surface here. +// That byte-for-byte equality moved, whole, to the named gate +// `packages/dev-tools/src/quality-gates/llms-txt-drift-check.ts` (`pnpm +// llms-index:check`, wired into `ci.yml` and the root `quality-gate`). It landed +// here first, in #216/PR #443, as the fastest way to pin a real miss +// (`story-local-markers.md`, in the KB tree and absent from the index on both the +// branch and `main`); #416 owns the coverage properly and the story says REPLACE +// this, do not duplicate it. Two byte-equality guards over one file is the "two +// definitions that drift apart" smell the story exists to remove — and the vitest +// experience here was the one AC2 rejects by name: a raw string diff over a +// 400-line file, with no missing/extra list and no regeneration command. // -// OVERLAPS #416 ("tech-debt: no gate covers .pair/llms.txt drift"), which owns this -// coverage properly. What lands here is #416's AC1 only — the byte-for-byte -// comparison — reached through `turbo test` (root `quality-gate` + ci.yml). What -// #416 still owns: an actionable failure message (the missing/extra lines, not a -// raw vitest diff over a 400-line file), naming the command that regenerates the -// index, fixture-tree + determinism tests, and MOVING the check to -// `packages/dev-tools/src/quality-gates` so gate composition stays uniform. Do not -// duplicate this file there — replace it. +// These cases stay because they are NOT drift assertions. They say what the +// generator must index for any tree of this layout, so a regression in +// `sectionDefs` fails loudly instead of being frozen as "conformant" by whatever +// bytes happen to be committed — the exact trap the byte-equality guard set when it +// pinned an output that carried no Adoption sections at all. const REPO_ROOT = join(__dirname, '../../../..') -describe('.pair/llms.txt — the committed index equals the generator over the repo tree', () => { - it('matches `generateLlmsTxt` byte-for-byte', async () => { - const generated = await generateLlmsTxt(fileSystemService, REPO_ROOT) - const committed = readFileSync(join(REPO_ROOT, '.pair/llms.txt'), 'utf-8') - - expect(committed).toBe(generated) - }) - +describe('generateLlmsTxt over the real repo tree — the sections the index must carry', () => { // The generator scanned `.pair/product/adopted` / `.pair/tech/adopted` — // directories no shipped dataset ever created — so the index every agent reads // to find project context carried NO Adoption sections at all: an adopting // project ran `pair install` and got an llms.txt missing its own PRD, // architecture and tech-stack, the highest-value entries in the file, with - // nothing reporting it. Byte equality above pins whatever the generator emits, - // so without this case the wrong output would be asserted correct. + // nothing reporting it. The byte-equality guard in + // `packages/dev-tools/src/quality-gates/llms-txt-drift-check.ts` pins whatever + // the generator emits, so without this case the wrong output would be asserted + // correct. it('indexes the project adoption files from the real layout', async () => { const generated = await generateLlmsTxt(fileSystemService, REPO_ROOT) diff --git a/apps/website/content/docs/contributing/development-setup.mdx b/apps/website/content/docs/contributing/development-setup.mdx index df8bb5ea3..46ffea77e 100644 --- a/apps/website/content/docs/contributing/development-setup.mdx +++ b/apps/website/content/docs/contributing/development-setup.mdx @@ -74,7 +74,7 @@ Before committing, always run: pnpm quality-gate ``` -This runs (in order): `ts:check`, `test`, `lint`, `format:check` (prettier + markdownlint, **check mode**), `gate:composition`, `hygiene:check`, `docs:staleness`, `skills:conformance`, `dup:check`. +This runs (in order): `ts:check`, `test`, `lint`, `workflows:test`, `format:check` (prettier + markdownlint, **check mode**), `gate:composition`, `hygiene:check`, `smoke-modes:check`, `docs:staleness`, `skills:conformance`, `llms-index:check`, `dup:check`. A red `llms-index:check` means `.pair/llms.txt` no longer matches its generator: run `pnpm llms-index:regen` and commit the result (the gate prints the missing/extra lines and never writes the file; the regen script writes that one file and refuses when the report says regenerating is the wrong move). The gate never formats. It is the pre-push hook, where the commits already exist: a write-mode formatter would rewrite the working tree without touching what is being pushed, so it only pollutes diff --git a/package.json b/package.json index ddf80d3f8..c9a43623f 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,10 @@ "sync-version": "pnpm --filter @pair/dev-tools sync-version", "docs:staleness": "pnpm --filter @pair/website docs:staleness", "skills:conformance": "pnpm --filter @pair/knowledge-hub skills:conformance", + "llms-index:check": "pnpm --filter @pair/dev-tools llms-index:check", + "llms-index:regen": "pnpm --filter @pair/dev-tools llms-index:regen", "dup:check": "jscpd apps packages", - "quality-gate": "turbo ts:check test lint && pnpm workflows:test && pnpm format:check && pnpm gate:composition && pnpm hygiene:check && pnpm smoke-modes:check && pnpm docs:staleness && pnpm skills:conformance && pnpm dup:check", + "quality-gate": "turbo ts:check test lint && pnpm workflows:test && pnpm format:check && pnpm gate:composition && pnpm hygiene:check && pnpm smoke-modes:check && pnpm docs:staleness && pnpm skills:conformance && pnpm llms-index:check && pnpm dup:check", "e2e": "pnpm --filter @pair/website e2e", "smoke-tests": "./scripts/smoke-tests/run-all.sh --cleanup", "format": "pnpm prettier:fix && pnpm mdlint:fix", diff --git a/packages/dev-tools/README.md b/packages/dev-tools/README.md index 3e3e76e07..a0fb59651 100644 --- a/packages/dev-tools/README.md +++ b/packages/dev-tools/README.md @@ -4,18 +4,22 @@ Pair's own automation scripts for development and deployment — gate/tooling sc ## Tools -| Script | Module | Purpose | -| ------------------------ | ------------------------------------------- | ------------------------------------------------------------------------ | -| `code-hygiene:check` | `src/quality-gates/code-hygiene-check.ts` | Fails if suppression markers (`@ts-ignore`, `eslint-disable`, `.skip`) are committed | -| `sync-version` | `src/quality-gates/sync-version-in-docs.ts` | Detects/rewrites hardcoded CLI version strings across `.md`/`.mdx` docs | -| `benchmark-update-link` | `src/quality-gates/benchmark-update-link.ts` | Perf gate for the CLI's `update-link` command — thresholds: <30,000ms (large KB), >100 links/sec (every size) | -| `determine-version` | `src/release/determine-version.ts` | Resolves the release version from `--input-version` > `--release-tag` > `--github-ref` tag pattern, writes GITHUB_OUTPUT/GITHUB_ENV | - -The first three are runnable via the repo-root scripts (`pnpm hygiene:check`, `pnpm sync-version -- `, `pnpm test:perf`); `determine-version` is invoked directly by `.github/workflows/release.yml`'s "Determine version" step. All four delegate here (`pnpm --filter @pair/dev-tools