@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.
- package/LICENSE +202 -0
- package/NOTICE +16 -0
- package/README.md +411 -7
- package/dist/bin.d.ts +20 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +26 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli/corpus-loader.d.ts +198 -0
- package/dist/cli/corpus-loader.d.ts.map +1 -0
- package/dist/cli/corpus-loader.js +351 -0
- package/dist/cli/corpus-loader.js.map +1 -0
- package/dist/cli/corpus-statistics.d.ts +113 -0
- package/dist/cli/corpus-statistics.d.ts.map +1 -0
- package/dist/cli/corpus-statistics.js +119 -0
- package/dist/cli/corpus-statistics.js.map +1 -0
- package/dist/cli/diagnostic-reporter.d.ts +103 -0
- package/dist/cli/diagnostic-reporter.d.ts.map +1 -0
- package/dist/cli/diagnostic-reporter.js +254 -0
- package/dist/cli/diagnostic-reporter.js.map +1 -0
- package/dist/cli/project-ignore.d.ts +113 -0
- package/dist/cli/project-ignore.d.ts.map +1 -0
- package/dist/cli/project-ignore.js +199 -0
- package/dist/cli/project-ignore.js.map +1 -0
- package/dist/cli/safe-text.d.ts +25 -0
- package/dist/cli/safe-text.d.ts.map +1 -0
- package/dist/cli/safe-text.js +34 -0
- package/dist/cli/safe-text.js.map +1 -0
- package/dist/cli/supervisor.d.ts +108 -0
- package/dist/cli/supervisor.d.ts.map +1 -0
- package/dist/cli/supervisor.js +139 -0
- package/dist/cli/supervisor.js.map +1 -0
- package/dist/cli.d.ts +247 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +591 -0
- package/dist/cli.js.map +1 -0
- package/dist/corpus-validator.d.ts +183 -0
- package/dist/corpus-validator.d.ts.map +1 -0
- package/dist/corpus-validator.js +479 -0
- package/dist/corpus-validator.js.map +1 -0
- package/dist/document-identity-rule.d.ts +198 -0
- package/dist/document-identity-rule.d.ts.map +1 -0
- package/dist/document-identity-rule.js +244 -0
- package/dist/document-identity-rule.js.map +1 -0
- package/dist/ecr.d.ts +103 -0
- package/dist/ecr.d.ts.map +1 -0
- package/dist/ecr.js +216 -0
- package/dist/ecr.js.map +1 -0
- package/dist/glob-pattern.d.ts +94 -0
- package/dist/glob-pattern.d.ts.map +1 -0
- package/dist/glob-pattern.js +188 -0
- package/dist/glob-pattern.js.map +1 -0
- package/dist/heading-source-form.d.ts +77 -0
- package/dist/heading-source-form.d.ts.map +1 -0
- package/dist/heading-source-form.js +129 -0
- package/dist/heading-source-form.js.map +1 -0
- package/dist/identifier-grammar.d.ts +212 -0
- package/dist/identifier-grammar.d.ts.map +1 -0
- package/dist/identifier-grammar.js +273 -0
- package/dist/identifier-grammar.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/inline-reference-rule.d.ts +557 -0
- package/dist/inline-reference-rule.d.ts.map +1 -0
- package/dist/inline-reference-rule.js +802 -0
- package/dist/inline-reference-rule.js.map +1 -0
- package/dist/meta-documents.d.ts +91 -0
- package/dist/meta-documents.d.ts.map +1 -0
- package/dist/meta-documents.js +133 -0
- package/dist/meta-documents.js.map +1 -0
- package/dist/per-document-visitor.d.ts +510 -0
- package/dist/per-document-visitor.d.ts.map +1 -0
- package/dist/per-document-visitor.js +1032 -0
- package/dist/per-document-visitor.js.map +1 -0
- package/dist/references-section-rule.d.ts +482 -0
- package/dist/references-section-rule.d.ts.map +1 -0
- package/dist/references-section-rule.js +777 -0
- package/dist/references-section-rule.js.map +1 -0
- package/dist/section-hierarchy-rule.d.ts +299 -0
- package/dist/section-hierarchy-rule.d.ts.map +1 -0
- package/dist/section-hierarchy-rule.js +482 -0
- package/dist/section-hierarchy-rule.js.map +1 -0
- package/dist/source-alignment.d.ts +41 -0
- package/dist/source-alignment.d.ts.map +1 -0
- package/dist/source-alignment.js +166 -0
- package/dist/source-alignment.js.map +1 -0
- package/dist/source-lines.d.ts +69 -0
- package/dist/source-lines.d.ts.map +1 -0
- package/dist/source-lines.js +104 -0
- package/dist/source-lines.js.map +1 -0
- package/dist/spec-version.d.ts +19 -0
- package/dist/spec-version.d.ts.map +1 -0
- package/dist/spec-version.js +19 -0
- package/dist/spec-version.js.map +1 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/examples/docs/0. Orientation/0.0 - System Overview.md +34 -0
- package/examples/docs/0. Orientation/0.0.1 - Glossary.md +21 -0
- package/examples/docs/0. Orientation/0.0.2 - Quality Gates.md +22 -0
- package/examples/docs/3. Ingestion/3.1 - Ingestion - Validation Rules.md +33 -0
- package/examples/docs/4. Payments/4.10 - Settlement.md +22 -0
- package/examples/docs/4. Payments/4.2 - Payment Processing Contract.md +33 -0
- package/examples/docs/8. Orchestration/8.1 - Workflow Orchestration Contract.md +38 -0
- package/examples/docs/8. Orchestration/8.1.3 - Retry Policy.md +17 -0
- package/examples/docs/8. Orchestration/8.10 - Dead Letter Queue.md +20 -0
- package/examples/docs/README.md +7 -0
- package/package.json +65 -7
- package/protocol/navigation-protocol.md +223 -0
- package/spec/v2/1 - ECR - Structural Specification.md +1467 -0
- package/spec/v2/2 - ECR - User Guide.md +845 -0
- package/spec/v2/3 - Design Rationale - The Section Separator.md +243 -0
- package/src/bin.ts +30 -0
- package/src/cli/corpus-loader.ts +482 -0
- package/src/cli/corpus-statistics.ts +196 -0
- package/src/cli/diagnostic-reporter.ts +344 -0
- package/src/cli/project-ignore.ts +238 -0
- package/src/cli/safe-text.ts +39 -0
- package/src/cli/supervisor.ts +204 -0
- package/src/cli.ts +836 -0
- package/src/corpus-validator.ts +642 -0
- package/src/document-identity-rule.ts +422 -0
- package/src/ecr.ts +298 -0
- package/src/glob-pattern.ts +242 -0
- package/src/heading-source-form.ts +173 -0
- package/src/identifier-grammar.ts +380 -0
- package/src/index.ts +78 -0
- package/src/inline-reference-rule.ts +1235 -0
- package/src/meta-documents.ts +159 -0
- package/src/per-document-visitor.ts +1528 -0
- package/src/references-section-rule.ts +1182 -0
- package/src/section-hierarchy-rule.ts +705 -0
- package/src/source-alignment.ts +209 -0
- package/src/source-lines.ts +123 -0
- package/src/spec-version.ts +19 -0
- 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
|
+
|