@descent-vtt/spec-graph 0.2.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 +21 -0
- package/README.md +627 -0
- package/bin/spec-graph.js +24 -0
- package/dist/baseline.d.ts +93 -0
- package/dist/baseline.js +187 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cli.d.ts +66 -0
- package/dist/cli.js +565 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +66 -0
- package/dist/config.js +183 -0
- package/dist/config.js.map +1 -0
- package/dist/directives.d.ts +56 -0
- package/dist/directives.js +125 -0
- package/dist/directives.js.map +1 -0
- package/dist/extract.d.ts +116 -0
- package/dist/extract.js +1043 -0
- package/dist/extract.js.map +1 -0
- package/dist/glob.d.ts +73 -0
- package/dist/glob.js +284 -0
- package/dist/glob.js.map +1 -0
- package/dist/graph.d.ts +55 -0
- package/dist/graph.js +201 -0
- package/dist/graph.js.map +1 -0
- package/dist/identity.d.ts +87 -0
- package/dist/identity.js +294 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +57 -0
- package/dist/lifecycle.js +258 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/markdown.d.ts +152 -0
- package/dist/markdown.js +820 -0
- package/dist/markdown.js.map +1 -0
- package/dist/paths.d.ts +22 -0
- package/dist/paths.js +58 -0
- package/dist/paths.js.map +1 -0
- package/dist/report.d.ts +89 -0
- package/dist/report.js +326 -0
- package/dist/report.js.map +1 -0
- package/dist/resolve.d.ts +61 -0
- package/dist/resolve.js +340 -0
- package/dist/resolve.js.map +1 -0
- package/dist/rules.d.ts +61 -0
- package/dist/rules.js +557 -0
- package/dist/rules.js.map +1 -0
- package/dist/runner.d.ts +108 -0
- package/dist/runner.js +186 -0
- package/dist/runner.js.map +1 -0
- package/dist/sections.d.ts +94 -0
- package/dist/sections.js +372 -0
- package/dist/sections.js.map +1 -0
- package/dist/select.d.ts +96 -0
- package/dist/select.js +413 -0
- package/dist/select.js.map +1 -0
- package/dist/source.d.ts +43 -0
- package/dist/source.js +109 -0
- package/dist/source.js.map +1 -0
- package/dist/state.d.ts +45 -0
- package/dist/state.js +330 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.js +52 -0
- package/dist/types.js.map +1 -0
- package/dist/yaml.d.ts +37 -0
- package/dist/yaml.js +240 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +71 -0
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item state resolution.
|
|
3
|
+
*
|
|
4
|
+
* The premise of this module is that `- [ ]` and `- [x]` are the *least*
|
|
5
|
+
* reliable statement a document makes about an obligation. Real specifications
|
|
6
|
+
* record outcomes in prose, on a continuation line, days after the checkbox was
|
|
7
|
+
* written and nobody went back to tick it:
|
|
8
|
+
*
|
|
9
|
+
* ```md
|
|
10
|
+
* - [ ] Should the write path shard before the migration?
|
|
11
|
+
* **Resolved (2026-03):** no - one node holds three years of growth.
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A grep for `- [ ]` calls that an open question and re-opens a settled debate.
|
|
15
|
+
* A grep for `- [x]` misses it entirely. spec-graph reads both signals, lets the
|
|
16
|
+
* more specific one win, and *reports the disagreement* rather than hiding it,
|
|
17
|
+
* because a checkbox that contradicts its own body is itself a defect worth
|
|
18
|
+
* knowing about.
|
|
19
|
+
*/
|
|
20
|
+
import type { Directive } from './directives.js';
|
|
21
|
+
import type { ListItem } from './markdown.js';
|
|
22
|
+
import { type LineIndex } from './source.js';
|
|
23
|
+
import { type Disposition, type Openness, type StateSignal } from './types.js';
|
|
24
|
+
export interface StateInput {
|
|
25
|
+
readonly file: string;
|
|
26
|
+
readonly index: LineIndex;
|
|
27
|
+
readonly item: ListItem;
|
|
28
|
+
/** Heading path the item sits under. */
|
|
29
|
+
readonly section: readonly string[];
|
|
30
|
+
/** A `@spec-item` directive attached to the item, when there is one. */
|
|
31
|
+
readonly directive: Directive | null;
|
|
32
|
+
/** True when the item sits under a heading that declares obligations. */
|
|
33
|
+
readonly inObligationSection: boolean;
|
|
34
|
+
}
|
|
35
|
+
export interface ResolvedState {
|
|
36
|
+
readonly disposition: Disposition;
|
|
37
|
+
readonly openness: Openness;
|
|
38
|
+
readonly evidence: StateSignal;
|
|
39
|
+
/** Signals that disagree with `evidence` about how open the item is. */
|
|
40
|
+
readonly conflicts: readonly StateSignal[];
|
|
41
|
+
}
|
|
42
|
+
/** Collects every state signal on an item and decides between them. */
|
|
43
|
+
export declare function resolveItemState(input: StateInput): ResolvedState;
|
|
44
|
+
/** Exposed for the query language, which lets users filter on these names. */
|
|
45
|
+
export declare const KNOWN_STATE_WORDS: readonly string[];
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item state resolution.
|
|
3
|
+
*
|
|
4
|
+
* The premise of this module is that `- [ ]` and `- [x]` are the *least*
|
|
5
|
+
* reliable statement a document makes about an obligation. Real specifications
|
|
6
|
+
* record outcomes in prose, on a continuation line, days after the checkbox was
|
|
7
|
+
* written and nobody went back to tick it:
|
|
8
|
+
*
|
|
9
|
+
* ```md
|
|
10
|
+
* - [ ] Should the write path shard before the migration?
|
|
11
|
+
* **Resolved (2026-03):** no - one node holds three years of growth.
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A grep for `- [ ]` calls that an open question and re-opens a settled debate.
|
|
15
|
+
* A grep for `- [x]` misses it entirely. spec-graph reads both signals, lets the
|
|
16
|
+
* more specific one win, and *reports the disagreement* rather than hiding it,
|
|
17
|
+
* because a checkbox that contradicts its own body is itself a defect worth
|
|
18
|
+
* knowing about.
|
|
19
|
+
*/
|
|
20
|
+
import { attr } from './directives.js';
|
|
21
|
+
import { refOf } from './source.js';
|
|
22
|
+
import { OPENNESS_OF } from './types.js';
|
|
23
|
+
/** Which signal wins when two disagree. Higher is more specific. */
|
|
24
|
+
const SIGNAL_PRIORITY = {
|
|
25
|
+
directive: 5,
|
|
26
|
+
marker: 4,
|
|
27
|
+
strikethrough: 3,
|
|
28
|
+
checkbox: 2,
|
|
29
|
+
section: 1,
|
|
30
|
+
default: 0,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Checkbox characters.
|
|
34
|
+
*
|
|
35
|
+
* `[ ]` and `[x]` are universal; the rest come from conventions that several
|
|
36
|
+
* ecosystems share (`[~]` partial, `[-]` dropped, `[?]` question, `[/]` in
|
|
37
|
+
* progress). Reading them costs nothing and refusing to would throw away the
|
|
38
|
+
* one place a document states its nuance unambiguously.
|
|
39
|
+
*/
|
|
40
|
+
const CHECKBOX_STATE = {
|
|
41
|
+
' ': 'unresolved',
|
|
42
|
+
x: 'satisfied',
|
|
43
|
+
X: 'satisfied',
|
|
44
|
+
'+': 'satisfied',
|
|
45
|
+
'~': 'narrowed',
|
|
46
|
+
'/': 'narrowed',
|
|
47
|
+
'-': 'rejected',
|
|
48
|
+
'?': 'unresolved',
|
|
49
|
+
'!': 'unresolved',
|
|
50
|
+
'*': 'unresolved',
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Prose markers, by the state they announce.
|
|
54
|
+
*
|
|
55
|
+
* Matched only in the qualified forms real documents use - `**Resolved:**`,
|
|
56
|
+
* `RESOLVED -`, `**Moot**` - never as bare words, so ordinary prose such as
|
|
57
|
+
* "we resolved to keep the queue" does not close an item.
|
|
58
|
+
*/
|
|
59
|
+
const MARKERS = {
|
|
60
|
+
satisfied: [
|
|
61
|
+
'resolved',
|
|
62
|
+
'answered',
|
|
63
|
+
'decided',
|
|
64
|
+
'settled',
|
|
65
|
+
'done',
|
|
66
|
+
'complete',
|
|
67
|
+
'completed',
|
|
68
|
+
'closed',
|
|
69
|
+
'fixed',
|
|
70
|
+
'shipped',
|
|
71
|
+
'addressed',
|
|
72
|
+
'implemented',
|
|
73
|
+
'solved',
|
|
74
|
+
'confirmed',
|
|
75
|
+
],
|
|
76
|
+
narrowed: [
|
|
77
|
+
'partially resolved',
|
|
78
|
+
'partially answered',
|
|
79
|
+
'partially done',
|
|
80
|
+
'partly resolved',
|
|
81
|
+
'narrowed',
|
|
82
|
+
'narrowed to',
|
|
83
|
+
'scoped down',
|
|
84
|
+
'reduced scope',
|
|
85
|
+
'reduced to',
|
|
86
|
+
'partial',
|
|
87
|
+
'in progress',
|
|
88
|
+
'remaining',
|
|
89
|
+
'split',
|
|
90
|
+
],
|
|
91
|
+
delegated: [
|
|
92
|
+
'delegated',
|
|
93
|
+
'delegated to',
|
|
94
|
+
'moved to',
|
|
95
|
+
'tracked in',
|
|
96
|
+
'tracked by',
|
|
97
|
+
'handed off',
|
|
98
|
+
'handed off to',
|
|
99
|
+
'handed to',
|
|
100
|
+
'deferred to',
|
|
101
|
+
'follow up in',
|
|
102
|
+
'follow-up in',
|
|
103
|
+
'followup in',
|
|
104
|
+
'continued in',
|
|
105
|
+
'owned by',
|
|
106
|
+
],
|
|
107
|
+
'accepted-debt': [
|
|
108
|
+
'accepted debt',
|
|
109
|
+
'accepted as debt',
|
|
110
|
+
'accepted risk',
|
|
111
|
+
'known limitation',
|
|
112
|
+
'known issue',
|
|
113
|
+
'known gap',
|
|
114
|
+
'technical debt',
|
|
115
|
+
'tech debt',
|
|
116
|
+
'wontfix',
|
|
117
|
+
"won't fix",
|
|
118
|
+
'will not fix',
|
|
119
|
+
'by design',
|
|
120
|
+
'tolerated',
|
|
121
|
+
'living with it',
|
|
122
|
+
],
|
|
123
|
+
rejected: [
|
|
124
|
+
'rejected',
|
|
125
|
+
'declined',
|
|
126
|
+
'dropped',
|
|
127
|
+
'not doing',
|
|
128
|
+
'will not do',
|
|
129
|
+
'abandoned',
|
|
130
|
+
'cancelled',
|
|
131
|
+
'canceled',
|
|
132
|
+
'withdrawn',
|
|
133
|
+
],
|
|
134
|
+
obviated: [
|
|
135
|
+
'no longer applicable',
|
|
136
|
+
'no longer relevant',
|
|
137
|
+
'no longer needed',
|
|
138
|
+
'overtaken by events',
|
|
139
|
+
'premise invalid',
|
|
140
|
+
'obviated',
|
|
141
|
+
'obsolete',
|
|
142
|
+
'not applicable',
|
|
143
|
+
'moot',
|
|
144
|
+
'void',
|
|
145
|
+
'n/a',
|
|
146
|
+
'obe',
|
|
147
|
+
],
|
|
148
|
+
unresolved: ['unresolved', 'still open', 'open question', 'tbd', 'undecided', 'reopened'],
|
|
149
|
+
};
|
|
150
|
+
/** Words accepted in a `state=` attribute, including friendly synonyms. */
|
|
151
|
+
const STATE_ALIASES = {
|
|
152
|
+
open: 'unresolved',
|
|
153
|
+
unresolved: 'unresolved',
|
|
154
|
+
todo: 'unresolved',
|
|
155
|
+
narrowed: 'narrowed',
|
|
156
|
+
partial: 'narrowed',
|
|
157
|
+
delegated: 'delegated',
|
|
158
|
+
satisfied: 'satisfied',
|
|
159
|
+
resolved: 'satisfied',
|
|
160
|
+
done: 'satisfied',
|
|
161
|
+
closed: 'satisfied',
|
|
162
|
+
'accepted-debt': 'accepted-debt',
|
|
163
|
+
debt: 'accepted-debt',
|
|
164
|
+
wontfix: 'accepted-debt',
|
|
165
|
+
rejected: 'rejected',
|
|
166
|
+
declined: 'rejected',
|
|
167
|
+
obviated: 'obviated',
|
|
168
|
+
moot: 'obviated',
|
|
169
|
+
};
|
|
170
|
+
const MARKER_PATTERN = buildMarkerPattern();
|
|
171
|
+
const MARKER_LOOKUP = markerLookup();
|
|
172
|
+
/** Collects every state signal on an item and decides between them. */
|
|
173
|
+
export function resolveItemState(input) {
|
|
174
|
+
const { file, index, item } = input;
|
|
175
|
+
const signals = [];
|
|
176
|
+
const at = (start, end) => refOf(file, index, start, end);
|
|
177
|
+
if (input.directive) {
|
|
178
|
+
const declared = attr(input.directive, 'state');
|
|
179
|
+
if (declared) {
|
|
180
|
+
const mapped = STATE_ALIASES[declared.value.toLowerCase()];
|
|
181
|
+
if (mapped) {
|
|
182
|
+
signals.push({
|
|
183
|
+
source: 'directive',
|
|
184
|
+
disposition: mapped,
|
|
185
|
+
raw: declared.value,
|
|
186
|
+
at: at(declared.start, declared.end),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Markers are searched in the masked body: a fenced example nested under an
|
|
192
|
+
// item must never be able to close it.
|
|
193
|
+
for (const found of findMarkers(item.maskedBody)) {
|
|
194
|
+
signals.push({
|
|
195
|
+
source: 'marker',
|
|
196
|
+
disposition: found.disposition,
|
|
197
|
+
raw: found.raw,
|
|
198
|
+
at: at(item.textStart + found.start, item.textStart + found.end),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const struck = strikethroughOf(item.firstLine);
|
|
202
|
+
if (struck) {
|
|
203
|
+
signals.push({
|
|
204
|
+
source: 'strikethrough',
|
|
205
|
+
disposition: 'satisfied',
|
|
206
|
+
raw: struck,
|
|
207
|
+
at: at(item.textStart, item.textStart + item.firstLine.length),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (item.checkbox !== null && item.checkboxStart !== null) {
|
|
211
|
+
const mapped = CHECKBOX_STATE[item.checkbox];
|
|
212
|
+
if (mapped) {
|
|
213
|
+
signals.push({
|
|
214
|
+
source: 'checkbox',
|
|
215
|
+
disposition: mapped,
|
|
216
|
+
raw: `[${item.checkbox}]`,
|
|
217
|
+
at: at(item.checkboxStart, item.checkboxStart + 3),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (input.inObligationSection) {
|
|
222
|
+
signals.push({
|
|
223
|
+
source: 'section',
|
|
224
|
+
disposition: 'unresolved',
|
|
225
|
+
raw: input.section[input.section.length - 1] ?? '',
|
|
226
|
+
at: at(item.start, item.end),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (signals.length === 0) {
|
|
230
|
+
const fallback = {
|
|
231
|
+
source: 'default',
|
|
232
|
+
disposition: 'unresolved',
|
|
233
|
+
raw: '',
|
|
234
|
+
at: at(item.start, item.end),
|
|
235
|
+
};
|
|
236
|
+
return { disposition: 'unresolved', openness: 'open', evidence: fallback, conflicts: [] };
|
|
237
|
+
}
|
|
238
|
+
// The most specific signal wins; ties go to the one written last, which is
|
|
239
|
+
// the one a reader of the document sees as the final word.
|
|
240
|
+
let winner = signals[0];
|
|
241
|
+
for (const signal of signals) {
|
|
242
|
+
const better = SIGNAL_PRIORITY[signal.source] - SIGNAL_PRIORITY[winner.source];
|
|
243
|
+
if (better > 0 || (better === 0 && signal.at.span.start.offset > winner.at.span.start.offset)) {
|
|
244
|
+
winner = signal;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const openness = OPENNESS_OF[winner.disposition];
|
|
248
|
+
// Only disagreement about *openness* is a conflict. A body that says
|
|
249
|
+
// "Resolved" under a ticked checkbox agrees with it; one under an empty
|
|
250
|
+
// checkbox does not, and that is worth a human looking at.
|
|
251
|
+
const conflicts = signals.filter((signal) => signal !== winner && signal.source !== 'section' && OPENNESS_OF[signal.disposition] !== openness);
|
|
252
|
+
return { disposition: winner.disposition, openness, evidence: winner, conflicts };
|
|
253
|
+
}
|
|
254
|
+
function buildMarkerPattern() {
|
|
255
|
+
const phrases = Object.values(MARKERS)
|
|
256
|
+
.flat()
|
|
257
|
+
// Longest first so `partially resolved` is not eaten by `resolved`.
|
|
258
|
+
.sort((a, b) => b.length - a.length)
|
|
259
|
+
.map(escapeRegExp)
|
|
260
|
+
// Allow the writer to hyphenate or space multi-word markers either way.
|
|
261
|
+
.map((phrase) => phrase.replace(/\\?[ -]/g, '[ \\-]'));
|
|
262
|
+
return new RegExp(
|
|
263
|
+
// A marker only counts at the start of a statement: the start of the item,
|
|
264
|
+
// a new line, after a sentence ends, or after an emphasis opener.
|
|
265
|
+
String.raw `(?:^|\n|[.?!]\s+|[(\[])` +
|
|
266
|
+
// Skip block-quote and list markers, but only when the marker character is
|
|
267
|
+
// followed by whitespace. Without that guard the `*` of `**Resolved**`
|
|
268
|
+
// reads as a bullet and the emphasis wrapper is lost.
|
|
269
|
+
String.raw `(?:[ \t>•]|(?:[-*+]|\d{1,9}[.)])[ \t]+)*` +
|
|
270
|
+
// Optional emphasis wrapper, remembered so `**Moot**` qualifies without a colon.
|
|
271
|
+
String.raw `(\*{1,2}|_{1,2})?` +
|
|
272
|
+
String.raw `\s*(${phrases.join('|')})` +
|
|
273
|
+
String.raw `(\*{1,2}|_{1,2})?` +
|
|
274
|
+
// An optional dated parenthetical: `Resolved (2026-03):`.
|
|
275
|
+
String.raw `\s*(?:[(\[][^)\]\n]{0,60}[)\]])?` +
|
|
276
|
+
// Optional, and validated in code below: an emphasised or shouted marker
|
|
277
|
+
// stands on its own, a bare lower-case word never does.
|
|
278
|
+
String.raw `\s*([::]|[-–—]\s|\.|$|\n)?`, 'gim');
|
|
279
|
+
}
|
|
280
|
+
function findMarkers(body) {
|
|
281
|
+
const out = [];
|
|
282
|
+
MARKER_PATTERN.lastIndex = 0;
|
|
283
|
+
for (let m = MARKER_PATTERN.exec(body); m !== null; m = MARKER_PATTERN.exec(body)) {
|
|
284
|
+
const phrase = m[2];
|
|
285
|
+
const emphasisOpen = m[1];
|
|
286
|
+
const emphasisClose = m[3];
|
|
287
|
+
const qualifier = (m[4] ?? '').trim();
|
|
288
|
+
const emphasised = emphasisOpen !== undefined && emphasisClose !== undefined;
|
|
289
|
+
const punctuated = qualifier === ':' || qualifier === ':' || qualifier.startsWith('-') || qualifier === '.';
|
|
290
|
+
const shouted = phrase === phrase.toUpperCase() && /[A-Z]/.test(phrase);
|
|
291
|
+
// Without one of these three the match is ordinary prose. "We resolved to
|
|
292
|
+
// keep the queue" must not close an item.
|
|
293
|
+
if (!emphasised && !punctuated && !shouted)
|
|
294
|
+
continue;
|
|
295
|
+
const disposition = MARKER_LOOKUP.get(normalisePhrase(phrase));
|
|
296
|
+
if (!disposition)
|
|
297
|
+
continue;
|
|
298
|
+
const start = (m.index ?? 0) + m[0].indexOf(phrase);
|
|
299
|
+
out.push({ disposition, raw: phrase, start, end: start + phrase.length });
|
|
300
|
+
// Overlapping alternatives would double-count; resume after this match.
|
|
301
|
+
MARKER_PATTERN.lastIndex = start + phrase.length;
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
/** Built once: this used to be rebuilt for every item in the corpus. */
|
|
306
|
+
function markerLookup() {
|
|
307
|
+
const map = new Map();
|
|
308
|
+
for (const [disposition, phrases] of Object.entries(MARKERS)) {
|
|
309
|
+
for (const phrase of phrases)
|
|
310
|
+
map.set(normalisePhrase(phrase), disposition);
|
|
311
|
+
}
|
|
312
|
+
return map;
|
|
313
|
+
}
|
|
314
|
+
function normalisePhrase(phrase) {
|
|
315
|
+
return phrase.toLowerCase().replace(/[\s-]+/g, ' ').trim();
|
|
316
|
+
}
|
|
317
|
+
/** Returns the struck text when the whole line is wrapped in `~~`. */
|
|
318
|
+
function strikethroughOf(line) {
|
|
319
|
+
const trimmed = line.trim();
|
|
320
|
+
if (!trimmed.startsWith('~~') || !trimmed.endsWith('~~') || trimmed.length < 5)
|
|
321
|
+
return null;
|
|
322
|
+
const inner = trimmed.slice(2, -2);
|
|
323
|
+
return inner.includes('~~') ? null : trimmed;
|
|
324
|
+
}
|
|
325
|
+
function escapeRegExp(value) {
|
|
326
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
327
|
+
}
|
|
328
|
+
/** Exposed for the query language, which lets users filter on these names. */
|
|
329
|
+
export const KNOWN_STATE_WORDS = Object.keys(STATE_ALIASES);
|
|
330
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Item state resolution.\n *\n * The premise of this module is that `- [ ]` and `- [x]` are the *least*\n * reliable statement a document makes about an obligation. Real specifications\n * record outcomes in prose, on a continuation line, days after the checkbox was\n * written and nobody went back to tick it:\n *\n * ```md\n * - [ ] Should the write path shard before the migration?\n * **Resolved (2026-03):** no - one node holds three years of growth.\n * ```\n *\n * A grep for `- [ ]` calls that an open question and re-opens a settled debate.\n * A grep for `- [x]` misses it entirely. spec-graph reads both signals, lets the\n * more specific one win, and *reports the disagreement* rather than hiding it,\n * because a checkbox that contradicts its own body is itself a defect worth\n * knowing about.\n */\n\nimport type { Directive } from './directives.js';\nimport { attr } from './directives.js';\nimport type { ListItem } from './markdown.js';\nimport { refOf, type LineIndex } from './source.js';\nimport { OPENNESS_OF, type Disposition, type Openness, type SourceRef, type StateSignal } from './types.js';\n\n/** Which signal wins when two disagree. Higher is more specific. */\nconst SIGNAL_PRIORITY: Readonly<Record<StateSignal['source'], number>> = {\n directive: 5,\n marker: 4,\n strikethrough: 3,\n checkbox: 2,\n section: 1,\n default: 0,\n};\n\n/**\n * Checkbox characters.\n *\n * `[ ]` and `[x]` are universal; the rest come from conventions that several\n * ecosystems share (`[~]` partial, `[-]` dropped, `[?]` question, `[/]` in\n * progress). Reading them costs nothing and refusing to would throw away the\n * one place a document states its nuance unambiguously.\n */\nconst CHECKBOX_STATE: Readonly<Record<string, Disposition>> = {\n ' ': 'unresolved',\n x: 'satisfied',\n X: 'satisfied',\n '+': 'satisfied',\n '~': 'narrowed',\n '/': 'narrowed',\n '-': 'rejected',\n '?': 'unresolved',\n '!': 'unresolved',\n '*': 'unresolved',\n};\n\n/**\n * Prose markers, by the state they announce.\n *\n * Matched only in the qualified forms real documents use - `**Resolved:**`,\n * `RESOLVED -`, `**Moot**` - never as bare words, so ordinary prose such as\n * \"we resolved to keep the queue\" does not close an item.\n */\nconst MARKERS: Readonly<Record<Disposition, readonly string[]>> = {\n satisfied: [\n 'resolved',\n 'answered',\n 'decided',\n 'settled',\n 'done',\n 'complete',\n 'completed',\n 'closed',\n 'fixed',\n 'shipped',\n 'addressed',\n 'implemented',\n 'solved',\n 'confirmed',\n ],\n narrowed: [\n 'partially resolved',\n 'partially answered',\n 'partially done',\n 'partly resolved',\n 'narrowed',\n 'narrowed to',\n 'scoped down',\n 'reduced scope',\n 'reduced to',\n 'partial',\n 'in progress',\n 'remaining',\n 'split',\n ],\n delegated: [\n 'delegated',\n 'delegated to',\n 'moved to',\n 'tracked in',\n 'tracked by',\n 'handed off',\n 'handed off to',\n 'handed to',\n 'deferred to',\n 'follow up in',\n 'follow-up in',\n 'followup in',\n 'continued in',\n 'owned by',\n ],\n 'accepted-debt': [\n 'accepted debt',\n 'accepted as debt',\n 'accepted risk',\n 'known limitation',\n 'known issue',\n 'known gap',\n 'technical debt',\n 'tech debt',\n 'wontfix',\n \"won't fix\",\n 'will not fix',\n 'by design',\n 'tolerated',\n 'living with it',\n ],\n rejected: [\n 'rejected',\n 'declined',\n 'dropped',\n 'not doing',\n 'will not do',\n 'abandoned',\n 'cancelled',\n 'canceled',\n 'withdrawn',\n ],\n obviated: [\n 'no longer applicable',\n 'no longer relevant',\n 'no longer needed',\n 'overtaken by events',\n 'premise invalid',\n 'obviated',\n 'obsolete',\n 'not applicable',\n 'moot',\n 'void',\n 'n/a',\n 'obe',\n ],\n unresolved: ['unresolved', 'still open', 'open question', 'tbd', 'undecided', 'reopened'],\n};\n\n/** Words accepted in a `state=` attribute, including friendly synonyms. */\nconst STATE_ALIASES: Readonly<Record<string, Disposition>> = {\n open: 'unresolved',\n unresolved: 'unresolved',\n todo: 'unresolved',\n narrowed: 'narrowed',\n partial: 'narrowed',\n delegated: 'delegated',\n satisfied: 'satisfied',\n resolved: 'satisfied',\n done: 'satisfied',\n closed: 'satisfied',\n 'accepted-debt': 'accepted-debt',\n debt: 'accepted-debt',\n wontfix: 'accepted-debt',\n rejected: 'rejected',\n declined: 'rejected',\n obviated: 'obviated',\n moot: 'obviated',\n};\n\nconst MARKER_PATTERN = buildMarkerPattern();\nconst MARKER_LOOKUP = markerLookup();\n\nexport interface StateInput {\n readonly file: string;\n readonly index: LineIndex;\n readonly item: ListItem;\n /** Heading path the item sits under. */\n readonly section: readonly string[];\n /** A `@spec-item` directive attached to the item, when there is one. */\n readonly directive: Directive | null;\n /** True when the item sits under a heading that declares obligations. */\n readonly inObligationSection: boolean;\n}\n\nexport interface ResolvedState {\n readonly disposition: Disposition;\n readonly openness: Openness;\n readonly evidence: StateSignal;\n /** Signals that disagree with `evidence` about how open the item is. */\n readonly conflicts: readonly StateSignal[];\n}\n\n/** Collects every state signal on an item and decides between them. */\nexport function resolveItemState(input: StateInput): ResolvedState {\n const { file, index, item } = input;\n const signals: StateSignal[] = [];\n\n const at = (start: number, end: number): SourceRef => refOf(file, index, start, end);\n\n if (input.directive) {\n const declared = attr(input.directive, 'state');\n if (declared) {\n const mapped = STATE_ALIASES[declared.value.toLowerCase()];\n if (mapped) {\n signals.push({\n source: 'directive',\n disposition: mapped,\n raw: declared.value,\n at: at(declared.start, declared.end),\n });\n }\n }\n }\n\n // Markers are searched in the masked body: a fenced example nested under an\n // item must never be able to close it.\n for (const found of findMarkers(item.maskedBody)) {\n signals.push({\n source: 'marker',\n disposition: found.disposition,\n raw: found.raw,\n at: at(item.textStart + found.start, item.textStart + found.end),\n });\n }\n\n const struck = strikethroughOf(item.firstLine);\n if (struck) {\n signals.push({\n source: 'strikethrough',\n disposition: 'satisfied',\n raw: struck,\n at: at(item.textStart, item.textStart + item.firstLine.length),\n });\n }\n\n if (item.checkbox !== null && item.checkboxStart !== null) {\n const mapped = CHECKBOX_STATE[item.checkbox];\n if (mapped) {\n signals.push({\n source: 'checkbox',\n disposition: mapped,\n raw: `[${item.checkbox}]`,\n at: at(item.checkboxStart, item.checkboxStart + 3),\n });\n }\n }\n\n if (input.inObligationSection) {\n signals.push({\n source: 'section',\n disposition: 'unresolved',\n raw: input.section[input.section.length - 1] ?? '',\n at: at(item.start, item.end),\n });\n }\n\n if (signals.length === 0) {\n const fallback: StateSignal = {\n source: 'default',\n disposition: 'unresolved',\n raw: '',\n at: at(item.start, item.end),\n };\n return { disposition: 'unresolved', openness: 'open', evidence: fallback, conflicts: [] };\n }\n\n // The most specific signal wins; ties go to the one written last, which is\n // the one a reader of the document sees as the final word.\n let winner = signals[0] as StateSignal;\n for (const signal of signals) {\n const better = SIGNAL_PRIORITY[signal.source] - SIGNAL_PRIORITY[winner.source];\n if (better > 0 || (better === 0 && signal.at.span.start.offset > winner.at.span.start.offset)) {\n winner = signal;\n }\n }\n\n const openness = OPENNESS_OF[winner.disposition];\n // Only disagreement about *openness* is a conflict. A body that says\n // \"Resolved\" under a ticked checkbox agrees with it; one under an empty\n // checkbox does not, and that is worth a human looking at.\n const conflicts = signals.filter(\n (signal) => signal !== winner && signal.source !== 'section' && OPENNESS_OF[signal.disposition] !== openness,\n );\n\n return { disposition: winner.disposition, openness, evidence: winner, conflicts };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Marker matching */\n/* -------------------------------------------------------------------------- */\n\ninterface MarkerHit {\n readonly disposition: Disposition;\n readonly raw: string;\n readonly start: number;\n readonly end: number;\n}\n\nfunction buildMarkerPattern(): RegExp {\n const phrases = Object.values(MARKERS)\n .flat()\n // Longest first so `partially resolved` is not eaten by `resolved`.\n .sort((a, b) => b.length - a.length)\n .map(escapeRegExp)\n // Allow the writer to hyphenate or space multi-word markers either way.\n .map((phrase) => phrase.replace(/\\\\?[ -]/g, '[ \\\\-]'));\n\n return new RegExp(\n // A marker only counts at the start of a statement: the start of the item,\n // a new line, after a sentence ends, or after an emphasis opener.\n String.raw`(?:^|\\n|[.?!]\\s+|[(\\[])` +\n // Skip block-quote and list markers, but only when the marker character is\n // followed by whitespace. Without that guard the `*` of `**Resolved**`\n // reads as a bullet and the emphasis wrapper is lost.\n String.raw`(?:[ \\t>•]|(?:[-*+]|\\d{1,9}[.)])[ \\t]+)*` +\n // Optional emphasis wrapper, remembered so `**Moot**` qualifies without a colon.\n String.raw`(\\*{1,2}|_{1,2})?` +\n String.raw`\\s*(${phrases.join('|')})` +\n String.raw`(\\*{1,2}|_{1,2})?` +\n // An optional dated parenthetical: `Resolved (2026-03):`.\n String.raw`\\s*(?:[(\\[][^)\\]\\n]{0,60}[)\\]])?` +\n // Optional, and validated in code below: an emphasised or shouted marker\n // stands on its own, a bare lower-case word never does.\n String.raw`\\s*([::]|[-–—]\\s|\\.|$|\\n)?`,\n 'gim',\n );\n}\n\nfunction findMarkers(body: string): MarkerHit[] {\n const out: MarkerHit[] = [];\n\n MARKER_PATTERN.lastIndex = 0;\n for (let m = MARKER_PATTERN.exec(body); m !== null; m = MARKER_PATTERN.exec(body)) {\n const phrase = m[2] as string;\n const emphasisOpen = m[1];\n const emphasisClose = m[3];\n const qualifier = (m[4] ?? '').trim();\n\n const emphasised = emphasisOpen !== undefined && emphasisClose !== undefined;\n const punctuated = qualifier === ':' || qualifier === ':' || qualifier.startsWith('-') || qualifier === '.';\n const shouted = phrase === phrase.toUpperCase() && /[A-Z]/.test(phrase);\n\n // Without one of these three the match is ordinary prose. \"We resolved to\n // keep the queue\" must not close an item.\n if (!emphasised && !punctuated && !shouted) continue;\n\n const disposition = MARKER_LOOKUP.get(normalisePhrase(phrase));\n if (!disposition) continue;\n\n const start = (m.index ?? 0) + (m[0] as string).indexOf(phrase);\n out.push({ disposition, raw: phrase, start, end: start + phrase.length });\n\n // Overlapping alternatives would double-count; resume after this match.\n MARKER_PATTERN.lastIndex = start + phrase.length;\n }\n\n return out;\n}\n\n/** Built once: this used to be rebuilt for every item in the corpus. */\nfunction markerLookup(): Map<string, Disposition> {\n const map = new Map<string, Disposition>();\n for (const [disposition, phrases] of Object.entries(MARKERS) as [Disposition, readonly string[]][]) {\n for (const phrase of phrases) map.set(normalisePhrase(phrase), disposition);\n }\n return map;\n}\n\nfunction normalisePhrase(phrase: string): string {\n return phrase.toLowerCase().replace(/[\\s-]+/g, ' ').trim();\n}\n\n/** Returns the struck text when the whole line is wrapped in `~~`. */\nfunction strikethroughOf(line: string): string | null {\n const trimmed = line.trim();\n if (!trimmed.startsWith('~~') || !trimmed.endsWith('~~') || trimmed.length < 5) return null;\n const inner = trimmed.slice(2, -2);\n return inner.includes('~~') ? null : trimmed;\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n/** Exposed for the query language, which lets users filter on these names. */\nexport const KNOWN_STATE_WORDS: readonly string[] = Object.keys(STATE_ALIASES);\n"]}
|