One triage decision needs two baseline entries, and nothing tells you
main went red at 9c256b67 (#954) and stayed red until #965. The cause was not the lockfile, and not a mistake in the triage reasoning — the reasoning in #951 is correct and unchanged. It was that Hypatia emits the same defect from two rule modules, and the acknowledgement named only one.
| field |
acknowledged by #954 |
left blocking |
rule_module |
workflow_audit |
workflow_hardening |
file |
actions.lock |
.github/workflows/actions.lock |
type |
invalid_actions_lock |
invalid_actions_lock |
severity |
high |
high |
reason |
transitive_dependencies_missing, asana/push-signed-commits@d615 |
identical string |
scripts/apply-baseline.sh:188 matches rule_module by exact string equality:
and .rule_module == $finding.rule_module
So the author of a triage decision must know, in advance and without help, every rule module capable of emitting that finding, and write one entry per module. There is no warning when they write fewer, and the failure surfaces as a red main on an unrelated PR rather than at the point the baseline was edited.
This is the same shape as the recurring "a guard asks a different question than its consumer" defect: the ledger asks which module reported it, while the triage decision is about what the defect is.
Proposed cure (needs an owner call on which arm)
Arm A — rule_module accepts a string or a list. One triage decision becomes one entry:
{ "rule_module": ["workflow_audit", "workflow_hardening"], ... }
Touches scripts/apply-baseline.sh (the match predicate and the schema validation block at :89-102), .machine_readable/hypatia-baseline.schema.json, and scripts/tests/apply-baseline-test.sh. This is the elegant long-term arm: it makes the data model match the decision being recorded. type already accepts three shapes (see the comment at apply-baseline.sh:104), so list-valued fields are not a new idea in this file.
Arm B — leave the matcher alone, add a lint. A check that warns when a (severity, type, file_pattern) triple is acknowledged for some but not all rule modules seen in the most recent scan. Cheaper, but it is advisory, and ::warning:: cannot fail a job.
Arm C — do nothing, document it. Record in the baseline schema docs that entries are per-module and that a triage decision may need several. Zero code risk; relies entirely on the next author reading it.
Acceptance criteria
Evidence commands
# the two copies, from the failing run
gh api repos/hyperpolymath/standards/actions/jobs/106709862347/logs \
| grep -B6 -A4 invalid_actions_lock
# the exact-equality match predicate
sed -n '183,205p' scripts/apply-baseline.sh
Cross-refs: #951 (the underlying asana/setup-python float, unchanged), #954 (round 2, which introduced the half-ack), #965 (round 3, the one-entry unblock), #787 (decision surface).
🤖 Generated with Claude Code
https://claude.ai/code/session_01Ji1bq3TypfycfUPAR7hSxR
One triage decision needs two baseline entries, and nothing tells you
mainwent red at9c256b67(#954) and stayed red until #965. The cause was not the lockfile, and not a mistake in the triage reasoning — the reasoning in #951 is correct and unchanged. It was that Hypatia emits the same defect from two rule modules, and the acknowledgement named only one.rule_moduleworkflow_auditworkflow_hardeningfileactions.lock.github/workflows/actions.locktypeinvalid_actions_lockinvalid_actions_lockseverityhighhighreasontransitive_dependencies_missing, asana/push-signed-commits@d615scripts/apply-baseline.sh:188matchesrule_moduleby exact string equality:So the author of a triage decision must know, in advance and without help, every rule module capable of emitting that finding, and write one entry per module. There is no warning when they write fewer, and the failure surfaces as a red
mainon an unrelated PR rather than at the point the baseline was edited.This is the same shape as the recurring "a guard asks a different question than its consumer" defect: the ledger asks which module reported it, while the triage decision is about what the defect is.
Proposed cure (needs an owner call on which arm)
Arm A —
rule_moduleaccepts a string or a list. One triage decision becomes one entry:{ "rule_module": ["workflow_audit", "workflow_hardening"], ... }Touches
scripts/apply-baseline.sh(the match predicate and the schema validation block at :89-102),.machine_readable/hypatia-baseline.schema.json, andscripts/tests/apply-baseline-test.sh. This is the elegant long-term arm: it makes the data model match the decision being recorded.typealready accepts three shapes (see the comment atapply-baseline.sh:104), so list-valued fields are not a new idea in this file.Arm B — leave the matcher alone, add a lint. A check that warns when a
(severity, type, file_pattern)triple is acknowledged for some but not all rule modules seen in the most recent scan. Cheaper, but it is advisory, and::warning::cannot fail a job.Arm C — do nothing, document it. Record in the baseline schema docs that entries are per-module and that a triage decision may need several. Zero code risk; relies entirely on the next author reading it.
Acceptance criteria
scripts/tests/apply-baseline-test.shgains a control proving a multi-module entry suppresses every listed module and no unlisted one. (Today: 15 passed, 0 failed — verified onfix/hypatia-baseline-workflow-hardening-ack.)type, a higherseverity, and a non-matchingfile_patternall remainKEPT. (Verified for the single-module case in fix(hypatia): ack the workflow_hardening copy of the lock finding (unblocks red main) #965 — 4 controls,kept=4 suppressed=0.)invalid_actions_lockentries collapse back to one, which shrinks.hypatia-baseline.json212 -> 211 and so passes the exemption ratchet with noRatchet-exceptiontrailer.Evidence commands
Cross-refs: #951 (the underlying asana/setup-python float, unchanged), #954 (round 2, which introduced the half-ack), #965 (round 3, the one-entry unblock), #787 (decision surface).
🤖 Generated with Claude Code
https://claude.ai/code/session_01Ji1bq3TypfycfUPAR7hSxR