rouse-context/scripts/lib
monkopedia-coder 8cbcc40eb1
Gate: a broad catch must be preceded by a cancellation clause (#674) (#720)
* Gate: a broad catch must be preceded by a cancellation clause (#674)

`CancellationException` extends `IllegalStateException` on the JVM, so all
four of `Exception`, `Throwable`, `RuntimeException` and
`IllegalStateException` receive it. Source order within one try/catch chain
is the only thing that decides delivery, and the Kotlin compiler diagnoses
neither an unreachable nor a mis-ordered clause -- which is why six
human-directed sweeps in two days (#660, #662/#665, #666/#669, #670/#673,
#638/#672, #667) each found what the previous one had not looked at.

The gate:

  scripts/lib/kotlin-catch-chains.awk   per-file scanner
  scripts/check-cancellation-catch-order.sh
  scripts/tests/check-cancellation-catch-order-test.sh
  scripts/cancellation-catch-allowlist.tsv   reasoned exemptions (1 entry)
  scripts/cancellation-catch-backlog.tsv     known-unfixed defects (60)

A scanner rather than a regex, because the property is order WITHIN ONE
CHAIN and neither half survives a line match: this tree has a four-line
clause (McpTool.kt:170) no line regex parses, and adjacent chains a line
regex would credit to each other. Comments and string literals are blanked
in place, then tokens are walked tracking brace depth.

RED ON A KNOWN-BAD HISTORICAL TREE
----------------------------------
Run against 9b50f469, the commit before #660: exit 1, 87 findings. Run
against 8479cf56 (#660 applied): exit 1, 75 findings. The 12 that
disappear are exactly the twelve #660's own commit message lists, file for
file and count for count:

    CertProvisioningFlow.kt   2 -> 1
    CtLogMonitor.kt           3 -> 1     (check x2)
    KtorWebSocketFactory.kt   1 -> 0
    MuxDemux.kt               1 -> 0     (sendPingAwaitPong)
    OnboardingFlow.kt         1 -> 0
    RelayApiClient.kt         2 -> 1     (executeRequest)
    SelfCertVerifier.kt       3 -> 0
    TunnelClientImpl.kt       2 -> 0     (runKeepaliveLoop, healthCheck)
                                  net -12

The positive fires on the defect and the negative goes quiet on the fix.
A reduced form of the pre-#660 CtLogMonitor shape is in the self-test, so
that pairing is pinned in CI rather than observed once.

TWO CONTROLS, EVERYWHERE
------------------------
Every rule is asserted in both directions -- an empty pattern matches
everything and a filtered search reaches nothing, and both look clean. All
four broad types, three narrow types, all three spellings of the guard,
guard-after-broad, a guard on a neighbouring try, a guard earlier in the
same chain with a clause between, the multi-line annotated clause, prose in
a KDoc, a guard quoted in a string literal, `Flow.catch { }`.

Part 6 mutates the gate itself: a copy that recognises no broad type, and a
copy that ignores the cancellation guard. Each must make the suite go red
and SAY so. Without those, an `expect 1` proves only that something went
red and an `expect 0` only that something was quiet.

The gate also refuses to report clean for a tree in which it parsed zero
catch clauses: a scanner that reaches nothing and a tree that has nothing
print the same empty result.

WHAT IT FINDS ON MAIN
---------------------
61 violations across 54 <path, function> keys, from 177 catch clauses in
278 files across 12 production source sets. One is exempt with a written
reason (IntegrationSetupViewModel.pushIntegrationSecrets -- no suspension
point in the try, pinned by IntegrationSecretsPersistCancellationTest).
The other 60 are recorded in the backlog file, which is not an allowlist:
they are defects, they cite #667, and the gate prints the outstanding count
on every run rather than reporting the tree as clean. None of them is fixed
here; the gate is the deliverable.

Both files are exact-match ratchets in BOTH directions, so fixing a site
fails the gate until its entry is lowered or removed. That is what stops
either file becoming the place things go to be forgotten, and it is
asserted by the self-test.

Two scanner defects found and fixed while measuring, both of which had
mis-attributed real findings: an ordinary `"` string whose `${ ... }`
template spans lines (TestRelayFixture.kt:392) was read as code from the
newline on, shifting the depth counter for the rest of the file; and
`fun interface`, brace-less interface members and default parameter values
each disarmed the function-header parser.

Closes #674.

Claude-Session: https://claude.ai/code/session_01YYSPuh9bZEJkrzvnEpY1QK
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Repoint the backlog rows at #722, and say what the citation check does not do

All 53 backlog rows cited #667, which closed on 2026-09-02 -- the day before
this branch's head commit -- and which was scoped to its own six
`app/src/main` sites, never to this list. #674 closes when this PR merges.
So on the day this landed, 60 recorded defects would have cited two closed
issues and no open issue would have tracked them: invisible to any
open-state triage, in a file whose header says its purpose is not to become
the place things go to be forgotten.

#722 is the open tracker for this debt, and it records the triage rule
(reachability before severity), so the rows are not read as 60 mechanical
edits. Every row now cites it.

Also removes the two remaining places #667 was used as a POINTER rather
than as history: the allowlist entry's reason, which now stands on the
in-tree comment and `IntegrationSecretsPersistCancellationTest` alone, and
the gate's developer-facing failure text. The remaining mentions are
factual provenance -- the list of six sweeps, and which issue established
that both spellings of `CancellationException` are live -- and the sweep
list now says outright that all six are closed and that #722 is the tracker.

The gate itself cannot catch this class, and now says so at length rather
than implying otherwise. `read_ledger` matches `#[0-9]+` and never asks
whether the number resolves to anything, let alone to anything open;
`#999999` passes. Resolving it means a GitHub API call, and this gate runs
from a plain checkout with no token, offline on a developer machine, and
inside the throwaway repo its own self-test builds -- which has no remote.
A network check would have to fail closed in all three (a gate nobody can
run) or skip silently (a gate that reports clean without having looked,
which is the exact failure mode this file exists to prevent). So the check
is not made, the gap is written down, and the reviewer of a citation is a
human. The related ceiling problem is noted as #721 and is not fixed here.

No change to the gate's logic, the scanner, the self-test's assertions or
any production Kotlin. The self-test's two ledger fixtures move to #722 for
consistency; they only ever exercised the `#[0-9]+` shape.

Closes #674.

Claude-Session: https://claude.ai/code/session_01YYSPuh9bZEJkrzvnEpY1QK
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Monkopedia <monkopedia@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 06:59:24 -04:00
..
gate-filters.sh Share one anchoring helper across the grep gates (#597) (#697) 2026-09-03 00:31:25 -04:00
kotlin-catch-chains.awk Gate: a broad catch must be preceded by a cancellation clause (#674) (#720) 2026-09-03 06:59:24 -04:00