@descent-vtt/spec-graph 0.2.1 → 0.2.3
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/CHANGELOG.md +131 -0
- package/dist/resolve.js +35 -5
- package/dist/resolve.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes, newest first. Versions follow [semver](https://semver.org):
|
|
4
|
+
a patch fixes behaviour without asking anything of a repository that upgrades.
|
|
5
|
+
|
|
6
|
+
## 0.2.3
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
**A path spelled without naming a file exactly could resolve to the wrong file.**
|
|
11
|
+
`docs/A` is two things at once - `docs/A.md` with its extension dropped, and
|
|
12
|
+
`docs/A/README.md` standing for its directory - and the index answering such
|
|
13
|
+
spellings held one id per key. A second claimant therefore did not make the link
|
|
14
|
+
ambiguous; it overwrote the first, and the link went to whichever file was
|
|
15
|
+
indexed last.
|
|
16
|
+
|
|
17
|
+
[ADR-0004](docs/adr/0004-reference-resolution.md) has always said two candidates
|
|
18
|
+
is worse than none, and a reference matching several documents is reported as
|
|
19
|
+
ambiguous rather than bound to whichever was indexed first. That now holds for
|
|
20
|
+
paths too.
|
|
21
|
+
|
|
22
|
+
Naming a file exactly is never ambiguous, whatever else is spelled the same way,
|
|
23
|
+
and a spelling only one file answers to still resolves in silence - so
|
|
24
|
+
`docs/adr/0007` addressing `docs/adr/0007/README.md` is unchanged.
|
|
25
|
+
|
|
26
|
+
**This can surface a new warning on upgrade**, in the one situation where it is
|
|
27
|
+
earned: a deliberate link written as a path that genuinely names two files. It
|
|
28
|
+
is a warning rather than an error, it lists both candidates, and prose that
|
|
29
|
+
happens to mention such a path stays silent, because an ambiguity nobody wrote
|
|
30
|
+
down is not a mistake anybody made.
|
|
31
|
+
|
|
32
|
+
## 0.2.2
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
**A link to a file holding a register resolved to one of its rows.** Both halves
|
|
37
|
+
of this needed no flag to trigger and no configuration to fix: a repository that
|
|
38
|
+
keeps no registers is unaffected, and one that does needs no change beyond the
|
|
39
|
+
upgrade.
|
|
40
|
+
|
|
41
|
+
Since 0.1.3 a register has yielded one specification per row or per section
|
|
42
|
+
([ADR-0009](docs/adr/0009-a-specification-is-a-region.md)), and every one of
|
|
43
|
+
those carried the file's path into the resolver's path index. That index keeps
|
|
44
|
+
whichever entry was written last, so the file's own entry was overwritten by its
|
|
45
|
+
final region, and:
|
|
46
|
+
|
|
47
|
+
- **A plain path link bound to the wrong node, silently.** `[A](docs/A.md)`
|
|
48
|
+
pointing at a file whose last table row is `FR-2` produced an edge to `FR-2`.
|
|
49
|
+
No diagnostic said so, and where the link carried a load-bearing relation -
|
|
50
|
+
`depends on`, `assumes` - the graph asserted a dependency nobody wrote. This
|
|
51
|
+
is the more serious half, because a wrong edge is invisible in a report that
|
|
52
|
+
says everything is consistent.
|
|
53
|
+
- **An anchored link reported a broken reference that was not broken.**
|
|
54
|
+
`[x](docs/A.md#how-to-read-this-document)` resolved to a row, and a row's
|
|
55
|
+
anchor set holds only the headings inside its own span - none, for a table
|
|
56
|
+
row - so a heading plainly present in the file was reported missing.
|
|
57
|
+
|
|
58
|
+
Both forms of register were affected, the table form and the heading form.
|
|
59
|
+
|
|
60
|
+
A region now stays out of the path index; only the file it lives in answers to
|
|
61
|
+
its path. The region keeps `path` on its node, so findings still name the file
|
|
62
|
+
it is written in and `document[path=docs/A.md]` still lists the file together
|
|
63
|
+
with its regions.
|
|
64
|
+
|
|
65
|
+
Found while adopting spec-graph in
|
|
66
|
+
[VirtualCortex](https://github.com/DescentVTT/VirtualCortex), where a
|
|
67
|
+
requirements table headed `Status` was enough to trigger it.
|
|
68
|
+
|
|
69
|
+
## 0.2.1
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- One written citation is one reference. `Superseded by ADR-0009` in a status
|
|
74
|
+
section was read twice - once by the status reader, which reports the whole
|
|
75
|
+
line, and once by prose scanning, which reports the identifier inside it.
|
|
76
|
+
Invisible while the target resolved, because two identical edges collapse into
|
|
77
|
+
one; two findings on one line the moment it did not.
|
|
78
|
+
|
|
79
|
+
## 0.2.0
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- **Historical records.** A journal, changelog or set of minutes is a log of
|
|
84
|
+
what was decided, not a decision. `record` is a lifecycle phase, declared by
|
|
85
|
+
`historyPatterns` in the configuration or by `<!-- @spec-history -->`, and
|
|
86
|
+
never guessed from a filename. Its links are still checked and work handed
|
|
87
|
+
*into* it is still a ghost handover; its own obligations and lifecycle are
|
|
88
|
+
not. See [ADR-0011](docs/adr/0011-a-record-is-not-a-specification.md).
|
|
89
|
+
- **Baselines.** `--record-baseline` writes today's findings as accepted debt;
|
|
90
|
+
`--baseline` reports only what is new since. Keyed on the specification and
|
|
91
|
+
the citation rather than on a line number, so it survives edits, reordering
|
|
92
|
+
and renames. Paid debt is reported, not failed. See
|
|
93
|
+
[ADR-0012](docs/adr/0012-a-baseline-is-a-ratchet.md).
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- The published tarball shipped source maps pointing at `../src/*.ts`, which was
|
|
98
|
+
not in the package. They are self-contained now, and the declaration maps that
|
|
99
|
+
could never resolve are gone.
|
|
100
|
+
|
|
101
|
+
## 0.1.3
|
|
102
|
+
|
|
103
|
+
### Added
|
|
104
|
+
|
|
105
|
+
- **A specification is a region of a file, not a file.** A register kept as
|
|
106
|
+
headings or as a table yields one specification per decision, each with its
|
|
107
|
+
own lifecycle, obligations and relations. See
|
|
108
|
+
[ADR-0009](docs/adr/0009-a-specification-is-a-region.md).
|
|
109
|
+
- **Repository configuration.** `.spec-graph.json`, `spec-graph.config.json`, or
|
|
110
|
+
a `"spec-graph"` key in `package.json`, with family allow and deny rules. See
|
|
111
|
+
[ADR-0010](docs/adr/0010-configuration-belongs-to-the-repository.md).
|
|
112
|
+
|
|
113
|
+
## 0.1.2
|
|
114
|
+
|
|
115
|
+
### Added
|
|
116
|
+
|
|
117
|
+
- `--ignore-ref`, for repositories where `[[...]]` tags a concept rather than
|
|
118
|
+
naming a file. See
|
|
119
|
+
[ADR-0008](docs/adr/0008-wiki-links-carry-no-path.md).
|
|
120
|
+
|
|
121
|
+
## 0.1.1
|
|
122
|
+
|
|
123
|
+
### Added
|
|
124
|
+
|
|
125
|
+
- `--strict`, raising every warning to an error. An explicit `--rule` still
|
|
126
|
+
wins, so `--strict --rule x=warn` exempts one rule rather than forcing a
|
|
127
|
+
choice between all of it and none.
|
|
128
|
+
|
|
129
|
+
## 0.1.0
|
|
130
|
+
|
|
131
|
+
First release.
|
package/dist/resolve.js
CHANGED
|
@@ -113,6 +113,7 @@ function buildIndex(extracted) {
|
|
|
113
113
|
byId: new Map(),
|
|
114
114
|
byAlias: new Map(),
|
|
115
115
|
byPath: new Map(),
|
|
116
|
+
byPathAlias: new Map(),
|
|
116
117
|
byFamilyNumber: new Map(),
|
|
117
118
|
families: new Set(),
|
|
118
119
|
anchors: new Map(),
|
|
@@ -129,8 +130,23 @@ function buildIndex(extracted) {
|
|
|
129
130
|
set.add(id);
|
|
130
131
|
index.byAlias.set(alias, set);
|
|
131
132
|
}
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
// Only a file answers to its path. A region keeps the path on its node, so a
|
|
134
|
+
// finding can say where it lives and `document[path=...]` can select it, but
|
|
135
|
+
// it must not be reachable *by* that path: two nodes answering to one path
|
|
136
|
+
// makes every link to the file ambiguous, and this index resolves a
|
|
137
|
+
// collision by keeping whichever was written last. A link to a file holding
|
|
138
|
+
// a register bound to its final row, quietly and with no diagnostic, and
|
|
139
|
+
// anchors were then checked against that row's span rather than the file's.
|
|
140
|
+
// ADR-0009 said a region does not claim the file's path; this is the index
|
|
141
|
+
// where that had to be true.
|
|
142
|
+
if (entry.containerId === null) {
|
|
143
|
+
index.byPath.set(entry.document.path.toLowerCase(), id);
|
|
144
|
+
for (const spelling of pathAliases(entry.document.path)) {
|
|
145
|
+
const set = index.byPathAlias.get(spelling) ?? new Set();
|
|
146
|
+
set.add(id);
|
|
147
|
+
index.byPathAlias.set(spelling, set);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
134
150
|
if (entry.identity.family !== null && entry.identity.number !== null) {
|
|
135
151
|
index.families.add(entry.identity.family);
|
|
136
152
|
const key = `${entry.identity.family}:${entry.identity.number}`;
|
|
@@ -143,9 +159,9 @@ function buildIndex(extracted) {
|
|
|
143
159
|
}
|
|
144
160
|
return index;
|
|
145
161
|
}
|
|
146
|
-
/** Every spelling of a path that a
|
|
147
|
-
function
|
|
148
|
-
const keys = new Set(
|
|
162
|
+
/** Every spelling of a path that addresses a file without naming it exactly. */
|
|
163
|
+
function pathAliases(path) {
|
|
164
|
+
const keys = new Set();
|
|
149
165
|
const extension = extnamePosix(path);
|
|
150
166
|
if (extension.length > 0)
|
|
151
167
|
keys.add(path.slice(0, path.length - extension.length).toLowerCase());
|
|
@@ -156,6 +172,10 @@ function pathKeys(path) {
|
|
|
156
172
|
if (parent.length > 0)
|
|
157
173
|
keys.add(parent.toLowerCase());
|
|
158
174
|
}
|
|
175
|
+
// A file never addresses itself inexactly. Leaving this in would let a
|
|
176
|
+
// contrived name - `docs/a.md.md`, whose stem is another file's whole path -
|
|
177
|
+
// make an exact link look ambiguous.
|
|
178
|
+
keys.delete(path.toLowerCase());
|
|
159
179
|
return [...keys];
|
|
160
180
|
}
|
|
161
181
|
/* -------------------------------------------------------------------------- */
|
|
@@ -209,9 +229,19 @@ function resolveOne(candidate, entry, index, nodes, options) {
|
|
|
209
229
|
function lookup(target, entry, index) {
|
|
210
230
|
if (looksLikePath(target)) {
|
|
211
231
|
const resolved = resolveFrom(entry.document.path, target).toLowerCase();
|
|
232
|
+
// Naming the file exactly is never ambiguous, whatever else is spelled the
|
|
233
|
+
// same way.
|
|
212
234
|
const byPath = index.byPath.get(resolved);
|
|
213
235
|
if (byPath)
|
|
214
236
|
return { ids: [byPath], near: [] };
|
|
237
|
+
// An inexact spelling can belong to more than one file: `docs/A` is both
|
|
238
|
+
// `docs/A.md` with its extension dropped and `docs/A/README.md` standing
|
|
239
|
+
// for its directory. Handing back every claimant makes that an
|
|
240
|
+
// ambiguous-reference the author can settle, rather than a silent pick of
|
|
241
|
+
// whichever happened to be indexed last.
|
|
242
|
+
const spelled = index.byPathAlias.get(resolved);
|
|
243
|
+
if (spelled && spelled.size > 0)
|
|
244
|
+
return { ids: [...spelled], near: [] };
|
|
215
245
|
// A path may still be spelled as an identifier in a nested folder layout.
|
|
216
246
|
const stem = basenamePosix(resolved);
|
|
217
247
|
const byStem = index.byAlias.get(normaliseRef(stem.replace(/\.[^.]+$/, '')));
|
package/dist/resolve.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Corpus-level reference resolution - the foreign-key check.\n *\n * Extraction produces relations whose targets are still just text. This module\n * turns each into an edge or into an explicit, reportable failure. The whole\n * value of the exercise is in refusing to do either job carelessly:\n *\n * - **A deliberate reference that does not resolve is an error.** A link, a\n * front-matter field or a directive naming a document that does not exist is\n * a broken foreign key, and silence about it is how reference graphs rot.\n * - **An opportunistic reference that does not resolve is nothing.** `ADR-0007`\n * written in prose is a citation; `SHA-256` is not. Since the two are\n * indistinguishable in isolation, bare identifiers are only reported when\n * their family already exists in the corpus - so `ADR-0099` in a repository\n * full of ADRs is a finding, and `T-1000` in the same repository is not.\n * - **Two candidates is worse than none.** A reference that matches several\n * documents is reported as ambiguous rather than silently bound to whichever\n * one happened to be indexed first.\n */\n\nimport type { ExtractedDocument, ReferenceCandidate } from './extract.js';\nimport {\n isDocumentTarget,\n isExternal,\n looksLikePath,\n normaliseRef,\n parseBareRef,\n parsePrefixedRef,\n splitAnchor,\n} from './identity.js';\nimport { basenamePosix, extnamePosix, resolveFrom } from './paths.js';\nimport type { DanglingRef, DocumentNode, Edge, ItemNode, ParseProblem, SpecNode } from './types.js';\n\nexport interface ResolveOptions {\n /**\n * Whether a repository-relative path exists on disk.\n *\n * Used only to tell \"you linked to a file that is not a specification\" apart\n * from \"you linked to nothing at all\", which are different mistakes with\n * different fixes.\n */\n readonly fileExists?: ((path: string) => boolean) | undefined;\n /**\n * Targets whose failure to resolve should not be reported.\n *\n * This is a *suppression* filter, not a resolution one: it is consulted only\n * when a reference has already failed to resolve. A reference that resolves\n * still becomes an edge no matter what this says, so no amount of\n * configuration can silently delete a relation from the graph.\n */\n readonly isIgnoredReference?: ((target: string) => boolean) | undefined;\n /**\n * Families that are never citations in this repository.\n *\n * `RFC 2119` is the canonical case: every specification cites it, and a\n * repository that keeps its own `RFC-*` documents will otherwise read that\n * sentence as a dangling reference to a local RFC 2119 it does not have.\n * Consulted only after resolution has failed, like every other filter here.\n */\n readonly isIgnoredFamily?: ((family: string) => boolean) | undefined;\n}\n\nexport interface ResolvedCorpus {\n readonly nodes: ReadonlyMap<string, SpecNode>;\n readonly documents: readonly DocumentNode[];\n readonly items: readonly ItemNode[];\n readonly edges: readonly Edge[];\n readonly dangling: readonly DanglingRef[];\n readonly problems: readonly ParseProblem[];\n}\n\ninterface Index {\n readonly byId: Map<string, string>;\n readonly byAlias: Map<string, Set<string>>;\n readonly byPath: Map<string, string>;\n readonly byFamilyNumber: Map<string, Set<string>>;\n readonly families: Set<string>;\n readonly anchors: Map<string, ReadonlySet<string>>;\n readonly itemIds: Set<string>;\n readonly familyOf: Map<string, string | null>;\n}\n\n/** Resolves every extracted reference against the whole corpus. */\nexport function resolveCorpus(\n extracted: readonly ExtractedDocument[],\n options: ResolveOptions = {},\n): ResolvedCorpus {\n const index = buildIndex(extracted);\n const nodes = new Map<string, SpecNode>();\n const documents: DocumentNode[] = [];\n const items: ItemNode[] = [];\n const problems: ParseProblem[] = [];\n\n for (const entry of extracted) {\n const existing = nodes.get(entry.document.id);\n if (existing && existing.kind === 'document') {\n problems.push({\n message: `duplicate document id \"${entry.document.id}\", already declared by ${existing.path}`,\n at: entry.document.at,\n });\n continue;\n }\n nodes.set(entry.document.id, entry.document);\n documents.push(entry.document);\n for (const item of entry.items) {\n nodes.set(item.id, item);\n items.push(item);\n }\n problems.push(...entry.problems);\n }\n\n const edges: Edge[] = [];\n const dangling: DanglingRef[] = [];\n const byKey = new Map<string, number>();\n\n // Structural edges: a register contains the specifications written inside it.\n for (const entry of extracted) {\n if (entry.containerId === null) continue;\n if (!nodes.has(entry.containerId) || !nodes.has(entry.document.id)) continue;\n edges.push({\n kind: 'contains',\n from: entry.containerId,\n to: entry.document.id,\n origin: 'structural',\n declaredAt: entry.document.at,\n raw: entry.document.title,\n declaredIn: [entry.document.at.file],\n reflexive: false,\n });\n }\n\n // Structural edges: a document contains its items.\n for (const item of items) {\n edges.push({\n kind: 'contains',\n from: item.document,\n to: item.id,\n origin: 'structural',\n declaredAt: item.at,\n raw: item.text,\n declaredIn: [item.at.file],\n reflexive: false,\n });\n }\n\n for (const entry of extracted) {\n for (const candidate of entry.references) {\n if (!nodes.has(candidate.from)) continue;\n const outcome = resolveOne(candidate, entry, index, nodes, options);\n if (outcome === null) continue;\n if ('reason' in outcome) {\n dangling.push(outcome);\n continue;\n }\n // Collapse duplicates: the same relation stated twice is one edge - but\n // every declaration site is kept, because a supersession recorded in only\n // one of the two documents is itself a finding.\n const key = `${outcome.kind} ${outcome.from} ${outcome.to}`;\n const existing = byKey.get(key);\n if (existing !== undefined) {\n const previous = edges[existing] as Edge;\n if (!previous.declaredIn.includes(outcome.declaredAt.file)) {\n edges[existing] = { ...previous, declaredIn: [...previous.declaredIn, outcome.declaredAt.file] };\n }\n continue;\n }\n byKey.set(key, edges.length);\n edges.push(outcome);\n }\n }\n\n return { nodes, documents, items, edges, dangling, problems };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Indexing */\n/* -------------------------------------------------------------------------- */\n\nfunction buildIndex(extracted: readonly ExtractedDocument[]): Index {\n const index: Index = {\n byId: new Map(),\n byAlias: new Map(),\n byPath: new Map(),\n byFamilyNumber: new Map(),\n families: new Set(),\n anchors: new Map(),\n itemIds: new Set(),\n familyOf: new Map(),\n };\n\n for (const entry of extracted) {\n const id = entry.document.id;\n index.byId.set(normaliseRef(id), id);\n index.anchors.set(id, entry.anchors);\n index.familyOf.set(id, entry.identity.family);\n\n for (const alias of entry.document.aliases) {\n const set = index.byAlias.get(alias) ?? new Set<string>();\n set.add(id);\n index.byAlias.set(alias, set);\n }\n\n for (const path of pathKeys(entry.document.path)) index.byPath.set(path, id);\n\n if (entry.identity.family !== null && entry.identity.number !== null) {\n index.families.add(entry.identity.family);\n const key = `${entry.identity.family}:${entry.identity.number}`;\n const set = index.byFamilyNumber.get(key) ?? new Set<string>();\n set.add(id);\n index.byFamilyNumber.set(key, set);\n }\n\n for (const item of entry.items) index.itemIds.add(item.id);\n }\n\n return index;\n}\n\n/** Every spelling of a path that a link might use. */\nfunction pathKeys(path: string): string[] {\n const keys = new Set<string>([path.toLowerCase()]);\n const extension = extnamePosix(path);\n if (extension.length > 0) keys.add(path.slice(0, path.length - extension.length).toLowerCase());\n // `docs/adr/0007/README.md` is also addressed as `docs/adr/0007`.\n const base = basenamePosix(path);\n if (/^(readme|index)\\./i.test(base)) {\n const parent = path.slice(0, path.length - base.length - 1);\n if (parent.length > 0) keys.add(parent.toLowerCase());\n }\n return [...keys];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Resolution */\n/* -------------------------------------------------------------------------- */\n\nfunction resolveOne(\n candidate: ReferenceCandidate,\n entry: ExtractedDocument,\n index: Index,\n nodes: ReadonlyMap<string, SpecNode>,\n options: ResolveOptions,\n): Edge | DanglingRef | null {\n const { target: bare, anchor } = splitAnchor(candidate.target);\n\n // A pure `#anchor` points inside the citing document.\n if (bare.length === 0 && anchor !== null) {\n return bindAnchor(candidate, entry.document.id, anchor, index, nodes, options);\n }\n\n if (isExternal(bare)) return null;\n // A link to source code, an image or a config file is not a specification\n // reference. Validating it would report an error on the most ordinary thing a\n // design document does.\n if (!isDocumentTarget(bare)) return null;\n\n const found = lookup(bare, entry, index);\n\n if (found.ids.length === 0) {\n if (candidate.opportunistic && !worthReporting(bare, index)) return null;\n if (isFiltered(candidate.target, bare, options)) return null;\n // A trailing slash names a directory. Linking to one is ordinary - \"the\n // decisions live in [archive/](archive/)\" - and is not a citation of any\n // document. Resolution is still attempted first, so a directory-style\n // document written as `0007-sharding/` continues to resolve.\n if (bare.endsWith('/')) return null;\n const reason = pathMissingReason(bare, entry, options);\n return dangle(candidate, reason, found.near);\n }\n\n if (found.ids.length > 1) {\n if (candidate.opportunistic) return null;\n if (isFiltered(candidate.target, bare, options)) return null;\n return dangle(candidate, 'ambiguous', found.ids);\n }\n\n const documentId = found.ids[0] as string;\n\n // A document naming itself in prose - almost always its own H1, `# ADR-0007:\n // Sharding` - is not a citation. Deliberate self-links are kept, because a\n // link that points at its own document is a real mistake worth reporting.\n if (candidate.opportunistic && documentId === entry.document.id) return null;\n\n if (anchor !== null) return bindAnchor(candidate, documentId, anchor, index, nodes, options);\n\n return makeEdge(candidate, documentId);\n}\n\ninterface Lookup {\n readonly ids: readonly string[];\n /** Near-misses worth suggesting in the report. */\n readonly near: readonly string[];\n}\n\nfunction lookup(target: string, entry: ExtractedDocument, index: Index): Lookup {\n if (looksLikePath(target)) {\n const resolved = resolveFrom(entry.document.path, target).toLowerCase();\n const byPath = index.byPath.get(resolved);\n if (byPath) return { ids: [byPath], near: [] };\n // A path may still be spelled as an identifier in a nested folder layout.\n const stem = basenamePosix(resolved);\n const byStem = index.byAlias.get(normaliseRef(stem.replace(/\\.[^.]+$/, '')));\n if (byStem && byStem.size > 0) return { ids: [...byStem], near: [] };\n return { ids: [], near: [] };\n }\n\n const key = normaliseRef(target);\n if (key.length === 0) return { ids: [], near: [] };\n\n const byId = index.byId.get(key);\n if (byId) return { ids: [byId], near: [] };\n\n const byAlias = index.byAlias.get(key);\n if (byAlias && byAlias.size > 0) return { ids: [...byAlias], near: [] };\n\n const prefixed = parsePrefixedRef(target);\n if (prefixed) {\n const byFamily = index.byFamilyNumber.get(`${prefixed.family}:${prefixed.number}`);\n if (byFamily && byFamily.size > 0) return { ids: [...byFamily], near: [] };\n return { ids: [], near: [] };\n }\n\n // A bare number resolves only inside the citing document's own family. A\n // repository with both `adr/0007` and `rfc/0007` is ordinary, and guessing\n // between them would be worse than reporting nothing.\n const number = parseBareRef(target);\n if (number !== null) {\n const family = index.familyOf.get(entry.document.id) ?? null;\n if (family === null) return { ids: [], near: [] };\n const byFamily = index.byFamilyNumber.get(`${family}:${number}`);\n if (byFamily && byFamily.size > 0) return { ids: [...byFamily], near: [] };\n }\n\n return { ids: [], near: [] };\n}\n\n/** Whether a repository has declared this reference none of its business. */\nfunction isFiltered(target: string, bare: string, options: ResolveOptions): boolean {\n if (options.isIgnoredReference?.(target)) return true;\n const prefixed = parsePrefixedRef(bare);\n return prefixed !== null && options.isIgnoredFamily?.(prefixed.family) === true;\n}\n\n/**\n * Whether an unresolved bare identifier deserves a report.\n *\n * Only when the corpus already contains that family. `ADR-0099` in a repository\n * of ADRs is a real dangling citation; `T-1000` in the same repository is a\n * sentence about a robot.\n */\nfunction worthReporting(target: string, index: Index): boolean {\n const prefixed = parsePrefixedRef(target);\n return prefixed !== null && index.families.has(prefixed.family);\n}\n\nfunction pathMissingReason(\n target: string,\n entry: ExtractedDocument,\n options: ResolveOptions,\n): DanglingRef['reason'] {\n if (!looksLikePath(target) || !options.fileExists) return 'unknown-target';\n const resolved = resolveFrom(entry.document.path, target);\n return options.fileExists(resolved) ? 'not-a-spec' : 'unknown-target';\n}\n\n/**\n * Binds a `#anchor` to an item or a section.\n *\n * Anchors are how a delegation names a specific obligation rather than a whole\n * document, so resolving them precisely is what lets a ghost-handover finding\n * say *which* question was handed into the archive.\n */\nfunction bindAnchor(\n candidate: ReferenceCandidate,\n documentId: string,\n anchor: string,\n index: Index,\n nodes: ReadonlyMap<string, SpecNode>,\n options: ResolveOptions,\n): Edge | DanglingRef | null {\n const itemId = `${documentId}#${anchor}`;\n if (index.itemIds.has(itemId)) return makeEdge(candidate, itemId);\n\n const anchors = index.anchors.get(documentId);\n if (anchors?.has(anchor.toLowerCase())) return makeEdge(candidate, documentId);\n\n // Some renderers slugify differently; try the loosest reasonable match before\n // calling it broken.\n const loose = anchor.toLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, '-').replace(/^-|-$/g, '');\n if (anchors?.has(loose)) return makeEdge(candidate, documentId);\n\n if (isFiltered(candidate.target, splitAnchor(candidate.target).target, options)) return null;\n\n const candidates = [...(nodes.keys() as Iterable<string>)].filter(\n (id) => id.startsWith(`${documentId}#`) && id.toLowerCase().includes(loose),\n );\n return {\n kind: candidate.kind,\n from: candidate.from,\n target: candidate.target,\n reason: 'unknown-anchor',\n origin: candidate.origin,\n declaredAt: candidate.declaredAt,\n raw: candidate.raw,\n candidates,\n };\n}\n\nfunction makeEdge(candidate: ReferenceCandidate, resolved: string): Edge {\n const from = candidate.inverted ? resolved : candidate.from;\n const to = candidate.inverted ? candidate.from : resolved;\n return {\n kind: candidate.kind,\n from,\n to,\n origin: candidate.origin,\n declaredAt: candidate.declaredAt,\n raw: candidate.raw,\n declaredIn: [candidate.declaredAt.file],\n reflexive: documentOf(from) === documentOf(to),\n };\n}\n\nfunction dangle(\n candidate: ReferenceCandidate,\n reason: DanglingRef['reason'],\n candidates: readonly string[],\n): DanglingRef {\n return {\n kind: candidate.kind,\n from: candidate.from,\n target: candidate.target,\n reason,\n origin: candidate.origin,\n declaredAt: candidate.declaredAt,\n raw: candidate.raw,\n candidates,\n };\n}\n\n/** The document part of a node id: `ADR-7#q.1` -> `ADR-7`. */\nexport function documentOf(nodeId: string): string {\n const hash = nodeId.indexOf('#');\n return hash === -1 ? nodeId : nodeId.slice(0, hash);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EACL,gBAAgB,EAChB,UAAU,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AA2DtE,mEAAmE;AACnE,MAAM,UAAU,aAAa,CAC3B,SAAuC,EACvC,OAAO,GAAmB,EAAE;IAE5B,MAAM,KAAK,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC1C,MAAM,SAAS,GAAmB,EAAE,CAAC;IACrC,MAAM,KAAK,GAAe,EAAE,CAAC;IAC7B,MAAM,QAAQ,GAAmB,EAAE,CAAC;IAEpC,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC9C,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC7C,QAAQ,CAAC,IAAI,CAAC;gBACZ,OAAO,EAAE,0BAA0B,KAAK,CAAC,QAAQ,CAAC,EAAE,0BAA0B,QAAQ,CAAC,IAAI,EAAE;gBAC7F,EAAE,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE;aACtB,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC7C,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAC/B,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;YACzB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;IAExC,8EAA8E;IAC9E,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI;YAAE,SAAS;QACzC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,SAAS;QAC7E,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,KAAK,CAAC,WAAW;YACvB,EAAE,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE;YACrB,MAAM,EAAE,YAAY;YACpB,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE;YAC7B,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK;YACzB,UAAU,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC;YACpC,SAAS,EAAE,KAAK;SACjB,CAAC,CAAC;IACL,CAAC;IAED,mDAAmD;IACnD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,UAAU;YAChB,IAAI,EAAE,IAAI,CAAC,QAAQ;YACnB,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,MAAM,EAAE,YAAY;YACpB,UAAU,EAAE,IAAI,CAAC,EAAE;YACnB,GAAG,EAAE,IAAI,CAAC,IAAI;YACd,UAAU,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;YAC1B,SAAS,EAAE,KAAK;SACjB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;YACzC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC;gBAAE,SAAS;YACzC,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YACpE,IAAI,OAAO,KAAK,IAAI;gBAAE,SAAS;YAC/B,IAAI,QAAQ,IAAI,OAAO,EAAE,CAAC;gBACxB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvB,SAAS;YACX,CAAC;YACD,wEAAwE;YACxE,0EAA0E;YAC1E,gDAAgD;YAChD,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC;YAC5D,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAS,CAAC;gBACzC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC3D,KAAK,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,UAAU,EAAE,CAAC,GAAG,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnG,CAAC;gBACD,SAAS;YACX,CAAC;YACD,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAChE,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF,SAAS,UAAU,CAAC,SAAuC;IACzD,MAAM,KAAK,GAAU;QACnB,IAAI,EAAE,IAAI,GAAG,EAAE;QACf,OAAO,EAAE,IAAI,GAAG,EAAE;QAClB,MAAM,EAAE,IAAI,GAAG,EAAE;QACjB,WAAW,EAAE,IAAI,GAAG,EAAE;QACtB,cAAc,EAAE,IAAI,GAAG,EAAE;QACzB,QAAQ,EAAE,IAAI,GAAG,EAAE;QACnB,OAAO,EAAE,IAAI,GAAG,EAAE;QAClB,OAAO,EAAE,IAAI,GAAG,EAAE;QAClB,QAAQ,EAAE,IAAI,GAAG,EAAE;KACpB,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QACrC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;QACrC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAE9C,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YAC3C,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,GAAG,EAAU,CAAC;YAC1D,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACZ,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QAChC,CAAC;QAED,6EAA6E;QAC7E,6EAA6E;QAC7E,2EAA2E;QAC3E,oEAAoE;QACpE,4EAA4E;QAC5E,yEAAyE;QACzE,4EAA4E;QAC5E,2EAA2E;QAC3E,6BAA6B;QAC7B,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;YAC/B,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC;YACxD,KAAK,MAAM,QAAQ,IAAI,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;gBACxD,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,GAAG,EAAU,CAAC;gBACjE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACZ,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YACvC,CAAC;QACH,CAAC;QAED,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;YACrE,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC1C,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAChE,MAAM,GAAG,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,EAAU,CAAC;YAC/D,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACZ,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACrC,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK;YAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gFAAgF;AAChF,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,SAAS,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAChG,kEAAkE;IAClE,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC5D,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,uEAAuE;IACvE,6EAA6E;IAC7E,qCAAqC;IACrC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAChC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;AACnB,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF,SAAS,UAAU,CACjB,SAA6B,EAC7B,KAAwB,EACxB,KAAY,EACZ,KAAoC,EACpC,OAAuB;IAEvB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAE/D,sDAAsD;IACtD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACzC,OAAO,UAAU,CAAC,SAAS,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IACjF,CAAC;IAED,IAAI,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,0EAA0E;IAC1E,8EAA8E;IAC9E,wBAAwB;IACxB,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IAEzC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,IAAI,SAAS,CAAC,aAAa,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACzE,IAAI,UAAU,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7D,wEAAwE;QACxE,yEAAyE;QACzE,sEAAsE;QACtE,6DAA6D;QAC7D,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QACpC,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QACvD,OAAO,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,IAAI,SAAS,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC;QACzC,IAAI,UAAU,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7D,OAAO,MAAM,CAAC,SAAS,EAAE,WAAW,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACnD,CAAC;IAED,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAW,CAAC;IAE1C,6EAA6E;IAC7E,2EAA2E;IAC3E,0EAA0E;IAC1E,IAAI,SAAS,CAAC,aAAa,IAAI,UAAU,KAAK,KAAK,CAAC,QAAQ,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAE7E,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;IAE7F,OAAO,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AACzC,CAAC;AAQD,SAAS,MAAM,CAAC,MAAc,EAAE,KAAwB,EAAE,KAAY;IACpE,IAAI,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;QACxE,2EAA2E;QAC3E,YAAY;QACZ,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,MAAM;YAAE,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAC/C,yEAAyE;QACzE,yEAAyE;QACzE,+DAA+D;QAC/D,0EAA0E;QAC1E,yCAAyC;QACzC,MAAM,OAAO,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAChD,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC;YAAE,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QACxE,0EAA0E;QAC1E,MAAM,IAAI,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;QACrC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QAC7E,IAAI,MAAM,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC;YAAE,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QACrE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAC/B,CAAC;IAED,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAEnD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,IAAI;QAAE,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAE3C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAExE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QACnF,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI,GAAG,CAAC;YAAE,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAC3E,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAC/B,CAAC;IAED,yEAAyE;IACzE,2EAA2E;IAC3E,sDAAsD;IACtD,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACpC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC;QAC7D,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC,CAAC;QACjE,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI,GAAG,CAAC;YAAE,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,QAAQ,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;IAC7E,CAAC;IAED,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AAC/B,CAAC;AAED,6EAA6E;AAC7E,SAAS,UAAU,CAAC,MAAc,EAAE,IAAY,EAAE,OAAuB;IACvE,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACtD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO,QAAQ,KAAK,IAAI,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;AAClF,CAAC;AAED;;;;;;GAMG;AACH,SAAS,cAAc,CAAC,MAAc,EAAE,KAAY;IAClD,MAAM,QAAQ,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;IAC1C,OAAO,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAClE,CAAC;AAED,SAAS,iBAAiB,CACxB,MAAc,EACd,KAAwB,EACxB,OAAuB;IAEvB,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU;QAAE,OAAO,gBAAgB,CAAC;IAC3E,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC1D,OAAO,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,gBAAgB,CAAC;AACxE,CAAC;AAED;;;;;;GAMG;AACH,SAAS,UAAU,CACjB,SAA6B,EAC7B,UAAkB,EAClB,MAAc,EACd,KAAY,EACZ,KAAoC,EACpC,OAAuB;IAEvB,MAAM,MAAM,GAAG,GAAG,UAAU,IAAI,MAAM,EAAE,CAAC;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAElE,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC9C,IAAI,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAAE,OAAO,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAE/E,8EAA8E;IAC9E,qBAAqB;IACrB,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC1F,IAAI,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAEhE,IAAI,UAAU,CAAC,SAAS,CAAC,MAAM,EAAE,WAAW,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAE7F,MAAM,UAAU,GAAG,CAAC,GAAI,KAAK,CAAC,IAAI,EAAuB,CAAC,CAAC,MAAM,CAC/D,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,UAAU,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAC5E,CAAC;IACF,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,MAAM,EAAE,gBAAgB;QACxB,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,UAAU,EAAE,SAAS,CAAC,UAAU;QAChC,GAAG,EAAE,SAAS,CAAC,GAAG;QAClB,UAAU;KACX,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,SAA6B,EAAE,QAAgB;IAC/D,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC;IAC5D,MAAM,EAAE,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;IAC1D,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,IAAI;QACJ,EAAE;QACF,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,UAAU,EAAE,SAAS,CAAC,UAAU;QAChC,GAAG,EAAE,SAAS,CAAC,GAAG;QAClB,UAAU,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC;QACvC,SAAS,EAAE,UAAU,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,EAAE,CAAC;KAC/C,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CACb,SAA6B,EAC7B,MAA6B,EAC7B,UAA6B;IAE7B,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,MAAM;QACN,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,UAAU,EAAE,SAAS,CAAC,UAAU;QAChC,GAAG,EAAE,SAAS,CAAC,GAAG;QAClB,UAAU;KACX,CAAC;AACJ,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AACtD,CAAC","sourcesContent":["/**\n * Corpus-level reference resolution - the foreign-key check.\n *\n * Extraction produces relations whose targets are still just text. This module\n * turns each into an edge or into an explicit, reportable failure. The whole\n * value of the exercise is in refusing to do either job carelessly:\n *\n * - **A deliberate reference that does not resolve is an error.** A link, a\n * front-matter field or a directive naming a document that does not exist is\n * a broken foreign key, and silence about it is how reference graphs rot.\n * - **An opportunistic reference that does not resolve is nothing.** `ADR-0007`\n * written in prose is a citation; `SHA-256` is not. Since the two are\n * indistinguishable in isolation, bare identifiers are only reported when\n * their family already exists in the corpus - so `ADR-0099` in a repository\n * full of ADRs is a finding, and `T-1000` in the same repository is not.\n * - **Two candidates is worse than none.** A reference that matches several\n * documents is reported as ambiguous rather than silently bound to whichever\n * one happened to be indexed first.\n */\n\nimport type { ExtractedDocument, ReferenceCandidate } from './extract.js';\nimport {\n isDocumentTarget,\n isExternal,\n looksLikePath,\n normaliseRef,\n parseBareRef,\n parsePrefixedRef,\n splitAnchor,\n} from './identity.js';\nimport { basenamePosix, extnamePosix, resolveFrom } from './paths.js';\nimport type { DanglingRef, DocumentNode, Edge, ItemNode, ParseProblem, SpecNode } from './types.js';\n\nexport interface ResolveOptions {\n /**\n * Whether a repository-relative path exists on disk.\n *\n * Used only to tell \"you linked to a file that is not a specification\" apart\n * from \"you linked to nothing at all\", which are different mistakes with\n * different fixes.\n */\n readonly fileExists?: ((path: string) => boolean) | undefined;\n /**\n * Targets whose failure to resolve should not be reported.\n *\n * This is a *suppression* filter, not a resolution one: it is consulted only\n * when a reference has already failed to resolve. A reference that resolves\n * still becomes an edge no matter what this says, so no amount of\n * configuration can silently delete a relation from the graph.\n */\n readonly isIgnoredReference?: ((target: string) => boolean) | undefined;\n /**\n * Families that are never citations in this repository.\n *\n * `RFC 2119` is the canonical case: every specification cites it, and a\n * repository that keeps its own `RFC-*` documents will otherwise read that\n * sentence as a dangling reference to a local RFC 2119 it does not have.\n * Consulted only after resolution has failed, like every other filter here.\n */\n readonly isIgnoredFamily?: ((family: string) => boolean) | undefined;\n}\n\nexport interface ResolvedCorpus {\n readonly nodes: ReadonlyMap<string, SpecNode>;\n readonly documents: readonly DocumentNode[];\n readonly items: readonly ItemNode[];\n readonly edges: readonly Edge[];\n readonly dangling: readonly DanglingRef[];\n readonly problems: readonly ParseProblem[];\n}\n\ninterface Index {\n readonly byId: Map<string, string>;\n readonly byAlias: Map<string, Set<string>>;\n /** The literal path of a file. Unique: no two files share one. */\n readonly byPath: Map<string, string>;\n /**\n * Spellings that address a file without naming it exactly - the extension\n * dropped, a directory standing for its README. Several files can answer to\n * one of those, so this is a set, exactly as `byAlias` is.\n */\n readonly byPathAlias: Map<string, Set<string>>;\n readonly byFamilyNumber: Map<string, Set<string>>;\n readonly families: Set<string>;\n readonly anchors: Map<string, ReadonlySet<string>>;\n readonly itemIds: Set<string>;\n readonly familyOf: Map<string, string | null>;\n}\n\n/** Resolves every extracted reference against the whole corpus. */\nexport function resolveCorpus(\n extracted: readonly ExtractedDocument[],\n options: ResolveOptions = {},\n): ResolvedCorpus {\n const index = buildIndex(extracted);\n const nodes = new Map<string, SpecNode>();\n const documents: DocumentNode[] = [];\n const items: ItemNode[] = [];\n const problems: ParseProblem[] = [];\n\n for (const entry of extracted) {\n const existing = nodes.get(entry.document.id);\n if (existing && existing.kind === 'document') {\n problems.push({\n message: `duplicate document id \"${entry.document.id}\", already declared by ${existing.path}`,\n at: entry.document.at,\n });\n continue;\n }\n nodes.set(entry.document.id, entry.document);\n documents.push(entry.document);\n for (const item of entry.items) {\n nodes.set(item.id, item);\n items.push(item);\n }\n problems.push(...entry.problems);\n }\n\n const edges: Edge[] = [];\n const dangling: DanglingRef[] = [];\n const byKey = new Map<string, number>();\n\n // Structural edges: a register contains the specifications written inside it.\n for (const entry of extracted) {\n if (entry.containerId === null) continue;\n if (!nodes.has(entry.containerId) || !nodes.has(entry.document.id)) continue;\n edges.push({\n kind: 'contains',\n from: entry.containerId,\n to: entry.document.id,\n origin: 'structural',\n declaredAt: entry.document.at,\n raw: entry.document.title,\n declaredIn: [entry.document.at.file],\n reflexive: false,\n });\n }\n\n // Structural edges: a document contains its items.\n for (const item of items) {\n edges.push({\n kind: 'contains',\n from: item.document,\n to: item.id,\n origin: 'structural',\n declaredAt: item.at,\n raw: item.text,\n declaredIn: [item.at.file],\n reflexive: false,\n });\n }\n\n for (const entry of extracted) {\n for (const candidate of entry.references) {\n if (!nodes.has(candidate.from)) continue;\n const outcome = resolveOne(candidate, entry, index, nodes, options);\n if (outcome === null) continue;\n if ('reason' in outcome) {\n dangling.push(outcome);\n continue;\n }\n // Collapse duplicates: the same relation stated twice is one edge - but\n // every declaration site is kept, because a supersession recorded in only\n // one of the two documents is itself a finding.\n const key = `${outcome.kind} ${outcome.from} ${outcome.to}`;\n const existing = byKey.get(key);\n if (existing !== undefined) {\n const previous = edges[existing] as Edge;\n if (!previous.declaredIn.includes(outcome.declaredAt.file)) {\n edges[existing] = { ...previous, declaredIn: [...previous.declaredIn, outcome.declaredAt.file] };\n }\n continue;\n }\n byKey.set(key, edges.length);\n edges.push(outcome);\n }\n }\n\n return { nodes, documents, items, edges, dangling, problems };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Indexing */\n/* -------------------------------------------------------------------------- */\n\nfunction buildIndex(extracted: readonly ExtractedDocument[]): Index {\n const index: Index = {\n byId: new Map(),\n byAlias: new Map(),\n byPath: new Map(),\n byPathAlias: new Map(),\n byFamilyNumber: new Map(),\n families: new Set(),\n anchors: new Map(),\n itemIds: new Set(),\n familyOf: new Map(),\n };\n\n for (const entry of extracted) {\n const id = entry.document.id;\n index.byId.set(normaliseRef(id), id);\n index.anchors.set(id, entry.anchors);\n index.familyOf.set(id, entry.identity.family);\n\n for (const alias of entry.document.aliases) {\n const set = index.byAlias.get(alias) ?? new Set<string>();\n set.add(id);\n index.byAlias.set(alias, set);\n }\n\n // Only a file answers to its path. A region keeps the path on its node, so a\n // finding can say where it lives and `document[path=...]` can select it, but\n // it must not be reachable *by* that path: two nodes answering to one path\n // makes every link to the file ambiguous, and this index resolves a\n // collision by keeping whichever was written last. A link to a file holding\n // a register bound to its final row, quietly and with no diagnostic, and\n // anchors were then checked against that row's span rather than the file's.\n // ADR-0009 said a region does not claim the file's path; this is the index\n // where that had to be true.\n if (entry.containerId === null) {\n index.byPath.set(entry.document.path.toLowerCase(), id);\n for (const spelling of pathAliases(entry.document.path)) {\n const set = index.byPathAlias.get(spelling) ?? new Set<string>();\n set.add(id);\n index.byPathAlias.set(spelling, set);\n }\n }\n\n if (entry.identity.family !== null && entry.identity.number !== null) {\n index.families.add(entry.identity.family);\n const key = `${entry.identity.family}:${entry.identity.number}`;\n const set = index.byFamilyNumber.get(key) ?? new Set<string>();\n set.add(id);\n index.byFamilyNumber.set(key, set);\n }\n\n for (const item of entry.items) index.itemIds.add(item.id);\n }\n\n return index;\n}\n\n/** Every spelling of a path that addresses a file without naming it exactly. */\nfunction pathAliases(path: string): string[] {\n const keys = new Set<string>();\n const extension = extnamePosix(path);\n if (extension.length > 0) keys.add(path.slice(0, path.length - extension.length).toLowerCase());\n // `docs/adr/0007/README.md` is also addressed as `docs/adr/0007`.\n const base = basenamePosix(path);\n if (/^(readme|index)\\./i.test(base)) {\n const parent = path.slice(0, path.length - base.length - 1);\n if (parent.length > 0) keys.add(parent.toLowerCase());\n }\n // A file never addresses itself inexactly. Leaving this in would let a\n // contrived name - `docs/a.md.md`, whose stem is another file's whole path -\n // make an exact link look ambiguous.\n keys.delete(path.toLowerCase());\n return [...keys];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Resolution */\n/* -------------------------------------------------------------------------- */\n\nfunction resolveOne(\n candidate: ReferenceCandidate,\n entry: ExtractedDocument,\n index: Index,\n nodes: ReadonlyMap<string, SpecNode>,\n options: ResolveOptions,\n): Edge | DanglingRef | null {\n const { target: bare, anchor } = splitAnchor(candidate.target);\n\n // A pure `#anchor` points inside the citing document.\n if (bare.length === 0 && anchor !== null) {\n return bindAnchor(candidate, entry.document.id, anchor, index, nodes, options);\n }\n\n if (isExternal(bare)) return null;\n // A link to source code, an image or a config file is not a specification\n // reference. Validating it would report an error on the most ordinary thing a\n // design document does.\n if (!isDocumentTarget(bare)) return null;\n\n const found = lookup(bare, entry, index);\n\n if (found.ids.length === 0) {\n if (candidate.opportunistic && !worthReporting(bare, index)) return null;\n if (isFiltered(candidate.target, bare, options)) return null;\n // A trailing slash names a directory. Linking to one is ordinary - \"the\n // decisions live in [archive/](archive/)\" - and is not a citation of any\n // document. Resolution is still attempted first, so a directory-style\n // document written as `0007-sharding/` continues to resolve.\n if (bare.endsWith('/')) return null;\n const reason = pathMissingReason(bare, entry, options);\n return dangle(candidate, reason, found.near);\n }\n\n if (found.ids.length > 1) {\n if (candidate.opportunistic) return null;\n if (isFiltered(candidate.target, bare, options)) return null;\n return dangle(candidate, 'ambiguous', found.ids);\n }\n\n const documentId = found.ids[0] as string;\n\n // A document naming itself in prose - almost always its own H1, `# ADR-0007:\n // Sharding` - is not a citation. Deliberate self-links are kept, because a\n // link that points at its own document is a real mistake worth reporting.\n if (candidate.opportunistic && documentId === entry.document.id) return null;\n\n if (anchor !== null) return bindAnchor(candidate, documentId, anchor, index, nodes, options);\n\n return makeEdge(candidate, documentId);\n}\n\ninterface Lookup {\n readonly ids: readonly string[];\n /** Near-misses worth suggesting in the report. */\n readonly near: readonly string[];\n}\n\nfunction lookup(target: string, entry: ExtractedDocument, index: Index): Lookup {\n if (looksLikePath(target)) {\n const resolved = resolveFrom(entry.document.path, target).toLowerCase();\n // Naming the file exactly is never ambiguous, whatever else is spelled the\n // same way.\n const byPath = index.byPath.get(resolved);\n if (byPath) return { ids: [byPath], near: [] };\n // An inexact spelling can belong to more than one file: `docs/A` is both\n // `docs/A.md` with its extension dropped and `docs/A/README.md` standing\n // for its directory. Handing back every claimant makes that an\n // ambiguous-reference the author can settle, rather than a silent pick of\n // whichever happened to be indexed last.\n const spelled = index.byPathAlias.get(resolved);\n if (spelled && spelled.size > 0) return { ids: [...spelled], near: [] };\n // A path may still be spelled as an identifier in a nested folder layout.\n const stem = basenamePosix(resolved);\n const byStem = index.byAlias.get(normaliseRef(stem.replace(/\\.[^.]+$/, '')));\n if (byStem && byStem.size > 0) return { ids: [...byStem], near: [] };\n return { ids: [], near: [] };\n }\n\n const key = normaliseRef(target);\n if (key.length === 0) return { ids: [], near: [] };\n\n const byId = index.byId.get(key);\n if (byId) return { ids: [byId], near: [] };\n\n const byAlias = index.byAlias.get(key);\n if (byAlias && byAlias.size > 0) return { ids: [...byAlias], near: [] };\n\n const prefixed = parsePrefixedRef(target);\n if (prefixed) {\n const byFamily = index.byFamilyNumber.get(`${prefixed.family}:${prefixed.number}`);\n if (byFamily && byFamily.size > 0) return { ids: [...byFamily], near: [] };\n return { ids: [], near: [] };\n }\n\n // A bare number resolves only inside the citing document's own family. A\n // repository with both `adr/0007` and `rfc/0007` is ordinary, and guessing\n // between them would be worse than reporting nothing.\n const number = parseBareRef(target);\n if (number !== null) {\n const family = index.familyOf.get(entry.document.id) ?? null;\n if (family === null) return { ids: [], near: [] };\n const byFamily = index.byFamilyNumber.get(`${family}:${number}`);\n if (byFamily && byFamily.size > 0) return { ids: [...byFamily], near: [] };\n }\n\n return { ids: [], near: [] };\n}\n\n/** Whether a repository has declared this reference none of its business. */\nfunction isFiltered(target: string, bare: string, options: ResolveOptions): boolean {\n if (options.isIgnoredReference?.(target)) return true;\n const prefixed = parsePrefixedRef(bare);\n return prefixed !== null && options.isIgnoredFamily?.(prefixed.family) === true;\n}\n\n/**\n * Whether an unresolved bare identifier deserves a report.\n *\n * Only when the corpus already contains that family. `ADR-0099` in a repository\n * of ADRs is a real dangling citation; `T-1000` in the same repository is a\n * sentence about a robot.\n */\nfunction worthReporting(target: string, index: Index): boolean {\n const prefixed = parsePrefixedRef(target);\n return prefixed !== null && index.families.has(prefixed.family);\n}\n\nfunction pathMissingReason(\n target: string,\n entry: ExtractedDocument,\n options: ResolveOptions,\n): DanglingRef['reason'] {\n if (!looksLikePath(target) || !options.fileExists) return 'unknown-target';\n const resolved = resolveFrom(entry.document.path, target);\n return options.fileExists(resolved) ? 'not-a-spec' : 'unknown-target';\n}\n\n/**\n * Binds a `#anchor` to an item or a section.\n *\n * Anchors are how a delegation names a specific obligation rather than a whole\n * document, so resolving them precisely is what lets a ghost-handover finding\n * say *which* question was handed into the archive.\n */\nfunction bindAnchor(\n candidate: ReferenceCandidate,\n documentId: string,\n anchor: string,\n index: Index,\n nodes: ReadonlyMap<string, SpecNode>,\n options: ResolveOptions,\n): Edge | DanglingRef | null {\n const itemId = `${documentId}#${anchor}`;\n if (index.itemIds.has(itemId)) return makeEdge(candidate, itemId);\n\n const anchors = index.anchors.get(documentId);\n if (anchors?.has(anchor.toLowerCase())) return makeEdge(candidate, documentId);\n\n // Some renderers slugify differently; try the loosest reasonable match before\n // calling it broken.\n const loose = anchor.toLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, '-').replace(/^-|-$/g, '');\n if (anchors?.has(loose)) return makeEdge(candidate, documentId);\n\n if (isFiltered(candidate.target, splitAnchor(candidate.target).target, options)) return null;\n\n const candidates = [...(nodes.keys() as Iterable<string>)].filter(\n (id) => id.startsWith(`${documentId}#`) && id.toLowerCase().includes(loose),\n );\n return {\n kind: candidate.kind,\n from: candidate.from,\n target: candidate.target,\n reason: 'unknown-anchor',\n origin: candidate.origin,\n declaredAt: candidate.declaredAt,\n raw: candidate.raw,\n candidates,\n };\n}\n\nfunction makeEdge(candidate: ReferenceCandidate, resolved: string): Edge {\n const from = candidate.inverted ? resolved : candidate.from;\n const to = candidate.inverted ? candidate.from : resolved;\n return {\n kind: candidate.kind,\n from,\n to,\n origin: candidate.origin,\n declaredAt: candidate.declaredAt,\n raw: candidate.raw,\n declaredIn: [candidate.declaredAt.file],\n reflexive: documentOf(from) === documentOf(to),\n };\n}\n\nfunction dangle(\n candidate: ReferenceCandidate,\n reason: DanglingRef['reason'],\n candidates: readonly string[],\n): DanglingRef {\n return {\n kind: candidate.kind,\n from: candidate.from,\n target: candidate.target,\n reason,\n origin: candidate.origin,\n declaredAt: candidate.declaredAt,\n raw: candidate.raw,\n candidates,\n };\n}\n\n/** The document part of a node id: `ADR-7#q.1` -> `ADR-7`. */\nexport function documentOf(nodeId: string): string {\n const hash = nodeId.indexOf('#');\n return hash === -1 ? nodeId : nodeId.slice(0, hash);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descent-vtt/spec-graph",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Turn Markdown specs into a verifiable relational graph. Catches ghost handovers, stale premises and broken references in CI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adr",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"./package.json": "./package.json"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
|
+
"CHANGELOG.md",
|
|
36
37
|
"LICENSE",
|
|
37
38
|
"README.md",
|
|
38
39
|
"bin",
|