@agentskit/doc-bridge 1.7.45 → 1.10.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 +488 -0
- package/CONTRIBUTING.md +23 -0
- package/README.md +87 -40
- package/SECURITY.md +7 -0
- package/action.yml +1 -1
- package/bin/ak-docs.js +2 -2
- package/bin/ak-verify.js +13 -7
- package/dist/cli/program.d.ts +3 -1
- package/dist/cli/program.js +15871 -6218
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +95 -6
- package/dist/config/index.js.map +1 -1
- package/dist/{index-BUL0q7s8.d.ts → index-7wYGbllW.d.ts} +134 -1
- package/dist/index.d.ts +9880 -1062
- package/dist/index.js +16143 -6127
- package/dist/index.js.map +1 -1
- package/docs/MARKETPLACE.md +1 -1
- package/docs/PRD-documentation-efficiency-study.md +406 -0
- package/docs/PRD-knowledge-retrieval-and-enrichment.md +466 -0
- package/docs/adr/0002-documentation-audit-boundary.md +22 -0
- package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
- package/docs/adr/0004-controlled-study-runner.md +25 -0
- package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
- package/docs/adr/0006-registry-semantic-grounding.md +20 -0
- package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
- package/docs/adr/0008-study-verification-boundary.md +21 -0
- package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
- package/docs/agent-corpus/INDEX.md +12 -1
- package/docs/agent-corpus/OVERVIEW.md +25 -0
- package/docs/agent-corpus/chat.md +5 -1
- package/docs/agent-corpus/cli.md +16 -0
- package/docs/agent-corpus/conformance.md +12 -0
- package/docs/agent-corpus/doc-bridge.md +47 -0
- package/docs/agent-corpus/doctor.md +8 -0
- package/docs/agent-corpus/gates.md +4 -0
- package/docs/agent-corpus/mcp.md +13 -0
- package/docs/agent-corpus/memory.md +4 -0
- package/docs/agent-corpus/query.md +33 -0
- package/docs/bench/README.md +122 -0
- package/docs/bench/retrieval-baseline-v1.json +28 -0
- package/docs/bench/retrieval-suite-v1.json +1033 -0
- package/docs/chat-and-rag.md +3 -2
- package/docs/for-agents.md +9 -1
- package/docs/getting-started.md +4 -11
- package/docs/guides/gate-ci.md +11 -1
- package/docs/guides/install-and-run.md +9 -65
- package/docs/index.md +22 -1
- package/docs/knowledge-engine-runbook.md +38 -3
- package/docs/landing/assets/context-payload-reduction.svg +21 -0
- package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
- package/docs/landing/index.html +119 -5
- package/docs/loop-workflow.md +117 -0
- package/docs/mcp.md +6 -1
- package/docs/parity/public-claims-v1.json +145 -0
- package/docs/playbook/doc-bridge-pattern.md +1 -1
- package/docs/query.md +90 -2
- package/docs/recipes/index-pipeline.md +1 -1
- package/docs/schemas/agent-handoff-v1.md +15 -0
- package/docs/schemas/doc-bridge-index-v1.md +65 -0
- package/docs/spec/benchmark-v1.md +33 -1
- package/docs/spec/cli.md +30 -10
- package/docs/spec/config-v1.md +157 -8
- package/docs/spec/documentation-audit-v1.md +61 -0
- package/docs/spec/enrichment-overlay-v1.md +241 -0
- package/docs/spec/graph-signals-v1.md +92 -0
- package/docs/spec/incremental-scan-v1.md +102 -0
- package/docs/spec/markdown-analyzer-v1.md +73 -0
- package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
- package/docs/spec/measured-enrichment-v1.md +229 -0
- package/docs/spec/public-parity-v1.md +119 -0
- package/docs/spec/registry-agents.md +6 -0
- package/docs/spec/render-v1.md +122 -0
- package/docs/spec/retrieval-index-v1.md +164 -0
- package/docs/spec/study-metrics-v1.md +58 -0
- package/docs/spec/study-protocol-v1.md +46 -0
- package/docs/spec/study-provider-cli-v1.md +116 -0
- package/docs/spec/study-runner-v1.md +35 -0
- package/docs/spec/study-task-suite-v1.md +41 -0
- package/docs/spec/study-verification-v1.md +40 -0
- package/docs/study/README.md +84 -0
- package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
- package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
- package/docs/study/ab-baseline-analysis-v1.md +21 -0
- package/docs/study/ab-baseline-plan-v1.json +33 -0
- package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
- package/docs/study/ab-baseline-result-v1.json +79 -0
- package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
- package/docs/study/historical-evidence-v1.json +252 -0
- package/docs/study/observation-ledger-v1.json +30632 -0
- package/docs/study/phase3-task-coverage-v1.json +34 -0
- package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
- package/docs/study/phase4-public-pilot-result-v1.json +52 -0
- package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
- package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
- package/docs/study/pilot-round-2026-08-31.json +46 -0
- package/docs/study/protocol-v1.json +90 -0
- package/docs/study/publication-gate-v1.md +45 -0
- package/docs/study/quality-scorecard-cycle-plan.md +545 -0
- package/docs/study/quality-scorecard-v1.json +38 -0
- package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
- package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
- package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
- package/docs/study/round-2-expanded-validation-v1.md +58 -0
- package/docs/study/round-3-evidence-contract-v1.json +75 -0
- package/docs/study/round-3-evidence-contract-v1.md +57 -0
- package/docs/study/round-4-confirmation-v1.json +75 -0
- package/docs/study/round-4-confirmation-v1.md +55 -0
- package/docs/study/run-plan-v1.json +33 -0
- package/docs/study/semantic-adjudication-cycle-8.md +20 -0
- package/docs/study/task-suite-v1.json +96 -0
- package/docs/study/token-efficiency-plan-v1.md +337 -0
- package/docs/study/token-efficiency-protocol-v2.json +62 -0
- package/docs/study/verification-binding-v1.json +27 -0
- package/docs/validation-cycle-plan.md +14 -0
- package/docs/verification-harness.md +11 -6
- package/ecosystem-claims.json +2 -2
- package/ecosystem-upstream.json +2 -2
- package/ecosystem.json +4 -4
- package/mcpb/manifest.json +9 -1
- package/package.json +34 -15
- package/scripts/check-ecosystem-upstream.mjs +34 -6
- package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +192 -24
- package/src/audit/documentation.ts +513 -0
- package/src/bench/baseline.ts +198 -0
- package/src/bench/overlay-delta.ts +139 -0
- package/src/bench/retrieval.ts +319 -0
- package/src/budget/compile.ts +91 -0
- package/src/budget/sections.ts +70 -0
- package/src/cli/program.ts +684 -74
- package/src/cli/usage.ts +71 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/config/schema.ts +126 -2
- package/src/conformance/documentation-standard-v1.ts +8 -6
- package/src/discovery/areas.ts +197 -0
- package/src/discovery/documentation.ts +211 -5
- package/src/discovery/identity.ts +24 -0
- package/src/discovery/incremental.ts +314 -0
- package/src/discovery/inputs.ts +110 -0
- package/src/discovery/markdown.ts +481 -0
- package/src/discovery/repository.ts +485 -101
- package/src/doctor/run-doctor.ts +244 -12
- package/src/enrich/approvals.ts +190 -0
- package/src/enrich/cache.ts +93 -0
- package/src/enrich/context-pack.ts +272 -0
- package/src/enrich/overlay.ts +255 -0
- package/src/enrich/review.ts +106 -0
- package/src/enrich/stage.ts +374 -0
- package/src/enrich/stats.ts +100 -0
- package/src/enrich/validate.ts +410 -0
- package/src/federation/llms.ts +2 -4
- package/src/findings/report.ts +103 -0
- package/src/graph/build.ts +356 -0
- package/src/graph/memory.ts +208 -0
- package/src/index-builder/build-handoffs.ts +22 -11
- package/src/index-builder/build-index.ts +132 -3
- package/src/index-builder/llms-txt.ts +48 -8
- package/src/index-builder/project-corpus.ts +111 -0
- package/src/index.ts +630 -2
- package/src/lib/fuzzy-match.ts +235 -0
- package/src/mcp/knowledge.ts +554 -0
- package/src/mcp/server.ts +113 -18
- package/src/metrics/benchmark.ts +21 -0
- package/src/parity/check.ts +309 -0
- package/src/parity/claims.ts +259 -0
- package/src/parity/resolve.ts +160 -0
- package/src/query/handoff.ts +326 -0
- package/src/query/load-index.ts +53 -1
- package/src/query/query.ts +92 -59
- package/src/query/search.ts +289 -92
- package/src/query/text.ts +155 -0
- package/src/reconciliation/reconcile.ts +101 -10
- package/src/render/data.ts +356 -0
- package/src/render/engine.ts +398 -0
- package/src/render/generated.ts +77 -0
- package/src/render/render.ts +209 -0
- package/src/render/template-source.ts +52 -0
- package/src/render/templates.ts +289 -0
- package/src/report/html.ts +2 -2
- package/src/retrieval/bm25.ts +161 -0
- package/src/retrieval/project.ts +495 -0
- package/src/retrieval/rank.ts +383 -0
- package/src/retrieval/weights.ts +39 -0
- package/src/retriever/doc-bridge-retriever.ts +100 -15
- package/src/rules/engine.ts +30 -10
- package/src/schemas/agent-handoff.ts +56 -0
- package/src/schemas/budget.ts +37 -0
- package/src/schemas/doc-bridge-index.ts +53 -2
- package/src/schemas/enrichment.ts +369 -0
- package/src/schemas/json-schemas.ts +39 -2
- package/src/schemas/knowledge.ts +14 -1
- package/src/schemas/retrieval-index.ts +152 -0
- package/src/shims/graphology.d.ts +91 -0
- package/src/study/adjudication.ts +196 -0
- package/src/study/execution.ts +350 -0
- package/src/study/expectations.ts +219 -0
- package/src/study/metrics.ts +467 -0
- package/src/study/protocol.ts +271 -0
- package/src/study/provider-cli.ts +115 -0
- package/src/study/provider-telemetry.ts +47 -0
- package/src/study/quality-scorecard.ts +164 -0
- package/src/study/runner.ts +461 -0
- package/src/study/task-suite.ts +321 -0
- package/src/study/verification.ts +134 -0
- package/src/validate.ts +1 -4
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +36 -11
- package/scripts/verification-harness.mjs +0 -482
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jaro-Winkler similarity, mirroring `fuzzyMatchList` from `@agentskit/core/fuzzy-match`.
|
|
3
|
+
*
|
|
4
|
+
* The ecosystem owns this contract; it is mirrored rather than imported for the same reason the
|
|
5
|
+
* eval format is: `@agentskit/core` is an optional peer, loaded dynamically, and discovery is
|
|
6
|
+
* synchronous — an analyzer that resolved a reference only when an optional package happened to
|
|
7
|
+
* be installed would make the snapshot depend on the installation. A test cross-checks these
|
|
8
|
+
* functions against the real ones when the peer is present, so the mirror cannot drift.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type FuzzyMatch = {
|
|
12
|
+
readonly candidate: string
|
|
13
|
+
readonly score: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const normalize = (value: string, caseSensitive: boolean): string =>
|
|
17
|
+
(caseSensitive ? value : value.toLowerCase()).trim().replace(/\s+/g, ' ')
|
|
18
|
+
|
|
19
|
+
/** Jaro similarity: matching characters within a sliding window, minus half the transpositions. */
|
|
20
|
+
export const jaro = (a: string, b: string): number => {
|
|
21
|
+
if (a === b) return 1
|
|
22
|
+
if (a.length === 0 || b.length === 0) return 0
|
|
23
|
+
|
|
24
|
+
const window = Math.max(0, Math.floor(Math.max(a.length, b.length) / 2) - 1)
|
|
25
|
+
const matchedA = new Array<boolean>(a.length).fill(false)
|
|
26
|
+
const matchedB = new Array<boolean>(b.length).fill(false)
|
|
27
|
+
let matches = 0
|
|
28
|
+
|
|
29
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
30
|
+
const from = Math.max(0, index - window)
|
|
31
|
+
const to = Math.min(index + window + 1, b.length)
|
|
32
|
+
for (let candidate = from; candidate < to; candidate += 1) {
|
|
33
|
+
if (matchedB[candidate] || a[index] !== b[candidate]) continue
|
|
34
|
+
matchedA[index] = true
|
|
35
|
+
matchedB[candidate] = true
|
|
36
|
+
matches += 1
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (matches === 0) return 0
|
|
41
|
+
|
|
42
|
+
let transpositions = 0
|
|
43
|
+
let position = 0
|
|
44
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
45
|
+
if (!matchedA[index]) continue
|
|
46
|
+
while (!matchedB[position]) position += 1
|
|
47
|
+
if (a[index] !== b[position]) transpositions += 1
|
|
48
|
+
position += 1
|
|
49
|
+
}
|
|
50
|
+
transpositions /= 2
|
|
51
|
+
|
|
52
|
+
return (matches / a.length + matches / b.length + (matches - transpositions) / matches) / 3
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Jaro, plus a bonus for up to four shared leading characters. Case-insensitive by default. */
|
|
56
|
+
export const jaroWinkler = (a: string, b: string, options: { readonly caseSensitive?: boolean } = {}): number => {
|
|
57
|
+
const caseSensitive = options.caseSensitive ?? false
|
|
58
|
+
const left = normalize(a, caseSensitive)
|
|
59
|
+
const right = normalize(b, caseSensitive)
|
|
60
|
+
const similarity = jaro(left, right)
|
|
61
|
+
if (similarity === 0) return 0
|
|
62
|
+
|
|
63
|
+
let prefix = 0
|
|
64
|
+
for (let index = 0; index < Math.min(4, left.length, right.length) && left[index] === right[index]; index += 1) {
|
|
65
|
+
prefix += 1
|
|
66
|
+
}
|
|
67
|
+
return similarity + prefix * 0.1 * (1 - similarity)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The lengths a candidate can have and still reach a threshold.
|
|
72
|
+
*
|
|
73
|
+
* Jaro-Winkler is bounded by the lengths of the two strings, so most candidates can be discarded
|
|
74
|
+
* without computing anything. With `m` matching characters, `jaro ≤ (m/|a| + m/|b| + 1) / 3`, and
|
|
75
|
+
* the prefix bonus can only raise the result: `jw ≤ 0.6·jaro + 0.4`. Requiring `jw ≥ t` therefore
|
|
76
|
+
* requires `jaro ≥ (t − 0.4) / 0.6`, hence `m/|a| + m/|b| ≥ 3·that − 1 = R`. Since `m` cannot
|
|
77
|
+
* exceed the shorter string, a candidate longer than the query needs `|b| ≤ |a| / (R − 1)` and a
|
|
78
|
+
* shorter one needs `|b| ≥ |a| · (R − 1)`.
|
|
79
|
+
*
|
|
80
|
+
* At the resolution threshold of 0.92 that is a window of 0.6× to 1.67× the query's length. When
|
|
81
|
+
* `R ≤ 1` the inequality constrains nothing and every length stays a candidate, so a low threshold
|
|
82
|
+
* still scans everything — as it must.
|
|
83
|
+
*/
|
|
84
|
+
export const fuzzyLengthWindow = (queryLength: number, threshold: number): { readonly min: number; readonly max: number } => {
|
|
85
|
+
const jaroFloor = Math.max(0, (threshold - 0.4) / 0.6)
|
|
86
|
+
const ratio = 3 * jaroFloor - 1
|
|
87
|
+
if (ratio <= 1) return { min: 0, max: Number.POSITIVE_INFINITY }
|
|
88
|
+
return { min: queryLength * (ratio - 1), max: queryLength / (ratio - 1) }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Candidates grouped by length, so a scan only visits the lengths that can pass.
|
|
93
|
+
*
|
|
94
|
+
* Built once per universe and reused by every query. Without it, resolving references across a
|
|
95
|
+
* repository is quadratic: each document rebuilt the candidate list and ran the full similarity
|
|
96
|
+
* scan per reference, which on a four-thousand-document repository did not finish.
|
|
97
|
+
*/
|
|
98
|
+
type IndexedCandidate = {
|
|
99
|
+
readonly value: string
|
|
100
|
+
/** Length after normalization, which is what the similarity compares. */
|
|
101
|
+
readonly length: number
|
|
102
|
+
readonly counts: Uint16Array
|
|
103
|
+
/** Position in the universe the index was built from, so a filtered scan stays in the caller's order. */
|
|
104
|
+
readonly order: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type FuzzyCandidateIndex = {
|
|
108
|
+
readonly byLength: ReadonlyMap<number, readonly IndexedCandidate[]>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Character counts over a fixed alphabet slot per code unit.
|
|
113
|
+
*
|
|
114
|
+
* Every code unit at or above the alphabet shares the last slot, which can only over-count the
|
|
115
|
+
* characters two strings share — the safe direction for a bound that decides what to skip.
|
|
116
|
+
*/
|
|
117
|
+
const ALPHABET = 128
|
|
118
|
+
const countsOf = (value: string): Uint16Array => {
|
|
119
|
+
const counts = new Uint16Array(ALPHABET)
|
|
120
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
121
|
+
const code = value.charCodeAt(index)
|
|
122
|
+
const slot = code < ALPHABET ? code : ALPHABET - 1
|
|
123
|
+
counts[slot] = (counts[slot] ?? 0) + 1
|
|
124
|
+
}
|
|
125
|
+
return counts
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The form the bounds are computed over.
|
|
130
|
+
*
|
|
131
|
+
* `jaroWinkler` compares normalized strings, so the index has to measure normalized ones: a
|
|
132
|
+
* candidate padded with whitespace is sixteen characters on disk and ten to the similarity, and a
|
|
133
|
+
* window built on the wrong number would skip a candidate that matches. Case is folded even for a
|
|
134
|
+
* case-sensitive query — folding can only raise the shared-character count, and the bound is
|
|
135
|
+
* allowed to over-estimate, never to under-estimate.
|
|
136
|
+
*/
|
|
137
|
+
const indexedForm = (value: string): string => normalize(value, false)
|
|
138
|
+
|
|
139
|
+
export const createFuzzyCandidateIndex = (candidates: readonly string[]): FuzzyCandidateIndex => {
|
|
140
|
+
const byLength = new Map<number, IndexedCandidate[]>()
|
|
141
|
+
for (const [order, value] of candidates.entries()) {
|
|
142
|
+
const form = indexedForm(value)
|
|
143
|
+
const entry: IndexedCandidate = { value, length: form.length, counts: countsOf(form), order }
|
|
144
|
+
const bucket = byLength.get(entry.length)
|
|
145
|
+
if (bucket) bucket.push(entry)
|
|
146
|
+
else byLength.set(entry.length, [entry])
|
|
147
|
+
}
|
|
148
|
+
return { byLength }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The characters two strings share, as an upper bound on Jaro's `m`.
|
|
153
|
+
*
|
|
154
|
+
* Jaro counts a character as matching only if it appears in both strings (within a window), so `m`
|
|
155
|
+
* can never exceed the multiset intersection. Comparing two count vectors costs one pass over the
|
|
156
|
+
* alphabet, where computing Jaro costs a pass over one string per character of the other — so this
|
|
157
|
+
* is the cheap half of the test, and it is exact in the direction that matters: it only ever
|
|
158
|
+
* over-estimates `m`, and therefore only ever keeps a candidate that might have passed.
|
|
159
|
+
*/
|
|
160
|
+
const sharedCharacterBound = (a: Uint16Array, b: Uint16Array): number => {
|
|
161
|
+
let shared = 0
|
|
162
|
+
for (let slot = 0; slot < ALPHABET; slot += 1) {
|
|
163
|
+
const left = a[slot] ?? 0
|
|
164
|
+
if (left === 0) continue
|
|
165
|
+
const right = b[slot] ?? 0
|
|
166
|
+
shared += left < right ? left : right
|
|
167
|
+
}
|
|
168
|
+
return shared
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const candidatesWithin = (
|
|
172
|
+
index: FuzzyCandidateIndex,
|
|
173
|
+
query: string,
|
|
174
|
+
threshold: number,
|
|
175
|
+
): readonly string[] => {
|
|
176
|
+
const form = indexedForm(query)
|
|
177
|
+
const { min, max } = fuzzyLengthWindow(form.length, threshold)
|
|
178
|
+
const jaroFloor = Math.max(0, (threshold - 0.4) / 0.6)
|
|
179
|
+
const required = 3 * jaroFloor - 1
|
|
180
|
+
const queryCounts = countsOf(form)
|
|
181
|
+
const within: IndexedCandidate[] = []
|
|
182
|
+
for (const [length, bucket] of index.byLength) {
|
|
183
|
+
if (length < min || length > max) continue
|
|
184
|
+
for (const candidate of bucket) {
|
|
185
|
+
if (required > 1) {
|
|
186
|
+
const bound = sharedCharacterBound(queryCounts, candidate.counts)
|
|
187
|
+
if (bound / form.length + bound / candidate.length < required) continue
|
|
188
|
+
}
|
|
189
|
+
within.push(candidate)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/*
|
|
193
|
+
* Back into the universe's own order. Scores that tie are ordered by the scan that found them, so
|
|
194
|
+
* an index that visits candidates grouped by length would otherwise rank equal matches differently
|
|
195
|
+
* from a plain list — the filter is meant to be invisible, and a test asserts the two agree.
|
|
196
|
+
*/
|
|
197
|
+
return within.sort((left, right) => left.order - right.order).map((candidate) => candidate.value)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const fuzzyMatchList = (
|
|
201
|
+
query: string,
|
|
202
|
+
candidates: readonly string[] | FuzzyCandidateIndex,
|
|
203
|
+
options: { readonly threshold?: number; readonly topK?: number; readonly caseSensitive?: boolean } = {},
|
|
204
|
+
): FuzzyMatch[] => {
|
|
205
|
+
const threshold = options.threshold ?? 0.85
|
|
206
|
+
const topK = options.topK ?? 10
|
|
207
|
+
const searched = Array.isArray(candidates)
|
|
208
|
+
? candidates
|
|
209
|
+
: candidatesWithin(candidates as FuzzyCandidateIndex, query, threshold)
|
|
210
|
+
const matches: FuzzyMatch[] = []
|
|
211
|
+
for (const candidate of searched) {
|
|
212
|
+
const score = jaroWinkler(query, candidate, { caseSensitive: options.caseSensitive ?? false })
|
|
213
|
+
if (score >= threshold) matches.push({ candidate, score })
|
|
214
|
+
}
|
|
215
|
+
return matches.sort((left, right) => right.score - left.score).slice(0, topK)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Resolve a reference only when the evidence is unambiguous.
|
|
220
|
+
*
|
|
221
|
+
* A near-miss is a guess, and a guess in a knowledge graph is worse than a gap: it sends an agent
|
|
222
|
+
* to the wrong file with the same confidence as a fact. So a fuzzy resolution requires a high
|
|
223
|
+
* score *and* a single candidate at that score — two plausible targets mean the reference stays
|
|
224
|
+
* unresolved and is reported instead.
|
|
225
|
+
*/
|
|
226
|
+
export const FUZZY_RESOLUTION_THRESHOLD = 0.92
|
|
227
|
+
|
|
228
|
+
export const resolveFuzzyReference = (
|
|
229
|
+
query: string,
|
|
230
|
+
candidates: readonly string[] | FuzzyCandidateIndex,
|
|
231
|
+
threshold = FUZZY_RESOLUTION_THRESHOLD,
|
|
232
|
+
): { readonly candidate: string; readonly score: number } | undefined => {
|
|
233
|
+
const matches = fuzzyMatchList(query, candidates, { threshold, topK: 2 })
|
|
234
|
+
return matches.length === 1 ? matches[0] : undefined
|
|
235
|
+
}
|