@holmes-lab/holmes-kit 0.2.1 → 0.3.1

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 (132) hide show
  1. package/CHANGELOG.md +55 -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 -21
  13. package/dist/holmes/cli/doctor.d.ts +56 -0
  14. package/dist/holmes/cli/doctor.js +311 -5
  15. package/dist/holmes/cli/index.js +13 -1
  16. package/dist/holmes/cli/semantic-key.d.ts +19 -0
  17. package/dist/holmes/cli/semantic-key.js +93 -0
  18. package/dist/holmes/config/config.d.ts +9 -0
  19. package/dist/holmes/config/config.js +8 -1
  20. package/dist/holmes/cpg/language-capability.d.ts +65 -0
  21. package/dist/holmes/cpg/language-capability.js +145 -0
  22. package/dist/holmes/cpg/language-parser-walk.js +179 -34
  23. package/dist/holmes/cpg/language-parser.d.ts +1 -1
  24. package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
  25. package/dist/holmes/governance/ledger-rechain.js +17 -2
  26. package/dist/holmes/governance/provenance-ledger.js +21 -0
  27. package/dist/holmes/guardrail/blind-spots.js +12 -1
  28. package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
  29. package/dist/holmes/guardrail/impact-gate.js +263 -0
  30. package/dist/holmes/guardrail/write-target.d.ts +38 -1
  31. package/dist/holmes/guardrail/write-target.js +41 -4
  32. package/dist/holmes/hooks/adapters/antigravity.js +12 -1
  33. package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
  34. package/dist/holmes/hooks/corrupt-state-run.js +16 -0
  35. package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
  36. package/dist/holmes/hooks/pre-tool-use.js +203 -11
  37. package/dist/holmes/hooks/stop.d.ts +65 -0
  38. package/dist/holmes/hooks/stop.js +200 -2
  39. package/dist/holmes/mcp/elicit-approval.d.ts +32 -2
  40. package/dist/holmes/mcp/elicit-approval.js +30 -2
  41. package/dist/holmes/mcp/handlers.d.ts +88 -0
  42. package/dist/holmes/mcp/handlers.js +636 -35
  43. package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
  44. package/dist/holmes/mcp/maintenance-analyze.js +994 -0
  45. package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
  46. package/dist/holmes/mcp/maintenance-evidence.js +253 -0
  47. package/dist/holmes/mcp/server.js +13 -10
  48. package/dist/holmes/mcp/tool-schemas.js +71 -0
  49. package/dist/holmes/project/root.js +3 -1
  50. package/dist/holmes/review/baseline-arm.d.ts +37 -0
  51. package/dist/holmes/review/baseline-arm.js +51 -0
  52. package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
  53. package/dist/holmes/review/captured-stdin-guard.js +48 -0
  54. package/dist/holmes/review/coherence-verify.d.ts +31 -0
  55. package/dist/holmes/review/coherence-verify.js +144 -0
  56. package/dist/holmes/review/commit-text.d.ts +50 -0
  57. package/dist/holmes/review/commit-text.js +76 -0
  58. package/dist/holmes/review/confidence-calibration.d.ts +39 -0
  59. package/dist/holmes/review/confidence-calibration.js +39 -0
  60. package/dist/holmes/review/content-baseline.d.ts +38 -0
  61. package/dist/holmes/review/content-baseline.js +103 -0
  62. package/dist/holmes/review/content-verify.d.ts +20 -0
  63. package/dist/holmes/review/content-verify.js +73 -0
  64. package/dist/holmes/review/dense-retrieval.d.ts +66 -0
  65. package/dist/holmes/review/dense-retrieval.js +97 -0
  66. package/dist/holmes/review/edge-quality.d.ts +44 -0
  67. package/dist/holmes/review/edge-quality.js +117 -0
  68. package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
  69. package/dist/holmes/review/evaluation-metrics.js +175 -0
  70. package/dist/holmes/review/graph-verifier.d.ts +34 -0
  71. package/dist/holmes/review/graph-verifier.js +62 -0
  72. package/dist/holmes/review/hop-ablation.d.ts +100 -0
  73. package/dist/holmes/review/hop-ablation.js +89 -0
  74. package/dist/holmes/review/manual-baseline.d.ts +209 -0
  75. package/dist/holmes/review/manual-baseline.js +2846 -0
  76. package/dist/holmes/review/oracle-gap.d.ts +32 -0
  77. package/dist/holmes/review/oracle-gap.js +102 -0
  78. package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
  79. package/dist/holmes/review/point-in-time-replay.js +161 -0
  80. package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
  81. package/dist/holmes/review/rank-diagnosis.js +163 -0
  82. package/dist/holmes/review/replay-calibration.d.ts +62 -0
  83. package/dist/holmes/review/replay-calibration.js +83 -0
  84. package/dist/holmes/review/replay-corpus.d.ts +135 -0
  85. package/dist/holmes/review/replay-corpus.js +210 -0
  86. package/dist/holmes/review/run-replay.d.ts +260 -0
  87. package/dist/holmes/review/run-replay.js +729 -0
  88. package/dist/holmes/review/semantic-arm.d.ts +271 -0
  89. package/dist/holmes/review/semantic-arm.js +717 -0
  90. package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
  91. package/dist/holmes/review/semantic-retrieval.js +156 -0
  92. package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
  93. package/dist/holmes/review/spec-layer-stats.js +52 -0
  94. package/dist/holmes/review/temporal-prior.d.ts +33 -0
  95. package/dist/holmes/review/temporal-prior.js +53 -0
  96. package/dist/holmes/review/test-runner.d.ts +15 -0
  97. package/dist/holmes/review/test-runner.js +41 -4
  98. package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
  99. package/dist/holmes/review/test-selection-breadth.js +57 -0
  100. package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
  101. package/dist/holmes/review/traceability-benchmark.js +135 -0
  102. package/dist/holmes/review/union-verify.d.ts +12 -0
  103. package/dist/holmes/review/union-verify.js +70 -0
  104. package/dist/holmes/rtm/graph-store.d.ts +51 -0
  105. package/dist/holmes/rtm/graph-store.js +122 -0
  106. package/dist/holmes/rtm/incremental.d.ts +25 -1
  107. package/dist/holmes/rtm/incremental.js +18 -1
  108. package/dist/holmes/rtm/localize.d.ts +28 -0
  109. package/dist/holmes/rtm/localize.js +272 -14
  110. package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
  111. package/dist/holmes/rtm/rtm-builder.js +265 -28
  112. package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
  113. package/dist/holmes/rtm/rtm-graph.js +194 -31
  114. package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
  115. package/dist/holmes/rtm/taint-benchmark.js +141 -0
  116. package/dist/holmes/rtm/test-scope.js +24 -1
  117. package/dist/holmes/semantic/credentials.d.ts +15 -0
  118. package/dist/holmes/semantic/credentials.js +134 -0
  119. package/dist/holmes/semantic/embedder.d.ts +44 -0
  120. package/dist/holmes/semantic/embedder.js +185 -0
  121. package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
  122. package/dist/holmes/semantic/hit-rerank.js +38 -0
  123. package/dist/holmes/semantic/tier.d.ts +37 -0
  124. package/dist/holmes/semantic/tier.js +54 -0
  125. package/dist/holmes/semantic/vector-cache.d.ts +11 -0
  126. package/dist/holmes/semantic/vector-cache.js +91 -0
  127. package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
  128. package/dist/holmes/spec/acceptance-quality.js +169 -0
  129. package/dist/holmes/spec/validator.js +33 -1
  130. package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
  131. package/dist/holmes/spec/yaml-scalar.js +43 -0
  132. package/package.json +1 -1
@@ -0,0 +1,271 @@
1
+ import type { ScannedFile } from '../cpg/cpg-scanner';
2
+ import type { Spec } from '../spec/spec-parser';
3
+ /** Tokens of a file as the ranker sees it: its symbol names and its path. */
4
+ export declare function fileTokens(f: ScannedFile): Set<string>;
5
+ /**
6
+ * Inverse document frequency over the scanned corpus.
7
+ *
8
+ * Smoothed (`(N+1)/(df+1)`) and floored at zero: a term present in every file must be WEAK evidence,
9
+ * never negative evidence. Unsmoothed IDF would make it exactly 0 and let a file that matches only
10
+ * ubiquitous terms tie with a file that matches nothing, which reads as "no opinion" when the truth
11
+ * is "a little opinion".
12
+ */
13
+ export declare function corpusIdf(scanned: readonly ScannedFile[]): Map<string, number>;
14
+ export interface ArmHit {
15
+ file: string;
16
+ score: number;
17
+ }
18
+ /**
19
+ * Rank files by IDF-weighted term overlap.
20
+ *
21
+ * Symbol matches stay worth 3× a path match, as in the product's scorer — changing two things at
22
+ * once would leave the measurement unable to say which one moved. The only difference from the
23
+ * current arm is that each match is multiplied by how rare its term is in this corpus.
24
+ */
25
+ export declare function rankIdf(issueText: string, scanned: readonly ScannedFile[], topN: number): ArmHit[];
26
+ /** How much a term counts. The product's scorer is this arm with `() => 1`. */
27
+ export type TermWeight = (term: string) => number;
28
+ /**
29
+ * IDF, rescaled so the MEAN weight over the corpus vocabulary is 1.
30
+ *
31
+ * Without this the arm would not be a one-variable change. Raw IDF values sit well below 1 on a
32
+ * corpus of a few hundred files, so every lexical contribution shrinks at once while the spec-anchor
33
+ * bonus (+4/sqrt(anchors)) and the citation bonus (+20/sqrt(anchors)) keep their size — the arm would
34
+ * then be measuring how much the graph bonuses dominate, not how much term rarity helps. Rescaling
35
+ * keeps the lexical term's total magnitude comparable and changes only its DISTRIBUTION across
36
+ * terms, which is the hypothesis under test.
37
+ *
38
+ * An unseen term weighs exactly 1: a word absent from the corpus is not evidence against a file, and
39
+ * mapping it to 0 would silently delete the request's most distinctive vocabulary.
40
+ */
41
+ export declare function normalisedIdf(scanned: readonly ScannedFile[]): Map<string, number>;
42
+ /**
43
+ * The product's scorer with the term weight lifted out as a parameter.
44
+ *
45
+ * This mirrors `localizeIssue` deliberately and the mirror is load-bearing, so a test pins it: with
46
+ * `() => 1` this function must return the product's ranking and its scores EXACTLY. If someone
47
+ * changes the product's scoring and not this one, that test goes red rather than the replay quietly
48
+ * reporting a difference between rankers that is really a difference between vintages.
49
+ */
50
+ export declare function rankWeighted(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight,
51
+ /**
52
+ * @implements A-SPEC-352
53
+ * Weighted spec gate. Absent = the product's `overlap >= 2` count.
54
+ *
55
+ * Passed in here rather than reimplemented by the caller for a reason paid for three times: the
56
+ * first IDF arm hand-rolled a scorer and silently dropped the spec and citation bonuses; a probe
57
+ * of this very gate hand-rolled one again and dropped the citation path, whose +20 bonus is the
58
+ * largest single gain this repository has measured. Both produced confident, worthless numbers.
59
+ * Every arm goes through this function so the features it does NOT vary cannot go missing.
60
+ */
61
+ gateThreshold?: number): ArmHit[];
62
+ export interface ExpansionOptions {
63
+ /** How far to walk from a matched spec. 0 reproduces the unexpanded arm exactly. */
64
+ hops: number;
65
+ /** Weight multiplier per hop. 0 disables expansion; 1 would treat a neighbour like a hit. */
66
+ decay: number;
67
+ }
68
+ /**
69
+ * Rank with GraphRAG-style expansion over the spec graph.
70
+ *
71
+ * The gap this addresses is the one graph retrieval exists for: the answer does not contain the
72
+ * request's words, but it is the neighbour of something that does. The lexical layer scores a file
73
+ * only if the request's terms reach the file or a spec it anchors — one hop. A sibling requirement,
74
+ * written in different words for the same feature, is invisible however closely it is related.
75
+ *
76
+ * Deliberately weaker than a hit at every hop: the bonus is the matched-spec bonus times `decay^k`.
77
+ * A neighbour is evidence, not an answer, and if decay ever let it outrank a direct match the
78
+ * retrieval would be trading the answer for its neighbourhood. A test pins that ordering.
79
+ *
80
+ * Everything else is `rankWeighted` unchanged — the one-variable rule from A-SPEC-349 applies here
81
+ * too, or a win could not be attributed to expansion rather than to weighting.
82
+ */
83
+ export declare function rankExpanded(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, opts: ExpansionOptions): ArmHit[];
84
+ /**
85
+ * Which A-SPECs the request is taken to have matched — the entry point into the knowledge graph.
86
+ *
87
+ * The product's rule counts overlapping terms: two shared words admit a spec, one does not (unless
88
+ * the request is very short). That treats a shared `handler` exactly like a shared `quarantine`, and
89
+ * it costs more here than it does in file scoring: an admitted spec sprays its bonus over EVERY file
90
+ * anchoring it, so one wrongly-opened gate promotes a whole group of unrelated files at once.
91
+ *
92
+ * Weighing the overlap instead of counting it means a request has to bring either several ordinary
93
+ * words or one genuinely distinctive one. With `weight = () => 1` and `threshold = 2` this is the
94
+ * product's original rule exactly, which is what makes any measured difference attributable.
95
+ *
96
+ * A-SPEC only: files anchor A-SPECs, so admitting any other kind can never pull a file through the
97
+ * hop and would only overstate what influenced the ranking.
98
+ */
99
+ export declare function matchedSpecsWeighted(issueText: string, specs: readonly Spec[], weight: TermWeight, threshold: number): string[];
100
+ /**
101
+ * The spec bonus, damped by how much code the spec covers.
102
+ *
103
+ * The existing normalisation divides by how many anchors a FILE carries (A-SPEC-270) — it answers
104
+ * "how much of this file's identity is this spec?". It never asks the other question: "how much of
105
+ * this spec's territory is this file?". A spec spanning twenty files says little about any one of
106
+ * them; a spec covering one says a great deal, and today they pay the same +4.
107
+ *
108
+ * That asymmetry is invisible in a repository with hundreds of narrow specs and decisive in one with
109
+ * a few broad ones — which is exactly where the measured precision loss appeared (A-SPEC-362: on a
110
+ * 54-spec corpus the product loses Top-5 recall and Top-10 precision to plain word matching, and the
111
+ * weighting-only arm is indistinguishable from that baseline).
112
+ *
113
+ * sqrt, matching the existing normalisation's shape: a one-file spec is untaxed, a twenty-file spec
114
+ * is damped by ~4.5x rather than erased. Damping to zero would delete the very hop that carries all
115
+ * of this harness's measured advantage.
116
+ */
117
+ export declare function rankBreadthDamped(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, gateThreshold?: number): ArmHit[];
118
+ /**
119
+ * How much of the corpus carries an `@implements` anchor at all.
120
+ *
121
+ * The spec layer's evidence is only representative when a decent share of the tree is governed. If
122
+ * five files in a thousand carry anchors, matching one of their specs says something about those
123
+ * five and nothing about the other nine hundred and ninety-five — yet the bonus is paid at full
124
+ * strength and displaces lexically correct candidates.
125
+ */
126
+ export declare function anchorCoverage(scanned: readonly ScannedFile[], specs?: readonly Spec[]): number;
127
+ /**
128
+ * The spec bonus scaled by how much of the tree the spec layer actually covers.
129
+ *
130
+ * Measured on one repository with only the spec count varied (A-SPEC-364): 585 specs gave Top-10
131
+ * recall 0.770, but 146 gave 0.253 and 59 gave 0.264 — BELOW the 0.305 of no spec layer at all. A
132
+ * partly populated spec store is worse than an empty one, and every project that adopts Holmes
133
+ * passes through that valley on its way to the peak. jarvis, at 54 specs, is sitting in it.
134
+ *
135
+ * Scaling by coverage makes the layer fade in rather than switch on: at zero coverage the ranking is
136
+ * exactly the lexical arm (the floor a project starts from), and at full coverage the bonus is
137
+ * unchanged (the peak this repository measures). No threshold, because a threshold would create a
138
+ * cliff where authoring one more spec changes every ranking at once.
139
+ */
140
+ export declare function rankCoverageAware(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, gateThreshold?: number): ArmHit[];
141
+ /**
142
+ * The gate, chosen by whether the request names a spec at all.
143
+ *
144
+ * No single threshold serves both conditions. Measured over 60 point-in-time cases (A-SPEC-369):
145
+ *
146
+ * threshold 1 — cited Top-10 recall 0.770, uncited 0.275 (a no-graph baseline gets 0.357)
147
+ * threshold 4 — cited 0.743 and Top-1 0.333 → 0.283, uncited 0.349
148
+ *
149
+ * Under a citation, the named spec locates the request in the corpus, and the specs that merely
150
+ * share vocabulary with it are corroborated rather than speculative — the permissive gate pays. With
151
+ * no citation, those same matches are twenty-six unanchored guesses per case whose bonuses bury the
152
+ * lexical signal, and the strict gate keeps them out.
153
+ *
154
+ * The condition is read from the request itself, and only a citation the store can RESOLVE counts:
155
+ * a stray `A-SPEC-999` in a bug report must not unlock the permissive path.
156
+ */
157
+ export declare function rankCitationConditioned(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, citedThreshold: number, uncitedThreshold: number): ArmHit[];
158
+ /**
159
+ * Drop the lexically-matched specs when too many of them match.
160
+ *
161
+ * Conditioning on whether the request cited a spec repaired this repository and cost the second
162
+ * corpus its lead, because that corpus cites nothing and would always take the strict path. Both
163
+ * measurements point at the same underlying quantity: HOW MANY specs the request matched. jarvis
164
+ * matches 0.87 per case, this repository 26.6 (A-SPEC-367).
165
+ *
166
+ * One matching spec is a claim about where the request belongs. Twenty-six matching specs is a fact
167
+ * about the request's vocabulary — it was written in words the corpus uses everywhere — and their
168
+ * bonuses, spread over a hundred anchored files, bury the lexical ranking that would otherwise work.
169
+ *
170
+ * A citation is never capped: it names something rather than resembling it, and it is the one signal
171
+ * in this scorer measured to carry the localization result.
172
+ */
173
+ export declare function rankCapped(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, gateThreshold: number, maxMatchedSpecs: number): ArmHit[];
174
+ /**
175
+ * The cap, applied only to requests that named nothing.
176
+ *
177
+ * The two halves the measurements each found: a citation corroborates the specs that merely share
178
+ * vocabulary with the request, so with one present they help (dropping them cost Top-10 recall
179
+ * 0.770 → 0.743). Without one, a large number of matches is a fact about the request's wording
180
+ * rather than evidence, and their bonuses bury the lexical ranking (0.275 against a 0.357 baseline).
181
+ * A SMALL number of matches with no citation is still meaningful — that is the second corpus, at
182
+ * 0.87 matches per case, and capping it there threw away the modest lead it had.
183
+ *
184
+ * So: exempt cited requests, and cap only the uncited ones that matched more specs than a request
185
+ * can plausibly be about.
186
+ */
187
+ export declare function rankUncitedCapped(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, gateThreshold: number, maxMatchedSpecs: number): ArmHit[];
188
+ /**
189
+ * One `calls` hop out from the lexical hits, added at a discount.
190
+ *
191
+ * With no citation the spec layer is off and the ranking is exactly what grepping the request's
192
+ * words would produce. The call graph is the only structure left that could beat it, and A-SPEC-318
193
+ * measured the opportunity: 13 missed files one `calls` hop away, all 58 hop edges being calls, none
194
+ * imports or inherits. That was measured under cited requests, where the spec layer had already
195
+ * found most of the answer — uncited, the lexical hits are the only seed, so the hop has more to add.
196
+ *
197
+ * Half the seed's score, and never more: a callee is evidence about the seed's neighbourhood, not
198
+ * about itself. If a hop could outrank a direct hit, the ranking would be trading the answer for its
199
+ * surroundings, which is the failure the spec-graph expansion arm was rejected for.
200
+ */
201
+ export declare function rankWithCallHop(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, gateThreshold: number, maxMatchedSpecs: number,
202
+ /** @implements A-SPEC-394 — use import edges to bind names the global map calls ambiguous. */
203
+ importAware?: boolean): ArmHit[];
204
+ /**
205
+ * @implements A-SPEC-395
206
+ * The product's scorer with each layer switchable, so a cost can be attributed instead of guessed.
207
+ *
208
+ * On history sampled away from the benchmarker's own commits, this ranking scores BELOW the no-graph
209
+ * baseline (Top-10 recall 0.618 against 0.668). The contaminated slice showed the opposite, so the
210
+ * regression is newly visible and it is shipped. Four layers sit between the two — corpus IDF
211
+ * weighting, the spec gate and anchor bonus, the citation bonus, and the vendored demotion — and
212
+ * turning them off one at a time is the only way to learn which one costs.
213
+ */
214
+ export interface AblationOptions {
215
+ idf: boolean;
216
+ specBonus: boolean;
217
+ vendorDemotion: boolean;
218
+ }
219
+ export declare function ablate(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, opts: AblationOptions): ArmHit[];
220
+ /**
221
+ * @implements A-SPEC-396
222
+ * Limit how many files each matched spec may lift.
223
+ *
224
+ * The ablation isolated the cost: turning the spec bonus off restores the baseline exactly, so that
225
+ * layer buys Top-1 (0.244 → 0.267 here, 0.400 → 0.433 on jarvis) and sells precision (0.156 → 0.104).
226
+ *
227
+ * Three earlier attempts all changed WHICH specs are admitted — breadth damping, coverage scaling,
228
+ * a cap on matched specs — and all three were rejected. This changes something else: once a spec is
229
+ * admitted, every file anchoring it rises together, and that simultaneity is the shape of the
230
+ * precision loss. Ranking a spec's anchored files by their own lexical evidence and lifting only the
231
+ * strongest few keeps the top of the list while leaving the tail alone.
232
+ *
233
+ * The cost is explicit: a file with no lexical evidence can no longer be lifted by its spec alone,
234
+ * so the layer loses the files it uniquely contributed. Whether that trade is positive is the
235
+ * measurement, not the assumption.
236
+ */
237
+ export declare function rankSpecLiftLimited(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, gateThreshold: number, liftPerSpec: number): ArmHit[];
238
+ /**
239
+ * @implements A-SPEC-397
240
+ * The spec bonus with its magnitude as a parameter.
241
+ *
242
+ * Four arms failed to recover the 0.052 of precision the layer costs, and each changed a different
243
+ * thing: which specs are admitted (breadth damping, coverage scaling, a cap on matches) and how far
244
+ * an admitted one reaches (the lift limit). Magnitude was never touched.
245
+ *
246
+ * The default is 4 per anchor against a lexical symbol hit worth 3 and a path hit worth 1, so a
247
+ * single matched spec outweighs a symbol match — a file whose only evidence is one anchor can
248
+ * outrank a file whose name actually contains the request's words. That is a plausible source of the
249
+ * tail pollution and it is the last untried one.
250
+ *
251
+ * Reducing it should cost Top-1 as it recovers precision, so both are reported: a setting that fixes
252
+ * one by destroying the other is not a fix.
253
+ */
254
+ export declare function rankWithBonusScale(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, gateThreshold: number, bonusScale: number): ArmHit[];
255
+ /**
256
+ * @implements A-SPEC-398
257
+ * The spec bonus may REORDER, but may not ADMIT.
258
+ *
259
+ * Five arms failed to recover the 0.052 of precision the spec layer costs, and counting the lists
260
+ * showed why none of them could: the baseline returns 7.09 files per case and the product returns
261
+ * 9.27, filling all ten slots in 36 of 45 cases against the baseline's 16. The layer is not ranking
262
+ * the same set differently — it is **adding about two files per case**, and those additions are
263
+ * mostly wrong. Every arm so far reordered or rescaled; none declined to add.
264
+ *
265
+ * So: a file with lexical evidence keeps its bonus and can move up. A file whose only evidence is an
266
+ * anchor does not enter the list at all.
267
+ *
268
+ * The cost is the one thing the spec layer uniquely did — reaching a file the request's words never
269
+ * touch. That is a real loss and the measurement has to weigh it, not assume it away.
270
+ */
271
+ export declare function rankSpecReorderOnly(issueText: string, scanned: readonly ScannedFile[], specs: readonly Spec[], topN: number, weight: TermWeight, gateThreshold: number): ArmHit[];