Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 83 additions & 4 deletions .github/groom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,26 @@ rather than buried in a runner script.
`REJECT` (premature / overstated / not worth it).
- **`security: true`** marks any auth/permission/security-adjacent finding —
those are filed as investigations, **never** auto-implemented.
- **`signature`** is a stable dedup key (`<repo-basename>:<scope>:<slug>`) whose
`<slug>` is derived **deterministically** from the finding's core subject, so it
stays identical across re-runs of the same finding and a consumer never re-files
a finding it has already seen.
- **`signature`** is a stable dedup key, `<repo-basename>:<scope>:<path-slug>`,
where `<path-slug>` is the finding's **primary file or directory path** —
lowercased, every run of non-alphanumeric characters collapsed to a single
hyphen, leading/trailing hyphens trimmed (`src/tools.ts` → `src-tools-ts`,
`services/ingest/` → `services-ingest`). Multi-file finding: the
**alphabetically first** of the cited paths — a mechanical rule, because "the
most representative one" is a judgment the verifier would re-make differently
next run. Only a repo-wide pattern with no single anchor falls back to a
normalized subject noun-phrase. A `security: true` finding's slug is prefixed
`sec_` — underscore, because slugification can never produce one, so the
security lane for `auth.ts` (`sec_auth-ts`) cannot collide with a routine
finding about `sec/auth.ts` (`sec-auth-ts`). That lane is what stops a routine
finding already filed for a file from deduping away a security finding about
that same file.
It is anchored to the **path, not the title**, because titles are re-generated
on every run: a re-worded title yields a new title-slug, the ledger's
exact-string match sees a "new" finding, and the same finding is filed twice
(observed in a consumer repo: one `src/tools.ts` finding, two issues opened by
consecutive runs). Paths survive rewordings, so the signature stays identical
across re-runs and a consumer never re-files a finding it has already seen.

## How a consumer uses these briefs

Expand Down Expand Up @@ -119,6 +135,7 @@ Keyed on `(repo, finding_signature) → {filed | rejected | superseded}`:
| Open **builder PR** for the signature (BE-4003) | `pr-open` | no |
| **Builder PR merged** | `merged` | no (shipped) |
| **Builder PR closed unmerged** | `pr-closed` | **no — durable** (human declined) |
| A known, non-`superseded` signature shares the candidate's `<path-slug>`, and the candidate is not `security: true` (BE-4460) | `path-collision` | no |
| No `groom` issue or PR carries the signature | `unknown` | **yes** |

Only an `unknown` signature is filed/proposed. Human rejection — close-as-not-planned,
Expand All @@ -140,6 +157,62 @@ issue for a `to_file` finding **must**:

Skip either and the next run cannot recognize the issue and will re-file it.

### The path-token backstop (BE-4460)

Classification treats the signature as an opaque string, with one exception: a
candidate whose exact signature is `unknown` but whose `<path-slug>` segment
(everything after the **last** `:` — `<repo-basename>` and `<path-slug>` are
colon-free by construction, so counting from the right is what keeps a scope
label that itself contains a colon, `pkg:api`, from shearing the token) is
already covered — by a known signature, or by a candidate already routed to
`to_file` in the same batch — is suppressed as **`path-collision`** rather than
filed. That keeps one issue per anchoring path when the leading segments differ
(a re-scoped run, a legacy signature whose slug coincides). Matching is **exact
string equality** on the path segment — no substring or fuzzy matching, which
would silently drop real findings about different files that share a basename
(`src/index.ts` vs `lib/index.ts`).

Because the backstop suppresses a candidate whose *own* signature is new, it is
deliberately narrow — two carve-outs:

- **`security: true` candidates are never suppressed by it.** A path anchors a
location, not a finding, so without this an already-filed routine finding on
`src/tools.ts` would bury a later security finding on the same file and break
the "security findings always surface as investigations" guarantee. (The
verifier's `sec_` slug prefix separates the two lanes up front; this is the
code-side guarantee for legacy and cross-scope signatures that predate it, and
it fails **closed** — `is_security_finding` treats a finding whose flag the
verifier omitted or mangled as security, so a malformed flag can never be what
buries one.) Exact-signature dedup still applies, so the exemption costs at
most one extra issue — the next run sees it as `filed`. It does **not** make
security findings individually addressable: two distinct vulnerabilities in one
file share the `sec_<path>` key and collapse, exactly as two routine findings
on one file do (see the limit below).
- **`superseded` records are left out of the path index.** `groom-superseded` is
the documented "retire this issue so its finding can be re-filed under the
current format" signal; keeping it in the index would let the retired issue go
on suppressing the replacement by path and defeat the label a human applied.

Known, accepted limit — and a **permanent** suppression, not a one-time
transition cost. A path-anchored key identifies a *location*, so every later
finding that maps to an already-covered token is dropped for good: two different
findings about one file, and two genuinely distinct paths that slugify alike
(`src/foo/bar.ts` and `src/foo-bar.ts` both → `src-foo-bar-ts`, or a file and a
same-stem directory). That is the deliberate trade for a key that survives
re-wording — one issue per anchoring path, chosen over the duplicate-per-run
spam the title-derived key produced. Widening it needs a per-finding
discriminator that is *stable across runs*, which is exactly what the LLM cannot
supply today; the `security` flag is one bit that is, which is why the security
lane is carved out of this and nothing else is.

Consequence for the format transition: a legacy *title*-derived slug that merely
*embeds* the path (`split-tools-ts-into-focused-modules`) is **not** matched, so
such a finding can be filed once more under its new path-anchored signature —
then it is stable forever. Label the superseded legacy issue `groom-superseded`
(or close it as not planned) to retire it. A security finding already filed under
an unprefixed slug re-files once for the same reason when it picks up its `sec_`
prefix — same one-per-finding, one-time transition cost, same fix.

The dedup decision is a point-in-time snapshot of GitHub issue state read
*before* filing, and issue creation happens in a later step. Two overlapping
groom runs could therefore both classify the same signature as `unknown` and
Expand All @@ -164,8 +237,14 @@ Single-signature probe (exit 0 = should file, 1 = suppressed):

```bash
python3 .github/groom/ledger.py --repo owner/name --check "<signature>"
# ...as a security finding, which the path backstop never suppresses:
python3 .github/groom/ledger.py --repo owner/name --check "<signature>" --check-security
```

A bare signature carries no `security` flag, so the probe answers for a routine
finding by default; pass `--check-security` to mirror `partition`'s decision for
a `security: true` candidate.

- **`tests/`** — `unittest` suite, run by
[`test-groom-scripts.yml`](../workflows/test-groom-scripts.yml).

Expand Down
Loading