@sledorze/cairn 0.1.0 → 0.3.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.
Files changed (117) hide show
  1. package/README.md +69 -18
  2. package/dist/cli.js +69378 -70958
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +10 -10
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/Config.d.ts +22 -22
  8. package/dist/core/Config.d.ts.map +1 -1
  9. package/dist/core/Config.js +33 -33
  10. package/dist/core/Config.js.map +1 -1
  11. package/dist/core/glob.bench.js +1 -1
  12. package/dist/core/glob.d.ts.map +1 -1
  13. package/dist/core/hashing.d.ts +3 -0
  14. package/dist/core/hashing.d.ts.map +1 -0
  15. package/dist/core/hashing.js +16 -0
  16. package/dist/core/hashing.js.map +1 -0
  17. package/dist/core/links/Anchors.d.ts +30 -0
  18. package/dist/core/links/Anchors.d.ts.map +1 -0
  19. package/dist/core/links/Anchors.js +119 -0
  20. package/dist/core/links/Anchors.js.map +1 -0
  21. package/dist/core/links/MarkdownLinks.bench.d.ts.map +1 -0
  22. package/dist/core/{MarkdownLinks.bench.js → links/MarkdownLinks.bench.js} +1 -1
  23. package/dist/core/links/MarkdownLinks.bench.js.map +1 -0
  24. package/dist/core/links/MarkdownLinks.d.ts +121 -0
  25. package/dist/core/links/MarkdownLinks.d.ts.map +1 -0
  26. package/dist/core/links/MarkdownLinks.js +228 -0
  27. package/dist/core/links/MarkdownLinks.js.map +1 -0
  28. package/dist/core/links/RefStore.d.ts +32 -0
  29. package/dist/core/links/RefStore.d.ts.map +1 -0
  30. package/dist/core/links/RefStore.js +60 -0
  31. package/dist/core/links/RefStore.js.map +1 -0
  32. package/dist/core/links/markdownFences.d.ts +10 -0
  33. package/dist/core/links/markdownFences.d.ts.map +1 -0
  34. package/dist/core/links/markdownFences.js +51 -0
  35. package/dist/core/links/markdownFences.js.map +1 -0
  36. package/dist/core/paths.d.ts +8 -0
  37. package/dist/core/paths.d.ts.map +1 -1
  38. package/dist/core/paths.js +13 -0
  39. package/dist/core/paths.js.map +1 -1
  40. package/dist/core/sidecar.d.ts +45 -0
  41. package/dist/core/sidecar.d.ts.map +1 -0
  42. package/dist/core/sidecar.js +83 -0
  43. package/dist/core/sidecar.js.map +1 -0
  44. package/dist/core/{DocSummaries.d.ts → summaries/DocSummaries.d.ts} +13 -3
  45. package/dist/core/summaries/DocSummaries.d.ts.map +1 -0
  46. package/dist/core/{DocSummaries.js → summaries/DocSummaries.js} +31 -11
  47. package/dist/core/summaries/DocSummaries.js.map +1 -0
  48. package/dist/core/summaries/StampStore.d.ts +19 -0
  49. package/dist/core/summaries/StampStore.d.ts.map +1 -0
  50. package/dist/core/summaries/StampStore.js +34 -0
  51. package/dist/core/summaries/StampStore.js.map +1 -0
  52. package/dist/core/summaries/SummaryTree.bench.d.ts.map +1 -0
  53. package/dist/core/{SummaryTree.bench.js → summaries/SummaryTree.bench.js} +24 -14
  54. package/dist/core/summaries/SummaryTree.bench.js.map +1 -0
  55. package/dist/core/{SummaryTree.d.ts → summaries/SummaryTree.d.ts} +10 -1
  56. package/dist/core/summaries/SummaryTree.d.ts.map +1 -0
  57. package/dist/core/{SummaryTree.js → summaries/SummaryTree.js} +35 -9
  58. package/dist/core/summaries/SummaryTree.js.map +1 -0
  59. package/dist/index.d.ts +5 -5
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +10 -10
  62. package/dist/index.js.map +1 -1
  63. package/dist/init/content.d.ts +2 -2
  64. package/dist/init/content.d.ts.map +1 -1
  65. package/dist/init/content.js +45 -15
  66. package/dist/init/content.js.map +1 -1
  67. package/dist/init/generate.d.ts +1 -1
  68. package/dist/init/generate.d.ts.map +1 -1
  69. package/dist/init/generate.js +1 -1
  70. package/dist/io/DocsFs.d.ts +2 -3
  71. package/dist/io/DocsFs.d.ts.map +1 -1
  72. package/dist/io/DocsFs.js +15 -5
  73. package/dist/io/DocsFs.js.map +1 -1
  74. package/dist/program/JsonReport.d.ts +2 -2
  75. package/dist/program/JsonReport.d.ts.map +1 -1
  76. package/dist/program/JsonReport.js +2 -2
  77. package/dist/program/JsonReport.js.map +1 -1
  78. package/dist/program/{CheckLinks.d.ts → links/CheckLinks.d.ts} +9 -4
  79. package/dist/program/links/CheckLinks.d.ts.map +1 -0
  80. package/dist/program/links/CheckLinks.js +243 -0
  81. package/dist/program/links/CheckLinks.js.map +1 -0
  82. package/dist/program/links/CheckRefs.d.ts +47 -0
  83. package/dist/program/links/CheckRefs.d.ts.map +1 -0
  84. package/dist/program/links/CheckRefs.js +151 -0
  85. package/dist/program/links/CheckRefs.js.map +1 -0
  86. package/dist/program/locale.d.ts.map +1 -1
  87. package/dist/program/summaries/CheckSummaries.bench.d.ts +2 -0
  88. package/dist/program/summaries/CheckSummaries.bench.d.ts.map +1 -0
  89. package/dist/program/summaries/CheckSummaries.bench.js +94 -0
  90. package/dist/program/summaries/CheckSummaries.bench.js.map +1 -0
  91. package/dist/program/summaries/CheckSummaries.d.ts +58 -0
  92. package/dist/program/summaries/CheckSummaries.d.ts.map +1 -0
  93. package/dist/program/summaries/CheckSummaries.js +302 -0
  94. package/dist/program/summaries/CheckSummaries.js.map +1 -0
  95. package/package.json +7 -8
  96. package/schema/cairn.schema.json +35 -38
  97. package/dist/core/DocSummaries.d.ts.map +0 -1
  98. package/dist/core/DocSummaries.js.map +0 -1
  99. package/dist/core/MarkdownLinks.bench.d.ts.map +0 -1
  100. package/dist/core/MarkdownLinks.bench.js.map +0 -1
  101. package/dist/core/MarkdownLinks.d.ts +0 -51
  102. package/dist/core/MarkdownLinks.d.ts.map +0 -1
  103. package/dist/core/MarkdownLinks.js +0 -115
  104. package/dist/core/MarkdownLinks.js.map +0 -1
  105. package/dist/core/SummaryTree.bench.d.ts.map +0 -1
  106. package/dist/core/SummaryTree.bench.js.map +0 -1
  107. package/dist/core/SummaryTree.d.ts.map +0 -1
  108. package/dist/core/SummaryTree.js.map +0 -1
  109. package/dist/program/CheckLinks.d.ts.map +0 -1
  110. package/dist/program/CheckLinks.js +0 -122
  111. package/dist/program/CheckLinks.js.map +0 -1
  112. package/dist/program/CheckSummaries.d.ts +0 -38
  113. package/dist/program/CheckSummaries.d.ts.map +0 -1
  114. package/dist/program/CheckSummaries.js +0 -198
  115. package/dist/program/CheckSummaries.js.map +0 -1
  116. /package/dist/core/{MarkdownLinks.bench.d.ts → links/MarkdownLinks.bench.d.ts} +0 -0
  117. /package/dist/core/{SummaryTree.bench.d.ts → summaries/SummaryTree.bench.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { bench, describe } from 'vitest';
2
- import { buildBasenameIndex, checkContent, extractLinkDefinitions, extractLinks, stripCode } from "./MarkdownLinks.js";
2
+ import { buildBasenameIndex, checkContent, extractLinkDefinitions, extractLinks, stripCode } from './MarkdownLinks.js';
3
3
  const makeDoc = (linkCount) => {
4
4
  const lines = [];
5
5
  for (let i = 0; i < linkCount; i++) {
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownLinks.bench.js","sourceRoot":"","sources":["../../../src/core/links/MarkdownLinks.bench.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ export interface MarkdownLink {
2
+ readonly target: string;
3
+ readonly text: string;
4
+ }
5
+ export interface MarkdownLinkDef {
6
+ readonly label: string;
7
+ readonly target: string;
8
+ }
9
+ /** Why a link was reported broken — additive on `BrokenLink` (issue #39):
10
+ * `'path'` the target itself doesn't resolve (the original, only check);
11
+ * `'anchor'` the target resolves but its `#heading` fragment doesn't;
12
+ * `'line'` the target resolves but its `#L10`/`#L10-L20` fragment is out of range. */
13
+ export type BrokenReason = 'anchor' | 'line' | 'path';
14
+ export interface BrokenLink {
15
+ /** Short, human-readable elaboration — what's actually there instead, so
16
+ * fixing this doesn't require opening the target file first. E.g. for
17
+ * `reason: 'anchor'`, the target's real headings; for `reason: 'line'`,
18
+ * the target's real line count. */
19
+ readonly detail?: string;
20
+ readonly reason?: BrokenReason;
21
+ readonly suggestion?: string;
22
+ readonly target: string;
23
+ readonly text: string;
24
+ }
25
+ export interface SuggestFixArgs {
26
+ readonly fromDir: string;
27
+ readonly index: ReadonlyMap<string, readonly string[]>;
28
+ readonly target: string;
29
+ }
30
+ /**
31
+ * A checkable link whose target's existence and/or `#fragment` couldn't be
32
+ * decided without IO — either the target resolves outside the eagerly-listed
33
+ * `roots` (its existence itself is unknown), or it carries an anchor (its
34
+ * validity needs the target's real content, which `checkContent` — pure and
35
+ * IO-free — never reads for a file other than the one it's scanning). The
36
+ * caller (../program/CheckLinks.ts) resolves these, bounded by `base`.
37
+ */
38
+ export interface PendingCheck {
39
+ /** `null` when the target itself has no `#fragment` — only existence (out of `roots`) is unresolved. */
40
+ readonly anchor: string | null;
41
+ readonly fromDir: string;
42
+ readonly target: string;
43
+ readonly targetAbs: string;
44
+ readonly text: string;
45
+ }
46
+ export interface CheckContentResult {
47
+ readonly broken: readonly BrokenLink[];
48
+ readonly pending: readonly PendingCheck[];
49
+ }
50
+ export interface CheckContentArgs {
51
+ readonly content: string;
52
+ readonly existsAbs: (absPath: string) => boolean;
53
+ readonly fileAbs: string;
54
+ readonly index?: ReadonlyMap<string, readonly string[]>;
55
+ /** True for any absolute path inside the eagerly-listed `roots` (regardless
56
+ * of whether it exists there) — lets `checkContent` tell "genuinely absent,
57
+ * in-root" (resolvable now, no IO) apart from "outside `roots` entirely"
58
+ * (existence itself unknown, deferred to `pending`). Defaults to "everything
59
+ * is in-root", matching this function's original, `roots`-only behaviour. */
60
+ readonly inRoots?: (absPath: string) => boolean;
61
+ }
62
+ /**
63
+ * Blank out fenced (``` / ~~~, via `maskFencedCode`) and inline (`code`)
64
+ * spans so links that only appear inside code examples are NOT treated as
65
+ * real links. Newlines are kept so line-based reasoning is unaffected; other
66
+ * characters become spaces.
67
+ */
68
+ export declare const stripCode: (content: string) => string;
69
+ /** Extract inline Markdown links/images as `{ target, text }`. */
70
+ export declare const extractLinks: (content: string) => MarkdownLink[];
71
+ /** Extract reference-style link definitions (`[label]: ./path "title"`). */
72
+ export declare const extractLinkDefinitions: (content: string) => MarkdownLinkDef[];
73
+ /** True only for relative paths we can resolve on disk — INCLUDING a bare
74
+ * `#heading` (same-page anchor), now checkable against the file's own
75
+ * headings (issue #39, scenario C); previously always skipped. */
76
+ export declare const isCheckableTarget: (target: string) => boolean;
77
+ export interface ParsedTarget {
78
+ /** The raw text after `#`, or `null` if the target carries no fragment. */
79
+ readonly anchor: string | null;
80
+ /** The target with `#anchor`/`?query` removed; `''` for a bare `#anchor`
81
+ * (same-page fragment — the "path" is the current file itself). */
82
+ readonly path: string;
83
+ }
84
+ /** Split a link target into its path and (optional) `#anchor`, dropping any
85
+ * `?query` — plain `indexOf`/`slice`, not a regex (CodeQL flagged the
86
+ * previous `/\?.*$/` form as a polynomial-ReDoS risk on library input). */
87
+ export declare const parseTarget: (target: string) => ParsedTarget;
88
+ /** Drop `#anchor` and `?query` from a target. */
89
+ export declare const stripAnchor: (target: string) => string;
90
+ export interface Reference {
91
+ readonly anchor: string | null;
92
+ readonly target: string;
93
+ }
94
+ /**
95
+ * Every real (checkable, cross-file) reference a doc makes — deduped by
96
+ * `(target, anchor)` — for issue #39 Scenario I's content-hash drift
97
+ * tracking (`../program/CheckRefs.ts`). Same-page anchors (`path === ''`)
98
+ * are excluded: a same-page fragment isn't a reference to another file's
99
+ * content, it's a position within this one — `checkContent`'s own anchor
100
+ * check already covers whether it resolves.
101
+ */
102
+ export declare const extractReferences: (content: string) => Reference[];
103
+ /** Map basename -> list of absolute paths, for ambiguity-aware fixing. */
104
+ export declare const buildBasenameIndex: (absPaths: readonly string[]) => Map<string, string[]>;
105
+ /**
106
+ * Suggest a repaired relative path for a broken target. Returns a path only
107
+ * when exactly one indexed file matches the basename (no ambiguity); else null.
108
+ */
109
+ export declare const suggestFix: ({ fromDir, index, target }: SuggestFixArgs) => string | null;
110
+ /**
111
+ * Check one file's content for broken relative links, path existence and
112
+ * `#fragment` validity. Resolves everything decidable without IO — an
113
+ * in-`roots` path's existence (`existsAbs`, unchanged fast path) and a
114
+ * same-page `#anchor` (the file's own content is already in hand, no IO
115
+ * needed — issue #39 scenario C) — synchronously into `broken`. Anything
116
+ * needing another file's content (a cross-file anchor, or a target outside
117
+ * `roots` whose existence isn't yet known) is deferred into `pending` for the
118
+ * caller (../program/CheckLinks.ts) to resolve with real, `base`-bounded IO.
119
+ */
120
+ export declare const checkContent: ({ content, existsAbs, fileAbs, index, inRoots, }: CheckContentArgs) => CheckContentResult;
121
+ //# sourceMappingURL=MarkdownLinks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownLinks.d.ts","sourceRoot":"","sources":["../../../src/core/links/MarkdownLinks.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;sFAGsF;AACtF,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAA;AAErD,MAAM,WAAW,UAAU;IACzB;;;uCAGmC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAA;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAA;IACtD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,wGAAwG;IACxG,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,CAAA;IACtC,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;CAC1C;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAA;IACvD;;;;iFAI6E;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;CAChD;AAKD;;;;;GAKG;AACH,eAAO,MAAM,SAAS,YAAa,MAAM,KAAG,MAC6C,CAAA;AAIzF,kEAAkE;AAClE,eAAO,MAAM,YAAY,YAAa,MAAM,KAAG,YAAY,EAM1D,CAAA;AAED,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,YAAa,MAAM,KAAG,eAAe,EAMvE,CAAA;AAwCD;;kEAEkE;AAClE,eAAO,MAAM,iBAAiB,WAAY,MAAM,KAAG,OAWlD,CAAA;AAED,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B;uEACmE;IACnE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED;;2EAE2E;AAC3E,eAAO,MAAM,WAAW,WAAY,MAAM,KAAG,YAQ5C,CAAA;AAED,iDAAiD;AACjD,eAAO,MAAM,WAAW,WAAY,MAAM,KAAG,MAAkC,CAAA;AAE/E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,YAAa,MAAM,KAAG,SAAS,EAwB5D,CAAA;AAED,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB,aAAc,SAAS,MAAM,EAAE,KAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAYpF,CAAA;AAOD;;;GAGG;AACH,eAAO,MAAM,UAAU,+BAAgC,cAAc,KAAG,MAAM,GAAG,IAOhF,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,qDAMtB,gBAAgB,KAAG,kBA2DrB,CAAA"}
@@ -0,0 +1,228 @@
1
+ // Pure, IO-free helpers to find and auto-repair relative Markdown links.
2
+ // All functions here are deterministic and unit-tested in MarkdownLinks.unit.test.ts.
3
+ // The Effect program that touches the filesystem lives in ../program/CheckLinks.ts.
4
+ import * as nodePath from 'node:path';
5
+ import { describeAnchors, extractAnchors } from './Anchors.js';
6
+ import { maskFencedCode } from './markdownFences.js';
7
+ // Reason in POSIX so link resolution is identical on every OS (inputs are
8
+ // normalised to `/` at the IO boundary).
9
+ const path = nodePath.posix;
10
+ const LINK_RE = /!?\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
11
+ const INLINE_CODE_RE = /`[^`\n]*`/g;
12
+ /**
13
+ * Blank out fenced (``` / ~~~, via `maskFencedCode`) and inline (`code`)
14
+ * spans so links that only appear inside code examples are NOT treated as
15
+ * real links. Newlines are kept so line-based reasoning is unaffected; other
16
+ * characters become spaces.
17
+ */
18
+ export const stripCode = (content) => maskFencedCode(content).replaceAll(INLINE_CODE_RE, (block) => ' '.repeat(block.length));
19
+ const LINK_DEF_RE = /^[ \t]*\[([^\]]+)\]:[ \t]*<?([^>\s]+)>?/gm;
20
+ /** Extract inline Markdown links/images as `{ target, text }`. */
21
+ export const extractLinks = (content) => {
22
+ const links = [];
23
+ for (const match of content.matchAll(LINK_RE)) {
24
+ links.push({ target: match[2] ?? '', text: match[1] ?? '' });
25
+ }
26
+ return links;
27
+ };
28
+ /** Extract reference-style link definitions (`[label]: ./path "title"`). */
29
+ export const extractLinkDefinitions = (content) => {
30
+ const defs = [];
31
+ for (const match of content.matchAll(LINK_DEF_RE)) {
32
+ defs.push({ label: match[1] ?? '', target: match[2] ?? '' });
33
+ }
34
+ return defs;
35
+ };
36
+ /** Start offset of a regex match's first capture group, given the group's
37
+ * own text starts right after the FIRST `[` in the whole match (true for
38
+ * both `LINK_RE`'s text group and `LINK_DEF_RE`'s label group). */
39
+ const captureGroupStart = (match) => (match.index ?? 0) + match[0].indexOf('[') + 1;
40
+ /**
41
+ * Like `extractLinks`, but matches against `masked` (to keep the existing
42
+ * "a link written inside a code example isn't a real link" exclusion,
43
+ * unchanged) while reading the TEXT back from `original` at that same
44
+ * position. Without this, a link whose own visible text is itself
45
+ * backtick-styled (e.g. `` [`glob.ts`](../glob.ts) ``) reports as blank —
46
+ * `stripCode`'s inline-code masking blanks that backtick span before
47
+ * extraction ever sees it, since it can't distinguish "styling inside a
48
+ * link's text" from "a link written inside a code example." Errors need the
49
+ * real text to be actionable, so this is a position-preserving re-read, not a
50
+ * masking change (masking's own exclusion behaviour must stay exactly as-is).
51
+ */
52
+ const extractLinksPreservingText = (original, masked) => {
53
+ const links = [];
54
+ for (const match of masked.matchAll(LINK_RE)) {
55
+ const textStart = captureGroupStart(match);
56
+ const textLength = match[1]?.length ?? 0;
57
+ links.push({ target: match[2] ?? '', text: original.slice(textStart, textStart + textLength) });
58
+ }
59
+ return links;
60
+ };
61
+ /** The `extractLinkDefinitions` counterpart of `extractLinksPreservingText`. */
62
+ const extractLinkDefinitionsPreservingLabel = (original, masked) => {
63
+ const defs = [];
64
+ for (const match of masked.matchAll(LINK_DEF_RE)) {
65
+ const labelStart = captureGroupStart(match);
66
+ const labelLength = match[1]?.length ?? 0;
67
+ defs.push({ label: original.slice(labelStart, labelStart + labelLength), target: match[2] ?? '' });
68
+ }
69
+ return defs;
70
+ };
71
+ /** True only for relative paths we can resolve on disk — INCLUDING a bare
72
+ * `#heading` (same-page anchor), now checkable against the file's own
73
+ * headings (issue #39, scenario C); previously always skipped. */
74
+ export const isCheckableTarget = (target) => {
75
+ if (!target) {
76
+ return false;
77
+ }
78
+ if (target.startsWith('//')) {
79
+ return false;
80
+ } // protocol-relative URL
81
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target)) {
82
+ return false;
83
+ } // http:, mailto:, etc.
84
+ return true;
85
+ };
86
+ /** Split a link target into its path and (optional) `#anchor`, dropping any
87
+ * `?query` — plain `indexOf`/`slice`, not a regex (CodeQL flagged the
88
+ * previous `/\?.*$/` form as a polynomial-ReDoS risk on library input). */
89
+ export const parseTarget = (target) => {
90
+ const hashIdx = target.indexOf('#');
91
+ const rawPath = hashIdx === -1 ? target : target.slice(0, hashIdx);
92
+ const queryIdx = rawPath.indexOf('?');
93
+ return {
94
+ anchor: hashIdx === -1 ? null : target.slice(hashIdx + 1),
95
+ path: queryIdx === -1 ? rawPath : rawPath.slice(0, queryIdx),
96
+ };
97
+ };
98
+ /** Drop `#anchor` and `?query` from a target. */
99
+ export const stripAnchor = (target) => parseTarget(target).path;
100
+ /**
101
+ * Every real (checkable, cross-file) reference a doc makes — deduped by
102
+ * `(target, anchor)` — for issue #39 Scenario I's content-hash drift
103
+ * tracking (`../program/CheckRefs.ts`). Same-page anchors (`path === ''`)
104
+ * are excluded: a same-page fragment isn't a reference to another file's
105
+ * content, it's a position within this one — `checkContent`'s own anchor
106
+ * check already covers whether it resolves.
107
+ */
108
+ export const extractReferences = (content) => {
109
+ const masked = stripCode(content);
110
+ const candidates = [
111
+ ...extractLinks(masked),
112
+ ...extractLinkDefinitions(masked).map((def) => ({ target: def.target, text: '' })),
113
+ ];
114
+ const seen = new Set();
115
+ const refs = [];
116
+ for (const link of candidates) {
117
+ if (!isCheckableTarget(link.target)) {
118
+ continue;
119
+ }
120
+ const { anchor, path: target } = parseTarget(link.target);
121
+ if (target === '') {
122
+ continue;
123
+ }
124
+ const key = `${target}#${anchor ?? ''}`;
125
+ if (seen.has(key)) {
126
+ continue;
127
+ }
128
+ seen.add(key);
129
+ refs.push({ anchor, target });
130
+ }
131
+ return refs;
132
+ };
133
+ /** Map basename -> list of absolute paths, for ambiguity-aware fixing. */
134
+ export const buildBasenameIndex = (absPaths) => {
135
+ const index = new Map();
136
+ for (const abs of absPaths) {
137
+ const base = path.basename(abs);
138
+ const bucket = index.get(base);
139
+ if (bucket) {
140
+ bucket.push(abs);
141
+ }
142
+ else {
143
+ index.set(base, [abs]);
144
+ }
145
+ }
146
+ return index;
147
+ };
148
+ const toRelative = (fromDir, toAbs) => {
149
+ const rel = path.relative(fromDir, toAbs).split(path.sep).join('/');
150
+ return rel.startsWith('.') ? rel : `./${rel}`;
151
+ };
152
+ /**
153
+ * Suggest a repaired relative path for a broken target. Returns a path only
154
+ * when exactly one indexed file matches the basename (no ambiguity); else null.
155
+ */
156
+ export const suggestFix = ({ fromDir, index, target }) => {
157
+ const base = path.basename(stripAnchor(target));
158
+ const candidates = index.get(base);
159
+ if (!candidates || candidates.length !== 1) {
160
+ return null;
161
+ }
162
+ return toRelative(fromDir, candidates[0] ?? '');
163
+ };
164
+ /**
165
+ * Check one file's content for broken relative links, path existence and
166
+ * `#fragment` validity. Resolves everything decidable without IO — an
167
+ * in-`roots` path's existence (`existsAbs`, unchanged fast path) and a
168
+ * same-page `#anchor` (the file's own content is already in hand, no IO
169
+ * needed — issue #39 scenario C) — synchronously into `broken`. Anything
170
+ * needing another file's content (a cross-file anchor, or a target outside
171
+ * `roots` whose existence isn't yet known) is deferred into `pending` for the
172
+ * caller (../program/CheckLinks.ts) to resolve with real, `base`-bounded IO.
173
+ */
174
+ export const checkContent = ({ content, existsAbs, fileAbs, index, inRoots = () => true, }) => {
175
+ const fromDir = path.dirname(fileAbs);
176
+ const broken = [];
177
+ const pending = [];
178
+ const masked = stripCode(content);
179
+ const candidates = [
180
+ ...extractLinksPreservingText(content, masked),
181
+ // Reference-style definitions are checked by their target too.
182
+ ...extractLinkDefinitionsPreservingLabel(content, masked).map((def) => ({
183
+ target: def.target,
184
+ text: `[${def.label}]`,
185
+ })),
186
+ ];
187
+ let sourceAnchors = null;
188
+ const getSourceAnchors = () => (sourceAnchors ??= extractAnchors(content));
189
+ for (const link of candidates) {
190
+ if (!isCheckableTarget(link.target)) {
191
+ continue;
192
+ }
193
+ const { anchor, path: rel } = parseTarget(link.target);
194
+ if (rel === '') {
195
+ // Same-page anchor: no other file to read, resolve now.
196
+ if (anchor !== null && !getSourceAnchors().has(anchor)) {
197
+ broken.push({
198
+ detail: describeAnchors(getSourceAnchors()),
199
+ reason: 'anchor',
200
+ target: link.target,
201
+ text: link.text,
202
+ });
203
+ }
204
+ continue;
205
+ }
206
+ const abs = path.resolve(fromDir, rel);
207
+ if (existsAbs(abs)) {
208
+ if (anchor !== null) {
209
+ pending.push({ anchor, fromDir, target: link.target, targetAbs: abs, text: link.text });
210
+ }
211
+ continue;
212
+ }
213
+ if (!inRoots(abs)) {
214
+ // Outside the eagerly-listed universe: existence itself is unknown —
215
+ // never assume broken (issue #39 scenario E), defer to real IO.
216
+ pending.push({ anchor, fromDir, target: link.target, targetAbs: abs, text: link.text });
217
+ continue;
218
+ }
219
+ // In `roots` and genuinely absent — `existsAbs` already covers the
220
+ // complete in-root existence universe, no IO needed to know this.
221
+ const suggestion = index ? suggestFix({ fromDir, index, target: link.target }) : null;
222
+ broken.push(suggestion
223
+ ? { reason: 'path', suggestion, target: link.target, text: link.text }
224
+ : { reason: 'path', target: link.target, text: link.text });
225
+ }
226
+ return { broken, pending };
227
+ };
228
+ //# sourceMappingURL=MarkdownLinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MarkdownLinks.js","sourceRoot":"","sources":["../../../src/core/links/MarkdownLinks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { MetaLayout } from '../sidecar.ts';
2
+ /** The subdirectory, under `metaRoot`, every reference sidecar lives under —
3
+ * disjoint from the summary-freshness sidecars living directly under
4
+ * `metaRoot` (see module header: this is load-bearing, not stylistic). */
5
+ export declare const REFS_DIR = "refs";
6
+ export interface RefRecord {
7
+ readonly anchor?: string;
8
+ readonly hash: string;
9
+ readonly target: string;
10
+ }
11
+ export interface RefsRecord {
12
+ readonly refs: readonly RefRecord[];
13
+ }
14
+ /** Bumped only if this shape changes incompatibly (never for adding an
15
+ * optional field — the codec already tolerates unknown keys). */
16
+ export declare const REFS_VERSION = 1;
17
+ /** The sidecar's on-disk JSON form. Trailing newline for a clean git diff. */
18
+ export declare const serializeRefs: (record: RefsRecord) => string;
19
+ /**
20
+ * Read a sidecar's `RefsRecord` back, or `null` if it's missing/corrupt/
21
+ * malformed/hand-edited/merge-conflicted — this must NEVER throw, mirroring
22
+ * `StampStore.ts`'s `parseStamp`: an untrustworthy record reads as no
23
+ * recorded references at all, not a crash.
24
+ */
25
+ export declare const parseRefs: (content: string) => RefsRecord | null;
26
+ /**
27
+ * `<base>/docs/architecture.md` -> `<metaRoot>/refs/docs/architecture.md.json`.
28
+ * Throws if `docAtPath` isn't under `base`, mirroring `sidecarPathFor`'s own
29
+ * rule (a caller bug, not a runtime data condition).
30
+ */
31
+ export declare const refsSidecarPathFor: (docAtPath: string, layout: MetaLayout) => string;
32
+ //# sourceMappingURL=RefStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RefStore.d.ts","sourceRoot":"","sources":["../../../src/core/links/RefStore.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAG/C;;0EAE0E;AAC1E,eAAO,MAAM,QAAQ,SAAS,CAAA;AAE9B,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,CAAA;CACpC;AAED;iEACiE;AACjE,eAAO,MAAM,YAAY,IAAI,CAAA;AAc7B,8EAA8E;AAC9E,eAAO,MAAM,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,MAA4B,CAAA;AAEhF;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAsB,CAAA;AAEhF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,cAAe,MAAM,UAAU,UAAU,KAAG,MAC9B,CAAA"}
@@ -0,0 +1,60 @@
1
+ // Sidecar tracking for issue #39 Scenario I ("this doc's claim about a
2
+ // referenced target may be stale, even though the link isn't broken"):
3
+ // records the content hash of each real reference (a cross-file/cross-
4
+ // hierarchy link target) a doc makes, at stamp time, so a LATER change to
5
+ // the target's content — the target still exists, the link still resolves —
6
+ // can be surfaced as drift, distinct from `MarkdownLinks.ts`'s "broken."
7
+ //
8
+ // Own path namespace, `.cairn/refs/**`, via `../sidecar.ts`'s `namespace`
9
+ // parameter — NOT the raw doc path directly under `.cairn/`. That was the
10
+ // first design (reasoning: `docs/architecture.md` never had a sidecar of its
11
+ // own, so `.cairn/docs/architecture.md.json` looked free) — wrong in
12
+ // general and caught by construction while dogfooding: `docs/_SUMMARY.md`
13
+ // is ALSO a real `.md` file `stampRefs` scans (it links to every child doc,
14
+ // including `architecture.md`), and its OWN freshness sidecar already lives
15
+ // at exactly `.cairn/docs/_SUMMARY.md.json` — writing a refs record there
16
+ // silently clobbered it. Every summary-tree node (`X.summary.md`,
17
+ // `_SUMMARY.md`) is simultaneously a scannable doc, so the two concerns need
18
+ // genuinely disjoint path spaces, not "usually disjoint."
19
+ //
20
+ // Deliberately a SEPARATE record shape from `StampStore.ts`'s `StampRecord`,
21
+ // not a shared one: a source doc's sidecar has no summary-freshness hash to
22
+ // carry, and a summary's sidecar (today) carries no references — forcing
23
+ // one schema to serve both would leave one half always meaningless for any
24
+ // given sidecar. Same lenient-decode philosophy as `StampStore.ts` (both
25
+ // build on `../sidecar.ts`'s codec): unknown keys are ignored, and a
26
+ // corrupt/hand-edited sidecar reads as absent, never throws.
27
+ import { Schema } from 'effect';
28
+ import { makeSidecarCodec, sidecarPathFor } from '../sidecar.js';
29
+ /** The subdirectory, under `metaRoot`, every reference sidecar lives under —
30
+ * disjoint from the summary-freshness sidecars living directly under
31
+ * `metaRoot` (see module header: this is load-bearing, not stylistic). */
32
+ export const REFS_DIR = 'refs';
33
+ /** Bumped only if this shape changes incompatibly (never for adding an
34
+ * optional field — the codec already tolerates unknown keys). */
35
+ export const REFS_VERSION = 1;
36
+ const RefRecordSchema = Schema.Struct({
37
+ anchor: Schema.optionalKey(Schema.String),
38
+ hash: Schema.String,
39
+ target: Schema.String,
40
+ });
41
+ const RefsRecordSchema = Schema.Struct({
42
+ refs: Schema.Array(RefRecordSchema),
43
+ });
44
+ const refsCodec = makeSidecarCodec(RefsRecordSchema);
45
+ /** The sidecar's on-disk JSON form. Trailing newline for a clean git diff. */
46
+ export const serializeRefs = refsCodec.serialize;
47
+ /**
48
+ * Read a sidecar's `RefsRecord` back, or `null` if it's missing/corrupt/
49
+ * malformed/hand-edited/merge-conflicted — this must NEVER throw, mirroring
50
+ * `StampStore.ts`'s `parseStamp`: an untrustworthy record reads as no
51
+ * recorded references at all, not a crash.
52
+ */
53
+ export const parseRefs = refsCodec.parse;
54
+ /**
55
+ * `<base>/docs/architecture.md` -> `<metaRoot>/refs/docs/architecture.md.json`.
56
+ * Throws if `docAtPath` isn't under `base`, mirroring `sidecarPathFor`'s own
57
+ * rule (a caller bug, not a runtime data condition).
58
+ */
59
+ export const refsSidecarPathFor = (docAtPath, layout) => sidecarPathFor(docAtPath, layout, REFS_DIR);
60
+ //# sourceMappingURL=RefStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RefStore.js","sourceRoot":"","sources":["../../../src/core/links/RefStore.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,uEAAuE;AACvE,uEAAuE;AACvE,0EAA0E;AAC1E,4EAA4E;AAC5E,yEAAyE;AACzE,EAAE;AACF,0EAA0E;AAC1E,0EAA0E;AAC1E,6EAA6E;AAC7E,qEAAqE;AACrE,0EAA0E;AAC1E,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,kEAAkE;AAClE,6EAA6E;AAC7E,0DAA0D;AAC1D,EAAE;AACF,6EAA6E;AAC7E,4EAA4E;AAC5E,yEAAyE;AACzE,2EAA2E;AAC3E,yEAAyE;AACzE,qEAAqE;AACrE,6DAA6D;AAE7D,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAG/B,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAEhE;;0EAE0E;AAC1E,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAA;AAY9B;iEACiE;AACjE,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAA;AAE7B,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,CAAC;IACzC,IAAI,EAAE,MAAM,CAAC,MAAM;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM;CACtB,CAAC,CAAA;AAEF,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC;CACpC,CAAC,CAAA;AAEF,MAAM,SAAS,GAAG,gBAAgB,CAAC,gBAAgB,CAAC,CAAA;AAEpD,8EAA8E;AAC9E,MAAM,CAAC,MAAM,aAAa,GAAmC,SAAS,CAAC,SAAS,CAAA;AAEhF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAA2C,SAAS,CAAC,KAAK,CAAA;AAEhF;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,SAAiB,EAAE,MAAkB,EAAU,EAAE,CAClF,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAA"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Replace every character of each fenced code block — opening and closing
3
+ * marker lines included — with a space, keeping line count/newlines intact.
4
+ * An unclosed fence masks to end of document (matches the prior regex's own
5
+ * behaviour, which also never matched — and so never masked — an unclosed
6
+ * fence... this instead fails safe: unterminated is treated as "still in
7
+ * code," not leaking a stray marker line as real content).
8
+ */
9
+ export declare const maskFencedCode: (content: string) => string;
10
+ //# sourceMappingURL=markdownFences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdownFences.d.ts","sourceRoot":"","sources":["../../../src/core/links/markdownFences.ts"],"names":[],"mappings":"AAwBA;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,YAAa,MAAM,KAAG,MAoBhD,CAAA"}
@@ -0,0 +1,51 @@
1
+ // Fenced (``` / ~~~) code block masking — a provably linear line scan, not a
2
+ // single regex. The prior single-regex form
3
+ // (`/(^|\n)[ \t]*(```|~~~)[\s\S]*?\n[ \t]*\2[ \t]*(?=\n|$)/g`) is a genuine
4
+ // CodeQL js/polynomial-redos finding: a lazy `[\s\S]*?` hunting for a
5
+ // backreferenced closing marker is O(n^2) on adversarial input (many
6
+ // unclosed or repeated fence markers) — exactly the kind of untrusted
7
+ // document content `cairn check` runs over in CI. Shared by
8
+ // MarkdownLinks.ts (stripCode) and Anchors.ts (heading extraction must not
9
+ // misparse a heading-shaped line inside a fence).
10
+ const fenceOpenMarker = (line) => {
11
+ const trimmed = line.trimStart();
12
+ if (trimmed.startsWith('```')) {
13
+ return '```';
14
+ }
15
+ if (trimmed.startsWith('~~~')) {
16
+ return '~~~';
17
+ }
18
+ return null;
19
+ };
20
+ /** A closing fence line is the marker alone, with only surrounding whitespace. */
21
+ const isFenceClose = (line, marker) => line.trim() === marker;
22
+ /**
23
+ * Replace every character of each fenced code block — opening and closing
24
+ * marker lines included — with a space, keeping line count/newlines intact.
25
+ * An unclosed fence masks to end of document (matches the prior regex's own
26
+ * behaviour, which also never matched — and so never masked — an unclosed
27
+ * fence... this instead fails safe: unterminated is treated as "still in
28
+ * code," not leaking a stray marker line as real content).
29
+ */
30
+ export const maskFencedCode = (content) => {
31
+ const lines = content.split('\n');
32
+ let i = 0;
33
+ while (i < lines.length) {
34
+ const marker = fenceOpenMarker(lines[i] ?? '');
35
+ if (marker === null) {
36
+ i += 1;
37
+ continue;
38
+ }
39
+ let j = i + 1;
40
+ while (j < lines.length && !isFenceClose(lines[j] ?? '', marker)) {
41
+ j += 1;
42
+ }
43
+ const end = j < lines.length ? j : lines.length - 1;
44
+ for (let k = i; k <= end; k += 1) {
45
+ lines[k] = ' '.repeat((lines[k] ?? '').length);
46
+ }
47
+ i = end + 1;
48
+ }
49
+ return lines.join('\n');
50
+ };
51
+ //# sourceMappingURL=markdownFences.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdownFences.js","sourceRoot":"","sources":["../../../src/core/links/markdownFences.ts"],"names":[],"mappings":"AAAA,6EAA6E;AAC7E,4CAA4C;AAC5C,4EAA4E;AAC5E,sEAAsE;AACtE,qEAAqE;AACrE,sEAAsE;AACtE,4DAA4D;AAC5D,2EAA2E;AAC3E,kDAAkD;AAElD,MAAM,eAAe,GAAG,CAAC,IAAY,EAAwB,EAAE;IAC7D,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,CAAA;IAChC,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAA;IACd,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED,kFAAkF;AAClF,MAAM,YAAY,GAAG,CAAC,IAAY,EAAE,MAAc,EAAW,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM,CAAA;AAEtF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAAe,EAAU,EAAE;IACxD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACjC,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;QAC9C,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,CAAC,IAAI,CAAC,CAAA;YACN,SAAQ;QACV,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACb,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC;YACjE,CAAC,IAAI,CAAC,CAAA;QACR,CAAC;QACD,MAAM,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;QACnD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAA;QAChD,CAAC;QACD,CAAC,GAAG,GAAG,GAAG,CAAC,CAAA;IACb,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC,CAAA"}
@@ -1,3 +1,11 @@
1
1
  /** Convert an OS path to POSIX form (`\` -> `/`). */
2
2
  export declare const toPosix: (p: string) => string;
3
+ /**
4
+ * True when `candidate` resolves inside `base` (or equals it). The same
5
+ * containment check `sidecar.ts`'s `sidecarPathFor`/`nodePathForSidecar`
6
+ * use for sidecar paths — non-throwing here since callers (link-checking's
7
+ * out-of-hierarchy targets, issue #39) need a boolean to decide "cannot
8
+ * verify" from, not a programming-error signal.
9
+ */
10
+ export declare const isWithinBase: (candidate: string, base: string) => boolean;
3
11
  //# sourceMappingURL=paths.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAIA,qDAAqD;AACrD,eAAO,MAAM,OAAO,GAAI,GAAG,MAAM,KAAG,MAAiC,CAAA"}
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAQA,qDAAqD;AACrD,eAAO,MAAM,OAAO,MAAO,MAAM,KAAG,MAAiC,CAAA;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,cAAe,MAAM,QAAQ,MAAM,KAAG,OAG9D,CAAA"}
@@ -1,6 +1,19 @@
1
1
  // Path normalisation. The pure planners reason in POSIX (`/`) paths so behaviour
2
2
  // is identical on every OS; the IO layer normalises real filesystem paths (which
3
3
  // may use `\` on Windows) to POSIX before they reach the core.
4
+ import * as nodePath from 'node:path';
5
+ const path = nodePath.posix;
4
6
  /** Convert an OS path to POSIX form (`\` -> `/`). */
5
7
  export const toPosix = (p) => p.replaceAll('\\', '/');
8
+ /**
9
+ * True when `candidate` resolves inside `base` (or equals it). The same
10
+ * containment check `sidecar.ts`'s `sidecarPathFor`/`nodePathForSidecar`
11
+ * use for sidecar paths — non-throwing here since callers (link-checking's
12
+ * out-of-hierarchy targets, issue #39) need a boolean to decide "cannot
13
+ * verify" from, not a programming-error signal.
14
+ */
15
+ export const isWithinBase = (candidate, base) => {
16
+ const rel = path.relative(base, candidate);
17
+ return !rel.startsWith('..') && !path.isAbsolute(rel);
18
+ };
6
19
  //# sourceMappingURL=paths.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,iFAAiF;AACjF,+DAA+D;AAE/D,qDAAqD;AACrD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA"}
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,iFAAiF;AACjF,+DAA+D;AAE/D,OAAO,KAAK,QAAQ,MAAM,WAAW,CAAA;AAErC,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAA;AAE3B,qDAAqD;AACrD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;AAErE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,SAAiB,EAAE,IAAY,EAAW,EAAE;IACvE,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;IAC1C,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;AACvD,CAAC,CAAA"}