@timiagama/ecr 0.0.1 → 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 (138) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +16 -0
  3. package/README.md +411 -7
  4. package/dist/bin.d.ts +20 -0
  5. package/dist/bin.d.ts.map +1 -0
  6. package/dist/bin.js +26 -0
  7. package/dist/bin.js.map +1 -0
  8. package/dist/cli/corpus-loader.d.ts +198 -0
  9. package/dist/cli/corpus-loader.d.ts.map +1 -0
  10. package/dist/cli/corpus-loader.js +351 -0
  11. package/dist/cli/corpus-loader.js.map +1 -0
  12. package/dist/cli/corpus-statistics.d.ts +113 -0
  13. package/dist/cli/corpus-statistics.d.ts.map +1 -0
  14. package/dist/cli/corpus-statistics.js +119 -0
  15. package/dist/cli/corpus-statistics.js.map +1 -0
  16. package/dist/cli/diagnostic-reporter.d.ts +103 -0
  17. package/dist/cli/diagnostic-reporter.d.ts.map +1 -0
  18. package/dist/cli/diagnostic-reporter.js +254 -0
  19. package/dist/cli/diagnostic-reporter.js.map +1 -0
  20. package/dist/cli/project-ignore.d.ts +113 -0
  21. package/dist/cli/project-ignore.d.ts.map +1 -0
  22. package/dist/cli/project-ignore.js +199 -0
  23. package/dist/cli/project-ignore.js.map +1 -0
  24. package/dist/cli/safe-text.d.ts +25 -0
  25. package/dist/cli/safe-text.d.ts.map +1 -0
  26. package/dist/cli/safe-text.js +34 -0
  27. package/dist/cli/safe-text.js.map +1 -0
  28. package/dist/cli/supervisor.d.ts +108 -0
  29. package/dist/cli/supervisor.d.ts.map +1 -0
  30. package/dist/cli/supervisor.js +139 -0
  31. package/dist/cli/supervisor.js.map +1 -0
  32. package/dist/cli.d.ts +247 -0
  33. package/dist/cli.d.ts.map +1 -0
  34. package/dist/cli.js +591 -0
  35. package/dist/cli.js.map +1 -0
  36. package/dist/corpus-validator.d.ts +183 -0
  37. package/dist/corpus-validator.d.ts.map +1 -0
  38. package/dist/corpus-validator.js +479 -0
  39. package/dist/corpus-validator.js.map +1 -0
  40. package/dist/document-identity-rule.d.ts +198 -0
  41. package/dist/document-identity-rule.d.ts.map +1 -0
  42. package/dist/document-identity-rule.js +244 -0
  43. package/dist/document-identity-rule.js.map +1 -0
  44. package/dist/ecr.d.ts +103 -0
  45. package/dist/ecr.d.ts.map +1 -0
  46. package/dist/ecr.js +216 -0
  47. package/dist/ecr.js.map +1 -0
  48. package/dist/glob-pattern.d.ts +94 -0
  49. package/dist/glob-pattern.d.ts.map +1 -0
  50. package/dist/glob-pattern.js +188 -0
  51. package/dist/glob-pattern.js.map +1 -0
  52. package/dist/heading-source-form.d.ts +77 -0
  53. package/dist/heading-source-form.d.ts.map +1 -0
  54. package/dist/heading-source-form.js +129 -0
  55. package/dist/heading-source-form.js.map +1 -0
  56. package/dist/identifier-grammar.d.ts +212 -0
  57. package/dist/identifier-grammar.d.ts.map +1 -0
  58. package/dist/identifier-grammar.js +273 -0
  59. package/dist/identifier-grammar.js.map +1 -0
  60. package/dist/index.d.ts +19 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +11 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/inline-reference-rule.d.ts +557 -0
  65. package/dist/inline-reference-rule.d.ts.map +1 -0
  66. package/dist/inline-reference-rule.js +802 -0
  67. package/dist/inline-reference-rule.js.map +1 -0
  68. package/dist/meta-documents.d.ts +91 -0
  69. package/dist/meta-documents.d.ts.map +1 -0
  70. package/dist/meta-documents.js +133 -0
  71. package/dist/meta-documents.js.map +1 -0
  72. package/dist/per-document-visitor.d.ts +510 -0
  73. package/dist/per-document-visitor.d.ts.map +1 -0
  74. package/dist/per-document-visitor.js +1032 -0
  75. package/dist/per-document-visitor.js.map +1 -0
  76. package/dist/references-section-rule.d.ts +482 -0
  77. package/dist/references-section-rule.d.ts.map +1 -0
  78. package/dist/references-section-rule.js +777 -0
  79. package/dist/references-section-rule.js.map +1 -0
  80. package/dist/section-hierarchy-rule.d.ts +299 -0
  81. package/dist/section-hierarchy-rule.d.ts.map +1 -0
  82. package/dist/section-hierarchy-rule.js +482 -0
  83. package/dist/section-hierarchy-rule.js.map +1 -0
  84. package/dist/source-alignment.d.ts +41 -0
  85. package/dist/source-alignment.d.ts.map +1 -0
  86. package/dist/source-alignment.js +166 -0
  87. package/dist/source-alignment.js.map +1 -0
  88. package/dist/source-lines.d.ts +69 -0
  89. package/dist/source-lines.d.ts.map +1 -0
  90. package/dist/source-lines.js +104 -0
  91. package/dist/source-lines.js.map +1 -0
  92. package/dist/spec-version.d.ts +19 -0
  93. package/dist/spec-version.d.ts.map +1 -0
  94. package/dist/spec-version.js +19 -0
  95. package/dist/spec-version.js.map +1 -0
  96. package/dist/types.d.ts +219 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +11 -0
  99. package/dist/types.js.map +1 -0
  100. package/examples/docs/0. Orientation/0.0 - System Overview.md +34 -0
  101. package/examples/docs/0. Orientation/0.0.1 - Glossary.md +21 -0
  102. package/examples/docs/0. Orientation/0.0.2 - Quality Gates.md +22 -0
  103. package/examples/docs/3. Ingestion/3.1 - Ingestion - Validation Rules.md +33 -0
  104. package/examples/docs/4. Payments/4.10 - Settlement.md +22 -0
  105. package/examples/docs/4. Payments/4.2 - Payment Processing Contract.md +33 -0
  106. package/examples/docs/8. Orchestration/8.1 - Workflow Orchestration Contract.md +38 -0
  107. package/examples/docs/8. Orchestration/8.1.3 - Retry Policy.md +17 -0
  108. package/examples/docs/8. Orchestration/8.10 - Dead Letter Queue.md +20 -0
  109. package/examples/docs/README.md +7 -0
  110. package/package.json +65 -7
  111. package/protocol/navigation-protocol.md +223 -0
  112. package/spec/v2/1 - ECR - Structural Specification.md +1467 -0
  113. package/spec/v2/2 - ECR - User Guide.md +845 -0
  114. package/spec/v2/3 - Design Rationale - The Section Separator.md +243 -0
  115. package/src/bin.ts +30 -0
  116. package/src/cli/corpus-loader.ts +482 -0
  117. package/src/cli/corpus-statistics.ts +196 -0
  118. package/src/cli/diagnostic-reporter.ts +344 -0
  119. package/src/cli/project-ignore.ts +238 -0
  120. package/src/cli/safe-text.ts +39 -0
  121. package/src/cli/supervisor.ts +204 -0
  122. package/src/cli.ts +836 -0
  123. package/src/corpus-validator.ts +642 -0
  124. package/src/document-identity-rule.ts +422 -0
  125. package/src/ecr.ts +298 -0
  126. package/src/glob-pattern.ts +242 -0
  127. package/src/heading-source-form.ts +173 -0
  128. package/src/identifier-grammar.ts +380 -0
  129. package/src/index.ts +78 -0
  130. package/src/inline-reference-rule.ts +1235 -0
  131. package/src/meta-documents.ts +159 -0
  132. package/src/per-document-visitor.ts +1528 -0
  133. package/src/references-section-rule.ts +1182 -0
  134. package/src/section-hierarchy-rule.ts +705 -0
  135. package/src/source-alignment.ts +209 -0
  136. package/src/source-lines.ts +123 -0
  137. package/src/spec-version.ts +19 -0
  138. package/src/types.ts +267 -0
@@ -0,0 +1,183 @@
1
+ import type { CorpusDocumentEntry, CorpusResult } from './types.js';
2
+ /**
3
+ * Validates a corpus of ECR documents for corpus-wide structural integrity.
4
+ *
5
+ * Performs Pass 2 of the ECR validation algorithm (per 1#11.2):
6
+ * global uniqueness of DocIDs and SectionIDs, resolution of reference
7
+ * targets, and resolution of inline reference targets.
8
+ *
9
+ * This class is stateless. Each invocation of {@link validateCorpus}
10
+ * operates purely over the provided document entries and produces a
11
+ * self-contained {@link CorpusResult}.
12
+ */
13
+ declare class CorpusValidator {
14
+ /**
15
+ * Validates corpus-wide structural invariants across a set of
16
+ * per-document extraction results.
17
+ *
18
+ * Documents whose per-document lint result does not include a
19
+ * successfully extracted artefact are excluded from corpus-wide
20
+ * indexing and cross-reference resolution.
21
+ *
22
+ * Corpus-wide violations are reported as diagnostics with severity
23
+ * `'error'` (per 1#9.9, 1#12.2), except a stale References title, which
24
+ * is a `'warning'` (1#9.9 rule 5).
25
+ *
26
+ * @param documents - The per-document entries to validate as a corpus.
27
+ * @returns A {@link CorpusResult} containing per-document results,
28
+ * corpus-wide diagnostics, and an optional global identifier index.
29
+ */
30
+ validateCorpus(documents: readonly CorpusDocumentEntry[]): CorpusResult;
31
+ /**
32
+ * Filters the input document entries to only those with successfully
33
+ * extracted structural artefacts.
34
+ *
35
+ * Documents without an `extracted` field on their lint result are excluded
36
+ * from corpus-wide indexing and cross-reference resolution.
37
+ *
38
+ * @param documents - The full set of corpus document entries.
39
+ * @returns An array of indexable documents with guaranteed `extracted` fields.
40
+ */
41
+ private collectIndexableDocuments;
42
+ /**
43
+ * Builds a global DocID-to-URI index and emits error diagnostics for
44
+ * any duplicate DocIDs found across the corpus.
45
+ *
46
+ * Per 1#9.9: DocID MUST be globally unique. Violations are errors
47
+ * (per 1#12.2).
48
+ *
49
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
50
+ * @param accumulator - Diagnostic accumulator for recording violations.
51
+ * @returns A map from each DocID to the URI of its first-encountered defining document.
52
+ */
53
+ private buildDocIdIndex;
54
+ /**
55
+ * Builds a global SectionID-to-URI index and emits error diagnostics for
56
+ * any duplicate SectionIDs found across the corpus.
57
+ *
58
+ * Per 1#9.9: SectionID MUST be globally unique across corpus.
59
+ * Violations are errors (per 1#12.2).
60
+ *
61
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
62
+ * @param accumulator - Diagnostic accumulator for recording violations.
63
+ * @returns A map from each SectionID to the URI of its first-encountered defining document.
64
+ */
65
+ private buildSectionIdIndex;
66
+ /**
67
+ * Maps each DocID to the title in its document's H1.
68
+ *
69
+ * Where a DocID is duplicated, the first-encountered document wins, matching
70
+ * {@link CorpusValidator.buildDocIdIndex}. The duplicate is reported there.
71
+ *
72
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
73
+ * @returns A map from each DocID to its canonical title.
74
+ */
75
+ private buildCanonicalTitleIndex;
76
+ /**
77
+ * Resolves all `ReferenceEdge.toDocId` targets against the global DocID
78
+ * index and emits error diagnostics for unresolved targets.
79
+ *
80
+ * Per 1#9.9 rule 3: Every TargetDocID in References MUST resolve to an
81
+ * existing DocID. Violations are errors (per 1#12.2).
82
+ *
83
+ * Per 1#9.9 rule 5: where the target resolves but the entry's title differs
84
+ * from the target's H1 title, a warning is emitted carrying the canonical
85
+ * title. Titles are not part of graph identity, so a stale title never
86
+ * fails validation.
87
+ *
88
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
89
+ * @param docIdIndex - The global DocID-to-URI index.
90
+ * @param canonicalTitles - Each DocID's title as written in its own H1.
91
+ * @param accumulator - Diagnostic accumulator for recording violations.
92
+ */
93
+ private resolveReferenceTargets;
94
+ /**
95
+ * Resolves all `InlineReferenceEdge.toId` targets against the union of
96
+ * DocID and SectionID indexes and emits error diagnostics for unresolved
97
+ * targets.
98
+ *
99
+ * Per 1#9.9 rule 4: Every inline TargetID MUST resolve to an existing
100
+ * SectionID. Violations are errors (per 1#12.2).
101
+ *
102
+ * Note: the spec says "existing SectionID" but the algorithm (1#11.2
103
+ * step 5) resolves against "the union of DocID and SectionID indexes",
104
+ * meaning a DocID is also a valid resolution target for inline references.
105
+ *
106
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
107
+ * @param docIdIndex - The global DocID-to-URI index.
108
+ * @param sectionIdIndex - The global SectionID-to-URI index.
109
+ * @param accumulator - Diagnostic accumulator for recording violations.
110
+ */
111
+ private resolveInlineReferenceTargets;
112
+ /**
113
+ * Validates that for each extracted inline reference whose target is a
114
+ * SectionID, the DocID of that SectionID is declared in the referring
115
+ * document's References section.
116
+ *
117
+ * Per 1#9.5 rule 2 and 1#11.2 step 6: if an inline reference targets
118
+ * `X#Y`, the DocID `X` MUST appear in the References section of the
119
+ * referring document.
120
+ *
121
+ * The linter itself never extracts an undeclared edge, so through the
122
+ * {@link Ecr} facade this cannot fire; undeclared targets are handled by
123
+ * {@link CorpusValidator.escalateUndeclaredInlineTargets}. It guards hosts
124
+ * that assemble extracted documents themselves.
125
+ *
126
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
127
+ * @param docIdIndex - The global DocID-to-URI index.
128
+ * @param accumulator - Diagnostic accumulator for recording violations.
129
+ */
130
+ private validateInlineReferenceParentDeclarations;
131
+ /**
132
+ * Raises a document-level undeclared-target warning to an error when the
133
+ * target turns out to be a document in the corpus.
134
+ *
135
+ * Per 1#9.5 rule 3: a single document cannot tell `per 3.1` (a reference
136
+ * whose declaration is missing) from `per 60 seconds` (prose), so ECR104
137
+ * only warns. Here the corpus is known. If DocID `3.1` exists, the author
138
+ * has referenced a real document without declaring it, which is an error.
139
+ * If no document `60` exists, the warning stands on its own.
140
+ *
141
+ * An undeclared SectionID target (`per 3.1#2`) is already an ECR104 error,
142
+ * so only warnings are considered here; raising it again would report the
143
+ * same mistake twice.
144
+ *
145
+ * @param documents - The per-document entries, including their diagnostics.
146
+ * @param docIdIndex - The global DocID-to-URI index.
147
+ * @param accumulator - Diagnostic accumulator for recording violations.
148
+ */
149
+ private escalateUndeclaredInlineTargets;
150
+ /**
151
+ * Converts the mutable maps into a frozen {@link CorpusIndex}.
152
+ *
153
+ * @param docIdIndex - The global DocID-to-URI map.
154
+ * @param sectionIdIndex - The global SectionID-to-URI map.
155
+ * @returns A frozen corpus index suitable for graph construction.
156
+ */
157
+ private buildCorpusIndex;
158
+ /**
159
+ * Collects the set of target DocIDs declared in a document's References
160
+ * section.
161
+ *
162
+ * @param extracted - The extracted document whose references are inspected.
163
+ * @returns A set of DocIDs declared as reference targets.
164
+ */
165
+ private collectDeclaredReferenceDocIds;
166
+ /**
167
+ * Finds the DocID a SectionID belongs to.
168
+ *
169
+ * This is a direct read rather than a search: everything before the `#`
170
+ * separator is the DocID. A dotted-only grammar would have to try
171
+ * progressively longer dotted prefixes against the index, which cannot
172
+ * distinguish section `1` of document `0.0.1` from section `1.3` of
173
+ * document `0.0`, and silently prefers whichever the corpus happens to
174
+ * contain.
175
+ *
176
+ * @param sectionId - The SectionID whose DocID is sought.
177
+ * @param docIdIndex - The global DocID-to-URI index, used to confirm the document exists.
178
+ * @returns The DocID the section belongs to, or `undefined` if it is not in the corpus.
179
+ */
180
+ private findParentDocId;
181
+ }
182
+ export { CorpusValidator };
183
+ //# sourceMappingURL=corpus-validator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"corpus-validator.d.ts","sourceRoot":"","sources":["../src/corpus-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EAEnB,YAAY,EAKb,MAAM,YAAY,CAAC;AAoCpB;;;;;;;;;;GAUG;AACH,cAAM,eAAe;IACnB;;;;;;;;;;;;;;;OAeG;IACI,cAAc,CACnB,SAAS,EAAE,SAAS,mBAAmB,EAAE,GACxC,YAAY;IA2Df;;;;;;;;;OASG;IACH,OAAO,CAAC,yBAAyB;IAqBjC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,eAAe;IAyCvB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,mBAAmB;IAuD3B;;;;;;;;OAQG;IACH,OAAO,CAAC,wBAAwB;IAchC;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,uBAAuB;IA2C/B;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,6BAA6B;IA8BrC;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,yCAAyC;IAuDjD;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,+BAA+B;IA2DvC;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAsBxB;;;;;;OAMG;IACH,OAAO,CAAC,8BAA8B;IAYtC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,eAAe;CAexB;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
@@ -0,0 +1,479 @@
1
+ import { CITATION_SOURCE_FORM_CAUSE, INLINE_REFERENCE_RULE_ID, UNDECLARED_TARGET_REASON, } from './inline-reference-rule.js';
2
+ // ---------------------------------------------------------------------------
3
+ // CorpusValidator
4
+ // ---------------------------------------------------------------------------
5
+ /**
6
+ * Validates a corpus of ECR documents for corpus-wide structural integrity.
7
+ *
8
+ * Performs Pass 2 of the ECR validation algorithm (per 1#11.2):
9
+ * global uniqueness of DocIDs and SectionIDs, resolution of reference
10
+ * targets, and resolution of inline reference targets.
11
+ *
12
+ * This class is stateless. Each invocation of {@link validateCorpus}
13
+ * operates purely over the provided document entries and produces a
14
+ * self-contained {@link CorpusResult}.
15
+ */
16
+ class CorpusValidator {
17
+ /**
18
+ * Validates corpus-wide structural invariants across a set of
19
+ * per-document extraction results.
20
+ *
21
+ * Documents whose per-document lint result does not include a
22
+ * successfully extracted artefact are excluded from corpus-wide
23
+ * indexing and cross-reference resolution.
24
+ *
25
+ * Corpus-wide violations are reported as diagnostics with severity
26
+ * `'error'` (per 1#9.9, 1#12.2), except a stale References title, which
27
+ * is a `'warning'` (1#9.9 rule 5).
28
+ *
29
+ * @param documents - The per-document entries to validate as a corpus.
30
+ * @returns A {@link CorpusResult} containing per-document results,
31
+ * corpus-wide diagnostics, and an optional global identifier index.
32
+ */
33
+ validateCorpus(documents) {
34
+ const indexableDocuments = this.collectIndexableDocuments(documents);
35
+ const accumulator = { items: [] };
36
+ // Step 1–3: Build indexes and validate global uniqueness
37
+ const docIdIndex = this.buildDocIdIndex(indexableDocuments, accumulator);
38
+ const sectionIdIndex = this.buildSectionIdIndex(indexableDocuments, accumulator);
39
+ const hasDuplicateIdentifiers = accumulator.items.length > 0;
40
+ // Step 4: Resolve ReferenceEdge.toDocId targets, and warn where an
41
+ // entry's title has drifted from the target document's own title
42
+ this.resolveReferenceTargets(indexableDocuments, docIdIndex, this.buildCanonicalTitleIndex(indexableDocuments), accumulator);
43
+ // Step 5: Resolve InlineReferenceEdge.toId targets
44
+ this.resolveInlineReferenceTargets(indexableDocuments, docIdIndex, sectionIdIndex, accumulator);
45
+ // Step 6: Validate inline reference parent DocID declared in References
46
+ this.validateInlineReferenceParentDeclarations(indexableDocuments, docIdIndex, accumulator);
47
+ // Step 6b: An undeclared inline target that names a real document is an error
48
+ this.escalateUndeclaredInlineTargets(documents, docIdIndex, accumulator);
49
+ if (hasDuplicateIdentifiers) {
50
+ return {
51
+ documents,
52
+ diagnostics: accumulator.items,
53
+ };
54
+ }
55
+ return {
56
+ documents,
57
+ index: this.buildCorpusIndex(docIdIndex, sectionIdIndex),
58
+ diagnostics: accumulator.items,
59
+ };
60
+ }
61
+ // -------------------------------------------------------------------------
62
+ // Private helpers — filtering
63
+ // -------------------------------------------------------------------------
64
+ /**
65
+ * Filters the input document entries to only those with successfully
66
+ * extracted structural artefacts.
67
+ *
68
+ * Documents without an `extracted` field on their lint result are excluded
69
+ * from corpus-wide indexing and cross-reference resolution.
70
+ *
71
+ * @param documents - The full set of corpus document entries.
72
+ * @returns An array of indexable documents with guaranteed `extracted` fields.
73
+ */
74
+ collectIndexableDocuments(documents) {
75
+ const indexable = [];
76
+ for (const entry of documents) {
77
+ if (entry.result.extracted !== undefined) {
78
+ indexable.push({
79
+ uri: entry.uri,
80
+ extracted: entry.result.extracted,
81
+ });
82
+ }
83
+ }
84
+ return indexable;
85
+ }
86
+ // -------------------------------------------------------------------------
87
+ // Private helpers — index building (Steps 1–3)
88
+ // -------------------------------------------------------------------------
89
+ /**
90
+ * Builds a global DocID-to-URI index and emits error diagnostics for
91
+ * any duplicate DocIDs found across the corpus.
92
+ *
93
+ * Per 1#9.9: DocID MUST be globally unique. Violations are errors
94
+ * (per 1#12.2).
95
+ *
96
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
97
+ * @param accumulator - Diagnostic accumulator for recording violations.
98
+ * @returns A map from each DocID to the URI of its first-encountered defining document.
99
+ */
100
+ buildDocIdIndex(indexableDocuments, accumulator) {
101
+ const docIdIndex = new Map();
102
+ const duplicateDocIds = new Map();
103
+ for (const document of indexableDocuments) {
104
+ const docId = document.extracted.docId;
105
+ const existingUri = docIdIndex.get(docId);
106
+ if (existingUri !== undefined) {
107
+ const existing = duplicateDocIds.get(docId);
108
+ if (existing !== undefined) {
109
+ duplicateDocIds.set(docId, [...existing, document.uri]);
110
+ }
111
+ else {
112
+ duplicateDocIds.set(docId, [existingUri, document.uri]);
113
+ }
114
+ }
115
+ else {
116
+ docIdIndex.set(docId, document.uri);
117
+ }
118
+ }
119
+ for (const [docId, uris] of duplicateDocIds) {
120
+ for (const uri of uris) {
121
+ accumulator.items.push({
122
+ severity: 'error',
123
+ ruleId: 'corpus/duplicate-doc-id',
124
+ message: `Duplicate DocID '${docId}' found in multiple documents`,
125
+ uri,
126
+ docId,
127
+ data: { duplicateUris: uris },
128
+ });
129
+ }
130
+ }
131
+ return docIdIndex;
132
+ }
133
+ /**
134
+ * Builds a global SectionID-to-URI index and emits error diagnostics for
135
+ * any duplicate SectionIDs found across the corpus.
136
+ *
137
+ * Per 1#9.9: SectionID MUST be globally unique across corpus.
138
+ * Violations are errors (per 1#12.2).
139
+ *
140
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
141
+ * @param accumulator - Diagnostic accumulator for recording violations.
142
+ * @returns A map from each SectionID to the URI of its first-encountered defining document.
143
+ */
144
+ buildSectionIdIndex(indexableDocuments, accumulator) {
145
+ const sectionIdIndex = new Map();
146
+ const duplicateSectionIds = new Map();
147
+ for (const document of indexableDocuments) {
148
+ for (const section of document.extracted.sections) {
149
+ // Skip the root H1 node — its id is the DocID, handled by DocID index
150
+ if (section.headingDepth === 1) {
151
+ continue;
152
+ }
153
+ const sectionId = section.id;
154
+ const existingUri = sectionIdIndex.get(sectionId);
155
+ if (existingUri !== undefined) {
156
+ const existing = duplicateSectionIds.get(sectionId);
157
+ if (existing !== undefined) {
158
+ duplicateSectionIds.set(sectionId, [...existing, document.uri]);
159
+ }
160
+ else {
161
+ duplicateSectionIds.set(sectionId, [existingUri, document.uri]);
162
+ }
163
+ }
164
+ else {
165
+ sectionIdIndex.set(sectionId, document.uri);
166
+ }
167
+ }
168
+ }
169
+ for (const [sectionId, uris] of duplicateSectionIds) {
170
+ for (const uri of uris) {
171
+ const baseDiagnostic = {
172
+ severity: 'error',
173
+ ruleId: 'corpus/duplicate-section-id',
174
+ message: `Duplicate SectionID '${sectionId}' found in multiple documents`,
175
+ uri,
176
+ sectionId,
177
+ data: { duplicateUris: uris },
178
+ };
179
+ accumulator.items.push(baseDiagnostic);
180
+ }
181
+ }
182
+ return sectionIdIndex;
183
+ }
184
+ // -------------------------------------------------------------------------
185
+ // Private helpers — reference resolution (Steps 4–6)
186
+ // -------------------------------------------------------------------------
187
+ /**
188
+ * Maps each DocID to the title in its document's H1.
189
+ *
190
+ * Where a DocID is duplicated, the first-encountered document wins, matching
191
+ * {@link CorpusValidator.buildDocIdIndex}. The duplicate is reported there.
192
+ *
193
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
194
+ * @returns A map from each DocID to its canonical title.
195
+ */
196
+ buildCanonicalTitleIndex(indexableDocuments) {
197
+ const canonicalTitles = new Map();
198
+ for (const document of indexableDocuments) {
199
+ if (!canonicalTitles.has(document.extracted.docId)) {
200
+ canonicalTitles.set(document.extracted.docId, document.extracted.title.trim());
201
+ }
202
+ }
203
+ return canonicalTitles;
204
+ }
205
+ /**
206
+ * Resolves all `ReferenceEdge.toDocId` targets against the global DocID
207
+ * index and emits error diagnostics for unresolved targets.
208
+ *
209
+ * Per 1#9.9 rule 3: Every TargetDocID in References MUST resolve to an
210
+ * existing DocID. Violations are errors (per 1#12.2).
211
+ *
212
+ * Per 1#9.9 rule 5: where the target resolves but the entry's title differs
213
+ * from the target's H1 title, a warning is emitted carrying the canonical
214
+ * title. Titles are not part of graph identity, so a stale title never
215
+ * fails validation.
216
+ *
217
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
218
+ * @param docIdIndex - The global DocID-to-URI index.
219
+ * @param canonicalTitles - Each DocID's title as written in its own H1.
220
+ * @param accumulator - Diagnostic accumulator for recording violations.
221
+ */
222
+ resolveReferenceTargets(indexableDocuments, docIdIndex, canonicalTitles, accumulator) {
223
+ for (const document of indexableDocuments) {
224
+ for (const reference of document.extracted.references) {
225
+ const targetExists = docIdIndex.has(reference.toDocId);
226
+ if (!targetExists) {
227
+ accumulator.items.push({
228
+ severity: 'error',
229
+ ruleId: 'corpus/unresolved-reference-target',
230
+ message: `References entry target DocID '${reference.toDocId}' does not resolve to any document in the corpus`,
231
+ uri: document.uri,
232
+ docId: document.extracted.docId,
233
+ data: { unresolvedDocId: reference.toDocId },
234
+ });
235
+ continue;
236
+ }
237
+ const canonicalTitle = canonicalTitles.get(reference.toDocId);
238
+ const referencedTitle = reference.title.trim();
239
+ if (canonicalTitle !== undefined && referencedTitle !== canonicalTitle) {
240
+ accumulator.items.push({
241
+ severity: 'warning',
242
+ ruleId: 'corpus/reference-title-mismatch',
243
+ message: `References entry for '${reference.toDocId}' is titled '${referencedTitle}', but that document's title is '${canonicalTitle}'`,
244
+ uri: document.uri,
245
+ docId: document.extracted.docId,
246
+ data: {
247
+ targetDocId: reference.toDocId,
248
+ referencedTitle,
249
+ canonicalTitle,
250
+ },
251
+ });
252
+ }
253
+ }
254
+ }
255
+ }
256
+ /**
257
+ * Resolves all `InlineReferenceEdge.toId` targets against the union of
258
+ * DocID and SectionID indexes and emits error diagnostics for unresolved
259
+ * targets.
260
+ *
261
+ * Per 1#9.9 rule 4: Every inline TargetID MUST resolve to an existing
262
+ * SectionID. Violations are errors (per 1#12.2).
263
+ *
264
+ * Note: the spec says "existing SectionID" but the algorithm (1#11.2
265
+ * step 5) resolves against "the union of DocID and SectionID indexes",
266
+ * meaning a DocID is also a valid resolution target for inline references.
267
+ *
268
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
269
+ * @param docIdIndex - The global DocID-to-URI index.
270
+ * @param sectionIdIndex - The global SectionID-to-URI index.
271
+ * @param accumulator - Diagnostic accumulator for recording violations.
272
+ */
273
+ resolveInlineReferenceTargets(indexableDocuments, docIdIndex, sectionIdIndex, accumulator) {
274
+ for (const document of indexableDocuments) {
275
+ for (const inlineReference of document.extracted.inlineReferences) {
276
+ const targetId = inlineReference.toId;
277
+ const resolvedInDocIds = docIdIndex.has(targetId);
278
+ const resolvedInSectionIds = sectionIdIndex.has(targetId);
279
+ if (!resolvedInDocIds && !resolvedInSectionIds) {
280
+ accumulator.items.push({
281
+ severity: 'error',
282
+ ruleId: 'corpus/unresolved-inline-target',
283
+ message: `Inline reference target '${targetId}' does not resolve to any DocID or SectionID in the corpus`,
284
+ uri: document.uri,
285
+ docId: document.extracted.docId,
286
+ data: {
287
+ unresolvedTargetId: targetId,
288
+ fromId: inlineReference.fromId,
289
+ kind: inlineReference.kind,
290
+ },
291
+ });
292
+ }
293
+ }
294
+ }
295
+ }
296
+ /**
297
+ * Validates that for each extracted inline reference whose target is a
298
+ * SectionID, the DocID of that SectionID is declared in the referring
299
+ * document's References section.
300
+ *
301
+ * Per 1#9.5 rule 2 and 1#11.2 step 6: if an inline reference targets
302
+ * `X#Y`, the DocID `X` MUST appear in the References section of the
303
+ * referring document.
304
+ *
305
+ * The linter itself never extracts an undeclared edge, so through the
306
+ * {@link Ecr} facade this cannot fire; undeclared targets are handled by
307
+ * {@link CorpusValidator.escalateUndeclaredInlineTargets}. It guards hosts
308
+ * that assemble extracted documents themselves.
309
+ *
310
+ * @param indexableDocuments - Documents with successfully extracted artefacts.
311
+ * @param docIdIndex - The global DocID-to-URI index.
312
+ * @param accumulator - Diagnostic accumulator for recording violations.
313
+ */
314
+ validateInlineReferenceParentDeclarations(indexableDocuments, docIdIndex, accumulator) {
315
+ for (const document of indexableDocuments) {
316
+ const declaredTargetDocIds = this.collectDeclaredReferenceDocIds(document.extracted);
317
+ for (const inlineReference of document.extracted.inlineReferences) {
318
+ const targetId = inlineReference.toId;
319
+ // If the target is itself a known DocID, no parent-declaration
320
+ // check is needed — the inline reference targets the document
321
+ // directly.
322
+ if (docIdIndex.has(targetId)) {
323
+ continue;
324
+ }
325
+ // The target is a SectionID. Its DocID is the text before the `#`.
326
+ const parentDocId = this.findParentDocId(targetId, docIdIndex);
327
+ if (parentDocId === undefined) {
328
+ // No parent DocID found in corpus — the unresolved-inline-target
329
+ // diagnostic already covers this case (step 5). Skip here.
330
+ continue;
331
+ }
332
+ // If the parent DocID is the current document itself, no
333
+ // cross-document dependency declaration is needed — the
334
+ // section is local.
335
+ if (parentDocId === document.extracted.docId) {
336
+ continue;
337
+ }
338
+ if (!declaredTargetDocIds.has(parentDocId)) {
339
+ accumulator.items.push({
340
+ severity: 'error',
341
+ ruleId: 'corpus/undeclared-inline-target',
342
+ message: `Inline reference to '${targetId}' targets undeclared DocID '${parentDocId}' — declare it in the References section`,
343
+ uri: document.uri,
344
+ docId: document.extracted.docId,
345
+ data: {
346
+ targetId,
347
+ parentDocId,
348
+ fromId: inlineReference.fromId,
349
+ kind: inlineReference.kind,
350
+ },
351
+ });
352
+ }
353
+ }
354
+ }
355
+ }
356
+ /**
357
+ * Raises a document-level undeclared-target warning to an error when the
358
+ * target turns out to be a document in the corpus.
359
+ *
360
+ * Per 1#9.5 rule 3: a single document cannot tell `per 3.1` (a reference
361
+ * whose declaration is missing) from `per 60 seconds` (prose), so ECR104
362
+ * only warns. Here the corpus is known. If DocID `3.1` exists, the author
363
+ * has referenced a real document without declaring it, which is an error.
364
+ * If no document `60` exists, the warning stands on its own.
365
+ *
366
+ * An undeclared SectionID target (`per 3.1#2`) is already an ECR104 error,
367
+ * so only warnings are considered here; raising it again would report the
368
+ * same mistake twice.
369
+ *
370
+ * @param documents - The per-document entries, including their diagnostics.
371
+ * @param docIdIndex - The global DocID-to-URI index.
372
+ * @param accumulator - Diagnostic accumulator for recording violations.
373
+ */
374
+ escalateUndeclaredInlineTargets(documents, docIdIndex, accumulator) {
375
+ for (const entry of documents) {
376
+ for (const warning of entry.result.diagnostics) {
377
+ const targetDocId = warning.data?.targetDocId;
378
+ if (warning.ruleId !== INLINE_REFERENCE_RULE_ID ||
379
+ warning.severity !== 'warning' ||
380
+ warning.data?.reason !== UNDECLARED_TARGET_REASON ||
381
+ typeof targetDocId !== 'string' ||
382
+ !docIdIndex.has(targetDocId)) {
383
+ continue;
384
+ }
385
+ const targetId = warning.data.targetId;
386
+ const cause = warning.data.cause;
387
+ // A citation no search can find (1#9.5 rule 4) warns for the same
388
+ // reason an undeclared one does, and is raised here for the same
389
+ // reason. Declaring the target would not make it findable, so the
390
+ // message has to name the source form as well. Only an undeclared
391
+ // target ever warns, so both parts always apply.
392
+ const message = cause === CITATION_SOURCE_FORM_CAUSE
393
+ ? `Inline reference to '${String(targetId)}' is not written as adjacent literal ` +
394
+ `text on one line, so no search finds it, and DocID '${targetDocId}' exists in ` +
395
+ `the corpus, so it is certainly a citation — write it as literal text and ` +
396
+ `declare it in the References section`
397
+ : `Inline reference to '${String(targetId)}' targets DocID '${targetDocId}', ` +
398
+ `which exists in the corpus but is not declared — declare it in the References section`;
399
+ accumulator.items.push({
400
+ severity: 'error',
401
+ ruleId: 'corpus/undeclared-inline-target',
402
+ message,
403
+ uri: entry.uri,
404
+ ...(warning.range !== undefined ? { range: warning.range } : {}),
405
+ ...(entry.result.extracted !== undefined ? { docId: entry.result.extracted.docId } : {}),
406
+ data: {
407
+ ...(cause !== undefined ? { cause } : {}),
408
+ targetId,
409
+ parentDocId: targetDocId,
410
+ fromId: warning.data.fromId,
411
+ kind: warning.data.kind,
412
+ },
413
+ });
414
+ }
415
+ }
416
+ }
417
+ // -------------------------------------------------------------------------
418
+ // Private helpers — index construction
419
+ // -------------------------------------------------------------------------
420
+ /**
421
+ * Converts the mutable maps into a frozen {@link CorpusIndex}.
422
+ *
423
+ * @param docIdIndex - The global DocID-to-URI map.
424
+ * @param sectionIdIndex - The global SectionID-to-URI map.
425
+ * @returns A frozen corpus index suitable for graph construction.
426
+ */
427
+ buildCorpusIndex(docIdIndex, sectionIdIndex) {
428
+ const docIds = {};
429
+ const sectionIds = {};
430
+ for (const [docId, uri] of docIdIndex) {
431
+ docIds[docId] = uri;
432
+ }
433
+ for (const [sectionId, uri] of sectionIdIndex) {
434
+ sectionIds[sectionId] = uri;
435
+ }
436
+ return { docIds, sectionIds };
437
+ }
438
+ // -------------------------------------------------------------------------
439
+ // Private helpers — identifier utilities
440
+ // -------------------------------------------------------------------------
441
+ /**
442
+ * Collects the set of target DocIDs declared in a document's References
443
+ * section.
444
+ *
445
+ * @param extracted - The extracted document whose references are inspected.
446
+ * @returns A set of DocIDs declared as reference targets.
447
+ */
448
+ collectDeclaredReferenceDocIds(extracted) {
449
+ const declaredDocIds = new Set();
450
+ for (const reference of extracted.references) {
451
+ declaredDocIds.add(reference.toDocId);
452
+ }
453
+ return declaredDocIds;
454
+ }
455
+ /**
456
+ * Finds the DocID a SectionID belongs to.
457
+ *
458
+ * This is a direct read rather than a search: everything before the `#`
459
+ * separator is the DocID. A dotted-only grammar would have to try
460
+ * progressively longer dotted prefixes against the index, which cannot
461
+ * distinguish section `1` of document `0.0.1` from section `1.3` of
462
+ * document `0.0`, and silently prefers whichever the corpus happens to
463
+ * contain.
464
+ *
465
+ * @param sectionId - The SectionID whose DocID is sought.
466
+ * @param docIdIndex - The global DocID-to-URI index, used to confirm the document exists.
467
+ * @returns The DocID the section belongs to, or `undefined` if it is not in the corpus.
468
+ */
469
+ findParentDocId(sectionId, docIdIndex) {
470
+ const separatorIndex = sectionId.indexOf('#');
471
+ if (separatorIndex < 0) {
472
+ return undefined;
473
+ }
474
+ const docId = sectionId.substring(0, separatorIndex);
475
+ return docIdIndex.has(docId) ? docId : undefined;
476
+ }
477
+ }
478
+ export { CorpusValidator };
479
+ //# sourceMappingURL=corpus-validator.js.map