@event4u/agent-config 9.6.0 → 9.8.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 (204) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +207 -942
  3. package/README.md +21 -21
  4. package/dist/agent-src/README.md +2 -2
  5. package/dist/agent-src/commands/commit/in-chunks.md +8 -0
  6. package/dist/agent-src/commands/commit.md +17 -0
  7. package/dist/agent-src/commands/pr/create.md +4 -0
  8. package/dist/agent-src/contexts/override-system.md +75 -5
  9. package/dist/agent-src/rules/context-hygiene.md +2 -0
  10. package/dist/agent-src/rules/doc-screenshot-hygiene.md +97 -0
  11. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  12. package/dist/agent-src/rules/downstream-changes.md +31 -1
  13. package/dist/agent-src/rules/external-code-graph-interop.md +13 -3
  14. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +2 -0
  15. package/dist/agent-src/rules/git-history-discipline.md +2 -0
  16. package/dist/agent-src/rules/language-and-tone.md +2 -0
  17. package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
  18. package/dist/agent-src/rules/media-governance-routing.md +2 -0
  19. package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
  20. package/dist/agent-src/rules/no-roadmap-references.md +3 -0
  21. package/dist/agent-src/rules/non-destructive-by-default.md +20 -0
  22. package/dist/agent-src/rules/onboarding-gate.md +2 -0
  23. package/dist/agent-src/rules/output-discipline.md +2 -0
  24. package/dist/agent-src/rules/persona-governance.md +2 -0
  25. package/dist/agent-src/rules/preservation-guard.md +3 -0
  26. package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
  27. package/dist/agent-src/rules/secret-vcs-guard.md +106 -0
  28. package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
  29. package/dist/agent-src/rules/skill-quality.md +2 -0
  30. package/dist/agent-src/rules/source-confidentiality.md +2 -0
  31. package/dist/agent-src/rules/source-of-truth.md +2 -0
  32. package/dist/agent-src/rules/telegraph-speak.md +2 -0
  33. package/dist/agent-src/rules/token-optimizer-maintenance.md +2 -0
  34. package/dist/agent-src/rules/tool-safety.md +1 -0
  35. package/dist/agent-src/rules/verify-before-complete.md +2 -0
  36. package/dist/agent-src/skills/adr-create/SKILL.md +17 -1
  37. package/dist/agent-src/skills/agent-docs-writing/SKILL.md +34 -16
  38. package/dist/agent-src/skills/agents-md-thin-root/SKILL.md +9 -9
  39. package/dist/agent-src/skills/code-intelligence/SKILL.md +94 -0
  40. package/dist/agent-src/skills/gated-reach/SKILL.md +185 -0
  41. package/dist/agent-src/skills/gated-reach/evals/triggers.json +72 -0
  42. package/dist/agent-src/skills/image-analyser/SKILL.md +14 -12
  43. package/dist/agent-src/skills/image-editing/SKILL.md +1 -0
  44. package/dist/agent-src/skills/readme-writing/SKILL.md +6 -1
  45. package/dist/agent-src/skills/screenshot-hygiene/SKILL.md +171 -0
  46. package/dist/agent-src/skills/secrets-management/SKILL.md +44 -1
  47. package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +29 -0
  48. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge-two-stage.md +29 -0
  49. package/dist/agent-src/skills/subagent-orchestration/prompts/do-and-judge.md +29 -0
  50. package/dist/agent-src/skills/subagent-orchestration/prompts/do-competitively.md +29 -0
  51. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-parallel.md +29 -0
  52. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-steps.md +29 -0
  53. package/dist/agent-src/skills/subagent-orchestration/prompts/do-in-worktrees.md +29 -0
  54. package/dist/agent-src/skills/subagent-orchestration/prompts/judge-with-debate.md +29 -0
  55. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +4 -0
  56. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  57. package/dist/agent-src/templates/github-workflows/doc-impact.yml +101 -0
  58. package/dist/agent-src/templates/github-workflows/secret-scan.yml +65 -0
  59. package/dist/cli/configRoot.js +112 -0
  60. package/dist/cli/configRoot.js.map +1 -0
  61. package/dist/cli/configRoot.test.js +97 -0
  62. package/dist/cli/configRoot.test.js.map +1 -0
  63. package/dist/cli/help.js +4 -1
  64. package/dist/cli/help.js.map +1 -1
  65. package/dist/cli/main.js +19 -2
  66. package/dist/cli/main.js.map +1 -1
  67. package/dist/cli/registry.js +1 -0
  68. package/dist/cli/registry.js.map +1 -1
  69. package/dist/discovery/deprecation-report.md +1 -1
  70. package/dist/discovery/discovery-manifest.json +164 -47
  71. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  72. package/dist/discovery/discovery-manifest.summary.md +4 -4
  73. package/dist/discovery/orphan-report.md +1 -1
  74. package/dist/discovery/packs.json +13 -8
  75. package/dist/discovery/trust-report.md +11 -11
  76. package/dist/discovery/workspaces.json +11 -6
  77. package/dist/install/install.mjs +16 -0
  78. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  79. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  80. package/dist/mcp/registry-manifest.json +4 -4
  81. package/dist/router.json +1 -1
  82. package/dist/server/app.js +10 -0
  83. package/dist/server/app.js.map +1 -1
  84. package/dist/server/routes/ping.js +18 -0
  85. package/dist/server/routes/ping.js.map +1 -1
  86. package/dist/server/schemas/settings.js +8 -0
  87. package/dist/server/schemas/settings.js.map +1 -1
  88. package/dist/server/serverInfo.js +6 -2
  89. package/dist/server/serverInfo.js.map +1 -1
  90. package/dist/server/token.js +11 -4
  91. package/dist/server/token.js.map +1 -1
  92. package/dist/server/writeRoot.js +15 -1
  93. package/dist/server/writeRoot.js.map +1 -1
  94. package/dist/shared/capabilities.js +33 -0
  95. package/dist/shared/capabilities.js.map +1 -0
  96. package/dist/shared/capabilities.test.js +39 -0
  97. package/dist/shared/capabilities.test.js.map +1 -0
  98. package/dist/ui/assets/{index-BiEZiZ31.js → index-CrmZ34Rg.js} +16 -16
  99. package/dist/ui/assets/index-CrmZ34Rg.js.map +1 -0
  100. package/dist/ui/index.html +14 -5
  101. package/docs/CLAIMS.md +64 -38
  102. package/docs/architecture.md +2 -2
  103. package/docs/archive/CHANGELOG-6.0.0-overview.md +953 -0
  104. package/docs/benchmark.md +170 -9
  105. package/docs/catalog.md +8 -3
  106. package/docs/comparison.yaml +44 -2
  107. package/docs/contracts/local-server-ports.md +53 -0
  108. package/docs/contracts/no-runtime-boundary.md +1 -1
  109. package/docs/contracts/subagent-boundary.md +15 -0
  110. package/docs/customization.md +14 -3
  111. package/docs/decisions/ADR-016-installer-architecture.md +2 -0
  112. package/docs/decisions/ADR-088-no-external-runtime-federation.md +1 -1
  113. package/docs/decisions/ADR-094-agent-memory-layer-removal.md +1 -1
  114. package/docs/decisions/ADR-109-subagent-v1-contract.md +2 -0
  115. package/docs/decisions/ADR-110-discipline-profile-resolution-locus.md +2 -0
  116. package/docs/decisions/ADR-113-ci-native-release-label-trigger.md +11 -3
  117. package/docs/decisions/ADR-122-adversarial-verification-council.md +30 -0
  118. package/docs/decisions/ADR-123-runtime-security-scope-and-spawn-hardening.md +31 -0
  119. package/docs/decisions/ADR-124-embedded-engine-doctrine.md +255 -0
  120. package/docs/decisions/ADR-125-doc-screenshot-anonymization.md +131 -0
  121. package/docs/decisions/ADR-126-internet-reach-operator-tooling.md +254 -0
  122. package/docs/decisions/ADR-127-enforcement-claims-must-resolve.md +150 -0
  123. package/docs/decisions/ADR-128-executable-evidence-and-measured-escalation.md +201 -0
  124. package/docs/decisions/INDEX.md +21 -0
  125. package/docs/decisions/engine-reclassification-2026-07.md +104 -0
  126. package/docs/distribution/awesome-list-submission.md +1 -1
  127. package/docs/distribution/registries.md +36 -27
  128. package/docs/enforcement-by-host.md +7 -0
  129. package/docs/featured-skills.md +2 -2
  130. package/docs/getting-started-by-role.md +1 -1
  131. package/docs/getting-started.md +1 -1
  132. package/docs/governance-advantage.md +1 -1
  133. package/docs/guides/gated-platform-reads.md +221 -0
  134. package/docs/positioning-evidence.md +1 -1
  135. package/docs/proof.md +92 -16
  136. package/docs/spawn-site-policy.md +72 -0
  137. package/docs/threat-model.md +2 -1
  138. package/package.json +6 -3
  139. package/src/agent-src/README.md +2 -2
  140. package/src/config/agent-settings.template.yml +43 -0
  141. package/src/config/reach-channels.yml +217 -0
  142. package/src/config/reach-prescriptions-intake.yml +200 -0
  143. package/src/scripts/_dispatch.bash +15 -0
  144. package/src/scripts/_lib/adversarial_bench_score.ts +106 -0
  145. package/src/scripts/_lib/exec_evidence.ts +251 -0
  146. package/src/scripts/_lib/fixtures/secret-detector/negatives.txt +10 -0
  147. package/src/scripts/_lib/fixtures/secret-detector/positives.txt +11 -0
  148. package/src/scripts/_lib/kernel_rules.ts +46 -0
  149. package/src/scripts/_lib/reddit_thread_parse.ts +598 -0
  150. package/src/scripts/_lib/risk_paths.ts +160 -0
  151. package/src/scripts/_lib/secret_detector.test.ts +103 -0
  152. package/src/scripts/_lib/secret_detector.ts +319 -0
  153. package/src/scripts/_lib/spawn_env.ts +24 -4
  154. package/src/scripts/_lib/tool_probe.ts +359 -0
  155. package/src/scripts/adr/regenerate_index.ts +81 -6
  156. package/src/scripts/audit_adr_coverage.ts +33 -0
  157. package/src/scripts/bench_adversarial_council.ts +292 -0
  158. package/src/scripts/build_proof.ts +162 -0
  159. package/src/scripts/check_adr_frontmatter.ts +195 -0
  160. package/src/scripts/check_artefact_count_messaging.ts +4 -0
  161. package/src/scripts/check_backstop_debt.ts +200 -0
  162. package/src/scripts/check_claims.ts +188 -8
  163. package/src/scripts/check_comparison.ts +12 -0
  164. package/src/scripts/check_enforcement_coverage.ts +592 -0
  165. package/src/scripts/check_kernel_rule_bundle.ts +16 -12
  166. package/src/scripts/check_no_new_legacy_path.ts +2 -0
  167. package/src/scripts/check_reach_channels.ts +407 -0
  168. package/src/scripts/check_reach_staleness.ts +364 -0
  169. package/src/scripts/check_secret_leak.test.ts +47 -0
  170. package/src/scripts/check_secret_leak.ts +290 -0
  171. package/src/scripts/check_source_pointer_freshness.ts +165 -0
  172. package/src/scripts/code_graph/build.ts +0 -0
  173. package/src/scripts/code_graph/cli.ts +207 -0
  174. package/src/scripts/code_graph/detect.ts +134 -0
  175. package/src/scripts/code_graph/extract.ts +386 -0
  176. package/src/scripts/code_graph/loader.ts +123 -0
  177. package/src/scripts/code_graph/query.ts +176 -0
  178. package/src/scripts/code_graph/sanitize.ts +30 -0
  179. package/src/scripts/code_graph/types.ts +83 -0
  180. package/src/scripts/code_graph/validate.ts +63 -0
  181. package/src/scripts/external_sources_denylist.json +3 -1
  182. package/src/scripts/generate_subagent_floor.ts +194 -0
  183. package/src/scripts/hook_manifest.yaml +13 -3
  184. package/src/scripts/hooks/code_graph_nudge_hook.ts +184 -0
  185. package/src/scripts/hooks/dispatch_hook.ts +5 -1
  186. package/src/scripts/iron_law_sha.ts +3 -12
  187. package/src/scripts/lint_override_kernel_guard.ts +219 -0
  188. package/src/scripts/lint_readme_serial_comma.ts +169 -0
  189. package/src/scripts/measure_rule_budget.ts +3 -12
  190. package/src/scripts/postinstall_gui.ts +129 -0
  191. package/src/scripts/reach_doctor.ts +1651 -0
  192. package/src/scripts/release.ts +24 -3
  193. package/src/scripts/risk_escalation_shadow.ts +160 -0
  194. package/src/scripts/schemas/agent-settings.schema.json +23 -0
  195. package/src/scripts/schemas/reach-channels.schema.json +134 -0
  196. package/src/scripts/schemas/reach-doctor-payload.schema.json +220 -0
  197. package/src/scripts/schemas/rule.schema.json +10 -0
  198. package/src/scripts/skill_linter.ts +55 -0
  199. package/src/scripts/validate_reach_prescriptions.ts +751 -0
  200. package/src/server/schemas/settings.ts +16 -0
  201. package/src/shared/capabilities.test.ts +43 -0
  202. package/src/shared/capabilities.ts +75 -0
  203. package/src/templates/minimal/agents-overrides-readme.md +20 -0
  204. package/dist/ui/assets/index-BiEZiZ31.js.map +0 -1
@@ -546,6 +546,39 @@ consistency:
546
546
  # reading of whichever source it happened to read).
547
547
  cross_source: "on"
548
548
 
549
+ # --- Documentation screenshots ---
550
+ #
551
+ # When the agent creates screenshots for documentation (e.g. generating a docs
552
+ # site) and embeds them, sensitive data visible on the screenshot MUST be
553
+ # anonymized / pixelated before it ships. Consumed by the `doc-screenshot-hygiene`
554
+ # rule and the `screenshot-hygiene` skill. Automated detection is a helper that
555
+ # flags + pre-redacts — never a clearance; the gate for a data-bearing screenshot
556
+ # is human confirmation (routing into `non-destructive-by-default`, since a
557
+ # shipped screenshot is an irreversible published egress).
558
+ screenshots:
559
+ # Public identity tokens that are SAFE to show unredacted — the maintainer's
560
+ # OWN public handles (e.g. a username shown in an admin panel) plus well-known
561
+ # fake-data tokens. This is NOT a general fake-data dictionary and NOT
562
+ # identity-resolution: everything not on this list is treated as sensitive by
563
+ # default. A public handle co-located with a real name does NOT whitelist the
564
+ # real name. Default: [] (nothing auto-allowed; every identity is human-decided).
565
+ identity_allowlist: []
566
+
567
+ # true | false (default: true)
568
+ # true = terminal / CLI / IDE screenshots are FORBIDDEN (highest leak vector:
569
+ # absolute local paths, env tokens). Use text code blocks with text
570
+ # redaction instead.
571
+ # false = allowed, but still subject to the data-bearing human gate.
572
+ forbid_terminal_capture: true
573
+
574
+ # on | off (default: on)
575
+ # on = a data-bearing screenshot embed is gated behind this-turn human
576
+ # confirmation; uncertain/unresolved regions → redact-or-refuse, never
577
+ # ship-and-hope. Illustrative / no-data screenshots (diagrams, logos,
578
+ # placeholder-only UI) may embed with a stated justification.
579
+ # off = no data-bearing gate (legacy; the anonymization taxonomy still applies).
580
+ data_bearing_gate: "on"
581
+
549
582
  code_style:
550
583
  # minimal | full (default: minimal)
551
584
  #
@@ -1007,6 +1040,16 @@ hooks:
1007
1040
  design_slop:
1008
1041
  enabled: false
1009
1042
 
1043
+ # PreToolUse code-graph nudge (ADR-124 Phase 4). Default-OFF. When enabled
1044
+ # AND a native code-graph engine has built a cache (or the repo ships a
1045
+ # graph.json / SCIP index), WARNS once per session (exit 2, never blocks) as
1046
+ # the agent is about to Grep/Glob or Read a source file: query the graph
1047
+ # first for "who calls / where used / impact" questions (or rebuild if the
1048
+ # index is stale, or build one if absent). Source G's strict block-first-read
1049
+ # mode is deliberately un-ported. Build the cache with `code_graph build`.
1050
+ code_graph:
1051
+ enabled: false
1052
+
1010
1053
  # --- Decision engine ---
1011
1054
  #
1012
1055
  # Controllable gates layered over the observability surface. Absent
@@ -0,0 +1,217 @@
1
+ # Reach channel registry — standalone operator tooling
2
+ # (road-to-internet-reach Phase 1).
3
+ #
4
+ # WHAT THIS FILE IS. A machine-checked answer to three operator questions:
5
+ # which external backend does a reach recipe need, is that backend healthy,
6
+ # and is its install command pinned. Schema:
7
+ # src/scripts/schemas/reach-channels.schema.json; gate:
8
+ # `task check-reach-channels`.
9
+ #
10
+ # WHAT THIS FILE IS NOT. Not a routing table, not a recommendation, not an
11
+ # agent-facing surface. The Phase 0 pre-registered benchmark returned
12
+ # `band: stop` (native arm 12/12, reach arm 0 outright wins), so no router
13
+ # skill ships and NO channel below is routed, preferred, or suggested to an
14
+ # agent. Reading a channel here as "the agent should use this" is a
15
+ # misreading of the verdict.
16
+ #
17
+ # BACKEND ORDER IS THE SWITCH. `backends:` is an ordered candidate list —
18
+ # each entry satisfies the channel on its own (an alternative, never a
19
+ # pipeline stage). Changing the preferred candidate is reordering this list,
20
+ # never editing code. Channel order carries no meaning.
21
+ #
22
+ # LIFECYCLE VOCABULARY IS BORROWED, NOT FORKED. `lifecycle:` uses the four
23
+ # values from docs/contracts/provider-lifecycle.md (`experimental | stable |
24
+ # deprecated | community`). `stable` here means the same thing it means
25
+ # there: a maintainer exercised it against a real run at least once.
26
+ #
27
+ # PINNING IS LOAD-BEARING. Every `install:` string is either an explicitly
28
+ # version-pinned command or an `os-baseline:` sentinel (see below). Moving
29
+ # targets — `latest`, `main`, `master`, `HEAD`, archive/tarball URLs — are
30
+ # rejected by the schema pattern, not by good intentions. The package never
31
+ # executes these strings: a human reads and runs them.
32
+ #
33
+ # OS BASELINES ARE NOT PACKAGE INSTALLS. `curl` and `node` are assumed
34
+ # present: they ship with the OS or the developer's base image. Their
35
+ # prescriptions therefore name the platform's own package manager and stay
36
+ # unpinned wherever a version pin is not meaningful, marked with the
37
+ # explicit `os-baseline:` sentinel so the absence of a pin is a declared
38
+ # decision rather than an oversight. Where a pin IS meaningful — Node's
39
+ # major line — a real versioned formula is used.
40
+ #
41
+ # `jq` IS DELIBERATELY NOT A BACKEND. JSON shaping happens inside the
42
+ # operator's own recipe, downstream of retrieval; it is not a candidate
43
+ # that satisfies a channel. If it ever becomes one, it inherits the same
44
+ # `os-baseline:` convention as curl.
45
+ #
46
+ # PIN PROVENANCE (looked up 2026-07-24, never invented):
47
+ # gh 2.96.0 — `gh --version` on the authoring machine, corroborated by
48
+ # https://api.github.com/repos/cli/cli/releases/latest
49
+ # (tag_name v2.96.0) and
50
+ # https://formulae.brew.sh/api/formula/gh.json (stable 2.96.0).
51
+ # yt-dlp 2026.7.4 — https://pypi.org/pypi/yt-dlp/json (.info.version).
52
+ # node@22 — https://formulae.brew.sh/api/formula/node@22.json
53
+ # (versioned formula exists; stable 22.23.1 at lookup time).
54
+ # asdf plugin `github-cli` — exists in the asdf plugin index
55
+ # (raw.githubusercontent.com/asdf-vm/asdf-plugins → plugins/github-cli).
56
+ # Homebrew has no versioned `gh` formula, so an exactly
57
+ # pinned macOS/Linux prescription goes through asdf.
58
+ # winget id `GitHub.cli` version 2.96.0 — present in the winget-pkgs manifest
59
+ # tree (api.github.com/repos/microsoft/winget-pkgs/contents/
60
+ # manifests/g/GitHub/cli lists 2.96.0).
61
+ #
62
+ # `last_verified:` MUST stay quoted — an unquoted ISO date parses as a
63
+ # timestamp, not a string, and fails the schema type check.
64
+ schema_version: reach-channels-v1
65
+
66
+ channels:
67
+ # Repository metadata: issues, PRs, releases, workflow runs, raw file reads.
68
+ # `free-key`: unauthenticated GitHub API access is rate-limited hard enough
69
+ # that a token is the practical floor. The token is operator-provisioned;
70
+ # nothing here reads, stores, or forwards it.
71
+ - id: github
72
+ description: Repository, issue, release and workflow metadata from the GitHub API.
73
+ tier: free-key
74
+ lifecycle: stable
75
+ override_key: reach.channels.github.backend
76
+ last_verified: "2026-07-24"
77
+ backends:
78
+ - id: gh
79
+ probe_cmd: gh
80
+ probe_args: ["--version"]
81
+ install:
82
+ darwin: asdf plugin add github-cli && asdf install github-cli 2.96.0
83
+ linux: asdf plugin add github-cli && asdf install github-cli 2.96.0
84
+ win32: winget install --id GitHub.cli --version 2.96.0
85
+ default: asdf plugin add github-cli && asdf install github-cli 2.96.0
86
+
87
+ # Video metadata and subtitle tracks. `yt-dlp` is NOT installed on the
88
+ # authoring machine, and that is expected: this file is validated offline
89
+ # against the schema only, so an absent backend cannot fail the gate. Its
90
+ # health is the probe layer's question, never the registry's.
91
+ # `experimental` for exactly that reason — the shape is right, no
92
+ # maintainer has exercised this backend against a real run.
93
+ - id: youtube
94
+ description: Video metadata and subtitle tracks for a public video URL.
95
+ tier: zero-config
96
+ lifecycle: experimental
97
+ override_key: reach.channels.youtube.backend
98
+ last_verified: "2026-07-24"
99
+ backends:
100
+ - id: yt-dlp
101
+ probe_cmd: yt-dlp
102
+ probe_args: ["--version"]
103
+ install:
104
+ darwin: pipx install yt-dlp==2026.7.4
105
+ linux: pipx install yt-dlp==2026.7.4
106
+ win32: py -m pipx install yt-dlp==2026.7.4
107
+ default: pipx install yt-dlp==2026.7.4
108
+
109
+ # Feed retrieval. Two independent candidates: fetch the feed with curl, or
110
+ # fetch-and-parse it in a Node one-liner. Either satisfies the channel.
111
+ - id: rss
112
+ description: Atom and RSS feed retrieval for a known feed URL.
113
+ tier: zero-config
114
+ lifecycle: stable
115
+ override_key: reach.channels.rss.backend
116
+ last_verified: "2026-07-24"
117
+ backends:
118
+ - id: curl
119
+ probe_cmd: curl
120
+ probe_args: ["--version"]
121
+ install:
122
+ darwin: "os-baseline: macOS ships curl (8.7.1 observed 2026-07-24) at /usr/bin/curl — no package install prescribed"
123
+ linux: "os-baseline: distro-provided curl package (apt / dnf / apk) — version tracks the distro, no pin meaningful"
124
+ win32: "os-baseline: Windows 10+ ships curl.exe in System32 — no package install prescribed"
125
+ default: "os-baseline: a system curl is assumed present; if absent, install it from the platform package manager — never a piped remote installer"
126
+ - id: node
127
+ probe_cmd: node
128
+ probe_args: ["--version"]
129
+ install:
130
+ darwin: brew install node@22
131
+ linux: "os-baseline: distro or NodeSource nodejs package — version tracks the chosen channel, no pin meaningful here"
132
+ win32: "os-baseline: winget package OpenJS.NodeJS.LTS — version tracks the LTS line, no pin meaningful here"
133
+ default: "os-baseline: any Node runtime at or above this package's engines floor (>=20.11.0) already on PATH"
134
+
135
+ # Single-page read. Same two candidates as `rss`: curl fetches the bytes;
136
+ # Node covers the case where the extraction step runs in-process.
137
+ - id: web-read
138
+ description: Single public web page fetch for a URL the operator already has.
139
+ tier: zero-config
140
+ lifecycle: stable
141
+ override_key: reach.channels.web-read.backend
142
+ last_verified: "2026-07-24"
143
+ backends:
144
+ - id: curl
145
+ probe_cmd: curl
146
+ probe_args: ["--version"]
147
+ install:
148
+ darwin: "os-baseline: macOS ships curl (8.7.1 observed 2026-07-24) at /usr/bin/curl — no package install prescribed"
149
+ linux: "os-baseline: distro-provided curl package (apt / dnf / apk) — version tracks the distro, no pin meaningful"
150
+ win32: "os-baseline: Windows 10+ ships curl.exe in System32 — no package install prescribed"
151
+ default: "os-baseline: a system curl is assumed present; if absent, install it from the platform package manager — never a piped remote installer"
152
+ - id: node
153
+ probe_cmd: node
154
+ probe_args: ["--version"]
155
+ install:
156
+ darwin: brew install node@22
157
+ linux: "os-baseline: distro or NodeSource nodejs package — version tracks the chosen channel, no pin meaningful here"
158
+ win32: "os-baseline: winget package OpenJS.NodeJS.LTS — version tracks the LTS line, no pin meaningful here"
159
+ default: "os-baseline: any Node runtime at or above this package's engines floor (>=20.11.0) already on PATH"
160
+
161
+ # Discussion search over the public Hacker News search API. Keyless, and
162
+ # the endpoint returns JSON directly — one candidate is enough.
163
+ - id: hackernews
164
+ description: Discussion and comment search over the public Hacker News search API.
165
+ tier: zero-config
166
+ lifecycle: stable
167
+ override_key: reach.channels.hackernews.backend
168
+ last_verified: "2026-07-24"
169
+ backends:
170
+ - id: curl
171
+ probe_cmd: curl
172
+ probe_args: ["--version"]
173
+ install:
174
+ darwin: "os-baseline: macOS ships curl (8.7.1 observed 2026-07-24) at /usr/bin/curl — no package install prescribed"
175
+ linux: "os-baseline: distro-provided curl package (apt / dnf / apk) — version tracks the distro, no pin meaningful"
176
+ win32: "os-baseline: Windows 10+ ships curl.exe in System32 — no package install prescribed"
177
+ default: "os-baseline: a system curl is assumed present; if absent, install it from the platform package manager — never a piped remote installer"
178
+
179
+ - id: reddit
180
+ description: Reddit discussion reading in two tiers — durable Atom text, and time-bounded ranked HTML.
181
+ tier: zero-config
182
+ lifecycle: experimental
183
+ override_key: reach.channels.reddit.backend
184
+ last_verified: "2026-07-25"
185
+ backends:
186
+ - id: curl
187
+ probe_cmd: curl
188
+ probe_args: ["--version"]
189
+ install:
190
+ darwin: "os-baseline: macOS ships curl (8.7.1 observed 2026-07-25) at /usr/bin/curl — no package install prescribed"
191
+ linux: "os-baseline: distro-provided curl package (apt / dnf / apk) — version tracks the distro, no pin meaningful"
192
+ win32: "os-baseline: Windows 10+ ships curl.exe in System32 — no package install prescribed"
193
+ default: "os-baseline: a system curl is assumed present; if absent, install it from the platform package manager — never a piped remote installer"
194
+ - id: node
195
+ probe_cmd: node
196
+ probe_args: ["--version"]
197
+ install:
198
+ darwin: brew install node@22
199
+ linux: "os-baseline: distro or NodeSource nodejs package — version tracks the chosen channel, no pin meaningful here"
200
+ win32: "os-baseline: winget package OpenJS.NodeJS.LTS — version tracks the LTS line, no pin meaningful here"
201
+ default: "os-baseline: any Node runtime at or above this package's engines floor (>=20.11.0) already on PATH"
202
+
203
+ - id: twitter-oembed
204
+ description: Single public tweet read through the platform's own oEmbed endpoint, credential-free.
205
+ tier: zero-config
206
+ lifecycle: experimental
207
+ override_key: reach.channels.twitter-oembed.backend
208
+ last_verified: "2026-07-25"
209
+ backends:
210
+ - id: curl
211
+ probe_cmd: curl
212
+ probe_args: ["--version"]
213
+ install:
214
+ darwin: "os-baseline: macOS ships curl (8.7.1 observed 2026-07-25) at /usr/bin/curl — no package install prescribed"
215
+ linux: "os-baseline: distro-provided curl package (apt / dnf / apk) — version tracks the distro, no pin meaningful"
216
+ win32: "os-baseline: Windows 10+ ships curl.exe in System32 — no package install prescribed"
217
+ default: "os-baseline: a system curl is assumed present; if absent, install it from the platform package manager — never a piped remote installer"
@@ -0,0 +1,200 @@
1
+ # Reach prescription intake record — committed supply-chain evidence
2
+ # (road-to-internet-reach Phase 3, Step 2).
3
+ #
4
+ # WHAT THIS FILE IS. The offline substitute for a network lookup at lint time.
5
+ # `src/scripts/validate_reach_prescriptions.ts` (gate:
6
+ # `task check-reach-prescriptions`) reads THIS file to answer "does the package
7
+ # this prescription names exist, is its pin the one we checked, and was it
8
+ # CVE-reviewed" — it never fetches a registry. That is a hard architectural
9
+ # constraint, not a convenience: the Class A boundary
10
+ # (docs/contracts/no-runtime-boundary.md, ADR-124) forbids network access in the
11
+ # build path, and a lint that phones a third party is a flaky lint.
12
+ #
13
+ # CONSEQUENCE. Every claim below is a HUMAN-RUN check frozen at `checked_on`.
14
+ # Freshness is a maintenance duty, not a guarantee this file can make about
15
+ # itself: re-run the recorded commands, update `pinned_version` +
16
+ # `verified_on`, and change the registry pin in the same commit.
17
+ #
18
+ # WHAT AN ENTRY IS FOR. `tools:` carries exactly one entry per EXTERNALLY
19
+ # INSTALLED third-party tool named by an install prescription in
20
+ # src/config/reach-channels.yml. A prescription whose backend has no entry
21
+ # here cannot ship — the validator fails closed on it.
22
+ #
23
+ # WHY `os_baseline:` IS A SEPARATE CLASS, NOT A THIN `tools:` ENTRY. `curl`,
24
+ # `node` and `jq` are not package installs this project prescribes: they ship
25
+ # with the OS or the developer's base image. Writing a fake intake entry for
26
+ # them — "existence verified: it is curl" — would launder an assumption into
27
+ # evidence and put a registry/CVE claim on this project's ledger that nobody
28
+ # actually checked per platform. They are declared as baselines instead, with
29
+ # the reason, and the validator exempts them from the `tools:` requirement
30
+ # while still enforcing the pinning rules on any string they do carry.
31
+ #
32
+ # EXECUTION HONESTY. `asdf` and `pipx` are ABSENT on the authoring machine
33
+ # (`which asdf` / `which pipx` → not found), so no install command below was
34
+ # ever executed here. That is the design: prescription-first, human-performed,
35
+ # no auto-install (roadmap `## Non-goals`). Existence was verified against the
36
+ # registries' own metadata APIs, which is what the pin depends on.
37
+ #
38
+ # ISO DATES MUST STAY QUOTED — unquoted, YAML parses them as timestamps and
39
+ # the validator's string check fails loudly.
40
+ schema_version: reach-prescriptions-intake-v1
41
+
42
+ # The date every `verification.command` below was last executed by a human.
43
+ checked_on: "2026-07-24"
44
+
45
+ tools:
46
+ # ── gh ────────────────────────────────────────────────────────────────────
47
+ # Backend of the `github` channel. Two package identities because no
48
+ # single registry offers an exactly-pinnable `gh` on every platform:
49
+ # Homebrew ships only an unversioned `gh` formula (a pin there would be a
50
+ # lie), so macOS/Linux go through asdf, and Windows through winget.
51
+ - backend: gh
52
+ tool: gh
53
+ pinned_version: "2.96.0"
54
+ packages:
55
+ - registry: asdf
56
+ package: github-cli
57
+ - registry: winget
58
+ package: GitHub.cli
59
+ intake:
60
+ existence_verified: >-
61
+ Three independent confirmations at the pin. (1) The binary itself
62
+ reports 2.96.0 on the authoring machine. (2) The asdf plugin index
63
+ contains plugins/github-cli (blob sha
64
+ b8ab100243bc01e361a61c50d11d4ab66b790c92). (3) The winget-pkgs manifest
65
+ tree lists exactly 2.96.0 under manifests/g/GitHub/cli. Not a
66
+ model-produced name: every identifier came out of a registry response.
67
+ pinned: >-
68
+ Exact, both platforms — `asdf install github-cli 2.96.0` and
69
+ `winget install --id GitHub.cli --version 2.96.0`. No range, no
70
+ `latest`, no branch, no archive URL. Homebrew was rejected as the
71
+ macOS/Linux path precisely because its unversioned `gh` formula admits
72
+ no exact pin at all — a pinned-looking Homebrew line would be fiction.
73
+ lockfile_note: >-
74
+ No project lockfile applies — gh is an external CLI a human installs,
75
+ never a dependency of this package (absent from package.json, and this
76
+ repo ships no .tool-versions). The lock equivalent is the exact pin in
77
+ this record plus asdf's own .tool-versions in the operator's
78
+ environment, if they use one. Nothing in this repo can pin it for them.
79
+ cve_note: >-
80
+ GitHub Advisory Database, ecosystem `go`, package
81
+ github.com/cli/cli/v2: 6 advisories, newest first_patched_version
82
+ 2.93.0 (GHSA-8xvp-7hj6-mcj9, TUF-mirror auth header). 2.93.0 <= 2.96.0,
83
+ so no listed advisory affects the pin. Reviewed by advisory listing, not
84
+ by a scanner — osv-scanner and pip-audit are not installed here, and
85
+ neither covers a system CLI install anyway.
86
+ verified_on: "2026-07-24"
87
+ verification:
88
+ - command: gh --version
89
+ source: local PATH on the authoring machine
90
+ observed: "gh version 2.96.0 (2026-07-02)"
91
+ - command: gh api repos/asdf-vm/asdf-plugins/contents/plugins/github-cli --jq '.sha + " " + .path'
92
+ source: https://api.github.com/repos/asdf-vm/asdf-plugins
93
+ observed: "b8ab100243bc01e361a61c50d11d4ab66b790c92 plugins/github-cli"
94
+ - command: gh api repos/microsoft/winget-pkgs/contents/manifests/g/GitHub/cli --jq '[.[].name] | map(select(startswith("2.96")))'
95
+ source: https://api.github.com/repos/microsoft/winget-pkgs
96
+ observed: '["2.96.0"]'
97
+ - command: gh api "/advisories?ecosystem=go&affects=github.com%2Fcli%2Fcli%2Fv2&per_page=100" --jq '[.[].vulnerabilities[] | select(.package.name=="github.com/cli/cli/v2") | .first_patched_version] | unique | sort'
98
+ source: https://api.github.com/advisories
99
+ observed: '["2.62.0","2.63.0","2.63.1","2.67.0","2.92.0","2.93.0"]'
100
+
101
+ # ── yt-dlp ────────────────────────────────────────────────────────────────
102
+ # Backend of the `youtube` channel, and NOT installed here — the roadmap
103
+ # blocker `yt-dlp-not-installed` is open by design. An intake record is
104
+ # about whether the prescription is safe to hand a human, which is a
105
+ # different question from whether the tool is present locally; the probe
106
+ # layer owns the second one.
107
+ - backend: yt-dlp
108
+ tool: yt-dlp
109
+ pinned_version: "2026.7.4"
110
+ packages:
111
+ - registry: PyPI
112
+ package: yt-dlp
113
+ intake:
114
+ existence_verified: >-
115
+ PyPI JSON API reports .info.version 2026.7.4 for the project, and the
116
+ per-release endpoint /pypi/yt-dlp/2026.7.4/json resolves with 2 upload
117
+ files — so the exact pinned release exists, not merely the project name.
118
+ Typo-adjacency checked deliberately: `yt-dlp` is the hyphenated form,
119
+ the shape most prone to hallucinated variants (`ytdlp`, `yt_dlp`,
120
+ `youtube-dl`); the identifier used here is the one PyPI returned.
121
+ pinned: >-
122
+ Exact PyPI equality pin — `pipx install yt-dlp==2026.7.4` (Windows:
123
+ `py -m pipx install yt-dlp==2026.7.4`). yt-dlp publishes date-shaped
124
+ versions, so a `latest`-style install would silently roll forward
125
+ every few days; the schema rejects that form outright.
126
+ lockfile_note: >-
127
+ No project lockfile applies — an isolated pipx application, not a
128
+ dependency of this package. pipx records the exact spec in its own
129
+ venv metadata; the pin above is the only lock this repo controls.
130
+ cve_note: >-
131
+ GitHub Advisory Database, ecosystem `pip`, package yt-dlp: 11
132
+ advisories, newest first_patched_version 2026.6.9 (GHSA-69qj-pvh9-c5wg
133
+ `--exec` command injection, plus three siblings patched in the same
134
+ release). 2026.6.9 < 2026.7.4, so no listed advisory affects the pin.
135
+ Worth carrying forward: the tool's own history is dominated by
136
+ argument-injection and filename-sanitization classes, which is one more
137
+ reason its output is untrusted content, never instructions
138
+ (src/rules/untrusted-input-defense.md).
139
+ verified_on: "2026-07-24"
140
+ verification:
141
+ - command: curl -fsSL https://pypi.org/pypi/yt-dlp/json | jq -r '.info.version'
142
+ source: https://pypi.org/pypi/yt-dlp/json
143
+ observed: "2026.7.4"
144
+ - command: curl -fsSL https://pypi.org/pypi/yt-dlp/2026.7.4/json | jq -r '.info.version + " files=" + (.urls|length|tostring)'
145
+ source: https://pypi.org/pypi/yt-dlp/2026.7.4/json
146
+ observed: "2026.7.4 files=2"
147
+ - command: gh api "/advisories?ecosystem=pip&affects=yt-dlp&per_page=100" --jq '[.[].vulnerabilities[] | select(.package.name=="yt-dlp") | .first_patched_version] | unique | sort'
148
+ source: https://api.github.com/advisories
149
+ observed: '["2023.09.24","2023.11.14","2023.7.06","2024.04.09","2024.07.01","2024.07.07","2026.02.21","2026.6.9"]'
150
+
151
+ # ── OS baselines — declared non-installs ──────────────────────────────────
152
+ # Present because the registry names them as backends, NOT because this
153
+ # project prescribes installing them. Each entry states why a pin is not
154
+ # meaningful; the validator exempts these from the `tools:` requirement and
155
+ # still enforces "pinned or explicit os-baseline sentinel" on every string
156
+ # they carry.
157
+ os_baseline:
158
+ - tool: curl
159
+ os_baseline: true
160
+ rationale: >-
161
+ Ships with macOS (/usr/bin/curl) and Windows 10+ (System32\curl.exe),
162
+ and is a distro package on Linux whose version tracks the distro. There
163
+ is no cross-platform version this project could pin without prescribing
164
+ a source it does not control — so the registry carries `os-baseline:`
165
+ sentinels and no version, deliberately.
166
+ verification:
167
+ - command: curl --version | head -1
168
+ source: local PATH on the authoring machine
169
+ observed: "curl 8.7.1 (x86_64-apple-darwin24.0) libcurl/8.7.1"
170
+
171
+ - tool: node
172
+ os_baseline: true
173
+ rationale: >-
174
+ A Node runtime is this package's own execution baseline
175
+ (package.json engines >=20.11.0), so any environment that can run the
176
+ validator already has it. The one platform where a pin IS meaningful —
177
+ Homebrew's versioned major-line formula — carries a real pin
178
+ (`brew install node@22`), verified below; the remaining platforms track
179
+ their own LTS channel and stay unpinned behind an explicit sentinel.
180
+ verification:
181
+ - command: curl -fsSL https://formulae.brew.sh/api/formula/node@22.json | jq -r '.versions.stable'
182
+ source: https://formulae.brew.sh/api/formula/node@22.json
183
+ observed: "22.23.1 (versioned formula node@22 exists)"
184
+ - command: node --version
185
+ source: local PATH on the authoring machine
186
+ observed: "v25.9.0"
187
+
188
+ - tool: jq
189
+ os_baseline: true
190
+ rationale: >-
191
+ Named nowhere in the registry as a backend on purpose — JSON shaping
192
+ happens inside the operator's own recipe, downstream of retrieval, so jq
193
+ never satisfies a channel. Recorded here so its absence from `tools:` is
194
+ a documented decision rather than an omission: if jq ever becomes a
195
+ backend it inherits curl's `os-baseline:` treatment, not a fabricated
196
+ pin.
197
+ verification:
198
+ - command: jq --version
199
+ source: local PATH on the authoring machine
200
+ observed: "jq-1.7.1-apple"
@@ -205,6 +205,15 @@ Tier 2 — maintenance / internal (hooks, MCP, memory, telemetry):
205
205
  Usage: hooks:replay --platform <name> --event <event>
206
206
  --payload <path|event-name> [--native-event <native>]
207
207
  [--manifest <path>] [--json] [--dry-run]
208
+ reach:doctor Health report over the reach channel registry: per channel
209
+ the active backend, lifecycle, and the pinned install command
210
+ for this platform when a backend is missing/broken.
211
+ Read-only — no writes, no installs; NOT a router and NOT an
212
+ agent-facing recommendation.
213
+ Flags: --format json|table, --strict (CI), --channel <id>,
214
+ --registry <path>, --deep (⚠️ opt-in NETWORK: one real
215
+ read-only request per declared backend; never runs in CI,
216
+ writes nothing)
208
217
  memory:lookup Retrieve memory entries (text or JSON envelope)
209
218
  linked-projects:list List opted-in IDE-attached sibling repos (path · detected_via · large)
210
219
  Flags: --all (show undecided too), --format json
@@ -636,6 +645,11 @@ cmd_hooks_replay() {
636
645
  exec_hook "src/scripts/hooks/replay_hook" "$@"
637
646
  }
638
647
 
648
+ # Read-only reach-channel health report. Never installs, never writes.
649
+ cmd_reach_doctor() {
650
+ exec_hook "src/scripts/reach_doctor" "$@"
651
+ }
652
+
639
653
  cmd_chat_history_checkpoint() {
640
654
  local script
641
655
  script="$(resolve_script "src/scripts/chat_history.ts")" || return 1
@@ -1085,6 +1099,7 @@ main() {
1085
1099
  hooks:status) cmd_hooks_status "$@" ;;
1086
1100
  hooks:doctor) cmd_hooks_doctor "$@" ;;
1087
1101
  hooks:replay) cmd_hooks_replay "$@" ;;
1102
+ reach:doctor) cmd_reach_doctor "$@" ;;
1088
1103
  telemetry:record) cmd_telemetry_record "$@" ;;
1089
1104
  telemetry:status) cmd_telemetry_status "$@" ;;
1090
1105
  telemetry:report) cmd_telemetry_report "$@" ;;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Deterministic scorer for the adversarial-council residual-detection benchmark
3
+ * (road-to-adversarial-council-benchmark Phase 1).
4
+ *
5
+ * Pure + countable — never LLM-computed (the anti-lesson from ADR-122). Given a
6
+ * corpus item's ground truth and a reviewer's structured findings, decide
7
+ * whether the reviewer CAUGHT the planted defect (for a defect fixture) or
8
+ * raised a FALSE POSITIVE (for a clean control). The two-stage recalls and FP
9
+ * rates this produces feed `evaluateCouncilBench` (adversarial_council_gate.ts).
10
+ *
11
+ * Matching rule (deliberately NOT exact file:line — the scorer bug from the
12
+ * earlier defect-finding benchmark required an exact line token and mis-scored
13
+ * genuine catches that cited a nearby line): a finding CATCHES a planted defect
14
+ * iff it names one of the defect's file(s) by basename AND its category falls in
15
+ * the same normalized family as the planted defect. Line numbers are advisory,
16
+ * never required. This credits a real catch that localizes the right file +
17
+ * defect class, and denies credit for a scattershot "something's wrong here"
18
+ * with the wrong class.
19
+ */
20
+
21
+ /** Normalize a free-form category string to a coarse family. */
22
+ export function categoryFamily(raw: string): string {
23
+ const c = raw.toLowerCase().replace(/[^a-z]/g, '');
24
+ if (/(accesscontrol|authz|authorization|idor|bola|ownership|privilege|rbac)/.test(c)) return 'access-control';
25
+ if (/(auth|authentication|session|credential|login)/.test(c)) return 'auth';
26
+ if (/(ssrf|serverside)/.test(c)) return 'ssrf';
27
+ if (/(pathtraversal|traversal|directorytraversal|lfi)/.test(c)) return 'path-traversal';
28
+ // NB: no bare "rce" token — it substring-matches inside "resou[rce]leak".
29
+ if (/(injection|sqli|xss|commandinjection|deserial|remotecodeexec)/.test(c)) return 'injection';
30
+ if (/(crypto|cryptographic|timing|constanttime|hash|signature)/.test(c)) return 'crypto';
31
+ if (/(concurrency|race|atomic|deadlock|idempoten)/.test(c)) return 'concurrency';
32
+ if (/(resourceleak|leak|memory|handle|unsubscrib|cleanup)/.test(c)) return 'resource-leak';
33
+ if (/(dataintegrity|integrity|corrupt|consistency|stale)/.test(c)) return 'data-integrity';
34
+ if (/(correctness|logic|offbyone|boundary|edgecase|bug)/.test(c)) return 'correctness';
35
+ return 'other';
36
+ }
37
+
38
+ export interface Finding {
39
+ /** File the finding points at (any path form; matched by basename). */
40
+ file: string;
41
+ /** Reviewer-supplied category (free text; normalized via categoryFamily). */
42
+ category: string;
43
+ /** Reviewer confidence, if given. Low-confidence findings can be excluded from FP counting. */
44
+ confidence?: 'high' | 'medium' | 'low';
45
+ }
46
+
47
+ export interface GroundTruth {
48
+ id: string;
49
+ /** true = clean control (no defect); a finding here is a false positive. */
50
+ is_clean: boolean;
51
+ /** Basenames of the file(s) the planted defect lives in (empty for clean). */
52
+ defect_files: string[];
53
+ /** Planted defect category family (via categoryFamily); empty for clean. */
54
+ defect_category: string;
55
+ }
56
+
57
+ function basename(p: string): string {
58
+ return p.split(/[\\/]/).pop() ?? p;
59
+ }
60
+
61
+ /**
62
+ * Did the reviewer CATCH the planted defect? file-basename match on any defect
63
+ * file AND category-family match. Clean controls always return false (they have
64
+ * no defect to catch — use `isFalsePositive` for them).
65
+ */
66
+ export function caughtDefect(truth: GroundTruth, findings: Finding[]): boolean {
67
+ if (truth.is_clean) return false;
68
+ const wantFiles = new Set(truth.defect_files.map(basename));
69
+ const wantFamily = truth.defect_category;
70
+ return findings.some(
71
+ (f) => wantFiles.has(basename(f.file)) && categoryFamily(f.category) === wantFamily,
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Did the reviewer raise a FALSE POSITIVE on a clean control? Any finding that
77
+ * points at the control file counts (a claimed defect where none exists).
78
+ * Low-confidence findings are excluded — a hedged "might check X" is not a FP
79
+ * claim. Non-clean fixtures always return false.
80
+ */
81
+ export function isFalsePositive(truth: GroundTruth, findings: Finding[]): boolean {
82
+ if (!truth.is_clean) return false;
83
+ return findings.some((f) => (f.confidence ?? 'high') !== 'low');
84
+ }
85
+
86
+ export interface StageResult {
87
+ /** Per-defect-fixture: id → caught?. Excludes clean controls. */
88
+ caught: Record<string, boolean>;
89
+ /** Per-clean-control: id → false-positive?. */
90
+ fp: Record<string, boolean>;
91
+ }
92
+
93
+ /** Recall over a set of defect ids, given per-id caught flags. */
94
+ export function recall(caught: Record<string, boolean>): number {
95
+ const ids = Object.keys(caught);
96
+ if (ids.length === 0) return 0;
97
+ const hits = ids.filter((id) => caught[id]).length;
98
+ return hits / ids.length;
99
+ }
100
+
101
+ /** FP rate over clean controls. */
102
+ export function fpRate(fp: Record<string, boolean>): number {
103
+ const ids = Object.keys(fp);
104
+ if (ids.length === 0) return 0;
105
+ return ids.filter((id) => fp[id]).length / ids.length;
106
+ }