Skip to content

tech-debt(guards): every ratchet baseline is produced by its own detector, so its blind spots become silent permanent exemptions #15897

Open

Description

@mrveiss

Every ratchet in this repo freezes a baseline that its own detector produced. At the moment of freezing, that baseline cannot distinguish "the tree contains N instances" from "the detector can see N instances" — and nothing in the pattern ever forces the difference to surface.

The consequence is specific: whatever the detector is blind to becomes permanently exempt, without appearing in the baseline — the list whose entire purpose is to record exemptions. An exemption you can read is a decision. An exemption caused by a blind spot is invisible, and the ratchet reports clean forever.

The worked example, measured

#15896 added a guard forbidding path:LINE citations in comments, with a baseline of the citations that predate the rule. Its scan matched lines starting with #, or — for Python — lines containing a triple quote. That matches the line opening or closing a docstring, never a line inside one, and in this codebase the citations live in docstring bodies.

  • baseline as measured by the detector: 8
  • true population, re-derived independently: 26

Both numbers were correct about their own question. The 8 was never a count of the tree; it was a count of what the filter could see. Freezing it would have made 18 citations permanently exempt with no record that they existed.

It was caught only because the population was re-derived a second way — git ls-tree enumeration instead of Path.glob, tokenize.STRING tokens instead of AST docstring spans — and the two runs compared. Fixed in #15896; both implementations now agree at 26 with an empty symmetric difference.

The PR's stated justification for its rule was "it costs nothing, because the form is already rare — 8 instances in 464 files." That argument was measuring the instrument.

This is structural, not one bad filter

Verified against the other ratchets on Dev_new_gui:

repo_tests/python_file_size_ratchet_baseline.py — its own docstring at :536 describes the mechanism:

A regeneration that re-measures the tree emits "path": count and nothing else

The baseline is the detector's output. There is no second derivation.

repo_tests/frontend_api_contract_ratchet_test.py:71-94 — _BASELINES holds literals ("inline_generics": 577, 87) produced by _inline_generic_count at :185. That counter is known defective: #15771 records that it counts matches inside comments. So 577 cannot be read as a count of inline generics — it is 577 detector-hits, an unknown number of which are comments, and no inspection of the number can separate them. The baseline silently encodes the detector's defect and then enforces it.

pipeline-scripts/hardcoded_values_baseline.txt — 1411 entries, and this one is the closest to right: detect-hardcoded-values.sh offers --audit-baseline ("Fail on baseline entries that match nothing") and --prune-baseline.

That last one matters for scoping this issue, because it shows two independent failure directions and only one is guarded anywhere:

Direction Question Guarded today
staleness does every baseline entry still match something? yes, in places — --audit-baseline, and #15896's test_the_baseline_still_describes_real_citations
blindness does the detector's population equal the tree's? nowhere

Staleness is checkable with the detector alone, which is why it got built. Blindness is not checkable with the detector alone by definition — that is the whole problem.

Why review does not catch it

Reading the detector re-uses the model that produced the blind spot. In #15896 the author wrote a test asserting the fixed behaviour and it passed against the broken filter, because the module's docstring quotes a triple-quote character while explaining the bug — so the prose describing the blind spot was what satisfied the test for it. It was caught by reverting the filter and re-running, not by reading.

That generalises: once the detector, its baseline, and its test share an author, inspection cannot reach the class. Only a second world to compare against can.

Proposed remedy

Not a permanently maintained second implementation — a cross-check at the only two moments it can pay:

  1. At freeze. Before a baseline is committed, derive the population a second way — different file enumeration and different matching — and require set equality, not just an equal count. Record the second derivation in the baseline file so a later reader knows it was done and how.
  2. Whenever the matcher changes. A change to what the detector matches (not what it reports) invalidates the cross-check and needs a fresh one. A change to formatting or messaging does not.

A count-only comparison is not sufficient: two implementations can agree on a total and disagree on membership. #15896's check compared sets and found the symmetric difference empty, which is the assertion worth making.

Acceptance criteria

Explicitly out of scope

Building a generic cross-check harness. Each ratchet matches something different and a shared harness would reintroduce the shared implementation this issue is about. The second derivation should be ad hoc by construction — that is the property that makes it independent.

Notes for whoever picks this up

Order by consequence, not by ease: inline_generics first, because it is the one where the detector is already proven defective (#15771) and the baseline is therefore known to encode it. The others are unverified rather than known-wrong.

This does not need a write slot to start — the re-derivations are read-only analysis, and the finding for each ratchet can be posted before any code changes.

Credit: shape identified jointly while reviewing #15896; the --audit-baseline distinction came from reading detect-hardcoded-values.sh and is what keeps this issue from overclaiming that nothing is guarded.

Activity

  1. mrveiss commented on Sep 6, 2026

    @mrveiss
    OwnerAuthor

    Re-derivation 1 of 5 — inline_generics: 577 → 573, 87 → 86

    First of the independent re-derivations this issue asks for. Done read-only, no write slot needed, which is the point worth noting for scheduling the rest.

    Independence

    detector (frontend_api_contract_ratchet_test.py) this re-derivation
    enumeration root.rglob("*") on the working tree git ls-tree -r origin/Dev_new_gui
    matching _INLINE_GENERIC_RE over raw file text same regex over lexer-stripped source

    The lexer walks the source character-wise tracking string literals, so // inside "https://…" is not mistaken for a comment — a naive strip would have deleted the rest of those lines and undercounted.

    Control first

    My enumeration reproduces the detector's raw counts exactly — 577 and 87. That is what licenses the comparison: it establishes the two runs are looking at the same file set, so the delta below is attributable to matching and not to scope. Without this the corrected numbers would be uninterpretable.

    Result

    app baseline comment-borne corrected
    autobot-frontend 577 4 573
    autobot-slm-frontend 87 1 86

    All five verified by hand as JSDoc block-comment content, e.g. useApiResource.ts:17 and :25:

     *     apiClient.get<KnowledgeStatsBasic>(`${getApiBase()}/knowledge_base/stats/basic`)
    

    Others: useErrorHandler.ts, useFakeProgress.ts, usePrometheusMetrics.ts:170.

    I also checked the opposite direction — a call split as apiClient\n .get<T>( cannot match a regex requiring the dot adjacent to the identifier. Zero instances in either app, so there is no compensating undercount and the deltas are the whole story.

    What this means for #15771

    The over-count is 4 and 1 — real, and smaller than the framing of "an unknown comment fraction" might have suggested. That is worth stating plainly rather than letting the issue imply a large correction.

    The actionable part is unchanged, and it is not the size:

    Whoever fixes #15771's counter must lower the baseline to 573/86 in the same commit. Carrying 577/87 against a corrected counter silently grants four and one free violations — headroom nobody decided to give, indistinguishable afterwards from work that was never done. That is this issue's failure mode inverted: not a false improvement, but false slack, and equally invisible.

    Scope of this claim

    This establishes the comment fraction and rules out the multi-line undercount. It does not establish that the remaining 573 are all genuine inline generics — \b\w*[aA]pi\w*\. matches any identifier containing "api", so a non-API object named e.g. rapidCache.get<T> would count. I did not test that, and it is a separate question from the defect #15771 records.

    Next: hardcoded-values, file-size, commit-trailer. Taking them in that order — hardcoded-values has --audit-baseline already, so its re-derivation also tests whether that guard's existence changed the outcome.

  2. mrveiss commented on Sep 6, 2026

    @mrveiss
    OwnerAuthor

    Re-derivation 2 of 5 — hardcoded-values: the congruence is the finding

    This is the case where staleness and blindness separate most cleanly, because this detector already guards the first one.

    The tautology, measured

    The 1363 baseline entries break down by file extension as:

    ext entries
    py 695
    yml 332
    sh 119
    js 94
    ts 55
    vue 44
    yaml 24

    HV_SCAN_EXTENSIONS is py|ts|vue|js|sh|yml|yaml. The baseline's extension set is exactly that list, with zero entries outside it.

    That congruence is not evidence the tree is clean outside those extensions. It is evidence of nothing at all — the baseline is the detector's output, so it contains what the detector scans and cannot contain anything else. A file type that was never opened is indistinguishable, in the baseline, from a file type that was opened and found clean.

    --audit-baseline confirms the distinction rather than closing it

    detect-hardcoded-values.sh offers --audit-baseline ("fail on baseline entries that match nothing") and --prune-baseline. Both are real and both work the same direction: does every recorded entry still correspond to something? That is checkable with the detector alone, which is why it exists.

    The other direction — does the detector's population equal the tree's? — is not checkable with the detector alone, because the instrument's blind spot is inherited by any census it performs. Hence unguarded here, as everywhere.

    What blindness costs here, concretely

    Filed as #15903. scripts/lib/hardcoded-value-rules.sh:149 defines HV_ACCOUNT_POSITION matching User=/Group= — systemd unit directive syntax, which occurs in essentially no other file type. Applied verbatim to non-comment lines:

    • 35 matches in 11 scanned files — the shell and ansible that write units
    • 32 matches in 18 unscanned files — the .service units and .service.j2 templates that are the units

    The rule catches the scripts that generate systemd units and cannot see the units. Its author wrote that syntax deliberately, so the intent is on the record; the extension list makes it unreachable.

    Not a claim that those 32 are violations — a unit naming a service account is plausibly correct. The claim is that nobody decided. A baseline entry is a recorded judgement; an unscanned file is not a judgement of any kind, and the baseline offers no way to tell the two apart.

    Also: the header's count has drifted

    hardcoded_values_baseline.txt:17 — "All 1408 entries below carry exactly three." The file holds 1363. The invariant it asserts still holds; the count is 45 stale. In #15903's scope, with a suggestion to drop the number rather than correct it, since "every entry below carries exactly three" needs no maintenance.

    Method note

    Enumeration by git ls-tree against merged base, rules transcribed from the shell source rather than sourced, comment lines excluded using the detector's own _HV_COMMENT_RE shape. I deliberately did not count .md (219 files) or generated test-result JSON toward the finding: documentation naming a deployment path is not the same defect, and folding it in would have inflated a real result into an unusable one. The 32 are deployment configuration only.

    Verdict for this ratchet: staleness guarded, blindness unguarded, and blindness has a measured cost of 32 undecided findings across 18 files.

    Next: file-size and commit-trailer.

  3. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Re-derivation: the file-size ratchet. 47 oversized files are exempt and none of them appear in the exemption list.

    Taking the file-size baseline re-derivation. The thesis holds here, measured.

    What the detector can see

    scripts/check_python_file_size.py:166-187 enumerates with
    git ls-files "*.py" minus EXCLUDED_PREFIXES:

    EXCLUDED_PREFIXES = (".worktrees/", "autobot-infrastructure/", "autobot-backend/code_analysis/")

    .worktrees/ is right — those are not repository files. The other two are real
    tracked trees.

    The measurement

    tracked .py total          : 5570
    in scope (detector sees)   : 5241
    excluded real repo trees   :  329
      of those OVER 600 lines  :   47      <- never in KNOWN_LARGE, and cannot be
    

    The twelve largest:

    2341  autobot-backend/code_analysis/src/anti_pattern_detector.py
    1686  autobot-backend/code_analysis/src/env_analyzer.py
    1593  autobot-infrastructure/shared/scripts/microservice_architecture_evaluator.py
    1395  autobot-infrastructure/shared/scripts/logging/log_forwarder.py
    1340  autobot-infrastructure/shared/tests/comprehensive_test_suite.py
    1169  autobot-infrastructure/shared/scripts/analysis/npu_performance_measurement.py
    1124  autobot-infrastructure/shared/scripts/zero_downtime_deploy.py
    1094  autobot-infrastructure/shared/scripts/metrics_collector.py
    1034  autobot-infrastructure/shared/scripts/deployment_rollback.py
    1017  autobot-infrastructure/shared/scripts/log_aggregator.py
    1002  autobot-backend/code_analysis/src/ownership_analyzer.py
     974  autobot-infrastructure/shared/scripts/phase_validation_system.py
    

    One is 3.9x the ceiling. KNOWN_LARGE exists to record every file allowed
    to exceed 600 lines, and 47 files exceed it with no entry — not because anyone
    decided they should be exempt, but because the enumeration never offered them
    for the decision. That is this issue's shape exactly: an exemption you can read
    is a decision; an exemption caused by a blind spot is invisible.

    The part that makes it more than an instance

    autobot-backend/code_analysis/ is the same tree #15915 just proved was dark
    to flake8 for six to nine months
    , hiding five constructors that raised
    NameError on every call. #15915's conclusion was that a guard bounded by the
    exclusion that caused the defect can only ever find the instance you already
    knew about
    , and it deleted the F821 scope rather than widening it.

    The file-size guard still carries the same exclusion, and two of its largest
    invisible files — anti_pattern_detector.py at 2341 and env_analyzer.py at
    1686 — are in that tree. One exclusion string, propagated across guards, each
    one independently reporting a clean tree.

    The floor has the #15913 shape too

    MIN_TRACKED_PY_FILES = 3000 against 5241 in scope. Tested by removing each
    top-level tree:

    lose autobot-backend    (3928) -> 1313  fails
    lose autobot-slm-backend (443) -> 4798  passes
    lose autobot_shared      (308) -> 4933  passes
    lose repo_tests          (252) -> 4989  passes
    lose tools                (89) -> 5152  passes
    lose autobot-npu-worker   (59) -> 5182  passes
    

    The floor detects one failure: losing the tree holding 75% of the files. Its
    docstring says it guards against "a walk that silently stopped reaching most of
    the tree"
    — which is accurate, and "most" is doing the work. A walk that
    stopped reaching five of six trees passes.

    What I would do

    Not "make 47 files shorter". The point is that the decision should be
    visible:

    1. Enumerate without the two real-tree exclusions, and add every file
      already over the ceiling to KNOWN_LARGE at its current size, in both
      mirrors. That converts 47 invisible exemptions into 47 readable ones that
      ratchet down like every other entry. It is a large diff and a boring one,
      which is the correct shape for this.
    2. If autobot-infrastructure/ is genuinely out of scope as deployment tooling
      rather than production code, say so in KNOWN_LARGE's docstring as a
      scope statement
      , not as a prefix in the enumerator. The difference is that
      a reader of the exemption list learns it either way.
    3. Raise MIN_TRACKED_PY_FILES near the in-scope count, or add a per-tree
      presence assertion so losing any tree fails rather than only the largest.

    Item 1 is the re-derivation this issue asks for. Items 2 and 3 are what stop it
    being needed again.

    Method note

    Everything above is a second enumeration, deliberately not reusing the
    detector's own code path — git ls-files "*.py", the same prefix filter applied
    independently, and a line count per file. That is the point of a re-derivation:
    a baseline checked with the instrument that produced it can only ever confirm
    itself.

  4. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Re-derivation 3 of 5 — file-size ratchet: passes, and it is the template

    This is the counter-example the issue needs. The pattern is not inevitable, and this ratchet shows what the fixed version looks like.

    The measurement

    Enumerated git ls-files "*.py" independently, counted lines myself, parsed KNOWN_LARGE out of the source rather than importing it, and compared both directions:

    tracked .py 5,574
    in the walk's declared scope 5,244
    in scope and over 600 lines 497
    KNOWN_LARGE entries 497
    over 600 with no baseline entry 0
    baseline entries now ≤ 600 (stale) 0

    Exact congruence, both directions. No blind spot inside the scope, and no stale entries either — so this one satisfies both the staleness and the blindness direction, which none of the others do.

    Why it passes, and it is not luck

    The blindness was here, was severe, and was found. check_python_file_size.py:34-40 records it:

    Before this, audit_ceilings iterated KNOWN_LARGE.items(), so it could re-verify a file someone remembered to list, never discover one nobody did. A repo-wide walk found 509 files already over MAX_LINES with no entry at all — no audit could ever have caught them, because none of them had ever been added.

    That is this issue's thesis, in the repository's own words, discovered independently at #14547 and fixed by exactly the remedy proposed here: derive the population from the tree instead of from the list. autobot-slm-backend/services/reconciler.py surfaced it at over 2,000 lines and absent from the list entirely.

    So the fix is not hypothetical and its cost is known: grandfathering 509 files at measured size, then ratcheting.

    The refinement this forces on the issue

    47 .py files over 600 lines sit outside the walk — autobot-infrastructure/ (330 files excluded), autobot-backend/code_analysis/, .worktrees/. The largest is 2,341 lines.

    That is not blindness, and the difference is the useful part. It is:

    1. declared — EXCLUDED_PREFIXES at :99-103, three literal prefixes;
    2. justified — they mirror the hook's own exclude: in .pre-commit-config.yaml, so the audit's scope and the staged-file scope are the same set (file-size guard: several autobot-slm-backend/services/*.py exceed 600 lines with no grandfather entry #14547);
    3. guarded — test_excluded_prefixes_mirror_the_pre_commit_config fails if the two drift apart.

    So a reader can discover the boundary by reading it, and the boundary cannot move silently.

    That sharpens the issue's claim. The defect is not "the baseline's population is narrower than the tree's" — every guard is scoped. It is "the boundary is invisible, so absence and cleanliness are indistinguishable." Compare:

    boundary recorded? guarded?
    file-size yes, EXCLUDED_PREFIXES yes, mirrors the hook config
    hardcoded-values HV_SCAN_EXTENSIONS exists, but nothing says systemd units are therefore unexamined no
    inline_generics no — the count encodes the comment defect invisibly no

    I'd amend AC 4 to read: the population boundary is stated where a reader meets the baseline, and a test fails when the boundary moves. Exact congruence is the outcome; a declared, guarded boundary is the mechanism.

    Whether autobot-infrastructure/ should stay excluded is a separate question worth someone's decision — 47 files over the limit, one at 2,341 lines, in a tree the ratchet does not constrain at all — but it is a scope question, not a defect, and it should not ride on this issue.

    One correction, recorded because it is this issue's own failure mode

    My first run reported 497 over-limit files with no baseline entry — a catastrophic-looking finding. It was my bug: KNOWN_LARGE is annotated (KNOWN_LARGE: dict[str, int] = {...}), so it parses as ast.AnnAssign, and I only handled ast.Assign. My parse returned zero entries, so every file read as missing.

    The tell was the number: 497 missing and 497 over-limit are the same number, which is what "compared against an empty set" looks like. A re-derivation is an instrument too, and mine needed the same non-vacuity check I have been asking of everyone else's — a floor asserting the baseline parsed to more than zero entries would have caught it before I looked at the result at all.

    Next: re-derivation 4, the commit-trailer baseline.

  5. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Re-derivation 4 of 5 — commit-trailer baseline: 4 entries, 7,087 commits

    The largest gap of the four, and the one where the remedy is not to close it.

    The measurement

    Replicated the gate's own logic against full history rather than a PR range — agent_re, coauthor_re, and both exemptions (owner_re, bot_ident_re) transcribed from .github/workflows/no-commit-trailers.yml:79-95, applied line-anchored:

    commits on Dev_new_gui 12,786
    agent-trailer violations 881
    co-author violations, after owner and bot exemptions 6,206
    total commits the gate's rules would fail 7,087
    baseline exemptions 4
    violations not baselined 7,083
    baselined but not violating 0

    The identities are the banned forms, not false positives — Co-Authored-By: Claude (2,024), Claude Sonnet 4.6 (1,550 + 1,285), Claude Opus 4.5 (1,481), Paperclip (1,239 + 791). github-actions[bot] (757) and dependabot[bot] (470) are correctly exempted by bot_ident_re and are excluded from the 6,206.

    Why this is not a bug report

    The gate is right and should not change. It runs on pull_request and iterates git rev-list "${BASE_SHA}..${HEAD_SHA}" — new commits only. A forward ratchet is the correct design, and the alternative, rewriting 12,786 commits, is worse than the problem.

    So the 7,083 are not outstanding work. The finding is what the number 4 communicates.

    The invisible boundary, in its clearest form

    The baseline's own header:

    Commits exempted from the no-commit-trailers gate — a RATCHET, not an allowlist. Entries may only be REMOVED, never added.

    A reader meets a four-entry file, described as a ratchet that only shrinks, and takes away that the repository has four outstanding trailer violations and is nearly clean. The true reading is: four commits tripped the gate while it was running and were exempted rather than fixed. It is a count of gate events, not of tree state, and the two differ by three orders of magnitude.

    The workflow header says "Fails a PR whose commits carry…", so the mechanism is stated. What is not stated anywhere is the consequence — that history was never examined, and that the baseline's size is therefore unrelated to how much of the tree carries banned trailers.

    This is re-derivation 3's distinction at its widest. Compare:

    ratchet boundary stated? baseline vs population
    file-size EXCLUDED_PREFIXES, guarded by a mirror test yes 497 vs 497
    commit-trailer history is unexamined; only PR ranges are no 4 vs 7,087
    hardcoded-values HV_SCAN_EXTENSIONS; systemd units unreachable partly congruent-by-construction
    inline_generics none; the counter's comment defect is encoded in the number no 577 vs 573

    What I'd actually change

    Nothing in the gate. One paragraph in the baseline file, saying what the count is a count of:

    This file records commits that tripped this gate and were exempted. It is not an inventory of trailer-carrying commits in the repository — the gate examines only the commits a PR adds, so history predating it was never examined and is not represented here. A small number here means few exemptions were granted, not that the tree is clean.

    That is the cheapest possible instance of the AC 4 amendment from re-derivation 3: state the boundary where a reader meets the baseline. Here it costs one comment block and converts a badly misleading number into an honestly narrow one.

    Method note

    My first attempt reported 3,444 and 8,010 — both wrong. I had flattened the log with tr -d '\n' before matching, which destroyed the ^[[:space:]]* anchoring the gate's regexes depend on, and I had not applied the owner or bot exemptions at all. The numbers were large and plausible and I nearly wrote them up. A re-derivation that does not replicate the original's exemptions is not measuring the same predicate — it is answering a looser question and reporting the answer as if it were the same one.


    That completes the five ratchets — file-size, hardcoded-values, inline_generics, commit-trailer, plus the #15896 citations that started this. Four of the five have a boundary a reader cannot see; the one that passes is the one that declares and guards it.

  6. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    ACs 4 and 5 — the doc, ready to commit verbatim

    Path: docs/developer/RATCHET_BASELINES.md (new file, 174 lines).

    Not folded into CANONICAL_RULES.md: that file is a catalogue of canonical_check*.py rules with a fixed per-rule schema, and this is a convention rather than a rule with a RULE_ID.

    One line for the trigger table in CLAUDE.md, so authors meet it where AC 4 requires:

    | Adding a ratchet, or freezing/regenerating a baseline | [`RATCHET_BASELINES.md`](docs/developer/RATCHET_BASELINES.md) |
    

    AC 4 as amended (per re-derivation 3): the criterion is not exact congruence — every guard is scoped and a narrow scope is not the defect. It is that the boundary is stated where a reader meets the baseline, and a test fails when the boundary moves. Rules 1 and 2 carry that.

    AC 5 is served by two worked examples rather than one: file-size at 497 vs 497 as the case that passes, and #15896's 8 vs 26 as the failure. The counter-example is the load-bearing half — a rule with only failures reads as "be careful"; a rule with a passing instance shows the shape to copy.

    Rules 5 and 6 are the two failures I hit while doing this work, kept because they are the ones most likely to catch whoever follows the page.


    # Ratchet Baselines
    
    > **Tracking:** [#15897](https://github.com/mrveiss/AutoBot-AI/issues/15897)
    
    A ratchet freezes a baseline that its own detector produced. At the moment of
    freezing, that baseline cannot distinguish **"the tree contains N instances"**
    from **"the detector can see N instances"** — and nothing in the pattern ever
    forces the difference to surface.
    
    Five baselines were re-derived by implementations sharing neither enumeration nor
    matching with their detectors. **Four had a boundary a reader cannot see. The one
    that passes is the one that declares and guards it.**
    
    ## What goes wrong
    
    Whatever the detector is blind to becomes permanently exempt **without appearing
    in the baseline** — the list whose entire purpose is to record exemptions. An
    exemption you can read is a decision. An exemption caused by a blind spot is
    invisible, and the ratchet reports clean forever.
    
    There are two independent directions, and only one of them is checkable with the
    detector alone. That is why only one of them ever gets built:
    
    | Direction | Question | Checkable by the detector? |
    |---|---|---|
    | **Staleness** | Does every baseline entry still match something? | Yes — hence `--audit-baseline`, and #15896's `test_the_baseline_still_describes_real_citations` |
    | **Blindness** | Does the detector's population equal the tree's? | **No, by definition** — a census performed by the instrument inherits the instrument's blind spot |
    
    ## The rules
    
    ### 1. State the boundary where a reader meets the baseline
    
    Every guard is scoped, and a narrow scope is not the defect. **An undeclared
    scope is.** A file type never opened is indistinguishable, in the baseline, from
    one opened and found clean.
    
    Write the boundary into the baseline file or the detector's docstring, in the
    place someone lands when they read the number.
    
    ### 2. A test must fail when the boundary moves
    
    A stated boundary that drifts silently is a stale comment. `check_python_file_size.py`
    declares `EXCLUDED_PREFIXES` **and** pins it with
    `test_excluded_prefixes_mirror_the_pre_commit_config`, which fails if the audit's
    scope and the hook's `exclude:` diverge. That pairing is what makes the exclusion
    a decision rather than an accident.
    
    ### 3. Derive the population a second way before freezing it
    
    Once, at freeze time, and again whenever the matcher changes — not a permanently
    maintained second implementation. A change to *what the detector matches*
    invalidates the cross-check; a change to formatting or messaging does not.
    
    Do **not** build a shared harness for this. A generic harness reintroduces the
    shared implementation this whole page is about. The second derivation has to be
    ad hoc by construction; independence is the property doing the work.
    
    ### 4. Compare sets, not counts
    
    Two implementations can agree on a total and disagree on membership. #15896's
    cross-check compared the sets and found the symmetric difference empty — that is
    the assertion worth making, not `26 = 26`.
    
    ### 5. The second implementation must replicate the *predicate*, exemptions included
    
    This is the rule most likely to catch whoever follows this page, because it was
    found by hitting it.
    
    A re-derivation of the commit-trailer gate first reported **3,444** and **8,010**
    violations. Both were wrong: the log had been flattened with `tr -d '\n'` before
    matching, destroying the `^[[:space:]]*` anchoring the gate's regexes depend on,
    and neither the owner nor the bot exemption had been applied. The numbers were
    large, plausible, and nearly published.
    
    **A re-derivation that drops the original's anchoring or exemptions is not
    measuring the same predicate.** It answers a looser question and reports the
    answer as though it were the same one.
    
    ### 6. Give the re-derivation its own non-vacuity floor
    
    The instrument checking the instrument is an instrument. A re-derivation of the
    file-size ratchet first reported **497 over-limit files with no baseline entry** —
    a catastrophic-looking finding produced by parsing `KNOWN_LARGE` as an
    `ast.Assign` when it is annotated and therefore an `ast.AnnAssign`. The parse
    returned zero entries, so every file read as missing.
    
    The tell was the number: 497 missing and 497 over-limit are the same number,
    which is what comparing against an empty set looks like. A floor asserting the
    baseline parsed to more than zero entries would have caught it before the result
    was ever read.
    
    ## Worked example — the one that passes
    
    `repo_tests/python_file_size_ratchet_baseline.py`, measured against
    `origin/Dev_new_gui`:
    
    | | |
    |---|---|
    | tracked `.py` | 5,574 |
    | in the walk's declared scope | 5,244 |
    | in scope and over `MAX_LINES` (600) | **497** |
    | `KNOWN_LARGE` entries | **497** |
    | over the limit with no entry | **0** |
    | entries whose file is now compliant | **0** |
    
    Exact congruence in both directions — the only one of the five satisfying
    staleness *and* blindness.
    
    It is not luck. The blindness was here, was severe, and was found:
    `scripts/check_python_file_size.py:34-40` records that `audit_ceilings` used to
    iterate `KNOWN_LARGE.items()`, so it *"could re-verify a file someone remembered
    to list, never discover one nobody did"* — and a repo-wide walk then found **509
    files already over the limit with no entry at all**. `reconciler.py` surfaced it
    at over 2,000 lines, absent from the list entirely.
    
    47 `.py` files over the limit still sit outside the walk. That is **not**
    blindness, and the difference is the whole point: the exclusion is declared
    (`EXCLUDED_PREFIXES`), justified (it mirrors the hook's own `exclude:`), and
    guarded (a test fails if the two drift). A reader can discover the boundary by
    reading it, and the boundary cannot move silently.
    
    ## Worked example — the shape of the failure
    
    **#15896, citation guard: baseline 8, true population 26.**
    
    The scan matched lines starting with `#`, or — for Python — lines *containing* a
    triple quote. That matches the line opening or closing a docstring, never a line
    *inside* one, and in this codebase the citations live in docstring bodies. Across
    246 `repo_tests/*.py` there were 35 occurrences; the filter saw 4.
    
    The PR's justification for its rule was *"it costs nothing, because the form is
    already rare — 8 instances in 464 files."* **That 8 was produced by the same
    filter**, so it counted what the filter could see. Freezing it would have made 18
    citations permanently exempt with no record that they existed.
    
    It was caught by deriving the population a second way — `git ls-tree` enumeration
    instead of `Path.glob`, `tokenize.STRING` tokens instead of AST docstring spans —
    and comparing the sets. Both implementations now agree at 26 with an empty
    symmetric difference.
    
    ## Evidence across the five
    
    | Ratchet | Boundary | Declared? | Guarded? | Baseline vs population |
    |---|---|---|---|---|
    | file-size | `EXCLUDED_PREFIXES` | yes | yes | **497 vs 497** |
    | citations (#15896) | docstring bodies unreadable by the filter | no | no | 8 vs 26 |
    | commit-trailer | history unexamined; only PR ranges | no | no | 4 vs 7,087 |
    | `inline_generics` | counter matches inside comments (#15771) | no | no | 577 vs 573 |
    | hardcoded-values | `HV_SCAN_EXTENSIONS`; systemd units unreachable (#15903) | partly | no | congruent by construction |
    
    ### Why commit-trailer is the clearest case
    
    Its remedy is *nothing*. The gate is correct — `pull_request`, `rev-list
    BASE..HEAD`, forward-only — and the alternative, rewriting 12,786 commits, is
    worse than the problem. The 7,083 unbaselined violations are not outstanding
    work.
    
    The defect is entirely in what the number **4** communicates. Under a header
    reading *"a RATCHET, not an allowlist… entries may only be REMOVED"*, a reader
    takes four outstanding violations in a nearly-clean tree. It is four gate events.
    The fix is one paragraph saying what the count is a count of.
    
    That case separates **"the boundary is invisible"** from **"the code is wrong"**
    more cleanly than any of the others, and it is why rule 1 is about disclosure
    rather than coverage.
    
    ## Checklist for a new ratchet
    
    - [ ] The population boundary is stated where a reader meets the baseline
    - [ ] A test fails when that boundary moves
    - [ ] The population was derived a second way before freezing, sharing neither enumeration nor matching
    - [ ] That comparison was over **sets**, not counts
    - [ ] The second derivation replicated the predicate — anchoring, exemptions, and all
    - [ ] Both the detector and the re-derivation have non-vacuity floors bound to *what was examined*, never to *what was found*
  7. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Acceptance criteria 4 and 5 are met by PR #15959, merged (d9c8fbd082). Verified
    against origin/Dev_new_gui after the merge.

    AC4 — the convention is written where ratchet authors will meet it, with the
    two-directions table and the compare-sets-not-counts rule.

    docs/developer/RATCHET_BASELINES.md, 257 lines on merged base. Structure:

    :16  ## What goes wrong          (the staleness/blindness table -- both directions)
    :31  ## How to read the numbers on this page
    :51  ## The rules
    :53  1. State the boundary where a reader meets the baseline
    :62  2. A test must fail when the boundary moves
    :70  3. Derive the population a second way before freezing it
    :80  4. Compare sets, not counts
    :86  5. The second implementation must replicate the *predicate*, exemptions included
    :119 6. Give the re-derivation a known positive it must find
    

    CLAUDE.md carries one trigger-table row pointing at it (1 occurrence on base),
    per that file's lean-instructions rule.

    AC5 — a worked example is linked so the next author sees a real 8-vs-26.

    Both are present: #15896's 8-vs-26 as the failure shape, and the file-size
    ratchet at 497-vs-497 as the counter-example. The passing case was the load-bearing
    half — a rule with only failures reads as "be careful"; a rule with a passing
    instance shows the shape to copy.

    AC1–AC3 were satisfied by the five re-derivations recorded on this issue
    before the PR existed, and the deltas are stated there.

    What the review changed, because it bears on the page's own subject

    The document contained the defect it describes, three times, and each was caught
    by a reader rather than by its authors:

    1. Rule 6 was a floor on what was found — "assert the baseline parsed to
      more than zero entries" — contradicting the page's own checklist six lines
      below and failing any ratchet whose population is legitimately empty. Replaced
      with a known-positive control, which is a claim about the instrument and
      holds at any size including zero.
    2. The evidence table claimed five independent re-derivations while one row
      read congruent by construction, which tests nothing about blindness. Now
      four, with the fifth marked outstanding rather than counted as evidence.
    3. The numbers carried no predicate. 2,426 · 7,087 · 497 · 8 · 26 were
      presented without the revision, file set, or matching rule that makes them
      checkable — on a page whose subject is numbers that do not carry their frame.
      ## How to read the numbers on this page now binds each cluster to its
      predicate, and states which clusters are deliberately not comparable.

    A further correction came from a peer who re-derived every figure independently:
    counting the 7,373/5,033 occurrence totals as commits gives 12,408,
    and only the predicate table prevented that being reported as an error in the
    page. The table is doing work, not decoration.

  8. github-actions commented on Sep 7, 2026

    @github-actions
    Contributor

    PR #15959 (merged to Dev_new_gui) references this issue with a close keyword.

    docs(guards): write down the ratchet-baseline convention and its worked examples (#15897)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  9. mrveiss commented on Sep 8, 2026

    @mrveiss
    OwnerAuthor

    #16068 merged — the file-size half is delivered. Staying open for the other half.

    Verified against origin/Dev_new_gui, not the PR diff:
    EXCLUDED_TREE_DEBT = 47 and both tests present in
    repo_tests/excluded_tree_size_debt_test.py.

    What this issue asked for, and what landed

    The thesis was that a ratchet baseline is produced by its own detector, so
    whatever the detector cannot see becomes permanently exempt without appearing
    in the list whose purpose is recording exemptions
    .

    The file-size instance is now measured and pinned: 331 files in
    autobot-infrastructure/ and autobot-backend/code_analysis/, 47 of them
    over the 600-line ceiling
    , largest at 2,341 — none of which could ever appear
    in KNOWN_LARGE, because the enumeration removed them before the mapping was
    asked.

    The design changed once I read the code, and my own recommendation on this
    issue was the thing that was wrong.
    I had proposed adding all 47 to
    KNOWN_LARGE. Two rulings say otherwise: that mapping's docstring states it
    only ever shrinks and no entry may be added to make a file pass, and
    EXCLUDED_PREFIXES is mirrored in .pre-commit-config.yaml and pinned by a
    test — so narrowing it widens the pre-commit hook for every committer, which
    is a scope decision rather than a visibility one.

    So the debt is counted rather than grandfathered. The reviewer's argument for
    that is stronger than mine and belongs here: conflating the two does not merely
    blur a distinction, it moves 47 files from recorded to unrecorded in the
    register that exists to record them
    , which is this issue's own thesis pointed
    at a boundary instead of a detector.

    Still open

    One measurement worth leaving for whoever takes the rest

    The 47 is stable across base movement: none of the five commits on base at merge
    time touched *.py in either excluded tree, and an independent count on current
    base returns 47. The equality pin makes this issue's guard sensitive to base
    changes in those trees even with no file overlap in a diff, which a standard
    overlap check does not catch — worth knowing before the next PR that touches
    either tree.

  10. added this to the Backlog milestone on Sep 12, 2026
  11. mrveiss commented on Sep 12, 2026

    @mrveiss
    OwnerAuthor

    Stale-issue sweep (never auto-closed — Dev_new_gui wasn't GitHub's default branch at merge time). Verified against current origin/main; leaving open.

    The delivering PR (#15959) states in its own body that it delivers only ACs 4-5 (documenting the convention + a worked example) — docs/developer/RATCHET_BASELINES.md:26-30,231-241.

    NOT MET: AC1 (independent re-derivation of all 5 baselines) — the doc itself lists hardcoded-values as "not independently re-derived" (lines 11-13). AC2 (deltas corrected) — frontend_api_contract_ratchet_test.py:80,95 still hardcodes 577/87 despite the doc noting the real deltas (573 vs 577, commit-trailer 4 vs 7,087) as uncorrected. AC3 (re-derived number substituted into the test) — never done; 573 never replaces 577 in the file.

  12. modified the milestones: Backlog, v0.14.0 on Sep 14, 2026
  13. mrveiss commented on Sep 20, 2026

    @mrveiss
    OwnerAuthor

    Re-verification against current origin/main (2026-09-20). Tracked branch issue-15897-ratchet-baselines is already gone from origin.

    Substantial analysis done, corrections partial — docs/developer/RATCHET_BASELINES.md is the real, thorough artifact this issue asked for, tracking itself by number:

    AC1 (independent re-derivation) — 4 of 5 done, 1 not started:

    • file-size: 497 vs 497 (match, confirmed correct).
    • docs(guards): fix eight stale referents and guard the two classes behind them (#15877, #15887) #15896 citations: 8 vs 26 (the original worked example) — corrected, both implementations now agree at 26.
    • commit-trailer: 4 vs 7,087 — re-derived, with the doc candidly narrating two WRONG re-derivation attempts along the way (3,444/8,010 from a flattened-log anchoring bug; 2,426 from a dropped case-insensitive flag) before converging on 7,087. The "4" isn't simply wrong — the boundary column notes the guard only examines unmerged PR ranges, not full history, and that boundary itself is undeclared anywhere.
    • inline_generics: 577 vs 573 — re-derived, confirms the counter is provably defective (bug(ci): the inline_generics ratchet counts comments, so documenting the fix fails the build #15771: counts matches inside comments), matching this issue's own prediction that it was the known-bad one.
    • hardcoded-values: "not independently re-derived", stated plainly in the doc's own summary table — this one hasn't been started.

    AC2 (correct any baseline that differs, state the delta) — partial. The citations baseline (8→26) was corrected. commit-trailer and inline_generics are NOT corrected — both differences are documented with real numbers, but no baseline-file change has landed for either.

    AC3 (inline_generics re-derived with comments stripped, or #15771 fixed first) — not done. #15771 is still OPEN; the 577-vs-573 finding exists but the counter itself hasn't been fixed and the baseline hasn't been regenerated.

    AC4 (convention documented) — done. RATCHET_BASELINES.md states the two-directions rule, the "compare sets, not counts" rule, and a dedicated section ("the second implementation must replicate the predicate, exemptions included") built directly from the commit-trailer near-misses.

    AC5 (worked example linked) — done. The 8-vs-26 citations case is documented in full, including how it was caught.

    2 of 5 ACs fully done (documentation + worked example), the re-derivation analysis itself is 80% complete with real findings, but the corrections that analysis should produce (AC2/AC3) are mostly still pending — inline_generics is blocked on #15771, commit-trailer's undeclared-boundary finding hasn't been resolved into a fix, and hardcoded-values hasn't been touched. Leaving open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions