@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 +30 -0
- package/README.md +33 -71
- package/dist/directives.js +1 -2
- package/dist/directives.js.map +1 -1
- package/dist/extract.d.ts +1 -14
- package/dist/extract.js +24 -68
- package/dist/extract.js.map +1 -1
- package/dist/lifecycle.d.ts +1 -11
- package/dist/lifecycle.js +31 -187
- package/dist/lifecycle.js.map +1 -1
- package/dist/sections.js +6 -7
- package/dist/sections.js.map +1 -1
- package/dist/state.d.ts +12 -0
- package/dist/state.js +20 -20
- package/dist/state.js.map +1 -1
- package/dist/yaml.js +2 -2
- package/dist/yaml.js.map +1 -1
- package/package.json +1 -1
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,
|
|
137
|
-
(
|
|
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`,
|
|
199
|
-
| A section, under the heading, with or without a colon
|
|
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
|
-
|
|
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
|
|
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
|
|
280
|
-
| `satisfied` | `[x]` `[X]` `[+]` | `resolved`, `answered`, `decided`, `settled`, `done`, `complete`, `completed`, `closed`, `fixed`, `shipped`, `addressed`, `implemented`, `solved`, `confirmed
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
package/dist/directives.js
CHANGED
|
@@ -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
|
|
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];
|
package/dist/directives.js.map
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
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
|
-
* `|
|
|
847
|
-
*
|
|
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
|
|
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 =
|
|
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
|
|
1313
|
-
* Chinese writes none.
|
|
1314
|
-
*
|
|
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
|
-
|
|
1328
|
-
|
|
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,
|
|
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
|
|
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,
|
|
1411
|
+
export { OBLIGATION_SECTIONS, sectionPathAt, WEAK_SECTIONS };
|
|
1456
1412
|
//# sourceMappingURL=extract.js.map
|