@holmes-lab/holmes-kit 0.2.1 → 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.
- package/CHANGELOG.md +21 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
- package/dist/holmes/assoc/assoc-arm.js +187 -0
- package/dist/holmes/assoc/explore.d.ts +21 -0
- package/dist/holmes/assoc/explore.js +160 -0
- package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
- package/dist/holmes/assoc/impact-baseline.js +48 -0
- package/dist/holmes/assoc/ppr.d.ts +64 -0
- package/dist/holmes/assoc/ppr.js +110 -0
- package/dist/holmes/cli/agents.d.ts +24 -11
- package/dist/holmes/cli/agents.js +93 -21
- package/dist/holmes/cli/doctor.d.ts +56 -0
- package/dist/holmes/cli/doctor.js +311 -5
- package/dist/holmes/cli/index.js +13 -1
- package/dist/holmes/cli/semantic-key.d.ts +19 -0
- package/dist/holmes/cli/semantic-key.js +93 -0
- package/dist/holmes/config/config.d.ts +9 -0
- package/dist/holmes/config/config.js +8 -1
- package/dist/holmes/cpg/language-capability.d.ts +65 -0
- package/dist/holmes/cpg/language-capability.js +145 -0
- package/dist/holmes/cpg/language-parser-walk.js +179 -34
- package/dist/holmes/cpg/language-parser.d.ts +1 -1
- package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
- package/dist/holmes/governance/ledger-rechain.js +17 -2
- package/dist/holmes/governance/provenance-ledger.js +21 -0
- package/dist/holmes/guardrail/blind-spots.js +12 -1
- package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
- package/dist/holmes/guardrail/impact-gate.js +263 -0
- package/dist/holmes/guardrail/write-target.d.ts +38 -1
- package/dist/holmes/guardrail/write-target.js +41 -4
- package/dist/holmes/hooks/adapters/antigravity.js +12 -1
- package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
- package/dist/holmes/hooks/corrupt-state-run.js +16 -0
- package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
- package/dist/holmes/hooks/pre-tool-use.js +203 -11
- package/dist/holmes/hooks/stop.d.ts +65 -0
- package/dist/holmes/hooks/stop.js +200 -2
- package/dist/holmes/mcp/handlers.d.ts +88 -0
- package/dist/holmes/mcp/handlers.js +597 -16
- package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
- package/dist/holmes/mcp/maintenance-analyze.js +994 -0
- package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
- package/dist/holmes/mcp/maintenance-evidence.js +253 -0
- package/dist/holmes/mcp/tool-schemas.js +71 -0
- package/dist/holmes/project/root.js +3 -1
- package/dist/holmes/review/baseline-arm.d.ts +37 -0
- package/dist/holmes/review/baseline-arm.js +51 -0
- package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
- package/dist/holmes/review/captured-stdin-guard.js +48 -0
- package/dist/holmes/review/coherence-verify.d.ts +31 -0
- package/dist/holmes/review/coherence-verify.js +144 -0
- package/dist/holmes/review/commit-text.d.ts +50 -0
- package/dist/holmes/review/commit-text.js +76 -0
- package/dist/holmes/review/confidence-calibration.d.ts +39 -0
- package/dist/holmes/review/confidence-calibration.js +39 -0
- package/dist/holmes/review/content-baseline.d.ts +38 -0
- package/dist/holmes/review/content-baseline.js +103 -0
- package/dist/holmes/review/content-verify.d.ts +20 -0
- package/dist/holmes/review/content-verify.js +73 -0
- package/dist/holmes/review/dense-retrieval.d.ts +66 -0
- package/dist/holmes/review/dense-retrieval.js +97 -0
- package/dist/holmes/review/edge-quality.d.ts +44 -0
- package/dist/holmes/review/edge-quality.js +117 -0
- package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
- package/dist/holmes/review/evaluation-metrics.js +175 -0
- package/dist/holmes/review/graph-verifier.d.ts +34 -0
- package/dist/holmes/review/graph-verifier.js +62 -0
- package/dist/holmes/review/hop-ablation.d.ts +100 -0
- package/dist/holmes/review/hop-ablation.js +89 -0
- package/dist/holmes/review/manual-baseline.d.ts +209 -0
- package/dist/holmes/review/manual-baseline.js +2846 -0
- package/dist/holmes/review/oracle-gap.d.ts +32 -0
- package/dist/holmes/review/oracle-gap.js +102 -0
- package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
- package/dist/holmes/review/point-in-time-replay.js +161 -0
- package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
- package/dist/holmes/review/rank-diagnosis.js +163 -0
- package/dist/holmes/review/replay-calibration.d.ts +62 -0
- package/dist/holmes/review/replay-calibration.js +83 -0
- package/dist/holmes/review/replay-corpus.d.ts +135 -0
- package/dist/holmes/review/replay-corpus.js +210 -0
- package/dist/holmes/review/run-replay.d.ts +260 -0
- package/dist/holmes/review/run-replay.js +729 -0
- package/dist/holmes/review/semantic-arm.d.ts +271 -0
- package/dist/holmes/review/semantic-arm.js +717 -0
- package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
- package/dist/holmes/review/semantic-retrieval.js +156 -0
- package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
- package/dist/holmes/review/spec-layer-stats.js +52 -0
- package/dist/holmes/review/temporal-prior.d.ts +33 -0
- package/dist/holmes/review/temporal-prior.js +53 -0
- package/dist/holmes/review/test-runner.d.ts +15 -0
- package/dist/holmes/review/test-runner.js +41 -4
- package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
- package/dist/holmes/review/test-selection-breadth.js +57 -0
- package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
- package/dist/holmes/review/traceability-benchmark.js +135 -0
- package/dist/holmes/review/union-verify.d.ts +12 -0
- package/dist/holmes/review/union-verify.js +70 -0
- package/dist/holmes/rtm/graph-store.d.ts +51 -0
- package/dist/holmes/rtm/graph-store.js +122 -0
- package/dist/holmes/rtm/incremental.d.ts +25 -1
- package/dist/holmes/rtm/incremental.js +18 -1
- package/dist/holmes/rtm/localize.d.ts +28 -0
- package/dist/holmes/rtm/localize.js +272 -14
- package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
- package/dist/holmes/rtm/rtm-builder.js +265 -28
- package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
- package/dist/holmes/rtm/rtm-graph.js +194 -31
- package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
- package/dist/holmes/rtm/taint-benchmark.js +141 -0
- package/dist/holmes/rtm/test-scope.js +24 -1
- package/dist/holmes/semantic/credentials.d.ts +15 -0
- package/dist/holmes/semantic/credentials.js +134 -0
- package/dist/holmes/semantic/embedder.d.ts +44 -0
- package/dist/holmes/semantic/embedder.js +185 -0
- package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
- package/dist/holmes/semantic/hit-rerank.js +38 -0
- package/dist/holmes/semantic/tier.d.ts +37 -0
- package/dist/holmes/semantic/tier.js +54 -0
- package/dist/holmes/semantic/vector-cache.d.ts +11 -0
- package/dist/holmes/semantic/vector-cache.js +91 -0
- package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
- package/dist/holmes/spec/acceptance-quality.js +169 -0
- package/dist/holmes/spec/validator.js +33 -1
- package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
- package/dist/holmes/spec/yaml-scalar.js +43 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ 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
|
+
|
|
8
29
|
## [0.2.1] - 2026-08-28
|
|
9
30
|
|
|
10
31
|
Codex governance that is actually wired: the MCP server holmes-kit writes for Codex now lives where Codex reads it.
|
package/dist/.build-id
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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 {};
|