@nielspeter/eess-md 0.1.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/builders/docs.d.ts +28 -0
  4. package/dist/builders/docs.d.ts.map +1 -0
  5. package/dist/builders/docs.js +44 -0
  6. package/dist/builders/docs.js.map +1 -0
  7. package/dist/builders/links.d.ts +26 -0
  8. package/dist/builders/links.d.ts.map +1 -0
  9. package/dist/builders/links.js +42 -0
  10. package/dist/builders/links.js.map +1 -0
  11. package/dist/builders/pointers.d.ts +26 -0
  12. package/dist/builders/pointers.d.ts.map +1 -0
  13. package/dist/builders/pointers.js +43 -0
  14. package/dist/builders/pointers.js.map +1 -0
  15. package/dist/builders/rows.d.ts +36 -0
  16. package/dist/builders/rows.d.ts.map +1 -0
  17. package/dist/builders/rows.js +42 -0
  18. package/dist/builders/rows.js.map +1 -0
  19. package/dist/builders/task-items.d.ts +21 -0
  20. package/dist/builders/task-items.d.ts.map +1 -0
  21. package/dist/builders/task-items.js +33 -0
  22. package/dist/builders/task-items.js.map +1 -0
  23. package/dist/builders/vocabulary.d.ts +66 -0
  24. package/dist/builders/vocabulary.d.ts.map +1 -0
  25. package/dist/builders/vocabulary.js +118 -0
  26. package/dist/builders/vocabulary.js.map +1 -0
  27. package/dist/conditions/pointer-resolve.d.ts +51 -0
  28. package/dist/conditions/pointer-resolve.d.ts.map +1 -0
  29. package/dist/conditions/pointer-resolve.js +164 -0
  30. package/dist/conditions/pointer-resolve.js.map +1 -0
  31. package/dist/conditions/resolve.d.ts +36 -0
  32. package/dist/conditions/resolve.d.ts.map +1 -0
  33. package/dist/conditions/resolve.js +108 -0
  34. package/dist/conditions/resolve.js.map +1 -0
  35. package/dist/conditions/structure.d.ts +13 -0
  36. package/dist/conditions/structure.d.ts.map +1 -0
  37. package/dist/conditions/structure.js +42 -0
  38. package/dist/conditions/structure.js.map +1 -0
  39. package/dist/conditions/table-rows.d.ts +28 -0
  40. package/dist/conditions/table-rows.d.ts.map +1 -0
  41. package/dist/conditions/table-rows.js +25 -0
  42. package/dist/conditions/table-rows.js.map +1 -0
  43. package/dist/corpus.d.ts +40 -0
  44. package/dist/corpus.d.ts.map +1 -0
  45. package/dist/corpus.js +73 -0
  46. package/dist/corpus.js.map +1 -0
  47. package/dist/index.d.ts +26 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +22 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/model/document.d.ts +78 -0
  52. package/dist/model/document.d.ts.map +1 -0
  53. package/dist/model/document.js +68 -0
  54. package/dist/model/document.js.map +1 -0
  55. package/dist/model/links.d.ts +32 -0
  56. package/dist/model/links.d.ts.map +1 -0
  57. package/dist/model/links.js +53 -0
  58. package/dist/model/links.js.map +1 -0
  59. package/dist/model/pointers.d.ts +29 -0
  60. package/dist/model/pointers.d.ts.map +1 -0
  61. package/dist/model/pointers.js +65 -0
  62. package/dist/model/pointers.js.map +1 -0
  63. package/dist/model/query.d.ts +6 -0
  64. package/dist/model/query.d.ts.map +1 -0
  65. package/dist/model/query.js +15 -0
  66. package/dist/model/query.js.map +1 -0
  67. package/dist/model/rows.d.ts +52 -0
  68. package/dist/model/rows.d.ts.map +1 -0
  69. package/dist/model/rows.js +42 -0
  70. package/dist/model/rows.js.map +1 -0
  71. package/dist/model/task-items.d.ts +28 -0
  72. package/dist/model/task-items.d.ts.map +1 -0
  73. package/dist/model/task-items.js +47 -0
  74. package/dist/model/task-items.js.map +1 -0
  75. package/dist/model/violation.d.ts +23 -0
  76. package/dist/model/violation.d.ts.map +1 -0
  77. package/dist/model/violation.js +22 -0
  78. package/dist/model/violation.js.map +1 -0
  79. package/dist/predicates/document.d.ts +8 -0
  80. package/dist/predicates/document.d.ts.map +1 -0
  81. package/dist/predicates/document.js +22 -0
  82. package/dist/predicates/document.js.map +1 -0
  83. package/dist/rules/adr.d.ts +34 -0
  84. package/dist/rules/adr.d.ts.map +1 -0
  85. package/dist/rules/adr.js +103 -0
  86. package/dist/rules/adr.js.map +1 -0
  87. package/dist/rules/ledger.d.ts +44 -0
  88. package/dist/rules/ledger.d.ts.map +1 -0
  89. package/dist/rules/ledger.js +119 -0
  90. package/dist/rules/ledger.js.map +1 -0
  91. package/package.json +60 -0
@@ -0,0 +1,118 @@
1
+ import picomatch from 'picomatch';
2
+ import { RuleBuilder } from '@nielspeter/eess';
3
+ const defaultNormalize = (value) => value.trim();
4
+ /** Derive a controlled vocabulary from the corpus. */
5
+ export function vocabulary(corpus, options) {
6
+ const normalize = options.normalize ?? defaultNormalize;
7
+ const terms = new Set();
8
+ if (options.fromFolders !== undefined) {
9
+ const matches = picomatch(options.fromFolders);
10
+ const dirs = new Set();
11
+ for (const rel of corpus.fileIndex) {
12
+ const parts = rel.split('/');
13
+ for (let i = 1; i < parts.length; i++) {
14
+ dirs.add(parts.slice(0, i).join('/'));
15
+ }
16
+ }
17
+ for (const dir of dirs) {
18
+ if (matches(dir)) {
19
+ const base = dir.split('/').pop();
20
+ if (base !== undefined && base !== '')
21
+ terms.add(normalize(base));
22
+ }
23
+ }
24
+ }
25
+ if (options.fromHeadings !== undefined) {
26
+ const { files, depth } = options.fromHeadings;
27
+ const inFiles = picomatch(files);
28
+ for (const doc of corpus.documents()) {
29
+ if (!inFiles(doc.relPath))
30
+ continue;
31
+ for (const section of doc.sections) {
32
+ if (depth !== undefined && section.depth !== depth)
33
+ continue;
34
+ terms.add(normalize(section.name));
35
+ }
36
+ }
37
+ }
38
+ for (const t of options.terms ?? [])
39
+ terms.add(normalize(t));
40
+ return { terms, normalize };
41
+ }
42
+ // Blank out fenced code (line-preserving) so example prose never yields terms.
43
+ const FENCE_RE = /(```|~~~)[\s\S]*?\1/g;
44
+ function stripFencedCode(s) {
45
+ return s.replace(FENCE_RE, (m) => '\n'.repeat((m.match(/\n/g) ?? []).length));
46
+ }
47
+ const defaultValue = (rest) => rest
48
+ .replace(/[*_`]/g, '')
49
+ .trim()
50
+ .replace(/\s+[·—–|].*$/, '')
51
+ .trim();
52
+ function collectTerms(corpus, options) {
53
+ const extract = options.value ?? defaultValue;
54
+ // Non-global copy: a caller's /g regex would carry lastIndex across lines.
55
+ const label = new RegExp(options.label.source, options.label.flags.replace(/g/g, ''));
56
+ const out = [];
57
+ for (const doc of corpus.documents()) {
58
+ const lines = stripFencedCode(doc.text).split('\n');
59
+ for (let i = 0; i < lines.length; i++) {
60
+ const line = lines[i] ?? '';
61
+ const m = label.exec(line);
62
+ if (!m)
63
+ continue;
64
+ const raw = line.slice(m.index + m[0].length);
65
+ const value = extract(raw);
66
+ if (value !== '')
67
+ out.push({ value, raw, doc, line: i + 1 });
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+ const resideInFile = (glob) => {
73
+ const matches = picomatch(glob);
74
+ return {
75
+ description: `reside in file '${glob}'`,
76
+ test: (t) => matches(t.doc.relPath),
77
+ };
78
+ };
79
+ const resolveAgainst = (vocab) => ({
80
+ description: 'resolve against the vocabulary',
81
+ evaluate: (elements, ctx) => elements
82
+ .filter((t) => !vocab.terms.has(vocab.normalize(t.value)))
83
+ .map((t) => ({
84
+ rule: ctx.rule,
85
+ ...(ctx.ruleId !== undefined && { ruleId: ctx.ruleId }),
86
+ element: `${t.doc.relPath}:${t.line}`,
87
+ file: t.doc.file,
88
+ line: t.line,
89
+ message: `'${t.value}' does not resolve against the vocabulary (${vocab.terms.size} terms)`,
90
+ ...(ctx.because !== undefined && { because: ctx.because }),
91
+ ...(ctx.suggestion !== undefined && { suggestion: ctx.suggestion }),
92
+ ...(ctx.docs !== undefined && { docs: ctx.docs }),
93
+ })),
94
+ });
95
+ /** Rule builder over labeled named references in the corpus's prose. */
96
+ export class TermRuleBuilder extends RuleBuilder {
97
+ opts;
98
+ constructor(corpus, opts) {
99
+ super(corpus);
100
+ this.opts = opts;
101
+ }
102
+ getElements() {
103
+ return collectTerms(this.project, this.opts);
104
+ }
105
+ /** Filter to references in documents whose path matches the glob. */
106
+ resideInFile(glob) {
107
+ return this.addPredicate(resideInFile(glob));
108
+ }
109
+ /** Assert every reference's value is a term of the vocabulary. */
110
+ resolveAgainst(vocab) {
111
+ return this.addCondition(resolveAgainst(vocab));
112
+ }
113
+ }
114
+ /** Entry point: build rules over labeled named references (`Label: Value` lines). */
115
+ export function terms(corpus, options) {
116
+ return new TermRuleBuilder(corpus, options);
117
+ }
118
+ //# sourceMappingURL=vocabulary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/builders/vocabulary.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { Condition } from '@nielspeter/eess';
2
+ import type { Corpus } from '../corpus.js';
3
+ import type { MdPointer } from '../model/pointers.js';
4
+ /** How a pointer path resolves against the repo file index. */
5
+ export interface PointerResolveOptions {
6
+ /**
7
+ * - `suffix` (default) — a pointer resolves to the one file whose repo-relative
8
+ * path *ends with* the pointer path on a `/` boundary. Bare basenames are the
9
+ * single-segment case, so `index.vue` and `admin/index.vue` resolve by the
10
+ * same rule. Multiple matches → ambiguous (skipped, never failed).
11
+ * - `exact` — a pointer must be the exact repo-relative path; no suffix or
12
+ * basename leniency. Choose this when pointers are required to carry full
13
+ * paths (e.g. the external repo's `spec-check.ts` strictness).
14
+ */
15
+ readonly paths?: 'suffix' | 'exact';
16
+ /**
17
+ * Additional absolute roots to try when a pointer does not resolve in-repo —
18
+ * e.g. a sibling checkout of the legacy system a traceability matrix cites
19
+ * (plan 0069 Phase 5). Resolution under an external root is by exact
20
+ * relative path (no suffix leniency; no tree walk). Semantics when a pointer
21
+ * fails in-repo:
22
+ * - some listed root exists → the pointer must ground there (broken/stale
23
+ * violations name the roots searched);
24
+ * - **no listed root exists** → the pointer is **skipped, not passed** — it
25
+ * yields no violation, and the calling gate must say so out loud (use
26
+ * `presentExternalRoots` to report the skipped scope in its summary line;
27
+ * a skip that reads as a pass is the vacuity this option must not create).
28
+ */
29
+ readonly externalRoots?: readonly string[];
30
+ }
31
+ /** The subset of `roots` that actually exist on disk — for gate summary lines. */
32
+ export declare function presentExternalRoots(roots: readonly string[]): readonly string[];
33
+ /**
34
+ * Condition: every code pointer resolves to a real file with the referenced
35
+ * line in range. Closes over the `Corpus` for the repo file index.
36
+ *
37
+ * Classification mirrors the hand-rolled `spec-check.ts`:
38
+ * - **broken** — no file matches the pointer path
39
+ * - **stale** — file exists but is shorter than the referenced line
40
+ * - **ambiguous** — a path-suffix matching several files → **reported, never
41
+ * failed** (the fix is to write more of the path), so it yields no violation
42
+ * - **ok** — otherwise
43
+ *
44
+ * Resolution (default `suffix` mode): exact repo-relative path first, else the
45
+ * unique file whose path ends with the pointer path. `exact` mode requires the
46
+ * full repo-relative path. `.check()` fails on broken/stale; `.warn()` reports
47
+ * without failing. Frozen vs live scoping is done by the `areFrozen()` /
48
+ * `areLive()` predicates upstream.
49
+ */
50
+ export declare function pointerResolves(corpus: Corpus, options?: PointerResolveOptions): Condition<MdPointer>;
51
+ //# sourceMappingURL=pointer-resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointer-resolve.d.ts","sourceRoot":"","sources":["../../src/conditions/pointer-resolve.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AASrD,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;IACnC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC3C;AAED,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAShF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,qBAA0B,GAClC,SAAS,CAAC,SAAS,CAAC,CAsItB"}
@@ -0,0 +1,164 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { mdViolation } from '../model/violation.js';
4
+ /** The subset of `roots` that actually exist on disk — for gate summary lines. */
5
+ export function presentExternalRoots(roots) {
6
+ return roots.filter((r) => {
7
+ try {
8
+ return statSync(r).isDirectory();
9
+ }
10
+ catch (err) {
11
+ void err; // absent / unreadable root — by definition not present
12
+ return false;
13
+ }
14
+ });
15
+ }
16
+ /**
17
+ * Condition: every code pointer resolves to a real file with the referenced
18
+ * line in range. Closes over the `Corpus` for the repo file index.
19
+ *
20
+ * Classification mirrors the hand-rolled `spec-check.ts`:
21
+ * - **broken** — no file matches the pointer path
22
+ * - **stale** — file exists but is shorter than the referenced line
23
+ * - **ambiguous** — a path-suffix matching several files → **reported, never
24
+ * failed** (the fix is to write more of the path), so it yields no violation
25
+ * - **ok** — otherwise
26
+ *
27
+ * Resolution (default `suffix` mode): exact repo-relative path first, else the
28
+ * unique file whose path ends with the pointer path. `exact` mode requires the
29
+ * full repo-relative path. `.check()` fails on broken/stale; `.warn()` reports
30
+ * without failing. Frozen vs live scoping is done by the `areFrozen()` /
31
+ * `areLive()` predicates upstream.
32
+ */
33
+ export function pointerResolves(corpus, options = {}) {
34
+ const mode = options.paths ?? 'suffix';
35
+ const externalRoots = options.externalRoots ?? [];
36
+ const byBasename = new Map();
37
+ for (const rel of corpus.fileIndex) {
38
+ const base = rel.slice(rel.lastIndexOf('/') + 1);
39
+ const list = byBasename.get(base);
40
+ if (list)
41
+ list.push(rel);
42
+ else
43
+ byBasename.set(base, [rel]);
44
+ }
45
+ const lineCountCache = new Map();
46
+ const lineCount = (rel) => {
47
+ const cached = lineCountCache.get(rel);
48
+ if (cached !== undefined)
49
+ return cached;
50
+ const n = readFileSync(join(corpus.root, rel), 'utf8').split('\n').length;
51
+ lineCountCache.set(rel, n);
52
+ return n;
53
+ };
54
+ return {
55
+ description: 'resolve to a real file and line',
56
+ evaluate: (pointers, ctx) => pointers.flatMap((p) => {
57
+ const wanted = p.path.replace(/^\.?\//, '');
58
+ // The unique file whose path ends with `wanted` (on a / boundary).
59
+ // Narrow by last segment, then confirm the full suffix.
60
+ const uniqueSuffix = () => {
61
+ const lastSeg = wanted.slice(wanted.lastIndexOf('/') + 1);
62
+ const matches = (byBasename.get(lastSeg) ?? []).filter((f) => f === wanted || f.endsWith('/' + wanted));
63
+ const only = matches[0];
64
+ if (matches.length === 1 && only !== undefined)
65
+ return { kind: 'unique', file: only };
66
+ if (matches.length > 1)
67
+ return { kind: 'ambiguous' };
68
+ return { kind: 'none' };
69
+ };
70
+ let targetRel = null;
71
+ if (corpus.fileIndex.has(wanted)) {
72
+ // Exact repo-relative path — preferred in both modes.
73
+ targetRel = wanted;
74
+ }
75
+ else if (mode === 'suffix') {
76
+ const m = uniqueSuffix();
77
+ if (m.kind === 'ambiguous')
78
+ return []; // reported elsewhere, never failed
79
+ targetRel = m.kind === 'unique' ? m.file : null;
80
+ }
81
+ if (targetRel === null && externalRoots.length > 0) {
82
+ // Not in-repo, external roots configured (plan 0069 Phase 5).
83
+ const present = presentExternalRoots(externalRoots);
84
+ if (present.length === 0)
85
+ return []; // skipped, not passed — caller reports the scope
86
+ for (const rootDir of present) {
87
+ const abs = join(rootDir, wanted);
88
+ let external;
89
+ try {
90
+ external = readFileSync(abs, 'utf8').split('\n').length;
91
+ }
92
+ catch (err) {
93
+ void err; // not under this root — try the next configured root
94
+ continue;
95
+ }
96
+ if (Math.max(p.startLine, p.endLine) > external) {
97
+ return [
98
+ mdViolation({
99
+ element: `${p.doc.relPath} → ${p.raw}`,
100
+ file: p.doc.file,
101
+ line: p.line,
102
+ message: `stale code pointer: ${rootDir}/${wanted} has ${external} lines; "${p.raw}" references line ${Math.max(p.startLine, p.endLine)}`,
103
+ sourceText: p.doc.text,
104
+ context: ctx,
105
+ }),
106
+ ];
107
+ }
108
+ return []; // grounds in the external root
109
+ }
110
+ return [
111
+ mdViolation({
112
+ element: `${p.doc.relPath} → ${p.raw}`,
113
+ file: p.doc.file,
114
+ line: p.line,
115
+ message: `broken code pointer: "${p.raw}" — not in the repo, and not under external root(s) ${present.join(', ')}`,
116
+ sourceText: p.doc.text,
117
+ context: ctx,
118
+ }),
119
+ ];
120
+ }
121
+ if (targetRel === null) {
122
+ // Broken. If exactly one file uniquely resolves the shortened path, the
123
+ // repair is deterministic — attach an autofix that expands it to the
124
+ // full repo-relative path (plan 0066). Ambiguous → no fix.
125
+ const m = uniqueSuffix();
126
+ const fix = m.kind === 'unique' && m.file !== wanted
127
+ ? {
128
+ file: p.doc.file,
129
+ start: p.pathStart,
130
+ end: p.pathEnd,
131
+ replacement: m.file,
132
+ describe: `rewrite pointer "${p.path}" → "${m.file}"`,
133
+ }
134
+ : undefined;
135
+ return [
136
+ mdViolation({
137
+ element: `${p.doc.relPath} → ${p.raw}`,
138
+ file: p.doc.file,
139
+ line: p.line,
140
+ message: `broken code pointer: "${p.raw}" — no such file in the repo`,
141
+ sourceText: p.doc.text,
142
+ fix,
143
+ context: ctx,
144
+ }),
145
+ ];
146
+ }
147
+ const lines = lineCount(targetRel);
148
+ if (Math.max(p.startLine, p.endLine) > lines) {
149
+ return [
150
+ mdViolation({
151
+ element: `${p.doc.relPath} → ${p.raw}`,
152
+ file: p.doc.file,
153
+ line: p.line,
154
+ message: `stale code pointer: ${targetRel} has ${lines} lines; "${p.raw}" references line ${Math.max(p.startLine, p.endLine)}`,
155
+ sourceText: p.doc.text,
156
+ context: ctx,
157
+ }),
158
+ ];
159
+ }
160
+ return [];
161
+ }),
162
+ };
163
+ }
164
+ //# sourceMappingURL=pointer-resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pointer-resolve.js","sourceRoot":"","sources":["../../src/conditions/pointer-resolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { Condition } from '@nielspeter/eess';
2
+ import type { Corpus } from '../corpus.js';
3
+ import type { MdLink } from '../model/links.js';
4
+ /**
5
+ * Options for `linkResolves` — how a link URL is matched against the repo tree.
6
+ * Covers static-site conventions (VitePress, Docusaurus, MkDocs, GitBook…)
7
+ * generically instead of naming any tool: extensionless links resolve by
8
+ * trying extensions and/or a directory index file.
9
+ */
10
+ export interface LinkResolveOptions {
11
+ /**
12
+ * Extensions to try when the URL as written doesn't resolve, e.g. `['.md']`
13
+ * for sites that link `./guide` meaning `./guide.md`. Tried in order.
14
+ */
15
+ readonly tryExtensions?: readonly string[];
16
+ /**
17
+ * Directory index filename to try, e.g. `'index.md'` for sites where
18
+ * `./guide/` (or `./guide`) means `./guide/index.md`.
19
+ */
20
+ readonly tryIndex?: string;
21
+ /**
22
+ * Content root for site-absolute links (leading `/`). Static-site generators
23
+ * resolve `/page` against the site's content root, not the repo root — e.g.
24
+ * `rootDir: 'docs'` makes `/guide` mean `docs/guide(.md)`. Repo-root
25
+ * resolution is still tried first, so plain repo-absolute links keep working.
26
+ */
27
+ readonly rootDir?: string;
28
+ }
29
+ /**
30
+ * Condition: every (internal) link resolves to a file in the corpus's repo tree.
31
+ * Closes over the `Corpus` for the file index. External links and pure anchors
32
+ * are skipped. `options` adds extensionless-link resolution (`tryExtensions`,
33
+ * `tryIndex`) for static-site corpora.
34
+ */
35
+ export declare function linkResolves(corpus: Corpus, options?: LinkResolveOptions): Condition<MdLink>;
36
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/conditions/resolve.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAW,MAAM,kBAAkB,CAAA;AAC1D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAG/C;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1C;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAC1B;AAmCD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,SAAS,CAAC,MAAM,CAAC,CAgChG"}
@@ -0,0 +1,108 @@
1
+ import { posix } from 'node:path';
2
+ import { mdViolation } from '../model/violation.js';
3
+ /**
4
+ * Resolve an internal link's URL to its base repo-relative target(s), or `[]`
5
+ * if it is a pure fragment (`#anchor`) or otherwise not a file reference.
6
+ * Site-absolute links (leading `/`) yield the repo-root target plus, when
7
+ * `rootDir` is set, the content-root target.
8
+ */
9
+ function resolveTargets(link, options) {
10
+ const withoutFragment = link.url.split('#')[0] ?? '';
11
+ if (withoutFragment === '')
12
+ return []; // same-document anchor
13
+ const decoded = decodeURIComponent(withoutFragment);
14
+ if (decoded.startsWith('/')) {
15
+ const repoRooted = posix.normalize(decoded.replace(/^\/+/, ''));
16
+ return options.rootDir !== undefined
17
+ ? [repoRooted, posix.normalize(posix.join(options.rootDir, repoRooted))]
18
+ : [repoRooted];
19
+ }
20
+ const base = posix.dirname(link.doc.relPath);
21
+ return [posix.normalize(posix.join(base, decoded))];
22
+ }
23
+ /** All repo-relative candidates for a target under the given options. */
24
+ function candidates(target, options) {
25
+ const out = [target];
26
+ const bare = target.replace(/\/+$/, '');
27
+ for (const ext of options.tryExtensions ?? []) {
28
+ out.push(bare + ext);
29
+ }
30
+ if (options.tryIndex !== undefined) {
31
+ out.push(posix.join(bare, options.tryIndex));
32
+ }
33
+ return out;
34
+ }
35
+ /**
36
+ * Condition: every (internal) link resolves to a file in the corpus's repo tree.
37
+ * Closes over the `Corpus` for the file index. External links and pure anchors
38
+ * are skipped. `options` adds extensionless-link resolution (`tryExtensions`,
39
+ * `tryIndex`) for static-site corpora.
40
+ */
41
+ export function linkResolves(corpus, options = {}) {
42
+ // Basename → repo files, for finding a uniquely-moved target of a broken link.
43
+ const byBasename = new Map();
44
+ for (const rel of corpus.fileIndex) {
45
+ const base = rel.slice(rel.lastIndexOf('/') + 1);
46
+ const list = byBasename.get(base);
47
+ if (list)
48
+ list.push(rel);
49
+ else
50
+ byBasename.set(base, [rel]);
51
+ }
52
+ return {
53
+ description: 'resolve to an existing file',
54
+ evaluate: (links, ctx) => links.flatMap((link) => {
55
+ if (link.external)
56
+ return [];
57
+ const targets = resolveTargets(link, options);
58
+ if (targets.length === 0)
59
+ return [];
60
+ if (targets.some((t) => candidates(t, options).some((c) => corpus.fileIndex.has(c))))
61
+ return [];
62
+ return [
63
+ mdViolation({
64
+ element: `${link.doc.relPath} → ${link.url}`,
65
+ file: link.doc.file,
66
+ line: link.line,
67
+ message: `broken link: "${link.url}" does not resolve to a file in the repo`,
68
+ sourceText: link.doc.text,
69
+ fix: movedLinkFix(link, byBasename),
70
+ context: ctx,
71
+ }),
72
+ ];
73
+ }),
74
+ };
75
+ }
76
+ /**
77
+ * If a broken link's basename uniquely names one file in the repo (the target
78
+ * moved, not renamed), emit a deterministic autofix rewriting the URL to a path
79
+ * relative to the linking document (plan 0066). Ambiguous basename, or a URL
80
+ * whose exact span isn't known, → no fix.
81
+ */
82
+ function movedLinkFix(link, byBasename) {
83
+ if (link.urlStart === undefined || link.urlEnd === undefined)
84
+ return undefined;
85
+ const path = link.url.split('#')[0] ?? '';
86
+ const fragment = link.url.slice(path.length); // '' or '#anchor'
87
+ const base = path.slice(path.replace(/\/+$/, '').lastIndexOf('/') + 1);
88
+ const matches = byBasename.get(base) ?? [];
89
+ if (matches.length !== 1)
90
+ return undefined; // renamed (no match) or ambiguous → no fix
91
+ const target = matches[0];
92
+ if (target === undefined)
93
+ return undefined;
94
+ let rel = posix.relative(posix.dirname(link.doc.relPath), target);
95
+ if (!rel.startsWith('.'))
96
+ rel = './' + rel;
97
+ const replacement = rel + fragment;
98
+ if (replacement === link.url)
99
+ return undefined;
100
+ return {
101
+ file: link.doc.file,
102
+ start: link.urlStart,
103
+ end: link.urlEnd,
104
+ replacement,
105
+ describe: `rewrite link "${link.url}" → "${replacement}"`,
106
+ };
107
+ }
108
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/conditions/resolve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { Condition } from '@nielspeter/eess';
2
+ import type { MdDocument } from '../model/document.js';
3
+ /** Condition: every matched document must have a heading matching `name`. */
4
+ export declare function haveSection(name: string | RegExp): Condition<MdDocument>;
5
+ export interface HaveTableOptions {
6
+ /** Restrict to a table under this heading. */
7
+ readonly section?: string | RegExp;
8
+ /** Each regex must match at least one header cell. */
9
+ readonly columns: readonly RegExp[];
10
+ }
11
+ /** Condition: the document must contain a GFM table with the required columns. */
12
+ export declare function haveTable(opts: HaveTableOptions): Condition<MdDocument>;
13
+ //# sourceMappingURL=structure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure.d.ts","sourceRoot":"","sources":["../../src/conditions/structure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAItD,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,UAAU,CAAC,CAiBxE;AAED,MAAM,WAAW,gBAAgB;IAC/B,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAClC,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CACpC;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,SAAS,CAAC,UAAU,CAAC,CAyBvE"}
@@ -0,0 +1,42 @@
1
+ import { mdViolation } from '../model/violation.js';
2
+ import { hasSection, tableInSection } from '../model/query.js';
3
+ /** Condition: every matched document must have a heading matching `name`. */
4
+ export function haveSection(name) {
5
+ return {
6
+ description: `have section "${String(name)}"`,
7
+ evaluate: (docs, ctx) => docs
8
+ .filter((d) => !hasSection(d, name))
9
+ .map((d) => mdViolation({
10
+ element: d.relPath,
11
+ file: d.file,
12
+ line: 1,
13
+ message: `missing section "${String(name)}"`,
14
+ sourceText: d.text,
15
+ context: ctx,
16
+ })),
17
+ };
18
+ }
19
+ /** Condition: the document must contain a GFM table with the required columns. */
20
+ export function haveTable(opts) {
21
+ const colDesc = opts.columns.map(String).join(', ');
22
+ return {
23
+ description: `have a table with columns ${colDesc}${opts.section ? ` in section "${String(opts.section)}"` : ''}`,
24
+ evaluate: (docs, ctx) => docs.flatMap((d) => {
25
+ const candidates = d.tables.filter((t) => tableInSection(t, opts.section));
26
+ const satisfied = candidates.some((t) => opts.columns.every((col) => t.header.some((h) => col.test(h))));
27
+ if (satisfied)
28
+ return [];
29
+ return [
30
+ mdViolation({
31
+ element: d.relPath,
32
+ file: d.file,
33
+ line: candidates[0]?.line ?? 1,
34
+ message: `no table with the required columns (${colDesc})${opts.section ? ` in section "${String(opts.section)}"` : ''}`,
35
+ sourceText: d.text,
36
+ context: ctx,
37
+ }),
38
+ ];
39
+ }),
40
+ };
41
+ }
42
+ //# sourceMappingURL=structure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structure.js","sourceRoot":"","sources":["../../src/conditions/structure.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAE9D,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,IAAqB;IAC/C,OAAO;QACL,WAAW,EAAE,iBAAiB,MAAM,CAAC,IAAI,CAAC,GAAG;QAC7C,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CACtB,IAAI;aACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;aACnC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACT,WAAW,CAAC;YACV,OAAO,EAAE,CAAC,CAAC,OAAO;YAClB,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC;YACP,OAAO,EAAE,oBAAoB,MAAM,CAAC,IAAI,CAAC,GAAG;YAC5C,UAAU,EAAE,CAAC,CAAC,IAAI;YAClB,OAAO,EAAE,GAAG;SACb,CAAC,CACH;KACN,CAAA;AACH,CAAC;AASD,kFAAkF;AAClF,MAAM,UAAU,SAAS,CAAC,IAAsB;IAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACnD,OAAO;QACL,WAAW,EAAE,6BAA6B,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACjH,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CACtB,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;YACjB,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAA;YAC1E,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CACtC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAC/D,CAAA;YACD,IAAI,SAAS;gBAAE,OAAO,EAAE,CAAA;YACxB,OAAO;gBACL,WAAW,CAAC;oBACV,OAAO,EAAE,CAAC,CAAC,OAAO;oBAClB,IAAI,EAAE,CAAC,CAAC,IAAI;oBACZ,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC;oBAC9B,OAAO,EAAE,uCAAuC,OAAO,IACrD,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAC3D,EAAE;oBACF,UAAU,EAAE,CAAC,CAAC,IAAI;oBAClB,OAAO,EAAE,GAAG;iBACb,CAAC;aACH,CAAA;QACH,CAAC,CAAC;KACL,CAAA;AACH,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { Condition } from '@nielspeter/eess';
2
+ import type { MdDocument } from '../model/document.js';
3
+ import { type MdRow, type ColumnSpec } from '../model/rows.js';
4
+ /**
5
+ * Context passed to a row validator — the `rows()` element type ([[MdRow]]).
6
+ * Kept as a named export for back-compat; new code can use `MdRow` directly.
7
+ */
8
+ export type TableRowContext = MdRow;
9
+ export interface HaveTableRowsOptions {
10
+ /** Restrict to a table under this heading. */
11
+ readonly section?: string | RegExp;
12
+ /** role → header pattern. The table must have all of these columns; the row
13
+ * validator reads cells via `ctx.get(role)`. */
14
+ readonly columns: ColumnSpec;
15
+ /** Return violation messages for a data row (empty = row is fine). */
16
+ readonly row: (ctx: TableRowContext) => string[];
17
+ }
18
+ /**
19
+ * Generic table-row validator — the primitive `adrEnforcement` composes. For
20
+ * each document, finds the first table (optionally within a section) whose
21
+ * header matches every column pattern, and runs the `row` callback per data
22
+ * row. Table presence itself is `haveTable`'s job; this validates the rows of a
23
+ * matching table. Row extraction is shared with the `rows()` entry point via
24
+ * `matchTableRows` (here in `first` mode, preserving the historical
25
+ * single-table behavior); the reported line stays the table's start line.
26
+ */
27
+ export declare function haveTableRowsSatisfying(opts: HaveTableRowsOptions): Condition<MdDocument>;
28
+ //# sourceMappingURL=table-rows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table-rows.d.ts","sourceRoot":"","sources":["../../src/conditions/table-rows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEtD,OAAO,EAAkB,KAAK,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE9E;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,KAAK,CAAA;AAEnC,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAClC;oDACgD;IAChD,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,MAAM,EAAE,CAAA;CACjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,oBAAoB,GAAG,SAAS,CAAC,UAAU,CAAC,CAoBzF"}
@@ -0,0 +1,25 @@
1
+ import { mdViolation } from '../model/violation.js';
2
+ import { matchTableRows } from '../model/rows.js';
3
+ /**
4
+ * Generic table-row validator — the primitive `adrEnforcement` composes. For
5
+ * each document, finds the first table (optionally within a section) whose
6
+ * header matches every column pattern, and runs the `row` callback per data
7
+ * row. Table presence itself is `haveTable`'s job; this validates the rows of a
8
+ * matching table. Row extraction is shared with the `rows()` entry point via
9
+ * `matchTableRows` (here in `first` mode, preserving the historical
10
+ * single-table behavior); the reported line stays the table's start line.
11
+ */
12
+ export function haveTableRowsSatisfying(opts) {
13
+ return {
14
+ description: `have table rows satisfying the row validator`,
15
+ evaluate: (documents, ctx) => documents.flatMap((doc) => matchTableRows(doc, { section: opts.section, columns: opts.columns }, 'first').flatMap((row) => opts.row(row).map((message) => mdViolation({
16
+ element: `${doc.relPath} (${opts.section ? `${String(opts.section)} ` : ''}row ${row.rowIndex + 1})`,
17
+ file: doc.file,
18
+ line: row.table.line,
19
+ message,
20
+ sourceText: doc.text,
21
+ context: ctx,
22
+ })))),
23
+ };
24
+ }
25
+ //# sourceMappingURL=table-rows.js.map