@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.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/dist/builders/docs.d.ts +28 -0
- package/dist/builders/docs.d.ts.map +1 -0
- package/dist/builders/docs.js +44 -0
- package/dist/builders/docs.js.map +1 -0
- package/dist/builders/links.d.ts +26 -0
- package/dist/builders/links.d.ts.map +1 -0
- package/dist/builders/links.js +42 -0
- package/dist/builders/links.js.map +1 -0
- package/dist/builders/pointers.d.ts +26 -0
- package/dist/builders/pointers.d.ts.map +1 -0
- package/dist/builders/pointers.js +43 -0
- package/dist/builders/pointers.js.map +1 -0
- package/dist/builders/rows.d.ts +36 -0
- package/dist/builders/rows.d.ts.map +1 -0
- package/dist/builders/rows.js +42 -0
- package/dist/builders/rows.js.map +1 -0
- package/dist/builders/task-items.d.ts +21 -0
- package/dist/builders/task-items.d.ts.map +1 -0
- package/dist/builders/task-items.js +33 -0
- package/dist/builders/task-items.js.map +1 -0
- package/dist/builders/vocabulary.d.ts +66 -0
- package/dist/builders/vocabulary.d.ts.map +1 -0
- package/dist/builders/vocabulary.js +118 -0
- package/dist/builders/vocabulary.js.map +1 -0
- package/dist/conditions/pointer-resolve.d.ts +51 -0
- package/dist/conditions/pointer-resolve.d.ts.map +1 -0
- package/dist/conditions/pointer-resolve.js +164 -0
- package/dist/conditions/pointer-resolve.js.map +1 -0
- package/dist/conditions/resolve.d.ts +36 -0
- package/dist/conditions/resolve.d.ts.map +1 -0
- package/dist/conditions/resolve.js +108 -0
- package/dist/conditions/resolve.js.map +1 -0
- package/dist/conditions/structure.d.ts +13 -0
- package/dist/conditions/structure.d.ts.map +1 -0
- package/dist/conditions/structure.js +42 -0
- package/dist/conditions/structure.js.map +1 -0
- package/dist/conditions/table-rows.d.ts +28 -0
- package/dist/conditions/table-rows.d.ts.map +1 -0
- package/dist/conditions/table-rows.js +25 -0
- package/dist/conditions/table-rows.js.map +1 -0
- package/dist/corpus.d.ts +40 -0
- package/dist/corpus.d.ts.map +1 -0
- package/dist/corpus.js +73 -0
- package/dist/corpus.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/model/document.d.ts +78 -0
- package/dist/model/document.d.ts.map +1 -0
- package/dist/model/document.js +68 -0
- package/dist/model/document.js.map +1 -0
- package/dist/model/links.d.ts +32 -0
- package/dist/model/links.d.ts.map +1 -0
- package/dist/model/links.js +53 -0
- package/dist/model/links.js.map +1 -0
- package/dist/model/pointers.d.ts +29 -0
- package/dist/model/pointers.d.ts.map +1 -0
- package/dist/model/pointers.js +65 -0
- package/dist/model/pointers.js.map +1 -0
- package/dist/model/query.d.ts +6 -0
- package/dist/model/query.d.ts.map +1 -0
- package/dist/model/query.js +15 -0
- package/dist/model/query.js.map +1 -0
- package/dist/model/rows.d.ts +52 -0
- package/dist/model/rows.d.ts.map +1 -0
- package/dist/model/rows.js +42 -0
- package/dist/model/rows.js.map +1 -0
- package/dist/model/task-items.d.ts +28 -0
- package/dist/model/task-items.d.ts.map +1 -0
- package/dist/model/task-items.js +47 -0
- package/dist/model/task-items.js.map +1 -0
- package/dist/model/violation.d.ts +23 -0
- package/dist/model/violation.d.ts.map +1 -0
- package/dist/model/violation.js +22 -0
- package/dist/model/violation.js.map +1 -0
- package/dist/predicates/document.d.ts +8 -0
- package/dist/predicates/document.d.ts.map +1 -0
- package/dist/predicates/document.js +22 -0
- package/dist/predicates/document.js.map +1 -0
- package/dist/rules/adr.d.ts +34 -0
- package/dist/rules/adr.d.ts.map +1 -0
- package/dist/rules/adr.js +103 -0
- package/dist/rules/adr.js.map +1 -0
- package/dist/rules/ledger.d.ts +44 -0
- package/dist/rules/ledger.d.ts.map +1 -0
- package/dist/rules/ledger.js +119 -0
- package/dist/rules/ledger.js.map +1 -0
- 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
|