@holmes-lab/holmes-kit 0.2.0 → 0.3.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 (134) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/.build-id +1 -1
  3. package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
  4. package/dist/holmes/assoc/assoc-arm.js +187 -0
  5. package/dist/holmes/assoc/explore.d.ts +21 -0
  6. package/dist/holmes/assoc/explore.js +160 -0
  7. package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
  8. package/dist/holmes/assoc/impact-baseline.js +48 -0
  9. package/dist/holmes/assoc/ppr.d.ts +64 -0
  10. package/dist/holmes/assoc/ppr.js +110 -0
  11. package/dist/holmes/cli/agents.d.ts +24 -11
  12. package/dist/holmes/cli/agents.js +93 -17
  13. package/dist/holmes/cli/codex-toml.d.ts +26 -0
  14. package/dist/holmes/cli/codex-toml.js +282 -0
  15. package/dist/holmes/cli/doctor.d.ts +56 -0
  16. package/dist/holmes/cli/doctor.js +348 -18
  17. package/dist/holmes/cli/index.js +16 -2
  18. package/dist/holmes/cli/init.js +78 -0
  19. package/dist/holmes/cli/interactive-prompt.js +4 -4
  20. package/dist/holmes/cli/mcp-launcher.d.ts +2 -2
  21. package/dist/holmes/cli/semantic-key.d.ts +19 -0
  22. package/dist/holmes/cli/semantic-key.js +93 -0
  23. package/dist/holmes/config/config.d.ts +9 -0
  24. package/dist/holmes/config/config.js +8 -1
  25. package/dist/holmes/cpg/language-capability.d.ts +65 -0
  26. package/dist/holmes/cpg/language-capability.js +145 -0
  27. package/dist/holmes/cpg/language-parser-walk.js +179 -34
  28. package/dist/holmes/cpg/language-parser.d.ts +1 -1
  29. package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
  30. package/dist/holmes/governance/ledger-rechain.js +17 -2
  31. package/dist/holmes/governance/provenance-ledger.js +21 -0
  32. package/dist/holmes/guardrail/blind-spots.js +12 -1
  33. package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
  34. package/dist/holmes/guardrail/impact-gate.js +263 -0
  35. package/dist/holmes/guardrail/write-target.d.ts +38 -1
  36. package/dist/holmes/guardrail/write-target.js +48 -4
  37. package/dist/holmes/hooks/adapters/antigravity.js +12 -1
  38. package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
  39. package/dist/holmes/hooks/corrupt-state-run.js +16 -0
  40. package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
  41. package/dist/holmes/hooks/pre-tool-use.js +203 -11
  42. package/dist/holmes/hooks/stop.d.ts +65 -0
  43. package/dist/holmes/hooks/stop.js +200 -2
  44. package/dist/holmes/mcp/handlers.d.ts +88 -0
  45. package/dist/holmes/mcp/handlers.js +597 -16
  46. package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
  47. package/dist/holmes/mcp/maintenance-analyze.js +994 -0
  48. package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
  49. package/dist/holmes/mcp/maintenance-evidence.js +253 -0
  50. package/dist/holmes/mcp/tool-schemas.js +71 -0
  51. package/dist/holmes/project/root.js +3 -1
  52. package/dist/holmes/review/baseline-arm.d.ts +37 -0
  53. package/dist/holmes/review/baseline-arm.js +51 -0
  54. package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
  55. package/dist/holmes/review/captured-stdin-guard.js +48 -0
  56. package/dist/holmes/review/coherence-verify.d.ts +31 -0
  57. package/dist/holmes/review/coherence-verify.js +144 -0
  58. package/dist/holmes/review/commit-text.d.ts +50 -0
  59. package/dist/holmes/review/commit-text.js +76 -0
  60. package/dist/holmes/review/confidence-calibration.d.ts +39 -0
  61. package/dist/holmes/review/confidence-calibration.js +39 -0
  62. package/dist/holmes/review/content-baseline.d.ts +38 -0
  63. package/dist/holmes/review/content-baseline.js +103 -0
  64. package/dist/holmes/review/content-verify.d.ts +20 -0
  65. package/dist/holmes/review/content-verify.js +73 -0
  66. package/dist/holmes/review/dense-retrieval.d.ts +66 -0
  67. package/dist/holmes/review/dense-retrieval.js +97 -0
  68. package/dist/holmes/review/edge-quality.d.ts +44 -0
  69. package/dist/holmes/review/edge-quality.js +117 -0
  70. package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
  71. package/dist/holmes/review/evaluation-metrics.js +175 -0
  72. package/dist/holmes/review/graph-verifier.d.ts +34 -0
  73. package/dist/holmes/review/graph-verifier.js +62 -0
  74. package/dist/holmes/review/hop-ablation.d.ts +100 -0
  75. package/dist/holmes/review/hop-ablation.js +89 -0
  76. package/dist/holmes/review/manual-baseline.d.ts +209 -0
  77. package/dist/holmes/review/manual-baseline.js +2846 -0
  78. package/dist/holmes/review/oracle-gap.d.ts +32 -0
  79. package/dist/holmes/review/oracle-gap.js +102 -0
  80. package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
  81. package/dist/holmes/review/point-in-time-replay.js +161 -0
  82. package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
  83. package/dist/holmes/review/rank-diagnosis.js +163 -0
  84. package/dist/holmes/review/replay-calibration.d.ts +62 -0
  85. package/dist/holmes/review/replay-calibration.js +83 -0
  86. package/dist/holmes/review/replay-corpus.d.ts +135 -0
  87. package/dist/holmes/review/replay-corpus.js +210 -0
  88. package/dist/holmes/review/run-replay.d.ts +260 -0
  89. package/dist/holmes/review/run-replay.js +729 -0
  90. package/dist/holmes/review/semantic-arm.d.ts +271 -0
  91. package/dist/holmes/review/semantic-arm.js +717 -0
  92. package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
  93. package/dist/holmes/review/semantic-retrieval.js +156 -0
  94. package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
  95. package/dist/holmes/review/spec-layer-stats.js +52 -0
  96. package/dist/holmes/review/temporal-prior.d.ts +33 -0
  97. package/dist/holmes/review/temporal-prior.js +53 -0
  98. package/dist/holmes/review/test-runner.d.ts +15 -0
  99. package/dist/holmes/review/test-runner.js +41 -4
  100. package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
  101. package/dist/holmes/review/test-selection-breadth.js +57 -0
  102. package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
  103. package/dist/holmes/review/traceability-benchmark.js +135 -0
  104. package/dist/holmes/review/union-verify.d.ts +12 -0
  105. package/dist/holmes/review/union-verify.js +70 -0
  106. package/dist/holmes/rtm/graph-store.d.ts +51 -0
  107. package/dist/holmes/rtm/graph-store.js +122 -0
  108. package/dist/holmes/rtm/incremental.d.ts +25 -1
  109. package/dist/holmes/rtm/incremental.js +18 -1
  110. package/dist/holmes/rtm/localize.d.ts +28 -0
  111. package/dist/holmes/rtm/localize.js +272 -14
  112. package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
  113. package/dist/holmes/rtm/rtm-builder.js +265 -28
  114. package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
  115. package/dist/holmes/rtm/rtm-graph.js +194 -31
  116. package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
  117. package/dist/holmes/rtm/taint-benchmark.js +141 -0
  118. package/dist/holmes/rtm/test-scope.js +24 -1
  119. package/dist/holmes/semantic/credentials.d.ts +15 -0
  120. package/dist/holmes/semantic/credentials.js +134 -0
  121. package/dist/holmes/semantic/embedder.d.ts +44 -0
  122. package/dist/holmes/semantic/embedder.js +185 -0
  123. package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
  124. package/dist/holmes/semantic/hit-rerank.js +38 -0
  125. package/dist/holmes/semantic/tier.d.ts +37 -0
  126. package/dist/holmes/semantic/tier.js +54 -0
  127. package/dist/holmes/semantic/vector-cache.d.ts +11 -0
  128. package/dist/holmes/semantic/vector-cache.js +91 -0
  129. package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
  130. package/dist/holmes/spec/acceptance-quality.js +169 -0
  131. package/dist/holmes/spec/validator.js +33 -1
  132. package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
  133. package/dist/holmes/spec/yaml-scalar.js +43 -0
  134. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  <!-- @implements A-SPEC-209 -->
8
+ ## [0.3.0] - 2026-08-31
9
+
10
+ The semantic layer, shipped the way the measurements allow: a 3-tier semantic search over the knowledge graph, surfaced only as rerank, evidence and alternates — never as a hard filter — plus a graded impact surface with its Phase-B yardstick, and the governance hardening batch.
11
+
12
+ ### Added
13
+ - **3-tier semantic search (REQ-476/477)**: local `Xenova/bge-m3` (CLS pooling — frozen after a mean-pooling instrument accident cost 4.4×) and opt-in cloud `gemini-embedding-001`. Vectors live in a content-hash `VectorCache` (`.ax/cpg_cache/`, gitignored); `rtm_reindex` warms document vectors; the analysis hot path only LOOKS UP vectors and never writes disk (the read-only contract is pinned). Credentials resolve env → macOS keychain → `~/.holmes/credentials.json` (0600) via `holmes-kit semantic-key set|unset|status` (stdin only — the key never appears in argv, logs or reports); `doctor` reports the active tier/key source and WARNs on an in-tree `.env` key; the pre-tool-use gate denies an agent self-granting the key env vars (egress self-consent).
14
+ - **Uncited semantic head rerank (REQ-478) + direct-path parity (REQ-496)**: on requests that cite no spec, `maintenance_analyze` and `issue_localize` rerank the candidate head by cached-vector cosine (set fixed, covered candidates only, why-line attached). Measured +26%/+93% head accuracy on the two uncited corpora, replicated 3/3 on holdout windows (+26~122%); citation-rich requests are never reranked (−61% measured harm guards the gate).
15
+ - **Semantic verification evidence (REQ-486)**: cloud tier attaches `sem-verify(<model>, cos=…, τ0.65 pass|fail)` to candidates and `semCos` to `rankedImpact` rows — display-only calibration (the ×1.7 precision multiple replicated in three independent windows, but its recall cost proved window-dependent, so it ships as evidence, never as a filter).
16
+ - **`semanticAlternates` (REQ-493/494)**: the top-3 semantic matches the lexical emission missed, as an additive field — measured reach ceiling gain ×1.25–×2.25 in lexically-weak windows with precision rising too; sets, orders and gates untouched.
17
+ - **Graded impact surface `impacts.rankedImpact` (REQ-467~469)**: personalized-PageRank grading (uni-d0.3-h0, k=10), the first of 29 graph mechanisms to win both impact metrics on all three corpora (HOLMES recall 0.035→0.571); the gate-facing closure is unchanged.
18
+ - **Impact Phase-B yardstick (REQ-470)**: a frozen string-property baseline (same-dir/stem/prefix) scored beside `rankedImpact` on every replay — the shipped grading beats it on both metrics on all three corpora (up to ×17 recall), meeting the NECESSARY condition for any better-than-a-person phrasing on this axis.
19
+ - **Consumer contract in the tool description (REQ-495)**: `maintenance_analyze` now tells the consuming agent the three evidence surfaces exist and how to use them.
20
+ - **Measurement plumbing (flag-gated, absent by default)**: replay window offsets, per-case blind dumps with body excerpts, verification/gate/ceiling arms — none of it changes an un-flagged run; the pinned numbers did not move.
21
+ - **Governance hardening batch (S-448~S-470, previously committed)**: gate-bypass closures (20-syntax enumeration), constitutional backstop, double-spend post-detection on the approval ledger, doctor visibility.
22
+
23
+ ### Changed
24
+ - **`maintenance_analyze` description**: carries the semantic-evidence consumer contract; its length pin moved with cause (1200→1600).
25
+
26
+ ### Unverified (named)
27
+ - Windows execution and CI remain program-level gaps. The judgment-axis result (content-access agent selection P 0.744 ≈ 83% of the modeled human precision band) is a self-judged pilot; the official capability grade stays L2 until an independent judge replicates it (protocol package pre-generated). The modeled-human comparison and its per-axis verdicts live in `docs/research/human-engineer-modeling-2026-08-30.md` (revisions 4–5).
28
+
29
+ ## [0.2.1] - 2026-08-28
30
+
31
+ Codex governance that is actually wired: the MCP server holmes-kit writes for Codex now lives where Codex reads it.
32
+
33
+ ### Fixed
34
+ - **Codex MCP wiring goes where Codex actually reads it (REQ-266)**: `init` wrote the Codex harness's MCP server to a project-local `.codex/mcp_config.json` (JSON) — a file Codex never loads. Codex reads `[mcp_servers.*]` tables from `.codex/config.toml` (TOML), so **every Codex wiring since it was added was dead**, and because Codex enforces no hooks (`HARNESS_ENFORCES.codex = false`) that MCP server is Codex's *only* governance path — so Codex was wired but ungoverned. `init` now writes `[mcp_servers.holmes-kit]` into `.codex/config.toml`, **merging** so the user's other Codex settings and servers are preserved byte-for-byte (the same discipline as the `.mcp.json` merge), migrates away the obsolete `.codex/mcp_config.json` (only after the new write is confirmed), and `doctor`'s `codex wiring` check now verifies the file Codex reads (a leftover JSON with no `config.toml` is a WARN). The merge is string/bracket-aware — a `[mcp_servers.holmes-kit]` line inside a neighbor's multi-line string or array is not mistaken for a real table, and an unrecognized foreign header always closes our region rather than swallowing it. The three-harness parity meta-test now asserts not only identical launch command/args but that each harness's wiring lands where that harness reads it. Existing Codex projects: re-run `holmes-kit init --agent codex` (doctor will flag the stale wiring).
35
+ - **`.codex/config.toml` is protected from self-disarm like its siblings (REQ-266)**: because that file is now Codex's sole governance path, an un-approved `Write`/`Edit` to it is denied for a config-write approval, exactly as `.mcp.json` and `.agents/*` already were — a session can no longer quietly re-point or delete Codex's MCP wiring.
36
+
37
+ ### Changed
38
+ - **`init` pre-selects every harness by default (REQ-266)**: the interactive harness picker now checks all harnesses (Claude Code, Antigravity, Codex) by default, matching its own "Select All (Recommended)" affordance; Codex was previously left unchecked. Deselecting a harness is one keystroke; a silently un-wired harness is not.
39
+
40
+ ### Unverified (named)
41
+ - `doctor`'s `codex wiring` check does not WARN on an out-of-date npx pin the way the `.mcp.json` version-drift check does; porting that drift WARN to `.codex/config.toml` is a named follow-up (A-SPEC-251.2 family), not a wiring defect.
42
+
8
43
  ## [0.2.0] - 2026-08-27
9
44
 
10
45
  The approval decision surface, world-tier: the operator stays at one screen, and a grant is exactly what they saw.
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- f7a048d-mtbm5atb
1
+ 503a295-mth8qak7
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The measurement ARM around the PPR primitive — everything the replay harness needs to score
3
+ * associative recall beside the shipped ranker, and nothing the product itself consumes.
4
+ *
5
+ * Two modes, scored separately because they answer different questions and past mechanisms died
6
+ * on exactly this distinction (admission cost the spec layer 33% precision; caller closure added
7
+ * 59 files with zero correct):
8
+ * - rerank: reorder the PRODUCT's candidate pool by mass. The pool is fixed; only aiming moves.
9
+ * - admit: rank every file by mass, pool ignored. Measures what association alone would name.
10
+ */
11
+ import { type WeightProfile } from './ppr';
12
+ export interface PprArmConfig {
13
+ label: string;
14
+ d: number;
15
+ weights: WeightProfile;
16
+ }
17
+ /** Citation seeds outweigh term seeds: the one signal measured strong on this corpus is the
18
+ * request naming its spec (recall 0.601→0.851 when extraction stopped mangling the ids). */
19
+ export declare const SEED_CITATION_WEIGHT = 10;
20
+ export interface GraphView {
21
+ edges: Array<{
22
+ src: string;
23
+ dst: string;
24
+ rel: string;
25
+ }>;
26
+ /** Node ids, lowercased once, paired with the original — term matching is case-insensitive. */
27
+ nodes: Array<{
28
+ id: string;
29
+ lower: string;
30
+ }>;
31
+ }
32
+ /** Parse the canonical dump (N/E TSV rows) once per case. */
33
+ export declare function graphViewOf(dump: string): GraphView;
34
+ /**
35
+ * Seeds from the request text, reusing the shipped extractors (A-SPEC-349: a second extractor is a
36
+ * second thing to drift): cited spec ids seed their SPEC nodes at fixed high weight; significant
37
+ * terms seed every node whose identifier contains them, at the term's IDF weight.
38
+ */
39
+ export declare function seedNodes(request: string, specIds: ReadonlySet<string>, view: GraphView, idf: ReadonlyMap<string, number>): Map<string, number>;
40
+ /** Aggregate node mass to source files: `CODE:sym@path` and `FILE:path` both land on `path`. */
41
+ export declare function fileMass(mass: ReadonlyMap<string, number>): Map<string, number>;
42
+ /** rerank mode: the pool is the product's; only the order moves. Zero-mass files keep their
43
+ * original relative order behind every massed file — a stable sort so the arm never DELETES an
44
+ * answer the pool already had (the ranker-deletes-its-own-answer lesson). */
45
+ export declare function rerankPool(pool: readonly string[], fm: ReadonlyMap<string, number>): string[];
46
+ /** admit mode: association alone, top-N by mass, nothing back-filled — a blur here would make the
47
+ * admission cost unattributable. */
48
+ export declare function admitRank(fm: ReadonlyMap<string, number>, topN: number): string[];
49
+ /** One case, one config: both modes from a single spread. */
50
+ export declare function pprCaseRanking(view: GraphView, seeds: ReadonlyMap<string, number>, pool: readonly string[], topN: number, config: PprArmConfig): {
51
+ rerank: string[];
52
+ admit: string[];
53
+ };
54
+ /**
55
+ * @implements A-SPEC-468 — the impact-axis grading arm. The incumbent there is an UNGRADED
56
+ * transitive closure: measured, it reaches 84% of a commit's co-changed files and names 11% of
57
+ * them, with only 3 answers in the whole corpus sitting behind any stop — the bottleneck is
58
+ * selection within reach, which is exactly what damped mass grades. The seed FILE itself is
59
+ * excluded from the prediction: predicting "the changed file will change" is vacuously true and
60
+ * would inflate every metric it touches.
61
+ */
62
+ export declare function pprImpactPredict(view: GraphView, seeds: ReadonlyMap<string, number>, seedFile: string, k: number, config: PprArmConfig): string[];
63
+ /**
64
+ * @implements A-SPEC-469 — the SCORED form the product ships. One code path serves the
65
+ * measurement (pprImpactPredict above is a thin wrapper) and the shipped surface, so the S-484
66
+ * numbers transfer by identity rather than by reimplementation. The whole changed set is excluded
67
+ * — with several changed files, each predicting another member of the same commit is the same
68
+ * vacuous truth as predicting itself.
69
+ */
70
+ export declare function pprImpactRanked(view: GraphView, seeds: ReadonlyMap<string, number>, excludeFiles: ReadonlySet<string>, k: number, config: PprArmConfig): Array<{
71
+ file: string;
72
+ score: number;
73
+ }>;
74
+ /**
75
+ * @implements A-SPEC-469 — the adopted configuration, frozen to the S-484 three-corpus winner:
76
+ * uni-d0.3-h0 at k=10 beat the ungraded closure on BOTH recall and precision on every corpus
77
+ * (HOLMES 0.0347/0.0413 → 0.5713/0.1611; JARVIS 0.0980/0.0125 → 0.3333/0.0588; GEMINI
78
+ * 0.3143/0.0203 → 0.3611/0.0533). Change these numbers only with a new sweep beside them.
79
+ */
80
+ export declare const RANKED_IMPACT_CONFIG: PprArmConfig;
81
+ export declare const RANKED_IMPACT_K = 10;
82
+ /**
83
+ * @implements A-SPEC-471 — the emission rule, the knob that models the human high-precision
84
+ * profile ("say only what you are sure of"): keep everything scoring at least alpha of the top
85
+ * mass, cap at k. The RANKING never moves — only how much of it is spoken. Pure; sorts defensively
86
+ * so a caller's ordering mistake cannot masquerade as a confidence cut.
87
+ */
88
+ export declare function emitByRule(scored: ReadonlyArray<{
89
+ file: string;
90
+ score: number;
91
+ }>, alpha: number, k: number): string[];
92
+ /** The first sweep, small on purpose: d × hub × two direction profiles. Values are recorded with
93
+ * the numbers they produce; widening the sweep is a decision the first numbers make. */
94
+ export declare function defaultPprConfigs(): PprArmConfig[];
@@ -0,0 +1,187 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RANKED_IMPACT_K = exports.RANKED_IMPACT_CONFIG = exports.SEED_CITATION_WEIGHT = void 0;
4
+ exports.graphViewOf = graphViewOf;
5
+ exports.seedNodes = seedNodes;
6
+ exports.fileMass = fileMass;
7
+ exports.rerankPool = rerankPool;
8
+ exports.admitRank = admitRank;
9
+ exports.pprCaseRanking = pprCaseRanking;
10
+ exports.pprImpactPredict = pprImpactPredict;
11
+ exports.pprImpactRanked = pprImpactRanked;
12
+ exports.emitByRule = emitByRule;
13
+ exports.defaultPprConfigs = defaultPprConfigs;
14
+ // @implements A-SPEC-467
15
+ /**
16
+ * The measurement ARM around the PPR primitive — everything the replay harness needs to score
17
+ * associative recall beside the shipped ranker, and nothing the product itself consumes.
18
+ *
19
+ * Two modes, scored separately because they answer different questions and past mechanisms died
20
+ * on exactly this distinction (admission cost the spec layer 33% precision; caller closure added
21
+ * 59 files with zero correct):
22
+ * - rerank: reorder the PRODUCT's candidate pool by mass. The pool is fixed; only aiming moves.
23
+ * - admit: rank every file by mass, pool ignored. Measures what association alone would name.
24
+ */
25
+ const ppr_1 = require("./ppr");
26
+ const localize_1 = require("../rtm/localize");
27
+ /** Citation seeds outweigh term seeds: the one signal measured strong on this corpus is the
28
+ * request naming its spec (recall 0.601→0.851 when extraction stopped mangling the ids). */
29
+ exports.SEED_CITATION_WEIGHT = 10;
30
+ /** Parse the canonical dump (N/E TSV rows) once per case. */
31
+ function graphViewOf(dump) {
32
+ const edges = [];
33
+ const nodes = [];
34
+ for (const line of dump.split('\n')) {
35
+ const cols = line.split('\t');
36
+ if (cols[0] === 'E' && cols[1] && cols[2] && cols[3])
37
+ edges.push({ src: cols[1], dst: cols[2], rel: cols[3] });
38
+ else if (cols[0] === 'N' && cols[1])
39
+ nodes.push({ id: cols[1], lower: cols[1].toLowerCase() });
40
+ }
41
+ return { edges, nodes };
42
+ }
43
+ /**
44
+ * Seeds from the request text, reusing the shipped extractors (A-SPEC-349: a second extractor is a
45
+ * second thing to drift): cited spec ids seed their SPEC nodes at fixed high weight; significant
46
+ * terms seed every node whose identifier contains them, at the term's IDF weight.
47
+ */
48
+ function seedNodes(request, specIds, view, idf) {
49
+ const seeds = new Map();
50
+ for (const id of (0, localize_1.citationsIn)(request, specIds).cited) {
51
+ seeds.set(`SPEC:${id}`, (seeds.get(`SPEC:${id}`) ?? 0) + exports.SEED_CITATION_WEIGHT);
52
+ }
53
+ const terms = (0, localize_1.significantTerms)(request).slice(0, 16).map((t) => t.toLowerCase()).filter((t) => t.length >= 3);
54
+ for (const n of view.nodes) {
55
+ for (const t of terms) {
56
+ if (!n.lower.includes(t))
57
+ continue;
58
+ seeds.set(n.id, (seeds.get(n.id) ?? 0) + (idf.get(t) ?? 1));
59
+ }
60
+ }
61
+ return seeds;
62
+ }
63
+ /** Aggregate node mass to source files: `CODE:sym@path` and `FILE:path` both land on `path`. */
64
+ function fileMass(mass) {
65
+ const fm = new Map();
66
+ for (const [id, m] of mass) {
67
+ let file = null;
68
+ if (id.startsWith('CODE:')) {
69
+ const at = id.lastIndexOf('@');
70
+ if (at > 0)
71
+ file = id.slice(at + 1);
72
+ }
73
+ else if (id.startsWith('FILE:')) {
74
+ file = id.slice('FILE:'.length);
75
+ }
76
+ if (file !== null && file !== '')
77
+ fm.set(file, (fm.get(file) ?? 0) + m);
78
+ }
79
+ return fm;
80
+ }
81
+ /** rerank mode: the pool is the product's; only the order moves. Zero-mass files keep their
82
+ * original relative order behind every massed file — a stable sort so the arm never DELETES an
83
+ * answer the pool already had (the ranker-deletes-its-own-answer lesson). */
84
+ function rerankPool(pool, fm) {
85
+ return pool
86
+ .map((file, i) => ({ file, i, m: fm.get(file) ?? 0 }))
87
+ .sort((a, b) => (b.m - a.m) || (a.i - b.i))
88
+ .map((x) => x.file);
89
+ }
90
+ /** admit mode: association alone, top-N by mass, nothing back-filled — a blur here would make the
91
+ * admission cost unattributable. */
92
+ function admitRank(fm, topN) {
93
+ return [...fm.entries()]
94
+ .filter(([, m]) => m > 0)
95
+ .sort((a, b) => (b[1] - a[1]) || (a[0] < b[0] ? -1 : 1))
96
+ .slice(0, topN)
97
+ .map(([file]) => file);
98
+ }
99
+ /** One case, one config: both modes from a single spread. */
100
+ function pprCaseRanking(view, seeds, pool, topN, config) {
101
+ if (seeds.size === 0)
102
+ return { rerank: [...pool], admit: [] };
103
+ const adj = (0, ppr_1.buildAdjacency)(view.edges, config.weights);
104
+ const mass = (0, ppr_1.personalizedPageRank)(adj, seeds, { d: config.d });
105
+ const fm = fileMass(mass);
106
+ return { rerank: rerankPool(pool, fm), admit: admitRank(fm, topN) };
107
+ }
108
+ /**
109
+ * @implements A-SPEC-468 — the impact-axis grading arm. The incumbent there is an UNGRADED
110
+ * transitive closure: measured, it reaches 84% of a commit's co-changed files and names 11% of
111
+ * them, with only 3 answers in the whole corpus sitting behind any stop — the bottleneck is
112
+ * selection within reach, which is exactly what damped mass grades. The seed FILE itself is
113
+ * excluded from the prediction: predicting "the changed file will change" is vacuously true and
114
+ * would inflate every metric it touches.
115
+ */
116
+ function pprImpactPredict(view, seeds, seedFile, k, config) {
117
+ return pprImpactRanked(view, seeds, new Set([seedFile]), k, config).map((r) => r.file);
118
+ }
119
+ /**
120
+ * @implements A-SPEC-469 — the SCORED form the product ships. One code path serves the
121
+ * measurement (pprImpactPredict above is a thin wrapper) and the shipped surface, so the S-484
122
+ * numbers transfer by identity rather than by reimplementation. The whole changed set is excluded
123
+ * — with several changed files, each predicting another member of the same commit is the same
124
+ * vacuous truth as predicting itself.
125
+ */
126
+ function pprImpactRanked(view, seeds, excludeFiles, k, config) {
127
+ if (seeds.size === 0)
128
+ return [];
129
+ const adj = (0, ppr_1.buildAdjacency)(view.edges, config.weights);
130
+ const mass = (0, ppr_1.personalizedPageRank)(adj, seeds, { d: config.d });
131
+ const fm = fileMass(mass);
132
+ for (const f of excludeFiles)
133
+ fm.delete(f);
134
+ return [...fm.entries()]
135
+ .filter(([, m]) => m > 0)
136
+ .sort((a, b) => (b[1] - a[1]) || (a[0] < b[0] ? -1 : 1))
137
+ .slice(0, k)
138
+ .map(([file, score]) => ({ file, score: Math.round(score * 10000) / 10000 }));
139
+ }
140
+ /**
141
+ * @implements A-SPEC-469 — the adopted configuration, frozen to the S-484 three-corpus winner:
142
+ * uni-d0.3-h0 at k=10 beat the ungraded closure on BOTH recall and precision on every corpus
143
+ * (HOLMES 0.0347/0.0413 → 0.5713/0.1611; JARVIS 0.0980/0.0125 → 0.3333/0.0588; GEMINI
144
+ * 0.3143/0.0203 → 0.3611/0.0533). Change these numbers only with a new sweep beside them.
145
+ */
146
+ exports.RANKED_IMPACT_CONFIG = {
147
+ label: 'uni-d0.3-h0',
148
+ d: 0.3,
149
+ weights: {
150
+ rels: { implements: { fwd: 2, back: 2 }, calls: { fwd: 1, back: 1 }, imports: { fwd: 0.5, back: 0.5 } },
151
+ hubExponent: 0,
152
+ },
153
+ };
154
+ exports.RANKED_IMPACT_K = 10;
155
+ /**
156
+ * @implements A-SPEC-471 — the emission rule, the knob that models the human high-precision
157
+ * profile ("say only what you are sure of"): keep everything scoring at least alpha of the top
158
+ * mass, cap at k. The RANKING never moves — only how much of it is spoken. Pure; sorts defensively
159
+ * so a caller's ordering mistake cannot masquerade as a confidence cut.
160
+ */
161
+ function emitByRule(scored, alpha, k) {
162
+ if (scored.length === 0)
163
+ return [];
164
+ const sorted = [...scored].sort((a, b) => (b.score - a.score) || (a.file < b.file ? -1 : 1));
165
+ const cut = alpha * sorted[0].score;
166
+ return sorted.filter((r) => r.score >= cut).slice(0, k).map((r) => r.file);
167
+ }
168
+ /** The first sweep, small on purpose: d × hub × two direction profiles. Values are recorded with
169
+ * the numbers they produce; widening the sweep is a decision the first numbers make. */
170
+ function defaultPprConfigs() {
171
+ const uniform = (hub) => ({
172
+ rels: { implements: { fwd: 2, back: 2 }, calls: { fwd: 1, back: 1 }, imports: { fwd: 0.5, back: 0.5 } },
173
+ hubExponent: hub,
174
+ });
175
+ const callers = (hub) => ({
176
+ rels: { implements: { fwd: 2, back: 2 }, calls: { fwd: 0.3, back: 1 }, imports: { fwd: 0.5, back: 0.5 } },
177
+ hubExponent: hub,
178
+ });
179
+ const out = [];
180
+ for (const d of [0.3, 0.5, 0.85]) {
181
+ for (const hub of [0, 0.5]) {
182
+ out.push({ label: `uni-d${d}-h${hub}`, d, weights: uniform(hub) });
183
+ out.push({ label: `call-d${d}-h${hub}`, d, weights: callers(hub) });
184
+ }
185
+ }
186
+ return out;
187
+ }
@@ -0,0 +1,21 @@
1
+ import { type GraphView } from './assoc-arm';
2
+ export interface ExploreInput {
3
+ view: GraphView;
4
+ seedFile: string;
5
+ seedNodeIds: string[];
6
+ issueText: string;
7
+ idf: ReadonlyMap<string, number>;
8
+ /** Arm A's precomputed ranking (the static surface). */
9
+ staticList: readonly string[];
10
+ }
11
+ export interface ExploreOpts {
12
+ E?: number;
13
+ R?: number;
14
+ verify: (file: string) => boolean;
15
+ }
16
+ export interface RoundResult {
17
+ round: number;
18
+ emitted: string[];
19
+ hits: string[];
20
+ }
21
+ export declare function exploreIterative(input: ExploreInput, arm: 'A' | 'B' | 'C' | 'D', opts: ExploreOpts): RoundResult[];
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.exploreIterative = exploreIterative;
4
+ // @implements A-SPEC-472
5
+ /**
6
+ * The three-arm iteration experiment: does an agent that FEEDS BACK what it learned each round
7
+ * beat the same budget spent statically?
8
+ *
9
+ * Arms, identical budget and oracle, differing ONLY in adaptation policy:
10
+ * A — none: consume a precomputed static ranking (the S-486 shipped surface's list).
11
+ * B — re-seed: confirmed files join the PPR seeds; the walk re-spreads each round.
12
+ * C — judged expansion: typed edge candidates from the frontier, scored by a FROZEN
13
+ * deterministic judge (lex + confirmation + type prior, equal weights — no tuning before
14
+ * the first numbers; the judgment axis has a measured sign-flip precedent and earns nothing
15
+ * on faith).
16
+ *
17
+ * The oracle (`verify`) simulates a developer actually checking a candidate. It is called on
18
+ * EXACTLY the emitted set — consulting it about unemitted files would leak ground truth into the
19
+ * trajectory, and the whole comparison would be theater. The loop owns that invariant; a test
20
+ * pins it with a spy.
21
+ *
22
+ * Literature this operationalizes (measured wins, see docs/research/iterative-graph-exploration):
23
+ * ToG's explore-think alternation, LocAgent's graph-tool traversal, KGCompass's finding that
24
+ * 89.7% of its successful localizations were reachable only by multi-hop traversal.
25
+ */
26
+ const ppr_1 = require("./ppr");
27
+ const assoc_arm_1 = require("./assoc-arm");
28
+ const localize_1 = require("../rtm/localize");
29
+ const TYPE_PRIOR = { implements: 1.0, calls: 0.8, imports: 0.5 };
30
+ const fileOfNode = (id) => {
31
+ if (id.startsWith('CODE:')) {
32
+ const at = id.lastIndexOf('@');
33
+ return at > 0 ? id.slice(at + 1) : null;
34
+ }
35
+ if (id.startsWith('FILE:'))
36
+ return id.slice('FILE:'.length);
37
+ return null;
38
+ };
39
+ function exploreIterative(input, arm, opts) {
40
+ const E = opts.E ?? 2;
41
+ const R = opts.R ?? 10;
42
+ const emittedEver = new Set([input.seedFile]);
43
+ const confirmed = new Set();
44
+ const rounds = [];
45
+ // Precompute the structures every round reuses.
46
+ const adj = arm === 'B' ? (0, ppr_1.buildAdjacency)(input.view.edges, assoc_arm_1.RANKED_IMPACT_CONFIG.weights) : null;
47
+ // D = C's judged candidates first; the SHORTFALL — and only the shortfall — from the static
48
+ // ranking (@implements A-SPEC-473). C's measured failure was supply, not judgment: GEMINI used
49
+ // 9.4 of a 20-emission budget and saturated below the static arm. A fallback confirmation
50
+ // re-grows the frontier, so the starvation self-heals.
51
+ const nodesOfFile = new Map();
52
+ for (const n of input.view.nodes) {
53
+ const f = fileOfNode(n.id);
54
+ if (f !== null)
55
+ (nodesOfFile.get(f) ?? nodesOfFile.set(f, []).get(f)).push(n.id);
56
+ }
57
+ const terms = (0, localize_1.significantTerms)(input.issueText).slice(0, 16).map((t) => t.toLowerCase()).filter((t) => t.length >= 3);
58
+ const maxIdf = Math.max(1, ...[...input.idf.values()]);
59
+ const candidatesC = () => {
60
+ // Frontier = the seed file plus everything confirmed so far.
61
+ const frontier = new Set([input.seedFile, ...confirmed]);
62
+ const frontierNodes = new Set();
63
+ for (const f of frontier)
64
+ for (const id of nodesOfFile.get(f) ?? [])
65
+ frontierNodes.add(id);
66
+ // Typed expansion: calls both ways, implements-siblings via the shared SPEC, imports both ways.
67
+ const specsOfFrontier = new Set();
68
+ const reach = new Map();
69
+ const touch = (candFile, srcFile, prior) => {
70
+ if (candFile === null || frontier.has(candFile))
71
+ return;
72
+ const r = reach.get(candFile) ?? reach.set(candFile, { sources: new Set(), type: 0 }).get(candFile);
73
+ r.sources.add(srcFile);
74
+ r.type = Math.max(r.type, prior);
75
+ };
76
+ for (const e of input.view.edges) {
77
+ const sf = fileOfNode(e.src);
78
+ const df = fileOfNode(e.dst);
79
+ if (e.rel === 'implements' && sf !== null && frontier.has(sf))
80
+ specsOfFrontier.add(e.dst);
81
+ if (e.rel === 'calls' || e.rel === 'imports') {
82
+ const prior = TYPE_PRIOR[e.rel];
83
+ if (sf !== null && frontier.has(sf))
84
+ touch(df, sf, prior);
85
+ if (df !== null && frontier.has(df))
86
+ touch(sf, df, prior);
87
+ }
88
+ }
89
+ for (const e of input.view.edges) {
90
+ if (e.rel !== 'implements' || !specsOfFrontier.has(e.dst))
91
+ continue;
92
+ const sf = fileOfNode(e.src);
93
+ if (sf !== null && !frontier.has(sf))
94
+ touch(sf, `spec:${e.dst}`, TYPE_PRIOR.implements);
95
+ }
96
+ const lexOf = (file) => {
97
+ let best = 0;
98
+ for (const id of nodesOfFile.get(file) ?? []) {
99
+ const lower = id.toLowerCase();
100
+ for (const t of terms)
101
+ if (lower.includes(t))
102
+ best = Math.max(best, (input.idf.get(t) ?? 1) / maxIdf);
103
+ }
104
+ return best;
105
+ };
106
+ return [...reach.entries()].map(([file, r]) => ({
107
+ file,
108
+ score: lexOf(file) + Math.min(r.sources.size, 3) / 3 + r.type,
109
+ }));
110
+ };
111
+ for (let round = 1; round <= R; round++) {
112
+ let ranked;
113
+ if (arm === 'A') {
114
+ ranked = input.staticList.filter((f) => !emittedEver.has(f));
115
+ }
116
+ else if (arm === 'B') {
117
+ const seeds = new Map();
118
+ for (const id of input.seedNodeIds)
119
+ seeds.set(id, 1);
120
+ for (const f of confirmed)
121
+ for (const id of nodesOfFile.get(f) ?? [])
122
+ seeds.set(id, 1);
123
+ const fm = (0, assoc_arm_1.fileMass)((0, ppr_1.personalizedPageRank)(adj, seeds, { d: assoc_arm_1.RANKED_IMPACT_CONFIG.d }));
124
+ ranked = [...fm.entries()]
125
+ .filter(([f, m]) => m > 0 && !emittedEver.has(f))
126
+ .sort((a, b) => (b[1] - a[1]) || (a[0] < b[0] ? -1 : 1))
127
+ .map(([f]) => f);
128
+ }
129
+ else {
130
+ ranked = candidatesC()
131
+ .filter((c) => !emittedEver.has(c.file))
132
+ .sort((a, b) => (b.score - a.score) || (a.file < b.file ? -1 : 1))
133
+ .map((c) => c.file);
134
+ if (arm === 'D' && ranked.length < E) {
135
+ const have = new Set(ranked);
136
+ for (const f of input.staticList) {
137
+ if (ranked.length >= E)
138
+ break;
139
+ if (!emittedEver.has(f) && !have.has(f)) {
140
+ ranked.push(f);
141
+ have.add(f);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ const emitted = ranked.slice(0, E);
147
+ if (emitted.length === 0)
148
+ break;
149
+ const hits = [];
150
+ for (const f of emitted) {
151
+ emittedEver.add(f);
152
+ if (opts.verify(f)) {
153
+ hits.push(f);
154
+ confirmed.add(f);
155
+ }
156
+ }
157
+ rounds.push({ round, emitted, hits });
158
+ }
159
+ return rounds;
160
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The impact axis's Phase-B external yardstick — a deliberately POOR baseline: the first
3
+ * instinct of a person who just saw the changed file, using string properties only. No graph,
4
+ * no specs, no IDF, no history; the poverty is the point (operating principle 10: any
5
+ * better-than-a-person phrasing needs this comparison as its NECESSARY condition).
6
+ *
7
+ * Frozen scoring (pre-registered in REQ-470, adjusted only via a new slice):
8
+ * 3 × [same directory as the seed]
9
+ * + 2 × [shares the seed's stem (≥4 chars; test/spec/d suffix tokens stripped)]
10
+ * + 1 × per shared leading directory segment.
11
+ * Zero scores are not emitted; ties break lexicographically; the seed never appears.
12
+ */
13
+ export declare function impactBaselinePredict(seed: string, files: readonly string[], k?: number): string[];
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ // @implements A-SPEC-470
3
+ /**
4
+ * The impact axis's Phase-B external yardstick — a deliberately POOR baseline: the first
5
+ * instinct of a person who just saw the changed file, using string properties only. No graph,
6
+ * no specs, no IDF, no history; the poverty is the point (operating principle 10: any
7
+ * better-than-a-person phrasing needs this comparison as its NECESSARY condition).
8
+ *
9
+ * Frozen scoring (pre-registered in REQ-470, adjusted only via a new slice):
10
+ * 3 × [same directory as the seed]
11
+ * + 2 × [shares the seed's stem (≥4 chars; test/spec/d suffix tokens stripped)]
12
+ * + 1 × per shared leading directory segment.
13
+ * Zero scores are not emitted; ties break lexicographically; the seed never appears.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.impactBaselinePredict = impactBaselinePredict;
17
+ const SUFFIX_TOKENS = new Set(['test', 'spec', 'd']);
18
+ const stemOf = (file) => {
19
+ const base = file.slice(file.lastIndexOf('/') + 1).replace(/\.[^.]+$/, '');
20
+ const tokens = base.split(/[._-]/).filter((t) => t.length > 0);
21
+ while (tokens.length > 1 && SUFFIX_TOKENS.has(tokens[tokens.length - 1]))
22
+ tokens.pop();
23
+ return tokens[0] ?? '';
24
+ };
25
+ const dirOf = (file) => file.slice(0, Math.max(0, file.lastIndexOf('/')));
26
+ const sharedPrefixDepth = (a, b) => {
27
+ const as = dirOf(a).split('/');
28
+ const bs = dirOf(b).split('/');
29
+ let n = 0;
30
+ while (n < as.length && n < bs.length && as[n] === bs[n] && as[n] !== '')
31
+ n++;
32
+ return n;
33
+ };
34
+ function impactBaselinePredict(seed, files, k = 10) {
35
+ const seedDir = dirOf(seed);
36
+ const seedStem = stemOf(seed);
37
+ const scored = [];
38
+ for (const f of files) {
39
+ if (f === seed)
40
+ continue;
41
+ const stemShare = seedStem.length >= 4 && stemOf(f) === seedStem;
42
+ const score = (dirOf(f) === seedDir ? 3 : 0) + (stemShare ? 2 : 0) + sharedPrefixDepth(f, seed);
43
+ if (score > 0)
44
+ scored.push({ file: f, score });
45
+ }
46
+ scored.sort((a, b) => b.score - a.score || (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
47
+ return scored.slice(0, k).map((x) => x.file);
48
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Personalized PageRank over the RTM/CPG edge set — the associative-recall primitive.
3
+ *
4
+ * Why this exists: the shipped retrieval primitives sit at two extremes. `issue_localize` stops at
5
+ * one spec hop (its own comment defers anything further), and `rtm_impact` returns an UNGRADED
6
+ * transitive closure — measured on the replay corpus, 84% of a commit's co-changed files are
7
+ * reachable through that closure while the analysis names 11% of them: the bottleneck is selection
8
+ * within reach, not reach. What is missing between the two is graded multi-hop mass —
9
+ * Score(1-hop) > Score(2-hop) > … > 0 — which is exactly what a damped random walk provides.
10
+ *
11
+ * Eleven graph/semantic mechanisms were measured and rejected on this corpus before this one
12
+ * (binary k-hop expansion, caller closure, co-change retrieval, dense embeddings, …). PPR is
13
+ * structurally different from all of them: it produces a RANKING over nodes rather than an
14
+ * admission set, and the geometric damping suppresses the crowd problem that sank the closure.
15
+ * That difference is the reason this module exists; whether it PAYS is decided by the replay arm
16
+ * (rerank and admit modes scored separately, three corpora), never assumed here.
17
+ *
18
+ * Deterministic, pure, no I/O. PPR mass never enters a gate decision — association proposes,
19
+ * the deterministic layers judge.
20
+ */
21
+ /** Per-rel directed weights. `fwd` follows the edge, `back` walks it in reverse — direction is
22
+ * meaning in a software graph: diagnosis walks callers-ward, impact walks callee-ward. A rel
23
+ * absent from the profile contributes nothing. */
24
+ export interface WeightProfile {
25
+ rels: Record<string, {
26
+ fwd: number;
27
+ back: number;
28
+ }>;
29
+ /** Divide each contribution INTO a node by degree^hubExponent — utility hubs otherwise soak up
30
+ * the mass of every walk that grazes them (the fan-in problem the closure already measured). */
31
+ hubExponent: number;
32
+ }
33
+ export interface PprOptions {
34
+ /** Damping: probability of continuing the walk instead of teleporting back to the seeds. */
35
+ d: number;
36
+ maxIter?: number;
37
+ /** L1 convergence threshold; 0 means run all iterations. */
38
+ eps?: number;
39
+ }
40
+ interface OutEdge {
41
+ to: string;
42
+ w: number;
43
+ }
44
+ export interface Adjacency {
45
+ /** Out-transition lists, weight-normalized to sum 1 per node. */
46
+ out: Map<string, OutEdge[]>;
47
+ nodes: Set<string>;
48
+ }
49
+ /** Build the weighted, direction-aware, hub-damped transition structure once per graph. */
50
+ export declare function buildAdjacency(edges: Iterable<{
51
+ src: string;
52
+ dst: string;
53
+ rel: string;
54
+ }>, profile: WeightProfile): Adjacency;
55
+ /**
56
+ * Power iteration: v' = d·(Wᵀv + danglingMass·s) + (1−d)·s.
57
+ *
58
+ * Dangling mass refluxes to the SEEDS, not uniformly — a node with no out-edges returns the walk
59
+ * to the question rather than to the whole graph, and this is also what keeps a disconnected
60
+ * component at exactly zero. The vector always sums to 1 (asserted by test): a distribution that
61
+ * silently leaks mass would make cross-config comparisons meaningless.
62
+ */
63
+ export declare function personalizedPageRank(adj: Adjacency, seeds: ReadonlyMap<string, number>, opts: PprOptions): Map<string, number>;
64
+ export {};