@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 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
- **The package is scoped; the command is not.** After a local install the binary
67
- is plain `spec-graph`. Without one, `npx` needs the full name:
66
+ ## Names
68
67
 
69
- ```bash
70
- npx @descent-vtt/spec-graph "docs/**/*.md"
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@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
1093
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
1065
1094
  with:
1066
1095
  persist-credentials: false
1067
- - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
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@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
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
- - npx spec-graph --format gitlab > gl-code-quality-report.json
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,886 tests; 99.4% statement and 99.8% line
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.31% over 8,385 mutants, measured by CI's full sweep, and a build
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 { DocumentNode, EdgeKind, EdgeOrigin, ItemNode, MisreadKey, ParseProblem, SourceRef } from './types.js';
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 kind = attr(directive, 'kind')?.value;
1057
+ const written = attr(directive, 'kind')?.value;
1039
1058
  const to = attr(directive, 'to');
1040
1059
  const from = attr(directive, 'from');
1041
- if (!kind)
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="${kind}" to="${to.value}"`,
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="${kind}" from="${from.value}"`,
1085
+ raw: `@spec-edge kind="${written}" from="${from.value}"`,
1064
1086
  inverted: true,
1065
1087
  opportunistic: false,
1066
1088
  });