@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,1467 @@
1
+ # 1 - ECR - Structural Specification
2
+
3
+ **Version:** 2.0.0
4
+
5
+ ## 1#1 - Purpose
6
+
7
+ `@timiagama/ecr` is a structural linter. It is the reference implementation of the formal structural specification for **Explicit Constraint Referencing (ECR)**.
8
+
9
+ ECR exists to expose the otherwise hidden interdependent constraints within a document corpus.  
10
+
11
+ The linter exists to validate ECR-ready documents against ECR’s formal structural specification.
12
+
13
+ This implementation of ECR operates exclusively on Markdown documents.
14
+
15
+ Once validated by the linter, the ECR relationships within a corpus form an
16
+ explicit graph — one that a coding agent traverses directly, by searching the
17
+ raw Markdown with ordinary text search. Nothing sits between the documents and
18
+ the agent reading them: no server, no index, no embeddings.
19
+
20
+ The linter exists to give that traversal its guarantee. In a corpus that passes,
21
+ every identifier is unique and every reference resolves, so a search that finds
22
+ nothing can be trusted to mean that nothing is there.
23
+
24
+ The linter does not build graphs or perform file discovery. Its responsibilities are limited to:
25
+
26
+ - Validating Markdown documents against the ECR specification
27
+
28
+ - Extracting structured artefacts from valid documents
29
+
30
+ In other words, the linter provides the structural guarantees required for other systems to operate deterministically. It operates purely over `(uri, text)` inputs and produces structured diagnostics and extracted artefacts, where `uri` is an opaque, host-provided identifier for a document instance e.g. `file:///.../docs/3.1-scenario-authoring.md`.
31
+
32
+ The linter is embedded by a **host**: any program that discovers documents,
33
+ feeds them in, and decides what to do with the results. A host is expected to be
34
+ a modest thing — a command-line tool, a CI job, an editor integration.
35
+
36
+ The package also ships a command-line tool, `ecr`. The CLI is a host in the sense above: it performs the file discovery the linter does not, feeds each document to the linter, and reports the result with an exit code suitable for CI. The constraints this specification places on the linter — no IO, no file discovery, no CI concerns — apply to the library API, not to the CLI built on it.
37
+
38
+
39
+ ### 1#1.1 - Formal Specification Clarification
40
+
41
+ - ECR is not an independent markup language. It is a structural annotation layer applied to Markdown documents in accordance with ECR rules.
42
+
43
+ - The ECR grammar is therefore meaningful only in the context of an abstract syntax tree (AST) representing the underlying Markdown document.
44
+
45
+
46
+ ---
47
+
48
+ ## 1#2 - Why Explicit Constraint Referencing Exists
49
+
50
+ Coding agents and reasoning systems operate over visible text.
51
+
52
+ Architectural and institutional systems, however, are governed by constraints that are interdependent. Humans implicitly understand that:
53
+
54
+ - constraints reference other constraints
55
+
56
+ - some documents govern others
57
+
58
+ - contracts define obligations
59
+
60
+ - local sections are governed by external rules
61
+
62
+
63
+ Markdown does not encode this constraint topology explicitly. It flattens structure into prose, numbering conventions, and informal cross-references.
64
+
65
+ As documentation grows, constraint topology becomes illegible to agents. The result is not hallucination, but structural blindness:
66
+
67
+ - locally correct changes that violate global constraints
68
+
69
+ - failure to traverse governing authorities
70
+
71
+ - misapplication of precedence
72
+
73
+ - omission of cross-document obligations
74
+
75
+
76
+ ECR exists to make the implicit constraint graph explicit by requiring:
77
+
78
+ - stable document and section identifiers
79
+
80
+ - mandatory declaration of document-level references
81
+
82
+ - typed relationships between documents
83
+
84
+ - explicit inline references to governing sections
85
+
86
+
87
+ ECR does not add new information. It formalizes existing constraint topology so that agentic systems can traverse it deterministically.
88
+
89
+ ---
90
+
91
+ ## 1#3 - Architectural Scope
92
+
93
+ ### 1#3.1 - Responsibilities
94
+
95
+ The linter is responsible for:
96
+
97
+ - parsing Markdown into an **Abstract Syntax Tree (AST)**
98
+
99
+ - validating ECR structural invariants per document
100
+
101
+ - extracting:
102
+
103
+ - document identity
104
+
105
+ - section hierarchy
106
+
107
+ - typed document-level references
108
+
109
+ - inline section-level references
110
+
111
+ - validating corpus-wide referential integrity **when provided the corpus inputs**:
112
+
113
+ - duplicate DocIDs
114
+
115
+ - duplicate SectionIDs
116
+
117
+ - unresolved references
118
+
119
+
120
+ ### 1#3.2 - Non-Goals
121
+
122
+ The linter is **not**:
123
+
124
+ - a Markdown style linter
125
+
126
+ - a formatting tool
127
+
128
+ - a documentation generator
129
+
130
+ - a graph builder
131
+
132
+ - a filesystem crawler
133
+
134
+ - a CI enforcement engine
135
+
136
+ - a governance or policy framework
137
+
138
+
139
+ File discovery belongs to the host environment.
140
+ Traversing the graph belongs to whoever reads the corpus — an agent with a text
141
+ search needs nothing built first.
142
+ CI integration belongs to downstream tooling.
143
+
144
+ The bundled `ecr` CLI is one such host and one such piece of downstream tooling (see 1#1): it discovers files and returns CI exit codes so that the linter itself does not have to.
145
+
146
+ ---
147
+
148
+ ## 1#4 - Design Rationale
149
+
150
+ ### 1#4.1 - Why Markdown
151
+
152
+ ECR is embedded in Markdown because:
153
+
154
+ - Markdown is the de facto medium for architectural and operational documentation.
155
+
156
+ - It is human-native, diffable, and widely supported across repos and tooling.
157
+
158
+ - ECR constrains structure without introducing a new authoring surface.
159
+
160
+
161
+ ECR does not redefine Markdown. It defines structural invariants over Markdown ASTs.
162
+
163
+ ### 1#4.2 - Why a Formal Structural Specification
164
+
165
+ ECR is formally specified to:
166
+
167
+ - prevent interpretation drift
168
+
169
+ - enable deterministic validation
170
+
171
+ - provide a stable contract between documentation and tooling
172
+
173
+ - support multiple host integrations
174
+
175
+
176
+ The grammar defines invariants over AST(M). The single exception is the
177
+ navigation guarantee of 1#9.11, which constrains the source form of the lines
178
+ carrying identifiers, direction labels and the `References` heading, so that a
179
+ text search finds what the parser found.
180
+
181
+ ### 1#4.3 - Why AST Traversal Requires a Visitor Pattern
182
+
183
+ The linter validates documents by traversing the Markdown AST because ECR rules apply to specific node types and structural contexts.
184
+
185
+ Examples of rules that require structural traversal:
186
+
187
+ - inline references must be detected only in prose nodes and ignored inside code, inline code, HTML blocks, and link URLs
188
+
189
+ - section identity and hierarchy must be derived from heading nodes and maintained as heading depth changes
190
+
191
+ - the References section must be detected as a specific heading followed by a list of entries
192
+
193
+
194
+ The Visitor pattern provides a clean separation between traversal mechanics and rule logic while maintaining deterministic evaluation order.
195
+
196
+ ---
197
+
198
+ ## 1#5 - Tech Stack
199
+
200
+ The linter leverages mature components from the unified Markdown ecosystem:
201
+
202
+ - `unified` - the processing pipeline that hosts the parser
203
+
204
+ - `remark-parse` - Markdown → AST(M)
205
+
206
+ - `mdast-util-to-string` - extract plain text from AST nodes (e.g., headings)
207
+
208
+
209
+ No custom Markdown parser is implemented.
210
+
211
+ ---
212
+
213
+ ## 1#6 - Document Conventions (Non-Formal)
214
+
215
+ This section provides a human-readable description of ECR authoring conventions before the formal specification.
216
+
217
+ ### 1#6.1 - Document Identity
218
+
219
+ Every document begins with a numbered H1 heading:
220
+
221
+ ```
222
+ # 3.1 - Ingestion - Validation Rules
223
+ ```
224
+
225
+ The number (`3.1`) is the document’s stable identity (DocID).
226
+
227
+ **File names do not carry ECR identity.** Identity is the DocID in the H1.
228
+ Renaming or moving a document changes nothing about the graph, and two corpora
229
+ may name the same DocID differently without either being wrong. This repository
230
+ names its documents `<DocID> - <Title>.md`, which helps a human find a file
231
+ without opening it, but that is a local convention and a corpus that ignores it
232
+ is no less compliant.
233
+
234
+ Paths do, however, matter to **discovery**. A host decides which files to read
235
+ and which to skip, and it may use names to do so: the bundled CLI excludes
236
+ meta-documents such as `README.md`, `AGENTS.md` and `CHANGELOG.md` by name, and
237
+ accepts ignore patterns over paths. One navigation recipe takes a file path
238
+ directly, to read a known document's References section. Naming is therefore
239
+ non-normative for identity and consequential for discovery, and the two should
240
+ not be confused.
241
+
242
+ **A corpus is whatever the linter is pointed at.** DocIDs must be unique within
243
+ a corpus, not within a filesystem. This specification lives in `spec/v2/`
244
+ precisely so that a future `spec/v3/` can reuse DocIDs `1`, `2` and `3` without
245
+ collision: each version folder is validated as its own corpus, and linting a
246
+ parent that contains both would correctly report duplicate identifiers.
247
+
248
+ ---
249
+
250
+ ### 1#6.2 - Hierarchical Sub-Headings
251
+
252
+ All sub-headings are numbered relative to the H1 DocID:
253
+
254
+ ```
255
+ # 3.1 - Ingestion - Validation Rules
256
+
257
+ ## 3.1#1 - Mode-Aware Prompt Strategy
258
+
259
+ ### 3.1#1.1 - Expert Mode Prompts
260
+
261
+ ## 3.1#2 - Guardrail Stack
262
+
263
+ ```
264
+
265
+ Each numbered heading defines a stable SectionID.
266
+
267
+ ---
268
+
269
+ ### 1#6.3 - Inline References
270
+
271
+ Inline references create section-level edges.
272
+
273
+ Valid forms:
274
+
275
+ ```
276
+ The validation logic is defined per 3.1#2.
277
+ The retry semantics are enforced per 8.1#3.
278
+ For context assembly see 3.7#2.
279
+
280
+ ```
281
+
282
+ Rules:
283
+
284
+ - Only `see X` and `per X` forms are valid.
285
+
286
+ - `X` must be numeric (DocID or SectionID).
287
+
288
+ - If referencing a section `X#Y`, its DocID `X` must be declared in the References section (unless `X` is the current document).
289
+
290
+ - Inline references are ignored inside code blocks and inline code.
291
+
292
+
293
+ ---
294
+
295
+ ### 1#6.4 - References Section
296
+
297
+ Every document MUST include the heading:
298
+
299
+ ```
300
+ ## References
301
+
302
+ ```
303
+
304
+ A document that references others follows it immediately with a list of them.
305
+
306
+ A document that references nothing leaves the section empty. The heading is
307
+ still required, because an empty References section is a statement — *this
308
+ document depends on nothing* — and silence is not. A reader who finds no
309
+ References section cannot tell a document with no dependencies from one whose
310
+ author never recorded them; that distinction is the whole of *Explicit*
311
+ Constraint Referencing. The linter reports an empty section as information, not
312
+ as an error (1#9.6).
313
+
314
+ Example:
315
+
316
+ ```
317
+ ## References
318
+
319
+ - 3.1 - Ingestion - Validation Rules (authority - defines validation logic that evaluation must enforce)
320
+ - 8.1 - Workflow Orchestration Contract (constraint - retry semantics applied to orchestration)
321
+
322
+
323
+ ```
324
+
325
+ Format:
326
+
327
+ ```
328
+ - {DocID} - {title} ({direction} - {explanation})
329
+ ```
330
+
331
+ Allowed direction values:
332
+
333
+ - authority
334
+
335
+ - dependency
336
+
337
+ - constraint
338
+
339
+ - contract
340
+
341
+
342
+ References define document-level edges and legitimize inline references.
343
+
344
+ ---
345
+
346
+ ### 1#6.5 - Minimal Compliant Document
347
+
348
+ ```
349
+ # 5.1 - Reporting - Evaluation Strategy
350
+
351
+ ## 5.1#1 - Acceptance Criteria
352
+
353
+ Validation requirements are enforced per 3.1#2.
354
+
355
+ ## 5.1#2 - Evaluation Pipeline
356
+
357
+ Retry semantics are applied per 8.1#3.
358
+
359
+ ## References
360
+
361
+ - 3.1 - Ingestion - Validation Rules (authority - defines validation criteria enforced by evaluation)
362
+ - 8.1 - Workflow Orchestration Contract (constraint - retry semantics applied to evaluation runs)
363
+
364
+ ```
365
+
366
+ ---
367
+
368
+ ## 1#7 - Direction Semantics
369
+
370
+ Direction labels appear only within References entries.
371
+
372
+ Edges are directed:
373
+
374
+ ```
375
+ Current DocID → Target DocID
376
+
377
+ ```
378
+
379
+ Direction labels describe semantic meaning:
380
+
381
+ - **authority** - the referenced document governs the current document
382
+
383
+ - **dependency** - the current document consumes the referenced document
384
+
385
+ - **constraint** - the referenced document imposes restrictions on the current document
386
+
387
+ - **contract** - the referenced document defines structural obligations
388
+
389
+
390
+ Direction labels do not alter graph direction.
391
+
392
+ ---
393
+
394
+ ## 1#8 - Visitor and State Model
395
+
396
+ ### 1#8.1 - Visitor Pattern
397
+
398
+ The linter validates documents by traversing the Markdown Abstract Syntax Tree (AST) using a Visitor pattern.
399
+
400
+ Traversal is required because ECR rules apply to specific node types and structural contexts.
401
+
402
+ Examples:
403
+
404
+ - Heading nodes define SectionIDs and hierarchy.
405
+
406
+ - Text nodes may contain inline references.
407
+
408
+ - Code nodes must suppress inline reference detection.
409
+
410
+ - The References section must be detected structurally.
411
+
412
+
413
+ ---
414
+
415
+ ### 1#8.2 - Validation State
416
+
417
+ The linter maintains two internal state models during traversal.
418
+
419
+ #### 1#8.2.1 - ValidationState
420
+
421
+ Accumulates extracted artefacts:
422
+
423
+ - docId
424
+
425
+ - sections
426
+
427
+ - references
428
+
429
+ - inlineReferences
430
+
431
+ - diagnostics
432
+
433
+
434
+ #### 1#8.2.2 - CursorState
435
+
436
+ Tracks structural position:
437
+
438
+ - currentSectionId
439
+
440
+ - headingStack (depth-aware)
441
+
442
+ - inReferencesSection
443
+
444
+
445
+ The heading stack enables correct parent-child derivation across heading depth transitions.
446
+
447
+ State management is necessary to ensure deterministic evaluation and accurate edge attribution.
448
+
449
+ ---
450
+
451
+ ## 1#9 - ECR Formal Structural Specification
452
+
453
+ ### 1#9.1 - AST Model
454
+
455
+ Let `M` be a valid Markdown document.
456
+ Let `AST(M)` be its parsed Markdown **Abstract Syntax Tree (AST)**.
457
+
458
+ ECR compliance is defined as a predicate over `AST(M)`, together with the
459
+ source-form constraints of 1#9.11.
460
+
461
+ Structural validation operates on AST nodes. The exception is the navigation
462
+ guarantee: because ECR documents are navigated by searching raw file text, the
463
+ rules in 1#9.11 constrain the source form of the lines that carry identifiers.
464
+ Those rules are stated over the raw text of a node's source range, and are the
465
+ only rules that are.
466
+
467
+ ---
468
+
469
+ ### 1#9.2 - Identity Definitions
470
+
471
+ #### 1#9.2.1 - DocID
472
+
473
+ ```
474
+ DocID ::= Digit+ ("." Digit+)*
475
+ ```
476
+
477
+ Examples:
478
+
479
+ ```
480
+ 1
481
+ 3.1
482
+ 7.12
483
+ 2.4.3
484
+ ```
485
+
486
+ #### 1#9.2.2 - SectionID
487
+
488
+ ```
489
+ SectionPath ::= Digit+ ("." Digit+)*
490
+ SectionID ::= DocID "#" SectionPath
491
+ ```
492
+
493
+ A SectionID names a section of exactly one document: the DocID before the `#`
494
+ separator. The separator is mandatory.
495
+
496
+ Without it a dotted identifier has more than one legal reading. In a corpus
497
+ containing both a document `0.0` and a document `0.0.2`, the identifier
498
+ `0.0.2.1` would denote either section 1 of document `0.0.2` or section 2.1 of
499
+ document `0.0`, and both readings would occur. The separator removes the
500
+ ambiguity by construction, which is what makes SectionIDs globally unique and
501
+ reference resolution possible without a corpus index (see 3).
502
+
503
+ ---
504
+
505
+ ### 1#9.3 - Document Identity Rule
506
+
507
+ There MUST exist exactly one heading node in `AST(M)` such that: **[ECR101]**
508
+
509
+ - `depth = 1`
510
+
511
+ - `toString(heading)` matches:
512
+
513
+
514
+ ```
515
+ DocID " - " Title
516
+ ```
517
+
518
+ Where:
519
+
520
+ - `DocID` conforms to the grammar above
521
+
522
+ - `Title` is any non-empty string
523
+
524
+ - `" - "` stands for any dash separator permitted by 1#9.4
525
+
526
+
527
+ The DocID extracted from the H1 heading defines the document identity.
528
+
529
+ ---
530
+
531
+ ### 1#9.4 - Section Structure Rules
532
+
533
+ For every heading node with `depth ≥ 2`:
534
+
535
+ 1. The textual prefix MUST match a valid `SectionID`.
536
+ 2. The `SectionID`’s DocID MUST equal the document’s `DocID`.
537
+ 3. Let `d = heading depth`. The number of segments in the `SectionID`’s section
538
+ path MUST equal:
539
+
540
+ ```
541
+ d - 1
542
+ ```
543
+
544
+ Where:
545
+
546
+ ```
547
+ segments(X) ::= number of numeric components separated by "."
548
+ ```
549
+
550
+ The document’s own DocID depth does not enter the calculation: an H2 carries a
551
+ one-segment section path whether the document is `1` or `2.4.3.9`.
552
+
553
+ Example:
554
+
555
+ ```
556
+ DocID = 3.1
557
+
558
+ # 3.1 - Title → depth = 1 → no section path
559
+ ## 3.1#1 - Sec → depth = 2 → section path "1" → 1 segment
560
+ ### 3.1#1.2 - Sub → depth = 3 → section path "1.2" → 2 segments
561
+ ```
562
+
563
+ A heading that violates the depth-to-section-path rule is invalid.
564
+
565
+ 4. For a heading of depth `d ≥ 3`, the section path without its last segment MUST equal the section path of its parent — the nearest preceding heading of depth `d - 1`. `3.1#2.1` sits under `3.1#2`, never under `3.1#1`. This is what makes a SectionID self-locating: its number alone says where in the document it lives.
566
+
567
+ 5. The heading's source form MUST satisfy rule 1 of 1#9.11, so that the section
568
+ is discoverable by the published recipe. The title is unconstrained; only the
569
+ identifier's own characters are.
570
+
571
+ A heading separates its identifier from its title with a dash:
572
+
573
+ ```
574
+ DocID <dash> Title
575
+ SectionID <dash> Title
576
+ ```
577
+
578
+ The dash MAY be a hyphen-minus (U+002D), an en dash (U+2013) or an em dash
579
+ (U+2014), with any surrounding whitespace. The variant carries no structural
580
+ meaning: identity is carried entirely by the identifier, so rejecting a variant
581
+ would fail documents that are perfectly navigable. Corpora are free to require
582
+ one variant for consistency, but that is a style concern, not a structural one.
583
+
584
+ ---
585
+
586
+ ### 1#9.5 - Inline Reference Rules
587
+
588
+ Inline references are detected only within `text` nodes whose ancestor chain does NOT include:
589
+
590
+ - code
591
+
592
+ - inlineCode
593
+
594
+ - html
595
+
596
+ - link (URL portion)
597
+
598
+
599
+ Valid inline reference forms are:
600
+
601
+ ```
602
+ see TargetID
603
+ per TargetID
604
+ ```
605
+
606
+ Where:
607
+
608
+ ```
609
+ TargetID ::= DocID | SectionID
610
+ ```
611
+
612
+ Rules:
613
+
614
+ 1. `TargetID` MUST be numeric and conform to grammar, and MUST be recognised as
615
+ a **complete token** rather than as a prefix of the text that follows.
616
+
617
+ Recognition proceeds in three steps:
618
+
619
+ - **Candidate.** Starting at the first digit after the keyword, take the
620
+ maximal run of characters drawn from `0-9`, `.` and `#`. The candidate is
621
+ that entire run — never a shorter prefix of it.
622
+
623
+ - **Conformance.** The candidate MUST conform to the `DocID` or `SectionID`
624
+ grammar, and MUST be followed by whitespace, the end of the line, or one of
625
+ `, ; : ) ] } " ' ! ?`. A candidate that fails the grammar only because of a
626
+ single trailing `.` is accepted with that `.` removed, which is what makes
627
+ a citation at the end of a sentence valid.
628
+
629
+ - **Disposition.** A candidate that satisfies the previous step is a
630
+ reference. One that does not is judged by whether it contains a `#`:
631
+
632
+ - **With a `#`**, it is a *malformed reference*. No edge is extracted and
633
+ an ERROR is reported.
634
+
635
+ - **Without a `#`**, it is *ordinary prose*. No edge is extracted and no
636
+ diagnostic is reported.
637
+
638
+ Taking the whole run before testing it is what stops a malformed candidate
639
+ from decaying into a shorter valid one. `see 1#1oops` yields the candidate
640
+ `1#1`, which conforms but runs into a letter; `see 1#1#9` and `see 1#` yield
641
+ `1#1#9` and `1#`, which do not conform at all. All three contain a `#`, so
642
+ all three are errors, and none is quietly accepted as `1#1` or `1`.
643
+
644
+ The `#` decides the disposition because it is the one character of an ECR
645
+ identifier that ordinary writing never produces. Prose is full of numbers
646
+ that follow `see` and `per`: `per 60s`, `per 10ms`, `see 1..2` — a number
647
+ with a unit, or a range, from an author who intended no citation at all.
648
+ Reporting those would make the linter unusable on the technical prose ECR
649
+ exists to annotate, and rule 3 already reasons this way when it lets a bare
650
+ DocID warn rather than fail.
651
+
652
+ Prose stays prose even when the corpus contains a document whose DocID
653
+ matches its leading digits: `per 60s` is not a reference to document `60`,
654
+ and no edge is extracted for it.
655
+
656
+ Recognition of the keyword depends only on a word boundary before it, never
657
+ on the specific character that precedes it: `"see 3.1#2"` inside quotation
658
+ marks is a reference.
659
+
660
+ Recognition reads the text a reader sees on the line, not the text of one
661
+ parsed node. A Markdown text node ends wherever formatting begins, which is
662
+ not where a word or an identifier ends. So the whole citation, and the
663
+ characters either side of it, are read across the edges of formatting
664
+ spans: `per 60**s**` reads as `per 60s` and is prose, and `**x**see 8.1`
665
+ reads as `xsee 8.1` and has no keyword. A citation recognised across such
666
+ an edge — `see **8.1#3**`, `**see** 8.1#3`, `see <span>8.1#3</span>` — is
667
+ still a citation, and fails rule 4. Within that text:
668
+
669
+ - Emphasis, strong emphasis, strikethrough and link text contribute their
670
+ contents.
671
+
672
+ - Inline code contributes its contents. It may continue a candidate
673
+ (``per 60`s` `` is prose), but a keyword or candidate is never *recognised*
674
+ inside it, per the exclusions above. Code is text a reader sees, but not
675
+ text an author cites through.
676
+
677
+ - A hard line break, an image and a `br` tag end the text as whitespace
678
+ does. The tag may be in any case, and may carry attributes and a closing
679
+ `/`: `<br>`, `<BR/>` and `<br class="x">` are all breaks.
680
+
681
+ - Any other inline HTML — an HTML comment, or a tag such as `<span>` —
682
+ contributes nothing. It separates nothing a reader sees, so
683
+ `60<span>s</span>` and `60<!-- note -->s` both read as `60s`.
684
+
685
+ 2. The DocID of a `TargetID` is the `TargetID` itself if it is a DocID, or the text before the `#` if it is a SectionID (`X#Y`).
686
+
687
+ - That DocID MUST appear in the References section, unless it is the current document’s own DocID.
688
+
689
+ 3. Inline references MUST NOT target an undeclared document. No edge is extracted for an undeclared target. How the violation is reported depends on the form of the target, and on what the linter can know:
690
+
691
+ - A SectionID target (`see 3.1#2`) is an ERROR. The `#` form never occurs in ordinary prose, so the target is certainly a reference and its declaration is missing.
692
+
693
+ - A DocID target (`see 3.1`, `per 60`) is a WARNING within a single document. The words `see` and `per` followed by a number also occur in ordinary prose (`100 requests per 60 seconds`), and one document alone cannot tell such prose from a reference whose declaration is missing.
694
+
695
+ - Across a corpus, an undeclared DocID target that is a document in the corpus is an ERROR: it is a real reference whose declaration, and with it the direction and explanation of the edge, is missing. One that names no document remains a WARNING.
696
+
697
+ 4. The keyword and the `TargetID` MUST be adjacent literal text on one source line, per rule 2 of 1#9.11. A citation fails this rule when the `TargetID` begins a link, bold, italic or strikethrough span (`see [8.1#3](…)`), when any formatting, inline HTML or inline code falls between the keyword's first character and the identifier's last (`**see** 8.1#3`, `see <!-- x -->8.1#3`), when a line break separates it from the keyword, when it contains a backslash escape or character reference (`see 5\.1#1`), or when more than one space separates the two. In every such case the citation is invisible to a text search, so no edge is extracted.
698
+
699
+ The violation is classified by the same test as rule 3: whether the target is identifiable as a reference, or is indistinguishable from numeric prose.
700
+
701
+ It is an ERROR when the target is a SectionID (the `#` form never occurs in ordinary prose), when the target DocID is declared in the document's References section, when it is the document's own DocID, or when the corpus confirms that a document with that DocID exists. In each of those cases a citation was certainly intended, and it is certainly unfindable.
702
+
703
+ It is a WARNING only when the target is a bare DocID that names nothing known — undeclared, not the document's own, and absent from the corpus, or with no corpus available. There, emphasised prose (`See *8.1* for details`) cannot be distinguished from a citation whose author reached for italics.
704
+
705
+
706
+ ---
707
+
708
+ ### 1#9.6 - References Section Rules (Mandatory)
709
+
710
+ There MUST exist exactly one depth-2 heading node (`##`) whose text is:
711
+
712
+ ```
713
+ References
714
+ ```
715
+
716
+ The entries are the items of the list node that immediately follows this heading.
717
+
718
+ The heading and that list MUST both be children of the document root, per rule 3
719
+ of 1#9.11. A References section nested inside a blockquote, a list item, or any
720
+ other container is rejected: its entries would otherwise be silently discarded,
721
+ and the declared relationships lost. Requiring a predictable top-level position
722
+ is what makes the References section findable by a single anchored search.
723
+
724
+ A References section with no list declares that the document has no external references. This is valid: the linter reports it as `info`, not as an error.
725
+
726
+ Each list item MUST match exactly:
727
+
728
+ ```
729
+ TargetDocID " - " Title " (" Direction " - " Explanation ")"
730
+ ```
731
+
732
+ Where:
733
+
734
+ `Title` is any non-empty string, each `" - "` stands for a separator as defined in rule 5, and
735
+
736
+ ```
737
+ TargetDocID ::= DocID
738
+ Direction ::= authority | dependency | constraint | contract
739
+ ```
740
+
741
+ The **relationship parenthetical** is located by matching, not by searching for
742
+ the first or last `" ("`. The entry MUST end with `)`, and that `)` is matched to
743
+ its opening `(` by scanning right to left, counting nesting depth. What precedes
744
+ that `(` is `TargetDocID " - " Title`; what it encloses is
745
+ `Direction " - " Explanation`.
746
+
747
+ Parentheses MUST be balanced **within the relationship parenthetical**. In the
748
+ `Title` they are unconstrained: a title may carry an unmatched `(` or `)`
749
+ without consequence, because the parenthetical has already been delimited by the
750
+ match above.
751
+
752
+ Matching from the end is what lets an explanation hold parentheses of its own.
753
+ In
754
+
755
+ ```
756
+ - 8.1 - Target (dependency - defines retries (contract - policy))
757
+ ```
758
+
759
+ the final `)` matches the `(` before `dependency`, so the direction is
760
+ `dependency` and the explanation is `defines retries (contract - policy)` — not
761
+ the `contract` edge that taking the last `" ("` would produce.
762
+
763
+ An entry that does not end with `)`, or whose relationship parenthetical is
764
+ unbalanced, is malformed.
765
+
766
+ Rules:
767
+
768
+ 1. Each `TargetDocID` MUST be unique within the References section.
769
+ 2. Inline references to `X` or `X#Y` require that `X` appears as a `TargetDocID`, unless `X` is the current document's own DocID.
770
+ 3. `Direction` MUST be one of the allowed enumeration values.
771
+ 4. `Explanation` MUST be non-empty text.
772
+ 5. Each separator is a single space, a dash, and a single space. The dash MAY be a hyphen-minus (U+002D), an en dash (U+2013) or an em dash (U+2014), as in headings (1#9.4); the variant carries no structural meaning.
773
+
774
+ Example:
775
+
776
+ ```
777
+ - 3.1 - Ingestion - Validation Rules (authority - defines validation logic enforced by this document)
778
+ - 8.1 - Workflow Orchestration Contract (constraint - defines retry semantics applied to this workflow)
779
+ ```
780
+
781
+
782
+ ---
783
+
784
+ ### 1#9.7 - Direction Label Semantics
785
+
786
+ Edges are always directed:
787
+
788
+ ```
789
+ SourceDocID → TargetDocID
790
+ ```
791
+
792
+ Direction labels define semantic meaning only:
793
+
794
+ - authority - TargetDocID governs SourceDocID
795
+
796
+ - dependency - SourceDocID consumes TargetDocID
797
+
798
+ - constraint - TargetDocID imposes restrictions on SourceDocID
799
+
800
+ - contract - TargetDocID defines structural obligations for SourceDocID
801
+
802
+
803
+ Direction does not reverse edge orientation.
804
+
805
+ ---
806
+
807
+ ### 1#9.8 - Per-Document Structural Invariants
808
+
809
+ Within a single document:
810
+
811
+ - Exactly one valid H1 DocID
812
+
813
+ - SectionIDs unique within document
814
+
815
+ - References section present exactly once
816
+
817
+ - All inline references syntactically valid
818
+
819
+ - No duplicate TargetDocID entries in References
820
+
821
+ Violation of any rule is an ERROR.
822
+
823
+ ---
824
+
825
+ ### 1#9.9 - Corpus-Wide Invariants
826
+
827
+ When validating a corpus of documents:
828
+
829
+ 1. DocID MUST be globally unique.
830
+
831
+ 2. SectionID MUST be globally unique across corpus.
832
+
833
+ 3. Every TargetDocID in References MUST resolve to an existing DocID.
834
+
835
+ 4. Every inline TargetID MUST resolve to an existing DocID or SectionID.
836
+
837
+ 5. A References entry's `title` SHOULD match the title in the target document's H1. Where it does not, emit a WARNING whose diagnostic payload carries the canonical title.
838
+
839
+
840
+ Violation of rules 1–4 is an ERROR. Rule 5 is a WARNING only: titles are not part of graph identity (1#9.10), so a stale title never fails validation. It is a corpus-wide rule because the target's title is only known once the corpus is indexed.
841
+
842
+ ---
843
+
844
+ ### 1#9.10 - Graph Identity Model
845
+
846
+ The ECR graph consists solely of:
847
+
848
+ - Node identities:
849
+
850
+ - DocID
851
+
852
+ - SectionID
853
+
854
+ - Directed edges:
855
+
856
+ - (SourceDocID → TargetDocID)
857
+
858
+ - Typed by Direction
859
+
860
+ - Section-level inline references
861
+
862
+
863
+ The graph excludes:
864
+
865
+ - Document titles (H1 text)
866
+
867
+ - Section heading text (heading titles)
868
+
869
+ - Section content
870
+
871
+ - Line numbers
872
+
873
+ - File paths
874
+
875
+ - Formatting metadata
876
+
877
+
878
+ Although the linter extracts `title` fields for documents and sections, these are **metadata for display and diagnostics only**. They MUST NOT participate in node identity, edge identity, canonical ordering, or any equality comparison of graph structure.
879
+
880
+ Graph identity MUST remain stable under edits to document titles, section heading text, and section content, provided DocIDs, SectionIDs, and references remain unchanged.
881
+
882
+ ---
883
+
884
+ ### 1#9.11 - Navigation Guarantee
885
+
886
+ ECR documents are navigated by searching raw file text, using the recipes
887
+ published in the navigation protocol that ships with this specification.
888
+
889
+ **The guarantee.** In a corpus that passes validation, every document, every
890
+ section, and every reference the linter recognises — whether declared in a
891
+ References section or written inline — MUST be discoverable by the
892
+ corresponding recipe, applied to the raw bytes of the source files.
893
+
894
+ The guarantee runs one way. Every recognised reference is found, but not every
895
+ search hit is a reference. The recipes read raw source, where markup can sit
896
+ between a number and the unit that makes it prose: `per 60**s**` is prose under
897
+ 1#9.5, yet the recipe for document `60` matches it. Such hits are permitted. A
898
+ recipe may be broader than the reference grammar; it may never be narrower.
899
+
900
+ For the same reason, the recipes bound a citation explicitly rather than with a
901
+ regular-expression word boundary alone. `_` is a word character to both
902
+ published engines, so `\b` finds nothing in `_see 8.1_`, which is ordinary
903
+ Markdown emphasis. Before the keyword the recipes accept a word boundary or an
904
+ underscore. After the identifier they accept any character other than an ASCII
905
+ letter or digit, and additionally refuse, after a bare `DocID`, a `#` and a `.`
906
+ that does not end a sentence, so that `8.1` is not found inside `8.1#3` or
907
+ `8.1.2`.
908
+
909
+ Parsing and searching disagree in two ways. A Markdown parser discards syntax
910
+ that a search still sees: emphasis markers are removed and backslash escapes
911
+ resolved, so `**8.1**` and `8\.1` both parse to `8.1` while the source still
912
+ holds the asterisks and the backslash. The escaped form does not contain the
913
+ characters `8.1` at all; the emphasised form does, but neither satisfies a
914
+ recipe, which anchors an identifier to a heading's `#` characters or to the
915
+ keyword before it. And a parser joins what a search keeps apart: a soft
916
+ line break is preserved in the text it produces, and the reference matcher
917
+ treats that newline as ordinary whitespace, whereas a line-anchored search
918
+ cannot match across two lines at all.
919
+
920
+ Validation defined solely over the parsed tree would therefore accept documents
921
+ that no recipe can find, and the guarantee above would be false. The rules below
922
+ constrain the source form of the text that carries identifiers, direction
923
+ labels, and the `References` heading itself, so that the parsed meaning and the
924
+ searchable text agree.
925
+
926
+ Formatting is constrained only where the recipes look. Prose is unaffected, and
927
+ so is every heading's title: `## 8.1#3 - **Retry** policy` is valid, because no
928
+ recipe reads the title.
929
+
930
+ 1. **Heading source form.** A heading that carries a `DocID` or `SectionID`
931
+ MUST be an ATX heading whose source line begins at column 1 with its opening
932
+ `#` characters, followed by exactly one space (U+0020), followed by the
933
+ identifier in literal characters.
934
+
935
+ Within the identifier, inline formatting, backslash escapes and character
936
+ references MUST NOT appear. A setext heading MUST NOT carry an identifier.
937
+
938
+ The line MUST also be a line to a search engine, which begins a line only
939
+ at the start of the file or after a line feed. A heading that follows a
940
+ lone carriage return (CR) is, to a search, the middle of the line before
941
+ it, even though Markdown starts a new line there; use LF or CRLF line
942
+ endings. And a heading on the first line MUST NOT be preceded by a UTF-8
943
+ byte-order mark, which GNU grep reads as text before the `#`. A mark on a
944
+ line of its own obstructs nothing, since the heading then begins a later
945
+ line.
946
+
947
+ 2. **Citation source form.** In an inline reference, the keyword and the
948
+ `TargetID` MUST appear on the same source line, separated by exactly one
949
+ space (U+0020), with the `TargetID` in literal characters.
950
+
951
+ 3. **References placement.** The `## References` heading and the list that
952
+ follows it MUST both be children of the document root. Neither may be nested
953
+ inside a blockquote, a list item, or any other container node.
954
+
955
+ 4. **References source form.** The recipes read the References section itself,
956
+ not only the identifiers inside it, so its source form is constrained in
957
+ four places:
958
+
959
+ - The heading's source line MUST be `## References` exactly: two `#`
960
+ characters at column 1, one space (U+0020), and the word `References` in
961
+ literal characters. `## **References**` is rejected.
962
+
963
+ - The entry's target `DocID` MUST appear in literal characters, at the start
964
+ of the entry text, optionally preceded by a single `[`.
965
+
966
+ - The list marker, the target `DocID` and the direction label MUST all appear
967
+ on **one source line**, in that order, with the direction label in literal
968
+ characters immediately after the opening parenthesis of the parenthetical.
969
+ The explanation that follows the label may wrap over as many lines as it
970
+ needs. The recipe answering "which documents does `8.1` govern?" matches
971
+ the marker, the identifier and the label in a single line-anchored pattern,
972
+ and finds nothing when any of the three is on a line of its own.
973
+
974
+ Together with rule 2, which keeps a citation's keyword and identifier on
975
+ one line, these are the only rules in this specification that constrain
976
+ where a source line may break. Markdown renders a single newline as the
977
+ same line, so a rule of this kind must earn its place: each is confined to
978
+ adjacent fields that an author would have to work to separate, and the
979
+ alternative — multi-line search in every query — would make the recipes
980
+ markedly harder for the agents that run them.
981
+
982
+ The list marker itself is not constrained beyond sharing that line. A `-`, `*`
983
+ or `+` bullet and an ordered `1.` or `1)` marker are all equally navigable, and
984
+ the recipes match all five; rejecting a variant that is perfectly findable would
985
+ repeat a restriction this specification already declines to make for the dash
986
+ separator in 1#9.4.
987
+
988
+ The single space required by rules 1, 2 and 4 is not arbitrary strictness: the
989
+ published recipes match exactly one space, so two would not be found.
990
+
991
+ Violations are classified per 1#12.
992
+
993
+ ---
994
+
995
+ ## 1#10 - Output Schema (Normative)
996
+
997
+ This section defines the normative output schema produced by the linter. The schema mirrors the ECR formal structural specification and is host-agnostic: it reports what the linter found and assumes nothing about what the host does next.
998
+
999
+ ### 1#10.0 - URI Semantics (Normative)
1000
+
1001
+ All linter APIs accept and return a `uri` string.
1002
+
1003
+ `uri` is an opaque, host-provided identifier for a document instance. It MUST be stable within the scope of a validation run and SHOULD be stable across runs when the underlying document is the same.
1004
+
1005
+ Examples include (but are not limited to):
1006
+
1007
+ * `file:///.../docs/3.1-scenario-authoring.md`
1008
+ * `obsidian://vault/Architecture/3.1.md`
1009
+ * an editor-internal URI provided by an LSP client
1010
+
1011
+ The linter treats `uri` as an identifier only:
1012
+
1013
+ * It is used to associate diagnostics with a source document.
1014
+ * It is used by host environments to map extracted identifiers to concrete files.
1015
+
1016
+ `uri` is **not** part of ECR identity. Graph identity is defined solely by `DocID` and `SectionID`.
1017
+
1018
+ ---
1019
+
1020
+ ### 1#10.1 - Core Types
1021
+
1022
+ The linter operates over `(uri, text)` inputs and returns:
1023
+
1024
+ - structured diagnostics
1025
+
1026
+ - extracted structural artefacts
1027
+
1028
+
1029
+ All identifiers are lexical identifiers defined by ECR:
1030
+
1031
+ - `DocID`
1032
+
1033
+ - `SectionID`
1034
+
1035
+
1036
+ No section content or line numbers are part of the extracted graph artefacts.
1037
+
1038
+ ---
1039
+
1040
+ ### 1#10.2 - LintResult
1041
+
1042
+ A lint operation over a single Markdown document returns:
1043
+
1044
+ ```ts
1045
+ type LintResult = {
1046
+ input: {
1047
+ uri: string
1048
+ version?: number
1049
+ }
1050
+
1051
+ ok: boolean
1052
+ diagnostics: Diagnostic[]
1053
+
1054
+ extracted?: ExtractedDocument
1055
+ }
1056
+ ```
1057
+
1058
+ Semantics:
1059
+
1060
+ - `ok = true` iff the document satisfies per-document ECR structural invariants.
1061
+
1062
+ - `extracted` is present only when a valid `DocID` is recovered and structural extraction succeeds sufficiently to produce stable identifiers.
1063
+
1064
+
1065
+ ---
1066
+
1067
+ ### 1#10.3 - Diagnostic
1068
+
1069
+ Diagnostics are host-agnostic and can be mapped into LSP diagnostics or rendered in other UIs.
1070
+
1071
+ ```ts
1072
+ type Diagnostic = {
1073
+ severity: "error" | "warning" | "info"
1074
+ ruleId: string
1075
+ message: string
1076
+
1077
+ uri: string
1078
+
1079
+ range?: {
1080
+ start: { line: number; character: number }
1081
+ end: { line: number; character: number }
1082
+ }
1083
+
1084
+ docId?: string
1085
+ sectionId?: string
1086
+
1087
+ data?: Record<string, unknown>
1088
+ }
1089
+ ```
1090
+
1091
+ Notes:
1092
+
1093
+ - `range` is optional and depends on whether positional metadata is available from the Markdown parser.
1094
+
1095
+ - `docId` and `sectionId` provide structural context when determinable.
1096
+
1097
+
1098
+ ---
1099
+
1100
+ ### 1#10.4 - ExtractedDocument
1101
+
1102
+ `ExtractedDocument` is the canonical structural representation of a single ECR document.
1103
+
1104
+ ```ts
1105
+ type ExtractedDocument = {
1106
+ docId: DocID
1107
+ title: string
1108
+
1109
+ sections: SectionNode[]
1110
+ references: ReferenceEdge[]
1111
+ inlineReferences: InlineReferenceEdge[]
1112
+ }
1113
+ ```
1114
+
1115
+ Constraints:
1116
+
1117
+ - `docId` is extracted from the H1 heading.
1118
+
1119
+ - `sections` includes the H1 as the root section node and all numbered headings thereafter.
1120
+
1121
+ - `references` ReferenceEdge[] // each References list item yields (toDocId, title, direction, explanation) extracted verbatim from the `## References section`
1122
+
1123
+ - `inlineReferences` are extracted only from valid inline forms: `see TargetID` and `per TargetID`.
1124
+
1125
+ `title` is extracted for human/agent display and diagnostics. It is not part of ECR identity. Consumers may persist `title` as metadata and MUST treat graph structure as independent of title text.
1126
+
1127
+
1128
+ ---
1129
+
1130
+ ### 1#10.5 - SectionNode
1131
+
1132
+ ```ts
1133
+ type SectionNode = {
1134
+ id: DocID | SectionID
1135
+ title: string
1136
+
1137
+ headingDepth: number
1138
+
1139
+ parentId?: DocID | SectionID
1140
+ }
1141
+ ```
1142
+
1143
+ Constraints:
1144
+
1145
+ - The H1 node has `id = DocID`, `headingDepth = 1`, and no `parentId`.
1146
+
1147
+ - For headings of depth ≥ 2:
1148
+
1149
+ - `id` MUST be a `SectionID`.
1150
+
1151
+ - `parentId` MUST be the nearest preceding heading whose depth is exactly `headingDepth - 1`.
1152
+
1153
+ - `headingDepth` MUST align with the depth-to-segment rule in the formal specification.
1154
+
1155
+ Notes:
1156
+
1157
+ - `title` is extracted for human/agent display and diagnostics only.
1158
+ - `title` MUST NOT be treated as part of ECR structural identity.
1159
+ - Downstream consumers may persist `title` as metadata, but MUST keep it separately from the structural graph representation.
1160
+
1161
+
1162
+ ---
1163
+
1164
+ ### 1#10.6 - ReferenceEdge
1165
+
1166
+ Each References section list item defines a document-level typed edge:
1167
+
1168
+ ```ts
1169
+ type ReferenceEdge = {
1170
+ fromDocId: DocID
1171
+ toDocId: DocID
1172
+
1173
+ direction: "authority" | "dependency" | "constraint" | "contract"
1174
+ explanation: string
1175
+
1176
+ title: string
1177
+ }
1178
+ ```
1179
+
1180
+ Constraints:
1181
+
1182
+ - `fromDocId` is the current document’s DocID.
1183
+
1184
+ - `toDocId` is the TargetDocID from the References entry.
1185
+
1186
+ - `explanation` MUST be non-empty text.
1187
+
1188
+ - `title` MUST be non-empty text and MUST be extracted from the References entry.
1189
+
1190
+
1191
+ ---
1192
+
1193
+ ### 1#10.7 - InlineReferenceEdge
1194
+
1195
+ Each inline reference defines a section-attributed edge:
1196
+
1197
+ ```ts
1198
+ type InlineReferenceEdge = {
1199
+ fromId: DocID | SectionID
1200
+ toId: DocID | SectionID
1201
+
1202
+ kind: "see" | "per"
1203
+ }
1204
+ ```
1205
+
1206
+ Constraints:
1207
+
1208
+ - `fromId` is the current section context in which the inline reference occurs:
1209
+
1210
+ - the most recent heading identifier in document order at the time the reference is encountered
1211
+
1212
+ - `toId` is the referenced TargetID
1213
+
1214
+ - Inline references MUST NOT target an undeclared document:
1215
+
1216
+ - if `toId` is `X` or `X#Y`, then `X` MUST appear in the References section of the current document, unless `X` is the current document’s own DocID; an undeclared target yields no edge, and is reported as described in 1#9.5 rule 3
1217
+
1218
+ - Every `toId` MUST resolve to an existing identifier in the corpus (DocID or SectionID).
1219
+
1220
+
1221
+ ---
1222
+
1223
+ ### 1#10.8 - CorpusResult
1224
+
1225
+ When validating a corpus, the host supplies a set of documents. The linter returns corpus-wide diagnostics and an index suitable for graph construction.
1226
+
1227
+ ```ts
1228
+ type CorpusResult = {
1229
+ documents: Array<{
1230
+ uri: string
1231
+ result: LintResult
1232
+ }>
1233
+
1234
+ index?: {
1235
+ docIds: Record<DocID, string> // DocID -> uri
1236
+ sectionIds: Record<SectionID, string> // SectionID -> uri
1237
+ }
1238
+
1239
+ diagnostics: Diagnostic[]
1240
+ }
1241
+ ```
1242
+
1243
+ Constraints:
1244
+
1245
+ - Corpus-wide validation enforces:
1246
+
1247
+ - global uniqueness of DocIDs and SectionIDs
1248
+
1249
+ - resolution of References TargetDocIDs to existing DocIDs
1250
+
1251
+ - resolution of inline TargetIDs to existing headings
1252
+
1253
+ - agreement of References titles with their targets' H1 titles (warning only)
1254
+
1255
+
1256
+ All corpus-wide failures are errors, except a References title mismatch, which is a warning (1#9.9 rule 5).
1257
+
1258
+ ---
1259
+
1260
+ ## 1#11 - Validation Algorithm Overview (Deterministic)
1261
+
1262
+ This section describes the deterministic two-pass validation strategy used by the linter.
1263
+
1264
+ ### 1#11.1 - Pass 1: Per-Document Parse, Validate, Extract
1265
+
1266
+ For each input document `(uri, text)`:
1267
+
1268
+ 1. Parse Markdown into `AST(M)`.
1269
+
1270
+ 2. Extract the H1 `DocID` and title.
1271
+
1272
+ 3. Traverse heading nodes to extract:
1273
+
1274
+ - all section identifiers
1275
+
1276
+ - parent-child relationships via heading depth
1277
+
1278
+ 4. Locate the `## References` section and extract all `ReferenceEdge` entries.
1279
+
1280
+ 5. Traverse prose text nodes to extract `InlineReferenceEdge` entries in valid `see/per` forms, excluding code/inlineCode/HTML/link URL contexts.
1281
+
1282
+ 6. Emit per-document structural diagnostics.
1283
+
1284
+
1285
+ If per-document structural invariants fail, the document is not considered valid for graph ingestion.
1286
+
1287
+ ---
1288
+
1289
+ ### 1#11.2 - Pass 2: Corpus-Wide Integrity Checks
1290
+
1291
+ Given the set of extracted documents:
1292
+
1293
+ 1. Build a global index of `DocID -> uri`.
1294
+
1295
+ 2. Build a global index of `SectionID -> uri`.
1296
+
1297
+ 3. Validate global uniqueness:
1298
+
1299
+ - duplicate DocIDs are errors
1300
+
1301
+ - duplicate SectionIDs are errors
1302
+
1303
+ 4. Resolve all `ReferenceEdge.toDocId` targets against the DocID index, and compare each resolved entry's `title` with the target's H1 title.
1304
+
1305
+ 5. Resolve all `InlineReferenceEdge.toId` targets against the union of DocID and SectionID indexes.
1306
+
1307
+ 6. Validate that each inline reference’s parent DocID is declared in References. For each undeclared target reported in Pass 1, emit an error if its DocID is in the DocID index (1#9.5 rule 3).
1308
+
1309
+
1310
+ Emit corpus-wide diagnostics. Corpus-wide failures are errors; a title mismatch found in step 4 is a warning.
1311
+
1312
+ ---
1313
+
1314
+ ## 1#12 - Error Classification Philosophy
1315
+
1316
+ ### 1#12.1 - Structural Errors (Always ERROR)
1317
+
1318
+ - missing or invalid H1 DocID
1319
+
1320
+ - invalid section numbering / hierarchy
1321
+
1322
+ - missing References section
1323
+
1324
+ - malformed References entries
1325
+
1326
+ - an inline reference to a SectionID (`X#Y`) whose DocID `X` is not declared in References
1327
+
1328
+ - a heading whose identifier is not in the source form required by 1#9.11 rule 1 — indented, setext, separated from the hashes by a tab or more than one space, or carrying formatting or escapes inside the identifier itself
1329
+
1330
+ - a References heading or list that is not a child of the document root (1#9.11 rule 3)
1331
+
1332
+ - a References section whose source form defeats the recipes: a formatted `## References` heading, a target DocID that is not literal text at the start of the entry, or a direction label that is not literal text on the same line as the identifier (1#9.11 rule 4)
1333
+
1334
+ - a malformed inline reference: a candidate containing `#` that is not a complete, conforming token, such as `see 1#1#9`, `see 1#` or `see 1#1oops`. A candidate that fails the same test but contains no `#` is ordinary prose and is not reported at all. A candidate that passes the test is a reference whatever its form, so a conforming `see 8.1` is governed by the rules above and below, not by this exemption (1#9.5 rule 1)
1335
+
1336
+ - an inline reference whose keyword and identifier are not adjacent literal text on one line, where the target is identifiable — a SectionID, a declared DocID, the document's own DocID, or a DocID the corpus confirms (1#9.11 rule 2)
1337
+
1338
+
1339
+ ### 1#12.2 - Referential Integrity Errors (Always ERROR)
1340
+
1341
+ - duplicate DocID
1342
+
1343
+ - duplicate SectionID
1344
+
1345
+ - unresolved References targets
1346
+
1347
+ - unresolved inline targets
1348
+
1349
+ - an inline reference to a document in the corpus that the References section does not declare
1350
+
1351
+
1352
+ ### 1#12.3 - Advisory Findings (WARNING or INFO)
1353
+
1354
+ - a References entry whose title differs from its target's H1 title (WARNING)
1355
+
1356
+ - an inline `see`/`per` DocID target (no `#`) that is not declared, where no such document exists in the corpus or no corpus is available (WARNING)
1357
+
1358
+ - an inline reference to a bare DocID whose keyword and identifier are not adjacent literal text on one line — wrapped in a link or formatting, escaped, line-broken, or multiply spaced — where that DocID is undeclared, is not the document's own, and names no document in the corpus (WARNING; when the target is identifiable by any of those tests the same violation is an ERROR per 1#12.1, because only an unidentifiable bare number can be ordinary prose)
1359
+
1360
+ - a References section with no entries (INFO)
1361
+
1362
+
1363
+ These never fail validation.
1364
+
1365
+ ---
1366
+
1367
+ ## 1#13 - Public Interface Contract
1368
+
1369
+ The public API of the linter MUST:
1370
+
1371
+ - accept `(uri, text)` inputs
1372
+
1373
+ - return structured diagnostics
1374
+
1375
+ - return extracted structural artefacts
1376
+
1377
+ - remain host-agnostic
1378
+
1379
+
1380
+ It MUST NOT:
1381
+
1382
+ - discover files
1383
+
1384
+ - perform IO
1385
+
1386
+ - build graphs
1387
+
1388
+ - persist state
1389
+
1390
+ - log, or write to the console: everything the linter has to report is in the result it returns, and a host that wants logs writes them around the call
1391
+
1392
+ - assume CLI or editor context
1393
+
1394
+
1395
+ Host environments are responsible for:
1396
+
1397
+ - file enumeration
1398
+
1399
+ - aggregation across a corpus
1400
+
1401
+ - graph construction
1402
+
1403
+ - presentation of diagnostics
1404
+
1405
+
1406
+ The bundled `ecr` CLI is such a host environment. It performs file enumeration and presents diagnostics; the library API it calls remains bound by the constraints above.
1407
+
1408
+ ---
1409
+
1410
+ ## 1#14 - Versioning
1411
+
1412
+ This document defines **ECR**. It is versioned independently of any
1413
+ implementation: `@timiagama/ecr` states which specification version it
1414
+ implements, and the two version numbers move separately.
1415
+
1416
+ The specification uses semantic versioning, interpreted as follows. The contract
1417
+ is *which documents are conformant*, so compatibility is judged by what happens
1418
+ to a corpus that passes today:
1419
+
1420
+ - A **major** version may narrow what is conformant. A corpus valid under the
1421
+ previous major version may fail under the new one, and the new version must
1422
+ say which forms stopped being accepted and why.
1423
+
1424
+ - A **minor** version may widen what is conformant, or add optional structure.
1425
+ Every corpus valid under the previous minor version remains valid.
1426
+
1427
+ - A **patch** version changes wording only: clarifications, examples, corrected
1428
+ prose. The set of conformant documents is unchanged.
1429
+
1430
+ Diagnostic severity is part of the contract, not an implementation detail.
1431
+ Raising a finding to `error` narrows conformance and requires a major version;
1432
+ lowering one does not.
1433
+
1434
+ ### 1#14.1 - Version history
1435
+
1436
+ **2.0.0** — the first published specification. Adds the navigation guarantee
1437
+ (1#9.11) and the complete-token rule for inline targets (1#9.5), and makes
1438
+ explicit the severity model of 1#12.
1439
+
1440
+ This is a major version because it narrows conformance. Documents that were
1441
+ valid under 1.0.0 fail under 2.0.0 when they carry an identifier a text search
1442
+ cannot find: an identifier wrapped in formatting or containing a backslash
1443
+ escape, a setext or indented heading, a heading or citation whose spacing
1444
+ departs from a single space, a citation split across a line break, a References
1445
+ section nested in a container, or a References entry whose marker, identifier
1446
+ and direction label do not share a line. It also rejects malformed section
1447
+ targets — `see 1#1#9`, `see 1#`, `see 1#1oops` — which 1.0.0 silently truncated
1448
+ to a shorter identifier that happened to exist.
1449
+
1450
+ In the other direction, 2.0.0 ignores numeric prose that carries no `#`, such as
1451
+ `per 60s` or `see 1..2`. Under 1.0.0 such prose was truncated to its leading
1452
+ digits and treated as a citation, with an outcome that depended on the rest of
1453
+ the corpus: a warning where no such document existed, a corpus error where one
1454
+ existed but was undeclared, and a silent spurious edge where one existed and was
1455
+ declared. This does not narrow conformance — no document that passed now fails —
1456
+ but it does change what is extracted, and it removes edges that were never
1457
+ intended.
1458
+
1459
+ **1.0.0** — the original specification. Written for a different architecture, in
1460
+ which a constraint graph engine consumed the linter's output; never published.
1461
+
1462
+ ---
1463
+
1464
+ ## References
1465
+
1466
+ - 3 - Design Rationale - The Section Separator (dependency - records why the section separator exists and what it replaced)
1467
+