@descent-vtt/spec-graph 0.10.0 → 0.11.1

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/dist/state.js CHANGED
@@ -54,9 +54,7 @@ const CHECKBOX_STATE = {
54
54
  *
55
55
  * Matched only in the qualified forms real documents use - `**Resolved:**`,
56
56
  * `RESOLVED -`, `**Moot**` - never as bare words, so ordinary prose such as
57
- * "we resolved to keep the queue" does not close an item. A Chinese marker,
58
- * Traditional or Simplified, is the English one it translates, and has no
59
- * case to shout in: `已決定:` or `**已決定**`, never `我們已決定採用`.
57
+ * "we resolved to keep the queue" does not close an item.
60
58
  */
61
59
  const MARKERS = {
62
60
  satisfied: [
@@ -74,11 +72,6 @@ const MARKERS = {
74
72
  'implemented',
75
73
  'solved',
76
74
  'confirmed',
77
- '已解決',
78
- '已解决',
79
- '已決定',
80
- '已决定',
81
- '已定案',
82
75
  ],
83
76
  narrowed: [
84
77
  'partially resolved',
@@ -94,8 +87,6 @@ const MARKERS = {
94
87
  'in progress',
95
88
  'remaining',
96
89
  'split',
97
- '部分解決',
98
- '部分解决',
99
90
  ],
100
91
  delegated: [
101
92
  'delegated',
@@ -112,7 +103,6 @@ const MARKERS = {
112
103
  'followup in',
113
104
  'continued in',
114
105
  'owned by',
115
- '已移交',
116
106
  ],
117
107
  'accepted-debt': [
118
108
  'accepted debt',
@@ -129,9 +119,6 @@ const MARKERS = {
129
119
  'by design',
130
120
  'tolerated',
131
121
  'living with it',
132
- '技術債',
133
- '技术债',
134
- '已知限制',
135
122
  ],
136
123
  rejected: [
137
124
  'rejected',
@@ -286,11 +273,8 @@ function buildMarkerPattern() {
286
273
  .map((phrase) => phrase.replace(/\\?[ -]/g, '[ _\\-]').replace(/'/g, "['’]"));
287
274
  return new RegExp(
288
275
  // A marker only counts at the start of a statement: the start of the item,
289
- // a new line, after a sentence ends, or after an emphasis opener. A
290
- // Chinese sentence ends at `。?!;` with a space after it or none -
291
- // `是否分片?已決定:不分片。` - and the space before the phrase below
292
- // takes any that follows.
293
- String.raw `(?:^|\n|[.?!]\s+|[。?!;]|[(\[])` +
276
+ // a new line, after a sentence ends, or after an emphasis opener.
277
+ String.raw `(?:^|\n|[.?!]\s+|[(\[])` +
294
278
  // Skip block-quote and list markers, but only when the marker character is
295
279
  // followed by whitespace. Without that guard the `*` of `**Resolved**`
296
280
  // reads as a bullet and the emphasis wrapper is lost.
@@ -308,7 +292,7 @@ function buildMarkerPattern() {
308
292
  function findMarkers(body) {
309
293
  const out = [];
310
294
  MARKER_PATTERN.lastIndex = 0;
311
- for (let m = MARKER_PATTERN.exec(body); m !== null; m = MARKER_PATTERN.exec(body)) {
295
+ for (let m = MARKER_PATTERN.exec(body); m !== null; m = execPast(MARKER_PATTERN, body, m.index)) {
312
296
  const phrase = m[2];
313
297
  const emphasisOpen = m[1];
314
298
  const emphasisClose = m[3];
@@ -336,6 +320,22 @@ function findMarkers(body) {
336
320
  }
337
321
  return out;
338
322
  }
323
+ /**
324
+ * `exec` from past where the last match began.
325
+ *
326
+ * The other scans over a /g pattern use `matchAll`, which steps past an empty
327
+ * match. This one cannot: it resumes inside its match, on the stop a marker's
328
+ * qualifier took, so that the stop can start the next statement. No marker is
329
+ * empty, so no match is, and the resume is always past where its match began.
330
+ * A marker emptied by mistake would match empty at the start of every line,
331
+ * and `exec` would find it there for ever; this steps past it, and the marker
332
+ * tests fail instead.
333
+ */
334
+ export function execPast(pattern, text, index) {
335
+ if (pattern.lastIndex <= index)
336
+ pattern.lastIndex = index + 1;
337
+ return pattern.exec(text);
338
+ }
339
339
  /** Built once: this used to be rebuilt for every item in the corpus. */
340
340
  function markerLookup() {
341
341
  const map = new Map();
package/dist/state.js.map CHANGED
@@ -1 +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. A Chinese marker,\n * Traditional or Simplified, is the English one it translates, and has no\n * case to shout in: `已決定:` or `**已決定**`, never `我們已決定採用`.\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 '已解决',\n '已決定',\n '已决定',\n '已定案',\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 '部分解决',\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 ],\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 '技术债',\n '已知限制',\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 // The scanner reads a checkbox only from the ten characters above, and gives\n // it a start exactly when it gives it a character: these guards narrow types\n // and decide nothing, so their mutants are equivalent.\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 // An item in an obligation section is under a heading, so the fallback\n // narrows the type and is never read.\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 //\n // Signals are pushed most specific first and markers in document order, so\n // the winner only changes on a tie, where array order and document order\n // agree. Mutants of the comparisons are equivalent for that reason; they are\n // written out so that neither fact about the pushes above is load-bearing.\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. The winner agrees\n // with itself, so it is never among them.\n const conflicts = signals.filter(\n (signal) => 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 space, hyphenate or underscore a multi-word marker\n // whichever way, and to type its apostrophe straight or curly.\n .map((phrase) => phrase.replace(/\\\\?[ -]/g, '[ _\\\\-]').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. A\n // Chinese sentence ends at `。?!;` with a space after it or none -\n // `是否分片?已決定:不分片。` - and the space before the phrase below\n // takes any that follows.\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 // A dash arrives with the space after it. An absent qualifier is none of the\n // four, so what stands in for it is equivalent as long as it is not one.\n // Only the dash alternative puts a dash in the qualifier at all, so the\n // anchor on the dash test decides nothing either.\n const qualifier = m[4] ?? '';\n\n const emphasised = emphasisOpen !== undefined && emphasisClose !== undefined;\n const punctuated = qualifier === ':' || qualifier === ':' || /^[-–—]/.test(qualifier) || 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 // Every phrase the pattern matches is a table phrase respelled, so this\n // narrows the type and never skips: its mutant is equivalent.\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\n/**\n * The key a phrase is looked up by, from the table and from a match alike.\n *\n * Its case and separator mutants are equivalent, because it folds both sides\n * of one lookup: folded to upper case, or with the separator dropped, the same\n * phrases share a key - no two of the table's dispositions collide without\n * their spaces - and neither a table phrase nor a match ever holds two\n * separators in a row. The apostrophe is folded for the match alone: the table\n * spells it straight.\n */\nfunction normalisePhrase(phrase: string): string {\n return phrase.toLowerCase().replace(/’/g, \"'\").replace(/[\\s_-]+/g, ' ');\n}\n\n/** Returns the struck text when the whole line, which arrives trimmed, is wrapped in `~~`. */\nfunction strikethroughOf(line: string): string | null {\n if (!line.startsWith('~~') || !line.endsWith('~~')) return null;\n const inner = line.slice(2, -2);\n // Something has to be struck: `~~~~~` is the fence of a code block nested\n // under the item, and strikes nothing.\n return /[^~]/.test(inner) && !inner.includes('~~') ? line : null;\n}\n\n/**\n * No phrase in the table holds a metacharacter today, so blanking the\n * replacement is an equivalent mutant; this keeps the dots of a future `n.a.`\n * dots, rather than any character.\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"]}
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 // The scanner reads a checkbox only from the ten characters above, and gives\n // it a start exactly when it gives it a character: these guards narrow types\n // and decide nothing, so their mutants are equivalent.\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 // An item in an obligation section is under a heading, so the fallback\n // narrows the type and is never read.\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 //\n // Signals are pushed most specific first and markers in document order, so\n // the winner only changes on a tie, where array order and document order\n // agree. Mutants of the comparisons are equivalent for that reason; they are\n // written out so that neither fact about the pushes above is load-bearing.\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. The winner agrees\n // with itself, so it is never among them.\n const conflicts = signals.filter(\n (signal) => 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 space, hyphenate or underscore a multi-word marker\n // whichever way, and to type its apostrophe straight or curly.\n .map((phrase) => phrase.replace(/\\\\?[ -]/g, '[ _\\\\-]').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 = execPast(MARKER_PATTERN, body, m.index)) {\n const phrase = m[2] as string;\n const emphasisOpen = m[1];\n const emphasisClose = m[3];\n // A dash arrives with the space after it. An absent qualifier is none of the\n // four, so what stands in for it is equivalent as long as it is not one.\n // Only the dash alternative puts a dash in the qualifier at all, so the\n // anchor on the dash test decides nothing either.\n const qualifier = m[4] ?? '';\n\n const emphasised = emphasisOpen !== undefined && emphasisClose !== undefined;\n const punctuated = qualifier === ':' || qualifier === ':' || /^[-–—]/.test(qualifier) || 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 // Every phrase the pattern matches is a table phrase respelled, so this\n // narrows the type and never skips: its mutant is equivalent.\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/**\n * `exec` from past where the last match began.\n *\n * The other scans over a /g pattern use `matchAll`, which steps past an empty\n * match. This one cannot: it resumes inside its match, on the stop a marker's\n * qualifier took, so that the stop can start the next statement. No marker is\n * empty, so no match is, and the resume is always past where its match began.\n * A marker emptied by mistake would match empty at the start of every line,\n * and `exec` would find it there for ever; this steps past it, and the marker\n * tests fail instead.\n */\nexport function execPast(pattern: RegExp, text: string, index: number): RegExpExecArray | null {\n if (pattern.lastIndex <= index) pattern.lastIndex = index + 1;\n return pattern.exec(text);\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\n/**\n * The key a phrase is looked up by, from the table and from a match alike.\n *\n * Its case and separator mutants are equivalent, because it folds both sides\n * of one lookup: folded to upper case, or with the separator dropped, the same\n * phrases share a key - no two of the table's dispositions collide without\n * their spaces - and neither a table phrase nor a match ever holds two\n * separators in a row. The apostrophe is folded for the match alone: the table\n * spells it straight.\n */\nfunction normalisePhrase(phrase: string): string {\n return phrase.toLowerCase().replace(/’/g, \"'\").replace(/[\\s_-]+/g, ' ');\n}\n\n/** Returns the struck text when the whole line, which arrives trimmed, is wrapped in `~~`. */\nfunction strikethroughOf(line: string): string | null {\n if (!line.startsWith('~~') || !line.endsWith('~~')) return null;\n const inner = line.slice(2, -2);\n // Something has to be struck: `~~~~~` is the fence of a code block nested\n // under the item, and strikes nothing.\n return /[^~]/.test(inner) && !inner.includes('~~') ? line : null;\n}\n\n/**\n * No phrase in the table holds a metacharacter today, so blanking the\n * replacement is an equivalent mutant; this keeps the dots of a future `n.a.`\n * dots, rather than any character.\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"]}
package/dist/yaml.js CHANGED
@@ -23,8 +23,8 @@ import { readFrontMatter } from './vendor/spec-core/markdown/index.js';
23
23
  const NOTHING = { entries: [], problems: [] };
24
24
  /**
25
25
  * A line that reads as `key:` at any indentation, and the key it names: a key
26
- * as spec-core's reader takes one, of any script, so ` 狀態: 草案` names a
27
- * status and ` 標題: B` a title. A full-width colon ends no key there.
26
+ * as spec-core's reader takes one, of any script, since YAML allows one. A
27
+ * full-width colon ends no key there.
28
28
  */
29
29
  const KEY_SHAPE = /^[ \t]*([\p{L}_][\p{L}\p{M}\p{N}_.-]*)[ \t]*:/u;
30
30
  /** Reads a scanned document's front matter. */
package/dist/yaml.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"yaml.js","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What spec-graph reads out of a document's front matter.\n *\n * The reader is spec-core's, copied into `src/vendor/spec-core/markdown/`: the\n * subset of YAML that specifications use - scalars, quoted scalars, inline and\n * block sequences, `#` comments and, as spec-graph reads it, one level of nested\n * mapping flattened to `parent.child` - with the offset of every key and value,\n * because when a status is wrong the report has to point at the line that\n * declares it.\n *\n * Anything richer is recognised and not guessed at: a value continued on the\n * next line, a plain value holding `: `, a block scalar, an anchor. Such a value\n * is not read, and the reason is a parse problem at the value, so a key that\n * says nothing to the graph says why under `--verbose`. Each problem says\n * whether it left something out and which key, for the count a run prints\n * without `--verbose`.\n *\n * What stays here is the shape the rest of spec-graph reads: a key in lower\n * case, which is how a relation key, a status key and `fm.<key>` in a selector\n * have always been compared, and a value that is a string or a list of them.\n */\n\nimport { readFrontMatter, type FrontMatter as Read } from './vendor/spec-core/markdown/index.js';\nimport type { ScannedDocument } from './markdown.js';\n\nexport interface YamlEntry {\n /** Lower-cased key. Nested keys are flattened with a dot. */\n readonly key: string;\n readonly value: string | readonly string[];\n /** Offset of the key. */\n readonly start: number;\n /** Offset just past the value. */\n readonly end: number;\n /** Offset of the first character of the value, for precise diagnostics. */\n readonly valueStart: number;\n}\n\n/** Something in the front matter that was not read, and where. */\nexport interface FrontMatterProblem {\n readonly message: string;\n readonly start: number;\n readonly end: number;\n /**\n * Whether it left something out: a value, a line or the whole block. A key\n * written twice is a problem that leaves nothing out, since the last is read.\n */\n readonly unread: boolean;\n /**\n * The key, lower-cased, whose value or line was left out, when the line\n * names one; `null` for a line that names none and for the whole block.\n */\n readonly key: string | null;\n}\n\nexport interface FrontMatterReading {\n readonly entries: readonly YamlEntry[];\n readonly problems: readonly FrontMatterProblem[];\n}\n\nconst NOTHING: FrontMatterReading = { entries: [], problems: [] };\n\n/**\n * A line that reads as `key:` at any indentation, and the key it names: a key\n * as spec-core's reader takes one, of any script, so ` 狀態: 草案` names a\n * status and ` 標題: B` a title. A full-width colon ends no key there.\n */\nconst KEY_SHAPE = /^[ \\t]*([\\p{L}_][\\p{L}\\p{M}\\p{N}_.-]*)[ \\t]*:/u;\n\n/** Reads a scanned document's front matter. */\nexport function readEntries(scanned: ScannedDocument): FrontMatterReading {\n const block = scanned.frontMatter;\n if (block === null) return neverClosed(scanned);\n const index = scanned.index;\n // The block and nothing after it: the reader splits what it is given into\n // lines, and the body is none of its business. It stops at the closing\n // delimiter all the same, so the mutant that hands it the whole text reads\n // the same entries, and is equivalent.\n const read = readFrontMatter(scanned.text.slice(0, block.bodyStart), { nested: true });\n // The scan found a closed block, and the reader closes one by the same rule,\n // so this never returns and its mutant is equivalent.\n if (read === null) return NOTHING;\n const { entries, problems } = entriesOf(read, 0);\n return {\n entries,\n problems: [\n ...read.problems.map((problem) => {\n // A key written twice is read, the last of it; any other line with a\n // problem is not, and the key it looks like it names is what went\n // missing - ` status: accepted`, indented under nothing, is a status.\n const unread = !read.entries.some((entry) => entry.line === problem.line);\n const text = index.lineText(problem.line + 1);\n return {\n message: `front matter: ${problem.message}`,\n start: index.lineStart(problem.line + 1),\n end: index.lineEnd(problem.line + 1),\n unread,\n // A match always has its one group, so `?.[1].` reads the same as\n // `?.[1]?.`: that mutant is equivalent.\n key: KEY_SHAPE.exec(text)?.[1]?.toLowerCase() ?? null,\n };\n }),\n ...problems,\n ].sort((a, b) => a.start - b.start),\n };\n}\n\n/**\n * Front matter that opens and never closes, which the scan reads as a rule\n * and Markdown after it, so nothing in it reaches the graph.\n *\n * The scan reports it, with its opening line, rather than leaving a document\n * that wrote front matter looking like one that wrote none. The usual cause is\n * a closer that only looks like one: `----` and `....` closed front matter\n * here before 0.9.0, and neither is one of YAML's document markers.\n */\nfunction neverClosed(scanned: ScannedDocument): FrontMatterReading {\n const open = scanned.unclosedFrontMatter;\n if (open === null) return NOTHING;\n const delimiter = open.kind === 'yaml' ? '---' : '+++';\n return {\n entries: [],\n problems: [\n {\n message: `front matter: opened here and never closed, so none of it is read - close it with \"${delimiter}\" on a line of its own`,\n start: open.start,\n end: open.end,\n unread: true,\n key: null,\n },\n ],\n };\n}\n\n/**\n * Parses front-matter text: what lies between the delimiters, as\n * `FrontMatter.raw` holds it.\n *\n * @param raw The text between the delimiters. It cannot hold a line that would\n * close them, as text a scan read between them never does.\n * @param baseOffset Offset of `raw` within the containing file, so the returned\n * spans address the file rather than the fragment.\n */\nexport function parseFrontMatter(raw: string, baseOffset = 0): YamlEntry[] {\n // The reader reads front matter at the top of a document, so the text goes\n // back between delimiters, and every offset back by the four characters the\n // opening one takes. The line break before the closing one is a blank line\n // when `raw` already ends in one, and a blank line is no key's.\n const read = readFrontMatter(`---\\n${raw}\\n---`, { nested: true });\n // Never null: the text opens with a delimiter and closes with one. The\n // mutants of that branch are equivalent.\n return read === null ? [] : entriesOf(read, baseOffset - 4).entries;\n}\n\n/** The entries a reading holds, and the values it could not read as problems. */\nfunction entriesOf(read: Read, shift: number): { entries: YamlEntry[]; problems: FrontMatterProblem[] } {\n const entries: YamlEntry[] = [];\n const problems: FrontMatterProblem[] = [];\n for (const entry of read.entries) {\n const { value } = entry;\n if (value.kind === 'unsupported') {\n // Only `readEntries` keeps these, and it shifts by 0, where adding and\n // subtracting agree: the mutants of the two sums below are equivalent.\n problems.push({\n message: `front matter: \"${entry.key}\" is not read: ${value.reason}`,\n start: entry.valueStart + shift,\n end: entry.valueEnd + shift,\n unread: true,\n key: entry.key.toLowerCase(),\n });\n continue;\n }\n entries.push({\n key: entry.key.toLowerCase(),\n value: value.kind === 'scalar' ? value.scalar.text : value.items.map((item) => item.text),\n start: entry.keyStart + shift,\n end: entry.valueEnd + shift,\n valueStart: entry.valueStart + shift,\n });\n }\n return { entries, problems };\n}\n\n/** Collapses parsed entries into the flat record stored on a document node. */\nexport function toRecord(entries: readonly YamlEntry[]): Record<string, string | readonly string[]> {\n const out: Record<string, string | readonly string[]> = {};\n for (const entry of entries) out[entry.key] = entry.value;\n return out;\n}\n\n/** Every value of a key as a flat list, whether it was a scalar or a sequence. */\nexport function valuesOf(entry: YamlEntry | undefined): string[] {\n if (!entry) return [];\n return typeof entry.value === 'string'\n ? entry.value.length > 0\n ? [entry.value]\n : []\n : [...entry.value];\n}\n"]}
1
+ {"version":3,"file":"yaml.js","sourceRoot":"","sources":["../src/yaml.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,eAAe,EAA4B,MAAM,sCAAsC,CAAC;AAqCjG,MAAM,OAAO,GAAuB,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAElE;;;;GAIG;AACH,MAAM,SAAS,GAAG,gDAAgD,CAAC;AAEnE,+CAA+C;AAC/C,MAAM,UAAU,WAAW,CAAC,OAAwB;IAClD,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;IAClC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAC5B,0EAA0E;IAC1E,uEAAuE;IACvE,2EAA2E;IAC3E,uCAAuC;IACvC,MAAM,IAAI,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACvF,6EAA6E;IAC7E,sDAAsD;IACtD,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,OAAO,CAAC;IAClC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACjD,OAAO;QACL,OAAO;QACP,QAAQ,EAAE;YACR,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;gBAC/B,qEAAqE;gBACrE,kEAAkE;gBAClE,uEAAuE;gBACvE,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;gBAC1E,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;gBAC9C,OAAO;oBACL,OAAO,EAAE,iBAAiB,OAAO,CAAC,OAAO,EAAE;oBAC3C,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;oBACxC,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;oBACpC,MAAM;oBACN,kEAAkE;oBAClE,wCAAwC;oBACxC,GAAG,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,IAAI;iBACtD,CAAC;YACJ,CAAC,CAAC;YACF,GAAG,QAAQ;SACZ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;KACpC,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,WAAW,CAAC,OAAwB;IAC3C,MAAM,IAAI,GAAG,OAAO,CAAC,mBAAmB,CAAC;IACzC,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,OAAO,CAAC;IAClC,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;IACvD,OAAO;QACL,OAAO,EAAE,EAAE;QACX,QAAQ,EAAE;YACR;gBACE,OAAO,EAAE,sFAAsF,SAAS,wBAAwB;gBAChI,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,MAAM,EAAE,IAAI;gBACZ,GAAG,EAAE,IAAI;aACV;SACF;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,UAAU,GAAG,CAAC;IAC1D,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,gEAAgE;IAChE,MAAM,IAAI,GAAG,eAAe,CAAC,QAAQ,GAAG,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACnE,uEAAuE;IACvE,yCAAyC;IACzC,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;AACtE,CAAC;AAED,iFAAiF;AACjF,SAAS,SAAS,CAAC,IAAU,EAAE,KAAa;IAC1C,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,MAAM,QAAQ,GAAyB,EAAE,CAAC;IAC1C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAC;QACxB,IAAI,KAAK,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACjC,uEAAuE;YACvE,uEAAuE;YACvE,QAAQ,CAAC,IAAI,CAAC;gBACZ,OAAO,EAAE,kBAAkB,KAAK,CAAC,GAAG,kBAAkB,KAAK,CAAC,MAAM,EAAE;gBACpE,KAAK,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK;gBAC/B,GAAG,EAAE,KAAK,CAAC,QAAQ,GAAG,KAAK;gBAC3B,MAAM,EAAE,IAAI;gBACZ,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE;aAC7B,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC;YACX,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,EAAE;YAC5B,KAAK,EAAE,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;YACzF,KAAK,EAAE,KAAK,CAAC,QAAQ,GAAG,KAAK;YAC7B,GAAG,EAAE,KAAK,CAAC,QAAQ,GAAG,KAAK;YAC3B,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK;SACrC,CAAC,CAAC;IACL,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AAC/B,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,QAAQ,CAAC,OAA6B;IACpD,MAAM,GAAG,GAA+C,EAAE,CAAC;IAC3D,KAAK,MAAM,KAAK,IAAI,OAAO;QAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC;IAC1D,OAAO,GAAG,CAAC;AACb,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,QAAQ,CAAC,KAA4B;IACnD,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,OAAO,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QACpC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;YACtB,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;YACf,CAAC,CAAC,EAAE;QACN,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC","sourcesContent":["/**\n * What spec-graph reads out of a document's front matter.\n *\n * The reader is spec-core's, copied into `src/vendor/spec-core/markdown/`: the\n * subset of YAML that specifications use - scalars, quoted scalars, inline and\n * block sequences, `#` comments and, as spec-graph reads it, one level of nested\n * mapping flattened to `parent.child` - with the offset of every key and value,\n * because when a status is wrong the report has to point at the line that\n * declares it.\n *\n * Anything richer is recognised and not guessed at: a value continued on the\n * next line, a plain value holding `: `, a block scalar, an anchor. Such a value\n * is not read, and the reason is a parse problem at the value, so a key that\n * says nothing to the graph says why under `--verbose`. Each problem says\n * whether it left something out and which key, for the count a run prints\n * without `--verbose`.\n *\n * What stays here is the shape the rest of spec-graph reads: a key in lower\n * case, which is how a relation key, a status key and `fm.<key>` in a selector\n * have always been compared, and a value that is a string or a list of them.\n */\n\nimport { readFrontMatter, type FrontMatter as Read } from './vendor/spec-core/markdown/index.js';\nimport type { ScannedDocument } from './markdown.js';\n\nexport interface YamlEntry {\n /** Lower-cased key. Nested keys are flattened with a dot. */\n readonly key: string;\n readonly value: string | readonly string[];\n /** Offset of the key. */\n readonly start: number;\n /** Offset just past the value. */\n readonly end: number;\n /** Offset of the first character of the value, for precise diagnostics. */\n readonly valueStart: number;\n}\n\n/** Something in the front matter that was not read, and where. */\nexport interface FrontMatterProblem {\n readonly message: string;\n readonly start: number;\n readonly end: number;\n /**\n * Whether it left something out: a value, a line or the whole block. A key\n * written twice is a problem that leaves nothing out, since the last is read.\n */\n readonly unread: boolean;\n /**\n * The key, lower-cased, whose value or line was left out, when the line\n * names one; `null` for a line that names none and for the whole block.\n */\n readonly key: string | null;\n}\n\nexport interface FrontMatterReading {\n readonly entries: readonly YamlEntry[];\n readonly problems: readonly FrontMatterProblem[];\n}\n\nconst NOTHING: FrontMatterReading = { entries: [], problems: [] };\n\n/**\n * A line that reads as `key:` at any indentation, and the key it names: a key\n * as spec-core's reader takes one, of any script, since YAML allows one. A\n * full-width colon ends no key there.\n */\nconst KEY_SHAPE = /^[ \\t]*([\\p{L}_][\\p{L}\\p{M}\\p{N}_.-]*)[ \\t]*:/u;\n\n/** Reads a scanned document's front matter. */\nexport function readEntries(scanned: ScannedDocument): FrontMatterReading {\n const block = scanned.frontMatter;\n if (block === null) return neverClosed(scanned);\n const index = scanned.index;\n // The block and nothing after it: the reader splits what it is given into\n // lines, and the body is none of its business. It stops at the closing\n // delimiter all the same, so the mutant that hands it the whole text reads\n // the same entries, and is equivalent.\n const read = readFrontMatter(scanned.text.slice(0, block.bodyStart), { nested: true });\n // The scan found a closed block, and the reader closes one by the same rule,\n // so this never returns and its mutant is equivalent.\n if (read === null) return NOTHING;\n const { entries, problems } = entriesOf(read, 0);\n return {\n entries,\n problems: [\n ...read.problems.map((problem) => {\n // A key written twice is read, the last of it; any other line with a\n // problem is not, and the key it looks like it names is what went\n // missing - ` status: accepted`, indented under nothing, is a status.\n const unread = !read.entries.some((entry) => entry.line === problem.line);\n const text = index.lineText(problem.line + 1);\n return {\n message: `front matter: ${problem.message}`,\n start: index.lineStart(problem.line + 1),\n end: index.lineEnd(problem.line + 1),\n unread,\n // A match always has its one group, so `?.[1].` reads the same as\n // `?.[1]?.`: that mutant is equivalent.\n key: KEY_SHAPE.exec(text)?.[1]?.toLowerCase() ?? null,\n };\n }),\n ...problems,\n ].sort((a, b) => a.start - b.start),\n };\n}\n\n/**\n * Front matter that opens and never closes, which the scan reads as a rule\n * and Markdown after it, so nothing in it reaches the graph.\n *\n * The scan reports it, with its opening line, rather than leaving a document\n * that wrote front matter looking like one that wrote none. The usual cause is\n * a closer that only looks like one: `----` and `....` closed front matter\n * here before 0.9.0, and neither is one of YAML's document markers.\n */\nfunction neverClosed(scanned: ScannedDocument): FrontMatterReading {\n const open = scanned.unclosedFrontMatter;\n if (open === null) return NOTHING;\n const delimiter = open.kind === 'yaml' ? '---' : '+++';\n return {\n entries: [],\n problems: [\n {\n message: `front matter: opened here and never closed, so none of it is read - close it with \"${delimiter}\" on a line of its own`,\n start: open.start,\n end: open.end,\n unread: true,\n key: null,\n },\n ],\n };\n}\n\n/**\n * Parses front-matter text: what lies between the delimiters, as\n * `FrontMatter.raw` holds it.\n *\n * @param raw The text between the delimiters. It cannot hold a line that would\n * close them, as text a scan read between them never does.\n * @param baseOffset Offset of `raw` within the containing file, so the returned\n * spans address the file rather than the fragment.\n */\nexport function parseFrontMatter(raw: string, baseOffset = 0): YamlEntry[] {\n // The reader reads front matter at the top of a document, so the text goes\n // back between delimiters, and every offset back by the four characters the\n // opening one takes. The line break before the closing one is a blank line\n // when `raw` already ends in one, and a blank line is no key's.\n const read = readFrontMatter(`---\\n${raw}\\n---`, { nested: true });\n // Never null: the text opens with a delimiter and closes with one. The\n // mutants of that branch are equivalent.\n return read === null ? [] : entriesOf(read, baseOffset - 4).entries;\n}\n\n/** The entries a reading holds, and the values it could not read as problems. */\nfunction entriesOf(read: Read, shift: number): { entries: YamlEntry[]; problems: FrontMatterProblem[] } {\n const entries: YamlEntry[] = [];\n const problems: FrontMatterProblem[] = [];\n for (const entry of read.entries) {\n const { value } = entry;\n if (value.kind === 'unsupported') {\n // Only `readEntries` keeps these, and it shifts by 0, where adding and\n // subtracting agree: the mutants of the two sums below are equivalent.\n problems.push({\n message: `front matter: \"${entry.key}\" is not read: ${value.reason}`,\n start: entry.valueStart + shift,\n end: entry.valueEnd + shift,\n unread: true,\n key: entry.key.toLowerCase(),\n });\n continue;\n }\n entries.push({\n key: entry.key.toLowerCase(),\n value: value.kind === 'scalar' ? value.scalar.text : value.items.map((item) => item.text),\n start: entry.keyStart + shift,\n end: entry.valueEnd + shift,\n valueStart: entry.valueStart + shift,\n });\n }\n return { entries, problems };\n}\n\n/** Collapses parsed entries into the flat record stored on a document node. */\nexport function toRecord(entries: readonly YamlEntry[]): Record<string, string | readonly string[]> {\n const out: Record<string, string | readonly string[]> = {};\n for (const entry of entries) out[entry.key] = entry.value;\n return out;\n}\n\n/** Every value of a key as a flat list, whether it was a scalar or a sequence. */\nexport function valuesOf(entry: YamlEntry | undefined): string[] {\n if (!entry) return [];\n return typeof entry.value === 'string'\n ? entry.value.length > 0\n ? [entry.value]\n : []\n : [...entry.value];\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@descent-vtt/spec-graph",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
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",