@nielspeter/eess-md 0.7.0 → 0.9.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 (66) hide show
  1. package/README.md +3 -1
  2. package/dist/builders/declaration-findings.d.ts +37 -0
  3. package/dist/builders/declaration-findings.d.ts.map +1 -0
  4. package/dist/builders/declaration-findings.js +128 -0
  5. package/dist/builders/declaration-findings.js.map +1 -0
  6. package/dist/builders/links.d.ts +60 -1
  7. package/dist/builders/links.d.ts.map +1 -1
  8. package/dist/builders/links.js +92 -1
  9. package/dist/builders/links.js.map +1 -1
  10. package/dist/conditions/linked-back.d.ts +17 -0
  11. package/dist/conditions/linked-back.d.ts.map +1 -0
  12. package/dist/conditions/linked-back.js +122 -0
  13. package/dist/conditions/linked-back.js.map +1 -0
  14. package/dist/conditions/resolve.d.ts +8 -40
  15. package/dist/conditions/resolve.d.ts.map +1 -1
  16. package/dist/conditions/resolve.js +26 -138
  17. package/dist/conditions/resolve.js.map +1 -1
  18. package/dist/corpus.d.ts +3 -0
  19. package/dist/corpus.d.ts.map +1 -1
  20. package/dist/corpus.js +7 -10
  21. package/dist/corpus.js.map +1 -1
  22. package/dist/index.d.ts +4 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/model/document.d.ts +8 -1
  27. package/dist/model/document.d.ts.map +1 -1
  28. package/dist/model/document.js +14 -3
  29. package/dist/model/document.js.map +1 -1
  30. package/dist/model/label.d.ts +35 -0
  31. package/dist/model/label.d.ts.map +1 -0
  32. package/dist/model/label.js +68 -0
  33. package/dist/model/label.js.map +1 -0
  34. package/dist/model/links.d.ts +43 -0
  35. package/dist/model/links.d.ts.map +1 -1
  36. package/dist/model/links.js +112 -11
  37. package/dist/model/links.js.map +1 -1
  38. package/dist/model/prose.d.ts +11 -0
  39. package/dist/model/prose.d.ts.map +1 -1
  40. package/dist/model/prose.js +50 -5
  41. package/dist/model/prose.js.map +1 -1
  42. package/dist/model/query.d.ts +7 -0
  43. package/dist/model/query.d.ts.map +1 -1
  44. package/dist/model/query.js +10 -3
  45. package/dist/model/query.js.map +1 -1
  46. package/dist/model/resolve-link.d.ts +142 -0
  47. package/dist/model/resolve-link.d.ts.map +1 -0
  48. package/dist/model/resolve-link.js +194 -0
  49. package/dist/model/resolve-link.js.map +1 -0
  50. package/dist/predicates/declared-block.d.ts +29 -0
  51. package/dist/predicates/declared-block.d.ts.map +1 -0
  52. package/dist/predicates/declared-block.js +47 -0
  53. package/dist/predicates/declared-block.js.map +1 -0
  54. package/dist/predicates/live-target.d.ts +12 -0
  55. package/dist/predicates/live-target.d.ts.map +1 -0
  56. package/dist/predicates/live-target.js +17 -0
  57. package/dist/predicates/live-target.js.map +1 -0
  58. package/dist/rules/adr.d.ts +14 -5
  59. package/dist/rules/adr.d.ts.map +1 -1
  60. package/dist/rules/adr.js +5 -27
  61. package/dist/rules/adr.js.map +1 -1
  62. package/dist/rules/ledger.d.ts +10 -6
  63. package/dist/rules/ledger.d.ts.map +1 -1
  64. package/dist/rules/ledger.js +80 -24
  65. package/dist/rules/ledger.js.map +1 -1
  66. package/package.json +6 -3
package/README.md CHANGED
@@ -144,7 +144,9 @@ a `correspondence()` against the set you hold, both directions. The docs page's
144
144
  ```typescript
145
145
  import { adrEnforcement } from '@nielspeter/eess-md/rules/adr'
146
146
 
147
- adrEnforcement(c, { dir: 'docs/adr/**' }) // section+table present, valid tiers, citations resolve
147
+ // section+table present, valid tiers, cited paths exist. A cited it('…') title is not
148
+ // checked here — run eess-crossvalidate's adrCitationsResolve for that.
149
+ adrEnforcement(c, { dir: 'docs/adr/**' })
148
150
  ```
149
151
 
150
152
  An index/schema doc in the ADR directory (`README.md` or `index.md`) is exempt —
@@ -0,0 +1,37 @@
1
+ import type { ConditionContext } from '@nielspeter/eess';
2
+ import type { Corpus } from '../corpus.js';
3
+ import { type ArchViolation } from '../model/violation.js';
4
+ /** What an `areLabelled()`/`areInSection()` call declared, kept for the declaration scan. */
5
+ export type LinkDeclaration = {
6
+ readonly kind: 'label';
7
+ readonly label: string;
8
+ } | {
9
+ readonly kind: 'section';
10
+ readonly name: string | RegExp;
11
+ };
12
+ /** One key per distinct declaration, so a label declared twice is scanned once. */
13
+ export declare function declarationKey(d: LinkDeclaration): string;
14
+ /**
15
+ * Plan 0405 — findings about the declarations themselves, scanned over the whole
16
+ * corpus (not the rule's selection: a misspelt declaration is, by definition, not
17
+ * selected). Each names something the selector could not read (ADR-016 rule 7):
18
+ *
19
+ * - **a near-miss label:** a wrapped label (`**Label:**`, `**Label**:`,
20
+ * `__Label__:`) opening a block that matches the declared one except for case
21
+ * or inner spacing. The plain `Label:` form is never a near-miss: prose
22
+ * beginning "related to:" is common;
23
+ * - **a label inside the declaration:** a different wrapped label nested inside
24
+ * a declaration takes the links under it out of the declaration;
25
+ * - **a label where no block starts:** the label, exact or near, later in a
26
+ * paragraph, in a table cell or in a heading, where it declares nothing — or
27
+ * at a block's start in a form the grammar does not read (`**_Label:_**`, a
28
+ * whole line in bold);
29
+ * - **reference-style links:** a declaration holding `[text][ref]` links, which
30
+ * eess-md does not read;
31
+ * - **an empty declaration:** a wrapped label selecting no link — it names no
32
+ * record, or it stands alone with no list directly under it;
33
+ * - **a near-miss section:** a heading that misses a string declaration by case
34
+ * or spacing (a `RegExp` states its own tolerance).
35
+ */
36
+ export declare function declarationFindings(corpus: Corpus, declarations: readonly LinkDeclaration[], context: ConditionContext): ArchViolation[];
37
+ //# sourceMappingURL=declaration-findings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declaration-findings.d.ts","sourceRoot":"","sources":["../../src/builders/declaration-findings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAG1C,OAAO,EAAe,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAGvE,6FAA6F;AAC7F,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClD;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAA;AAEhE,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAGzD;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,SAAS,eAAe,EAAE,EACxC,OAAO,EAAE,gBAAgB,GACxB,aAAa,EAAE,CAyHjB"}
@@ -0,0 +1,128 @@
1
+ import { collectLinkBlocks } from '../model/links.js';
2
+ import { isLabelAlone, isNearMiss, labelPattern, wrappedLabelOf } from '../model/label.js';
3
+ import { mdViolation } from '../model/violation.js';
4
+ import { decidingBlock } from '../predicates/declared-block.js';
5
+ /** One key per distinct declaration, so a label declared twice is scanned once. */
6
+ export function declarationKey(d) {
7
+ if (d.kind === 'label')
8
+ return `label:${d.label}`;
9
+ return typeof d.name === 'string' ? `section:${d.name}` : `section-re:${String(d.name)}`;
10
+ }
11
+ /**
12
+ * Plan 0405 — findings about the declarations themselves, scanned over the whole
13
+ * corpus (not the rule's selection: a misspelt declaration is, by definition, not
14
+ * selected). Each names something the selector could not read (ADR-016 rule 7):
15
+ *
16
+ * - **a near-miss label:** a wrapped label (`**Label:**`, `**Label**:`,
17
+ * `__Label__:`) opening a block that matches the declared one except for case
18
+ * or inner spacing. The plain `Label:` form is never a near-miss: prose
19
+ * beginning "related to:" is common;
20
+ * - **a label inside the declaration:** a different wrapped label nested inside
21
+ * a declaration takes the links under it out of the declaration;
22
+ * - **a label where no block starts:** the label, exact or near, later in a
23
+ * paragraph, in a table cell or in a heading, where it declares nothing — or
24
+ * at a block's start in a form the grammar does not read (`**_Label:_**`, a
25
+ * whole line in bold);
26
+ * - **reference-style links:** a declaration holding `[text][ref]` links, which
27
+ * eess-md does not read;
28
+ * - **an empty declaration:** a wrapped label selecting no link — it names no
29
+ * record, or it stands alone with no list directly under it;
30
+ * - **a near-miss section:** a heading that misses a string declaration by case
31
+ * or spacing (a `RegExp` states its own tolerance).
32
+ */
33
+ export function declarationFindings(corpus, declarations, context) {
34
+ const out = [];
35
+ for (const doc of corpus.documents()) {
36
+ const { links, references, blocks, unreadLabels } = collectLinkBlocks(doc.root, doc.text);
37
+ // Each finding names its kind in its element, so one `.excluding()` sanctions one kind
38
+ // (`→ label "Supersedes" (empty)`), and carries its own remedy as the suggestion.
39
+ const report = (line, element, message, suggestion) => {
40
+ out.push(mdViolation({
41
+ file: doc.file,
42
+ line,
43
+ sourceText: doc.text,
44
+ context: { ...context, suggestion },
45
+ element,
46
+ message,
47
+ }));
48
+ };
49
+ for (const d of declarations) {
50
+ if (d.kind === 'section') {
51
+ if (typeof d.name !== 'string')
52
+ continue;
53
+ const declared = d.name;
54
+ for (const section of doc.sections) {
55
+ if (!isNearMiss(section.name, declared))
56
+ continue;
57
+ report(section.line, `${doc.relPath} → section "${section.name}" (near miss)`, `the heading "${section.name}" is not the declared section "${declared}", so the links under it are not checked`, `write it "${declared}"`);
58
+ }
59
+ continue;
60
+ }
61
+ const L = d.label;
62
+ const declared = new RegExp(labelPattern(L));
63
+ const element = (found, kind) => `${doc.relPath} → label "${found}" (${kind})`;
64
+ for (const block of blocks) {
65
+ const found = wrappedLabelOf(block.text);
66
+ if (found === undefined || !isNearMiss(found, L))
67
+ continue;
68
+ report(block.line, element(found, 'near miss'), `the label "${found}" is not the declared "${L}", so the links under it are not checked`, `write it "${L}"`);
69
+ }
70
+ // Which declaration blocks select a link, which hold a reference, which enclose anything.
71
+ const selects = new Set();
72
+ const holdsReference = new Set();
73
+ const encloses = new Set();
74
+ const taken = new Map();
75
+ const classify = (path, isReference) => {
76
+ for (const b of path)
77
+ encloses.add(b.line);
78
+ const decision = decidingBlock(path, declared);
79
+ if (decision === undefined)
80
+ return;
81
+ if (decision.matches) {
82
+ ;
83
+ (isReference ? holdsReference : selects).add(decision.block.line);
84
+ return;
85
+ }
86
+ const found = wrappedLabelOf(decision.block.text);
87
+ const outer = path.slice(0, decision.at).some((b) => declared.test(b.text));
88
+ if (found !== undefined && outer && !isNearMiss(found, L)) {
89
+ taken.set(decision.block.line, found);
90
+ }
91
+ };
92
+ for (const l of links)
93
+ classify(l.blockPath ?? [], false);
94
+ for (const path of references)
95
+ classify(path, true);
96
+ for (const [line, found] of taken) {
97
+ report(line, element(found, `inside "${L}"`), `the label "${found}" sits inside the "${L}" declaration and takes the links under it out of it`, `drop the label, or move it out of the "${L}" list`);
98
+ }
99
+ for (const block of blocks) {
100
+ if (!declared.test(block.text))
101
+ continue;
102
+ if (holdsReference.has(block.line)) {
103
+ report(block.line, element(L, 'reference links'), `the "${L}" declaration holds reference-style links, which eess-md does not read`, 'write them as inline links ([text](path))');
104
+ }
105
+ else if (wrappedLabelOf(block.text) === L &&
106
+ !selects.has(block.line) &&
107
+ // A link under it that is not selected is already a finding of its own.
108
+ !encloses.has(block.line)) {
109
+ const alone = isLabelAlone(block.text);
110
+ report(block.line, element(L, 'empty'), alone
111
+ ? `the "${L}" label stands alone with no list directly under it, so it declares nothing`
112
+ : `the "${L}" declaration names no record`, alone
113
+ ? 'put its list right after it, or its links on its line'
114
+ : 'add the links it declares, or remove the label');
115
+ }
116
+ }
117
+ for (const u of unreadLabels) {
118
+ if (u.label !== L && !isNearMiss(u.label, L))
119
+ continue;
120
+ report(u.line, element(u.label, u.reason === 'format' ? 'formatting' : 'not at a block start'), u.reason === 'format'
121
+ ? `the label "${u.label}" is written with formatting eess-md does not read, so the links after it are not read as a declaration`
122
+ : `the label "${u.label}" is not at the start of a paragraph or list item, so the links after it are not read as a declaration`, u.reason === 'format' ? `write it **${L}:**` : 'start a paragraph or list item with it');
123
+ }
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+ //# sourceMappingURL=declaration-findings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declaration-findings.js","sourceRoot":"","sources":["../../src/builders/declaration-findings.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { RuleBuilder } from '@nielspeter/eess';
1
+ import { RuleBuilder, type CollectResult } from '@nielspeter/eess';
2
2
  import type { Corpus } from '../corpus.js';
3
3
  import { type MdLink } from '../model/links.js';
4
4
  import { type LinkResolveOptions } from '../conditions/resolve.js';
@@ -9,17 +9,76 @@ export type { MdLink } from '../model/links.js';
9
9
  * Rule builder over the corpus's markdown links.
10
10
  */
11
11
  export declare class LinkRuleBuilder extends RuleBuilder<MdLink, Corpus> {
12
+ /** What `areLabelled()`/`areInSection()` declared, for the near-miss scan (plan 0405). */
13
+ private _declarations;
12
14
  protected getElements(): MdLink[];
13
15
  /** Filter to links that point within the repo (no scheme). */
14
16
  areInternal(): this;
15
17
  /** Filter to links with an external scheme (http, mailto, protocol-relative). */
16
18
  areExternal(): this;
19
+ /**
20
+ * Filter to links declared under `label` (plan 0405), in any of the ledger's
21
+ * forms (`**Label:**`, `**Label**:`, `__Label__:`, `Label:`), matched exactly
22
+ * and case-sensitively. A link is under the label when it sits in the block the
23
+ * label opens, in that block's nested lists, or — for a label alone on its line
24
+ * — in the lists directly after it. The nearest label decides: walking out from
25
+ * the link, the first block that is the label, or opens with a wrapped label of
26
+ * its own, settles it; a plain line that is not the label settles nothing.
27
+ *
28
+ * The rule also reports, over the whole corpus, what it cannot read: a wrapped
29
+ * label that misses the declared one only by case or spacing, a different
30
+ * wrapped label inside the declaration, the label where no block starts, a
31
+ * declaration holding reference-style links, and a wrapped declaration with
32
+ * nothing under it. That check
33
+ * runs through this method only: a selection taken with `.select()` carries no
34
+ * findings, so it carries no such check either.
35
+ */
36
+ areLabelled(label: string): this;
37
+ /**
38
+ * Filter to links under a heading matching `name` — a string exactly, or a
39
+ * `RegExp` — at any depth beneath it, matched the way `haveSection()` matches
40
+ * (plan 0405). A heading that misses a string `name` only by case or spacing
41
+ * is reported, as for `areLabelled()`.
42
+ */
43
+ areInSection(name: string | RegExp): this;
44
+ /**
45
+ * Record a declaration once; declaring the same label twice scans it once.
46
+ * Called on the copy `addPredicate()` returns, and it replaces the array rather
47
+ * than pushing into it, so a fork never shares its parent's declarations — the
48
+ * kernel's shallow `copy()` is enough.
49
+ */
50
+ private recordDeclaration;
17
51
  /**
18
52
  * Condition: internal links resolve to an existing repo file. Static-site
19
53
  * corpora with extensionless links pass `tryExtensions`/`tryIndex`, e.g.
20
54
  * `resolve({ tryExtensions: ['.md'], tryIndex: 'index.md' })`.
21
55
  */
22
56
  resolve(options?: LinkResolveOptions): this;
57
+ /**
58
+ * Filter to links whose target is not a frozen document (plan 0406) — a frozen
59
+ * record is history and cannot be edited to answer. Pass the options you pass
60
+ * `beLinkedBack()`, so the two resolve a link the same way. Every other target
61
+ * is kept, and examined.
62
+ */
63
+ haveLiveTargets(options?: LinkResolveOptions): this;
64
+ /**
65
+ * Condition: each link's target links back to the link's document (plan 0406).
66
+ * Any link back answers; several are one answer, and an incidental mention
67
+ * counts. A target that cannot answer — frozen, outside the corpus, missing, a
68
+ * directory, the record itself, an external URL — is one finding naming its
69
+ * cause, with its remedy as the suggestion. Takes the options `resolve()`
70
+ * takes, so a corpus with extensionless links resolves both ends the same way.
71
+ */
72
+ beLinkedBack(options?: LinkResolveOptions): this;
73
+ /**
74
+ * The base result, plus findings about the declarations themselves (plan
75
+ * 0405). The base evidence passes through unchanged: the declarations' scan
76
+ * examines nothing the rule asserts over, so it must not raise `examined`
77
+ * (which would hide the zero-examined finding when every declaration is
78
+ * misspelt) and must not go through `mergeCollectResults`, whose dead-member
79
+ * rule would read an empty findings part as a dead rule on every clean run.
80
+ */
81
+ protected collectViolations(): CollectResult;
23
82
  }
24
83
  /** Entry point: build rules over the corpus's markdown links. */
25
84
  export declare function links(corpus: Corpus): LinkRuleBuilder;
@@ -1 +1 @@
1
- {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/builders/links.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAkB,MAAM,kBAAkB,CAAA;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAgB,KAAK,MAAM,EAAkB,MAAM,mBAAmB,CAAA;AAE7E,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAEhF,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAElE,sEAAsE;AACtE,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAW/C;;GAEG;AACH,qBAAa,eAAgB,SAAQ,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC;IAC9D,SAAS,CAAC,WAAW,IAAI,MAAM,EAAE;IAIjC,8DAA8D;IAC9D,WAAW,IAAI,IAAI;IAInB,iFAAiF;IACjF,WAAW,IAAI,IAAI;IAInB;;;;OAIG;IACH,OAAO,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,IAAI;CAG5C;AAED,iEAAiE;AACjE,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAErD"}
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../../src/builders/links.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,KAAK,aAAa,EAAkB,MAAM,kBAAkB,CAAA;AACjG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAgB,KAAK,MAAM,EAAkB,MAAM,mBAAmB,CAAA;AAE7E,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAUhF,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAElE,sEAAsE;AACtE,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAW/C;;GAEG;AACH,qBAAa,eAAgB,SAAQ,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC;IAC9D,0FAA0F;IAC1F,OAAO,CAAC,aAAa,CAAiC;IAEtD,SAAS,CAAC,WAAW,IAAI,MAAM,EAAE;IAIjC,8DAA8D;IAC9D,WAAW,IAAI,IAAI;IAInB,iFAAiF;IACjF,WAAW,IAAI,IAAI;IAInB;;;;;;;;;;;;;;;;OAgBG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAOhC;;;;;OAKG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAOzC;;;;;OAKG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;;;OAIG;IACH,OAAO,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,IAAI;IAI3C;;;;;OAKG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,IAAI;IAInD;;;;;;;OAOG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,IAAI;IAIhD;;;;;;;OAOG;cACgB,iBAAiB,IAAI,aAAa;CAWtD;AAED,iEAAiE;AACjE,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAErD"}
@@ -1,7 +1,11 @@
1
- import { RuleBuilder } from '@nielspeter/eess';
1
+ import { RuleBuilder, collectResult } from '@nielspeter/eess';
2
2
  import { collectLinks } from '../model/links.js';
3
3
  import { stampedByDocument } from '../model/by-document.js';
4
4
  import { linkResolves } from '../conditions/resolve.js';
5
+ import { linkedBack } from '../conditions/linked-back.js';
6
+ import { haveLiveTargetsPredicate } from '../predicates/live-target.js';
7
+ import { areInSectionPredicate, areLabelledPredicate } from '../predicates/declared-block.js';
8
+ import { declarationFindings, declarationKey, } from './declaration-findings.js';
5
9
  const areInternal = () => ({
6
10
  description: 'are internal',
7
11
  test: (l) => !l.external,
@@ -14,6 +18,8 @@ const areExternal = () => ({
14
18
  * Rule builder over the corpus's markdown links.
15
19
  */
16
20
  export class LinkRuleBuilder extends RuleBuilder {
21
+ /** What `areLabelled()`/`areInSection()` declared, for the near-miss scan (plan 0405). */
22
+ _declarations = [];
17
23
  getElements() {
18
24
  return stampedByDocument(this.project, (doc) => collectLinks(doc.root, doc.text));
19
25
  }
@@ -25,6 +31,54 @@ export class LinkRuleBuilder extends RuleBuilder {
25
31
  areExternal() {
26
32
  return this.addPredicate(areExternal());
27
33
  }
34
+ /**
35
+ * Filter to links declared under `label` (plan 0405), in any of the ledger's
36
+ * forms (`**Label:**`, `**Label**:`, `__Label__:`, `Label:`), matched exactly
37
+ * and case-sensitively. A link is under the label when it sits in the block the
38
+ * label opens, in that block's nested lists, or — for a label alone on its line
39
+ * — in the lists directly after it. The nearest label decides: walking out from
40
+ * the link, the first block that is the label, or opens with a wrapped label of
41
+ * its own, settles it; a plain line that is not the label settles nothing.
42
+ *
43
+ * The rule also reports, over the whole corpus, what it cannot read: a wrapped
44
+ * label that misses the declared one only by case or spacing, a different
45
+ * wrapped label inside the declaration, the label where no block starts, a
46
+ * declaration holding reference-style links, and a wrapped declaration with
47
+ * nothing under it. That check
48
+ * runs through this method only: a selection taken with `.select()` carries no
49
+ * findings, so it carries no such check either.
50
+ */
51
+ areLabelled(label) {
52
+ return this.addPredicate(areLabelledPredicate(label)).recordDeclaration({
53
+ kind: 'label',
54
+ label,
55
+ });
56
+ }
57
+ /**
58
+ * Filter to links under a heading matching `name` — a string exactly, or a
59
+ * `RegExp` — at any depth beneath it, matched the way `haveSection()` matches
60
+ * (plan 0405). A heading that misses a string `name` only by case or spacing
61
+ * is reported, as for `areLabelled()`.
62
+ */
63
+ areInSection(name) {
64
+ return this.addPredicate(areInSectionPredicate(name)).recordDeclaration({
65
+ kind: 'section',
66
+ name,
67
+ });
68
+ }
69
+ /**
70
+ * Record a declaration once; declaring the same label twice scans it once.
71
+ * Called on the copy `addPredicate()` returns, and it replaces the array rather
72
+ * than pushing into it, so a fork never shares its parent's declarations — the
73
+ * kernel's shallow `copy()` is enough.
74
+ */
75
+ recordDeclaration(d) {
76
+ const key = declarationKey(d);
77
+ if (!this._declarations.some((x) => declarationKey(x) === key)) {
78
+ this._declarations = [...this._declarations, d];
79
+ }
80
+ return this;
81
+ }
28
82
  /**
29
83
  * Condition: internal links resolve to an existing repo file. Static-site
30
84
  * corpora with extensionless links pass `tryExtensions`/`tryIndex`, e.g.
@@ -33,6 +87,43 @@ export class LinkRuleBuilder extends RuleBuilder {
33
87
  resolve(options) {
34
88
  return this.addCondition(linkResolves(this.project, options));
35
89
  }
90
+ /**
91
+ * Filter to links whose target is not a frozen document (plan 0406) — a frozen
92
+ * record is history and cannot be edited to answer. Pass the options you pass
93
+ * `beLinkedBack()`, so the two resolve a link the same way. Every other target
94
+ * is kept, and examined.
95
+ */
96
+ haveLiveTargets(options) {
97
+ return this.addPredicate(haveLiveTargetsPredicate(this.project, options));
98
+ }
99
+ /**
100
+ * Condition: each link's target links back to the link's document (plan 0406).
101
+ * Any link back answers; several are one answer, and an incidental mention
102
+ * counts. A target that cannot answer — frozen, outside the corpus, missing, a
103
+ * directory, the record itself, an external URL — is one finding naming its
104
+ * cause, with its remedy as the suggestion. Takes the options `resolve()`
105
+ * takes, so a corpus with extensionless links resolves both ends the same way.
106
+ */
107
+ beLinkedBack(options) {
108
+ return this.addCondition(linkedBack(this.project, options));
109
+ }
110
+ /**
111
+ * The base result, plus findings about the declarations themselves (plan
112
+ * 0405). The base evidence passes through unchanged: the declarations' scan
113
+ * examines nothing the rule asserts over, so it must not raise `examined`
114
+ * (which would hide the zero-examined finding when every declaration is
115
+ * misspelt) and must not go through `mergeCollectResults`, whose dead-member
116
+ * rule would read an empty findings part as a dead rule on every clean run.
117
+ */
118
+ collectViolations() {
119
+ const base = super.collectViolations();
120
+ if (this._declarations.length === 0)
121
+ return base;
122
+ const findings = declarationFindings(this.project, this._declarations, this.buildConditionContext());
123
+ if (findings.length === 0)
124
+ return base;
125
+ return collectResult([...base, ...findings], base);
126
+ }
36
127
  }
37
128
  /** Entry point: build rules over the corpus's markdown links. */
38
129
  export function links(corpus) {
@@ -1 +1 @@
1
- {"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/builders/links.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAkB,MAAM,kBAAkB,CAAA;AAE9D,OAAO,EAAE,YAAY,EAA+B,MAAM,mBAAmB,CAAA;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EAAE,YAAY,EAA2B,MAAM,0BAA0B,CAAA;AAOhF,MAAM,WAAW,GAAG,GAAsB,EAAE,CAAC,CAAC;IAC5C,WAAW,EAAE,cAAc;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ;CACzB,CAAC,CAAA;AACF,MAAM,WAAW,GAAG,GAAsB,EAAE,CAAC,CAAC;IAC5C,WAAW,EAAE,cAAc;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ;CACxB,CAAC,CAAA;AAEF;;GAEG;AACH,MAAM,OAAO,eAAgB,SAAQ,WAA2B;IACpD,WAAW;QACnB,OAAO,iBAAiB,CAAY,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;IAC9F,CAAC;IAED,8DAA8D;IAC9D,WAAW;QACT,OAAO,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,CAAA;IACzC,CAAC;IAED,iFAAiF;IACjF,WAAW;QACT,OAAO,IAAI,CAAC,YAAY,CAAC,WAAW,EAAE,CAAC,CAAA;IACzC,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,OAA4B;QAClC,OAAO,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;IAC/D,CAAC;CACF;AAED,iEAAiE;AACjE,MAAM,UAAU,KAAK,CAAC,MAAc;IAClC,OAAO,IAAI,eAAe,CAAC,MAAM,CAAC,CAAA;AACpC,CAAC"}
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../src/builders/links.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { Condition } from '@nielspeter/eess';
2
+ import type { Corpus } from '../corpus.js';
3
+ import { type MdLink } from '../model/links.js';
4
+ import { type LinkResolveOptions } from '../model/resolve-link.js';
5
+ /**
6
+ * Condition (plan 0406): each link's target links back to the link's document.
7
+ * Any link back answers — several are one answer, and an incidental mention
8
+ * counts. The back-links are every internal link in the corpus, resolved with
9
+ * `resolveLink` and the same `options`, indexed on first evaluation.
10
+ *
11
+ * `examined` is the selected links: an empty selection fails with the
12
+ * zero-examined finding. A link that cannot answer — a frozen record, a file
13
+ * outside the corpus, a missing target — is one finding naming its cause, on
14
+ * the declaring link's line, with its remedy as the suggestion.
15
+ */
16
+ export declare function linkedBack(corpus: Corpus, options?: LinkResolveOptions): Condition<MdLink>;
17
+ //# sourceMappingURL=linked-back.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linked-back.d.ts","sourceRoot":"","sources":["../../src/conditions/linked-back.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAE7D,OAAO,EAIL,KAAK,kBAAkB,EAExB,MAAM,0BAA0B,CAAA;AAwFjC;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,SAAS,CAAC,MAAM,CAAC,CA8B9F"}
@@ -0,0 +1,122 @@
1
+ import { collectLinks } from '../model/links.js';
2
+ import { mdViolation } from '../model/violation.js';
3
+ import { basenameIndex, movedLinkFix, resolveLink, } from '../model/resolve-link.js';
4
+ /** Each document's repo path → the documents it links to, resolved as `options` resolves. */
5
+ function linksFrom(corpus, options) {
6
+ const out = new Map();
7
+ for (const doc of corpus.documents()) {
8
+ const targets = new Set();
9
+ for (const link of collectLinks(doc.root, doc.text)) {
10
+ const target = resolveLink({ url: link.url, external: link.external, doc }, corpus, options);
11
+ if (target.kind === 'document')
12
+ targets.add(target.path);
13
+ }
14
+ out.set(doc.relPath, targets);
15
+ }
16
+ return out;
17
+ }
18
+ function unanswered(link, target) {
19
+ const source = link.doc.relPath;
20
+ switch (target.kind) {
21
+ case 'document':
22
+ return target.doc.frozen
23
+ ? {
24
+ target: target.path,
25
+ message: `${target.path} is frozen and cannot answer`,
26
+ suggestion: `add .haveLiveTargets() to the rule, passing it the options you pass beLinkedBack(), or remove ${target.path} from the declaration`,
27
+ }
28
+ : {
29
+ target: target.path,
30
+ message: `${source} declares ${target.path}, and ${target.path} does not link back`,
31
+ suggestion: `add a link to ${source} in ${target.path}, or remove ${target.path} from the declaration`,
32
+ };
33
+ case 'not-in-corpus': {
34
+ // One finding, for the reason whose remedy comes first: a file that is not a
35
+ // record is not fixed by widening the corpus.
36
+ if (target.reasons.includes('not-markdown')) {
37
+ return {
38
+ target: target.path,
39
+ message: `${target.path} is not a Markdown record`,
40
+ suggestion: 'declare a record, not a file',
41
+ };
42
+ }
43
+ if (target.reasons.includes('ignored')) {
44
+ return {
45
+ target: target.path,
46
+ message: `${target.path} matches the corpus ignore option, so its links are not read`,
47
+ suggestion: 'correct the link, or stop ignoring the path',
48
+ };
49
+ }
50
+ return {
51
+ target: target.path,
52
+ message: `${target.path} is outside the corpus, so its links are not read`,
53
+ suggestion: 'add its folder to the corpus roots, or correct the link',
54
+ };
55
+ }
56
+ case 'missing':
57
+ return {
58
+ target: link.url,
59
+ message: `${link.url} does not exist`,
60
+ suggestion: 'correct the link to the record it means — the same fix clears resolve()',
61
+ };
62
+ case 'directory':
63
+ return {
64
+ target: target.path,
65
+ message: `the link names a directory, ${target.path}, not a record`,
66
+ suggestion: "link the record's file",
67
+ };
68
+ case 'self':
69
+ return {
70
+ target: source,
71
+ message: 'the link points at this record',
72
+ suggestion: 'remove it; a record does not relate to itself',
73
+ };
74
+ case 'external':
75
+ return {
76
+ target: link.url,
77
+ message: `the declaration links outside the repository, to ${link.url}`,
78
+ suggestion: 'declare a record in the corpus',
79
+ };
80
+ }
81
+ }
82
+ /**
83
+ * Condition (plan 0406): each link's target links back to the link's document.
84
+ * Any link back answers — several are one answer, and an incidental mention
85
+ * counts. The back-links are every internal link in the corpus, resolved with
86
+ * `resolveLink` and the same `options`, indexed on first evaluation.
87
+ *
88
+ * `examined` is the selected links: an empty selection fails with the
89
+ * zero-examined finding. A link that cannot answer — a frozen record, a file
90
+ * outside the corpus, a missing target — is one finding naming its cause, on
91
+ * the declaring link's line, with its remedy as the suggestion.
92
+ */
93
+ export function linkedBack(corpus, options = {}) {
94
+ let index;
95
+ let byBasename;
96
+ return {
97
+ description: 'be linked back by their targets',
98
+ evaluate: (links, ctx) => {
99
+ index ??= linksFrom(corpus, options);
100
+ const out = [];
101
+ for (const link of links) {
102
+ const target = resolveLink(link, corpus, options);
103
+ if (target.kind === 'document' && index.get(target.path)?.has(link.doc.relPath) === true) {
104
+ continue;
105
+ }
106
+ const cause = unanswered(link, target);
107
+ byBasename ??= basenameIndex(corpus);
108
+ out.push(mdViolation({
109
+ element: `${link.doc.relPath} → ${cause.target}`,
110
+ file: link.doc.file,
111
+ line: link.line,
112
+ message: cause.message,
113
+ sourceText: link.doc.text,
114
+ fix: target.kind === 'missing' ? movedLinkFix(link, byBasename) : undefined,
115
+ context: { ...ctx, suggestion: cause.suggestion },
116
+ }));
117
+ }
118
+ return out;
119
+ },
120
+ };
121
+ }
122
+ //# sourceMappingURL=linked-back.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linked-back.js","sourceRoot":"","sources":["../../src/conditions/linked-back.ts"],"names":[],"mappings":"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"}
@@ -1,55 +1,23 @@
1
1
  import type { Condition } from '@nielspeter/eess';
2
2
  import type { Corpus } from '../corpus.js';
3
3
  import type { MdLink } from '../model/links.js';
4
+ import { type LinkResolveOptions } from '../model/resolve-link.js';
4
5
  /**
5
6
  * 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.
7
+ * Defined beside `resolveLink` (`model/resolve-link.ts`), the one resolver both
8
+ * share (plan 0404); re-exported here so existing imports keep working.
9
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
- * Resolve a link naming a real directory (`./fixed/` or `./fixed`, with or
30
- * without the trailing slash — both forms occur in this repo's own corpus)
31
- * even when it has no index file. Correct for a repo-hosted corpus (GitHub,
32
- * GitLab render a directory link as its listing); wrong for a static-site
33
- * corpus where a bare directory is not itself a page — there, reach for
34
- * {@link tryIndex} instead. Off by default: this widens what "resolves"
35
- * means, and a link resolving that shouldn't is a false green, not a
36
- * convenience. Default `false`.
37
- *
38
- * A link naming a real directory always wins over an unrelated typo — if
39
- * `./foo` was meant as a deleted `foo.md` but a directory `foo/` happens to
40
- * exist at that path, it resolves as the directory. Not a new failure mode:
41
- * GitHub renders that link as the directory listing too, so this only
42
- * closes the gap between the gate and what the link actually does when
43
- * followed, it doesn't introduce a new way to be surprised by it.
44
- */
45
- readonly resolveDirectories?: boolean;
46
- }
10
+ export type { LinkResolveOptions } from '../model/resolve-link.js';
47
11
  /**
48
12
  * Condition: every (internal) link resolves to a file in the corpus's repo tree.
49
13
  * Closes over the `Corpus` for the file index. External links and pure anchors
50
14
  * are skipped. `options` adds extensionless-link resolution (`tryExtensions`,
51
15
  * `tryIndex`) for static-site corpora, and directory resolution
52
16
  * (`resolveDirectories`) for repo-hosted ones.
17
+ *
18
+ * Resolution is `resolveLink`'s (plan 0404): this condition only decides which
19
+ * of its cases count as resolved — a loaded document, an existing file outside
20
+ * the corpus, and (with `resolveDirectories`) a directory.
53
21
  */
54
22
  export declare function linkResolves(corpus: Corpus, options?: LinkResolveOptions): Condition<MdLink>;
55
23
  //# sourceMappingURL=resolve.d.ts.map
@@ -1 +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;IACzB;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CACtC;AA8DD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,SAAS,CAAC,MAAM,CAAC,CAyEhG"}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/conditions/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC1C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EAKL,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAA;AAEjC;;;;GAIG;AACH,YAAY,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAElE;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,SAAS,CAAC,MAAM,CAAC,CA+ChG"}