docbridge 0.13.1 → 0.15.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 +22 -1
- package/dist/bin/darwin-arm64/SHA256SUMS +3 -3
- package/dist/bin/darwin-arm64/docbridge-go-scanner +0 -0
- package/dist/bin/darwin-arm64/docbridge-swift-scanner +0 -0
- package/dist/bin/darwin-arm64/docbridge_dart_scanner +0 -0
- package/dist/bin/linux-x64/SHA256SUMS +2 -2
- package/dist/bin/linux-x64/docbridge-go-scanner +0 -0
- package/dist/bin/linux-x64/docbridge-swift-scanner +1 -1
- package/dist/index.js +65 -63
- package/dist/workers/java/docbridge-java-scanner.jar +0 -0
- package/docs/user/linking.md +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.15.0] - 2026-10-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `doc_anchor_not_found` suggests the closest anchor in the same Markdown file
|
|
15
|
+
with `Did you mean`, for both `@doc` annotations and link manifest entries.
|
|
16
|
+
Anchors in other doc files are not suggested, and no suggestion is added when
|
|
17
|
+
no anchor is close enough.
|
|
18
|
+
|
|
19
|
+
## [0.14.0] - 2026-10-08
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- A heading anchor keeps `_`, as GitHub's heading slugs do. Before, `_` became
|
|
24
|
+
`-`, so `### title_definitions` gave `#title-definitions`; it now gives
|
|
25
|
+
`#title_definitions`. A link written with the old `-` form now produces
|
|
26
|
+
`doc_anchor_not_found`; change it to match the heading, such as
|
|
27
|
+
`#title_definitions`.
|
|
28
|
+
|
|
10
29
|
## [0.13.1] - 2026-10-06
|
|
11
30
|
|
|
12
31
|
### Fixed
|
|
@@ -558,7 +577,9 @@ Initial release of the SpecLink CLI.
|
|
|
558
577
|
- `speclink check` command with `--root`, `--json`, and `--audit` options.
|
|
559
578
|
- `speclink --version` (alias `-v`) and `speclink --help` (alias `-h`).
|
|
560
579
|
|
|
561
|
-
[Unreleased]: https://github.com/salan70/docbridge/compare/v0.
|
|
580
|
+
[Unreleased]: https://github.com/salan70/docbridge/compare/v0.15.0...HEAD
|
|
581
|
+
[0.15.0]: https://github.com/salan70/docbridge/releases/tag/v0.15.0
|
|
582
|
+
[0.14.0]: https://github.com/salan70/docbridge/releases/tag/v0.14.0
|
|
562
583
|
[0.13.1]: https://github.com/salan70/docbridge/releases/tag/v0.13.1
|
|
563
584
|
[0.13.0]: https://github.com/salan70/docbridge/releases/tag/v0.13.0
|
|
564
585
|
[0.12.0]: https://github.com/salan70/docbridge/releases/tag/v0.12.0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
e07fdf1803763dd68bdbb274d2ffe439a5e9bdf19cb1d450d2fb163e0e387752 docbridge-swift-scanner
|
|
2
|
+
eddd64885f3c9071db22459cbcb1665f717c22bf9bd682ab3943b28338023f38 docbridge_dart_scanner
|
|
3
3
|
7fc3e6842b3e1bf425b8fd63e241eee7e64875f16be7ba8d1ae4a25baa7f2338 docbridge-rust-scanner
|
|
4
|
-
|
|
4
|
+
03b0a4dc30dd5e6a4958aa8d4ee1a6731c6efa7f6a90782c131641f0e02d084c docbridge-go-scanner
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
8b37cbbfd3afbad16d9b2f1a1a63c6f6db50e63573cdb7c0166ab2a7ea4d5f9a docbridge-swift-scanner
|
|
2
2
|
b75b14006ca792ed3ca8faf7ceee9e4d33c895f27dd416b640818cf8a4bdd9c8 docbridge_dart_scanner
|
|
3
3
|
e0da7b8cc6ee23704f5fe8514cc309f0bb60380c77b55e3bc5ed113abd1f3478 docbridge-rust-scanner
|
|
4
|
-
|
|
4
|
+
77fbcc7637f0719cc2628e8bcbaf29d0f84651a578a6b324ea6458c9c717b328 docbridge-go-scanner
|
|
Binary file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/dist/bin/linux-x64/docbridge-swift-scanner
|
|
3
3
|
size: 40932352 bytes
|
|
4
|
-
sha256:
|
|
4
|
+
sha256: 8b37cbbfd3afbad16d9b2f1a1a63c6f6db50e63573cdb7c0166ab2a7ea4d5f9a
|
package/dist/index.js
CHANGED
|
@@ -175581,7 +175581,7 @@ import { join as join15, resolve as resolve5 } from "node:path";
|
|
|
175581
175581
|
// package.json
|
|
175582
175582
|
var package_default = {
|
|
175583
175583
|
name: "docbridge",
|
|
175584
|
-
version: "0.
|
|
175584
|
+
version: "0.15.0",
|
|
175585
175585
|
description: "A CLI that links Markdown specifications and code declarations. Runs on Node.js and Bun.",
|
|
175586
175586
|
keywords: [
|
|
175587
175587
|
"bun",
|
|
@@ -177171,6 +177171,51 @@ function pluralize(word, count) {
|
|
|
177171
177171
|
return count === 1 ? word : `${word}s`;
|
|
177172
177172
|
}
|
|
177173
177173
|
|
|
177174
|
+
// src/shared/suggest.ts
|
|
177175
|
+
function nearestMatch(input, candidates) {
|
|
177176
|
+
const ranked = candidates.map((candidate, index) => ({
|
|
177177
|
+
candidate,
|
|
177178
|
+
distance: editDistance(input, candidate),
|
|
177179
|
+
index
|
|
177180
|
+
})).toSorted((left, right) => left.distance - right.distance || left.index - right.index);
|
|
177181
|
+
const abbreviation = ranked.find(({ candidate }) => isOrderedAbbreviation(input, candidate));
|
|
177182
|
+
if (abbreviation !== undefined) {
|
|
177183
|
+
return abbreviation.candidate;
|
|
177184
|
+
}
|
|
177185
|
+
const best = ranked[0];
|
|
177186
|
+
if (best === undefined) {
|
|
177187
|
+
return;
|
|
177188
|
+
}
|
|
177189
|
+
const closeEnough = best.distance <= Math.max(1, Math.floor(best.candidate.length / 2));
|
|
177190
|
+
return closeEnough ? best.candidate : undefined;
|
|
177191
|
+
}
|
|
177192
|
+
function isOrderedAbbreviation(input, candidate) {
|
|
177193
|
+
if (input.length < 3 || input.length >= candidate.length) {
|
|
177194
|
+
return false;
|
|
177195
|
+
}
|
|
177196
|
+
let candidateIndex = 0;
|
|
177197
|
+
for (const character of input) {
|
|
177198
|
+
const matchIndex = candidate.indexOf(character, candidateIndex);
|
|
177199
|
+
if (matchIndex === -1) {
|
|
177200
|
+
return false;
|
|
177201
|
+
}
|
|
177202
|
+
candidateIndex = matchIndex + 1;
|
|
177203
|
+
}
|
|
177204
|
+
return true;
|
|
177205
|
+
}
|
|
177206
|
+
function editDistance(left, right) {
|
|
177207
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
177208
|
+
for (let leftIndex = 1;leftIndex <= left.length; leftIndex += 1) {
|
|
177209
|
+
const current = [leftIndex];
|
|
177210
|
+
for (let rightIndex = 1;rightIndex <= right.length; rightIndex += 1) {
|
|
177211
|
+
const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
|
|
177212
|
+
current[rightIndex] = Math.min((current[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + substitutionCost);
|
|
177213
|
+
}
|
|
177214
|
+
previous = current;
|
|
177215
|
+
}
|
|
177216
|
+
return previous[right.length] ?? 0;
|
|
177217
|
+
}
|
|
177218
|
+
|
|
177174
177219
|
// src/link/resolver.ts
|
|
177175
177220
|
function resolveLinks(input) {
|
|
177176
177221
|
const diagnostics = [];
|
|
@@ -177178,10 +177223,12 @@ function resolveLinks(input) {
|
|
|
177178
177223
|
const docFilePaths = new Set(input.docFiles.map((file) => file.filePath));
|
|
177179
177224
|
const codeFilePaths = new Set(input.codeFiles.map((file) => file.filePath));
|
|
177180
177225
|
const docAnchorEndpoints = new Set;
|
|
177226
|
+
const docAnchorsByFile = new Map;
|
|
177181
177227
|
for (const file of input.docFiles) {
|
|
177182
177228
|
for (const anchor of file.anchors) {
|
|
177183
177229
|
docAnchorEndpoints.add(anchor.endpoint);
|
|
177184
177230
|
}
|
|
177231
|
+
docAnchorsByFile.set(file.filePath, file.anchors.map((anchor) => anchor.anchor));
|
|
177185
177232
|
}
|
|
177186
177233
|
const docToCodePairs = new Set;
|
|
177187
177234
|
for (const file of input.docFiles) {
|
|
@@ -177216,7 +177263,7 @@ function resolveLinks(input) {
|
|
|
177216
177263
|
continue;
|
|
177217
177264
|
}
|
|
177218
177265
|
if (!docAnchorEndpoints.has(docEndpoint)) {
|
|
177219
|
-
diagnostics.push(relationshipDiagnostic("doc_anchor_not_found", link.source, docEndpoint, `Doc anchor ${docEndpoint} referenced by ${link.source} does not exist.`, link));
|
|
177266
|
+
diagnostics.push(relationshipDiagnostic("doc_anchor_not_found", link.source, docEndpoint, withAnchorSuggestion(`Doc anchor ${docEndpoint} referenced by ${link.source} does not exist.`, docEndpoint, docAnchorsByFile), link));
|
|
177220
177267
|
continue;
|
|
177221
177268
|
}
|
|
177222
177269
|
if (!docToCodePairs.has(pairKey(docEndpoint, link.source))) {
|
|
@@ -177330,6 +177377,11 @@ function unlinkedDocSectionDiagnostic(node, anchor) {
|
|
|
177330
177377
|
}
|
|
177331
177378
|
return diagnostic;
|
|
177332
177379
|
}
|
|
177380
|
+
function withAnchorSuggestion(message, docEndpoint, docAnchorsByFile) {
|
|
177381
|
+
const docFilePath = filePathOf(docEndpoint);
|
|
177382
|
+
const suggestion = nearestMatch(fragmentOf(docEndpoint), docAnchorsByFile.get(docFilePath) ?? []);
|
|
177383
|
+
return suggestion === undefined ? message : `${message} Did you mean \`${docFilePath}#${suggestion}\`?`;
|
|
177384
|
+
}
|
|
177333
177385
|
function pairKey(source, target) {
|
|
177334
177386
|
return `${source}->${target}`;
|
|
177335
177387
|
}
|
|
@@ -177348,51 +177400,6 @@ function relationshipDiagnostic(code, source, target, message, link) {
|
|
|
177348
177400
|
return diagnostic;
|
|
177349
177401
|
}
|
|
177350
177402
|
|
|
177351
|
-
// src/shared/suggest.ts
|
|
177352
|
-
function nearestMatch(input, candidates) {
|
|
177353
|
-
const ranked = candidates.map((candidate, index) => ({
|
|
177354
|
-
candidate,
|
|
177355
|
-
distance: editDistance(input, candidate),
|
|
177356
|
-
index
|
|
177357
|
-
})).toSorted((left, right) => left.distance - right.distance || left.index - right.index);
|
|
177358
|
-
const abbreviation = ranked.find(({ candidate }) => isOrderedAbbreviation(input, candidate));
|
|
177359
|
-
if (abbreviation !== undefined) {
|
|
177360
|
-
return abbreviation.candidate;
|
|
177361
|
-
}
|
|
177362
|
-
const best = ranked[0];
|
|
177363
|
-
if (best === undefined) {
|
|
177364
|
-
return;
|
|
177365
|
-
}
|
|
177366
|
-
const closeEnough = best.distance <= Math.max(1, Math.floor(best.candidate.length / 2));
|
|
177367
|
-
return closeEnough ? best.candidate : undefined;
|
|
177368
|
-
}
|
|
177369
|
-
function isOrderedAbbreviation(input, candidate) {
|
|
177370
|
-
if (input.length < 3 || input.length >= candidate.length) {
|
|
177371
|
-
return false;
|
|
177372
|
-
}
|
|
177373
|
-
let candidateIndex = 0;
|
|
177374
|
-
for (const character of input) {
|
|
177375
|
-
const matchIndex = candidate.indexOf(character, candidateIndex);
|
|
177376
|
-
if (matchIndex === -1) {
|
|
177377
|
-
return false;
|
|
177378
|
-
}
|
|
177379
|
-
candidateIndex = matchIndex + 1;
|
|
177380
|
-
}
|
|
177381
|
-
return true;
|
|
177382
|
-
}
|
|
177383
|
-
function editDistance(left, right) {
|
|
177384
|
-
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
177385
|
-
for (let leftIndex = 1;leftIndex <= left.length; leftIndex += 1) {
|
|
177386
|
-
const current = [leftIndex];
|
|
177387
|
-
for (let rightIndex = 1;rightIndex <= right.length; rightIndex += 1) {
|
|
177388
|
-
const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
|
|
177389
|
-
current[rightIndex] = Math.min((current[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + substitutionCost);
|
|
177390
|
-
}
|
|
177391
|
-
previous = current;
|
|
177392
|
-
}
|
|
177393
|
-
return previous[right.length] ?? 0;
|
|
177394
|
-
}
|
|
177395
|
-
|
|
177396
177403
|
// src/link/manifest-apply.ts
|
|
177397
177404
|
function applyLinkManifest(input) {
|
|
177398
177405
|
if (input.manifest.entries.length === 0) {
|
|
@@ -177476,7 +177483,7 @@ class ApplyState {
|
|
|
177476
177483
|
return false;
|
|
177477
177484
|
}
|
|
177478
177485
|
if (!state.anchors.has(entry.docEndpoint)) {
|
|
177479
|
-
this.report("doc_anchor_not_found", entry.docEndpoint,
|
|
177486
|
+
this.report("doc_anchor_not_found", entry.docEndpoint, this.docAnchorNotFoundMessage(entry, state), entry, "doc");
|
|
177480
177487
|
return false;
|
|
177481
177488
|
}
|
|
177482
177489
|
this.markAnnotated(state, entry.docEndpoint);
|
|
@@ -177546,6 +177553,12 @@ class ApplyState {
|
|
|
177546
177553
|
const suggestion = nearestMatch(entry.code.fragment, candidates);
|
|
177547
177554
|
return suggestion === undefined ? base : `${base} Did you mean \`${suggestion}\`?`;
|
|
177548
177555
|
}
|
|
177556
|
+
docAnchorNotFoundMessage(entry, state) {
|
|
177557
|
+
const base = `Doc anchor ${entry.docEndpoint} declared in the link manifest does not exist.`;
|
|
177558
|
+
const candidates = state.file.anchors.map((anchor) => anchor.anchor);
|
|
177559
|
+
const suggestion = nearestMatch(entry.doc.fragment, candidates);
|
|
177560
|
+
return suggestion === undefined ? base : `${base} Did you mean \`${entry.doc.filePath}#${suggestion}\`?`;
|
|
177561
|
+
}
|
|
177549
177562
|
report(code, target, message, entry, side, language) {
|
|
177550
177563
|
const range = side === "code" ? entry.codeRange : entry.docRange;
|
|
177551
177564
|
const diagnostic = {
|
|
@@ -177597,24 +177610,13 @@ function manifestLink(source, target, entry, side) {
|
|
|
177597
177610
|
}
|
|
177598
177611
|
|
|
177599
177612
|
// src/shared/cancelable.ts
|
|
177600
|
-
function deferred() {
|
|
177601
|
-
let settle;
|
|
177602
|
-
const promise = new Promise((resolve2, reject) => {
|
|
177603
|
-
settle = { resolve: resolve2, reject };
|
|
177604
|
-
});
|
|
177605
|
-
return {
|
|
177606
|
-
promise,
|
|
177607
|
-
resolve: (value) => settle?.resolve(value),
|
|
177608
|
-
reject: (error) => settle?.reject(error)
|
|
177609
|
-
};
|
|
177610
|
-
}
|
|
177611
177613
|
function settledCancelable(value) {
|
|
177612
177614
|
return { promise: Promise.resolve(value), cancel: () => {
|
|
177613
177615
|
return;
|
|
177614
177616
|
} };
|
|
177615
177617
|
}
|
|
177616
177618
|
function mapCancelable(task, map) {
|
|
177617
|
-
const mapped =
|
|
177619
|
+
const mapped = Promise.withResolvers();
|
|
177618
177620
|
let cancelled = false;
|
|
177619
177621
|
task.promise.then((value) => {
|
|
177620
177622
|
if (cancelled) {
|
|
@@ -177640,7 +177642,7 @@ function isAbortError(error) {
|
|
|
177640
177642
|
return error instanceof Error && error.name === "AbortError";
|
|
177641
177643
|
}
|
|
177642
177644
|
function cancelableSequence(body) {
|
|
177643
|
-
const outcome =
|
|
177645
|
+
const outcome = Promise.withResolvers();
|
|
177644
177646
|
let cancelled = false;
|
|
177645
177647
|
let running;
|
|
177646
177648
|
const step = async (next) => {
|
|
@@ -178539,7 +178541,7 @@ function workerTimeoutMs(fileCount) {
|
|
|
178539
178541
|
return 30000 + 1000 * fileCount;
|
|
178540
178542
|
}
|
|
178541
178543
|
function runScannerWorkerProcess(input) {
|
|
178542
|
-
const run =
|
|
178544
|
+
const run = Promise.withResolvers();
|
|
178543
178545
|
let settled = false;
|
|
178544
178546
|
let timer;
|
|
178545
178547
|
let grace;
|
|
@@ -179495,7 +179497,7 @@ function stripClosingHashes(text) {
|
|
|
179495
179497
|
}
|
|
179496
179498
|
function toAnchor(headingText) {
|
|
179497
179499
|
const lowered = headingText.toLowerCase();
|
|
179498
|
-
const replaced = lowered.replace(/[^\p{L}\p{N}]+/gu, "-");
|
|
179500
|
+
const replaced = lowered.replace(/[^\p{L}\p{N}_]+/gu, "-");
|
|
179499
179501
|
return replaced.replace(/^-+/, "").replace(/-+$/, "");
|
|
179500
179502
|
}
|
|
179501
179503
|
function matchCodeComment(line, filePath, lineNumber) {
|
|
Binary file
|
package/docs/user/linking.md
CHANGED
|
@@ -255,8 +255,9 @@ Use the scanner-produced canonical symbol ID exactly.
|
|
|
255
255
|
## Heading anchors and reciprocity
|
|
256
256
|
|
|
257
257
|
DocBridge creates anchors from ATX headings only. It lowercases the heading,
|
|
258
|
-
collapses runs of whitespace and punctuation to `-`,
|
|
259
|
-
|
|
258
|
+
collapses runs of whitespace and punctuation other than `_` to `-`, keeps `_`
|
|
259
|
+
as GitHub's heading slugs do, preserves Unicode letters and numbers, and removes
|
|
260
|
+
leading and trailing hyphens. `## Login Spec (v2)` is
|
|
260
261
|
`#login-spec-v2`.
|
|
261
262
|
|
|
262
263
|
Empty headings have no anchor; a `@code` annotation attached to an empty
|