@descent-vtt/spec-graph 0.11.1 → 0.12.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 +55 -1
- package/README.md +52 -22
- package/dist/extract.d.ts +1 -1
- package/dist/extract.js +26 -4
- package/dist/extract.js.map +1 -1
- package/dist/identity.js +28 -22
- package/dist/identity.js.map +1 -1
- package/dist/sections.js +28 -24
- package/dist/sections.js.map +1 -1
- package/dist/select.js +27 -16
- package/dist/select.js.map +1 -1
- package/dist/state.js +28 -22
- package/dist/state.js.map +1 -1
- package/dist/yaml.js +4 -4
- package/dist/yaml.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,60 @@ 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.12.1
|
|
15
|
+
|
|
16
|
+
The README's commands gave `npx` the command's bare name, which on npm is
|
|
17
|
+
not this project's package. spec-graph itself is unchanged.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- Every command in the README gives `npx` the package's full name behind
|
|
22
|
+
`--no-install`, as in `npx --no-install @descent-vtt/spec-graph ...`.
|
|
23
|
+
`spec-graph` without the scope belongs to nobody on npm (2026-10-07), so
|
|
24
|
+
anyone can register it.
|
|
25
|
+
`npx` given a name the project has not installed fetches the package of
|
|
26
|
+
that name and runs it, unasked when no terminal is attached: a fresh
|
|
27
|
+
clone, a worktree before `npm ci`, a CI job with no install step.
|
|
28
|
+
The CI examples install first, and [Names](README.md#names) says whose the
|
|
29
|
+
names are; what was measured is in the family's
|
|
30
|
+
[adopting guide](https://github.com/DescentVTT/spec-core/blob/main/docs/adopting.md#names).
|
|
31
|
+
Upgrading: where a CI job or a script gives `npx` the command's name
|
|
32
|
+
alone, write `npx --no-install @descent-vtt/spec-graph`. `--no-install`
|
|
33
|
+
before the bare name is not enough: npm still runs a copy of that name's
|
|
34
|
+
package an earlier fetch left in its cache.
|
|
35
|
+
|
|
36
|
+
## 0.12.0
|
|
37
|
+
|
|
38
|
+
Names every JavaScript object answers to - `constructor`, `__proto__` - are
|
|
39
|
+
read as the unknown names they are in seven places, three of which could
|
|
40
|
+
leave a run that passed refusing or finding something now.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- A node type named `constructor` is refused as any unknown type is: a
|
|
45
|
+
query or a project rule written with it matched nothing in silence, and is
|
|
46
|
+
now a usage error, exit 2. Upgrading: write `document`, `item` or `*`.
|
|
47
|
+
- A table column headed `Constructor` declares no relation, where it made
|
|
48
|
+
the table a register and each cell an edge of no kind. Upgrading: a table
|
|
49
|
+
that was a register only by that column needs a `Status` column or a
|
|
50
|
+
relation the README lists.
|
|
51
|
+
- A `@spec-edge` whose `kind` names no relation is a `references` edge, and
|
|
52
|
+
`--verbose` lists it as a parse problem; any other word drew an edge of a
|
|
53
|
+
kind no rule knows, and one to its own document ended the check with exit
|
|
54
|
+
2 (ADR-0014). Upgrading: write one of the eight relations.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- A front-matter key, a `state=` word or a directory named `constructor` or
|
|
59
|
+
`__proto__` no longer ends a query or a check on a TypeError: the key is
|
|
60
|
+
found where it was written, the state is ignored, and a document under
|
|
61
|
+
such a directory keeps its own id.
|
|
62
|
+
|
|
63
|
+
### Documentation
|
|
64
|
+
|
|
65
|
+
- The README's SARIF example pins the actions the workflows run now, and a
|
|
66
|
+
test holds it to them.
|
|
67
|
+
|
|
14
68
|
## 0.11.1
|
|
15
69
|
|
|
16
70
|
Documentation and robustness: the README's SARIF example is pinned as CI
|
|
@@ -1378,7 +1432,7 @@ reference.
|
|
|
1378
1432
|
`$GITHUB_STEP_SUMMARY` or a pull-request comment:
|
|
1379
1433
|
|
|
1380
1434
|
```yaml
|
|
1381
|
-
- run: npx spec-graph --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
1435
|
+
- run: npx --no-install @descent-vtt/spec-graph --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
1382
1436
|
```
|
|
1383
1437
|
|
|
1384
1438
|
SARIF puts a finding on the line that caused it, which is where somebody fixing
|
package/README.md
CHANGED
|
@@ -53,22 +53,44 @@ document and obvious when you can query all of them at once.
|
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
55
|
npm install --save-dev @descent-vtt/spec-graph
|
|
56
|
-
npx spec-graph
|
|
56
|
+
npx --no-install @descent-vtt/spec-graph
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
No configuration, no annotations, no migration. It reads what your repository
|
|
60
60
|
already looks like:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
npx spec-graph "docs/**/*.md"
|
|
63
|
+
npx --no-install @descent-vtt/spec-graph "docs/**/*.md"
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
is plain `spec-graph`. Without one, `npx` needs the full name:
|
|
66
|
+
## Names
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
The package is `@descent-vtt/spec-graph`, and the command it installs is
|
|
69
|
+
`spec-graph`. The name without the scope is not this project: on npm,
|
|
70
|
+
`spec-graph` belonged to nobody on 2026-10-07, and whoever registers it decides
|
|
71
|
+
what it runs.
|
|
72
|
+
|
|
73
|
+
`npx` fetches and runs the package of whatever name it is given when the
|
|
74
|
+
project has none installed - a fresh clone, a worktree before `npm ci`, a CI job
|
|
75
|
+
without the install step - and without a terminal it does not ask first. So
|
|
76
|
+
give `npx` the full name:
|
|
77
|
+
|
|
78
|
+
- `npx --no-install @descent-vtt/spec-graph` in a project that installed it: it
|
|
79
|
+
runs that install, the version the lockfile pins, and where there is none it
|
|
80
|
+
stops with an error that names this package.
|
|
81
|
+
- `npx @descent-vtt/spec-graph`, without `--no-install`, where nothing is
|
|
82
|
+
installed: it fetches this package and runs it.
|
|
83
|
+
|
|
84
|
+
<!-- bare-name: the two forms in the next sentence are shown as what not to write -->
|
|
85
|
+
Never `npx spec-graph`, and not `npx --no-install spec-graph` either:
|
|
86
|
+
`--no-install` stops a download, and npm still runs a copy of the bare name's
|
|
87
|
+
package that an earlier fetch left in its cache. The family's
|
|
88
|
+
[adopting guide](https://github.com/DescentVTT/spec-core/blob/main/docs/adopting.md#names)
|
|
89
|
+
has what was measured, under npm 10, 11 and 12.
|
|
90
|
+
|
|
91
|
+
Where this page writes the command alone, as in `spec-graph query ...`, it is
|
|
92
|
+
what a `package.json` script holds - npm fetches nothing for a script - and what
|
|
93
|
+
follows `npx --no-install @descent-vtt/` at a prompt.
|
|
72
94
|
|
|
73
95
|
## What it checks
|
|
74
96
|
|
|
@@ -552,6 +574,8 @@ Each takes these attributes. Any other is a parse problem, listed by
|
|
|
552
574
|
|
|
553
575
|
- `reason`, `note`, `owner` and a node's `kind` are for whoever reads the
|
|
554
576
|
source; spec-graph reads nothing from them.
|
|
577
|
+
- An edge's `kind` is one of the relations a selector steps along. Any other
|
|
578
|
+
word is a parse problem too, and the edge is read as `references`.
|
|
555
579
|
- `aliases` splits at commas, semicolons and spaces.
|
|
556
580
|
- A value is quoted with either quote mark, or bare when it holds no space or
|
|
557
581
|
quote, and an attribute with no value is a flag.
|
|
@@ -628,7 +652,7 @@ gate without writing any code:
|
|
|
628
652
|
```yaml
|
|
629
653
|
- name: No PRD may depend on a draft decision
|
|
630
654
|
run: |
|
|
631
|
-
! npx spec-graph query 'document[path^=docs/prd] -depends-on-> document[phase=draft]'
|
|
655
|
+
! npx --no-install @descent-vtt/spec-graph query 'document[path^=docs/prd] -depends-on-> document[phase=draft]'
|
|
632
656
|
```
|
|
633
657
|
|
|
634
658
|
That shell line is a rule wearing a disguise. Give it a name and a sentence and
|
|
@@ -1031,7 +1055,7 @@ loop that would justify a resident process is one line of shell, and it belongs
|
|
|
1031
1055
|
to whatever the developer already uses rather than to this tool:
|
|
1032
1056
|
|
|
1033
1057
|
```bash
|
|
1034
|
-
npx spec-graph --format json | jq -r '.summary'
|
|
1058
|
+
npx --no-install @descent-vtt/spec-graph --format json | jq -r '.summary'
|
|
1035
1059
|
```
|
|
1036
1060
|
|
|
1037
1061
|
## In CI
|
|
@@ -1039,6 +1063,11 @@ npx spec-graph --format json | jq -r '.summary'
|
|
|
1039
1063
|
The exit code is the contract: `0` clean, `1` findings, `2` the tool could not
|
|
1040
1064
|
run. That is all most pipelines need.
|
|
1041
1065
|
|
|
1066
|
+
Every job below installs the project first, with `npm ci`, and gives `npx` the
|
|
1067
|
+
package's [full name](#names) behind `--no-install`: a job that lost its install
|
|
1068
|
+
step then stops, where the command's name alone would fetch whatever package
|
|
1069
|
+
has that name.
|
|
1070
|
+
|
|
1042
1071
|
Beyond that, a format puts the findings where somebody will look:
|
|
1043
1072
|
|
|
1044
1073
|
| Format | Where the findings land |
|
|
@@ -1061,16 +1090,16 @@ permissions:
|
|
|
1061
1090
|
contents: read
|
|
1062
1091
|
security-events: write
|
|
1063
1092
|
steps:
|
|
1064
|
-
- uses: actions/checkout@
|
|
1093
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
1065
1094
|
with:
|
|
1066
1095
|
persist-credentials: false
|
|
1067
|
-
- uses: actions/setup-node@
|
|
1096
|
+
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
1068
1097
|
with:
|
|
1069
1098
|
node-version: '24'
|
|
1070
1099
|
- run: npm ci
|
|
1071
|
-
- run: npx spec-graph --format sarif > spec-graph.sarif
|
|
1100
|
+
- run: npx --no-install @descent-vtt/spec-graph --format sarif > spec-graph.sarif
|
|
1072
1101
|
continue-on-error: true
|
|
1073
|
-
- uses: github/codeql-action/upload-sarif@
|
|
1102
|
+
- uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
|
1074
1103
|
with:
|
|
1075
1104
|
sarif_file: spec-graph.sarif
|
|
1076
1105
|
```
|
|
@@ -1091,7 +1120,7 @@ command per finding, and the job's log turns each into an annotation on the
|
|
|
1091
1120
|
diff, with no upload step:
|
|
1092
1121
|
|
|
1093
1122
|
```yaml
|
|
1094
|
-
- run: npx spec-graph --format github
|
|
1123
|
+
- run: npx --no-install @descent-vtt/spec-graph --format github
|
|
1095
1124
|
```
|
|
1096
1125
|
|
|
1097
1126
|
```text
|
|
@@ -1115,7 +1144,8 @@ resolved:
|
|
|
1115
1144
|
```yaml
|
|
1116
1145
|
spec-graph:
|
|
1117
1146
|
script:
|
|
1118
|
-
-
|
|
1147
|
+
- npm ci
|
|
1148
|
+
- npx --no-install @descent-vtt/spec-graph --format gitlab > gl-code-quality-report.json
|
|
1119
1149
|
artifacts:
|
|
1120
1150
|
when: always
|
|
1121
1151
|
reports:
|
|
@@ -1146,7 +1176,7 @@ actually look:
|
|
|
1146
1176
|
```yaml
|
|
1147
1177
|
- name: Summarise the specification graph
|
|
1148
1178
|
if: always()
|
|
1149
|
-
run: npx spec-graph --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
1179
|
+
run: npx --no-install @descent-vtt/spec-graph --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
1150
1180
|
```
|
|
1151
1181
|
|
|
1152
1182
|
It leads with the verdict, counts the corpus, gives every finding its hint, and
|
|
@@ -1161,7 +1191,7 @@ per-file detail that SARIF and Code Quality have nowhere to put.
|
|
|
1161
1191
|
|
|
1162
1192
|
```yaml
|
|
1163
1193
|
- name: Check the specification graph
|
|
1164
|
-
run: npx spec-graph "docs/**/*.md" --format json > spec-graph.json
|
|
1194
|
+
run: npx --no-install @descent-vtt/spec-graph "docs/**/*.md" --format json > spec-graph.json
|
|
1165
1195
|
```
|
|
1166
1196
|
|
|
1167
1197
|
The JSON report is versioned, flat, and carries start and end positions for
|
|
@@ -1197,9 +1227,9 @@ binary, so CI makes one from the base branch and one from the pull request:
|
|
|
1197
1227
|
|
|
1198
1228
|
```yaml
|
|
1199
1229
|
- run: git fetch --depth 1 origin "$GITHUB_BASE_REF" && git worktree add ../base FETCH_HEAD
|
|
1200
|
-
- run: npx spec-graph graph --root ../base --graph-format json > base.json
|
|
1201
|
-
- run: npx spec-graph graph --graph-format json > head.json
|
|
1202
|
-
- run: npx spec-graph diff base.json head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
1230
|
+
- run: npx --no-install @descent-vtt/spec-graph graph --root ../base --graph-format json > base.json
|
|
1231
|
+
- run: npx --no-install @descent-vtt/spec-graph graph --graph-format json > head.json
|
|
1232
|
+
- run: npx --no-install @descent-vtt/spec-graph diff base.json head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
|
|
1203
1233
|
```
|
|
1204
1234
|
|
|
1205
1235
|
It names only what it can tell apart:
|
|
@@ -1287,9 +1317,9 @@ tools share, whose code is copied into `src/vendor/` and checked by hash rather
|
|
|
1287
1317
|
than installed. So the whole package is auditable in an afternoon — and nothing
|
|
1288
1318
|
in the pipeline can take longer than its input.
|
|
1289
1319
|
|
|
1290
|
-
**Verified, not just covered.** 1,
|
|
1320
|
+
**Verified, not just covered.** 1,995 tests; 99.4% statement and 99.8% line
|
|
1291
1321
|
coverage. Coverage says a line ran, so the suite is also held to a mutation
|
|
1292
|
-
score - 96.
|
|
1322
|
+
score - 96.38% over 8,539 mutants, measured by CI's full sweep, and a build
|
|
1293
1323
|
fails under 93 - because a vocabulary entry or a boundary
|
|
1294
1324
|
condition can be weakened by an ordinary-looking refactor without a single test
|
|
1295
1325
|
going red. [ADR-0007](https://github.com/DescentVTT/spec-graph/blob/main/docs/adr/0007-mutation-testing.md) is straight about what
|
package/dist/extract.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type DocumentIdentity } from './identity.js';
|
|
18
18
|
import { type ScannedDocument } from './markdown.js';
|
|
19
|
-
import type
|
|
19
|
+
import { type DocumentNode, type EdgeKind, type EdgeOrigin, type ItemNode, type MisreadKey, type ParseProblem, type SourceRef } from './types.js';
|
|
20
20
|
import { type YamlEntry } from './yaml.js';
|
|
21
21
|
/**
|
|
22
22
|
* A relation that has been read but not yet resolved to a target node.
|
package/dist/extract.js
CHANGED
|
@@ -21,6 +21,7 @@ import { anchorsOf, isMarkdownLine, isOnlyComment, referenceLinks, scanMarkdown,
|
|
|
21
21
|
import { findSpecificationRegions, isRegisterTable, opensHeading, regionAt, } from './sections.js';
|
|
22
22
|
import { resolveItemState } from './state.js';
|
|
23
23
|
import { refOf } from './source.js';
|
|
24
|
+
import { EDGE_KINDS, } from './types.js';
|
|
24
25
|
import { readEntries, toRecord, valuesOf } from './yaml.js';
|
|
25
26
|
/* -------------------------------------------------------------------------- */
|
|
26
27
|
/* Vocabulary */
|
|
@@ -513,6 +514,13 @@ export function extractDocument(input) {
|
|
|
513
514
|
at: at(directive.start, directive.end),
|
|
514
515
|
});
|
|
515
516
|
}
|
|
517
|
+
const kind = directive.name === 'spec-edge' ? attr(directive, 'kind') : null;
|
|
518
|
+
if (kind !== null && !isEdgeKind(kind.value)) {
|
|
519
|
+
problems.push({
|
|
520
|
+
message: `unknown relation "${kind.value}" on @spec-edge, read as references - expected one of: ${EDGE_KINDS.join(', ')}`,
|
|
521
|
+
at: at(kind.start, kind.end),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
516
524
|
}
|
|
517
525
|
const { entries, problems: unread } = readEntries(scanned);
|
|
518
526
|
for (const problem of unread) {
|
|
@@ -981,6 +989,17 @@ function summarise(text) {
|
|
|
981
989
|
function isClaimed(claimed, offset) {
|
|
982
990
|
return claimed.some((range) => offset >= range.start && offset < range.end);
|
|
983
991
|
}
|
|
992
|
+
/**
|
|
993
|
+
* Whether a word a `@spec-edge` gives as its kind is a relation.
|
|
994
|
+
*
|
|
995
|
+
* Asked of the list, as a selector's relation is: the word was once taken as
|
|
996
|
+
* it stood, so `kind="blocks"` made an edge of a kind no rule knows, a
|
|
997
|
+
* document with such an edge to itself ended the run, and `kind="constructor"`
|
|
998
|
+
* did not, since the table of kinds is an object and answers to that name.
|
|
999
|
+
*/
|
|
1000
|
+
function isEdgeKind(word) {
|
|
1001
|
+
return EDGE_KINDS.includes(word);
|
|
1002
|
+
}
|
|
984
1003
|
function extractReferences(context) {
|
|
985
1004
|
const { scanned, directives, document, documentAt, items, byKey, status, claimed, file, index } = context;
|
|
986
1005
|
// This list and the one `withoutOverlaps` builds from it are only ever read
|
|
@@ -1035,11 +1054,14 @@ function extractReferences(context) {
|
|
|
1035
1054
|
for (const directive of directives) {
|
|
1036
1055
|
if (directive.name !== 'spec-edge')
|
|
1037
1056
|
continue;
|
|
1038
|
-
const
|
|
1057
|
+
const written = attr(directive, 'kind')?.value;
|
|
1039
1058
|
const to = attr(directive, 'to');
|
|
1040
1059
|
const from = attr(directive, 'from');
|
|
1041
|
-
if (!
|
|
1060
|
+
if (!written)
|
|
1042
1061
|
continue;
|
|
1062
|
+
// A word that names no relation is still a link to somewhere, as a cell
|
|
1063
|
+
// under a column that names none is: a citation, and no more than one.
|
|
1064
|
+
const kind = isEdgeKind(written) ? written : 'references';
|
|
1043
1065
|
const owner = ownerOf(items, directive.start) ?? documentAt(directive.start).id;
|
|
1044
1066
|
if (to) {
|
|
1045
1067
|
out.push({
|
|
@@ -1048,7 +1070,7 @@ function extractReferences(context) {
|
|
|
1048
1070
|
target: to.value,
|
|
1049
1071
|
origin: 'directive',
|
|
1050
1072
|
declaredAt: at(to.start, to.end),
|
|
1051
|
-
raw: `@spec-edge kind="${
|
|
1073
|
+
raw: `@spec-edge kind="${written}" to="${to.value}"`,
|
|
1052
1074
|
inverted: false,
|
|
1053
1075
|
opportunistic: false,
|
|
1054
1076
|
});
|
|
@@ -1060,7 +1082,7 @@ function extractReferences(context) {
|
|
|
1060
1082
|
target: from.value,
|
|
1061
1083
|
origin: 'directive',
|
|
1062
1084
|
declaredAt: at(from.start, from.end),
|
|
1063
|
-
raw: `@spec-edge kind="${
|
|
1085
|
+
raw: `@spec-edge kind="${written}" from="${from.value}"`,
|
|
1064
1086
|
inverted: true,
|
|
1065
1087
|
opportunistic: false,
|
|
1066
1088
|
});
|