@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.
- package/CHANGELOG.md +55 -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/elicit-approval.d.ts +32 -2
- package/dist/holmes/mcp/elicit-approval.js +30 -2
- package/dist/holmes/mcp/handlers.d.ts +88 -0
- package/dist/holmes/mcp/handlers.js +636 -35
- 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/server.js +13 -10
- 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
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Spec } from '../spec/spec-parser';
|
|
2
|
+
import type { ScannedFile } from '../cpg/cpg-scanner';
|
|
3
|
+
/**
|
|
4
|
+
* The task embeddings are actually claimed to be good at.
|
|
5
|
+
*
|
|
6
|
+
* Every benchmark in this work so far asks: given a commit's subject, which files did it change?
|
|
7
|
+
* Eight semantic mechanisms have been measured against it and all eight were negative. But that is
|
|
8
|
+
* not the task the semantic-layer argument is about. The argument is about the gap between the words
|
|
9
|
+
* a requirement is written in and the names the code is written in — a requirement says "erase
|
|
10
|
+
* personal data on withdrawal", the design says "account deprovisioning", the code says
|
|
11
|
+
* `revokeCredentials()`. No string matcher crosses that, which is exactly why an embedding might.
|
|
12
|
+
*
|
|
13
|
+
* The ground truth for it already exists here: an `@implements` anchor IS a requirement-to-code
|
|
14
|
+
* link, recorded by the person who wrote the code and verified by the constitution gate.
|
|
15
|
+
*
|
|
16
|
+
* **The identifier must be withheld.** A query containing `A-SPEC-350` is answered by the citation
|
|
17
|
+
* path, which resolves the anchor directly and measures nothing about semantics — it is the same
|
|
18
|
+
* leak that made the localization numbers look like a retrieval result when they were a commit
|
|
19
|
+
* convention. The query is the spec's prose with every spec id removed.
|
|
20
|
+
*/
|
|
21
|
+
export interface TraceabilityCase {
|
|
22
|
+
/** The spec's prose, with every identifier stripped. */
|
|
23
|
+
query: string;
|
|
24
|
+
/** Files anchoring this spec — the answer. */
|
|
25
|
+
truth: string[];
|
|
26
|
+
/** Kept for reporting only; never given to a ranker. */
|
|
27
|
+
specId: string;
|
|
28
|
+
}
|
|
29
|
+
/** Strip every spec identifier, so the citation path cannot answer the question. */
|
|
30
|
+
export declare function withholdIdentifiers(text: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Cases from a spec store and a scan: one per A-SPEC that at least one file anchors.
|
|
33
|
+
*
|
|
34
|
+
* A spec nothing implements has no answer to find, and scoring it would report a failure that no
|
|
35
|
+
* ranker could have avoided.
|
|
36
|
+
*/
|
|
37
|
+
export declare function traceabilityCases(specs: readonly Spec[], scanned: readonly ScannedFile[]): TraceabilityCase[];
|
|
38
|
+
export interface TraceabilityScore {
|
|
39
|
+
cases: number;
|
|
40
|
+
/** Share of cases where any truth file appears in the top K. */
|
|
41
|
+
hitRate: number | null;
|
|
42
|
+
/** Mean share of truth files recovered in the top K. */
|
|
43
|
+
recall: number | null;
|
|
44
|
+
/** Mean reciprocal rank of the first correct file — how near the top the answer lands. */
|
|
45
|
+
mrr: number | null;
|
|
46
|
+
}
|
|
47
|
+
export declare function scoreTraceability(cases: readonly TraceabilityCase[], rank: (query: string) => readonly string[], topK: number): TraceabilityScore;
|
|
48
|
+
/**
|
|
49
|
+
* @implements A-SPEC-383
|
|
50
|
+
* Which cases a string matcher could possibly answer.
|
|
51
|
+
*
|
|
52
|
+
* Dense retrieval lost the traceability benchmark 0.424 to 0.699 — but that average mixes two kinds
|
|
53
|
+
* of case. Measured over 225 of them, only **9%** of a spec's terms appear anywhere in the names of
|
|
54
|
+
* the files implementing it, and **24 cases share no term at all**. Where terms do overlap they are
|
|
55
|
+
* rare ones, and IDF is built to amplify exactly those, so lexical wins. Where NOTHING overlaps, a
|
|
56
|
+
* string matcher cannot win by any weighting — and that subset is precisely where an embedding is
|
|
57
|
+
* claimed to be necessary. It has never been scored on its own.
|
|
58
|
+
*
|
|
59
|
+
* Splitting the corpus this way is what turns "dense loses on average" into an answerable question:
|
|
60
|
+
* does it lose everywhere, or does it lose overall while winning where nothing else can reach?
|
|
61
|
+
*/
|
|
62
|
+
export declare function lexicallyReachable(c: TraceabilityCase, fileTerms: (file: string) => ReadonlySet<string>, queryTerms: (text: string) => readonly string[]): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* @implements A-SPEC-401
|
|
65
|
+
* Files reached by walking the spec graph from the specs a requirement's prose resembles.
|
|
66
|
+
*
|
|
67
|
+
* The Goal is about a NEW or CHANGED requirement being handed to the harness, and every benchmark in
|
|
68
|
+
* this work has used a commit subject as the query instead. On the requirement task, lexical, BM25
|
|
69
|
+
* and dense retrieval have all been scored; graph traversal has not.
|
|
70
|
+
*
|
|
71
|
+
* The path: requirement prose → the specs whose text it most resembles → their parents and siblings
|
|
72
|
+
* → the files those anchor. It is the shape the Goal describes, and the shape a person uses when
|
|
73
|
+
* they say "this is like the thing we did for X, so look where X lives".
|
|
74
|
+
*
|
|
75
|
+
* **The target spec is excluded**, without exception. The query IS its prose, so leaving it in the
|
|
76
|
+
* index returns its own anchors and scores 0.99 — a leak this benchmark has already produced once.
|
|
77
|
+
*/
|
|
78
|
+
export declare function reachThroughSpecGraph(seedSpecIds: readonly string[], specs: readonly {
|
|
79
|
+
id: string;
|
|
80
|
+
dependsOn?: string[];
|
|
81
|
+
}[], anchoredFiles: ReadonlyMap<string, readonly string[]>, hops: number): string[];
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withholdIdentifiers = withholdIdentifiers;
|
|
4
|
+
exports.traceabilityCases = traceabilityCases;
|
|
5
|
+
exports.scoreTraceability = scoreTraceability;
|
|
6
|
+
exports.lexicallyReachable = lexicallyReachable;
|
|
7
|
+
exports.reachThroughSpecGraph = reachThroughSpecGraph;
|
|
8
|
+
const ID_PATTERN = /\b(?:[AHT]-SPEC|REQ|S)-\d+(?:\.\d+)?\b/gi;
|
|
9
|
+
/** Strip every spec identifier, so the citation path cannot answer the question. */
|
|
10
|
+
function withholdIdentifiers(text) {
|
|
11
|
+
return text.replace(ID_PATTERN, ' ').replace(/\s{2,}/g, ' ').trim();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Cases from a spec store and a scan: one per A-SPEC that at least one file anchors.
|
|
15
|
+
*
|
|
16
|
+
* A spec nothing implements has no answer to find, and scoring it would report a failure that no
|
|
17
|
+
* ranker could have avoided.
|
|
18
|
+
*/
|
|
19
|
+
function traceabilityCases(specs, scanned) {
|
|
20
|
+
const bySpec = new Map();
|
|
21
|
+
for (const f of scanned) {
|
|
22
|
+
for (const id of new Set(f.implementsSpecs ?? [])) {
|
|
23
|
+
(bySpec.get(id) ?? bySpec.set(id, []).get(id)).push(f.sourcePath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const cases = [];
|
|
27
|
+
for (const s of specs) {
|
|
28
|
+
if (s.type !== 'A-SPEC')
|
|
29
|
+
continue;
|
|
30
|
+
const truth = bySpec.get(s.id);
|
|
31
|
+
if (!truth || truth.length === 0)
|
|
32
|
+
continue;
|
|
33
|
+
const query = withholdIdentifiers(`${s.title}. ${Object.values(s.sections).join(' ')}`);
|
|
34
|
+
if (query.length === 0)
|
|
35
|
+
continue;
|
|
36
|
+
cases.push({ query, truth: [...new Set(truth)].sort(), specId: s.id });
|
|
37
|
+
}
|
|
38
|
+
return cases.sort((a, b) => a.specId.localeCompare(b.specId));
|
|
39
|
+
}
|
|
40
|
+
function scoreTraceability(cases, rank, topK) {
|
|
41
|
+
if (cases.length === 0)
|
|
42
|
+
return { cases: 0, hitRate: null, recall: null, mrr: null };
|
|
43
|
+
let hits = 0, recallSum = 0, rrSum = 0;
|
|
44
|
+
for (const c of cases) {
|
|
45
|
+
const ranked = rank(c.query).slice(0, topK);
|
|
46
|
+
const truth = new Set(c.truth);
|
|
47
|
+
const found = ranked.filter((f) => truth.has(f));
|
|
48
|
+
if (found.length > 0)
|
|
49
|
+
hits++;
|
|
50
|
+
recallSum += found.length / c.truth.length;
|
|
51
|
+
const first = ranked.findIndex((f) => truth.has(f));
|
|
52
|
+
rrSum += first === -1 ? 0 : 1 / (first + 1);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
cases: cases.length,
|
|
56
|
+
hitRate: hits / cases.length,
|
|
57
|
+
recall: recallSum / cases.length,
|
|
58
|
+
mrr: rrSum / cases.length,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @implements A-SPEC-383
|
|
63
|
+
* Which cases a string matcher could possibly answer.
|
|
64
|
+
*
|
|
65
|
+
* Dense retrieval lost the traceability benchmark 0.424 to 0.699 — but that average mixes two kinds
|
|
66
|
+
* of case. Measured over 225 of them, only **9%** of a spec's terms appear anywhere in the names of
|
|
67
|
+
* the files implementing it, and **24 cases share no term at all**. Where terms do overlap they are
|
|
68
|
+
* rare ones, and IDF is built to amplify exactly those, so lexical wins. Where NOTHING overlaps, a
|
|
69
|
+
* string matcher cannot win by any weighting — and that subset is precisely where an embedding is
|
|
70
|
+
* claimed to be necessary. It has never been scored on its own.
|
|
71
|
+
*
|
|
72
|
+
* Splitting the corpus this way is what turns "dense loses on average" into an answerable question:
|
|
73
|
+
* does it lose everywhere, or does it lose overall while winning where nothing else can reach?
|
|
74
|
+
*/
|
|
75
|
+
function lexicallyReachable(c, fileTerms, queryTerms) {
|
|
76
|
+
const q = queryTerms(c.query);
|
|
77
|
+
if (q.length === 0)
|
|
78
|
+
return false;
|
|
79
|
+
for (const file of c.truth) {
|
|
80
|
+
const terms = fileTerms(file);
|
|
81
|
+
for (const t of q)
|
|
82
|
+
if (terms.has(t))
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @implements A-SPEC-401
|
|
89
|
+
* Files reached by walking the spec graph from the specs a requirement's prose resembles.
|
|
90
|
+
*
|
|
91
|
+
* The Goal is about a NEW or CHANGED requirement being handed to the harness, and every benchmark in
|
|
92
|
+
* this work has used a commit subject as the query instead. On the requirement task, lexical, BM25
|
|
93
|
+
* and dense retrieval have all been scored; graph traversal has not.
|
|
94
|
+
*
|
|
95
|
+
* The path: requirement prose → the specs whose text it most resembles → their parents and siblings
|
|
96
|
+
* → the files those anchor. It is the shape the Goal describes, and the shape a person uses when
|
|
97
|
+
* they say "this is like the thing we did for X, so look where X lives".
|
|
98
|
+
*
|
|
99
|
+
* **The target spec is excluded**, without exception. The query IS its prose, so leaving it in the
|
|
100
|
+
* index returns its own anchors and scores 0.99 — a leak this benchmark has already produced once.
|
|
101
|
+
*/
|
|
102
|
+
function reachThroughSpecGraph(seedSpecIds, specs, anchoredFiles, hops) {
|
|
103
|
+
const known = new Set(specs.map((s) => s.id));
|
|
104
|
+
const adj = new Map();
|
|
105
|
+
const link = (a, b) => {
|
|
106
|
+
(adj.get(a) ?? adj.set(a, new Set()).get(a)).add(b);
|
|
107
|
+
(adj.get(b) ?? adj.set(b, new Set()).get(b)).add(a);
|
|
108
|
+
};
|
|
109
|
+
for (const s of specs)
|
|
110
|
+
for (const parent of s.dependsOn ?? [])
|
|
111
|
+
if (known.has(parent))
|
|
112
|
+
link(s.id, parent);
|
|
113
|
+
const seen = new Set(seedSpecIds.filter((id) => known.has(id)));
|
|
114
|
+
let wave = [...seen];
|
|
115
|
+
for (let k = 0; k < hops && wave.length > 0; k++) {
|
|
116
|
+
const next = [];
|
|
117
|
+
for (const id of wave)
|
|
118
|
+
for (const n of adj.get(id) ?? []) {
|
|
119
|
+
if (seen.has(n))
|
|
120
|
+
continue;
|
|
121
|
+
seen.add(n);
|
|
122
|
+
next.push(n);
|
|
123
|
+
}
|
|
124
|
+
wave = next;
|
|
125
|
+
}
|
|
126
|
+
// Order is seed-first: a file anchored by a spec the request actually resembles is stronger
|
|
127
|
+
// evidence than one reached two hops away, and a flat set would throw that away.
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const id of [...seedSpecIds, ...seen]) {
|
|
130
|
+
for (const file of anchoredFiles.get(id) ?? [])
|
|
131
|
+
if (!out.includes(file))
|
|
132
|
+
out.push(file);
|
|
133
|
+
}
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const DEFAULT_SEM_TAUS: readonly number[];
|
|
2
|
+
export declare function semTauGrid(injected?: readonly number[]): readonly number[];
|
|
3
|
+
/** Keep the top `guard` unconditionally; past the guard, keep exactly what `accept` affirms. */
|
|
4
|
+
export declare function verifyEmission(files: readonly string[], accept: (f: string) => boolean, guard: number): string[];
|
|
5
|
+
/** Cosine-vs-query acceptor, inclusive at tau. Null query → no-op; null doc → keep. */
|
|
6
|
+
export declare function semAcceptor(queryVec: readonly number[] | null, docVecOf: (f: string) => readonly number[] | null, tau: number): (f: string) => boolean;
|
|
7
|
+
/**
|
|
8
|
+
* IDF-weighted token overlap, normalized by the subject's own mass (0..1). A token the IDF
|
|
9
|
+
* table has never seen weighs 1.0 — an unknown term is maximally distinctive, not weightless.
|
|
10
|
+
*/
|
|
11
|
+
export declare function lexPoverty(subjectTokens: ReadonlySet<string>, top1Tokens: ReadonlySet<string>, tau: number): boolean;
|
|
12
|
+
export declare function lexScore(subjectTokens: ReadonlySet<string>, fileTokens: ReadonlySet<string>, idf: ReadonlyMap<string, number>): number;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_SEM_TAUS = void 0;
|
|
4
|
+
exports.semTauGrid = semTauGrid;
|
|
5
|
+
exports.verifyEmission = verifyEmission;
|
|
6
|
+
exports.semAcceptor = semAcceptor;
|
|
7
|
+
exports.lexPoverty = lexPoverty;
|
|
8
|
+
exports.lexScore = lexScore;
|
|
9
|
+
// @implements A-SPEC-479
|
|
10
|
+
/**
|
|
11
|
+
* Pre-emission candidate verification — the pure half of the S-497 measurement arm.
|
|
12
|
+
*
|
|
13
|
+
* The L3 bottleneck is one number: union precision 0.167 against a modeled human ≈0.9±. S-488
|
|
14
|
+
* proved the ceiling is structural for rank-mass alone (tightening the cut saturates at head
|
|
15
|
+
* accuracy), so the next lever is an INDEPENDENT verdict per candidate before emission — the
|
|
16
|
+
* cognitive step the human model locates its precision in.
|
|
17
|
+
*
|
|
18
|
+
* Two contracts carry measured lessons:
|
|
19
|
+
* - No position budget inside the verdict (budgeted judgments flip with position). The only
|
|
20
|
+
* positional device is the explicit head guard, a separate axis of the sweep — the defense
|
|
21
|
+
* against the ranker-deletes-its-own-answer failure.
|
|
22
|
+
* - Verification unavailable is not rejection: a candidate without a vector is KEPT. Recall is
|
|
23
|
+
* spent only where the signal actually spoke.
|
|
24
|
+
*/
|
|
25
|
+
const dense_retrieval_1 = require("./dense-retrieval");
|
|
26
|
+
// @implements A-SPEC-484 — the sem τ grid is injectable because absolute cosines are a MODEL
|
|
27
|
+
// trait (bge-m3 masses below 0.5 where gemini's true matches sit at 0.73–0.78); the default is
|
|
28
|
+
// frozen so an uninjected run reproduces S-497 key-for-key.
|
|
29
|
+
exports.DEFAULT_SEM_TAUS = [0.4, 0.5, 0.6, 0.7];
|
|
30
|
+
function semTauGrid(injected) {
|
|
31
|
+
return injected ?? exports.DEFAULT_SEM_TAUS;
|
|
32
|
+
}
|
|
33
|
+
/** Keep the top `guard` unconditionally; past the guard, keep exactly what `accept` affirms. */
|
|
34
|
+
function verifyEmission(files, accept, guard) {
|
|
35
|
+
return files.filter((f, i) => i < guard || accept(f));
|
|
36
|
+
}
|
|
37
|
+
/** Cosine-vs-query acceptor, inclusive at tau. Null query → no-op; null doc → keep. */
|
|
38
|
+
function semAcceptor(queryVec, docVecOf, tau) {
|
|
39
|
+
return (f) => {
|
|
40
|
+
if (queryVec === null)
|
|
41
|
+
return true;
|
|
42
|
+
const dv = docVecOf(f);
|
|
43
|
+
if (dv === null)
|
|
44
|
+
return true;
|
|
45
|
+
return (0, dense_retrieval_1.cosine)(queryVec, dv) >= tau;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* IDF-weighted token overlap, normalized by the subject's own mass (0..1). A token the IDF
|
|
50
|
+
* table has never seen weighs 1.0 — an unknown term is maximally distinctive, not weightless.
|
|
51
|
+
*/
|
|
52
|
+
// @implements A-SPEC-488 — the lexical-poverty predicate for gate simulation: uniform-weight
|
|
53
|
+
// coverage of the subject's terms by the product's TOP-1 candidate, strictly below tau. Uniform
|
|
54
|
+
// (an empty IDF table) on purpose — an absolute threshold must not inherit corpus scale. An
|
|
55
|
+
// empty subject asked nothing lexically, so it is never "poor".
|
|
56
|
+
function lexPoverty(subjectTokens, top1Tokens, tau) {
|
|
57
|
+
if (subjectTokens.size === 0)
|
|
58
|
+
return false;
|
|
59
|
+
return lexScore(subjectTokens, top1Tokens, new Map()) < tau;
|
|
60
|
+
}
|
|
61
|
+
function lexScore(subjectTokens, fileTokens, idf) {
|
|
62
|
+
let hit = 0, total = 0;
|
|
63
|
+
for (const t of subjectTokens) {
|
|
64
|
+
const w = idf.get(t) ?? 1.0;
|
|
65
|
+
total += w;
|
|
66
|
+
if (fileTokens.has(t))
|
|
67
|
+
hit += w;
|
|
68
|
+
}
|
|
69
|
+
return total === 0 ? 0 : hit / total;
|
|
70
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { RtmGraph } from './rtm-graph';
|
|
2
|
+
import type { ScannedFile } from '../cpg/cpg-scanner';
|
|
3
|
+
/**
|
|
4
|
+
* Safe reuse of a persisted graph.
|
|
5
|
+
*
|
|
6
|
+
* Measured 2026-08-28 on this repository: the scan cache already makes a warm scan cost 37–46ms
|
|
7
|
+
* while building the graph costs 161ms, so **81% of the warm path is graph construction** and
|
|
8
|
+
* reopening a stored graph costs 0ms. Persistence is therefore worth real time — and the whole
|
|
9
|
+
* difficulty is not the caching, it is the INVALIDATION. A stale graph answering questions is
|
|
10
|
+
* exactly the failure Goal completion criterion 6 forbids: an incomplete or out-of-date graph must
|
|
11
|
+
* never be mistaken for "no impact".
|
|
12
|
+
*
|
|
13
|
+
* So reuse is allowed only when every field of the basis still matches, and a mismatch names the
|
|
14
|
+
* field that moved.
|
|
15
|
+
*/
|
|
16
|
+
export interface GraphBasis {
|
|
17
|
+
/** Bumped whenever the node/edge schema changes, so an old file is never read with new assumptions. */
|
|
18
|
+
graphSchema: string;
|
|
19
|
+
extractorVersion: string;
|
|
20
|
+
sourceCommit: string;
|
|
21
|
+
specFingerprint: string;
|
|
22
|
+
/**
|
|
23
|
+
* Digest of the scan the graph was built from. This is the field that actually protects the cache:
|
|
24
|
+
* an uncommitted edit does NOT move HEAD, so `sourceCommit` alone would happily reuse a graph
|
|
25
|
+
* describing content the working tree no longer has — the same trap A-SPEC-277 found one layer up
|
|
26
|
+
* when HEAD-only drift detection missed a modified target file.
|
|
27
|
+
*/
|
|
28
|
+
scanDigest: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Content address of a scan: every file's path and its symbols/edges, sorted so the digest depends
|
|
32
|
+
* on the scan's CONTENT and not on the order the scanner happened to walk the tree in.
|
|
33
|
+
*/
|
|
34
|
+
export declare function scanDigest(scanned: readonly ScannedFile[]): string;
|
|
35
|
+
export interface OpenedGraph {
|
|
36
|
+
graph: RtmGraph;
|
|
37
|
+
/** True only when the stored graph's basis matches in every field. */
|
|
38
|
+
reusable: boolean;
|
|
39
|
+
/** Why it could not be reused, naming the field that moved. Null when it was reused. */
|
|
40
|
+
reason: string | null;
|
|
41
|
+
/** Record the basis for this graph's CURRENT contents. Call after building. */
|
|
42
|
+
commitBasis(): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Open the graph at `dbPath`, reusing its contents only if `expected` still holds.
|
|
46
|
+
*
|
|
47
|
+
* Never throws for a bad cache. An unreadable or corrupt file is a reason to REBUILD, not a reason
|
|
48
|
+
* to fail: the caller always receives a usable graph, empty when the stored one could not be
|
|
49
|
+
* trusted, so "the cache is broken" degrades to "this call is slower" rather than to an outage.
|
|
50
|
+
*/
|
|
51
|
+
export declare function openReusableGraph(dbPath: string, expected: GraphBasis): OpenedGraph;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.scanDigest = scanDigest;
|
|
37
|
+
exports.openReusableGraph = openReusableGraph;
|
|
38
|
+
// @implements A-SPEC-282
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const node_crypto_1 = require("node:crypto");
|
|
42
|
+
const rtm_graph_1 = require("./rtm-graph");
|
|
43
|
+
const BASIS_FIELDS = [
|
|
44
|
+
'graphSchema', 'extractorVersion', 'sourceCommit', 'specFingerprint', 'scanDigest',
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Content address of a scan: every file's path and its symbols/edges, sorted so the digest depends
|
|
48
|
+
* on the scan's CONTENT and not on the order the scanner happened to walk the tree in.
|
|
49
|
+
*/
|
|
50
|
+
function scanDigest(scanned) {
|
|
51
|
+
const lines = scanned
|
|
52
|
+
.map((f) => {
|
|
53
|
+
const symbols = f.symbols.map((s) => `${s.qualifiedName}:${s.startLine}-${s.endLine}`).sort().join(',');
|
|
54
|
+
const edges = (f.edges ?? []).map((e) => `${e.from}->${e.to}:${e.rel}`).sort().join(',');
|
|
55
|
+
const implemented = [...(f.implementsSpecs ?? [])].sort().join(',');
|
|
56
|
+
return `${f.sourcePath}|${implemented}|${symbols}|${edges}`;
|
|
57
|
+
})
|
|
58
|
+
.sort();
|
|
59
|
+
return `sha256:${(0, node_crypto_1.createHash)('sha256').update(lines.join('\n')).digest('hex')}`;
|
|
60
|
+
}
|
|
61
|
+
function readStoredBasis(graph) {
|
|
62
|
+
try {
|
|
63
|
+
const rows = graph.readMeta();
|
|
64
|
+
return Object.keys(rows).length > 0 ? rows : null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Open the graph at `dbPath`, reusing its contents only if `expected` still holds.
|
|
72
|
+
*
|
|
73
|
+
* Never throws for a bad cache. An unreadable or corrupt file is a reason to REBUILD, not a reason
|
|
74
|
+
* to fail: the caller always receives a usable graph, empty when the stored one could not be
|
|
75
|
+
* trusted, so "the cache is broken" degrades to "this call is slower" rather than to an outage.
|
|
76
|
+
*/
|
|
77
|
+
function openReusableGraph(dbPath, expected) {
|
|
78
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
79
|
+
let graph;
|
|
80
|
+
let reason = null;
|
|
81
|
+
try {
|
|
82
|
+
graph = new rtm_graph_1.RtmGraph(dbPath);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Corrupt beyond opening: replace the file outright and carry on with an empty graph.
|
|
86
|
+
try {
|
|
87
|
+
fs.rmSync(dbPath, { force: true });
|
|
88
|
+
}
|
|
89
|
+
catch { /* best effort */ }
|
|
90
|
+
graph = new rtm_graph_1.RtmGraph(dbPath);
|
|
91
|
+
reason = 'stored graph was unreadable (corrupt file replaced)';
|
|
92
|
+
}
|
|
93
|
+
if (!reason) {
|
|
94
|
+
let stored;
|
|
95
|
+
try {
|
|
96
|
+
stored = readStoredBasis(graph);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
stored = null;
|
|
100
|
+
reason = 'stored graph was unreadable (corrupt metadata)';
|
|
101
|
+
}
|
|
102
|
+
if (!reason) {
|
|
103
|
+
if (!stored)
|
|
104
|
+
reason = 'no stored basis — nothing to reuse';
|
|
105
|
+
else {
|
|
106
|
+
const moved = BASIS_FIELDS.find((field) => stored[field] !== expected[field]);
|
|
107
|
+
if (moved)
|
|
108
|
+
reason = `${moved} changed since the stored graph was built`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const commitBasis = () => {
|
|
113
|
+
graph.writeMeta(Object.fromEntries(BASIS_FIELDS.map((f) => [f, expected[f]])));
|
|
114
|
+
};
|
|
115
|
+
if (reason) {
|
|
116
|
+
// Not reusable means EMPTY. Handing back a partially-stale graph would be worse than no cache:
|
|
117
|
+
// the caller would build on top of facts whose basis it has just been told it cannot trust.
|
|
118
|
+
graph.clear();
|
|
119
|
+
return { graph, reusable: false, reason, commitBasis };
|
|
120
|
+
}
|
|
121
|
+
return { graph, reusable: true, reason: null, commitBasis };
|
|
122
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RtmGraph } from './rtm-graph';
|
|
2
|
+
import { type BuildRtmOptions } from './rtm-builder';
|
|
2
3
|
import { Spec } from '../spec/spec-parser';
|
|
3
4
|
import { ScannedFile } from '../cpg/cpg-scanner';
|
|
4
5
|
/** Paths are repo-relative POSIX paths. */
|
|
@@ -25,6 +26,18 @@ export interface ChangeSet {
|
|
|
25
26
|
* operation is idempotent: re-applying the same ChangeSet has no net effect
|
|
26
27
|
* beyond the first application.
|
|
27
28
|
*
|
|
29
|
+
* CALL EDGES (A-SPEC-280). Measured 2026-08-28: without `allScanned` this function produced a graph
|
|
30
|
+
* that did NOT converge with a full rebuild — `buildFileSubgraph` emits nodes and `implements` edges
|
|
31
|
+
* only, so every call edge was simply missing, and `rtm_reindex` shipped that. Worse, call
|
|
32
|
+
* resolution is GLOBAL — a name resolves only when exactly one file across the whole scan defines it
|
|
33
|
+
* — so adding a file can invalidate a call edge owned by a file that did not change, and deleting
|
|
34
|
+
* one can make a previously-ambiguous call resolvable. No file-local update can see either.
|
|
35
|
+
*
|
|
36
|
+
* So when `allScanned` is supplied the call relation is cleared and re-resolved over the whole scan.
|
|
37
|
+
* That is honest about the cost: incremental saves the PARSE, not the call resolution. When it is
|
|
38
|
+
* not supplied the returned `callEdgesResolved: false` says so, because a caller that silently
|
|
39
|
+
* receives a graph missing a whole edge kind is exactly the failure this slice was written to end.
|
|
40
|
+
*
|
|
28
41
|
* SPEC-file changes (paths under .ax/specs/) are out of scope here: scanOne
|
|
29
42
|
* returns null for non-code files, so only their removeBySource side (a
|
|
30
43
|
* no-op, since SPEC nodes are keyed on the spec's own sourcePath — see
|
|
@@ -35,4 +48,15 @@ export declare function applyIncremental(graph: RtmGraph, changes: ChangeSet, ct
|
|
|
35
48
|
repoRoot: string;
|
|
36
49
|
specs: Spec[];
|
|
37
50
|
scanOne: (relPath: string) => ScannedFile | null;
|
|
38
|
-
|
|
51
|
+
/** Every file in the current scan. Required for the graph to converge with a full rebuild. */
|
|
52
|
+
allScanned?: () => ScannedFile[];
|
|
53
|
+
/**
|
|
54
|
+
* @implements A-SPEC-281
|
|
55
|
+
* The SAME build options the full build would use. Provenance is part of the graph's value, so
|
|
56
|
+
* an incremental update that omits it diverges from a full rebuild in exactly the way
|
|
57
|
+
* A-SPEC-280's missing call edges did — invisibly, until something compares whole values.
|
|
58
|
+
*/
|
|
59
|
+
buildOptions?: BuildRtmOptions;
|
|
60
|
+
}): {
|
|
61
|
+
callEdgesResolved: boolean;
|
|
62
|
+
};
|
|
@@ -16,6 +16,18 @@ const rtm_builder_1 = require("./rtm-builder");
|
|
|
16
16
|
* operation is idempotent: re-applying the same ChangeSet has no net effect
|
|
17
17
|
* beyond the first application.
|
|
18
18
|
*
|
|
19
|
+
* CALL EDGES (A-SPEC-280). Measured 2026-08-28: without `allScanned` this function produced a graph
|
|
20
|
+
* that did NOT converge with a full rebuild — `buildFileSubgraph` emits nodes and `implements` edges
|
|
21
|
+
* only, so every call edge was simply missing, and `rtm_reindex` shipped that. Worse, call
|
|
22
|
+
* resolution is GLOBAL — a name resolves only when exactly one file across the whole scan defines it
|
|
23
|
+
* — so adding a file can invalidate a call edge owned by a file that did not change, and deleting
|
|
24
|
+
* one can make a previously-ambiguous call resolvable. No file-local update can see either.
|
|
25
|
+
*
|
|
26
|
+
* So when `allScanned` is supplied the call relation is cleared and re-resolved over the whole scan.
|
|
27
|
+
* That is honest about the cost: incremental saves the PARSE, not the call resolution. When it is
|
|
28
|
+
* not supplied the returned `callEdgesResolved: false` says so, because a caller that silently
|
|
29
|
+
* receives a graph missing a whole edge kind is exactly the failure this slice was written to end.
|
|
30
|
+
*
|
|
19
31
|
* SPEC-file changes (paths under .ax/specs/) are out of scope here: scanOne
|
|
20
32
|
* returns null for non-code files, so only their removeBySource side (a
|
|
21
33
|
* no-op, since SPEC nodes are keyed on the spec's own sourcePath — see
|
|
@@ -27,7 +39,7 @@ function applyIncremental(graph, changes, ctx) {
|
|
|
27
39
|
graph.removeBySource(f);
|
|
28
40
|
const sf = ctx.scanOne(f);
|
|
29
41
|
if (sf)
|
|
30
|
-
(0, rtm_builder_1.buildFileSubgraph)(sf, ctx.specs, graph);
|
|
42
|
+
(0, rtm_builder_1.buildFileSubgraph)(sf, ctx.specs, graph, ctx.buildOptions);
|
|
31
43
|
};
|
|
32
44
|
for (const f of changes.deleted) {
|
|
33
45
|
graph.removeBySource(f);
|
|
@@ -42,4 +54,9 @@ function applyIncremental(graph, changes, ctx) {
|
|
|
42
54
|
for (const f of changes.modified) {
|
|
43
55
|
replaceFile(f);
|
|
44
56
|
}
|
|
57
|
+
if (!ctx.allScanned)
|
|
58
|
+
return { callEdgesResolved: false };
|
|
59
|
+
graph.removeEdgesByRel('calls');
|
|
60
|
+
(0, rtm_builder_1.addCallEdges)(ctx.allScanned(), graph, ctx.buildOptions);
|
|
61
|
+
return { callEdgesResolved: true };
|
|
45
62
|
}
|
|
@@ -25,7 +25,17 @@ export interface LocalizationReport {
|
|
|
25
25
|
terms: string[];
|
|
26
26
|
hits: LocalizationHit[];
|
|
27
27
|
matchedSpecs: string[];
|
|
28
|
+
/**
|
|
29
|
+
* @implements A-SPEC-273
|
|
30
|
+
* Spec ids the request cited that the store does NOT contain. Reported rather than dropped: a
|
|
31
|
+
* request citing a spec nobody can find is a fact about the request worth surfacing.
|
|
32
|
+
*/
|
|
33
|
+
unknownCitations: string[];
|
|
28
34
|
}
|
|
35
|
+
export declare function citationsIn(text: string, known: ReadonlySet<string>): {
|
|
36
|
+
cited: string[];
|
|
37
|
+
unknown: string[];
|
|
38
|
+
};
|
|
29
39
|
/**
|
|
30
40
|
* Significant tokens of the text, UNICODE-aware (review D2: the old [^a-z0-9] split dropped every
|
|
31
41
|
* non-ASCII codepoint, so Korean issue text — and this repo's Korean spec titles — tokenized to
|
|
@@ -33,4 +43,22 @@ export interface LocalizationReport {
|
|
|
33
43
|
* camelCase/snake are split; Latin tokens need length>=3, non-ASCII (e.g. Korean) length>=2.
|
|
34
44
|
*/
|
|
35
45
|
export declare function significantTerms(text: string): string[];
|
|
46
|
+
/**
|
|
47
|
+
* @implements A-SPEC-400
|
|
48
|
+
* Character bigrams for scripts that do not space their words.
|
|
49
|
+
*
|
|
50
|
+
* Measured before this existed: every script that separates words with spaces tokenized correctly —
|
|
51
|
+
* Korean 4 tokens, German 7, Spanish 6, Russian 6, Arabic 6, French 5 — while Japanese and Chinese
|
|
52
|
+
* produced ONE token, the entire sentence, because splitting on non-letters found nothing to split.
|
|
53
|
+
* A single token matches nothing, so those requests reached no code at all. That is a missing
|
|
54
|
+
* capability for two major languages rather than a ranking shortfall.
|
|
55
|
+
*
|
|
56
|
+
* Bigrams are the standard answer for unsegmented scripts and need no dictionary: `隔離処理` yields
|
|
57
|
+
* `隔離`, `離処`, `処理`, one of which will overlap a spec that discusses the same subject.
|
|
58
|
+
*
|
|
59
|
+
* Scoped by SCRIPT, not by "is it non-ASCII". Korean spaces its words and already tokenized into
|
|
60
|
+
* meaningful units; bigramming it would shred those into fragments and make six working languages pay
|
|
61
|
+
* for two broken ones. Han, Hiragana and Katakana only.
|
|
62
|
+
*/
|
|
63
|
+
export declare function bigramUnspaced(token: string): string[];
|
|
36
64
|
export declare function localizeIssue(issueText: string, scanned: ScannedFile[], specs: Spec[], topN?: number): LocalizationReport;
|