@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/CHANGELOG.md CHANGED
@@ -11,6 +11,36 @@ the family's
11
11
  [versioning policy](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0009-versions-before-1-0.md)
12
12
  records.
13
13
 
14
+ ## 0.11.1
15
+
16
+ Documentation and robustness: the README's SARIF example is pinned as CI
17
+ pins its own, and every scan steps past an empty match, so no table entry
18
+ can hang a run. Nothing a run answers has changed.
19
+
20
+ ### Documentation
21
+
22
+ - The README's SARIF example checks out and sets up Node itself, pins each
23
+ action to a commit with its version beside it, and keeps no token in the
24
+ checkout.
25
+
26
+ ## 0.11.0
27
+
28
+ spec-graph reads English only again, by the maintainer's decision; the
29
+ two-column status table stays with English keys. spec-core's copy is
30
+ unchanged.
31
+
32
+ ### Removed
33
+
34
+ - Statuses, relations, obligation and bookkeeping headings and markers
35
+ written in Chinese are no longer read: a Chinese status word, `狀態` or
36
+ `状态` as a status key, heading, label or table key, and a full-width
37
+ colon after a status label or heading give an unknown phase, and a
38
+ Chinese phrase, heading or marker draws nothing. The two-column status
39
+ table stays, with `Status` and `State`, and so do the stop at `。?!;`
40
+ and a full-width comma or semicolon between successors (ADR-0002,
41
+ ADR-0003, ADR-0006). Upgrading: write the status in English, e.g.
42
+ `status: superseded`.
43
+
14
44
  ## 0.10.0
15
45
 
16
46
  Statuses, status tables, relations, obligations and markers written in
package/README.md CHANGED
@@ -133,8 +133,9 @@ onto four phases, chosen so one question can be answered from the phase alone:
133
133
  | `frozen` | `final`, `ratified`, `locked`, `published`, … | **no** |
134
134
  | `retired` | `superseded`, `deprecated`, `rejected`, `withdrawn`, … | **no** |
135
135
 
136
- The status is found wherever your team writes it, in English or in Chinese
137
- (`## 狀態` / `已接受`), and however it is dressed (`**Accepted** (2026-03-01) ✅`):
136
+ The status is found wherever your team writes it, and however it is dressed
137
+ (`**Accepted** (2026-03-01) ✅`). A document may be written in any language; its
138
+ status is read in English (`status: superseded`):
138
139
 
139
140
  - front matter: `status:`, `state:`, `stage:`
140
141
  - a `## Status` section
@@ -167,44 +168,14 @@ that `provisionally accepted` is a draft.
167
168
  | `active` | `accepted`, `active`, `approved`, `adopted`, `agreed`, `implementable`, `implemented`, `implementing`, `current`, `effective`, `in-effect`, `enforced`, `stable`, `merged`, `released`, `shipped`, `live`, `done`, `complete`, `completed`, `signed-off`, `committed` |
168
169
  | `draft` | `draft`, `drafting`, `proposed`, `proposal`, `provisional`, `provisionally-accepted`, `prospective`, `wip`, `work-in-progress`, `in-progress`, `in_progress`, `review`, `in-review`, `under-review`, `reviewing`, `discussion`, `discussing`, `pending`, `idea`, `exploratory`, `candidate`, `open`, `new`, `unreviewed`, `rfc`, `experimental`, `alpha`, `beta`, `incubating` |
169
170
 
170
- Chinese is read in Traditional and Simplified characters, each word as the
171
- English word it translates, so it lands in that word's row: `延後` is
172
- `deferred` and retired, `封存` is `archived` and a record, and `暫定` is
173
- `provisionally accepted`, so `暫定接受` is a draft.
174
-
175
- - **A Chinese word counts wherever it stands in the status**, because Chinese
176
- puts no space between words: `已接受,後被ADR-0003取代` holds `已接受` and a
177
- supersession.
178
- - **Except directly after a negation:** `不`, `未`, `非`, `沒`, `没`, `無`,
179
- `无`, `勿`, `尚未` or `不再`. `未接受`, `尚未核准` and `不再生效` say nothing;
180
- a word that starts with one, `不採納`, is itself.
181
- - **Superseded is also `被` followed by `取代`, `替代` or `取而代之`**, within 30
182
- characters and in the same sentence: `已被 ADR-0003 取代` is retired, and
183
- names ADR-0003 as what replaced it.
184
- - **`已取代` alone is retired.** Followed by a name, after spaces or a colon -
185
- `已取代 ADR-0002` - it says what this document replaced, and is not read.
186
- - **`取代` with no `被`**, as in `已接受(取代 ADR-0002)`, retires nothing.
187
-
188
- | Phase | Chinese status words |
189
- | --- | --- |
190
- | `retired` | `已被取代`, `被取代`, `已取代`, `已棄用`, `棄用`, `已弃用`, `弃用`, `已廢棄`, `廢棄`, `已废弃`, `废弃`, `已停用`, `已過時`, `已过时`, `已否決`, `否決`, `已否决`, `否决`, `已拒絕`, `已拒绝`, `不採納`, `不采纳`, `已撤回`, `撤回`, `已作廢`, `作廢`, `已作废`, `作废`, `延後`, `延后`, `暫緩`, `暂缓`, `擱置`, `搁置` |
191
- | `record` | `封存`, `已封存`, `歸檔`, `已歸檔`, `归档`, `已归档` |
192
- | `frozen` | `已定案`, `定案`, `已凍結`, `已冻结` |
193
- | `active` | `已接受`, `接受`, `已採納`, `採納`, `已采纳`, `采纳`, `已核准`, `核准`, `已批准`, `批准`, `已生效`, `生效`, `已實施`, `已实施`, `已完成` |
194
- | `draft` | `暫定`, `暂定`, `草稿`, `草案`, `提議`, `提议`, `提案`, `審查中`, `审查中`, `審核中`, `审核中`, `討論中`, `讨论中`, `待審`, `待审`, `待審核`, `待审核` |
195
-
196
171
  | Where | Read from |
197
172
  | --- | --- |
198
- | Front matter, the first key present | `status`, `狀態`, `状态`, `state`, `stage`, `lifecycle`, `phase`, `adr-status` |
199
- | A section, under the heading, with or without a colon, `:` or `:` | `Status`, `State`, `Stage`, `Lifecycle`, `Current status`, `狀態`, `状态` |
200
- | A two-column table above the first `##`, in the cell right of the first key in the left column, the header row included | `Status`, `State`, `狀態`, `状态` |
173
+ | Front matter, the first key present | `status`, `state`, `stage`, `lifecycle`, `phase`, `adr-status` |
174
+ | A section, under the heading, with or without a colon | `Status`, `State`, `Stage`, `Lifecycle`, `Current status` |
175
+ | A two-column table above the first `##`, in the cell right of the first key in the left column, the header row included | `Status`, `State` |
201
176
  | A directory, for a document that declares no status | `archive`, `archived`, `attic`, `deprecated`, `graveyard`, `historical`, `obsolete`, `rejected`, `retired`, `superseded`, `superceded`, `withdrawn` |
202
177
 
203
- In front matter only an ASCII colon ends a key, as YAML has it: `狀態:已接受`
204
- there is a line left unread, where a heading or a label takes either colon.
205
-
206
- The table is where many teams put a document's fields, `| 狀態 | 已接受 |` or
207
- `| Status | Accepted |`:
178
+ The table is where many teams put a document's fields, `| Status | Accepted |`:
208
179
 
209
180
  - It is read where a `## Status` section is, after one: a document with both is
210
181
  read from the section, and front matter still wins over either.
@@ -275,26 +246,23 @@ code. *"We resolved to keep the queue"* does not close anything.
275
246
  | Disposition | Checkboxes | Markers | `state=` |
276
247
  | --- | --- | --- | --- |
277
248
  | `unresolved` | `[ ]` `[?]` `[!]` `[*]` | `unresolved`, `still open`, `open question`, `tbd`, `undecided`, `reopened` | `open`, `unresolved`, `todo` |
278
- | `narrowed` | `[~]` `[/]` | `partially resolved`, `partially answered`, `partially done`, `partly resolved`, `narrowed`, `narrowed to`, `scoped down`, `reduced scope`, `reduced to`, `partial`, `in progress`, `in_progress`, `remaining`, `split`, `部分解決`, `部分解决` | `narrowed`, `partial` |
279
- | `delegated` | | `delegated`, `delegated to`, `moved to`, `tracked in`, `tracked by`, `handed off`, `handed off to`, `handed to`, `deferred to`, `follow up in`, `follow-up in`, `followup in`, `continued in`, `owned by`, `已移交` | `delegated` |
280
- | `satisfied` | `[x]` `[X]` `[+]` | `resolved`, `answered`, `decided`, `settled`, `done`, `complete`, `completed`, `closed`, `fixed`, `shipped`, `addressed`, `implemented`, `solved`, `confirmed`, `已解決`, `已解决`, `已決定`, `已决定`, `已定案` | `satisfied`, `resolved`, `done`, `closed` |
281
- | `accepted-debt` | | `accepted debt`, `accepted as debt`, `accepted risk`, `known limitation`, `known issue`, `known gap`, `technical debt`, `tech debt`, `wontfix`, `won't fix`, `won’t fix`, `will not fix`, `by design`, `tolerated`, `living with it`, `技術債`, `技术债`, `已知限制` | `accepted-debt`, `debt`, `wontfix` |
249
+ | `narrowed` | `[~]` `[/]` | `partially resolved`, `partially answered`, `partially done`, `partly resolved`, `narrowed`, `narrowed to`, `scoped down`, `reduced scope`, `reduced to`, `partial`, `in progress`, `in_progress`, `remaining`, `split` | `narrowed`, `partial` |
250
+ | `delegated` | | `delegated`, `delegated to`, `moved to`, `tracked in`, `tracked by`, `handed off`, `handed off to`, `handed to`, `deferred to`, `follow up in`, `follow-up in`, `followup in`, `continued in`, `owned by` | `delegated` |
251
+ | `satisfied` | `[x]` `[X]` `[+]` | `resolved`, `answered`, `decided`, `settled`, `done`, `complete`, `completed`, `closed`, `fixed`, `shipped`, `addressed`, `implemented`, `solved`, `confirmed` | `satisfied`, `resolved`, `done`, `closed` |
252
+ | `accepted-debt` | | `accepted debt`, `accepted as debt`, `accepted risk`, `known limitation`, `known issue`, `known gap`, `technical debt`, `tech debt`, `wontfix`, `won't fix`, `won’t fix`, `will not fix`, `by design`, `tolerated`, `living with it` | `accepted-debt`, `debt`, `wontfix` |
282
253
  | `rejected` | `[-]` | `rejected`, `declined`, `dropped`, `not doing`, `will not do`, `abandoned`, `cancelled`, `canceled`, `withdrawn` | `rejected`, `declined` |
283
254
  | `obviated` | | `no longer applicable`, `no longer relevant`, `no longer needed`, `overtaken by events`, `premise invalid`, `obviated`, `obsolete`, `not applicable`, `moot`, `void`, `n/a`, `obe` | `obviated`, `moot` |
284
255
 
285
256
  Where a marker counts, and how it may be written:
286
257
 
287
258
  - A marker counts where a statement starts: at the start of the item or of a
288
- line, after the end of a sentence - a Chinese one, `。`, `?`, `!` or `;`,
289
- with a space after it or none - or after `(` or `[`.
259
+ line, after the end of a sentence, or after `(` or `[`.
290
260
  - Punctuated means followed by `:`, `:`, `-`, `–`, `—` or `.`, so `Done.`
291
261
  closes an item; emphasised means the emphasis closes on the marker, so
292
262
  `**Closed beta**` does not.
293
263
  - A marker of more than one word may be spaced, hyphenated or joined by an
294
264
  underscore, whichever the table has: `**Tech-debt:**` and `TECH_DEBT:` are
295
265
  `tech debt`. An apostrophe may be straight or curly.
296
- - A Chinese marker is the English one it translates, and has no case to shout
297
- in: `已決定:` and `**已決定**` close an item, and `我們已決定採用` does not.
298
266
 
299
267
  </details>
300
268
 
@@ -309,8 +277,7 @@ heading of its level. Every other bullet is the document's own prose.
309
277
  `Follow Ups`, `Follow-Ups`, `Followups`, `Follow Up`, `Follow-Up`,
310
278
  `Unresolved`, `Outstanding`, `Outstanding Questions`, `Tasks`, `Task List`,
311
279
  `Work Items`, `Remaining Work`, `Decisions Needed`, `Blockers`, `Parking Lot`,
312
- `Future Work`, `Deferred`, `待決事項`, `待决事项`, `未決問題`, `未决问题`,
313
- `待辦事項`, `待办事项`, `後續工作`, `后续工作`.
280
+ `Future Work`, `Deferred`.
314
281
 
315
282
  ### References, resolved forgivingly and validated strictly
316
283
 
@@ -473,6 +440,9 @@ A phrase governs a reference when it stands no more than 40 characters before
473
440
  it, in the same statement: a sentence, a paragraph, a list item or a table cell.
474
441
 
475
442
  - A negation between the two - `owned by nobody` - cancels it.
443
+ - A sentence written in Chinese ends at `。`, `?`, `!` or `;`, with a space
444
+ after it or none, so `Deferred to later。另見 ADR-0002` hands nothing to
445
+ ADR-0002. A stop only ends a statement sooner: it reads less, never more.
476
446
  - Prose reads both directions too: `supersedes` and `superseded by`, `amends`
477
447
  and `amended by`, `blocked by` and `blocks` or `blocking`, `depends on` and
478
448
  `depended on by` - the second of each pair points back at the document that
@@ -482,30 +452,12 @@ it, in the same statement: a sentence, a paragraph, a list item or a table cell.
482
452
  phrase: `[ADR-0002](0002.md) requires a migration` says nothing about this
483
453
  document.
484
454
 
485
- In Chinese a phrase before the reference hands the work on, as `deferred to`
486
- does. **Chinese hand-off phrases:** `延後至`, `延后至`, `延至`, `移交至`,
487
- `移交給`, `移交给`, `留待`, `交由`, `併入`, `并入`.
488
-
489
- - Chinese puts no space between words, so one counts wherever it stands,
490
- `此問題延後至ADR-0002`, except directly after a negation - `不`, `未`, `非`,
491
- `沒`, `没`, `無`, `无`, `勿`, `尚未` or `不再` - so `未移交給 ADR-0002` hands
492
- nothing on.
493
- - An English phrase counts beside Chinese as well, `此問題deferred to ADR-0002`:
494
- a Han character is not part of an English word.
495
- - A statement also ends at `。`, `?`, `!` and `;`, with a space after it or
496
- none, so `上次延後至別處。另見 ADR-0002` hands nothing to ADR-0002.
497
- - A phrase written around the reference, `由 ADR-0002 決定` or
498
- `在 ADR-0002 中追蹤`, is not read, and neither are `根據`, `依據` and `基於`:
499
- Chinese uses them for any citation, and an `assumes` read from one would make
500
- a stale premise of it.
501
-
502
455
  A link under a heading that files it as bookkeeping is `relates-to` unless a
503
456
  phrase beside it says more. **Bookkeeping headings:** `See also`, `References`,
504
457
  `Reference`, `Related`, `Related work`, `Related decisions`,
505
458
  `Related documents`, `Links`, `Further reading`, `Prior art`, `History`,
506
459
  `Changelog`, `Change log`, `Revision history`, `Bibliography`, `Sources`,
507
- `Appendix`, `More information`, `Resources`, `Index`, `參考資料`, `参考资料`,
508
- `相關文件`, `相关文件`, `延伸閱讀`, `延伸阅读`, `附錄`, `附录`, `修訂紀錄`, `修订记录`.
460
+ `Appendix`, `More information`, `Resources`, `Index`.
509
461
 
510
462
  Two properties do the work:
511
463
 
@@ -531,8 +483,8 @@ decisions. **A specification is a region of a file, not a file**, so both work:
531
483
  Shard by tenant id.
532
484
  ```
533
485
 
534
- Each section with an identifier *and* a status - a `**Status:**` line,
535
- `**狀態:**` in Chinese, or a `### Status` section under its heading - becomes a
486
+ Each section with an identifier *and* a status - a `**Status:**` line or a
487
+ `### Status` section under its heading - becomes a
536
488
  specification in its own right, with its own lifecycle, its own obligations and
537
489
  its own relations — resolvable from anywhere in the corpus as `ADR-0007`,
538
490
  whether it lives in a register or in its own file. Moving it out later breaks
@@ -1109,14 +1061,24 @@ permissions:
1109
1061
  contents: read
1110
1062
  security-events: write
1111
1063
  steps:
1112
- # check out the repository and set up Node, then:
1064
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
1065
+ with:
1066
+ persist-credentials: false
1067
+ - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
1068
+ with:
1069
+ node-version: '24'
1070
+ - run: npm ci
1113
1071
  - run: npx spec-graph --format sarif > spec-graph.sarif
1114
1072
  continue-on-error: true
1115
- - uses: github/codeql-action/upload-sarif@v4
1073
+ - uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
1116
1074
  with:
1117
1075
  sarif_file: spec-graph.sarif
1118
1076
  ```
1119
1077
 
1078
+ The actions are pinned to commits, with the version beside each, because a tag
1079
+ can be moved and a commit cannot; Dependabot's `github-actions` updates keep
1080
+ pins like these current.
1081
+
1120
1082
  Findings land as comments on the changed lines, and they persist across commits
1121
1083
  without drifting, because the `partialFingerprints` spec-graph writes are the
1122
1084
  same identity a baseline is keyed on — the rule, the specification, and the
@@ -1325,9 +1287,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
1325
1287
  than installed. So the whole package is auditable in an afternoon — and nothing
1326
1288
  in the pipeline can take longer than its input.
1327
1289
 
1328
- **Verified, not just covered.** 1,996 tests; 99.4% statement and 99.8% line
1290
+ **Verified, not just covered.** 1,886 tests; 99.4% statement and 99.8% line
1329
1291
  coverage. Coverage says a line ran, so the suite is also held to a mutation
1330
- score - 96.34% over 8,575 mutants, measured by CI's full sweep, and a build
1292
+ score - 96.31% over 8,385 mutants, measured by CI's full sweep, and a build
1331
1293
  fails under 93 - because a vocabulary entry or a boundary
1332
1294
  condition can be weakened by an ordinary-looking refactor without a single test
1333
1295
  going red. [ADR-0007](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0007-mutation-testing.md) is straight about what
@@ -63,8 +63,7 @@ export function parseDirectives(comments) {
63
63
  const attributes = new Map();
64
64
  const unknown = [];
65
65
  const allowed = SCHEMA[directiveName];
66
- ATTRIBUTE.lastIndex = 0;
67
- for (let m = ATTRIBUTE.exec(body); m !== null; m = ATTRIBUTE.exec(body)) {
66
+ for (const m of body.matchAll(ATTRIBUTE)) {
68
67
  const key = m[1].toLowerCase();
69
68
  const quoted = m[3] ?? m[4];
70
69
  const bare = m[5];
@@ -1 +1 @@
1
- {"version":3,"file":"directives.js","sourceRoot":"","sources":["../src/directives.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Explicit annotations, written as HTML comments.\n *\n * Inference covers the overwhelming majority of real documents, but inference\n * that cannot be overridden is a trap: the first time it guesses wrong about a\n * document somebody cannot restructure, the whole tool gets switched off. These\n * directives are the escape hatch, and they are HTML comments so they stay\n * invisible in GitHub, MkDocs, Docusaurus and every other renderer.\n *\n * ```md\n * <!-- @spec-node id=\"ADR-0007\" status=\"accepted\" aliases=\"adr-7, sharding\" -->\n * <!-- @spec-item id=\"shard-key\" state=\"narrowed\" -->\n * <!-- @spec-edge kind=\"delegates-to\" to=\"ADR-0011#scope\" -->\n * <!-- @spec-ignore -->\n * ```\n *\n * A directive always wins over anything inferred, and says so in the report, so\n * an override is visible rather than mysterious.\n */\n\nimport { isMarkdownLine, type HtmlComment, type ListItem, type ScannedDocument } from './markdown.js';\n\nexport type DirectiveName = 'spec-node' | 'spec-item' | 'spec-edge' | 'spec-ignore' | 'spec-history';\n\nconst KNOWN: ReadonlySet<string> = new Set<DirectiveName>([\n 'spec-node',\n 'spec-item',\n 'spec-edge',\n 'spec-ignore',\n 'spec-history',\n]);\n\nexport interface Attribute {\n readonly value: string;\n /** Absolute offset of the value text, quotes excluded. */\n readonly start: number;\n readonly end: number;\n}\n\nexport interface Directive {\n readonly name: DirectiveName;\n readonly attributes: ReadonlyMap<string, Attribute>;\n /** Offset of the enclosing comment. */\n readonly start: number;\n readonly end: number;\n readonly line: number;\n /** Attribute names that were written but are not part of this directive. */\n readonly unknownAttributes: readonly string[];\n}\n\nconst DIRECTIVE_HEAD = /^\\s*@([a-z][a-z0-9-]*)/;\nconst ATTRIBUTE = /([A-Za-z_][A-Za-z0-9_-]*)\\s*(?:=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s\"'<>]+)))?/g;\n\n/** Attributes each directive understands. Anything else is reported, not ignored. */\nconst SCHEMA: Readonly<Record<DirectiveName, readonly string[]>> = {\n 'spec-node': ['id', 'status', 'title', 'aliases', 'kind'],\n // Takes no attributes: the file either is a record or it is not. A mutant\n // that lists `Stryker was here` lists no name: one never holds a space.\n 'spec-history': [],\n 'spec-item': ['id', 'state', 'title', 'owner', 'note'],\n 'spec-edge': ['kind', 'to', 'from', 'reason'],\n 'spec-ignore': ['reason'],\n};\n\n/**\n * Reads every spec-graph directive out of a document's HTML comments.\n *\n * Only out of a comment that closes. One that opens a line and never does runs\n * to the end of the document, and read as a directive, a stray\n * `<!-- @spec-ignore` would drop the file, and a stray `<!-- @spec-node` would\n * take every `name=value` in the rest of it as an attribute.\n */\nexport function parseDirectives(comments: readonly HtmlComment[]): Directive[] {\n const out: Directive[] = [];\n for (const comment of comments) {\n if (comment.closed === false) continue;\n const head = DIRECTIVE_HEAD.exec(comment.inner);\n if (!head) continue;\n const name = head[1] as string;\n if (!KNOWN.has(name)) continue;\n\n const directiveName = name as DirectiveName;\n // The head is anchored, so it starts where the comment's text does.\n const bodyStart = (head[0] as string).length;\n const body = comment.inner.slice(bodyStart);\n const attributes = new Map<string, Attribute>();\n const unknown: string[] = [];\n const allowed = SCHEMA[directiveName];\n\n ATTRIBUTE.lastIndex = 0;\n for (let m = ATTRIBUTE.exec(body); m !== null; m = ATTRIBUTE.exec(body)) {\n const key = (m[1] as string).toLowerCase();\n const quoted = m[3] ?? m[4];\n const bare = m[5];\n // A bare attribute is a flag: `<!-- @spec-ignore reason -->`.\n const value = quoted ?? bare ?? 'true';\n const raw = m[2];\n const valueOffset =\n raw === undefined\n ? (m.index ?? 0) + (m[0] as string).length\n : (m.index ?? 0) + (m[0] as string).indexOf(raw) + (quoted === undefined ? 0 : 1);\n const start = comment.innerStart + bodyStart + valueOffset;\n\n if (!allowed.includes(key)) unknown.push(key);\n attributes.set(key, { value, start, end: start + value.length });\n }\n\n out.push({\n name: directiveName,\n attributes,\n start: comment.start,\n end: comment.end,\n line: comment.line,\n unknownAttributes: unknown,\n });\n }\n return out;\n}\n\n/** Reads an attribute, trimmed. Returns `null` when absent or blank. */\nexport function attr(directive: Directive, name: string): Attribute | null {\n const found = directive.attributes.get(name);\n if (!found) return null;\n const trimmed = found.value.trim();\n if (trimmed.length === 0) return null;\n const offset = found.value.indexOf(trimmed);\n return { value: trimmed, start: found.start + offset, end: found.start + offset + trimmed.length };\n}\n\n/** Splits a comma- or whitespace-separated attribute into its parts. */\nexport function attrList(directive: Directive, name: string): string[] {\n const found = attr(directive, name);\n if (!found) return [];\n // Whitespace is a separator, so no part holds any to trim. The filter drops\n // what a separator at either end leaves, and would drop what each separator\n // in a run leaves too, so splitting at one at a time is equivalent.\n return found.value.split(/[,;\\s]+/).filter((part) => part.length > 0);\n}\n\n/**\n * Finds the directive that annotates a given region.\n *\n * A `@spec-item` directive may sit on the line above the item, on the same\n * line, or inside the item body. All three read naturally in the source, so all\n * three are accepted, with the closest preceding directive winning.\n *\n * It answers for one region with no view of the others, so a directive in reach\n * of two items is found for both. Extraction uses `bindItemDirectives`.\n */\nexport function directiveFor(\n directives: readonly Directive[],\n name: DirectiveName,\n region: { start: number; end: number },\n /** How far above the region a directive may sit and still apply. */\n lookBehind: number,\n): Directive | null {\n let best: Directive | null = null;\n for (const directive of directives) {\n if (directive.name !== name) continue;\n const inside = directive.start >= region.start && directive.end <= region.end;\n const above = directive.end <= region.start && region.start - directive.end <= lookBehind;\n if (!inside && !above) continue;\n // Two directives never start at one offset, so `>=` reads the same.\n if (best === null || directive.start > best.start) best = directive;\n }\n return best;\n}\n\n/**\n * Pairs each `@spec-item` directive with the one list item it annotates.\n *\n * A directive is written for one item, so it binds to one: the item directly\n * below it, with nothing but blank lines and other comments between, or else\n * the item it is written in, the innermost where items nest. An item that\n * several bind to takes the last one written.\n *\n * A directive alone on its line is in an item only when indented further than\n * that item's marker, which is how CommonMark reads it. Indented under an item,\n * it annotates that item and not the sibling below; flush between two items, it\n * annotates the second.\n */\nexport function bindItemDirectives(\n scanned: ScannedDocument,\n directives: readonly Directive[],\n): Map<ListItem, Directive> {\n const bound = new Map<ListItem, Directive>();\n const wanted = directives.filter((directive) => directive.name === 'spec-item');\n // Most documents carry none. Sparing them the pass over their lines changes\n // how long this takes and nothing else.\n if (wanted.length === 0) return bound;\n\n const { lines, listItems } = scanned;\n const masked = scanned.masks.structure;\n // The indentation of each line holding nothing but whitespace and comments.\n // Code and raw-text HTML are masked too, so they are told apart by their flags.\n const quiet = new Map<number, number>();\n // For every other line, where the run of quiet lines directly above it began,\n // when there was one.\n const quietFrom = new Map<number, number | undefined>();\n let run: number | undefined;\n for (const line of lines) {\n if (isMarkdownLine(line) && masked.slice(line.contentStart, line.end).trim() === '') {\n quiet.set(line.line, line.indent);\n run ??= line.start;\n continue;\n }\n quietFrom.set(line.line, run);\n run = undefined;\n }\n\n // The items whose text the cursor is in, outermost first. Items nest, so one\n // that has ended is always on top. An item ends at the end of a line, the next\n // thing starts on a later one, and no two things start together, so `<` and\n // `<=` read alike in every comparison of offsets below.\n const open: ListItem[] = [];\n const close = (offset: number): void => {\n while (open.length > 0 && (open[open.length - 1] as ListItem).end <= offset) open.pop();\n };\n let next = 0;\n for (const directive of wanted) {\n while (next < listItems.length && (listItems[next] as ListItem).start < directive.start) {\n const item = listItems[next] as ListItem;\n close(item.start);\n open.push(item);\n next += 1;\n }\n close(directive.start);\n\n // Sharing its line with text, a directive is in whatever that text is in.\n const indent = quiet.get(directive.line) ?? Infinity;\n // No deeper than the scanner already went to find where these items end.\n let depth = open.length - 1;\n while (depth >= 0 && (open[depth] as ListItem).indent >= indent) depth -= 1;\n const holder = open[depth];\n\n const below = listItems[next];\n const directlyAbove = below !== undefined && (quietFrom.get(below.line) ?? Infinity) <= directive.start;\n if (directlyAbove && (holder === undefined || below.start < holder.end)) {\n bound.set(below, directive);\n continue;\n }\n // Flush under an item and above no other, it continues the item as any\n // unindented line does.\n const written = holder ?? open[open.length - 1];\n if (written !== undefined) bound.set(written, directive);\n }\n return bound;\n}\n"]}
1
+ {"version":3,"file":"directives.js","sourceRoot":"","sources":["../src/directives.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Explicit annotations, written as HTML comments.\n *\n * Inference covers the overwhelming majority of real documents, but inference\n * that cannot be overridden is a trap: the first time it guesses wrong about a\n * document somebody cannot restructure, the whole tool gets switched off. These\n * directives are the escape hatch, and they are HTML comments so they stay\n * invisible in GitHub, MkDocs, Docusaurus and every other renderer.\n *\n * ```md\n * <!-- @spec-node id=\"ADR-0007\" status=\"accepted\" aliases=\"adr-7, sharding\" -->\n * <!-- @spec-item id=\"shard-key\" state=\"narrowed\" -->\n * <!-- @spec-edge kind=\"delegates-to\" to=\"ADR-0011#scope\" -->\n * <!-- @spec-ignore -->\n * ```\n *\n * A directive always wins over anything inferred, and says so in the report, so\n * an override is visible rather than mysterious.\n */\n\nimport { isMarkdownLine, type HtmlComment, type ListItem, type ScannedDocument } from './markdown.js';\n\nexport type DirectiveName = 'spec-node' | 'spec-item' | 'spec-edge' | 'spec-ignore' | 'spec-history';\n\nconst KNOWN: ReadonlySet<string> = new Set<DirectiveName>([\n 'spec-node',\n 'spec-item',\n 'spec-edge',\n 'spec-ignore',\n 'spec-history',\n]);\n\nexport interface Attribute {\n readonly value: string;\n /** Absolute offset of the value text, quotes excluded. */\n readonly start: number;\n readonly end: number;\n}\n\nexport interface Directive {\n readonly name: DirectiveName;\n readonly attributes: ReadonlyMap<string, Attribute>;\n /** Offset of the enclosing comment. */\n readonly start: number;\n readonly end: number;\n readonly line: number;\n /** Attribute names that were written but are not part of this directive. */\n readonly unknownAttributes: readonly string[];\n}\n\nconst DIRECTIVE_HEAD = /^\\s*@([a-z][a-z0-9-]*)/;\nconst ATTRIBUTE = /([A-Za-z_][A-Za-z0-9_-]*)\\s*(?:=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s\"'<>]+)))?/g;\n\n/** Attributes each directive understands. Anything else is reported, not ignored. */\nconst SCHEMA: Readonly<Record<DirectiveName, readonly string[]>> = {\n 'spec-node': ['id', 'status', 'title', 'aliases', 'kind'],\n // Takes no attributes: the file either is a record or it is not. A mutant\n // that lists `Stryker was here` lists no name: one never holds a space.\n 'spec-history': [],\n 'spec-item': ['id', 'state', 'title', 'owner', 'note'],\n 'spec-edge': ['kind', 'to', 'from', 'reason'],\n 'spec-ignore': ['reason'],\n};\n\n/**\n * Reads every spec-graph directive out of a document's HTML comments.\n *\n * Only out of a comment that closes. One that opens a line and never does runs\n * to the end of the document, and read as a directive, a stray\n * `<!-- @spec-ignore` would drop the file, and a stray `<!-- @spec-node` would\n * take every `name=value` in the rest of it as an attribute.\n */\nexport function parseDirectives(comments: readonly HtmlComment[]): Directive[] {\n const out: Directive[] = [];\n for (const comment of comments) {\n if (comment.closed === false) continue;\n const head = DIRECTIVE_HEAD.exec(comment.inner);\n if (!head) continue;\n const name = head[1] as string;\n if (!KNOWN.has(name)) continue;\n\n const directiveName = name as DirectiveName;\n // The head is anchored, so it starts where the comment's text does.\n const bodyStart = (head[0] as string).length;\n const body = comment.inner.slice(bodyStart);\n const attributes = new Map<string, Attribute>();\n const unknown: string[] = [];\n const allowed = SCHEMA[directiveName];\n\n for (const m of body.matchAll(ATTRIBUTE)) {\n const key = (m[1] as string).toLowerCase();\n const quoted = m[3] ?? m[4];\n const bare = m[5];\n // A bare attribute is a flag: `<!-- @spec-ignore reason -->`.\n const value = quoted ?? bare ?? 'true';\n const raw = m[2];\n const valueOffset =\n raw === undefined\n ? (m.index ?? 0) + (m[0] as string).length\n : (m.index ?? 0) + (m[0] as string).indexOf(raw) + (quoted === undefined ? 0 : 1);\n const start = comment.innerStart + bodyStart + valueOffset;\n\n if (!allowed.includes(key)) unknown.push(key);\n attributes.set(key, { value, start, end: start + value.length });\n }\n\n out.push({\n name: directiveName,\n attributes,\n start: comment.start,\n end: comment.end,\n line: comment.line,\n unknownAttributes: unknown,\n });\n }\n return out;\n}\n\n/** Reads an attribute, trimmed. Returns `null` when absent or blank. */\nexport function attr(directive: Directive, name: string): Attribute | null {\n const found = directive.attributes.get(name);\n if (!found) return null;\n const trimmed = found.value.trim();\n if (trimmed.length === 0) return null;\n const offset = found.value.indexOf(trimmed);\n return { value: trimmed, start: found.start + offset, end: found.start + offset + trimmed.length };\n}\n\n/** Splits a comma- or whitespace-separated attribute into its parts. */\nexport function attrList(directive: Directive, name: string): string[] {\n const found = attr(directive, name);\n if (!found) return [];\n // Whitespace is a separator, so no part holds any to trim. The filter drops\n // what a separator at either end leaves, and would drop what each separator\n // in a run leaves too, so splitting at one at a time is equivalent.\n return found.value.split(/[,;\\s]+/).filter((part) => part.length > 0);\n}\n\n/**\n * Finds the directive that annotates a given region.\n *\n * A `@spec-item` directive may sit on the line above the item, on the same\n * line, or inside the item body. All three read naturally in the source, so all\n * three are accepted, with the closest preceding directive winning.\n *\n * It answers for one region with no view of the others, so a directive in reach\n * of two items is found for both. Extraction uses `bindItemDirectives`.\n */\nexport function directiveFor(\n directives: readonly Directive[],\n name: DirectiveName,\n region: { start: number; end: number },\n /** How far above the region a directive may sit and still apply. */\n lookBehind: number,\n): Directive | null {\n let best: Directive | null = null;\n for (const directive of directives) {\n if (directive.name !== name) continue;\n const inside = directive.start >= region.start && directive.end <= region.end;\n const above = directive.end <= region.start && region.start - directive.end <= lookBehind;\n if (!inside && !above) continue;\n // Two directives never start at one offset, so `>=` reads the same.\n if (best === null || directive.start > best.start) best = directive;\n }\n return best;\n}\n\n/**\n * Pairs each `@spec-item` directive with the one list item it annotates.\n *\n * A directive is written for one item, so it binds to one: the item directly\n * below it, with nothing but blank lines and other comments between, or else\n * the item it is written in, the innermost where items nest. An item that\n * several bind to takes the last one written.\n *\n * A directive alone on its line is in an item only when indented further than\n * that item's marker, which is how CommonMark reads it. Indented under an item,\n * it annotates that item and not the sibling below; flush between two items, it\n * annotates the second.\n */\nexport function bindItemDirectives(\n scanned: ScannedDocument,\n directives: readonly Directive[],\n): Map<ListItem, Directive> {\n const bound = new Map<ListItem, Directive>();\n const wanted = directives.filter((directive) => directive.name === 'spec-item');\n // Most documents carry none. Sparing them the pass over their lines changes\n // how long this takes and nothing else.\n if (wanted.length === 0) return bound;\n\n const { lines, listItems } = scanned;\n const masked = scanned.masks.structure;\n // The indentation of each line holding nothing but whitespace and comments.\n // Code and raw-text HTML are masked too, so they are told apart by their flags.\n const quiet = new Map<number, number>();\n // For every other line, where the run of quiet lines directly above it began,\n // when there was one.\n const quietFrom = new Map<number, number | undefined>();\n let run: number | undefined;\n for (const line of lines) {\n if (isMarkdownLine(line) && masked.slice(line.contentStart, line.end).trim() === '') {\n quiet.set(line.line, line.indent);\n run ??= line.start;\n continue;\n }\n quietFrom.set(line.line, run);\n run = undefined;\n }\n\n // The items whose text the cursor is in, outermost first. Items nest, so one\n // that has ended is always on top. An item ends at the end of a line, the next\n // thing starts on a later one, and no two things start together, so `<` and\n // `<=` read alike in every comparison of offsets below.\n const open: ListItem[] = [];\n const close = (offset: number): void => {\n while (open.length > 0 && (open[open.length - 1] as ListItem).end <= offset) open.pop();\n };\n let next = 0;\n for (const directive of wanted) {\n while (next < listItems.length && (listItems[next] as ListItem).start < directive.start) {\n const item = listItems[next] as ListItem;\n close(item.start);\n open.push(item);\n next += 1;\n }\n close(directive.start);\n\n // Sharing its line with text, a directive is in whatever that text is in.\n const indent = quiet.get(directive.line) ?? Infinity;\n // No deeper than the scanner already went to find where these items end.\n let depth = open.length - 1;\n while (depth >= 0 && (open[depth] as ListItem).indent >= indent) depth -= 1;\n const holder = open[depth];\n\n const below = listItems[next];\n const directlyAbove = below !== undefined && (quietFrom.get(below.line) ?? Infinity) <= directive.start;\n if (directlyAbove && (holder === undefined || below.start < holder.end)) {\n bound.set(below, directive);\n continue;\n }\n // Flush under an item and above no other, it continues the item as any\n // unindented line does.\n const written = holder ?? open[open.length - 1];\n if (written !== undefined) bound.set(written, directive);\n }\n return bound;\n}\n"]}
package/dist/extract.d.ts CHANGED
@@ -128,19 +128,6 @@ export declare function foldRelationKey(key: string): string;
128
128
  * be shadowed, which is {@link UNINHERITED}.
129
129
  */
130
130
  export declare function inheritableFrontMatter(entries: readonly YamlEntry[]): Readonly<Record<string, string | readonly string[]>>;
131
- /**
132
- * Phrases that give a link its meaning, longest-matching-nearest wins.
133
- *
134
- * Only phrases that genuinely commit the citing document are here. Vague ones
135
- * ("see", "covered by") stay out: a `references` edge is harmless, whereas a
136
- * wrongly inferred `assumes` edge produces a confident stale-premise finding
137
- * about a document that never depended on anything.
138
- */
139
- declare const VERB_RULES: readonly {
140
- kind: EdgeKind;
141
- inverted: boolean;
142
- phrases: readonly string[];
143
- }[];
144
131
  /** Headings under which a link is bookkeeping rather than a commitment. */
145
132
  declare const WEAK_SECTIONS: ReadonlySet<string>;
146
133
  /** Headings whose bullets are obligations even without a checkbox. */
@@ -183,4 +170,4 @@ export interface Classification {
183
170
  */
184
171
  export declare function classifyReference(text: string, start: number, end: number, section: readonly string[]): Classification;
185
172
  /** Exposed for tests and for callers that classify their own sections. */
186
- export { OBLIGATION_SECTIONS, sectionPathAt, VERB_RULES, WEAK_SECTIONS };
173
+ export { OBLIGATION_SECTIONS, sectionPathAt, WEAK_SECTIONS };
package/dist/extract.js CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { attr, attrList, bindItemDirectives, parseDirectives } from './directives.js';
18
18
  import { identify, isDocumentTarget, isExternal, looksLikePath, normaliseRef, parsePrefixedRef, withinOneEdit, ID_KEYS, } from './identity.js';
19
- import { isStatusHeading, negatedAt, phaseFromPath, phaseOf, STATUS_KEYS, supersessionTargetsIn, } from './lifecycle.js';
19
+ import { isStatusHeading, phaseFromPath, phaseOf, STATUS_KEYS, supersessionTargetsIn } from './lifecycle.js';
20
20
  import { anchorsOf, isMarkdownLine, isOnlyComment, referenceLinks, scanMarkdown, slugify, } from './markdown.js';
21
21
  import { findSpecificationRegions, isRegisterTable, opensHeading, regionAt, } from './sections.js';
22
22
  import { resolveItemState } from './state.js';
@@ -253,22 +253,6 @@ const VERB_RULES = [
253
253
  'to be resolved in',
254
254
  'to be handled in',
255
255
  'to be answered in',
256
- // Chinese, Traditional and Simplified, each standing before the
257
- // reference as its English counterpart does: deferred to, handed to,
258
- // left to. `併入`, merged into, hands a question on whole; read as
259
- // `rolled into`, a supersession, a constraint merged into another
260
- // decision's design would retire the document that states it. Nothing
261
- // that wraps the reference, `由 X 決定`: a phrase here only precedes one.
262
- '延後至',
263
- '延后至',
264
- '延至',
265
- '移交至',
266
- '移交給',
267
- '移交给',
268
- '留待',
269
- '交由',
270
- '併入',
271
- '并入',
272
256
  ],
273
257
  },
274
258
  {
@@ -354,22 +338,12 @@ const VERB_WINDOW = 40;
354
338
  * Neither the sort nor the escaping decides anything today, and no test can
355
339
  * pin them: no phrase in the table is another one followed by more words, and
356
340
  * none holds a character a pattern would read. Both are for the next phrase.
357
- *
358
- * An English phrase stands between word boundaries, and a Han character is
359
- * not part of an English word: `此問題deferred to ADR-0002` holds the whole
360
- * phrase, which the boundary read as a letter used to refuse. A Chinese phrase
361
- * has no boundary at all, since Chinese puts no space between words.
362
341
  */
363
342
  const VERB_LOOKUP = new Map(VERB_RULES.flatMap((rule) => rule.phrases.map((phrase) => [phrase, { kind: rule.kind, inverted: rule.inverted }])));
364
- const HAN = /\p{Script=Han}/u;
365
- /** A letter or digit an English phrase may not touch: any but a Han character. */
366
- const WORD_CHARACTER = String.raw `(?!\p{Script=Han})[\p{L}\p{N}]`;
367
- const alternation = (phrases) => [...phrases]
343
+ const VERB_PATTERN = new RegExp(`(?<![\\p{L}\\p{N}])(?:${[...VERB_LOOKUP.keys()]
368
344
  .sort((a, b) => b.length - a.length)
369
345
  .map((phrase) => phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
370
- .join('|');
371
- const VERB_PATTERN = new RegExp(`(?<!${WORD_CHARACTER})(?:${alternation([...VERB_LOOKUP.keys()].filter((phrase) => !HAN.test(phrase)))})(?!${WORD_CHARACTER})` +
372
- `|(?:${alternation([...VERB_LOOKUP.keys()].filter((phrase) => HAN.test(phrase)))})`, 'gu');
346
+ .join('|')})(?![\\p{L}\\p{N}])`, 'gu');
373
347
  /** Headings under which a link is bookkeeping rather than a commitment. */
374
348
  const WEAK_SECTIONS = new Set([
375
349
  'see also',
@@ -392,16 +366,6 @@ const WEAK_SECTIONS = new Set([
392
366
  'more information',
393
367
  'resources',
394
368
  'index',
395
- '參考資料',
396
- '参考资料',
397
- '相關文件',
398
- '相关文件',
399
- '延伸閱讀',
400
- '延伸阅读',
401
- '附錄',
402
- '附录',
403
- '修訂紀錄',
404
- '修订记录',
405
369
  ]);
406
370
  /** Headings whose bullets are obligations even without a checkbox. */
407
371
  const OBLIGATION_SECTIONS = new Set([
@@ -435,14 +399,6 @@ const OBLIGATION_SECTIONS = new Set([
435
399
  'parking lot',
436
400
  'future work',
437
401
  'deferred',
438
- '待決事項',
439
- '待决事项',
440
- '未決問題',
441
- '未决问题',
442
- '待辦事項',
443
- '待办事项',
444
- '後續工作',
445
- '后续工作',
446
402
  ]);
447
403
  /**
448
404
  * Prefixes that look like specification identifiers but never are.
@@ -840,11 +796,11 @@ function readStatus(scanned, byKey, nodeDirective, index, file, regions) {
840
796
  return { raw: null, at: null, phase: 'unknown' };
841
797
  }
842
798
  /** The left cells that make a two-column table's row the document's status. */
843
- const STATUS_TABLE_KEYS = new Set(['status', 'state', '狀態', '状态']);
799
+ const STATUS_TABLE_KEYS = new Set(['status', 'state']);
844
800
  /**
845
801
  * The status a key-value table at the top of a document gives:
846
- * `| 狀態 | 已接受 |`, or `| Status | Accepted |`, as the right cell of the first
847
- * row whose left cell is a status key, the header row included.
802
+ * `| Status | Accepted |`, as the right cell of the first row whose left cell
803
+ * is a status key, the header row included.
848
804
  *
849
805
  * Read only where a document describes itself. A table under a heading below
850
806
  * the title is a legend or a register, and one with more than two columns is
@@ -1252,12 +1208,15 @@ function ownerOf(items, offset) {
1252
1208
  export function classifyReference(text, start, end, section) {
1253
1209
  const before = sentenceBefore(text, start);
1254
1210
  // Left to right, so the last match within the window is the nearest one - and
1255
- // the nearest governing phrase is the one that governs.
1211
+ // the nearest governing phrase is the one that governs. `matchAll`, not a
1212
+ // loop over `exec`: an empty match leaves `lastIndex` where it began, and
1213
+ // `exec` finds it there again for ever, where `matchAll` steps past it. No
1214
+ // phrase is empty, so the pattern never matches empty; a phrase emptied by
1215
+ // mistake then fails the tests rather than hanging them.
1256
1216
  let best = null;
1257
- VERB_PATTERN.lastIndex = 0;
1258
- for (let m = VERB_PATTERN.exec(before); m !== null; m = VERB_PATTERN.exec(before)) {
1217
+ for (const m of before.matchAll(VERB_PATTERN)) {
1259
1218
  const phrase = m[0];
1260
- const end = (m.index ?? 0) + phrase.length;
1219
+ const end = m.index + phrase.length;
1261
1220
  const distance = before.length - end;
1262
1221
  if (distance > VERB_WINDOW)
1263
1222
  continue;
@@ -1265,9 +1224,6 @@ export function classifyReference(text, start, end, section) {
1265
1224
  // negation in there reverses the claim the phrase would otherwise make.
1266
1225
  if (NEGATION.test(before.slice(end)))
1267
1226
  continue;
1268
- // Chinese negates the phrase itself, directly before it: `未移交給`.
1269
- if (negatedAt(before, m.index))
1270
- continue;
1271
1227
  // Always found: the pattern was compiled from this table's own keys, and a
1272
1228
  // lower-cased search matches them only as written. The check is for types.
1273
1229
  const rule = VERB_LOOKUP.get(phrase);
@@ -1309,9 +1265,11 @@ export function classifyReference(text, start, end, section) {
1309
1265
  * following row's link into an `assumes` edge, and so turned every archived
1310
1266
  * document it listed into a stale premise.
1311
1267
  *
1312
- * A Chinese sentence ends at `。?!;`, with a space after it or none, since
1313
- * Chinese writes none. Without it `上次延後至別處。另見 ADR-0002` handed the
1314
- * question to ADR-0002 from the sentence before.
1268
+ * A sentence written in Chinese ends at `。?!;`, with a space after it or
1269
+ * none, since Chinese writes none. A stop only ends a statement sooner, so it
1270
+ * reads fewer relations and never more: without it,
1271
+ * `Deferred to later。另見 ADR-0002` handed the question to ADR-0002 from the
1272
+ * sentence before.
1315
1273
  */
1316
1274
  const STATEMENT_BREAK = /[.?!;]\s|[。?!;]|\n\s*\n|\n\s*[-*+>#]|\|/g;
1317
1275
  /**
@@ -1324,10 +1282,8 @@ const STATEMENT_BREAK = /[.?!;]\s|[。?!;]|\n\s*\n|\n\s*[-*+>#]|\|/g;
1324
1282
  function sentenceBefore(text, start) {
1325
1283
  const window = text.slice(Math.max(0, start - 160), start);
1326
1284
  let cut = 0;
1327
- STATEMENT_BREAK.lastIndex = 0;
1328
- for (let m = STATEMENT_BREAK.exec(window); m !== null; m = STATEMENT_BREAK.exec(window)) {
1329
- cut = (m.index ?? 0) + m[0].length;
1330
- }
1285
+ for (const m of window.matchAll(STATEMENT_BREAK))
1286
+ cut = m.index + m[0].length;
1331
1287
  return window
1332
1288
  .slice(cut)
1333
1289
  .toLowerCase()
@@ -1335,7 +1291,8 @@ function sentenceBefore(text, start) {
1335
1291
  .replace(/\s+/g, ' ');
1336
1292
  }
1337
1293
  /**
1338
- * The statement after a reference, to the first stop, Chinese ones included.
1294
+ * The statement after a reference, to the first stop, the stops of a sentence
1295
+ * written in Chinese included.
1339
1296
  *
1340
1297
  * Only its start is read. A line break matters as a stop, since the trim
1341
1298
  * below would otherwise bring the next line's phrase to the start. A Chinese
@@ -1377,8 +1334,7 @@ function findBareReferences(scanned, links) {
1377
1334
  }
1378
1335
  text += source.slice(cursor);
1379
1336
  const out = [];
1380
- BARE_REF.lastIndex = 0;
1381
- for (let m = BARE_REF.exec(text); m !== null; m = BARE_REF.exec(text)) {
1337
+ for (const m of text.matchAll(BARE_REF)) {
1382
1338
  const written = m[0];
1383
1339
  // The family as resolution will read it, which takes the shortest prefix
1384
1340
  // the number allows: `base64` is `base` and 64. Read off this pattern's
@@ -1452,5 +1408,5 @@ function renderLink(link) {
1452
1408
  }
1453
1409
  }
1454
1410
  /** Exposed for tests and for callers that classify their own sections. */
1455
- export { OBLIGATION_SECTIONS, sectionPathAt, VERB_RULES, WEAK_SECTIONS };
1411
+ export { OBLIGATION_SECTIONS, sectionPathAt, WEAK_SECTIONS };
1456
1412
  //# sourceMappingURL=extract.js.map