docbridge 0.14.0 → 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 CHANGED
@@ -7,6 +7,15 @@ 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
+
10
19
  ## [0.14.0] - 2026-10-08
11
20
 
12
21
  ### Changed
@@ -568,7 +577,8 @@ Initial release of the SpecLink CLI.
568
577
  - `speclink check` command with `--root`, `--json`, and `--audit` options.
569
578
  - `speclink --version` (alias `-v`) and `speclink --help` (alias `-h`).
570
579
 
571
- [Unreleased]: https://github.com/salan70/docbridge/compare/v0.14.0...HEAD
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
572
582
  [0.14.0]: https://github.com/salan70/docbridge/releases/tag/v0.14.0
573
583
  [0.13.1]: https://github.com/salan70/docbridge/releases/tag/v0.13.1
574
584
  [0.13.0]: https://github.com/salan70/docbridge/releases/tag/v0.13.0
@@ -1,4 +1,4 @@
1
- 2db42ddea8060f988cea4933696e73d652ae5b807a8e8ff00249375faf2b0be4 docbridge-swift-scanner
2
- f3da86ea5a0c1c92fa1e1f077251bec490e294d257b09cd03a46c8ce382aad19 docbridge_dart_scanner
1
+ e07fdf1803763dd68bdbb274d2ffe439a5e9bdf19cb1d450d2fb163e0e387752 docbridge-swift-scanner
2
+ eddd64885f3c9071db22459cbcb1665f717c22bf9bd682ab3943b28338023f38 docbridge_dart_scanner
3
3
  7fc3e6842b3e1bf425b8fd63e241eee7e64875f16be7ba8d1ae4a25baa7f2338 docbridge-rust-scanner
4
- 1646b06856b0420acdcb82d1692b7afea83fe2b7666adeb6412c99782dfc8cae docbridge-go-scanner
4
+ 03b0a4dc30dd5e6a4958aa8d4ee1a6731c6efa7f6a90782c131641f0e02d084c docbridge-go-scanner
@@ -1,4 +1,4 @@
1
- 0c023cfacede26a217a62420f9da1206b93c228122a3685469685a3db89f7735 docbridge-swift-scanner
1
+ 8b37cbbfd3afbad16d9b2f1a1a63c6f6db50e63573cdb7c0166ab2a7ea4d5f9a docbridge-swift-scanner
2
2
  b75b14006ca792ed3ca8faf7ceee9e4d33c895f27dd416b640818cf8a4bdd9c8 docbridge_dart_scanner
3
3
  e0da7b8cc6ee23704f5fe8514cc309f0bb60380c77b55e3bc5ed113abd1f3478 docbridge-rust-scanner
4
- 5b8aee5b85a38679875c473b2dc32f021f022cba242785bb0fd94ee6fda0a6bf docbridge-go-scanner
4
+ 77fbcc7637f0719cc2628e8bcbaf29d0f84651a578a6b324ea6458c9c717b328 docbridge-go-scanner
@@ -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: 0c023cfacede26a217a62420f9da1206b93c228122a3685469685a3db89f7735
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.14.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, `Doc anchor ${entry.docEndpoint} declared in the link manifest does not exist.`, entry, "doc");
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 = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docbridge",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "A CLI that links Markdown specifications and code declarations. Runs on Node.js and Bun.",
5
5
  "keywords": [
6
6
  "bun",