@sledorze/cairn 0.2.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 +37 -1
- package/dist/cli.js +1011 -526
- package/dist/cli.js.map +1 -1
- package/dist/core/Config.d.ts +1 -1
- package/dist/core/Config.d.ts.map +1 -1
- package/dist/core/Config.js +1 -1
- package/dist/core/Config.js.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/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} +0 -2
- package/dist/core/summaries/DocSummaries.d.ts.map +1 -0
- package/dist/core/{DocSummaries.js → summaries/DocSummaries.js} +6 -8
- 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} +2 -1
- package/dist/core/summaries/SummaryTree.bench.js.map +1 -0
- package/dist/core/summaries/SummaryTree.d.ts.map +1 -0
- package/dist/core/{SummaryTree.js → summaries/SummaryTree.js} +4 -3
- 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 +5 -5
- package/dist/index.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/summaries/CheckSummaries.bench.d.ts.map +1 -0
- package/dist/program/{CheckSummaries.bench.js → summaries/CheckSummaries.bench.js} +6 -4
- package/dist/program/summaries/CheckSummaries.bench.js.map +1 -0
- package/dist/program/{CheckSummaries.d.ts → summaries/CheckSummaries.d.ts} +7 -7
- package/dist/program/summaries/CheckSummaries.d.ts.map +1 -0
- package/dist/program/{CheckSummaries.js → summaries/CheckSummaries.js} +13 -12
- package/dist/program/summaries/CheckSummaries.js.map +1 -0
- package/package.json +3 -2
- 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/StampStore.d.ts +0 -47
- package/dist/core/StampStore.d.ts.map +0 -1
- package/dist/core/StampStore.js +0 -90
- package/dist/core/StampStore.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.bench.d.ts.map +0 -1
- package/dist/program/CheckSummaries.bench.js.map +0 -1
- package/dist/program/CheckSummaries.d.ts.map +0 -1
- 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/{MarkdownLinks.bench.js → links/MarkdownLinks.bench.js} +0 -0
- /package/dist/core/{SummaryTree.bench.d.ts → summaries/SummaryTree.bench.d.ts} +0 -0
- /package/dist/core/{SummaryTree.d.ts → summaries/SummaryTree.d.ts} +0 -0
- /package/dist/program/{CheckSummaries.bench.d.ts → summaries/CheckSummaries.bench.d.ts} +0 -0
|
@@ -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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
/** The hidden directory name every metadata tree lives under, relative to `base`. */
|
|
3
|
+
export declare const META_DIR = ".cairn";
|
|
4
|
+
export interface MetaLayout {
|
|
5
|
+
/** The project root every node path and every sidecar is resolved under. */
|
|
6
|
+
readonly base: string;
|
|
7
|
+
/** `join(base, META_DIR)` — passed explicitly (not recomputed) so callers who
|
|
8
|
+
* already have it (e.g. after listing `.cairn/**`) don't rederive it. */
|
|
9
|
+
readonly metaRoot: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const metaRootFor: (base: string) => string;
|
|
12
|
+
/** True for any path inside the hidden metadata tree — used to keep `.cairn/**`
|
|
13
|
+
* out of the set of markdown source files a plan considers. */
|
|
14
|
+
export declare const isSidecarPath: (candidate: string, metaRoot: string) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* `<base>/docs/a.summary.md` -> `<metaRoot>/docs/a.summary.md.json`, or, with
|
|
17
|
+
* a `namespace` (e.g. `'refs'`), `<metaRoot>/refs/docs/a.summary.md.json` —
|
|
18
|
+
* the mechanism that keeps two DIFFERENT sidecar kinds for the SAME doc path
|
|
19
|
+
* from colliding (a doc's own freshness stamp vs its reference hashes; see
|
|
20
|
+
* `links/RefStore.ts`'s header for the real collision this closes). Throws
|
|
21
|
+
* if `nodeAtPath` isn't under `base`: every real node path is (roots are
|
|
22
|
+
* always resolved under the project root before a plan ever runs), so this
|
|
23
|
+
* signals a caller bug, not a runtime data condition to recover from.
|
|
24
|
+
*/
|
|
25
|
+
export declare const sidecarPathFor: (nodeAtPath: string, layout: MetaLayout, namespace?: string) => string;
|
|
26
|
+
/**
|
|
27
|
+
* The inverse of `sidecarPathFor` (namespace-free form): given a sidecar's
|
|
28
|
+
* own path (e.g. found by listing `.cairn/**`), recover the node path it
|
|
29
|
+
* mirrors. Returns `null` for a path that isn't a well-formed sidecar under
|
|
30
|
+
* `metaRoot` (not `.json`, or outside the tree) rather than throwing —
|
|
31
|
+
* unlike `sidecarPathFor`, this reads data that could plausibly be malformed
|
|
32
|
+
* (a stray non-JSON file dropped into `.cairn/` by hand), so `null` is the
|
|
33
|
+
* right "not a sidecar" signal, not a crash.
|
|
34
|
+
*/
|
|
35
|
+
export declare const nodePathForSidecar: (sidecarPath: string, layout: MetaLayout) => string | null;
|
|
36
|
+
export interface SidecarCodec<A> {
|
|
37
|
+
/** Read a sidecar's record back, or `null` if missing/corrupt/malformed —
|
|
38
|
+
* NEVER throws (see module header). */
|
|
39
|
+
readonly parse: (content: string) => A | null;
|
|
40
|
+
/** The sidecar's on-disk JSON form. Trailing newline for a clean git diff. */
|
|
41
|
+
readonly serialize: (record: A) => string;
|
|
42
|
+
}
|
|
43
|
+
/** Build a lenient JSON codec for one sidecar record shape from its `Schema`. */
|
|
44
|
+
export declare const makeSidecarCodec: <S extends Parameters<typeof Schema.decodeUnknownResult>[0]>(schema: S) => SidecarCodec<S['Type']>;
|
|
45
|
+
//# sourceMappingURL=sidecar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../../src/core/sidecar.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAU,MAAM,EAAE,MAAM,QAAQ,CAAA;AAKvC,qFAAqF;AACrF,eAAO,MAAM,QAAQ,WAAW,CAAA;AAEhC,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;6EACyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,eAAO,MAAM,WAAW,SAAU,MAAM,KAAG,MAAmC,CAAA;AAE9E;+DAC+D;AAC/D,eAAO,MAAM,aAAa,cAAe,MAAM,YAAY,MAAM,KAAG,OACJ,CAAA;AAEhE;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,eAAgB,MAAM,UAAU,UAAU,cAAc,MAAM,KAAG,MAO3F,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,gBAAiB,MAAM,UAAU,UAAU,KAAG,MAAM,GAAG,IASrF,CAAA;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B;2CACuC;IACvC,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,CAAC,GAAG,IAAI,CAAA;IAC7C,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAA;CAC1C;AAED,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,UAAU,CAAC,OAAO,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,UACjF,CAAC,KACR,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAexB,CAAA"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// The shared low-level primitive under every hidden `.cairn/**` metadata
|
|
2
|
+
// sidecar: path mapping (a node's real path -> its sidecar's path, and back)
|
|
3
|
+
// and a lenient JSON codec factory. Used by BOTH `summaries/StampStore.ts`
|
|
4
|
+
// (doc-freshness records) and `links/RefStore.ts` (reference content-hash
|
|
5
|
+
// records) — genuinely shared mechanics, kept deliberately separate from
|
|
6
|
+
// either domain's own record shape (`StampRecord` vs `RefsRecord`), which
|
|
7
|
+
// stay domain-owned: forcing one schema to serve two different sidecar
|
|
8
|
+
// kinds would leave one half always meaningless for any given sidecar.
|
|
9
|
+
//
|
|
10
|
+
// Two invariants this module enforces:
|
|
11
|
+
// - every node path handed to `sidecarPathFor` must live under `base` (real
|
|
12
|
+
// usage: `base` is the project root, every root/node resolved under it) —
|
|
13
|
+
// violating this is a programming error, not a data error, so it throws;
|
|
14
|
+
// - a codec's `parse` decodes LENIENTLY: unknown keys are ignored rather
|
|
15
|
+
// than rejected (forward-compatible), and a corrupt/hand-edited/merge-
|
|
16
|
+
// conflicted sidecar reads as `null` — exactly equivalent to no record at
|
|
17
|
+
// all, never a crash.
|
|
18
|
+
import * as nodePath from 'node:path';
|
|
19
|
+
import { Result, Schema } from 'effect';
|
|
20
|
+
// POSIX path semantics so sidecar paths are identical on every OS.
|
|
21
|
+
const path = nodePath.posix;
|
|
22
|
+
/** The hidden directory name every metadata tree lives under, relative to `base`. */
|
|
23
|
+
export const META_DIR = '.cairn';
|
|
24
|
+
export const metaRootFor = (base) => path.join(base, META_DIR);
|
|
25
|
+
/** True for any path inside the hidden metadata tree — used to keep `.cairn/**`
|
|
26
|
+
* out of the set of markdown source files a plan considers. */
|
|
27
|
+
export const isSidecarPath = (candidate, metaRoot) => candidate === metaRoot || candidate.startsWith(`${metaRoot}/`);
|
|
28
|
+
/**
|
|
29
|
+
* `<base>/docs/a.summary.md` -> `<metaRoot>/docs/a.summary.md.json`, or, with
|
|
30
|
+
* a `namespace` (e.g. `'refs'`), `<metaRoot>/refs/docs/a.summary.md.json` —
|
|
31
|
+
* the mechanism that keeps two DIFFERENT sidecar kinds for the SAME doc path
|
|
32
|
+
* from colliding (a doc's own freshness stamp vs its reference hashes; see
|
|
33
|
+
* `links/RefStore.ts`'s header for the real collision this closes). Throws
|
|
34
|
+
* if `nodeAtPath` isn't under `base`: every real node path is (roots are
|
|
35
|
+
* always resolved under the project root before a plan ever runs), so this
|
|
36
|
+
* signals a caller bug, not a runtime data condition to recover from.
|
|
37
|
+
*/
|
|
38
|
+
export const sidecarPathFor = (nodeAtPath, layout, namespace) => {
|
|
39
|
+
const rel = path.relative(layout.base, nodeAtPath);
|
|
40
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
41
|
+
throw new Error(`sidecarPathFor: "${nodeAtPath}" is not under base "${layout.base}"`);
|
|
42
|
+
}
|
|
43
|
+
const root = namespace === undefined ? layout.metaRoot : path.join(layout.metaRoot, namespace);
|
|
44
|
+
return `${path.join(root, rel)}.json`;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The inverse of `sidecarPathFor` (namespace-free form): given a sidecar's
|
|
48
|
+
* own path (e.g. found by listing `.cairn/**`), recover the node path it
|
|
49
|
+
* mirrors. Returns `null` for a path that isn't a well-formed sidecar under
|
|
50
|
+
* `metaRoot` (not `.json`, or outside the tree) rather than throwing —
|
|
51
|
+
* unlike `sidecarPathFor`, this reads data that could plausibly be malformed
|
|
52
|
+
* (a stray non-JSON file dropped into `.cairn/` by hand), so `null` is the
|
|
53
|
+
* right "not a sidecar" signal, not a crash.
|
|
54
|
+
*/
|
|
55
|
+
export const nodePathForSidecar = (sidecarPath, layout) => {
|
|
56
|
+
if (!isSidecarPath(sidecarPath, layout.metaRoot) || !sidecarPath.endsWith('.json')) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const rel = path.relative(layout.metaRoot, sidecarPath);
|
|
60
|
+
if (rel.startsWith('..')) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return path.join(layout.base, rel.slice(0, -'.json'.length));
|
|
64
|
+
};
|
|
65
|
+
/** Build a lenient JSON codec for one sidecar record shape from its `Schema`. */
|
|
66
|
+
export const makeSidecarCodec = (schema) => {
|
|
67
|
+
const decode = Schema.decodeUnknownResult(schema);
|
|
68
|
+
return {
|
|
69
|
+
parse: (content) => {
|
|
70
|
+
let json;
|
|
71
|
+
try {
|
|
72
|
+
json = JSON.parse(content);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const decoded = decode(json);
|
|
78
|
+
return Result.isSuccess(decoded) ? decoded.success : null;
|
|
79
|
+
},
|
|
80
|
+
serialize: (record) => `${JSON.stringify(record, null, 2)}\n`,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=sidecar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar.js","sourceRoot":"","sources":["../../src/core/sidecar.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,8 +29,6 @@ export declare const isSummaryFile: (path: string, naming?: Naming) => boolean;
|
|
|
29
29
|
export declare const summaryPathFor: (path: string, naming?: Naming) => string;
|
|
30
30
|
/** A non-summary Markdown file longer than the threshold must have a summary. */
|
|
31
31
|
export declare const needsSummary: ({ lineCount, naming, path, thresholdLines, }: NeedsSummaryArgs) => boolean;
|
|
32
|
-
/** Deterministic content hash used to stamp and verify summaries. */
|
|
33
|
-
export declare const hashContent: (content: string) => string;
|
|
34
32
|
/** The HTML-comment stamp a summary must carry to declare which source it reflects. */
|
|
35
33
|
export declare const sourceHashTag: (hash: string) => string;
|
|
36
34
|
/** Read back the source hash recorded in a summary, or null if absent. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DocSummaries.d.ts","sourceRoot":"","sources":["../../../src/core/summaries/DocSummaries.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAAA;AAEtD,qDAAqD;AACrD,MAAM,WAAW,MAAM;IACrB,yDAAyD;IACzD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;CACnC;AAED,4DAA4D;AAC5D,eAAO,MAAM,cAAc,EAAE,MAG5B,CAAA;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IACpC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;CAChC;AAED,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAIzC,uDAAuD;AACvD,eAAO,MAAM,UAAU,YAAa,MAAM,KAAG,MAM5C,CAAA;AAED,uDAAuD;AACvD,eAAO,MAAM,aAAa,SAAU,MAAM,WAAU,MAAM,KAAoB,OACrC,CAAA;AAEzC,wEAAwE;AACxE,eAAO,MAAM,cAAc,SAAU,MAAM,WAAU,MAAM,KAAoB,MAC9B,CAAA;AAEjD,iFAAiF;AACjF,eAAO,MAAM,YAAY,iDAKtB,gBAAgB,KAAG,OAQrB,CAAA;AAED,uFAAuF;AACvF,eAAO,MAAM,aAAa,SAAU,MAAM,KAAG,MAA2C,CAAA;AAExF,0EAA0E;AAC1E,eAAO,MAAM,iBAAiB,mBAAoB,MAAM,KAAG,MAAM,GAAG,IAAiD,CAAA;AAErH;;;;0DAI0D;AAC1D,eAAO,MAAM,cAAc,mBAAoB,MAAM,QAAQ,MAAM,KAAG,MAMrE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,mBAAoB,MAAM,KAAG,MAKxD,CAAA;AAED,4EAA4E;AAC5E,eAAO,MAAM,eAAe,gDAAiD,YAAY,KAAG,aAQ3F,CAAA"}
|