Repository navigation
bug(ci): the blocking stale-baseline audit reds ssot-coverage on any PR touching 8% of the repo #14912
Description
Calibration finding on the guards shipped in #14883 (ac754f5e0). Corrected account, replacing this issue's first version: the guard was not defective and did not cause the base to go red. It fired on real drift and reported it accurately on the pull request that caused it. The base red came from a merge issued against a head whose check was already failing. A blocking guard that fires is not a bug.
What stands on its own is the recovery cost, and the numbers are why it is worth fixing.
The measured problem
| baseline entries | 1411 |
| distinct files covered | 776 |
| tracked files in repo | 9510 |
~8% of the repository is baselined, concentrated exactly where change is frequent:
328 autobot-slm-backend/ansible
297 autobot-infrastructure/shared
117 autobot-backend/services
99 autobot-backend/api
74 autobot-frontend/src
Any pull request that edits, moves, or fixes a baselined line strands its entry and reds ssot-coverage. The author is then told what is wrong and not what to do:
STALE other|autobot-slm-backend/ansible/roles/_shared/tasks/clean_wrong_node_dir.yml|"/opt/autobot/{{ dir_name }}/data
Nothing in that output names a command, and the file to edit —
pipeline-scripts/hardcoded_values_baseline.txt — has nothing to do with the change that tripped it. This taxes the exact behaviour the guard exists to encourage: fixing a hardcoded value. #14890 hit it on the first merge after the guards landed, which is evidence about the rate, not bad luck.
Fix: make the correction one command, keep the audit blocking
./pipeline-scripts/detect-hardcoded-values.sh --prune-baseline— rewrites the baseline to what the detector currently finds, but only ever removes. It must be structurally incapable of adding a key or raising a count; that property is what stops it becoming the bypasscheck_baseline_no_growth.shexists to prevent.- Fix the message, not just the mechanism.
--audit-baseline's failure must name the exact command and list the entries it would prune. Most of this cost is discoverability. - Keep the audit blocking. A stranded exemption naming a moved path really does re-permit the value when that path returns, and that is a known recurrence class in this repository. The answer is one-command recovery, not a downgrade to a warning.
- Note in
docs/developer/HARDCODING_PREVENTION.mdso the next person to hit the red finds the answer.
Does the cost land on the change that caused it? Yes — but nothing stops that change
Checked rather than assumed. ssot-coverage did run on #14890's own head and did fail there (744df346a, conclusion failure, 21:48). So the audit already reports on the causing pull request; the deeper fix that question implies is largely in place.
The real gap is enforcement. SSOT Configuration Compliance is not one of the ten required contexts on Dev_new_gui:
smoke-test · code-quality · startup-import-smoke · Unit & Integration Tests
No open blocks-merge issues reference this PR · No commit trailers
verify-generated-types · migration-matrix · verify-precommit-config · api-wiring
So every step in that workflow — the detector, the stale audit, and the new no-growth guard — is advisory at the branch-protection layer. A red ssot-coverage does not block a merge, which is precisely how a failing head could be merged. The whole hardcoded-value gate is a check nobody is required to satisfy.
Making it required is not a one-line change, and the reason is recorded in the workflow itself (#13388): ssot-coverage.yml carries a paths: filter, and a path-filtered required check that never triggers leaves a pull request deadlocked on "Expected — Waiting for status". This repository already has the established answer to that shape — a complementary shim job publishing the same context when the real one does not apply, as python-required-context.yml, frontend-required-context.yml and api-wiring's skip job all do. Promotion to a required context is an owner decision and is deliberately not bundled into this issue's scope; it is recorded here so the enforcement gap is not mistaken for solved once the ergonomics are.
Acceptance criteria
-
--prune-baselineexists and is proven removal-only by test: given a tree containing a NEW finding, a prune run must not add it - Mutation-proved that prune cannot be used to grow: fabricate a violation, run prune, confirm
check_baseline_no_growth.shstill fires - Mutation-proved recovery: strand an entry -> audit RED ->
--prune-baseline-> audit GREEN, no-growth still GREEN -
--audit-baseline's failure message names the command and the entries to be pruned -
docs/developer/HARDCODING_PREVENTION.mddocuments the recovery - The enforcement gap above is either actioned or carried forward as its own owner-gated issue
Related
- refactor(ci): consolidate duplicated, drifted and never-run CI guards (#14371, #13984, #12986, #14034) #14883 — where these guards shipped
- fix(ci): drop the baseline entry #14890 made stale (#14371) #14910 — removed the single stranded entry (symptom)
- tech-debt: three separate hardcoded-value detectors have drifted (pipeline-scripts, autobot-infrastructure, pre-commit hook) #14371 — the consolidation these guards belong to
- ci: ~25 workflow runs per PR head movement saturate GitHub-hosted concurrency and manufacture threshold-test failures #13388 — why
ssot-coverageis path-filtered and non-required
Activity
mrveiss commentedon Aug 23, 2026
Body rewritten with a corrected account. Two changes of substance:
ssot-coverageran on fix(provision): route every component removal through one data-guarded task, so an unknown fact cannot delete a database (#14856) #14890's own head and failed there. The real gap is thatSSOT Configuration Complianceis not one of the ten required contexts onDev_new_gui, so the detector, the stale audit and the new no-growth guard are all advisory at the protection layer. Recorded with the ci: ~25 workflow runs per PR head movement saturate GitHub-hosted concurrency and manufacture threshold-test failures #13388 path-filter constraint and the existing required-context-shim precedent; promotion is an owner decision and deliberately out of this issue's scope.mrveiss commentedon Aug 23, 2026
Prototyped
hv_pruned_baselineagainst a read-only copy of the merged library while blocked on #14910. Three properties confirmed empirically, not just by construction:The implementation iterates only keys already in
HV_BASELINE(so it can never introduce one) and emitsmin(baseline_count, seen_count)(so it can never raise one).hv_partitionincrementsHV_BASELINE_SEENbefore the allowance test, soseencounts every finding for a key — which is what makesmincorrect rather than accidentally lossy.Safety gap the prototype exposed, now part of the design. Prune drops any baselined key the scan did not see. That is the intended behaviour against a full tree scan, but it means a broken or partial scan silently guts the baseline: in the prototype a deliberately tiny simulated scan reduced 1411 entries to 2. A scan that fails to load rules, or whose scan directories are missing, produces few or zero findings and would take the whole record with it — and the no-growth guard would not object, because shrinking is allowed by design.
Two additional acceptance criteria:
removing 1 entryandremoving 1400are visibly different in the diff and in review.Still holding on #14910 as instructed — it touches the same baseline file. Waiter armed on its merge.
mrveiss commentedon Aug 23, 2026
Implemented in PR #14915 (
26c7064a0), branched fresh offorigin/Dev_new_gui(192a2d12c) after #14910 merged.All acceptance criteria met, each mutation-proved rather than asserted:
--prune-baselineremoval-onlymin(baseline, seen))HARDCODING_PREVENTION.mdgains the recovery sectionThe removal-only property is structural, not a check:
hv_pruned_baselineiterates keys already in the baseline (cannot introduce one) and emitsmin(baseline_count, seen_count)(cannot raise one). That matters because a prune able to add or increment would be the bypasscheck_baseline_no_growth.shexists to prevent.The empty-scan refusal came out of prototyping, not review: prune drops every key the scan did not see, so a broken detector or a moved scan directory would take all 1410 entries with it — and the no-growth guard would not object, since shrinking is allowed by design.
The required-context gap recorded in this issue is not addressed by #14915 and remains open here as an owner decision. Separately, #14914 was filed for a distinct finding: 8 of 12 rule classes never block, and 9 unbaselined violations sit on the base right now under
"status": "pass".fix(ci): refuse to scan a partial tree, so a moved directory cannot g…
fix(ci): give the stale-baseline audit a one-command recovery (#14912) (
github-actions commentedon Aug 23, 2026
PR #14915 (merged to
Dev_new_gui) references this issue with a close keyword.If this issue is fully resolved, close it manually. If work remains, no action is needed.
mrveiss commentedon Aug 23, 2026
Delivered and merged as
c83c2e39e(PR #14915).c7de43dcd. All three touched steps green on the runner: the detector, the stale-entry audit, and the no-growth guard.code-reviewerpass. Removal-only confirmed structural, not merely tested — one write path, no early-write error branch, cannot add a key, cannot raise a count, no escape found for either property. Header round-trips byte-for-byte.What this fixes: the recovery cost identified here. A fixed violation can now drop its baseline line with one command, and the audit failure names both the offending entries and the exact command.
A HIGH finding was raised in review and fixed before merge, not deferred.
The empty-scan refusal only caught a scan returning literally zero.
hv_scan_treetreats a missing root as a silent no-op, so one movedSCAN_DIRSentry yields nothing while the other five yield hundreds — the total stays non-zero, the refusal never fires, andmin(kept, 0)drops every key beneath the unreached directory. Reproduced hermetically before fixing:--audit-baselinethen passed, because every survivor still matched, and the no-growth guard stayed silent because shrinking is allowed by design. Nothing between them checked magnitude. Real blast radius on the committed baseline: 554 entries underautobot-backend, 405 underautobot-slm-backend, 337 underautobot-infrastructure.The fix guards every mode, not just prune — a deliberate widening of the suggested fix, and the right one. A partial scan corrupts all three:
--jsonunder-reports the countsssot-coverage.ymlgates on, and--audit-baselinereports the unreached directory's entries asSTALE. That last one compounds: the audit falsely accuses real entries, and its own remedy is to prune, which would then delete them. Two mechanisms that each look correct in isolation.Landed message, verified in base:
A proportional-loss guard was considered and declined, with the reasoning recorded in the code: no percentage threshold has a non-arbitrary value, it would block the one legitimate large prune (a sweep fixing many violations at once), and any loss under the threshold still passes. The existence check removes the cause rather than rationing the symptom.
Also corrected: the guard's own justification comment cited "a rules file that failed to load", which is unreachable — the script is already fatal on an unsourceable library before scanning. Naming a risk the code already forecloses makes the remaining real one look handled.
Tests: 8 added, parametrized over all six scan dirs so the guard cannot silently cover five, plus a
SCAN_DIRSdrift test. Includes the byte-identical no-op prune the PR body had claimed as a proof point with no committed test behind it. Also folded in:LC_ALL=Con the prune sort (unpinned collation could reorder every line in a different CI image) and the temp file added to the cleanup trap.This PR does not modify the baseline itself — 1433 lines before and after.
Remaining in this area: #14914, where
STATUSand the workflow exit are keyed onssot_violationsalone, so 8 of 12 emit classes never block and the base currently reportspassover 9 unbaselined violations.