Skip to content

fix(lint): lesson-fail-open-catch-ban — // totem-context: escape does not suppress in diff-scoped otem lint --base main (pushes authors to matcher-evasion) #2214

Description

@satur8d

Surface

The Tenet-4 rule lesson-fail-open-catch-ban (ast-grep kind: catch_clause with no throw_statement) has a sanctioned inline escape: // totem-context: <reason>. The escape does NOT suppress in the diff-scoped branch-lint path (totem lint --base main), even when the directive is correctly positioned (immediately above / inline on the catch line — the position #1889 establishes as the one that works in full-tree lint).

Effect: an author who adds a legitimately fail-soft catch at an IO/LLM boundary, annotates it with the sanctioned // totem-context: escape, and runs the branch-lint gate, still sees the rule FIRE. The escape works full-tree but not diff-scoped. This pushes authors toward matcher-evasion (rewriting catch (e) {} as .catch(() => …) so it parses as a call_expression, not a catch_clause) — strictly worse for the corpus than a recognized, attested escape. It hits every live IO/LLM adapter cohort-wide (the ADR-111 5b live-adapter family is the live exhibit).

Distinct from #1889

#1889 is the position axis: isSuppressed (packages/core/src/rule-engine.ts) checks only the matched line + the immediately-preceding line, so a directive above try (multi-line body) misses the catch line in full-tree lint. This issue is the diff-scope axis: a correctly positioned directive is honored full-tree but not under totem lint --base main. Same rule + same escape, different failure surface. Sibling to #1614 (terminal-handler false-positive) and the #1444 fail-open cluster.

Diff-anchoring nuance (for the fix investigation)

Observed across ADR-111 slices 2 + 5b-ii (totem-claude journals claude-0115/0116): the suppression appears diff-addition-anchored — it can read as un-suppressed on a dirty/uncommitted tree and pass once the change is committed, yet was re-confirmed still firing in the committed --base main diff-scoped path during 5b-ii. The grandfathered extract-shared.ts:tryParseJson bare catch only passes because it is not in the diff. So the fix needs to pin down exactly how the diff-scoped lint maps suppression directives onto changed-line ranges — likely the directive line falls outside the diff hunk window the rule matched within, so isSuppressed's two-line check never sees it. First step: extend the #1889 fixture (packages/cli/src/_tenet4_verify.ts) with a diff-scoped --base main case to lock the exact trigger before touching the engine.

Second half — recognized fail-soft carve-out (strategy concurrence)

Per the Tenet-4 fault-line ruling (strategy-claude, 2026-06-20; doctrine half tracked mmnto-ai/totem-strategy#702), the durable principle is fail-soft on EXPECTED operational failures, fail-loud on UNEXPECTED bugs. Two patterns are canonical:

  • rethrow-unexpected (if (!(err instanceof SyntaxError)) throw err; return <fail-soft>) — the rule working; keep it.
  • blanket .catch(() => …) on an all-expected IO/LLM boundary — legitimate only when licensed by a loud global backstop (e.g. ADR-111 fold-C assertPipelineProductive: all-items-failed ⇒ throw).

Today the second pattern passes lint only by syntax evasion (it's a call_expression, not a catch_clause). The fix should let legitimacy be recognized, not dodged: consider a structured annotation (e.g. // totem-context: fail-soft backstop=<assertion>) the rule recognizes for an all-expected boundary guarded by a named loud backstop. strategy will align the #702 doctrine sharpening to whatever this lint fix ships as.

Acceptance

  1. A correctly-positioned // totem-context: escape suppresses identically in full-tree lint AND totem lint --base main diff-scoped lint (fixture-locked, both paths).
  2. The diff-anchoring behavior is characterized + tested (dirty vs committed; in-diff vs grandfathered).
  3. (Optional, paired with Epic: Integrity Audit (Phase 2) #702) a recognized fail-soft carve-out so the all-expected .catch backstop pattern is attested rather than syntax-dodged.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    scope: cliIssues related to the CLI packagescope: coreIssues related to the Core engine packagetype: bugSomething is not working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions