@holmes-lab/holmes-kit 0.17.0 → 0.19.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 +127 -0
- package/README.md +6 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/release-docs.d.ts +27 -0
- package/dist/holmes/cli/release-docs.js +68 -0
- package/dist/holmes/cpg/arch-observe.d.ts +15 -0
- package/dist/holmes/cpg/arch-observe.js +19 -0
- package/dist/holmes/cpg/cpg-scanner.d.ts +10 -36
- package/dist/holmes/cpg/cpg-scanner.js +27 -3
- package/dist/holmes/cpg/cycle-detect.d.ts +87 -0
- package/dist/holmes/cpg/cycle-detect.js +251 -0
- package/dist/holmes/cpg/scan-cache.d.ts +1 -1
- package/dist/holmes/cpg/scanned-file.d.ts +36 -0
- package/dist/holmes/cpg/scanned-file.js +2 -0
- package/dist/holmes/governance/autonomy.js +1 -0
- package/dist/holmes/governance/constitution.d.ts +20 -0
- package/dist/holmes/governance/constitution.js +17 -0
- package/dist/holmes/governance/ledger-store.d.ts +9 -0
- package/dist/holmes/governance/ledger-store.js +47 -0
- package/dist/holmes/governance/provenance-chain.d.ts +16 -1
- package/dist/holmes/governance/provenance-chain.js +5 -3
- package/dist/holmes/hooks/pre-tool-use.js +3 -1
- package/dist/holmes/hooks/stop.d.ts +14 -0
- package/dist/holmes/hooks/stop.js +73 -0
- package/dist/holmes/mcp/defuse-bound.d.ts +1 -0
- package/dist/holmes/mcp/defuse-bound.js +8 -0
- package/dist/holmes/mcp/handlers.d.ts +23 -0
- package/dist/holmes/mcp/handlers.js +173 -6
- package/dist/holmes/mcp/history-admission.d.ts +15 -0
- package/dist/holmes/mcp/history-admission.js +37 -0
- package/dist/holmes/mcp/maintenance-analyze.d.ts +11 -0
- package/dist/holmes/mcp/maintenance-analyze.js +64 -8
- package/dist/holmes/mcp/spec-id-guard.d.ts +0 -8
- package/dist/holmes/mcp/spec-id-guard.js +10 -1
- package/dist/holmes/review/evaluation-metrics.d.ts +6 -0
- package/dist/holmes/review/evaluation-metrics.js +18 -1
- package/dist/holmes/review/paired-power.d.ts +14 -0
- package/dist/holmes/review/paired-power.js +57 -0
- package/dist/holmes/review/replay-corpus.d.ts +11 -0
- package/dist/holmes/review/replay-corpus.js +34 -0
- package/dist/holmes/review/run-replay.js +60 -4
- package/dist/holmes/review/symbol-truth.d.ts +14 -0
- package/dist/holmes/review/symbol-truth.js +23 -0
- package/dist/holmes/rtm/decision-context.d.ts +23 -0
- package/dist/holmes/rtm/decision-context.js +47 -0
- package/dist/holmes/rtm/defuse-symbols.d.ts +17 -0
- package/dist/holmes/rtm/defuse-symbols.js +91 -0
- package/dist/holmes/rtm/incremental.js +5 -0
- package/dist/holmes/rtm/localize.js +4 -2
- package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
- package/dist/holmes/rtm/rtm-builder.js +34 -5
- package/dist/holmes/rtm/rtm-graph.d.ts +11 -0
- package/dist/holmes/rtm/rtm-graph.js +13 -0
- package/dist/holmes/spec/legacy-fields.d.ts +2 -0
- package/dist/holmes/spec/legacy-fields.js +9 -0
- package/dist/holmes/spec/legacy-format.d.ts +1 -1
- package/dist/holmes/spec/legacy-format.js +4 -1
- package/dist/holmes/spec/spec-parser.js +5 -3
- package/dist/holmes/spec/spec-types.d.ts +1 -1
- package/dist/holmes/spec/spec-types.js +14 -1
- package/package.json +1 -1
- package/playbooks/author-slice/PLAYBOOK.md +33 -0
- package/playbooks/publish/PLAYBOOK.md +32 -0
- package/playbooks/tdd-slice/PLAYBOOK.md +19 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { SpecStore } from '../spec/spec-store';
|
|
2
|
+
import { Spec } from '../spec/spec-parser';
|
|
2
3
|
import { MaintenanceGroundTruth, MaintenanceAnalysis } from './maintenance-analyze';
|
|
3
4
|
import { Action } from '../guardrail/phase';
|
|
5
|
+
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
6
|
+
import { type DecisionRecord, type DecisionCitation } from '../rtm/rtm-builder';
|
|
4
7
|
import { Basis } from './basis';
|
|
5
8
|
import { Finding } from '../review/findings';
|
|
6
9
|
import { Approval, Enforcement } from '../guardrail/risk-gate';
|
|
@@ -53,6 +56,16 @@ export declare function makeHandlers(store: SpecStore, opts?: ElicitOpts): RawHa
|
|
|
53
56
|
}>;
|
|
54
57
|
};
|
|
55
58
|
type RawHandlers = ReturnType<typeof makeRawHandlers>;
|
|
59
|
+
/**
|
|
60
|
+
* @implements A-SPEC-293
|
|
61
|
+
* Read `.ax/decisions/*.md` and find who cites them. Citation is the link, chosen by measurement:
|
|
62
|
+
* `governs` resolves 9 of 65 names to symbols while 37 source files and 71 spec files cite an ADR.
|
|
63
|
+
* Failure here is never fatal — a project with no decisions directory simply has no decisions.
|
|
64
|
+
*/
|
|
65
|
+
export declare function collectDecisions(root: string, scanned: readonly ScannedFile[], specs: readonly Spec[]): {
|
|
66
|
+
decisions: DecisionRecord[];
|
|
67
|
+
citations: DecisionCitation[];
|
|
68
|
+
};
|
|
56
69
|
declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
57
70
|
spec_create(a: any): Promise<{
|
|
58
71
|
ok: boolean;
|
|
@@ -207,6 +220,16 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
207
220
|
ok: boolean;
|
|
208
221
|
reason: string;
|
|
209
222
|
} | {
|
|
223
|
+
graphPreview?: {
|
|
224
|
+
impact?: import("../rtm/impact-advisory").ImpactAdvisory;
|
|
225
|
+
density?: import("../rtm/anchor-density").AnchorDensityFinding[];
|
|
226
|
+
cycles?: {
|
|
227
|
+
findings: import("../cpg/cycle-detect").CycleFinding[];
|
|
228
|
+
note: string;
|
|
229
|
+
};
|
|
230
|
+
architecture?: import("../cpg/arch-observe").ArchObservation[];
|
|
231
|
+
graphAsOf?: string;
|
|
232
|
+
} | undefined;
|
|
210
233
|
id: string;
|
|
211
234
|
type?: string;
|
|
212
235
|
status: string;
|
|
@@ -37,6 +37,7 @@ exports.HandlerRefusal = void 0;
|
|
|
37
37
|
exports.isHandlerRefusal = isHandlerRefusal;
|
|
38
38
|
exports.unreadableAmong = unreadableAmong;
|
|
39
39
|
exports.makeHandlers = makeHandlers;
|
|
40
|
+
exports.collectDecisions = collectDecisions;
|
|
40
41
|
// @implements A-SPEC-293
|
|
41
42
|
// @implements A-SPEC-292
|
|
42
43
|
// @implements A-SPEC-290
|
|
@@ -44,6 +45,8 @@ exports.makeHandlers = makeHandlers;
|
|
|
44
45
|
// @implements A-SPEC-277
|
|
45
46
|
// @implements A-SPEC-269
|
|
46
47
|
// @implements A-SPEC-267
|
|
48
|
+
// @implements A-SPEC-573.4 — shared with the benchmark so the two cannot drift.
|
|
49
|
+
const defuse_bound_1 = require("./defuse-bound");
|
|
47
50
|
const fs = __importStar(require("node:fs"));
|
|
48
51
|
const http = __importStar(require("node:http"));
|
|
49
52
|
const assoc_arm_1 = require("../assoc/assoc-arm");
|
|
@@ -204,6 +207,8 @@ const ledger_timeline_1 = require("../governance/ledger-timeline");
|
|
|
204
207
|
const version_conflict_1 = require("../spec/version-conflict");
|
|
205
208
|
const ledger_store_1 = require("../governance/ledger-store");
|
|
206
209
|
const provenance_chain_1 = require("../governance/provenance-chain");
|
|
210
|
+
// @implements A-SPEC-574.3 — the caller owns the store and hands the chain the question.
|
|
211
|
+
const ledger_store_2 = require("../governance/ledger-store");
|
|
207
212
|
const ledger_lock_1 = require("../governance/ledger-lock");
|
|
208
213
|
const decision_ledger_1 = require("../guardrail/decision-ledger");
|
|
209
214
|
const cspec_change_1 = require("../guardrail/cspec-change");
|
|
@@ -699,8 +704,24 @@ function collectDecisions(root, scanned, specs) {
|
|
|
699
704
|
});
|
|
700
705
|
}
|
|
701
706
|
}
|
|
702
|
-
catch {
|
|
703
|
-
|
|
707
|
+
catch { /* @implements A-SPEC-571.2 — no legacy .ax/decisions dir is not "no decisions": store
|
|
708
|
+
ADRs below are still a source. An unreadable dir degrades to the empty legacy set, not an early
|
|
709
|
+
return that would skip the store population. */
|
|
710
|
+
}
|
|
711
|
+
// @implements A-SPEC-571.2 — store ADRs JOIN the decisions population, so the existing consumers
|
|
712
|
+
// (DECISION nodes, constrained_by citations, supersedes) light up with no new edge kind. A store
|
|
713
|
+
// ADR is the canon: same id in legacy .ax/decisions is replaced (store wins).
|
|
714
|
+
for (const spec of specs) {
|
|
715
|
+
if (spec.type !== 'ADR')
|
|
716
|
+
continue;
|
|
717
|
+
const sup = spec.frontmatter?.supersedes;
|
|
718
|
+
const supersedes = typeof sup === 'string' && sup.trim() && sup.trim() !== 'null' ? sup.trim() : null;
|
|
719
|
+
const idx = decisions.findIndex((d) => d.id === spec.id);
|
|
720
|
+
const rec = { id: spec.id, title: spec.title, status: spec.status, supersedes };
|
|
721
|
+
if (idx >= 0)
|
|
722
|
+
decisions[idx] = rec;
|
|
723
|
+
else
|
|
724
|
+
decisions.push(rec);
|
|
704
725
|
}
|
|
705
726
|
const ids = new Set(decisions.map((d) => d.id));
|
|
706
727
|
// @implements A-SPEC-546.1 — recognise ADR-\d{3,} (4-digit ADRs no longer invisible), via a pure fn.
|
|
@@ -1330,7 +1351,117 @@ function makeRawHandlers(store, opts) {
|
|
|
1330
1351
|
const unreadable = typeof dir === 'string' ? (0, spec_store_1.unreadableSpecFiles)(dir) : [];
|
|
1331
1352
|
return { ok: false, reason: (0, spec_store_1.notFoundReason)(a.id, unreadable) };
|
|
1332
1353
|
}
|
|
1333
|
-
|
|
1354
|
+
// @implements A-SPEC-572.1
|
|
1355
|
+
// The graph advisory, delivered at DESIGN time. Measured on our own work: A-SPEC-571.1's
|
|
1356
|
+
// sealing advisory named the exact cluster three regressions then landed in — and by then
|
|
1357
|
+
// the design was done. The calculation was never the gap; the delivery time was. So the
|
|
1358
|
+
// read-only "what is blocking this right now" tool also answers "what will this scope leak,
|
|
1359
|
+
// and where is it dense" — using the SAME functions spec_approve calls, so the preview can
|
|
1360
|
+
// never disagree with the seal. Read-only stays read-only: no scan, no build, no ledger
|
|
1361
|
+
// append (a query must not pollute the observation denominator), and every failure degrades
|
|
1362
|
+
// to an absent field on an otherwise identical response.
|
|
1363
|
+
let graphPreview;
|
|
1364
|
+
try {
|
|
1365
|
+
if (cur.spec.type === 'A-SPEC' && a.root) {
|
|
1366
|
+
const dbPath = path.join(a.root, '.ax', 'rtm.sqlite');
|
|
1367
|
+
if (fs.existsSync(dbPath)) {
|
|
1368
|
+
const { declaredImpactGap } = require('../rtm/impact-advisory');
|
|
1369
|
+
const { anchorDensityFindings } = require('../rtm/anchor-density');
|
|
1370
|
+
const { filesToTouch } = require('../spec/compat-impact');
|
|
1371
|
+
const { RtmGraph } = require('../rtm/rtm-graph');
|
|
1372
|
+
const graph = new RtmGraph(dbPath);
|
|
1373
|
+
try {
|
|
1374
|
+
const ftt = filesToTouch(cur.spec);
|
|
1375
|
+
const impact = declaredImpactGap(ftt, graph, (rel) => { try {
|
|
1376
|
+
return fs.readFileSync(path.join(a.root, rel), 'utf8');
|
|
1377
|
+
}
|
|
1378
|
+
catch {
|
|
1379
|
+
return null;
|
|
1380
|
+
} });
|
|
1381
|
+
const density = anchorDensityFindings(ftt, graph.implementsAnchorCounts());
|
|
1382
|
+
// @implements A-SPEC-574.2 — the computation lives in cycle-detect; this file holds
|
|
1383
|
+
// the wiring only (measured: 85 anchors here against a p90 of 7).
|
|
1384
|
+
const { cycleAdvisory, classifyEdgeByTarget, CYCLE_ADVISORY_NOTE } = require('../cpg/cycle-detect');
|
|
1385
|
+
const readCache = new Map();
|
|
1386
|
+
const readSource = (rel) => {
|
|
1387
|
+
const hit = readCache.get(rel);
|
|
1388
|
+
if (hit !== undefined)
|
|
1389
|
+
return hit;
|
|
1390
|
+
// A read failure degrades to the conservative kind rather than losing the finding.
|
|
1391
|
+
let text = '';
|
|
1392
|
+
try {
|
|
1393
|
+
text = fs.readFileSync(path.join(a.root, rel), 'utf8');
|
|
1394
|
+
}
|
|
1395
|
+
catch {
|
|
1396
|
+
text = '';
|
|
1397
|
+
}
|
|
1398
|
+
readCache.set(rel, text);
|
|
1399
|
+
return text;
|
|
1400
|
+
};
|
|
1401
|
+
const cycleFindings = cycleAdvisory(ftt, graph.importEdges().map((edge) => ({
|
|
1402
|
+
...edge,
|
|
1403
|
+
kind: classifyEdgeByTarget(readSource(edge.from), edge.from, edge.to),
|
|
1404
|
+
})));
|
|
1405
|
+
// @implements A-SPEC-574.5 — the same import edges the cycle pass already read, plus
|
|
1406
|
+
// the parent-time symbol spans the graph already holds. No new scan, no new parse.
|
|
1407
|
+
const { architectureObservation } = require('../cpg/arch-observe');
|
|
1408
|
+
const { TreeSitterTsParser } = require('../cpg/language-parser');
|
|
1409
|
+
const { langForPath } = require('../cpg/cpg-scanner');
|
|
1410
|
+
// Bounded to the DECLARED files, whose text is read once and used for both numbers.
|
|
1411
|
+
const archParser = new TreeSitterTsParser();
|
|
1412
|
+
const archText = new Map();
|
|
1413
|
+
const readArch = (f) => {
|
|
1414
|
+
if (!archText.has(f)) {
|
|
1415
|
+
try {
|
|
1416
|
+
archText.set(f, fs.readFileSync(path.join(a.root, f), 'utf8'));
|
|
1417
|
+
}
|
|
1418
|
+
catch {
|
|
1419
|
+
archText.set(f, null);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return archText.get(f) ?? null;
|
|
1423
|
+
};
|
|
1424
|
+
const spans = new Map(ftt.map((f) => {
|
|
1425
|
+
const text = readArch(f);
|
|
1426
|
+
if (text === null)
|
|
1427
|
+
return [f, []];
|
|
1428
|
+
try {
|
|
1429
|
+
return [f, archParser.extractSymbols(text, langForPath(f))
|
|
1430
|
+
.filter((sy) => sy.kind !== 'class')
|
|
1431
|
+
.map((sy) => ({ startLine: sy.startLine, endLine: sy.endLine }))];
|
|
1432
|
+
}
|
|
1433
|
+
catch {
|
|
1434
|
+
return [f, []];
|
|
1435
|
+
}
|
|
1436
|
+
}));
|
|
1437
|
+
const arch = architectureObservation(ftt, spans, graph.importEdges(), (f) => { const t = readArch(f); return t === null ? null : t.split('\n').length; });
|
|
1438
|
+
if (impact || density.length > 0 || cycleFindings.length > 0 || arch.length > 0) {
|
|
1439
|
+
const graphAsOf = (() => { try {
|
|
1440
|
+
return fs.statSync(dbPath).mtime.toISOString();
|
|
1441
|
+
}
|
|
1442
|
+
catch {
|
|
1443
|
+
return undefined;
|
|
1444
|
+
} })();
|
|
1445
|
+
graphPreview = {
|
|
1446
|
+
...(impact ? { impact } : {}),
|
|
1447
|
+
...(density.length > 0 ? { density } : {}),
|
|
1448
|
+
...(cycleFindings.length > 0
|
|
1449
|
+
? { cycles: { findings: cycleFindings, note: CYCLE_ADVISORY_NOTE } } : {}),
|
|
1450
|
+
...(arch.length > 0 ? { architecture: arch } : {}),
|
|
1451
|
+
...(graphAsOf ? { graphAsOf } : {}),
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
finally {
|
|
1456
|
+
graph.close();
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
catch {
|
|
1462
|
+
graphPreview = undefined;
|
|
1463
|
+
}
|
|
1464
|
+
return { ok: true, ...(0, approval_status_1.describeApproval)(cur.spec, resolver(all)), ...(graphPreview ? { graphPreview } : {}) };
|
|
1334
1465
|
},
|
|
1335
1466
|
/**
|
|
1336
1467
|
* @implements A-SPEC-538.3
|
|
@@ -2447,7 +2578,7 @@ function makeRawHandlers(store, opts) {
|
|
|
2447
2578
|
commitTextBoost[hit.file] = hit.score / top;
|
|
2448
2579
|
}
|
|
2449
2580
|
catch { /* no history, no boost — the ranking falls back to lexical evidence alone */ }
|
|
2450
|
-
const
|
|
2581
|
+
const analyzeWith = (defUse) => (0, maintenance_analyze_1.analyzeMaintenance)({
|
|
2451
2582
|
semantic,
|
|
2452
2583
|
...common,
|
|
2453
2584
|
coverage: { ...common.coverage, historyStatus },
|
|
@@ -2456,7 +2587,43 @@ function makeRawHandlers(store, opts) {
|
|
|
2456
2587
|
commitTextBoost,
|
|
2457
2588
|
contextBundle,
|
|
2458
2589
|
groundTruth: a.groundTruth,
|
|
2590
|
+
defUse,
|
|
2459
2591
|
});
|
|
2592
|
+
// @implements A-SPEC-573.4 — def-use for the TOP CANDIDATES ONLY. Extracting it for the whole
|
|
2593
|
+
// repository costs +88.8% (measured 2026-09-08), well past this slice's budget; the first pass
|
|
2594
|
+
// says which handful of files are worth parsing, and the second pass reads their data flow.
|
|
2595
|
+
// Every step is fail-open: a parse failure, an unsupported language or a missing file leaves
|
|
2596
|
+
// the candidate's symbols exactly as the first pass produced them.
|
|
2597
|
+
const firstPass = analyzeWith();
|
|
2598
|
+
const analysis = (() => {
|
|
2599
|
+
const targets = firstPass.candidates.slice(0, defuse_bound_1.DEFUSE_TOP_FILES).map((c) => c.file);
|
|
2600
|
+
if (targets.length === 0)
|
|
2601
|
+
return firstPass;
|
|
2602
|
+
const defUse = {};
|
|
2603
|
+
try {
|
|
2604
|
+
const { TreeSitterTsParser, hasDataFlowWalk } = require('../cpg/language-parser');
|
|
2605
|
+
const { langForPath } = require('../cpg/cpg-scanner');
|
|
2606
|
+
const parser = new TreeSitterTsParser();
|
|
2607
|
+
for (const file of targets) {
|
|
2608
|
+
try {
|
|
2609
|
+
// The LANGUAGE matters: the first wiring omitted it and parsed Python as TypeScript,
|
|
2610
|
+
// which produced wrong facts and cost the second corpus 0.2376 -> 0.1741 on the
|
|
2611
|
+
// symbol axis. A language with no walk is skipped rather than guessed at.
|
|
2612
|
+
const lang = langForPath(file);
|
|
2613
|
+
if (!hasDataFlowWalk(lang))
|
|
2614
|
+
continue;
|
|
2615
|
+
const facts = parser.extractDataFlow(fs.readFileSync(path.join(root, file), 'utf8'), lang);
|
|
2616
|
+
if (facts !== undefined)
|
|
2617
|
+
defUse[file] = facts;
|
|
2618
|
+
}
|
|
2619
|
+
catch { /* one unreadable or unparseable file must not cost the other nine */ }
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
catch {
|
|
2623
|
+
return firstPass;
|
|
2624
|
+
}
|
|
2625
|
+
return Object.keys(defUse).length === 0 ? firstPass : analyzeWith(defUse);
|
|
2626
|
+
})();
|
|
2460
2627
|
// @implements A-SPEC-268 — persistence is OPT-IN. The tool is advertised read-only, and a
|
|
2461
2628
|
// regression pins that a cold project gains no `.ax/cpg_cache`; writing evidence by default
|
|
2462
2629
|
// would break that contract for every caller who only wanted to look.
|
|
@@ -3024,7 +3191,7 @@ function makeRawHandlers(store, opts) {
|
|
|
3024
3191
|
summary: `consumed single-use approval for: review-resolve ${envLiftedCriticals.join(', ')}`.slice(0, 200),
|
|
3025
3192
|
inputs: [(0, provenance_chain_1.nonceFingerprint)(String(approval.nonce))], rationale: approval.rationale,
|
|
3026
3193
|
authorization: (0, provenance_chain_1.authorizationRef)(approval.actor, approval.token),
|
|
3027
|
-
});
|
|
3194
|
+
}, { isNonceConsumed: (0, ledger_store_2.nonceConsumedIn)(ledgerFile) });
|
|
3028
3195
|
if (!won) {
|
|
3029
3196
|
throw new HandlerRefusal(`review_record: 단일 사용 승인(nonce)이 이미 소비되었습니다 — 재사용은 거부됩니다. 새 승인을 발급받으십시오`);
|
|
3030
3197
|
}
|
|
@@ -3154,7 +3321,7 @@ function makeRawHandlers(store, opts) {
|
|
|
3154
3321
|
summary: `consumed single-use approval for: ${coverTarget.kind} ${(0, provenance_chain_1.redactTarget)('command', coverTarget.target)}`.slice(0, 200),
|
|
3155
3322
|
inputs: [(0, provenance_chain_1.nonceFingerprint)(String(a.approval.nonce))], rationale: a.approval.rationale,
|
|
3156
3323
|
authorization: (0, provenance_chain_1.authorizationRef)(a.approval.actor, a.approval.token),
|
|
3157
|
-
});
|
|
3324
|
+
}, { isNonceConsumed: (0, ledger_store_2.nonceConsumedIn)(ledgerFile) });
|
|
3158
3325
|
}
|
|
3159
3326
|
catch {
|
|
3160
3327
|
won = false;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow history-derived candidates to files that could be source at all.
|
|
3
|
+
*
|
|
4
|
+
* The predicate is the SCANNER's, not the replay corpus's truth predicate — scoring against a
|
|
5
|
+
* filter copied from the metric would be gaming it. Deliberately NOT membership in the scanned
|
|
6
|
+
* set: a file a commit CREATES does not exist in the parent-time scan, and admitting exactly such
|
|
7
|
+
* files is A-SPEC-388's boundary contract. Every offender measured above is excluded by the
|
|
8
|
+
* extension test alone (.jsonl / .json / .md), so the stronger predicate would have cost that
|
|
9
|
+
* contract and bought nothing.
|
|
10
|
+
*
|
|
11
|
+
* Order is preserved because the downstream RRF lists are position-indexed.
|
|
12
|
+
*/
|
|
13
|
+
export declare function admitHistoryFiles(historyFiles: readonly string[]): string[];
|
|
14
|
+
/** The lexical path's predicate (localize.ts), restated so both channels demote the same trees. */
|
|
15
|
+
export declare const isVendorPath: (p: string) => boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isVendorPath = void 0;
|
|
4
|
+
exports.admitHistoryFiles = admitHistoryFiles;
|
|
5
|
+
// @implements A-SPEC-573.1
|
|
6
|
+
// The commit-prose channel (A-SPEC-388) lets files the lexical layer never scored ENTER the
|
|
7
|
+
// candidate pool — that is where its measured gain comes from (Top-10 recall 0.393 -> 0.601). But
|
|
8
|
+
// its keys come from git history, so it also admits files that CANNOT be the answer: this
|
|
9
|
+
// repository's ledger JSONL, the last-green baseline, CHANGELOG.md. Measured 2026-09-08 over 12
|
|
10
|
+
// real commit-subject requests: 76 of 120 emitted candidate slots (63.3%) went to such files, and
|
|
11
|
+
// one took the emission head. The channel is not the problem; its POPULATION is.
|
|
12
|
+
const cpg_scanner_1 = require("../cpg/cpg-scanner");
|
|
13
|
+
/**
|
|
14
|
+
* Narrow history-derived candidates to files that could be source at all.
|
|
15
|
+
*
|
|
16
|
+
* The predicate is the SCANNER's, not the replay corpus's truth predicate — scoring against a
|
|
17
|
+
* filter copied from the metric would be gaming it. Deliberately NOT membership in the scanned
|
|
18
|
+
* set: a file a commit CREATES does not exist in the parent-time scan, and admitting exactly such
|
|
19
|
+
* files is A-SPEC-388's boundary contract. Every offender measured above is excluded by the
|
|
20
|
+
* extension test alone (.jsonl / .json / .md), so the stronger predicate would have cost that
|
|
21
|
+
* contract and bought nothing.
|
|
22
|
+
*
|
|
23
|
+
* Order is preserved because the downstream RRF lists are position-indexed.
|
|
24
|
+
*/
|
|
25
|
+
function admitHistoryFiles(historyFiles) {
|
|
26
|
+
const admitted = historyFiles.filter((file) => !file.endsWith('.d.ts') && cpg_scanner_1.SCANNABLE_EXTENSIONS.some((ext) => file.endsWith(ext)));
|
|
27
|
+
// @implements A-SPEC-573.2 — the lexical path halves a vendored file's score AND orders vendored
|
|
28
|
+
// last (localize.ts). Entering at score 0, this channel bypassed both: measured 2026-09-08, the
|
|
29
|
+
// only two non-source candidates the extension test let through were vendored files and BOTH held
|
|
30
|
+
// the emission head. Demote, never drop — vendored code is the answer in some projects, which is
|
|
31
|
+
// why the lexical path orders rather than filters. Stable within each group: the downstream RRF
|
|
32
|
+
// lists are position-indexed.
|
|
33
|
+
return [...admitted.filter((f) => !(0, exports.isVendorPath)(f)), ...admitted.filter(exports.isVendorPath)];
|
|
34
|
+
}
|
|
35
|
+
/** The lexical path's predicate (localize.ts), restated so both channels demote the same trees. */
|
|
36
|
+
const isVendorPath = (p) => /(^|\/)(reference|vendor|vendors|third_party|third-party|external)\//i.test(p);
|
|
37
|
+
exports.isVendorPath = isVendorPath;
|
|
@@ -3,6 +3,7 @@ import type { Spec } from '../spec/spec-parser';
|
|
|
3
3
|
import type { RtmGraph } from '../rtm/rtm-graph';
|
|
4
4
|
import type { ContextBundle } from '../context/bundler';
|
|
5
5
|
import { type TestScope } from '../rtm/test-scope';
|
|
6
|
+
import { type DecisionContextEntry } from '../rtm/decision-context';
|
|
6
7
|
import type { ResolutionReport } from '../rtm/rtm-builder';
|
|
7
8
|
import { type TestEvidence } from '../review/test-evidence';
|
|
8
9
|
import { type LanguageGap } from '../cpg/language-capability';
|
|
@@ -116,6 +117,14 @@ export interface MaintenanceAnalysisInput {
|
|
|
116
117
|
* that is the whole result.
|
|
117
118
|
*/
|
|
118
119
|
commitTextBoost?: Record<string, number>;
|
|
120
|
+
/**
|
|
121
|
+
* @implements A-SPEC-573.4
|
|
122
|
+
* Def-use facts for a BOUNDED set of files — the caller extracts them, because this core does no
|
|
123
|
+
* I/O. Extracting them for the whole repository costs +88.8% (measured 2026-09-08, 2572ms ->
|
|
124
|
+
* 4857ms), which is why the callers pass the top candidates only. Absent means "not extracted",
|
|
125
|
+
* and the symbols then read exactly as they did before this existed.
|
|
126
|
+
*/
|
|
127
|
+
defUse?: Record<string, import('../cpg/language-parser').DataFlowFacts>;
|
|
119
128
|
contextBundle?: ContextBundle | null;
|
|
120
129
|
/**
|
|
121
130
|
* @implements A-SPEC-290
|
|
@@ -212,6 +221,8 @@ export interface MaintenanceAnalysis {
|
|
|
212
221
|
score: number;
|
|
213
222
|
symbols: string[];
|
|
214
223
|
evidence: string[];
|
|
224
|
+
/** @implements A-SPEC-572.3 — decisions constraining this candidate; information only. */
|
|
225
|
+
decisionContext?: DecisionContextEntry[];
|
|
215
226
|
}>;
|
|
216
227
|
/**
|
|
217
228
|
* @implements A-SPEC-494 — the semantic ALTERNATES: top-3 cached-vector cosines among files
|
|
@@ -26,9 +26,16 @@ const scope_1 = require("../review/scope");
|
|
|
26
26
|
// the measured numbers transfer to the shipped surface.
|
|
27
27
|
const assoc_arm_1 = require("../assoc/assoc-arm");
|
|
28
28
|
const acceptance_quality_1 = require("../spec/acceptance-quality");
|
|
29
|
+
// @implements A-SPEC-572.3 — the decision lookup lives in its own pure module: this file
|
|
30
|
+
// already carries 35 anchors (p90 is 7), and the design-time density advisory said so BEFORE
|
|
31
|
+
// this slice fixed its Files-to-Touch. Adding logic here would have made the file denser; the
|
|
32
|
+
// call site is one line, the logic is next door.
|
|
33
|
+
const decision_context_1 = require("../rtm/decision-context");
|
|
29
34
|
const taint_1 = require("../rtm/taint");
|
|
30
35
|
const test_evidence_1 = require("../review/test-evidence");
|
|
31
36
|
const language_capability_1 = require("../cpg/language-capability");
|
|
37
|
+
const history_admission_1 = require("./history-admission");
|
|
38
|
+
const defuse_symbols_1 = require("../rtm/defuse-symbols");
|
|
32
39
|
// @implements A-SPEC-409 — the SHAPE gained a field (`rerankPool`), so a consumer can detect it.
|
|
33
40
|
// @implements A-SPEC-405 A-SPEC-407 — the EXTRACTOR changed too: the same request now returns a
|
|
34
41
|
// differently ordered candidate list (keep-head rank fusion, then the graph-hop list), so two
|
|
@@ -176,10 +183,12 @@ function analyzeMaintenance(input) {
|
|
|
176
183
|
// Files the commit prose names but the lexical layer never scored have to be able to ENTER, or the
|
|
177
184
|
// fusion can only reorder what lexical already found — and reaching what it missed is where the
|
|
178
185
|
// measured gain came from (Top-10 recall 0.393 -> 0.601 on the second corpus).
|
|
186
|
+
// @implements A-SPEC-573.1 — but only files that could be source at all. The keys come from git
|
|
187
|
+
// history, so without this the pool also admits ledger JSONL and build artifacts that cannot be
|
|
188
|
+
// the answer (measured: 63.3% of emitted slots). The lexical hits are untouched, and a file the
|
|
189
|
+
// scan has never seen still enters — that is the boundary contract directly above.
|
|
179
190
|
const seeded = hasBoost
|
|
180
|
-
? [...localization.hits, ...Object.keys(boost)
|
|
181
|
-
.filter((file) => !localization.hits.some((h) => h.file === file))
|
|
182
|
-
.map((file) => ({ file, score: 0, matchedSymbols: [], viaSpecs: [], why: ['reached through commit history'] }))]
|
|
191
|
+
? [...localization.hits, ...(0, history_admission_1.admitHistoryFiles)(Object.keys(boost).filter((file) => !localization.hits.some((h) => h.file === file))).map((file) => ({ file, score: 0, matchedSymbols: [], viaSpecs: [], why: ['reached through commit history'] }))]
|
|
183
192
|
: localization.hits;
|
|
184
193
|
const reranked = !hasPrior && !hasBoost
|
|
185
194
|
? localization.hits
|
|
@@ -197,7 +206,13 @@ function analyzeMaintenance(input) {
|
|
|
197
206
|
// than prose spread evenly. Kept, with the cost named rather than defended.
|
|
198
207
|
+ (hasBoost ? COMMIT_TEXT_WEIGHT * (boost[hit.file] ?? 0) * Math.max(1, hit.score) : 0),
|
|
199
208
|
}))
|
|
200
|
-
.
|
|
209
|
+
// @implements A-SPEC-573.2 — vendored last, exactly as the lexical path orders it
|
|
210
|
+
// (localize.ts). A history-seeded file scores 0 lexically, so the boost term alone can carry
|
|
211
|
+
// it to 2.0 and hand it the head — measured, a vendored file took the head in 2 of 12 real
|
|
212
|
+
// requests. This is the ordering rule that path already applies; the channel was bypassing
|
|
213
|
+
// it. Demotion, not exclusion: vendored code is the answer in some projects.
|
|
214
|
+
.sort((a, b) => Number((0, history_admission_1.isVendorPath)(a.file)) - Number((0, history_admission_1.isVendorPath)(b.file))
|
|
215
|
+
|| b.score - a.score || a.file.localeCompare(b.file));
|
|
201
216
|
// @implements A-SPEC-405
|
|
202
217
|
// S-405 decomposed the recall loss measured in S-404: on the eight blind cases 18 of 18 truth
|
|
203
218
|
// files were already in this pool and 7 sat at ranks 12-93, so widening retrieval could not have
|
|
@@ -289,7 +304,10 @@ function analyzeMaintenance(input) {
|
|
|
289
304
|
;
|
|
290
305
|
const lists = [
|
|
291
306
|
desc((f) => byFile.get(f).score),
|
|
292
|
-
|
|
307
|
+
// @implements A-SPEC-573.2 — vendored last inside this list. Ordering `seeded` was not enough:
|
|
308
|
+
// measured, the head is decided HERE, and a vendored file with a high boost took it twice in
|
|
309
|
+
// twelve requests. The boost VALUES are untouched, so the lexical scoring above is unchanged.
|
|
310
|
+
hasBoost ? desc((f) => boost[f] ?? 0).sort((a, b) => Number((0, history_admission_1.isVendorPath)(a)) - Number((0, history_admission_1.isVendorPath)(b))) : [],
|
|
293
311
|
hasPrior ? desc((f) => (prior[f] ?? 1) - 1) : [],
|
|
294
312
|
cited.length > 0 ? [] : hops,
|
|
295
313
|
];
|
|
@@ -340,10 +358,23 @@ function analyzeMaintenance(input) {
|
|
|
340
358
|
// @implements A-SPEC-486 — the S-502/503 calibration point for gemini cosines. Display only:
|
|
341
359
|
// nothing in this module compares against it to drop or reorder anything.
|
|
342
360
|
const SEM_VERIFY_TAU = 0.65;
|
|
361
|
+
// @implements A-SPEC-573.4 — def-use rides BEHIND the lexical match, never in front of it: the
|
|
362
|
+
// symbols the request actually named keep the front, and the data-flow neighbours are appended.
|
|
363
|
+
const defUseTerms = input.defUse === undefined ? [] : (0, localize_1.significantTerms)(input.request);
|
|
364
|
+
const symbolsByFile = new Map(scanned.map((f) => [f.sourcePath, f.symbols.map((sy) => sy.qualifiedName)]));
|
|
365
|
+
const symbolsOf = (file, matched) => {
|
|
366
|
+
const facts = input.defUse?.[file];
|
|
367
|
+
if (facts === undefined)
|
|
368
|
+
return matched;
|
|
369
|
+
const inFile = symbolsByFile.get(file) ?? [];
|
|
370
|
+
if (inFile.length === 0)
|
|
371
|
+
return matched;
|
|
372
|
+
return (0, defuse_symbols_1.enrichCandidateSymbols)(matched, (0, defuse_symbols_1.rankSymbolsByDefUse)(facts, defUseTerms, inFile));
|
|
373
|
+
};
|
|
343
374
|
const shape = (hit) => ({
|
|
344
375
|
file: hit.file,
|
|
345
376
|
score: hit.score,
|
|
346
|
-
symbols: sortedUnique(hit.matchedSymbols),
|
|
377
|
+
symbols: symbolsOf(hit.file, sortedUnique(hit.matchedSymbols)),
|
|
347
378
|
evidence: hit.why,
|
|
348
379
|
});
|
|
349
380
|
// @implements A-SPEC-428
|
|
@@ -371,12 +402,32 @@ function analyzeMaintenance(input) {
|
|
|
371
402
|
const rescuedFiles = new Set(rescued.map((h) => h.file));
|
|
372
403
|
const rerankPool = [...rescued, ...exposedTail.filter((h) => !rescuedFiles.has(h.file))]
|
|
373
404
|
.slice(0, RERANK_POOL_N).map(shape);
|
|
405
|
+
const lexicalSymbolsOf = new Map(ordered.slice(0, LOCALIZATION_TOP_N).map((hit) => [hit.file, sortedUnique(hit.matchedSymbols)]));
|
|
374
406
|
const candidates = ordered.slice(0, LOCALIZATION_TOP_N).map((hit) => ({
|
|
375
407
|
file: hit.file,
|
|
376
408
|
score: hit.score,
|
|
377
|
-
|
|
409
|
+
// @implements A-SPEC-573.4 — the emitted list, which is what a caller reads and what the
|
|
410
|
+
// benchmark scores on the symbol axis. `shape` above serves the rerank pool only; wiring the
|
|
411
|
+
// enrichment there and not here would have enriched a surface nobody grades (caught by the
|
|
412
|
+
// symbol axis reporting NO movement — the instrument earned its keep on its first use).
|
|
413
|
+
symbols: symbolsOf(hit.file, sortedUnique(hit.matchedSymbols)),
|
|
378
414
|
evidence: [...hit.why],
|
|
379
415
|
}));
|
|
416
|
+
// @implements A-SPEC-572.3
|
|
417
|
+
// "What broke" then "why is it this way" — the order a person diagnoses in. The decisions are
|
|
418
|
+
// already in the graph (REQ-571 put store ADRs there; A-SPEC-293 builds `constrained_by` from
|
|
419
|
+
// citations), so this is a lookup, not a new mechanism. Information only: it rides beside a
|
|
420
|
+
// candidate and never enters the score or the ordering above.
|
|
421
|
+
{
|
|
422
|
+
const anchorsOf = new Map(scanned.map((f) => [f.sourcePath, f.implementsSpecs ?? []]));
|
|
423
|
+
const ctx = (0, decision_context_1.decisionContextFor)(candidates.map((c) => c.file), (file) => anchorsOf.get(file) ?? [], input.graph);
|
|
424
|
+
for (const candidate of candidates) {
|
|
425
|
+
const entries = ctx.get(candidate.file);
|
|
426
|
+
if (entries && entries.length > 0) {
|
|
427
|
+
candidate.decisionContext = entries;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
380
431
|
// @implements A-SPEC-478 — the uncited semantic head rerank, exactly the arm S-495 measured:
|
|
381
432
|
// same math (cosine over cached doc vectors, pool FIXED), same gate (the request cited no
|
|
382
433
|
// spec). The gate is load-bearing both ways — uncited corpora gained +26%/+93% at the head,
|
|
@@ -440,7 +491,12 @@ function analyzeMaintenance(input) {
|
|
|
440
491
|
.flatMap((f) => f.symbols.map((sym) => sym.qualifiedName));
|
|
441
492
|
// A file you edited is not unaffected by your edit, so these are direct impact and not merely a
|
|
442
493
|
// traversal starting point.
|
|
443
|
-
|
|
494
|
+
// @implements A-SPEC-573.4 — the impact axis seeds from what the LEXICAL layer matched, not from
|
|
495
|
+
// the def-use symbols appended for emission. Measured: seeding the enriched list moved the union
|
|
496
|
+
// axis on both corpora (precision 0.1667->0.2075 / 0.1476->0.1689, recall 0.8755->0.8698 /
|
|
497
|
+
// 0.8302->0.7996) — a different axis than this slice targets, and axis transfer is exactly what
|
|
498
|
+
// this repository's record forbids. The emitted symbols stay enriched; the seeds do not.
|
|
499
|
+
const direct = sortedUnique([...lexicalSymbolsOf.values()].flat().concat(changedSymbols));
|
|
444
500
|
const transitiveIds = new Set();
|
|
445
501
|
const maxCallDepth = 3;
|
|
446
502
|
const hubInDegree = 12;
|
|
@@ -15,12 +15,4 @@ export type IdVerdict = {
|
|
|
15
15
|
reason: string;
|
|
16
16
|
nextAvailable: number;
|
|
17
17
|
};
|
|
18
|
-
/**
|
|
19
|
-
* 새 id 의 base 가 코퍼스 max base 를 한 칸 넘게 뛰면 거부한다.
|
|
20
|
-
*
|
|
21
|
-
* - 빈 코퍼스, 또는 base 를 못 뽑는 id → 통과(비교 대상이 없거나, 형태 검증은 별도 소관이라 한 결함에
|
|
22
|
-
* 두 이름을 주지 않는다).
|
|
23
|
-
* - base ≤ maxBase(갭 메우기·체인 완성) 또는 base == maxBase+1(새 체인) → 통과.
|
|
24
|
-
* - base > maxBase+1(leap) → 거부, 다음 가용 번호를 문면과 필드에 댄다.
|
|
25
|
-
*/
|
|
26
18
|
export declare function sequentialIdVerdict(newId: string, existingIds: string[]): IdVerdict;
|
|
@@ -30,11 +30,20 @@ function specIdBase(id) {
|
|
|
30
30
|
* - base ≤ maxBase(갭 메우기·체인 완성) 또는 base == maxBase+1(새 체인) → 통과.
|
|
31
31
|
* - base > maxBase+1(leap) → 거부, 다음 가용 번호를 문면과 필드에 댄다.
|
|
32
32
|
*/
|
|
33
|
+
/** The id's number SPACE. @implements A-SPEC-571.1 — ADR keeps its own sequence so a decision
|
|
34
|
+
* (jarvis's ADR-0001) neither blocks nor is blocked by the functional chain's max. Everything
|
|
35
|
+
* else shares one space, exactly as before. */
|
|
36
|
+
function idSpaceOf(id) {
|
|
37
|
+
return /^ADR-/.test(id.trim()) ? 'ADR' : 'functional';
|
|
38
|
+
}
|
|
33
39
|
function sequentialIdVerdict(newId, existingIds) {
|
|
34
40
|
const base = specIdBase(newId);
|
|
35
41
|
if (base === null)
|
|
36
42
|
return { ok: true };
|
|
37
|
-
|
|
43
|
+
// @implements A-SPEC-571.1 — compare only within the same number space.
|
|
44
|
+
const space = idSpaceOf(newId);
|
|
45
|
+
const bases = (existingIds ?? []).filter((id) => idSpaceOf(id) === space)
|
|
46
|
+
.map(specIdBase).filter((n) => n !== null);
|
|
38
47
|
if (bases.length === 0)
|
|
39
48
|
return { ok: true };
|
|
40
49
|
const maxBase = Math.max(...bases);
|
|
@@ -20,6 +20,10 @@ export interface ReplayOutcome {
|
|
|
20
20
|
truthFiles: string[];
|
|
21
21
|
selectedTests: string[];
|
|
22
22
|
truthTests: string[];
|
|
23
|
+
/** Symbols the change actually touched (parent-time ranges ∩ changed lines). */
|
|
24
|
+
truthSymbols?: string[];
|
|
25
|
+
/** Predicted symbols, best first — the qualified names the emitted candidates carried. */
|
|
26
|
+
rankedSymbols?: string[];
|
|
23
27
|
}
|
|
24
28
|
export interface CutMetrics {
|
|
25
29
|
/** Share of cases where at least one truth file appears in the first K. */
|
|
@@ -33,6 +37,8 @@ export interface EvaluationMetrics {
|
|
|
33
37
|
scorableCases: number;
|
|
34
38
|
topK: Record<number, CutMetrics>;
|
|
35
39
|
testRecall: number | null;
|
|
40
|
+
symbolTopK: Record<number, CutMetrics>;
|
|
41
|
+
symbolCases: number;
|
|
36
42
|
}
|
|
37
43
|
export declare function evaluationMetrics(outcomes: readonly ReplayOutcome[]): EvaluationMetrics;
|
|
38
44
|
/**
|
|
@@ -42,7 +42,24 @@ function evaluationMetrics(outcomes) {
|
|
|
42
42
|
}
|
|
43
43
|
const testScorable = outcomes.filter((o) => o.truthTests.length > 0);
|
|
44
44
|
const testRecall = mean(testScorable.map((o) => o.truthTests.filter((t) => o.selectedTests.includes(t)).length / o.truthTests.length));
|
|
45
|
-
|
|
45
|
+
// @implements A-SPEC-573.3
|
|
46
|
+
const symbolScorable = outcomes.filter((o) => (o.truthSymbols?.length ?? 0) > 0);
|
|
47
|
+
const symbolTopK = {};
|
|
48
|
+
for (const k of exports.TOP_K) {
|
|
49
|
+
const hits = [];
|
|
50
|
+
const recalls = [];
|
|
51
|
+
const precisions = [];
|
|
52
|
+
for (const o of symbolScorable) {
|
|
53
|
+
const cut = (o.rankedSymbols ?? []).slice(0, k);
|
|
54
|
+
const found = cut.filter((s2) => o.truthSymbols.includes(s2)).length;
|
|
55
|
+
hits.push(found > 0 ? 1 : 0);
|
|
56
|
+
recalls.push(found / o.truthSymbols.length);
|
|
57
|
+
precisions.push(cut.length === 0 ? 0 : found / cut.length);
|
|
58
|
+
}
|
|
59
|
+
symbolTopK[k] = { hitRate: mean(hits), recall: mean(recalls), precision: mean(precisions) };
|
|
60
|
+
}
|
|
61
|
+
return { cases: outcomes.length, scorableCases: scorable.length, topK, testRecall,
|
|
62
|
+
symbolTopK, symbolCases: symbolScorable.length };
|
|
46
63
|
}
|
|
47
64
|
/**
|
|
48
65
|
* Impact had no benchmark until this existed, so an impact regression could not fail anything.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** F1 over the SET of files, which is the edit set a caller actually acts on. */
|
|
2
|
+
export declare function editSetF1(predicted: readonly string[], truth: readonly string[]): number;
|
|
3
|
+
export interface PairedPower {
|
|
4
|
+
n: number;
|
|
5
|
+
meanDiff: number | null;
|
|
6
|
+
/** Sample standard deviation of the paired differences (n-1). Null below two observations. */
|
|
7
|
+
sdDiff: number | null;
|
|
8
|
+
sem: number | null;
|
|
9
|
+
/** Smallest difference detectable at 80% power, two-sided alpha 0.05. */
|
|
10
|
+
mde80: number | null;
|
|
11
|
+
/** True when the degrees of freedom fell outside the table and the normal approximation was used. */
|
|
12
|
+
approx: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function pairedPower(diffs: readonly number[]): PairedPower;
|