Skip to content

feat(rule-inventory): fail a PR whose deleted skill prose maps to no rule-inventory row (FAFF-1428) - #1182

Merged
alechill merged 13 commits into
mainfrom
FAFF-1428-rule-inventory-check
Nov 5, 2026
Merged

alechill merged 13 commits into
mainfrom
FAFF-1428-rule-inventory-check

Conversation

@alechill

@alechill alechill commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

faff rule-inventory check [--base REF] [--json] [--summary FILE] fails a PR whose deleted skill prose is neither moved nor accounted for. Every deleted block of skill prose must do one of two things:

  • reappear word for word in other skill prose (moved); or
  • be quoted by a ## Rule inventory row in the PR's committed spec that says what happened to it:
    • kept, naming a covering test that exists;
    • changed or dropped, with a reason.

Anything else is uncovered, and the check exits 1.

  • What it proves. Coverage is mechanical: every deleted block has a row, and every test a row names exists at HEAD. Whether the row's claim is true stays with review and the holdout. The check never claims a rule was implemented.
  • Exits.
    • 0 for pass or skipped;
    • 1 for uncovered blocks or row errors, listing every one, not just the first;
    • 2 for usage errors, any failing git call, or an unreadable spec or config. A git error is never a pass.
  • When it skips. It skips with no-base (local only, with no origin/HEAD), no-prose-deleted, or no-spec-committed (with a notice). A PR that touches .faff/anchors/ but commits no spec fails with footprint_without_spec.
  • CI. A new validate step, "Rule inventory covers deleted skill prose", sits right after the region step. It uses the same env-only base resolution and appends the covered rows to the job summary, dropped and changed first.
  • Factory region, no Commissaire surface. It reads only whether the diff adds or modifies a path under .faff/anchors/, never an anchor's content.

Mapping and exemption rules

  • Blocks.
    • Prose is split into blocks: paragraphs, headings, table rows, list items, and a fenced region as one block.
    • A block with any deleted line is a deleted block.
    • The diff runs with --text, --no-renames, --literal-pathspecs and no textconv or external diff, so a binary or -diff attribute can't hide a deletion.
  • Moved. The normalised deleted text is a substring of a single block of watched skill prose at HEAD.
  • Quoted.
    • A row's code-span anchor covers a block when it is at least 20 characters and a substring of that block's deleted text, or when it equals the block's whole deleted text.
    • Rows from every committed spec are pooled.
    • A malformed table or an invalid row is a row error, never "no inventory".
  • Heading and header-row exemption. It applies only to a group that is wholly and purely deleted (nothing added in its place), that holds at least one block with letters, and none of whose text reappears at HEAD.
  • Mode-only changes. A mode-only change, or deleting an empty file, deletes nothing. Any other content change with no text hunk exits 2.
  • Covering tests.
    • The path must match ^test/[A-Za-z0-9._/-]+\.test\.mjs$.
    • It is read through ls-tree -z and must be a blob.
    • A named test must appear in the file as written.
  • Config. tracking.spec_docs_path is normalised to a repo-relative directory; an absolute path, or one that leaves the repo, exits 2.

Depends on FAFF‑1463: config-location guard removed

This branch is rebased onto FAFF‑1463 (#1181). The shared config lookup's main-checkout probe (git -C <root> rev-parse --git-common-dir) now throws MainWorktreeProbeError on any failure inside a repo, so:

  • rule-inventory passes that error through to the dispatch arm. A failed probe exits 2 with one line: faff rule-inventory: cannot proceed — git could not resolve the main checkout for <root>: <git message>.
  • The two calls the every-call test had listed as pending the operator's decision are now ordinary assertions. Every failing git call, with and without --base, must exit 2 with empty stdout, and both probe calls must print exactly that line. The sweep collects every mismatch and asserts once.
  • verifyConfigLocation is removed. This is an operator decision. It re-ran the probe to catch a silently lost main-checkout fallback, which can no longer happen. Mutation evidence on the 45-case suite:
Guard Shared lookup Failing tests
removed FAFF‑1463 intact 0 (45/45 pass)
kept reverted to the null-on-failure wrapper 2
removed reverted to the null-on-failure wrapper 3, including the overlay fallback test

Every failure the guard caught is now caught through FAFF‑1463, and a regression there fails here as well as in FAFF‑1463's own tests. Removing either probe pass-through also makes tests fail. One mutation run that broke two different git calls reported both.

Other changes

  • Prose baseline: faffter-dark-nlspec/SKILL.md goes from 18443 to 18680 (+237) for the new pointer to the inventory format.
  • Docs: docs/reference/skill-authoring.md#rule-inventory documents the table format, and docs/guide/cli.md gains the command.
  • Registration: bin/faff, regions.js (factory region, no selftest), lint-cli-coverage.js, tsconfig.json, gates-report.js, the build manifest, and the count pins in test/check-no-boundary-cast.test.mjs and test/gates-ci-source.test.mjs.
  • Source: rule-inventory.ts with its committed emit, and 45 cases in test/rule-inventory.test.mjs.

Verification

  • Code-blind holdout: meets-spec on 21/21 running-stack criteria at head 0f7ea2a5. It ran in a network-isolated git 2.54 container and covered both the absent and the present origin/HEAD cases. This is the sixth run; earlier runs were invalidated by head changes.
  • Independent review: pass after 5 rounds, 22 findings:
    • 14 fixed;
    • 6 refuted;
    • 1 accepted risk (a main-checkout probe failure is fatal by FAFF‑1463's design);
    • 1 open observation (a redundant re-fetch in one test), left as is so the head stays fixed.
  • Fixture and git versions: git 2.48 and later create origin/HEAD on fetch. The fixtures now remove it with update-ref --no-deref -d, so the no-base cases behave the same on any git version. On git 2.54 the previous head passed 43/45 and this head passes 45/45; on 2.39 both pass 45/45.
  • Gate ladder: all 10 steps pass, including rule-inventory check on this branch and the full unit run. The quality-gates contract exits 0.
  • Anchor: the final anchor commit was minted by hand, on operator instruction. faff pr open short-circuited on the anchor directory already in the tree from the first open and did not re-anchor the new head. Follow-up: FAFF‑1470. The recovery bundle from the first open stands, because bundle refs are write-once.

Follow-ups

  • FAFF‑1458: the "no spec" skip must fail once governance.yml is set to on-missing: fail.
  • FAFF‑1459: a rewording exemption.
  • FAFF‑1460: gates discover counts a command inside echo.
  • FAFF‑1461: whether --if-eligible hypotheticals should write capture records.
  • FAFF‑1470: faff pr open treats an existing anchor directory as current, so a head pushed after the PR opens keeps a stale anchor and body.
  • FAFF‑1471: git-version drift in test fixtures, including the redundant re-fetch at test/rule-inventory.test.mjs:442.

Acceptance Criteria

  • faff-1428#done:1 A PR that deletes a non-exempt block of skill prose, commits a spec, and has no row or move covering the block fails faff rule-inventory check with exit 1, naming the file and base line.
    Verified: exit 1, uncovered names plugin/skills/faff-graft/SKILL.md:5
    Method: test test/rule-inventory.test.mjs::a deleted paragraph with a committed spec and no inventory section fails, naming the file and the missing section
  • faff-1428#done:2 faff rule-inventory check [--base REF] [--json] [--summary FILE] exists, with exits 0 / 1 / 2 as section 3 defines.
    Verified: exits 0/1/2 asserted; self-run on this branch exits 0 (skipped no-prose-deleted)
    Method: test test/rule-inventory.test.mjs::a deleted paragraph with a committed spec and no inventory section fails, naming the file and the missing section
    Method: test test/rule-inventory.test.mjs::a --base that is not a plain ref exits 2 and prints nothing on stdout
    Method: test test/rule-inventory.test.mjs::a failing git command exits 2, never 0
    Method: command node plugin/skills/faff/bin/faff rule-inventory check --base origin/main --json
  • faff-1428#done:3 --json output has every CheckResult field.
    Verified: every CheckResult key asserted present
    Method: test test/rule-inventory.test.mjs::a deleted paragraph with a committed spec and no inventory section fails, naming the file and the missing section
  • faff-1428#done:4 The inventory format (section heading, Prose rule code-span anchor, Covering test, Disposition) is documented in docs/reference/skill-authoring.md under a Rule inventory heading, and every uncovered-block message names that anchor.
    Verified: section at docs/reference/skill-authoring.md:37; uncovered line names #rule-inventory
    Method: test test/rule-inventory.test.mjs::a deleted paragraph with a committed spec and no inventory section fails, naming the file and the missing section
    Method: command grep -n '^## Rule inventory' docs/reference/skill-authoring.md
  • faff-1428#done:5 plugin/skills/faffter-dark-nlspec/SKILL.md gains one line telling the producer that a spec deleting skill prose carries a ## Rule inventory in that format, and prose-baseline.json is raised by exactly that file's growth.
    Verified: one Rule inventory line added; prose-baseline raised 18443 -> 18680 (+237, the file's exact growth); validate-adapters PASS
    Method: command node plugin/skills/faff/bin/faff validate-adapters
  • faff-1428#done:6 The command is TypeScript (rule-inventory.ts with its committed .js emit and build-manifest.json), in the factory region, with REGION_SELFTEST_ARGV["rule-inventory"] = null, a TEST_FILE_COVERAGE entry for test/rule-inventory.test.mjs, and a docs/guide/cli.md row; faff regions check, faff lint-cli-coverage and faff lint-cli-doc pass.
    Verified: rule-inventory.ts, committed .js emit and build-manifest; factory region, selftest null, TEST_FILE_COVERAGE, cli.md row; all three lints PASS
    Method: command node plugin/skills/faff/bin/faff regions check
    Method: command node plugin/skills/faff/bin/faff lint-cli-coverage
    Method: command node plugin/skills/faff/bin/faff lint-cli-doc
    Method: test test/rule-inventory.test.mjs::the CLI registration: a factory command with no selftest
  • faff-1428#done:7 A row's anchor covers a block only when it is at least 20 characters and a substring of the block's normalised deleted text, or when it equals that whole text; a short anchor never covers a longer block containing it.
    Verified: short anchor covers only equal text; the floor mutation flips the case
    Method: test test/rule-inventory.test.mjs::a short anchor covers only a block whose whole text it equals, never a longer block containing it
    Method: test test/rule-inventory.test.mjs::row errors: no code span, short anchor, bad disposition, empty reason, bad or missing test path, absent test name
    Method: test test/rule-inventory.test.mjs::mutations: dropping the anchor-length floor, the kept-needs-test rule or the moved test each fail a case
  • faff-1428#done:8 A deleted block whose text sits inside a single head block of any watched file counts as moved, with no row needed, including a reflow that leaves some lines unchanged.
    Verified: cross-file move and partial reflow counted moved; the moved-test mutation flips
    Method: test test/rule-inventory.test.mjs::text moved verbatim into another skill file, or reflowed in place, is moved and needs no row
    Method: test test/rule-inventory.test.mjs::mutations: dropping the anchor-length floor, the kept-needs-test rule or the moved test each fail a case
  • faff-1428#done:9 The watched set is prose-size's exported isCovered, imported, not a second list.
    Verified: isCovered imported from ./prose-size; no second list
    Method: command grep -n 'isCovered' plugin/skills/faff/bin/lib/rule-inventory.ts
  • faff-1428#done:10 Path listing uses -z with core.quotePath=false; a watched-looking path the parser cannot decode exits 2.
    Verified: diff -z --name-status under core.quotePath=false; an undecodable plugin/skills/ path throws (exit 2); per-file diff uses --text and --literal-pathspecs, a hunkless or Binary diff exits 2; a mode-only change or empty-base deletion deletes nothing, any other hunkless content change exits 2
    Method: test test/rule-inventory.test.mjs::a changed path under plugin/skills/ that is not valid UTF‑8 is an error, never a skip
    Method: command grep -n 'core.quotePath=false' plugin/skills/faff/bin/lib/rule-inventory.ts
    Method: test test/rule-inventory.test.mjs::a binary or -diff attribute, or a NUL byte at HEAD, never hides a deletion (review: binary diff)
    Method: test test/rule-inventory.test.mjs::a watched file whose diff has no text hunk is an error, never a skip (review: binary diff)
    Method: test test/rule-inventory.test.mjs::a watched path with glob characters is matched literally (review: literal pathspecs)
    Method: test test/rule-inventory.test.mjs::a mode-only change or an empty watched file's deletion deletes nothing; a content change with no hunk still exits 2 (review round 2: no-hunk guard)
  • faff-1428#done:11 A move into a non-watched file is not moved.
    Verified: a move into docs/guide/prep.md is uncovered
    Method: test test/rule-inventory.test.mjs::text moved into a non-watched file is not moved
  • faff-1428#done:12 Table separator rows and letterless blocks are exempt; a table header row is exempt only when its whole table is deleted, and a heading only when its whole section is deleted.
    Verified: separator and letterless exempt; header exempt only with its whole table; heading only with its whole section; exemption needs every line deleted by pure-deletion hunks, a lettered block, and no reappearance; an empty section is never exempt; a heading deleted alone over an edited paragraph is not exempt
    Method: test test/rule-inventory.test.mjs::a deleted thematic break alone is letterless and exempt
    Method: test test/rule-inventory.test.mjs::exemptions: separator rows are exempt; a header row is exempt only with its whole table
    Method: test test/rule-inventory.test.mjs::a whole section deleted counts only its paragraph: the heading is exempt
    Method: test test/rule-inventory.test.mjs::a reworded heading alone is not exempt: it needs a row
    Method: test test/rule-inventory.test.mjs::a reworded heading over a partly edited section is not exempt (review: section exemption)
    Method: test test/rule-inventory.test.mjs::a renamed table header is not exempt when the table's rows reappear (review: table exemption)
    Method: test test/rule-inventory.test.mjs::a section moved to another skill under a new heading is not exempt: its old heading needs a row (review: section exemption)
    Method: test test/rule-inventory.test.mjs::a heading with an empty section is not exempt when deleted (review: empty section)
    Method: test test/rule-inventory.test.mjs::a heading deleted alone while its one-line paragraph is edited is not exempt (review round 2: pure members)
  • faff-1428#done:13 Normalise deletes * and backticks and collapses whitespace, on anchors, deleted text and head blocks alike.
    Verified: normalise strips * and backticks and collapses whitespace
    Method: test test/rule-inventory.test.mjs::normalise, clean and mdEscape
    Method: test test/rule-inventory.test.mjs::a qualified header (`Prose rule (§2)`) is read; a kept row in a table with no Covering test column is a row error
  • faff-1428#done:14 Only specs the diff adds are read; a modified older spec contributes no rows.
    Verified: a modified older spec contributes no rows; ./records/specs/ normalised, absolute path exits 2
    Method: test test/rule-inventory.test.mjs::only specs the diff adds are read: an older spec the branch modifies contributes no rows
    Method: test test/rule-inventory.test.mjs::spec_docs_path is normalised: ./records/specs/ finds the spec; an absolute path exits 2 (review: spec_docs_path)
  • faff-1428#done:15 A diff that adds or modifies a path under .faff/anchors/ but commits no spec fails with footprint_without_spec: true.
    Verified: added and modified anchor paths both fail with footprint_without_spec
    Method: test test/rule-inventory.test.mjs::an added or modified .faff/anchors/ path with deleted prose and no spec fails with footprint_without_spec
  • faff-1428#done:16 --summary escapes markdown metacharacters and HTML in every spec-derived value.
    Verified: mdEscape escapes metacharacters and HTML; no unescaped ![ or pipe
    Method: test test/rule-inventory.test.mjs::normalise, clean and mdEscape
  • faff-1428#done:17 A covering-test path outside ^test/…\.test\.mjs$ or with a .. segment is a row error and is never looked up.
    Verified: src/a.js and test/../escape.test.mjs are row errors and never looked up
    Method: test test/rule-inventory.test.mjs::row errors: no code span, short anchor, bad disposition, empty reason, bad or missing test path, absent test name
  • faff-1428#done:18 Every printed line starts with rule-inventory: ; spec-derived values are stripped of control characters and capped at 200 characters; a committed spec over 1 MiB exits 2.
    Verified: every stdout line prefixed; ::error:: never starts a line; clean() strips controls and caps at 200; a spec over 1 MiB exits 2
    Method: test test/rule-inventory.test.mjs::a spec-derived workflow command never starts an output line
    Method: test test/rule-inventory.test.mjs::a committed spec over 1 MiB exits 2
    Method: test test/rule-inventory.test.mjs::normalise, clean and mdEscape
  • faff-1428#done:19 --summary FILE appends the row-covered table, dropped and changed first, and the CI step passes $GITHUB_STEP_SUMMARY.
    Verified: summary appends, dropped first; the CI step passes $GITHUB_STEP_SUMMARY
    Method: test test/rule-inventory.test.mjs::--summary appends the row-covered table with dropped rows first
    Method: command grep -n 'GITHUB_STEP_SUMMARY' .github/workflows/validate.yml
  • faff-1428#done:20 A row whose anchor matches no deleted block produces the warning anchor matches no deleted prose and does not fail the check.
    Verified: warning emitted, exit 0
    Method: test test/rule-inventory.test.mjs::a row whose anchor matches no deleted prose warns and does not fail
  • faff-1428#done:21 kept rows with no test, changed/dropped rows with no reason, and rows naming a missing test file or a test name absent from the file are row errors.
    Verified: kept with no test, empty reason, missing file and absent name are all row errors; a directory is not a covering test (cat-file -t blob); covering-test lookup is ls-tree through the failure guard and must be a blob
    Method: test test/rule-inventory.test.mjs::a kept row with an empty Covering test cell is a row error
    Method: test test/rule-inventory.test.mjs::row errors: no code span, short anchor, bad disposition, empty reason, bad or missing test path, absent test name
    Method: test test/rule-inventory.test.mjs::a covering test that is a directory at HEAD does not exist (review: cat-file -t)
  • faff-1428#done:22 A PR that commits no spec is skipped with no-spec-committed; one that deletes no non-exempt prose is skipped with no-prose-deleted.
    Verified: no-spec-committed and no-prose-deleted skips
    Method: test test/rule-inventory.test.mjs::deleted prose with no spec committed is skipped with no-spec-committed and the fixed notice
    Method: test test/rule-inventory.test.mjs::a deleted thematic break alone is letterless and exempt
  • faff-1428#done:23 Without --base, the base is the merge-base with origin/HEAD, or the result is skipped with no-base.
    Verified: no origin/HEAD gives no-base; with origin/HEAD the merge-base; a failing origin/HEAD lookup exits 2, never a no-base skip; merge-base takes refs/remotes/origin/HEAD
    Method: test test/rule-inventory.test.mjs::without --base: merge-base with origin/HEAD, else skipped no-base
    Method: test test/rule-inventory.test.mjs::without --base, a failing origin/HEAD lookup is an error, not a no-base skip (holdout scenarios:21)
    Method: test test/rule-inventory.test.mjs::without --base, the merge-base uses the full refs/remotes/origin/HEAD, never a local branch named origin/HEAD (review round 3)
  • faff-1428#done:24 A failing git command, an unreadable committed spec, or a bad --base exits 2, never 0.
    Verified: git failures (diff, cat-file, ls-tree), a bad ref and an unreadable config all exit 2; hunkless diff and absolute spec_docs_path exit 2; a lost main-checkout config fallback exits 2; a lost overlay fallback exits 2; from a passing run every injected git failure exits 2 with empty stdout, and the shared main-checkout probe prints the dispatch line naming the root and git's message (FAFF‑1463)
    Method: test test/rule-inventory.test.mjs::a failing git command exits 2, never 0
    Method: test test/rule-inventory.test.mjs::an unreadable config exits 2, never a pass
    Method: test test/rule-inventory.test.mjs::a --base that is not a plain ref exits 2 and prints nothing on stdout
    Method: test test/rule-inventory.test.mjs::a watched file whose diff has no text hunk is an error, never a skip (review: binary diff)
    Method: test test/rule-inventory.test.mjs::spec_docs_path is normalised: ./records/specs/ finds the spec; an absolute path exits 2 (review: spec_docs_path)
    Method: test test/rule-inventory.test.mjs::a failed main-checkout lookup in the shared config resolution exits 2, never a no-spec skip (holdout scenarios:21)
    Method: test test/rule-inventory.test.mjs::a lost overlay fallback in a linked worktree exits 2 (FAFF‑1463 shared lookup)
    Method: test test/rule-inventory.test.mjs::from a passing run, failing any single git call exits 2, with and without --base (holdout scenarios:21)
  • faff-1428#done:25 validate.yml runs the check on pull_request with --base origin/$BASE_REF and on push with BEFORE, with event values reaching the script only through env, an unfetchable BEFORE failing the step, and the literal no-base line present for faff gates discover.
    Verified: pull_request --base origin/$BASE_REF, push --base $BEFORE, env-only values, unfetchable BEFORE exits 1, literal no-base line discovered as a STATIC_ANALYSIS rung
    Method: command node plugin/skills/faff/bin/faff gates discover
    Method: command grep -n 'rule-inventory check' .github/workflows/validate.yml
  • faff-1428#done:26 test/rule-inventory.test.mjs covers each scenario in section 5, each row-error kind and each exemption rule (separator, letterless, header row with and without its whole table, heading with and without its whole section), using fixture repos with a local bare origin, and passes under node --test.
    Verified: 45 cases pass over fixture repos with a local bare origin: builder-view scenarios, every row-error kind and every exemption rule
    Method: command node --import ./test/hermetic-env.mjs --test test/rule-inventory.test.mjs
  • faff-1428#done:27 A mutation test shows each check bites: removing the anchor-length floor, the kept-needs-test rule, or the moved test each makes at least one case in the file fail.
    Verified: three mutants (floor, kept-needs-test, moved) each fail a case
    Method: test test/rule-inventory.test.mjs::mutations: dropping the anchor-length floor, the kept-needs-test rule or the moved test each fail a case

🤖 Generated with Claude Code

Closes FAFF-1428

alechill and others added 11 commits November 4, 2026 21:42
…ill-prose rule has no rule-inventory entry

Signed-off-by: Alec Hill <alec@shftwst.dev>
…rule-inventory row (FAFF-1428)

faff rule-inventory check compares the base tree with HEAD. Every block of
skill prose the diff deletes must either reappear verbatim inside one block of
skill prose at HEAD, or be quoted by a code-span anchor in a ## Rule inventory
row of a spec the diff adds under the spec docs path. kept rows name a test
that exists; changed and dropped rows give a reason.

The check applies when the PR adds a spec, fails a PR that changes an anchor
without one, and skips a hand-written edit with a notice. It runs in the
validate job beside regions check, with the same env-only base resolution,
and writes the row-covered table (dropped and changed first) to the step
summary. The format is documented in docs/reference/skill-authoring.md and the
nlspec producer points at it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
… pins (FAFF-1428)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
…AFF-1428)

- Per-file diffs run with --text and --literal-pathspecs; a watched file
  whose diff reports "Binary files" or has no hunk is exit 2, so a binary
  or -diff attribute or a NUL byte can no longer turn a deletion into a skip.
- A heading or table header is exempt only when every line of its section
  or table is deleted by pure-deletion hunks, the group holds a lettered
  block, and none of that content reappears in skill prose at HEAD.
- A heading over an empty section is never exempt.
- A covering test must be a blob at HEAD (cat-file -t), not a directory.
- tracking.spec_docs_path is normalised (./records/specs/ works); an
  absolute or out-of-repo path is exit 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
…anges delete nothing (FAFF-1428)

- The shared config lookup reads a failed `rev-parse --git-common-dir` as
  "no main checkout", which in a linked worktree silently dropped the
  main checkout's config and skipped the check. rule-inventory now resolves
  the common dir itself when this checkout lacks its own config or overlay,
  and exits 2 when the shared lookup found a different file. The shared
  lookup is unchanged.
- Without --base, origin/HEAD is probed with for-each-ref, so a failing
  git call is exit 2 rather than a no-base skip.
- A watched file with no text hunk deletes nothing when its blob is
  unchanged (a mode-only change) or its base was empty; any other content
  change with no hunk still exits 2.
- Tests pin a heading deleted alone over an edited paragraph, and fail every
  git call the check makes one at a time to show none ends in exit 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
…ookup is exit 2 (FAFF-1428)

- Covering-test and blob-identity lookups use `git ls-tree -z` through the
  failing-call guard: empty output is absence, a failed call is exit 2, and
  a covering test must be a blob.
- The overlay half of the config-location guard is now pinned by a
  linked-worktree case whose overlay lives only in the main checkout.
- merge-base takes refs/remotes/origin/HEAD, the ref the probe checks, so a
  local branch named origin/HEAD cannot move the base.
- The every-call test starts from a passing run, with and without --base,
  and lists the one call that still exits 0 when failed: the shared overlay
  probe in a main checkout, pending the operator's decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
…ispatch line (FAFF-1428)

FAFF-1463 makes the shared config lookup's `git -C <root> rev-parse
--git-common-dir` probe raise MainWorktreeProbeError instead of reading
a failure as "no main checkout". The two calls the every-call test
listed as pending the operator's decision now exit 2.

- rule-inventory passes the probe error through to the dispatch arm,
  so the one stderr line names the root and git's message.
- The every-call test asserts exit 2 and empty stdout for every
  failing call, and the exact dispatch line for the probe call.
- The linked-worktree scenarios:21 test asserts the exact line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
…kes redundant (FAFF-1428)

verifyConfigLocation re-ran `git rev-parse --git-common-dir` and refused
when the shared lookup had silently lost its main-checkout fallback.
Since FAFF-1463, that lookup's probe throws MainWorktreeProbeError on
every failure inside a repo (non-zero exit, spawn error, signal, empty
stdout), so the lost fallback can no longer happen.

Mutation evidence, on the rule-inventory suite (45 cases):
- guard removed, FAFF-1463 intact: 45/45 pass, every failure case
  still exits 2 through the dispatch arm;
- guard removed and the shared lookup reverted to the null-on-failure
  wrapper: 3 cases fail, including the overlay fallback case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
…smatches before asserting (FAFF-1428)

Two delta-review minors, test-only:
- The overlay fallback test now names what it pins, the FAFF-1463
  shared lookup, since verifyConfigLocation is gone.
- The every-call sweep collects status, stdout and probe-stderr
  mismatches across every failing call and asserts once, so one run
  reports every bad call. Strictness is unchanged: every call exits 2
  with empty stdout, and both probe calls print the one dispatch line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
Signed-off-by: Alec Hill <alec@shftwst.dev>
@alechill
alechill requested a review from shftwstdev as a code owner November 5, 2026 04:10
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Ready to land. Run this locally in a real terminal:

faff effects declare --run-dir .faff/anchors/run-20261104-043908-graft-FAFF-1428 --issue FAFF-1428 --step merge <<'EOF'
[{"kind":"merge","target":"pr:1182","reversible":true}]
EOF
faff merge-gate --pr 1182 --issue FAFF-1428 --run-dir .faff/anchors/run-20261104-043908-graft-FAFF-1428 --level L2 \
  --execute --merge-args "--squash --delete-branch"

alechill and others added 2 commits November 5, 2026 04:24
…git version (FAFF-1428)

git 2.48 and later create refs/remotes/origin/HEAD on fetch; older git
does not. CI runs git 2.56, so the two no-base cases found origin/HEAD
already present and failed there while passing on git 2.39.

fixture() and linkedWorktree() now delete the symref after their fetch
with `update-ref --no-deref -d`. Without --no-deref, git deletes the
ref the symref points at, refs/remotes/origin/main. Cases that need
origin/HEAD already set it with `remote set-head`.

Under git 2.54 (node:24-alpine): without this change 43/45 pass, and the
two failures are exactly the CI failures; with it, 45/45. Under git
2.39: 45/45 both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Alec Hill <alec@shftwst.dev>
Signed-off-by: Alec Hill <alec@shftwst.dev>
@alechill
alechill merged commit 3eb1f6d into main Nov 5, 2026
16 checks passed
@alechill
alechill deleted the FAFF-1428-rule-inventory-check branch November 5, 2026 04:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant