@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.
- package/README.md +69 -18
- package/dist/cli.js +69378 -70958
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -10
- package/dist/config.js.map +1 -1
- package/dist/core/Config.d.ts +22 -22
- package/dist/core/Config.d.ts.map +1 -1
- package/dist/core/Config.js +33 -33
- package/dist/core/Config.js.map +1 -1
- package/dist/core/glob.bench.js +1 -1
- package/dist/core/glob.d.ts.map +1 -1
- package/dist/core/hashing.d.ts +3 -0
- package/dist/core/hashing.d.ts.map +1 -0
- package/dist/core/hashing.js +16 -0
- package/dist/core/hashing.js.map +1 -0
- package/dist/core/links/Anchors.d.ts +30 -0
- package/dist/core/links/Anchors.d.ts.map +1 -0
- package/dist/core/links/Anchors.js +119 -0
- package/dist/core/links/Anchors.js.map +1 -0
- package/dist/core/links/MarkdownLinks.bench.d.ts.map +1 -0
- package/dist/core/{MarkdownLinks.bench.js → links/MarkdownLinks.bench.js} +1 -1
- package/dist/core/links/MarkdownLinks.bench.js.map +1 -0
- package/dist/core/links/MarkdownLinks.d.ts +121 -0
- package/dist/core/links/MarkdownLinks.d.ts.map +1 -0
- package/dist/core/links/MarkdownLinks.js +228 -0
- package/dist/core/links/MarkdownLinks.js.map +1 -0
- package/dist/core/links/RefStore.d.ts +32 -0
- package/dist/core/links/RefStore.d.ts.map +1 -0
- package/dist/core/links/RefStore.js +60 -0
- package/dist/core/links/RefStore.js.map +1 -0
- package/dist/core/links/markdownFences.d.ts +10 -0
- package/dist/core/links/markdownFences.d.ts.map +1 -0
- package/dist/core/links/markdownFences.js +51 -0
- package/dist/core/links/markdownFences.js.map +1 -0
- package/dist/core/paths.d.ts +8 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +13 -0
- package/dist/core/paths.js.map +1 -1
- package/dist/core/sidecar.d.ts +45 -0
- package/dist/core/sidecar.d.ts.map +1 -0
- package/dist/core/sidecar.js +83 -0
- package/dist/core/sidecar.js.map +1 -0
- package/dist/core/{DocSummaries.d.ts → summaries/DocSummaries.d.ts} +13 -3
- package/dist/core/summaries/DocSummaries.d.ts.map +1 -0
- package/dist/core/{DocSummaries.js → summaries/DocSummaries.js} +31 -11
- package/dist/core/summaries/DocSummaries.js.map +1 -0
- package/dist/core/summaries/StampStore.d.ts +19 -0
- package/dist/core/summaries/StampStore.d.ts.map +1 -0
- package/dist/core/summaries/StampStore.js +34 -0
- package/dist/core/summaries/StampStore.js.map +1 -0
- package/dist/core/summaries/SummaryTree.bench.d.ts.map +1 -0
- package/dist/core/{SummaryTree.bench.js → summaries/SummaryTree.bench.js} +24 -14
- package/dist/core/summaries/SummaryTree.bench.js.map +1 -0
- package/dist/core/{SummaryTree.d.ts → summaries/SummaryTree.d.ts} +10 -1
- package/dist/core/summaries/SummaryTree.d.ts.map +1 -0
- package/dist/core/{SummaryTree.js → summaries/SummaryTree.js} +35 -9
- package/dist/core/summaries/SummaryTree.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -10
- package/dist/index.js.map +1 -1
- package/dist/init/content.d.ts +2 -2
- package/dist/init/content.d.ts.map +1 -1
- package/dist/init/content.js +45 -15
- package/dist/init/content.js.map +1 -1
- package/dist/init/generate.d.ts +1 -1
- package/dist/init/generate.d.ts.map +1 -1
- package/dist/init/generate.js +1 -1
- package/dist/io/DocsFs.d.ts +2 -3
- package/dist/io/DocsFs.d.ts.map +1 -1
- package/dist/io/DocsFs.js +15 -5
- package/dist/io/DocsFs.js.map +1 -1
- package/dist/program/JsonReport.d.ts +2 -2
- package/dist/program/JsonReport.d.ts.map +1 -1
- package/dist/program/JsonReport.js +2 -2
- package/dist/program/JsonReport.js.map +1 -1
- package/dist/program/{CheckLinks.d.ts → links/CheckLinks.d.ts} +9 -4
- package/dist/program/links/CheckLinks.d.ts.map +1 -0
- package/dist/program/links/CheckLinks.js +243 -0
- package/dist/program/links/CheckLinks.js.map +1 -0
- package/dist/program/links/CheckRefs.d.ts +47 -0
- package/dist/program/links/CheckRefs.d.ts.map +1 -0
- package/dist/program/links/CheckRefs.js +151 -0
- package/dist/program/links/CheckRefs.js.map +1 -0
- package/dist/program/locale.d.ts.map +1 -1
- package/dist/program/summaries/CheckSummaries.bench.d.ts +2 -0
- package/dist/program/summaries/CheckSummaries.bench.d.ts.map +1 -0
- package/dist/program/summaries/CheckSummaries.bench.js +94 -0
- package/dist/program/summaries/CheckSummaries.bench.js.map +1 -0
- package/dist/program/summaries/CheckSummaries.d.ts +58 -0
- package/dist/program/summaries/CheckSummaries.d.ts.map +1 -0
- package/dist/program/summaries/CheckSummaries.js +302 -0
- package/dist/program/summaries/CheckSummaries.js.map +1 -0
- package/package.json +7 -8
- package/schema/cairn.schema.json +35 -38
- package/dist/core/DocSummaries.d.ts.map +0 -1
- package/dist/core/DocSummaries.js.map +0 -1
- package/dist/core/MarkdownLinks.bench.d.ts.map +0 -1
- package/dist/core/MarkdownLinks.bench.js.map +0 -1
- package/dist/core/MarkdownLinks.d.ts +0 -51
- package/dist/core/MarkdownLinks.d.ts.map +0 -1
- package/dist/core/MarkdownLinks.js +0 -115
- package/dist/core/MarkdownLinks.js.map +0 -1
- package/dist/core/SummaryTree.bench.d.ts.map +0 -1
- package/dist/core/SummaryTree.bench.js.map +0 -1
- package/dist/core/SummaryTree.d.ts.map +0 -1
- package/dist/core/SummaryTree.js.map +0 -1
- package/dist/program/CheckLinks.d.ts.map +0 -1
- package/dist/program/CheckLinks.js +0 -122
- package/dist/program/CheckLinks.js.map +0 -1
- package/dist/program/CheckSummaries.d.ts +0 -38
- package/dist/program/CheckSummaries.d.ts.map +0 -1
- package/dist/program/CheckSummaries.js +0 -198
- package/dist/program/CheckSummaries.js.map +0 -1
- /package/dist/core/{MarkdownLinks.bench.d.ts → links/MarkdownLinks.bench.d.ts} +0 -0
- /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
|
|
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":"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"}
|
|
@@ -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"}
|
package/dist/core/paths.d.ts
CHANGED
|
@@ -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
|
package/dist/core/paths.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/core/paths.js
CHANGED
|
@@ -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
|
package/dist/core/paths.js.map
CHANGED
|
@@ -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"}
|