@event4u/agent-config 14.17.0 → 14.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +207 -77
  5. package/README.md +72 -150
  6. package/dist/agent-src/README.md +1 -1
  7. package/dist/agent-src/commands/agent-handoff.md +2 -2
  8. package/dist/agent-src/commands/pr/merge.md +35 -44
  9. package/dist/agent-src/commands/review/changes.md +19 -19
  10. package/dist/agent-src/commands/review.md +2 -2
  11. package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
  12. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  13. package/dist/agent-src/rules/git-history-discipline.md +2 -2
  14. package/dist/agent-src/rules/guidelines.md +1 -1
  15. package/dist/agent-src/rules/session-canary.md +2 -2
  16. package/dist/agent-src/rules/token-efficiency.md +1 -1
  17. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
  18. package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
  19. package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
  20. package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
  21. package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
  22. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
  23. package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
  24. package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
  25. package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
  26. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
  27. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
  28. package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
  29. package/dist/agent-src/skills/design-review/SKILL.md +3 -1
  30. package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
  31. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
  32. package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
  33. package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
  34. package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
  35. package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
  36. package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
  37. package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
  38. package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
  39. package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
  40. package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
  41. package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
  42. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  43. package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  45. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
  46. package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
  47. package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
  48. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
  49. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
  50. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
  51. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  52. package/dist/cli/commands/recordTriggerEval.js +2 -2
  53. package/dist/cli/main.js +1 -1
  54. package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
  55. package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
  56. package/dist/cli-delegate/cmd_adr_effective.js +318 -81
  57. package/dist/cli-delegate/cmd_conformance.js +749 -669
  58. package/dist/cli-delegate/cmd_doctor.js +1 -1
  59. package/dist/cli-delegate/cmd_export.js +1 -1
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +1 -1
  62. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  63. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +1 -1
  65. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  66. package/dist/cli-delegate/cmd_validate.js +1 -1
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +39 -38
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +1 -1
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +2 -2
  74. package/dist/discovery/trust-report.md +1 -1
  75. package/dist/discovery/workspaces.json +2 -2
  76. package/dist/hooks/dispatch.js +2764 -2439
  77. package/dist/install/install.mjs +588 -527
  78. package/dist/mcp/registry-manifest.json +2 -2
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +1381 -1138
  81. package/dist/scripts/_lib/model_tier.js +1 -1
  82. package/dist/shared/settingsConsent.js +1 -1
  83. package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
  84. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  85. package/docs/guidelines/design-antipatterns.md +87 -64
  86. package/package.json +1 -1
  87. package/src/agent-src/README.md +1 -1
  88. package/src/config/ci-local-parity.yml +6 -0
  89. package/src/config/conformance-claim-baseline.json +9 -0
  90. package/src/config/gate-coverage.yml +170 -0
  91. package/src/config/gate-violation-baselines.json +4 -4
  92. package/src/config/release-gate-locality.yml +141 -0
  93. package/src/scripts/_cli/cmd_doctor.ts +2 -2
  94. package/src/scripts/_cli/cmd_handoff.ts +1 -1
  95. package/src/scripts/_cli/cmd_update.ts +1 -1
  96. package/src/scripts/_cli/cmd_versions.ts +1 -1
  97. package/src/scripts/_lib/adr_frontmatter.ts +44 -0
  98. package/src/scripts/_lib/consequence_objects.ts +152 -0
  99. package/src/scripts/_lib/exec_evidence.ts +7 -0
  100. package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
  101. package/src/scripts/_lib/host_permission_checks.ts +251 -0
  102. package/src/scripts/_lib/model_tier.ts +1 -1
  103. package/src/scripts/_lib/obligation_frequency.ts +2 -2
  104. package/src/scripts/_lib/release_highlights.ts +265 -1
  105. package/src/scripts/_lib/release_material.ts +129 -0
  106. package/src/scripts/_lib/repeated_run.ts +77 -0
  107. package/src/scripts/_lib/runtime_journal.ts +26 -3
  108. package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
  109. package/src/scripts/_lib/session_register.ts +11 -6
  110. package/src/scripts/_lib/sqlite_schema.ts +22 -0
  111. package/src/scripts/_lib/surface.ts +86 -0
  112. package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
  113. package/src/scripts/_lib/ui_authority.ts +116 -0
  114. package/src/scripts/_lib/unified_diff.ts +121 -0
  115. package/src/scripts/adr/regenerate_index.ts +54 -1
  116. package/src/scripts/adr_cite_check.ts +71 -15
  117. package/src/scripts/ai_council/clients.ts +1 -1
  118. package/src/scripts/autonomy_friction_corpus.ts +229 -0
  119. package/src/scripts/build_proof.ts +15 -2
  120. package/src/scripts/check_beta_review_markers.ts +141 -17
  121. package/src/scripts/check_enforcement_coverage.ts +1 -1
  122. package/src/scripts/check_enforcement_denominator.ts +121 -11
  123. package/src/scripts/check_finding_dispositions.ts +239 -6
  124. package/src/scripts/check_gate_coverage.ts +96 -0
  125. package/src/scripts/check_installed_hooks_fresh.ts +381 -0
  126. package/src/scripts/check_release_highlights.ts +99 -14
  127. package/src/scripts/check_skill_activation_claim.ts +179 -0
  128. package/src/scripts/check_suppression_hygiene.ts +11 -0
  129. package/src/scripts/check_trigger_evals.ts +1 -1
  130. package/src/scripts/code_graph/build.ts +230 -33
  131. package/src/scripts/code_graph/extract.ts +168 -33
  132. package/src/scripts/code_graph/types.ts +37 -2
  133. package/src/scripts/code_graph/validate.ts +12 -1
  134. package/src/scripts/conformance_scan.ts +45 -4
  135. package/src/scripts/detect_silent_catch.ts +403 -0
  136. package/src/scripts/detect_verification_tampering.ts +538 -0
  137. package/src/scripts/explain_run.ts +1 -1
  138. package/src/scripts/generate_capability_matrix.ts +1 -1
  139. package/src/scripts/git_authorization_hook.ts +228 -18
  140. package/src/scripts/hook_manifest.json +1 -1
  141. package/src/scripts/hook_manifest.yaml +47 -30
  142. package/src/scripts/hooks/augment-dispatcher.sh +1 -1
  143. package/src/scripts/hooks/block_no_verify.ts +3 -3
  144. package/src/scripts/hooks/category_a.ts +291 -0
  145. package/src/scripts/hooks/cline-dispatcher.sh +1 -1
  146. package/src/scripts/hooks/concern_registry.ts +2 -2
  147. package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
  148. package/src/scripts/hooks/dispatch_hook.ts +17 -22
  149. package/src/scripts/hooks/evidence_independence.ts +21 -2
  150. package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
  151. package/src/scripts/hooks/host_lowering.ts +215 -0
  152. package/src/scripts/hooks/host_lowering.yaml +185 -0
  153. package/src/scripts/hooks/host_semantics.ts +105 -37
  154. package/src/scripts/hooks/journal_record_hook.ts +10 -0
  155. package/src/scripts/hooks/permission_gate.ts +117 -0
  156. package/src/scripts/hooks/push_settle_hook.ts +250 -0
  157. package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
  158. package/src/scripts/hooks_status.ts +12 -10
  159. package/src/scripts/install-hooks.sh +134 -1
  160. package/src/scripts/install.ts +53 -82
  161. package/src/scripts/install_anthropic_key.sh +2 -2
  162. package/src/scripts/language_mirror_hook.ts +27 -5
  163. package/src/scripts/lint_design_antipattern_parity.ts +80 -0
  164. package/src/scripts/lint_evidence_artifacts.ts +45 -1
  165. package/src/scripts/lint_hook_manifest.ts +87 -3
  166. package/src/scripts/lint_motion_authority_drift.ts +731 -0
  167. package/src/scripts/lint_readme_size.ts +158 -12
  168. package/src/scripts/lint_reasoning_fixtures.ts +216 -0
  169. package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
  170. package/src/scripts/measure_sibling_naming.ts +150 -0
  171. package/src/scripts/redact_hook_capture.ts +1 -1
  172. package/src/scripts/release.ts +31 -143
  173. package/src/scripts/release_drill.ts +71 -1
  174. package/src/scripts/release_publication.ts +243 -3
  175. package/src/scripts/release_verify.ts +220 -0
  176. package/src/scripts/report_skill_activation.ts +319 -7
  177. package/src/scripts/schemas/review-findings.schema.json +6 -1
  178. package/src/scripts/schemas/skill.schema.json +12 -2
  179. package/src/scripts/self_review_gate.ts +10 -1
  180. package/src/scripts/session_canary_hook.ts +1 -1
  181. package/src/scripts/session_register_hook.ts +4 -3
  182. package/src/scripts/trigger_eval_grandfather.json +5 -1
  183. package/src/scripts/validate_agent_settings.ts +1 -1
  184. package/src/shared/settingsConsent.ts +1 -1
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * detect_silent_catch — a newly introduced error swallow, decided from the diff.
4
+ *
5
+ * Prose coverage for this failure already exists in three places —
6
+ * `error-handling-patterns`, `ai-code-blindspots`, and
7
+ * `testing-anti-patterns/process-anti-patterns.md` — and nothing decided it:
8
+ * `ls src/scripts/ | grep -icE 'swallow|bare_except'` returned 0 when this was
9
+ * written. Prose that nothing checks is a preference.
10
+ *
11
+ * SCOPE — the lines the diff ADDS.
12
+ *
13
+ * A pre-existing empty catch is debt this check does not own. The block must
14
+ * be wholly contained in added lines; a block whose body reaches into context
15
+ * lines is skipped. `neg-preexisting-empty-catch` in the corpus pins that — the
16
+ * same empty catch as unchanged context beside one unrelated added line must
17
+ * stay silent. If it ever fires, this has quietly become a whole-tree sweep,
18
+ * which is a different tool with a different false-positive budget.
19
+ *
20
+ * TWO SHAPES, both unambiguous.
21
+ *
22
+ * `catch-empty` no statements at all in the block
23
+ * `catch-discards-error` statements, but the caught value is never used,
24
+ * logged, or re-raised
25
+ *
26
+ * A COMMENT IS NOT A STATEMENT. That is deliberate and it is the roadmap's
27
+ * risk register answered directly: "a `pass # intentional` comment satisfies
28
+ * a naive shape check while changing nothing, so the detector teaches evasion
29
+ * rather than discipline". Comments and `pass` / `...` are stripped before the
30
+ * block is judged, so neither rescues an empty block.
31
+ *
32
+ * NO ESCAPE-HATCH MARKER, deliberately.
33
+ *
34
+ * Every other suppression in this tree is a declared, reasoned marker. This
35
+ * check has none, because it is WARN-ONLY: it blocks nothing, so a marker
36
+ * would buy no unblocking and would be exactly the one-token evasion above,
37
+ * wearing the house convention's clothes. Revisit together with promotion —
38
+ * a blocking version owes a hatch, a warning one does not.
39
+ *
40
+ * WARN-FIRST, and what that means for the exit code.
41
+ *
42
+ * Exit 0 on findings by default: this is a REPORT. `--strict` turns findings
43
+ * into exit 1 and exists so `--self-test` can prove the detection discriminates
44
+ * at all — a report-mode gate cannot be turned red by a fixture, which is the
45
+ * same shape `lint_consolidation_lineage` records in `gate-coverage.yml`.
46
+ *
47
+ * CLI:
48
+ * detect_silent_catch --diff <file> [--strict] [--json]
49
+ * detect_silent_catch --range <base>..<head> # count over merged history
50
+ * detect_silent_catch --corpus | --self-test
51
+ *
52
+ * Exit codes: 0 clean or report-mode findings · 1 findings under `--strict`,
53
+ * or a corpus/self-test failure · 2 usage error or crash.
54
+ */
55
+ import { execFileSync } from 'node:child_process';
56
+ import * as fs from 'node:fs';
57
+ import * as path from 'node:path';
58
+ import process from 'node:process';
59
+ import { fileURLToPath, pathToFileURL } from 'node:url';
60
+
61
+ import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
62
+ import { parseUnifiedDiff, type DiffFile, type DiffLine } from './_lib/unified_diff.js';
63
+
64
+ const _HERE = fileURLToPath(import.meta.url);
65
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
66
+ const CORPUS = path.join(REPO_ROOT, 'tests/fixtures/silent-catch-corpus');
67
+ const SELF_TEST_MIN_CASES = 12;
68
+ const SELF_TEST_MIN_REJECT = 6;
69
+
70
+ export type SilentCatchId = 'catch-empty' | 'catch-discards-error';
71
+
72
+ export interface SilentCatchFinding {
73
+ id: SilentCatchId;
74
+ file: string;
75
+ line: number;
76
+ evidence: string;
77
+ }
78
+
79
+ /** Brace-language opener: `catch (e) {`, `catch {`, `} catch (\Throwable $e) {`. */
80
+ const BRACE_CATCH_RE = /\bcatch\s*(?:\(([^)]*)\))?\s*\{/;
81
+ /** Python opener: `except:` / `except X:` / `except X as e:`. */
82
+ const PY_EXCEPT_RE = /^\s*except\b([^:]*):\s*$/;
83
+
84
+ /** The caught value reached something that observes it. */
85
+ const RETHROW_RE = /\b(?:throw|raise|panic|rethrow)\b/;
86
+ const LOG_RE =
87
+ /\b(?:log|logger|logging|console|Log|Logger|report|captureException|Sentry|trace|warn|error|debug|emit)\b/i;
88
+
89
+ /**
90
+ * Strip a trailing line comment.
91
+ *
92
+ * Load-bearing, and it was missing on the first implementation: without it
93
+ * `pass # intentional` is not equal to `pass`, so the block read as
94
+ * non-empty and the check stayed silent. That is the roadmap's Risk 3
95
+ * verbatim — "a `pass # intentional` comment satisfies a naive shape check
96
+ * while changing nothing" — reproduced by the detector that exists to close
97
+ * it. Caught by its own test, fixed here, pinned by a fixture.
98
+ *
99
+ * Narrow by construction: truncating a line that merely contains `//` inside
100
+ * a string can only matter if what remains is exactly `pass`, `...`, or a
101
+ * brace, and no real statement reduces to those.
102
+ */
103
+ function stripTrailingComment(text: string): string {
104
+ return text.replace(/\s*(?:#|\/\/).*$/, '');
105
+ }
106
+
107
+ /** A line that is only a comment, or only a no-op statement. */
108
+ function isNonStatement(text: string): boolean {
109
+ const t = stripTrailingComment(text).trim();
110
+ if (t === '') return true;
111
+ if (t === 'pass' || t === '...' || t === '{' || t === '}' || t === '};') return true;
112
+ if (t.startsWith('//') || t.startsWith('#') || t.startsWith('/*') || t.startsWith('*')) return true;
113
+ return false;
114
+ }
115
+
116
+ /** Identifier bound by `catch (e)` / `except X as e` — `null` when unbound. */
117
+ export function boundName(header: string): string | null {
118
+ const m = /(?:^|[\s(])(?:[\w\\.$]+\s+)?\$?([A-Za-z_][A-Za-z0-9_]*)\s*$/.exec(header.trim());
119
+ if (header.trim() === '') return null;
120
+ const asMatch = /\bas\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(header);
121
+ if (asMatch !== null) return asMatch[1] ?? null;
122
+ return m?.[1] ?? null;
123
+ }
124
+
125
+ /**
126
+ * Judge one catch body.
127
+ *
128
+ * `null` when the body is fine. Comments and `pass` are already excluded by
129
+ * {@link isNonStatement}, so an "explanatory" comment cannot make an empty
130
+ * block non-empty.
131
+ */
132
+ export function judgeBody(bound: string | null, body: readonly string[]): SilentCatchId | null {
133
+ const statements = body.filter((l) => !isNonStatement(l));
134
+ if (statements.length === 0) return 'catch-empty';
135
+ const joined = statements.join('\n');
136
+ if (RETHROW_RE.test(joined)) return null;
137
+ if (LOG_RE.test(joined)) return null;
138
+ if (bound !== null && new RegExp(`\\b${bound}\\b`).test(joined)) return null;
139
+ if (bound === null) return null; // unbound catch with real statements: not decidable by shape
140
+ return 'catch-discards-error';
141
+ }
142
+
143
+ interface AddedBlock {
144
+ lines: DiffLine[];
145
+ }
146
+
147
+ /** Runs of consecutive added lines — the only region this check reads. */
148
+ function addedRuns(file: DiffFile): AddedBlock[] {
149
+ const runs: AddedBlock[] = [];
150
+ for (const hunk of file.hunks) {
151
+ let current: DiffLine[] = [];
152
+ for (const l of hunk.lines) {
153
+ if (l.side === 'added') {
154
+ current.push(l);
155
+ } else if (current.length > 0) {
156
+ runs.push({ lines: current });
157
+ current = [];
158
+ }
159
+ }
160
+ if (current.length > 0) runs.push({ lines: current });
161
+ }
162
+ return runs;
163
+ }
164
+
165
+ function scanBraceRun(filePath: string, run: readonly DiffLine[]): SilentCatchFinding[] {
166
+ const out: SilentCatchFinding[] = [];
167
+ for (let i = 0; i < run.length; i += 1) {
168
+ const line = run[i];
169
+ if (line === undefined) continue;
170
+ const m = BRACE_CATCH_RE.exec(line.text);
171
+ if (m === null) continue;
172
+ const bound = boundName(m[1] ?? '');
173
+
174
+ // Single-line form: the body is whatever sits between the braces.
175
+ const after = line.text.slice((m.index ?? 0) + m[0].length);
176
+ if (after.includes('}')) {
177
+ const inner = after.slice(0, after.indexOf('}'));
178
+ const verdict = judgeBody(bound, [inner]);
179
+ if (verdict !== null) {
180
+ out.push({ id: verdict, file: filePath, line: line.line, evidence: line.text.trim() });
181
+ }
182
+ continue;
183
+ }
184
+
185
+ // Multi-line form: walk added lines only, tracking brace depth.
186
+ let depth = 1;
187
+ const body: string[] = [];
188
+ let closed = false;
189
+ for (let j = i + 1; j < run.length; j += 1) {
190
+ const inner = run[j];
191
+ if (inner === undefined) break;
192
+ for (const ch of inner.text) {
193
+ if (ch === '{') depth += 1;
194
+ else if (ch === '}') depth -= 1;
195
+ }
196
+ if (depth <= 0) { closed = true; break; }
197
+ body.push(inner.text);
198
+ }
199
+ // Not closed inside the added run → the block reaches pre-existing
200
+ // lines and is out of scope. Silence is the correct answer.
201
+ if (!closed) continue;
202
+ const verdict = judgeBody(bound, body);
203
+ if (verdict !== null) {
204
+ out.push({ id: verdict, file: filePath, line: line.line, evidence: line.text.trim() });
205
+ }
206
+ }
207
+ return out;
208
+ }
209
+
210
+ function indentOf(text: string): number {
211
+ return text.length - text.replace(/^\s*/, '').length;
212
+ }
213
+
214
+ function scanPythonRun(filePath: string, run: readonly DiffLine[]): SilentCatchFinding[] {
215
+ const out: SilentCatchFinding[] = [];
216
+ for (let i = 0; i < run.length; i += 1) {
217
+ const line = run[i];
218
+ if (line === undefined) continue;
219
+ const m = PY_EXCEPT_RE.exec(line.text);
220
+ if (m === null) continue;
221
+ const bound = boundName(m[1] ?? '');
222
+ const openIndent = indentOf(line.text);
223
+ const body: string[] = [];
224
+ for (let j = i + 1; j < run.length; j += 1) {
225
+ const inner = run[j];
226
+ if (inner === undefined) break;
227
+ if (inner.text.trim() !== '' && indentOf(inner.text) <= openIndent) break;
228
+ body.push(inner.text);
229
+ }
230
+ const verdict = judgeBody(bound, body);
231
+ if (verdict !== null) {
232
+ out.push({ id: verdict, file: filePath, line: line.line, evidence: line.text.trim() });
233
+ }
234
+ }
235
+ return out;
236
+ }
237
+
238
+ const PY_RE = /\.py$/;
239
+
240
+ export function scanDiff(diffText: string): SilentCatchFinding[] {
241
+ const out: SilentCatchFinding[] = [];
242
+ for (const file of parseUnifiedDiff(diffText)) {
243
+ if (file.deleted) continue;
244
+ for (const run of addedRuns(file)) {
245
+ out.push(
246
+ ...(PY_RE.test(file.path)
247
+ ? scanPythonRun(file.path, run.lines)
248
+ : scanBraceRun(file.path, run.lines)),
249
+ );
250
+ }
251
+ }
252
+ return out;
253
+ }
254
+
255
+ export function render(findings: readonly SilentCatchFinding[], strict: boolean): string {
256
+ if (findings.length === 0) return 'no newly introduced silent catch in this diff';
257
+ const lines = findings.map(
258
+ (f) => `${strict ? 'ERROR' : 'WARN '} ${f.id} ${f.file}:${String(f.line)} ${f.evidence}`,
259
+ );
260
+ lines.push(
261
+ `\n${String(findings.length)} finding(s). Warn-first: this check blocks nothing until its ` +
262
+ 'false-positive rate over merged history is measured and written down.',
263
+ );
264
+ return lines.join('\n');
265
+ }
266
+
267
+ /* ----------------------------------------------------------------- corpus */
268
+
269
+ interface Fixture {
270
+ id: string;
271
+ diff: string;
272
+ kind: 'positive' | 'negative';
273
+ expect: string[];
274
+ why: string;
275
+ }
276
+
277
+ export function loadCorpus(root: string = CORPUS): Fixture[] {
278
+ const raw = fs.readFileSync(path.join(root, 'manifest.json'), 'utf-8');
279
+ return (JSON.parse(raw) as { fixtures: Fixture[] }).fixtures;
280
+ }
281
+
282
+ function runCorpus(write: (s: string) => void): number {
283
+ const fixtures = loadCorpus();
284
+ let failed = 0;
285
+ for (const f of fixtures) {
286
+ const got = scanDiff(fs.readFileSync(path.join(CORPUS, f.diff), 'utf-8'))
287
+ .map((x) => x.id)
288
+ .sort();
289
+ const want = [...f.expect].sort();
290
+ const ok = JSON.stringify(got) === JSON.stringify(want);
291
+ if (!ok) failed += 1;
292
+ write(
293
+ `${ok ? 'OK ' : 'FAIL'} ${f.kind.padEnd(8)} ${f.id.padEnd(32)} ` +
294
+ `expect [${want.join(', ')}] got [${got.join(', ')}]\n`,
295
+ );
296
+ }
297
+ write(`\nscanned: ${String(fixtures.length)}\n`);
298
+ write(`${String(fixtures.length - failed)}/${String(fixtures.length)} fixtures behaved\n`);
299
+ return failed > 0 ? 1 : 0;
300
+ }
301
+
302
+ /* ------------------------------------------------------------------ range */
303
+
304
+ /**
305
+ * Count findings over a recorded commit range — the baseline step 3.2 requires
306
+ * before promotion is discussed at all.
307
+ */
308
+ function runRange(range: string, write: (s: string) => void): number {
309
+ const revs = execFileSync('git', ['rev-list', '--no-merges', range], {
310
+ cwd: REPO_ROOT,
311
+ encoding: 'utf-8',
312
+ maxBuffer: 256 * 1024 * 1024,
313
+ })
314
+ .split('\n')
315
+ .filter((l) => l.trim() !== '');
316
+ let total = 0;
317
+ const perCommit: Array<{ sha: string; n: number }> = [];
318
+ for (const sha of revs) {
319
+ const diff = execFileSync('git', ['show', '--format=', '--unified=3', sha], {
320
+ cwd: REPO_ROOT,
321
+ encoding: 'utf-8',
322
+ maxBuffer: 256 * 1024 * 1024,
323
+ });
324
+ const n = scanDiff(diff).length;
325
+ total += n;
326
+ if (n > 0) perCommit.push({ sha: sha.slice(0, 9), n });
327
+ }
328
+ for (const c of perCommit) write(`${c.sha} ${String(c.n)} finding(s)\n`);
329
+ write(`\nrange: ${range}\n`);
330
+ write(`scanned: ${String(revs.length)}\n`);
331
+ write(`commits with at least one finding: ${String(perCommit.length)}\n`);
332
+ write(`total findings: ${String(total)}\n`);
333
+ return 0;
334
+ }
335
+
336
+ /* -------------------------------------------------------------- self-test */
337
+
338
+ function selfTest(): number {
339
+ const rel = 'src/scripts/detect_silent_catch.ts';
340
+ const cases: SelfTestCase[] = loadCorpus().map((f) => ({
341
+ name: `${f.kind}/${f.id}`,
342
+ expect: f.kind === 'positive' ? 'reject' : 'accept',
343
+ // `--strict` is what makes a report-mode check provable: without it a
344
+ // planted violation could never turn it red, and a check that cannot
345
+ // go red proves nothing.
346
+ run: () => runGateCli(REPO_ROOT, rel, ['--diff', path.join(CORPUS, f.diff), '--strict'], REPO_ROOT),
347
+ }));
348
+ return runSelfTest({
349
+ gate: 'detect_silent_catch',
350
+ cases,
351
+ minCases: SELF_TEST_MIN_CASES,
352
+ minRejectCases: SELF_TEST_MIN_REJECT,
353
+ });
354
+ }
355
+
356
+ /* ------------------------------------------------------------------- main */
357
+
358
+ function usage(): never {
359
+ process.stderr.write(
360
+ 'usage: detect_silent_catch --diff <file> [--strict] [--json]\n' +
361
+ ' detect_silent_catch --range <base>..<head>\n' +
362
+ ' detect_silent_catch --corpus | --self-test\n',
363
+ );
364
+ process.exit(2);
365
+ }
366
+
367
+ export function main(argv: readonly string[]): number {
368
+ if (argv.includes('--self-test')) return selfTest();
369
+ if (argv.includes('--corpus')) return runCorpus((s) => process.stdout.write(s));
370
+
371
+ let diffPath: string | null = null;
372
+ let range: string | null = null;
373
+ let strict = false;
374
+ let json = false;
375
+ for (let i = 0; i < argv.length; i += 1) {
376
+ const a = argv[i];
377
+ if (a === '--diff') { diffPath = argv[++i] ?? null; }
378
+ else if (a === '--range') { range = argv[++i] ?? null; }
379
+ else if (a === '--strict') { strict = true; }
380
+ else if (a === '--json') { json = true; }
381
+ else usage();
382
+ }
383
+ if (range !== null) return runRange(range, (s) => process.stdout.write(s));
384
+ if (diffPath === null) usage();
385
+
386
+ const findings = scanDiff(fs.readFileSync(diffPath, 'utf-8'));
387
+ process.stdout.write(
388
+ (json ? JSON.stringify(findings, null, 2) : render(findings, strict)) + '\n',
389
+ );
390
+ return strict && findings.length > 0 ? 1 : 0;
391
+ }
392
+
393
+ const invokedDirectly =
394
+ process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url;
395
+ if (invokedDirectly) {
396
+ try {
397
+ process.exit(main(process.argv.slice(2)));
398
+ } catch (err) {
399
+ // Never swallowed — this file's whole subject is the swallow.
400
+ process.stderr.write(`detect_silent_catch crashed: ${String(err)}\n`);
401
+ process.exit(2);
402
+ }
403
+ }