Repository navigation
feat(rule-inventory): fail a PR whose deleted skill prose maps to no rule-inventory row (FAFF-1428) - #1182
Merged
Conversation
…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>
Contributor
|
Ready to land. Run this locally in a real terminal: |
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:## Rule inventoryrow in the PR's committed spec that says what happened to it:kept, naming a covering test that exists;changedordropped, with a reason.Anything else is uncovered, and the check exits 1.
no-base(local only, with no origin/HEAD),no-prose-deleted, orno-spec-committed(with a notice). A PR that touches.faff/anchors/but commits no spec fails withfootprint_without_spec.validatestep, "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,droppedandchangedfirst..faff/anchors/, never an anchor's content.Mapping and exemption rules
--text,--no-renames,--literal-pathspecsand no textconv or external diff, so a binary or-diffattribute can't hide a deletion.^test/[A-Za-z0-9._/-]+\.test\.mjs$.ls-tree -zand must be a blob.tracking.spec_docs_pathis 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 throwsMainWorktreeProbeErroron any failure inside a repo, so:rule-inventorypasses 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>.--base, must exit 2 with empty stdout, and both probe calls must print exactly that line. The sweep collects every mismatch and asserts once.verifyConfigLocationis 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: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
faffter-dark-nlspec/SKILL.mdgoes from 18443 to 18680 (+237) for the new pointer to the inventory format.docs/reference/skill-authoring.md#rule-inventorydocuments the table format, anddocs/guide/cli.mdgains the command.bin/faff,regions.js(factory region, no selftest),lint-cli-coverage.js,tsconfig.json,gates-report.js, the build manifest, and the count pins intest/check-no-boundary-cast.test.mjsandtest/gates-ci-source.test.mjs.rule-inventory.tswith its committed emit, and 45 cases intest/rule-inventory.test.mjs.Verification
meets-specon 21/21 running-stack criteria at head0f7ea2a5. It ran in a network-isolated git 2.54 container and covered both the absent and the presentorigin/HEADcases. This is the sixth run; earlier runs were invalidated by head changes.origin/HEADon fetch. The fixtures now remove it withupdate-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.rule-inventory checkon this branch and the full unit run. The quality-gates contract exits 0.faff pr openshort-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
governance.ymlis set toon-missing: fail.gates discovercounts a command insideecho.--if-eligiblehypotheticals should write capture records.faff pr opentreats an existing anchor directory as current, so a head pushed after the PR opens keeps a stale anchor and body.test/rule-inventory.test.mjs:442.Acceptance Criteria
faff-1428#done:1A PR that deletes a non-exempt block of skill prose, commits a spec, and has no row or move covering the block failsfaff rule-inventory checkwith 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 sectionfaff-1428#done:2faff 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 sectionMethod: test
test/rule-inventory.test.mjs::a --base that is not a plain ref exits 2 and prints nothing on stdoutMethod: test
test/rule-inventory.test.mjs::a failing git command exits 2, never 0Method: command
node plugin/skills/faff/bin/faff rule-inventory check --base origin/main --jsonfaff-1428#done:3--jsonoutput has everyCheckResultfield.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 sectionfaff-1428#done:4The inventory format (section heading,Prose rulecode-span anchor,Covering test,Disposition) is documented indocs/reference/skill-authoring.mdunder aRule inventoryheading, 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 sectionMethod: command
grep -n '^## Rule inventory' docs/reference/skill-authoring.mdfaff-1428#done:5plugin/skills/faffter-dark-nlspec/SKILL.mdgains one line telling the producer that a spec deleting skill prose carries a## Rule inventoryin that format, andprose-baseline.jsonis 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-adaptersfaff-1428#done:6The command is TypeScript (rule-inventory.tswith its committed.jsemit andbuild-manifest.json), in the factory region, withREGION_SELFTEST_ARGV["rule-inventory"] = null, aTEST_FILE_COVERAGEentry fortest/rule-inventory.test.mjs, and adocs/guide/cli.mdrow;faff regions check,faff lint-cli-coverageandfaff lint-cli-docpass.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 checkMethod: command
node plugin/skills/faff/bin/faff lint-cli-coverageMethod: command
node plugin/skills/faff/bin/faff lint-cli-docMethod: test
test/rule-inventory.test.mjs::the CLI registration: a factory command with no selftestfaff-1428#done:7A 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 itMethod: test
test/rule-inventory.test.mjs::row errors: no code span, short anchor, bad disposition, empty reason, bad or missing test path, absent test nameMethod: test
test/rule-inventory.test.mjs::mutations: dropping the anchor-length floor, the kept-needs-test rule or the moved test each fail a casefaff-1428#done:8A 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 rowMethod: test
test/rule-inventory.test.mjs::mutations: dropping the anchor-length floor, the kept-needs-test rule or the moved test each fail a casefaff-1428#done:9The watched set isprose-size's exportedisCovered, 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.tsfaff-1428#done:10Path listing uses-zwithcore.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 skipMethod: command
grep -n 'core.quotePath=false' plugin/skills/faff/bin/lib/rule-inventory.tsMethod: 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:11A 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 movedfaff-1428#done:12Table 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 exemptMethod: test
test/rule-inventory.test.mjs::exemptions: separator rows are exempt; a header row is exempt only with its whole tableMethod: test
test/rule-inventory.test.mjs::a whole section deleted counts only its paragraph: the heading is exemptMethod: test
test/rule-inventory.test.mjs::a reworded heading alone is not exempt: it needs a rowMethod: 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:13Normalise 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 mdEscapeMethod: 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 errorfaff-1428#done:14Only 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 rowsMethod: 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:15A diff that adds or modifies a path under.faff/anchors/but commits no spec fails withfootprint_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_specfaff-1428#done:16--summaryescapes 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 mdEscapefaff-1428#done:17A 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 namefaff-1428#done:18Every printed line starts withrule-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 lineMethod: test
test/rule-inventory.test.mjs::a committed spec over 1 MiB exits 2Method: test
test/rule-inventory.test.mjs::normalise, clean and mdEscapefaff-1428#done:19--summary FILEappends the row-covered table,droppedandchangedfirst, 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 firstMethod: command
grep -n 'GITHUB_STEP_SUMMARY' .github/workflows/validate.ymlfaff-1428#done:20A row whose anchor matches no deleted block produces the warninganchor matches no deleted proseand 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 failfaff-1428#done:21keptrows with no test,changed/droppedrows 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 errorMethod: test
test/rule-inventory.test.mjs::row errors: no code span, short anchor, bad disposition, empty reason, bad or missing test path, absent test nameMethod: 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:22A PR that commits no spec isskippedwithno-spec-committed; one that deletes no non-exempt prose isskippedwithno-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 noticeMethod: test
test/rule-inventory.test.mjs::a deleted thematic break alone is letterless and exemptfaff-1428#done:23Without--base, the base is the merge-base withorigin/HEAD, or the result isskippedwithno-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-baseMethod: 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:24A failing git command, an unreadable committed spec, or a bad--baseexits 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 0Method: test
test/rule-inventory.test.mjs::an unreadable config exits 2, never a passMethod: test
test/rule-inventory.test.mjs::a --base that is not a plain ref exits 2 and prints nothing on stdoutMethod: 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:25validate.ymlruns the check onpull_requestwith--base origin/$BASE_REFand onpushwithBEFORE, with event values reaching the script only throughenv, an unfetchableBEFOREfailing the step, and the literal no-base line present forfaff 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 discoverMethod: command
grep -n 'rule-inventory check' .github/workflows/validate.ymlfaff-1428#done:26test/rule-inventory.test.mjscovers 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 undernode --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.mjsfaff-1428#done:27A 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