sigmap 8.13.0 → 8.16.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/AGENTS.md CHANGED
@@ -33,16 +33,17 @@ Always run `sigmap ask` (or `sigmap --query`) before searching for files relevan
33
33
  src/extractors/python_ast.py ← ast
34
34
  ```
35
35
 
36
- ## changes (last 5 commits — 1 second ago)
36
+ ## changes (last 5 commits — 0 seconds ago)
37
37
  ```
38
- src/format/terse.js +splitAnchor +encodeTerseSig +encodeTerseSigs +_tokens
39
- src/graph/blast-radius.js +tierFor +_normRel +_bfs +methodBlastRadius
38
+ src/evidence/pack.js +riskFactorsFor ~parseAnchor ~buildEvidencePack ~formatMarkdown
39
+ src/graph/call-graph.js +calls +maskRust +name +goDefs
40
40
  src/mcp/handlers.js +that +getMethodImpact ~queryContext ~squeezeOutput
41
41
  src/mcp/server.js ~dispatch
42
42
  src/mcp/tools.js +it
43
+ src/retrieval/ranker.js ~scoreFile ~rank
44
+ src/graph/blast-radius.js +tierFor +_normRel +_bfs +methodBlastRadius
43
45
  src/review/pr-evidence.js ~buildPrEvidence ~formatPrEvidenceMarkdown
44
46
  src/review/review-pr.js ~isSource ~reviewPr
45
- src/wiki/generate.js +_rel +_pct +_identity +_modules
46
47
  ```
47
48
 
48
49
  ## packages
@@ -176,31 +177,61 @@ code-fence ---
176
177
 
177
178
  ### src/config/defaults.js
178
179
  ```
179
- module.exports = { DEFAULTS } :161-161
180
+ module.exports = { DEFAULTS } :163-163
180
181
  ```
181
182
 
182
- ### src/format/terse.js
183
+ ### src/evidence/pack.js
183
184
  ```
184
- module.exports = { encodeTerseSig, encodeTerseSigs, measureTerse, splitAnchor } :86-86
185
- function splitAnchor(sig) → { text: string, suffix: s :25-30
186
- function encodeTerseSig(sig) → string :37-50
187
- function encodeTerseSigs(sigs) → string[] :57-59
188
- function _tokens(sigs) :62-64
189
- function measureTerse(sigsList) → { beforeTokens: number, a :72-84
185
+ module.exports = { buildEvidencePack, formatJSON, formatMarkdown, parseAnchor, riskLabelFor, riskFactorsFor, findRelatedTests, SCHEMA_VERSION, SCHEMA_URL, TEST_DISCOVERY } :326-337
186
+ function parseAnchor(sig) → { symbol: string, start: :64-72
187
+ function riskLabelFor(relPath) → 'generated'|'test'|'migra :84-86
188
+ function riskFactorsFor(relPath) → string[] :96-108
189
+ function stemOf(relPath) :111-114
190
+ function testTargetStem(relPath) → string :126-132
191
+ function findRelatedTests(relPath, allFiles) → string[] :143-154
192
+ function reasonFor(signals) :157-168
193
+ function sigTokens(sigs) :171-173
194
+ function canonicalize(value) → string :180-182
195
+ function sortKeys(value) :184-192
196
+ function buildEvidencePack(query, cwd, opts = {}) → object :205-281
197
+ function ranked0Empty(query) :284-286
198
+ function formatJSON(pack) :289-291
199
+ function formatMarkdown(pack) :294-324
190
200
  ```
191
201
 
192
- ### src/graph/blast-radius.js
202
+ ### src/graph/call-graph.js
193
203
  ```
194
- module.exports = { methodBlastRadius, tierFor, DIRECT_WEIGHT, TRANSITIVE_WEIGHT } :132-132
195
- function tierFor(score) :25-31
196
- function _normRel(p) :33-35
197
- function _bfs(seedIds, reverse, maxDepth) :38-60
198
- function methodBlastRadius(changedFiles, cwd, opts = {}) → { * available: boolean, * :78-130
204
+ module.exports = { buildCallGraph, buildCallFileGraph, methodImpact, methodCallees, formatCallGraph, formatCallGraphJSON, extractDefs, maskJs, maskPy, maskRust } :563-567
205
+ function normalizePath(p) :41-41
206
+ function toRel(cwd, f) :42-42
207
+ function symId(cwd, absFile, name) :43-43
208
+ function maskJs(src) :49-65
209
+ function maskRust(src) :70-91
210
+ function maskPy(src) :93-110
211
+ function matchDelim(masked, openIdx, open, close) :113-120
212
+ function lineAt(src, idx) :122-127
213
+ function jsDefs(masked) :132-237
214
+ function pyDefs(masked) :200-220
215
+ function goDefs(masked) :224-344
216
+ function javaDefs(masked) :250-367
217
+ function rustDefs(masked) :278-401
218
+ function maskFor(filePath, src) :300-305
219
+ function extractDefs(filePath, src) :307-315
220
+ function callsInRange(masked, start, end) :318-330
221
+ function _walk(dir, excludeSet, out, depth) :334-347
222
+ function buildCallGraph(cwd, opts = {}) → { * forward: Map<string,s :363-446
223
+ function buildCallFileGraph(cwd, opts = {}) → { forward: Map<string,str :459-482
224
+ function _resolveSymbol(symbol, defs) :485-490
225
+ function _bfs(seedIds, graph, maxDepth) :493-506
226
+ function methodImpact(symbol, cwd, opts = {}) → { symbol:string, resolved :516-522
227
+ function methodCallees(symbol, cwd, opts = {}) → { symbol:string, resolved :528-534
228
+ function formatCallGraph(result, kind) :537-549
229
+ … +1 more signatures
199
230
  ```
200
231
 
201
232
  ### src/mcp/handlers.js
202
233
  ```
203
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } :966-966
234
+ module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getMethodImpact, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } :975-975
204
235
  function _readContextFiles(cwd) :10-17
205
236
  function readContext(args, cwd) :36-66
206
237
  function searchSignatures(args, cwd) :74-100
@@ -209,21 +240,21 @@ function createCheckpoint(args, cwd) :143-215
209
240
  function getRouting(args, cwd) :224-261
210
241
  function explainFile(args, cwd) :269-356
211
242
  function listModules(args, cwd) :364-403
212
- function queryContext(args, cwd) :411-429
213
- function getMethodImpact(args, cwd) :437-451
214
- function getImpact(args, cwd) :459-471
215
- function getLines(args, cwd) :480-528
216
- function readMemory(args, cwd) :536-571
217
- function getCalleeSignatures(args, cwd) :580-625
218
- function _pkgVersion(cwd) :632-635
219
- function notifyFileCreated(args, cwd) :639-661
220
- function notifySymbolAdded(args, cwd) :664-684
221
- function notifyFileDeleted(args, cwd) :687-701
222
- function _changedFiles(cwd, args) :707-719
223
- function getDiffContext(args, cwd) :728-799
224
- function getArchitectureOverview(args, cwd) :808-876
225
- function verifySuggestion(args, cwd) :886-921
226
- function squeezeOutput(args, cwd) :931-964
243
+ function queryContext(args, cwd) :411-438
244
+ function getMethodImpact(args, cwd) :446-460
245
+ function getImpact(args, cwd) :468-480
246
+ function getLines(args, cwd) :489-537
247
+ function readMemory(args, cwd) :545-580
248
+ function getCalleeSignatures(args, cwd) :589-634
249
+ function _pkgVersion(cwd) :641-644
250
+ function notifyFileCreated(args, cwd) :648-670
251
+ function notifySymbolAdded(args, cwd) :673-693
252
+ function notifyFileDeleted(args, cwd) :696-710
253
+ function _changedFiles(cwd, args) :716-728
254
+ function getDiffContext(args, cwd) :737-808
255
+ function getArchitectureOverview(args, cwd) :817-885
256
+ function verifySuggestion(args, cwd) :895-930
257
+ function squeezeOutput(args, cwd) :940-973
227
258
  ```
228
259
 
229
260
  ### src/mcp/server.js
@@ -240,33 +271,22 @@ function start(cwd) :113-139
240
271
  module.exports = { TOOLS } :392-392
241
272
  ```
242
273
 
243
- ### src/review/pr-evidence.js
244
- ```
245
- module.exports = { buildPrEvidence, formatPrEvidenceMarkdown } :157-157
246
- function buildPrEvidence(changedFiles, cwd, opts = {}) → { scope:string, files:obj :31-84
247
- function formatPrEvidenceMarkdown(evidence, opts = {}) :89-155
248
- ```
249
-
250
- ### src/review/review-pr.js
251
- ```
252
- module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD } :150-150
253
- function isTestFile(p) :32-34
254
- function isSource(p) :35-37
255
- function reviewPr(changedFiles, cwd, opts = {}) → { findings: object[], bla :48-148
256
- ```
257
-
258
- ### src/wiki/generate.js
274
+ ### src/retrieval/ranker.js
259
275
  ```
260
- module.exports = { buildWiki, renderWikiMarkdown } :250-250
261
- function _rel(cwd, f) :22-24
262
- function _pct(fraction) :26-28
263
- function _identity(cwd) :31-37
264
- function _modules(index) :40-67
265
- function _flow(cwd) :70-97
266
- function _conventions(cwd, index) :100-117
267
- function _health(cwd) :119-127
268
- function buildWiki(cwd, opts = {}) → { data: object, markdown: :137-162
269
- function renderWikiMarkdown(data, sigmapVersion) → string :171-248
276
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent } :598-598
277
+ function _computePenalty(filePath) :63-70
278
+ function _computeHubs(graph) :73-84
279
+ function _isHub(filePath) :87-91
280
+ function scoreFile(filePath, sigs, queryTokens, weights) → { score: number, signals: :102-159
281
+ function rank(query, sigIndex, opts) → { file: string, score: nu :177-302
282
+ function _parseContextFile(contextPath) → Map<string, string[]> :372-404
283
+ function _mergeSigIndex(target, source) :407-415
284
+ function _buildSigIndexFromCache(cwd) → Map<string, string[]> :422-442
285
+ function _enrichSigIndexFromStrategy(cwd, index) → Map<string, string[]> :450-456
286
+ function buildSigIndex(cwd, opts) → Map<string, string[]> :473-506
287
+ function formatRankTable(results, query) → string :515-551
288
+ function formatRankJSON(results, query) → object :560-575
289
+ function detectIntent(query) :590-596
270
290
  ```
271
291
 
272
292
  ### src/analysis/coverage-score.js
@@ -519,24 +539,6 @@ function scoreUsefulness(taskResult, rankingScore) :11-38
519
539
  function computeUsefulnessStats(taskResults) :40-66
520
540
  ```
521
541
 
522
- ### src/evidence/pack.js
523
- ```
524
- module.exports = { buildEvidencePack, formatJSON, formatMarkdown, parseAnchor, riskLabelFor, findRelatedTests, SCHEMA_VERSION } :293-301
525
- function parseAnchor(sig) → { symbol: string, start: :50-58
526
- function riskLabelFor(relPath) → 'generated'|'test'|'migra :70-81
527
- function stemOf(relPath) :84-87
528
- function testTargetStem(relPath) → string :99-105
529
- function findRelatedTests(relPath, allFiles) → string[] :116-127
530
- function reasonFor(signals) :130-141
531
- function sigTokens(sigs) :144-146
532
- function canonicalize(value) → string :153-155
533
- function sortKeys(value) :157-165
534
- function buildEvidencePack(query, cwd, opts = {}) → object :178-249
535
- function ranked0Empty(query) :252-254
536
- function formatJSON(pack) :257-259
537
- function formatMarkdown(pack) :262-291
538
- ```
539
-
540
542
  ### src/extractors/coverage.js
541
543
  ```
542
544
  module.exports = { buildTestIndex, isTested } :79-79
@@ -980,6 +982,16 @@ function detectVersion(cwd) :13-19
980
982
  function format(context, cwd, writtenFiles, sigmapVersion) :21-69
981
983
  ```
982
984
 
985
+ ### src/format/terse.js
986
+ ```
987
+ module.exports = { encodeTerseSig, encodeTerseSigs, measureTerse, splitAnchor } :86-86
988
+ function splitAnchor(sig) → { text: string, suffix: s :25-30
989
+ function encodeTerseSig(sig) → string :37-50
990
+ function encodeTerseSigs(sigs) → string[] :57-59
991
+ function _tokens(sigs) :62-64
992
+ function measureTerse(sigsList) → { beforeTokens: number, a :72-84
993
+ ```
994
+
983
995
  ### src/format/usage-guidance.js
984
996
  ```
985
997
  module.exports = { usageBlock } :28-28
@@ -996,6 +1008,15 @@ function renderReportHtml(result, opts = {}) → string :48-115
996
1008
  function renderReportMarkdown(result) :144-162
997
1009
  ```
998
1010
 
1011
+ ### src/graph/blast-radius.js
1012
+ ```
1013
+ module.exports = { methodBlastRadius, tierFor, DIRECT_WEIGHT, TRANSITIVE_WEIGHT } :132-132
1014
+ function tierFor(score) :25-31
1015
+ function _normRel(p) :33-35
1016
+ function _bfs(seedIds, reverse, maxDepth) :38-60
1017
+ function methodBlastRadius(changedFiles, cwd, opts = {}) → { * available: boolean, * :78-130
1018
+ ```
1019
+
999
1020
  ### src/graph/builder.js
1000
1021
  ```
1001
1022
  module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias } :494-494
@@ -1012,30 +1033,6 @@ function build(files, cwd, ctx) → { forward: Map<string,str :387-426
1012
1033
  function buildFromCwd(cwd, opts) → { forward: Map<string,str :438-492
1013
1034
  ```
1014
1035
 
1015
- ### src/graph/call-graph.js
1016
- ```
1017
- module.exports = { buildCallGraph, methodImpact, methodCallees, formatCallGraph, formatCallGraphJSON, extractDefs, maskJs, maskPy } :397-401
1018
- function normalizePath(p) :35-35
1019
- function toRel(cwd, f) :36-36
1020
- function symId(cwd, absFile, name) :37-37
1021
- function maskJs(src) :43-59
1022
- function maskPy(src) :61-78
1023
- function matchDelim(masked, openIdx, open, close) :81-88
1024
- function lineAt(src, idx) :90-95
1025
- function jsDefs(masked) :100-206
1026
- function pyDefs(masked) :168-188
1027
- function extractDefs(filePath, src) :190-195
1028
- function callsInRange(masked, start, end) :198-210
1029
- function _walk(dir, excludeSet, out, depth) :214-227
1030
- function buildCallGraph(cwd, opts = {}) → { * forward: Map<string,s :243-316
1031
- function _resolveSymbol(symbol, defs) :319-324
1032
- function _bfs(seedIds, graph, maxDepth) :327-340
1033
- function methodImpact(symbol, cwd, opts = {}) → { symbol:string, resolved :350-356
1034
- function methodCallees(symbol, cwd, opts = {}) → { symbol:string, resolved :362-368
1035
- function formatCallGraph(result, kind) :371-383
1036
- function formatCallGraphJSON(result, kind) :385-395
1037
- ```
1038
-
1039
1036
  ### src/graph/impact.js
1040
1037
  ```
1041
1038
  module.exports = { getImpact, analyzeImpact, formatImpact, formatImpactJSON } :240-240
@@ -1195,30 +1192,27 @@ function expandQuery(qToks) → Map<string, number> :132-141
1195
1192
  function bm25rank(query, candidates) → Array<object & { score: n :154-193
1196
1193
  ```
1197
1194
 
1198
- ### src/retrieval/ranker.js
1199
- ```
1200
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent } :570-570
1201
- function _computePenalty(filePath) :62-69
1202
- function _computeHubs(graph) :72-83
1203
- function _isHub(filePath) :86-90
1204
- function scoreFile(filePath, sigs, queryTokens, weights) → { score: number, signals: :101-158
1205
- function rank(query, sigIndex, opts) → { file: string, score: nu :174-298
1206
- function _parseContextFile(contextPath) → Map<string, string[]> :344-376
1207
- function _mergeSigIndex(target, source) :379-387
1208
- function _buildSigIndexFromCache(cwd) → Map<string, string[]> :394-414
1209
- function _enrichSigIndexFromStrategy(cwd, index) → Map<string, string[]> :422-428
1210
- function buildSigIndex(cwd, opts) → Map<string, string[]> :445-478
1211
- function formatRankTable(results, query) → string :487-523
1212
- function formatRankJSON(results, query) → object :532-547
1213
- function detectIntent(query) :562-568
1214
- ```
1215
-
1216
1195
  ### src/retrieval/tokenizer.js
1217
1196
  ```
1218
1197
  module.exports = { tokenize, STOP_WORDS } :54-54
1219
1198
  function tokenize(text, opts) → string[] :31-52
1220
1199
  ```
1221
1200
 
1201
+ ### src/review/pr-evidence.js
1202
+ ```
1203
+ module.exports = { buildPrEvidence, formatPrEvidenceMarkdown } :157-157
1204
+ function buildPrEvidence(changedFiles, cwd, opts = {}) → { scope:string, files:obj :31-84
1205
+ function formatPrEvidenceMarkdown(evidence, opts = {}) :89-155
1206
+ ```
1207
+
1208
+ ### src/review/review-pr.js
1209
+ ```
1210
+ module.exports = { reviewPr, SECURITY_PATTERNS, GOD_NODE_THRESHOLD, SCOPE_DIR_THRESHOLD } :150-150
1211
+ function isTestFile(p) :32-34
1212
+ function isSource(p) :35-37
1213
+ function reviewPr(changedFiles, cwd, opts = {}) → { findings: object[], bla :48-148
1214
+ ```
1215
+
1222
1216
  ### src/routing/classifier.js
1223
1217
  ```
1224
1218
  module.exports = { classify, classifyAll } :102-102
@@ -1416,6 +1410,20 @@ function extractNpmScripts(text) → { name: string, line: num :168-184
1416
1410
  function extractSymbols(text) → { name: string, line: num :192-209
1417
1411
  ```
1418
1412
 
1413
+ ### src/wiki/generate.js
1414
+ ```
1415
+ module.exports = { buildWiki, renderWikiMarkdown } :250-250
1416
+ function _rel(cwd, f) :22-24
1417
+ function _pct(fraction) :26-28
1418
+ function _identity(cwd) :31-37
1419
+ function _modules(index) :40-67
1420
+ function _flow(cwd) :70-97
1421
+ function _conventions(cwd, index) :100-117
1422
+ function _health(cwd) :119-127
1423
+ function buildWiki(cwd, opts = {}) → { data: object, markdown: :137-162
1424
+ function renderWikiMarkdown(data, sigmapVersion) → string :171-248
1425
+ ```
1426
+
1419
1427
  ### src/workspace/detector.js
1420
1428
  ```
1421
1429
  module.exports = { detectWorkspaces, inferPackage, scopeToPackage } :4-4
package/CHANGELOG.md CHANGED
@@ -10,6 +10,35 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.16.0] — 2026-07-12
14
+
15
+ Minor release — **Evidence Pack schema v2** (the last Machine-lever item from the master plan §7.4). Also the first published release carrying v8.15.0, which was prepared but never tagged (see its note below).
16
+
17
+ ### Added
18
+ - **Evidence Pack schema v2 (#477, PR #478):** additive over v1 — `schemaVersion: '2.0'` + `schemaUrl` pointing at a **published draft-07 JSON Schema** (`sigmap.io/schemas/evidence-pack-2.json`) that CI/agents can validate packs against; **`files[].riskFactors`** exposes *every* matched risk category in precedence order (a migration touching payments carries `['migration','payment']`) while `riskLabel` stays the first factor for v1 consumers; a **`testDiscovery` provenance block** carries the measured accuracy of the stem-affix related-tests method (**F1 0.98, precision 0.971, recall 0.988** over 3,701 pairs / 28 repos) — constants sourced from the committed benchmark report and guard-tested against it; and `generator: { name, version }` records what built the pack. Byte-stable; `contextHash` determinism preserved.
19
+
20
+ ---
21
+
22
+ ## [8.15.0] — 2026-07-11
23
+
24
+ > **Not published as a standalone release** — prepared and merged to main, but never tagged; first shipped as part of v8.16.0.
25
+
26
+ Minor release — **call-graph ranking boost, opt-in and measure-gated**. The milestone item "call-graph edges into ranking" is done — and shipped **dark**, because the measure gate said so.
27
+
28
+ ### Added
29
+ - **Call-graph ranking boost (#474, PR #475):** `buildCallFileGraph` collapses symbol edges to deterministic file-level bidirectional edges; `rank()` gains `opts.callGraph` — a single-hop, hub-suppressed neighbor boost (`callHop: 0.30`) with a `callGraphBoost` explain signal. Config-gated (non-fatal) into `ask`, `--query`, and the `query_context` MCP tool via **`retrieval.callGraphBoost` (default `false`)**. **Measured** by the new `npm run benchmark:callgraph-boost` A/B (90 tasks / 18 cached repos through the full ranker): import-graph arm 80% hit@5, +call-graph arm 80% — **delta +0, no repo moved** — so per the gate the default stays off; the boost exists for call-topology-heavy repos (Go/Java same-package) where import edges structurally can't see the relationship. The headline BM25 harness is untouched and reproduces 87.8%. Report: `benchmarks/reports/callgraph-boost.json`.
30
+
31
+ ---
32
+
33
+ ## [8.14.0] — 2026-07-11
34
+
35
+ Minor release — **call-graph support for Java, Go, and Rust** (GR1 language expansion). The method-level call-graph goes from 2 to 5 languages, and every consumer — `--callers`/`--callees`, blast-radius scoring, `review-pr` method-blast findings, `get_method_impact` — inherits them with no further changes.
36
+
37
+ ### Added
38
+ - **Java/Go/Rust call-graph (#471, PR #472):** new def extractors in `src/graph/call-graph.js` — Go `func` + receiver methods (parenthesized return lists handled), Java methods + constructors (generics/`throws` tolerated; control-flow and `new Foo(){}` anonymous classes rejected), Rust `fn` incl. generics + `where` clauses and `impl` methods (bodiless trait declarations skipped). New **lifetime-safe `maskRust`** masker: `'a` lifetimes pass through untouched while char literals and strings are masked, preserving offsets. **Same-package resolution scope:** Go/Java call same-package functions across files with no import statement, so same-directory same-language siblings join the resolution scope (sorted, deterministic). Per North-Star #1, unparseable constructs are skipped — never a parser dep. Surface claims updated to "JS/TS, Python, Java, Go, Rust" in `--help`, the `get_method_impact` description, and the MCP guide.
39
+
40
+ ---
41
+
13
42
  ## [8.13.0] — 2026-07-11
14
43
 
15
44
  Minor release — **method-level blast-radius scoring** (GR2, the biggest Phase-2 lever in the master plan §7.4). The D4 call-graph finally has consumers: review findings, the PR Evidence report, and a 20th MCP tool now answer *which functions break*, not just which files.
package/README.md CHANGED
@@ -122,7 +122,7 @@ Ask → Rank → Context → Validate → Judge → Learn
122
122
 
123
123
  <!--SM:benchmarkBlock-->
124
124
  ```
125
- Benchmark : sigmap-v8.13-main (21 repositories, including R language)
125
+ Benchmark : sigmap-v8.16-main (21 repositories, including R language)
126
126
  Date : 2026-07-11
127
127
 
128
128
  Hit@5 : 87.8% (baseline 13.6% — 6.5× lift)