@descent-vtt/spec-graph 0.7.0 → 0.9.0

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +477 -0
  2. package/README.md +109 -17
  3. package/dist/cli.d.ts +2 -2
  4. package/dist/cli.js +38 -19
  5. package/dist/cli.js.map +1 -1
  6. package/dist/directives.d.ts +9 -2
  7. package/dist/directives.js +15 -4
  8. package/dist/directives.js.map +1 -1
  9. package/dist/extract.d.ts +1 -1
  10. package/dist/extract.js +50 -16
  11. package/dist/extract.js.map +1 -1
  12. package/dist/glob.d.ts +44 -44
  13. package/dist/glob.js +152 -116
  14. package/dist/glob.js.map +1 -1
  15. package/dist/identity.d.ts +12 -4
  16. package/dist/identity.js +32 -8
  17. package/dist/identity.js.map +1 -1
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.js +4 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/lifecycle.js +10 -2
  22. package/dist/lifecycle.js.map +1 -1
  23. package/dist/markdown.d.ts +53 -161
  24. package/dist/markdown.js +42 -864
  25. package/dist/markdown.js.map +1 -1
  26. package/dist/report.d.ts +23 -0
  27. package/dist/report.js +80 -0
  28. package/dist/report.js.map +1 -1
  29. package/dist/rules.js +48 -14
  30. package/dist/rules.js.map +1 -1
  31. package/dist/sections.d.ts +1 -1
  32. package/dist/sections.js +18 -4
  33. package/dist/sections.js.map +1 -1
  34. package/dist/select.d.ts +2 -2
  35. package/dist/select.js +3 -3
  36. package/dist/select.js.map +1 -1
  37. package/dist/source.d.ts +6 -19
  38. package/dist/source.js +4 -70
  39. package/dist/source.js.map +1 -1
  40. package/dist/types.d.ts +23 -3
  41. package/dist/types.js +1 -1
  42. package/dist/types.js.map +1 -1
  43. package/dist/vendor/spec-core/markdown/frontmatter.d.ts +103 -0
  44. package/dist/vendor/spec-core/markdown/frontmatter.js +459 -0
  45. package/dist/vendor/spec-core/markdown/frontmatter.js.map +1 -0
  46. package/dist/vendor/spec-core/markdown/headings.d.ts +45 -0
  47. package/dist/vendor/spec-core/markdown/headings.js +199 -0
  48. package/dist/vendor/spec-core/markdown/headings.js.map +1 -0
  49. package/dist/vendor/spec-core/markdown/index.d.ts +5 -0
  50. package/dist/vendor/spec-core/markdown/index.js +4 -0
  51. package/dist/vendor/spec-core/markdown/index.js.map +1 -0
  52. package/dist/vendor/spec-core/markdown/layout.d.ts +39 -0
  53. package/dist/vendor/spec-core/markdown/layout.js +28 -0
  54. package/dist/vendor/spec-core/markdown/layout.js.map +1 -0
  55. package/dist/vendor/spec-core/markdown/links.d.ts +22 -0
  56. package/dist/vendor/spec-core/markdown/links.js +364 -0
  57. package/dist/vendor/spec-core/markdown/links.js.map +1 -0
  58. package/dist/vendor/spec-core/markdown/lists.d.ts +22 -0
  59. package/dist/vendor/spec-core/markdown/lists.js +127 -0
  60. package/dist/vendor/spec-core/markdown/lists.js.map +1 -0
  61. package/dist/vendor/spec-core/markdown/scan.d.ts +36 -0
  62. package/dist/vendor/spec-core/markdown/scan.js +439 -0
  63. package/dist/vendor/spec-core/markdown/scan.js.map +1 -0
  64. package/dist/vendor/spec-core/markdown/syntax.d.ts +75 -0
  65. package/dist/vendor/spec-core/markdown/syntax.js +181 -0
  66. package/dist/vendor/spec-core/markdown/syntax.js.map +1 -0
  67. package/dist/vendor/spec-core/markdown/tables.d.ts +18 -0
  68. package/dist/vendor/spec-core/markdown/tables.js +86 -0
  69. package/dist/vendor/spec-core/markdown/tables.js.map +1 -0
  70. package/dist/vendor/spec-core/markdown/types.d.ts +287 -0
  71. package/dist/vendor/spec-core/markdown/types.js +9 -0
  72. package/dist/vendor/spec-core/markdown/types.js.map +1 -0
  73. package/dist/vendor/spec-core/path/index.d.ts +1 -0
  74. package/dist/vendor/spec-core/path/index.js +2 -0
  75. package/dist/vendor/spec-core/path/index.js.map +1 -0
  76. package/dist/vendor/spec-core/path/posix.d.ts +83 -0
  77. package/dist/vendor/spec-core/path/posix.js +150 -0
  78. package/dist/vendor/spec-core/path/posix.js.map +1 -0
  79. package/dist/vendor/spec-core/pattern/automaton.d.ts +129 -0
  80. package/dist/vendor/spec-core/pattern/automaton.js +340 -0
  81. package/dist/vendor/spec-core/pattern/automaton.js.map +1 -0
  82. package/dist/vendor/spec-core/pattern/charset.d.ts +58 -0
  83. package/dist/vendor/spec-core/pattern/charset.js +116 -0
  84. package/dist/vendor/spec-core/pattern/charset.js.map +1 -0
  85. package/dist/vendor/spec-core/pattern/glob.d.ts +121 -0
  86. package/dist/vendor/spec-core/pattern/glob.js +481 -0
  87. package/dist/vendor/spec-core/pattern/glob.js.map +1 -0
  88. package/dist/vendor/spec-core/pattern/index.d.ts +6 -0
  89. package/dist/vendor/spec-core/pattern/index.js +4 -0
  90. package/dist/vendor/spec-core/pattern/index.js.map +1 -0
  91. package/dist/{regex.d.ts → vendor/spec-core/pattern/regex.d.ts} +20 -29
  92. package/dist/{regex.js → vendor/spec-core/pattern/regex.js} +40 -49
  93. package/dist/vendor/spec-core/pattern/regex.js.map +1 -0
  94. package/dist/vendor/spec-core/text/index.d.ts +2 -0
  95. package/dist/vendor/spec-core/text/index.js +2 -0
  96. package/dist/vendor/spec-core/text/index.js.map +1 -0
  97. package/dist/vendor/spec-core/text/lines.d.ts +72 -0
  98. package/dist/vendor/spec-core/text/lines.js +156 -0
  99. package/dist/vendor/spec-core/text/lines.js.map +1 -0
  100. package/dist/yaml.d.ts +44 -12
  101. package/dist/yaml.js +115 -206
  102. package/dist/yaml.js.map +1 -1
  103. package/package.json +3 -2
  104. package/src/vendor/spec-core/LICENSE +21 -0
  105. package/src/vendor/spec-core/README.md +8 -0
  106. package/dist/regex.js.map +0 -1
package/README.md CHANGED
@@ -123,12 +123,28 @@ onto four phases, chosen so one question can be answered from the phase alone:
123
123
  | `draft` | `proposed`, `provisional`, `wip`, `in-review`, `experimental`, … | yes |
124
124
  | `active` | `accepted`, `implementable`, `implemented`, `merged`, `adopted`, … | yes |
125
125
  | `frozen` | `final`, `ratified`, `locked`, `published`, … | **no** |
126
- | `retired` | `superseded`, `deprecated`, `rejected`, `withdrawn`, `archived`, … | **no** |
126
+ | `retired` | `superseded`, `deprecated`, `rejected`, `withdrawn`, … | **no** |
127
127
 
128
128
  The status is found wherever your team writes it — front matter (`status:`,
129
129
  `state:`, `stage:`), a `## Status` section, `**Accepted** (2026-03-01) ✅`, or a
130
130
  `docs/adr/archive/` directory. `Accepted, later superseded by ADR-0009` is
131
- retired: retirement is terminal, so a retirement word anywhere wins.
131
+ retired: retirement is terminal, so a retirement word anywhere wins. Front
132
+ matter is read as YAML reads it, and a value YAML would not read -
133
+ `status: Superseded by ADR-9: see notes`, where a plain value cannot hold `: ` -
134
+ is left out rather than guessed at. Every run says how many statuses, ids and
135
+ relations it left out that way, and `--verbose` says where and why: quote the
136
+ value and it is read.
137
+
138
+ `archived` is none of the four. It is what spec-brief writes on a round of work
139
+ it has closed, and every spec-* tool reads it as a **record**: depending on it
140
+ is normal, its unticked boxes are not anyone's work, its links are still
141
+ checked, and new work handed to it is still a ghost handover. See
142
+ [Journals, changelogs and minutes](#journals-changelogs-and-minutes). Unlike a
143
+ journal, it still answers for what it claims: a supersession it declares, a
144
+ cycle it closes and a link to itself are reported as they would be anywhere.
145
+ To retire a decision, say `superseded` or `deprecated` -
146
+ `archived, superseded by ADR-0009` is retired - and a document under an
147
+ `archive/` directory that says nothing about itself is still retired.
132
148
 
133
149
  An unrecognised status becomes `unknown`, and rules treat it permissively. It
134
150
  never guesses.
@@ -227,6 +243,13 @@ header you wrote:
227
243
 
228
244
  Findings point at the declaring cell, not at the file.
229
245
 
246
+ A row is named by its ID column, and a title is only a title. An identifier a
247
+ title opens with belongs to the document the row is *about* — an issue reading
248
+ `| OI-V-05 | ADR-040's enforcement point has no browser test |` is filed under
249
+ `OI-V-05`, and citations of `ADR-040` still reach `ADR-040`. The same holds for
250
+ a whole file: an explicit `id:`, then the file name, then the H1, and the H1 is
251
+ read as a name only when neither of the others gave one.
252
+
230
253
  The register itself stays in the graph as the thing that holds them, so
231
254
  `spec-graph query 'document[id=register] -contains-> document'` lists what is
232
255
  inside it.
@@ -392,10 +415,11 @@ against a fifty-four-character title takes `RegExp` 103 seconds and this 13
392
415
  microseconds. It reads the usual syntax minus backreferences and lookaround,
393
416
  which are not regular; both are refused when the selector is read, with the
394
417
  character pointed at. Globs - the patterns to check, `--ignore`, `--ignore-ref`,
395
- `--history` - are matched by the same automaton, because a glob with three stars
396
- in it is
397
- enough to keep `RegExp` busy for two minutes over a long reference target. See
398
- [ADR-0017](docs/adr/0017-a-predicate-must-finish.md).
418
+ `--history` - are matched by an automaton too, because a glob with three stars
419
+ in it is enough to keep `RegExp` busy for two minutes over a long reference
420
+ target. See [ADR-0017](docs/adr/0017-a-predicate-must-finish.md). Both automata
421
+ are spec-core's, the library the spec-* tools share, copied into this package
422
+ rather than installed.
399
423
 
400
424
  On a register, a region answers its file's front matter for every descriptive
401
425
  key — `fm.owner` on a decision inside a register is the register's owner — while
@@ -501,7 +525,10 @@ had:
501
525
  { "historyPatterns": ["**/JOURNAL_*.md", "archive/**"] }
502
526
  ```
503
527
 
504
- or, for one file, `<!-- @spec-history -->` at the top of it.
528
+ or, for one file, `<!-- @spec-history -->` at the top of it. A brief whose
529
+ status is `archived` is a record already, with nothing declared, though not a
530
+ log: the supersessions, cycles and self-references it declares are still
531
+ checked.
505
532
 
506
533
  | | |
507
534
  | --- | --- |
@@ -592,9 +619,34 @@ without drifting, because the `partialFingerprints` spec-graph writes are the
592
619
  same identity a baseline is keyed on — the rule, the specification, and the
593
620
  citation, with no line number in it (ADR-0012).
594
621
 
595
- `--format json` is the other machine-readable output, and the one to parse if
596
- you are building something of your own: it carries the baseline note, the
597
- summary and the per-file detail that SARIF has nowhere to put.
622
+ On GitLab, `--format gitlab` writes the Code Quality report a merge request
623
+ reads, and the merge request shows which findings it introduced and which it
624
+ resolved:
625
+
626
+ ```yaml
627
+ spec-graph:
628
+ script:
629
+ - npx spec-graph --format gitlab > gl-code-quality-report.json
630
+ artifacts:
631
+ when: always
632
+ reports:
633
+ codequality: gl-code-quality-report.json
634
+ ```
635
+
636
+ Each finding is an issue with its rule as `check_name`, its message and hint as
637
+ `description`, and its file and line as `location`. Severities map as `error`
638
+ to `critical` - or `major` when only `--strict` made it an error - `warn` to
639
+ `minor` and `info` to `info`. The `fingerprint` is a SHA-256 of the rule, the
640
+ specification and what within it - a citation's target, or the document at the
641
+ other end - which is what a baseline counts by. There is no line in it, so a
642
+ finding a paragraph moved is not reported as new, and no message, so ticking
643
+ one of three open boxes does not make an `orphaned-obligation` new either; the
644
+ second of two findings identical in all three is told apart by its order. `when: always` keeps the report when the job fails, which
645
+ it does, with exit `1`, whenever there is an error to report.
646
+
647
+ `--format json` is the machine-readable output to parse if you are building
648
+ something of your own: it carries the baseline note, the summary and the
649
+ per-file detail that SARIF and Code Quality have nowhere to put.
598
650
 
599
651
  **There is no `--watch`.** A full run on this repository takes 60 ms, so the
600
652
  loop that would justify a resident process is one line of shell, and it belongs
@@ -654,6 +706,39 @@ all — a silently ignored `ignoreReference` is a configuration that looks appli
654
706
  and is not. `--no-config` checks on defaults instead, and the exit code then
655
707
  says which run it was.
656
708
 
709
+ ### Patterns
710
+
711
+ The patterns to check, `--ignore`, `--history` and `historyPatterns` are the
712
+ glob dialect every spec-* tool reads, so a scope means the same to spec-graph
713
+ as to the tool that wrote it. They are matched against the whole
714
+ repository-relative path, **case-sensitively on every host** — a baseline
715
+ recorded on a Windows checkout holds in Linux CI:
716
+
717
+ | pattern | matches |
718
+ | --- | --- |
719
+ | `docs/**/*.md` | Markdown anywhere under `docs`; `**` is whole directories, none or more |
720
+ | `docs/**.md` | refused: `**` is whole directories or nothing, so write `docs/**/*.md` or `docs/*.md` |
721
+ | `docs` | a file called `docs`, or everything under the directory |
722
+ | `docs/` | everything under `docs`, and not `docs` itself |
723
+ | `{docs,specs}` | two literals, so both directories and what they hold |
724
+ | `adr/[0-9]*.md`, `adr/[!0-9]*.md` | a class, and a negated one; `[^0-9]` negates too |
725
+ | `!docs/drafts/**` | takes back what an earlier pattern matched; the last to match wins |
726
+ | `docs\adr\*.md` | the same as `docs/adr/*.md`: a `\` is a separator |
727
+
728
+ A class never matches a `/`, and a pattern that cannot mean a path under the
729
+ root — an unclosed `[` or `{`, `..`, a lone `.`, an empty one — stops the run
730
+ with exit `2` and names the pattern, wherever it was written. So does an
731
+ extended glob, `+(a|b)`: write `{a,b}`. A parenthesis with no `|` in its group
732
+ is itself, so `C++(notes).md` names that file. A `..` typed below the root,
733
+ `spec-graph "../*.md"` from `docs/deep`, is resolved against where you typed
734
+ it. A bare `--ignore` name, `--ignore drafts`, still prunes that directory at
735
+ any depth.
736
+
737
+ `--ignore-ref` is matched against reference targets rather than paths: a bare
738
+ pattern names one target exactly, case is ignored on every host, a `\` escapes
739
+ the next character, and `.` and `..` are the text of the link. See
740
+ [ADR-0022](docs/adr/0022-globs-are-the-family-path-dialect.md).
741
+
657
742
  ### Family rules
658
743
 
659
744
  Every specification cites RFC 2119. In a repository that also keeps its own
@@ -713,7 +798,8 @@ spec-graph diff <before> <after> Compare two JSON graph exports.
713
798
  --record-baseline <f> Write today's findings as accepted debt, exit 0
714
799
  --ratchet Also fail when a baseline entry no longer occurs
715
800
  --no-config Ignore .spec-graph.json and the package.json key
716
- --format <fmt> human | json | sarif (check) | markdown (check, diff)
801
+ --format <fmt> human | json | sarif (check) | gitlab (check)
802
+ | markdown (check, diff)
717
803
  --graph-format <fmt> dot | mermaid | json
718
804
  --documents-only Hide items; their relations lift onto their documents
719
805
  --rule <id>=<severity> error | warn | info | off (repeatable)
@@ -831,7 +917,7 @@ exported. Reporters, editor extensions and custom rules are all first-class.
831
917
 
832
918
  ## Design
833
919
 
834
- Twenty ADRs, which `spec-graph` validates on every CI run:
920
+ Twenty-three ADRs, which `spec-graph` validates on every CI run:
835
921
 
836
922
  - [ADR-0001 — A hand-written Markdown scanner](docs/adr/0001-hand-written-markdown-scanner.md)
837
923
  - [ADR-0002 — A four-phase lifecycle lattice](docs/adr/0002-lifecycle-lattice.md)
@@ -853,17 +939,23 @@ Twenty ADRs, which `spec-graph` validates on every CI run:
853
939
  - [ADR-0018 — The configuration file is the root](docs/adr/0018-the-configuration-file-is-the-root.md)
854
940
  - [ADR-0019 — The sweep runs in shards](docs/adr/0019-the-sweep-runs-in-shards.md)
855
941
  - [ADR-0020 — A diff names only what it can tell apart](docs/adr/0020-a-diff-names-what-it-can-tell-apart.md)
942
+ - [ADR-0021 — Releases are published by CI, with provenance](docs/adr/0021-releases-are-published-by-ci.md)
943
+ - [ADR-0022 — Globs are the family's path dialect](docs/adr/0022-globs-are-the-family-path-dialect.md)
944
+ - [ADR-0023 — The scanner is the family's](docs/adr/0023-the-scanner-is-the-familys.md)
856
945
 
857
946
  **Zero runtime dependencies.** Node 22+, native ESM, TypeScript strict with
858
947
  `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes`. The Markdown
859
948
  scanner, the front-matter reader, the glob matcher, the query parser and the
860
- regular-expression matcher behind `~=` are all written here, so the whole
861
- package is auditable in an afternoon — and nothing in the pipeline can take
862
- longer than its input.
949
+ regular-expression matcher behind `~=` are all written here or in
950
+ [spec-core](https://github.com/DescentVTT/spec-core), the library the spec-*
951
+ tools share, whose code is copied into `src/vendor/` and checked by hash rather
952
+ than installed. So the whole package is auditable in an afternoon — and nothing
953
+ in the pipeline can take longer than its input.
863
954
 
864
- **Verified, not just covered.** 411 tests; 94.2% statement and 96.9% line
955
+ **Verified, not just covered.** 1,053 tests; 96.8% statement and 98.2% line
865
956
  coverage. Coverage says a line ran, so the suite is also held to a mutation
866
- score - 74.05% over 6,150 mutants - because a vocabulary entry or a boundary
957
+ score - 76.73% over 8,400 mutants at 0.9.0, measured incrementally in CI -
958
+ because a vocabulary entry or a boundary
867
959
  condition can be weakened by an ordinary-looking refactor without a single test
868
960
  going red. [ADR-0007](docs/adr/0007-mutation-testing.md) is straight about what
869
961
  that number is and is not.
package/dist/cli.d.ts CHANGED
@@ -54,7 +54,7 @@ export interface CliOptions {
54
54
  readonly ratchet: boolean;
55
55
  /** Skip the repository configuration file entirely. */
56
56
  readonly noConfig: boolean;
57
- readonly format: 'human' | 'json' | 'sarif' | 'markdown';
57
+ readonly format: 'human' | 'json' | 'sarif' | 'markdown' | 'gitlab';
58
58
  readonly graphFormat: GraphFormat;
59
59
  readonly severities: Partial<Record<AnyRuleId, Severity>>;
60
60
  readonly color: boolean | null;
@@ -77,7 +77,7 @@ export interface CliIO {
77
77
  readonly env?: Readonly<Record<string, string | undefined>> | undefined;
78
78
  readonly isTTY?: boolean | undefined;
79
79
  }
80
- export declare const HELP = "spec-graph - turn Markdown specifications into a verifiable graph\n\nUSAGE\n spec-graph [check] [patterns...] [options]\n spec-graph query <selector|project:rule> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [rule-id] [--explain]\n spec-graph diff <before.json> <after.json> [--format human|json|markdown]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector - or a registered project rule, by its id - and\n print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the diagnostics that will run, built in and project. Name one\n to see only that one; --explain adds its selector and the ADR that\n decided it.\n diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against. Without it,\n .spec-graph.json is looked for in the working\n directory and then upward as far as the repository,\n and the directory holding it becomes the root - so a\n run from a subdirectory reports what a run from the\n top reports. Paths typed on the command line stay\n relative to where they were typed.\n --ignore <glob> Skip paths. Repeatable.\n --ignore-ref <glob> Do not report these reference targets when they fail\n to resolve, for repositories where [[...]] tags a\n concept rather than naming a file. Repeatable.\n Suppresses findings only, never edges.\n --family <name> Families a bare identifier in prose may name. When\n given, everything else stays prose. Repeatable.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable.\n --history <glob> Files that log what was decided rather than deciding\n it - journals, changelogs, minutes. Their links are\n still checked; their obligations are not. Repeatable.\n --baseline <file> Accept the findings recorded in this file and report\n only what is new since. The file has to be there: a\n path that reads nothing accepts nothing and says so\n nowhere. A \"baseline\" in the configuration may name a\n file not recorded yet; a path typed here may not.\n --record-baseline <f> Write today's findings to this file as accepted debt,\n and exit 0 without judging them.\n --ratchet Also fail when a baseline entry no longer occurs, so\n a paid-off exemption cannot outlive the defect.\n --no-config Ignore .spec-graph.json and the package.json key.\n --format <fmt> human, json, sarif or markdown. sarif is the\n interchange format GitHub code scanning and editors\n already read, for check; markdown is a table for a\n pull-request comment or $GITHUB_STEP_SUMMARY, for check\n or diff (default: human)\n --graph-format <fmt> dot, mermaid or json (default: dot)\n --documents-only Leave items out of the exported graph\n --rule <id>=<severity> Override one rule: error, warn, info or off. A project\n rule is named in full: project:no-draft-dependency.\n Repeatable.\n --max <n> Show at most n findings (0 = no limit)\n --max-warnings <n> Fail when warnings exceed n (default: no limit)\n --strict Raise every warning to an error. An explicit --rule\n still wins, so --strict --rule x=warn exempts x.\n --color / --no-color Force colour on or off\n --ascii Use ASCII glyphs only\n --verbose Include parse problems, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\n -h, --help Show this help\n -v, --version Show the version\n\nSELECTORS\n A selector is a path through the graph.\n\n item[openness=open] -delegates-to-> document[phase=retired]\n document[phase=active] -assumes-> document[phase=retired]\n document =supersedes=> document\n *[path^=docs/adr] -contains-> item[state=accepted-debt]\n\n Node types: document, item, * (any)\n Attributes: id, kind, title, path, file, line, phase, status, receptivity,\n alias, document, state (or disposition), openness, section,\n text, body, evidence, conflicted, fm.<front-matter-key>\n Operators: = != ^= $= *= ~= and [attr] for \"is present\"\n ~= is a regular expression, matched by an automaton that cannot\n backtrack: linear in the subject, whatever the pattern. It reads\n the usual syntax minus backreferences and lookaround, which are\n not regular - both are refused when the selector is read, with\n the character pointed at\n Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)\n\nEXIT CODES\n 0 clean\n 1 findings at error severity (or over --max-warnings)\n 2 the tool could not run\n\nCONFIGURATION\n Anything repeated on every run belongs in the repository rather than in the\n command. spec-graph reads the first of these that exists:\n\n .spec-graph.json, spec-graph.config.json, or a \"spec-graph\" key in\n package.json\n\n { \"patterns\": [\"docs/**/*.md\"],\n \"ignoreReferences\": [\"trap *\"],\n \"ignoreFamilies\": [\"RFC\"],\n \"historyPatterns\": [\"**/JOURNAL_*.md\", \"archive/**\"],\n \"baseline\": \".spec-graph-baseline.json\",\n \"severities\": { \"self-reference\": \"off\" },\n \"strict\": true }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile. A configuration that did\n not load checks a different repository than the one configured, and would\n report that one as consistent. --no-config runs on defaults instead.\n\nPROJECT RULES\n A convention spec-graph never anticipated is a selector plus a sentence, and\n belongs in the same file. Every rule declared here runs beside the built-ins\n and is reported, baselined, escalated by --strict and silenced by --rule in\n exactly the same way.\n\n { \"rules\": {\n \"no-draft-dependency\": {\n \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n \"message\": \"{0} depends on {1}, which is still a draft\",\n \"hint\": \"wait for {1} to be accepted, or drop it from {0.path}\",\n \"severity\": \"error\" } } }\n\n query One selector, or a list of them read as a union.\n message The headline. {0} is the first node on the path, {1} the next;\n {1.phase} and {0.fm.owner} read any selector attribute.\n hint The next action. Optional, and templated the same way.\n severity error, warn, info or off. Defaults to warn - a rule a team has\n just written has not yet earned the right to fail their build.\n\n The id is the name with project: in front, which is why it can never collide\n with a built-in. A selector that does not parse, or a {2} the query can never\n reach, is reported when the file is read rather than found missing later.\n\n To see what one matches without copying its selector out of the file:\n\n spec-graph query project:no-draft-dependency --verbose\n\nADOPTING THIS ON AN OLD REPOSITORY\n Record what is already wrong, then report only what happens next:\n\n spec-graph check --record-baseline .spec-graph-baseline.json\n git add .spec-graph-baseline.json\n\n The file is keyed by specification and citation, not by line number, so it\n survives edits, moves and renames. Findings that stop occurring are reported\n so the file can be tightened; nothing new gets in.\n\nEXAMPLES\n spec-graph \"docs/**/*.md\"\n spec-graph check --rule self-reference=off --format json\n spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'\n spec-graph check --ignore-ref \"trap *\" # [[trap 55]] tags a concept, not a file\n spec-graph graph --documents-only --graph-format mermaid > graph.mmd\n spec-graph check --history \"**/JOURNAL_*.md\" # a log is not a specification\n spec-graph check --baseline .spec-graph-baseline.json\n spec-graph rules --explain # including this repository's own\n spec-graph rules ghost-handover --explain # and why it exists\n spec-graph check --rule project:no-draft-dependency=off\n spec-graph query project:no-draft-dependency # what does that rule match?\n spec-graph check --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
80
+ export declare const HELP = "spec-graph - turn Markdown specifications into a verifiable graph\n\nUSAGE\n spec-graph [check] [patterns...] [options]\n spec-graph query <selector|project:rule> [patterns...] [options]\n spec-graph graph [patterns...] [--graph-format dot|mermaid|json]\n spec-graph rules [rule-id] [--explain]\n spec-graph diff <before.json> <after.json> [--format human|json|markdown]\n\nCOMMANDS\n check Validate the specification graph. The default.\n query Run a selector - or a registered project rule, by its id - and\n print the matching paths.\n graph Export the graph for Graphviz, Mermaid, or another tool.\n rules List the diagnostics that will run, built in and project. Name one\n to see only that one; --explain adds its selector and the ADR that\n decided it.\n diff Compare two graph exports: documents added, removed, moved or\n accepted; relations added or removed; and obligations resolved or\n reopened where they can be told apart. Make both exports with the\n same spec-graph. Exits 0 whether anything changed or not.\n\nOPTIONS\n --root <dir> Directory the patterns resolve against. Without it,\n .spec-graph.json is looked for in the working\n directory and then upward as far as the repository,\n and the directory holding it becomes the root - so a\n run from a subdirectory reports what a run from the\n top reports. Paths typed on the command line stay\n relative to where they were typed.\n --ignore <glob> Skip paths. Repeatable.\n --ignore-ref <glob> Do not report these reference targets when they fail\n to resolve, for repositories where [[...]] tags a\n concept rather than naming a file. Repeatable.\n Suppresses findings only, never edges.\n --family <name> Families a bare identifier in prose may name. When\n given, everything else stays prose. Repeatable.\n --ignore-family <name> Families that are never citations - RFC when the repo\n cites RFC 2119 and keeps its own RFCs. Repeatable.\n --history <glob> Files that log what was decided rather than deciding\n it - journals, changelogs, minutes. Their links are\n still checked; their obligations are not. Repeatable.\n --baseline <file> Accept the findings recorded in this file and report\n only what is new since. The file has to be there: a\n path that reads nothing accepts nothing and says so\n nowhere. A \"baseline\" in the configuration may name a\n file not recorded yet; a path typed here may not.\n --record-baseline <f> Write today's findings to this file as accepted debt,\n and exit 0 without judging them.\n --ratchet Also fail when a baseline entry no longer occurs, so\n a paid-off exemption cannot outlive the defect.\n --no-config Ignore .spec-graph.json and the package.json key.\n --format <fmt> human, json, sarif, gitlab or markdown. sarif is the\n interchange format GitHub code scanning and editors\n already read, and gitlab the Code Quality report a\n GitLab merge request reads, both for check; markdown\n is a table for a pull-request comment or\n $GITHUB_STEP_SUMMARY, for check or diff\n (default: human)\n --graph-format <fmt> dot, mermaid or json (default: dot)\n --documents-only Leave items out of the exported graph\n --rule <id>=<severity> Override one rule: error, warn, info or off. A project\n rule is named in full: project:no-draft-dependency.\n Repeatable.\n --max <n> Show at most n findings (0 = no limit)\n --max-warnings <n> Fail when warnings exceed n (default: no limit)\n --strict Raise every warning to an error. An explicit --rule\n still wins, so --strict --rule x=warn exempts x.\n --color / --no-color Force colour on or off\n --ascii Use ASCII glyphs only\n --verbose Include parse problems, per-file detail, and every\n reference --ignore-ref or --ignore-family silenced\n -h, --help Show this help\n -v, --version Show the version\n\nSELECTORS\n A selector is a path through the graph.\n\n item[openness=open] -delegates-to-> document[phase=retired]\n document[phase=active] -assumes-> document[phase=retired]\n document =supersedes=> document\n *[path^=docs/adr] -contains-> item[state=accepted-debt]\n\n Node types: document, item, * (any)\n Attributes: id, kind, title, path, file, line, phase, status, receptivity,\n alias, document, state (or disposition), openness, section,\n text, body, evidence, conflicted, fm.<front-matter-key>\n Operators: = != ^= $= *= ~= and [attr] for \"is present\"\n ~= is a regular expression, matched by an automaton that cannot\n backtrack: linear in the subject, whatever the pattern. It reads\n the usual syntax minus backreferences and lookaround, which are\n not regular - both are refused when the selector is read, with\n the character pointed at\n Relations: -kind-> <-kind- =kind=> <=kind= (= forms are transitive)\n\nEXIT CODES\n 0 clean\n 1 findings at error severity (or over --max-warnings)\n 2 the tool could not run\n\nCONFIGURATION\n Anything repeated on every run belongs in the repository rather than in the\n command. spec-graph reads the first of these that exists:\n\n .spec-graph.json, spec-graph.config.json, or a \"spec-graph\" key in\n package.json\n\n { \"patterns\": [\"docs/**/*.md\"],\n \"ignoreReferences\": [\"trap *\"],\n \"ignoreFamilies\": [\"RFC\"],\n \"historyPatterns\": [\"**/JOURNAL_*.md\", \"archive/**\"],\n \"baseline\": \".spec-graph-baseline.json\",\n \"severities\": { \"self-reference\": \"off\" },\n \"strict\": true }\n\n A flag always wins over the file, and list flags add to it rather than\n replacing it.\n\n A problem in that file stops the run with exit 2, named on stderr: invalid\n JSON, an unknown key, a rule that does not compile. A configuration that did\n not load checks a different repository than the one configured, and would\n report that one as consistent. --no-config runs on defaults instead.\n\nPROJECT RULES\n A convention spec-graph never anticipated is a selector plus a sentence, and\n belongs in the same file. Every rule declared here runs beside the built-ins\n and is reported, baselined, escalated by --strict and silenced by --rule in\n exactly the same way.\n\n { \"rules\": {\n \"no-draft-dependency\": {\n \"query\": \"document[phase=active] -depends-on-> document[phase=draft]\",\n \"message\": \"{0} depends on {1}, which is still a draft\",\n \"hint\": \"wait for {1} to be accepted, or drop it from {0.path}\",\n \"severity\": \"error\" } } }\n\n query One selector, or a list of them read as a union.\n message The headline. {0} is the first node on the path, {1} the next;\n {1.phase} and {0.fm.owner} read any selector attribute.\n hint The next action. Optional, and templated the same way.\n severity error, warn, info or off. Defaults to warn - a rule a team has\n just written has not yet earned the right to fail their build.\n\n The id is the name with project: in front, which is why it can never collide\n with a built-in. A selector that does not parse, or a {2} the query can never\n reach, is reported when the file is read rather than found missing later.\n\n To see what one matches without copying its selector out of the file:\n\n spec-graph query project:no-draft-dependency --verbose\n\nADOPTING THIS ON AN OLD REPOSITORY\n Record what is already wrong, then report only what happens next:\n\n spec-graph check --record-baseline .spec-graph-baseline.json\n git add .spec-graph-baseline.json\n\n The file is keyed by specification and citation, not by line number, so it\n survives edits, moves and renames. Findings that stop occurring are reported\n so the file can be tightened; nothing new gets in.\n\nEXAMPLES\n spec-graph \"docs/**/*.md\"\n spec-graph check --rule self-reference=off --format json\n spec-graph query 'item[openness=open] -delegates-to-> document[phase=retired]'\n spec-graph check --ignore-ref \"trap *\" # [[trap 55]] tags a concept, not a file\n spec-graph graph --documents-only --graph-format mermaid > graph.mmd\n spec-graph check --history \"**/JOURNAL_*.md\" # a log is not a specification\n spec-graph check --baseline .spec-graph-baseline.json\n spec-graph rules --explain # including this repository's own\n spec-graph rules ghost-handover --explain # and why it exists\n spec-graph check --rule project:no-draft-dependency=off\n spec-graph query project:no-draft-dependency # what does that rule match?\n spec-graph check --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n spec-graph check --format gitlab > gl-code-quality-report.json\n spec-graph diff base.json head.json --format markdown >> \"$GITHUB_STEP_SUMMARY\"\n";
81
81
  /** Parses argv into options. Throws {@link UsageError} on anything malformed. */
82
82
  export declare function parseArgs(argv: readonly string[], cwd: string): CliOptions;
83
83
  /** Runs the CLI and returns the process exit code. Never throws. */
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { discoverConfig, loadConfig } from './config.js';
14
14
  import { DiffInputError, diffExports, formatDiffJson, formatDiffMarkdown, formatDiffText, parseGraphExport, } from './diff.js';
15
15
  import { isGlob, underRoot } from './glob.js';
16
16
  import { analyse, DEFAULT_PATTERNS, withDiagnostics } from './runner.js';
17
- import { formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
17
+ import { formatGitlab, formatGraph, formatJson, formatMarkdown, formatReport, formatSarif, shouldUseAscii, shouldUseColor, } from './report.js';
18
18
  import { DEFAULT_SEVERITIES, resolveStrict, RULE_DECISIONS, RULE_DESCRIPTIONS, RULE_IDS, RULE_QUERIES, } from './rules.js';
19
19
  import { isAbsolutePath, toPosix } from './paths.js';
20
20
  import { formatRef } from './source.js';
@@ -81,11 +81,13 @@ OPTIONS
81
81
  --ratchet Also fail when a baseline entry no longer occurs, so
82
82
  a paid-off exemption cannot outlive the defect.
83
83
  --no-config Ignore .spec-graph.json and the package.json key.
84
- --format <fmt> human, json, sarif or markdown. sarif is the
84
+ --format <fmt> human, json, sarif, gitlab or markdown. sarif is the
85
85
  interchange format GitHub code scanning and editors
86
- already read, for check; markdown is a table for a
87
- pull-request comment or $GITHUB_STEP_SUMMARY, for check
88
- or diff (default: human)
86
+ already read, and gitlab the Code Quality report a
87
+ GitLab merge request reads, both for check; markdown
88
+ is a table for a pull-request comment or
89
+ $GITHUB_STEP_SUMMARY, for check or diff
90
+ (default: human)
89
91
  --graph-format <fmt> dot, mermaid or json (default: dot)
90
92
  --documents-only Leave items out of the exported graph
91
93
  --rule <id>=<severity> Override one rule: error, warn, info or off. A project
@@ -201,6 +203,7 @@ EXAMPLES
201
203
  spec-graph check --rule project:no-draft-dependency=off
202
204
  spec-graph query project:no-draft-dependency # what does that rule match?
203
205
  spec-graph check --format markdown >> "$GITHUB_STEP_SUMMARY"
206
+ spec-graph check --format gitlab > gl-code-quality-report.json
204
207
  spec-graph diff base.json head.json --format markdown >> "$GITHUB_STEP_SUMMARY"
205
208
  `;
206
209
  const SEVERITIES = ['error', 'warn', 'info', 'off'];
@@ -330,8 +333,8 @@ export function parseArgs(argv, cwd) {
330
333
  break;
331
334
  case '--format': {
332
335
  const value = next(arg, i);
333
- if (value !== 'human' && value !== 'json' && value !== 'sarif' && value !== 'markdown') {
334
- throw new UsageError(`--format must be human, json, sarif or markdown, got "${value}"`);
336
+ if (value !== 'human' && value !== 'json' && value !== 'sarif' && value !== 'markdown' && value !== 'gitlab') {
337
+ throw new UsageError(`--format must be human, json, sarif, markdown or gitlab, got "${value}"`);
335
338
  }
336
339
  format = value;
337
340
  i += 1;
@@ -381,12 +384,13 @@ export function parseArgs(argv, cwd) {
381
384
  throw new UsageError(`unknown option "${arg}"\n run "spec-graph --help" to see the available options`);
382
385
  }
383
386
  }
384
- // SARIF is a report about findings, and only `check` produces those. Falling
385
- // back to JSON would hand a pipeline something its uploader rejects with a
386
- // message about a schema rather than about the command that was run. Markdown
387
- // is for a pull request, which reads a check or a diff.
388
- if (format === 'sarif' && command !== 'check' && !help && !version) {
389
- throw new UsageError(`--format sarif reports findings, so it belongs to check, not to ${command}`);
387
+ // SARIF and GitLab's Code Quality are reports about findings, and only
388
+ // `check` produces those. Falling back to JSON would hand a pipeline something
389
+ // its uploader rejects with a message about a schema rather than about the
390
+ // command that was run. Markdown is for a pull request, which reads a check
391
+ // or a diff.
392
+ if ((format === 'sarif' || format === 'gitlab') && command !== 'check' && !help && !version) {
393
+ throw new UsageError(`--format ${format} reports findings, so it belongs to check, not to ${command}`);
390
394
  }
391
395
  if (format === 'markdown' && command !== 'check' && command !== 'diff' && !help && !version) {
392
396
  throw new UsageError(`--format markdown is a report for a pull request, so it belongs to check or diff, not to ${command}`);
@@ -702,11 +706,13 @@ export async function main(io = {}) {
702
706
  const reporterOptions = { verbose: options.verbose, max: options.max, escalated, ...baselineNote };
703
707
  out(options.format === 'sarif'
704
708
  ? formatSarif(reported, reported.graph, { version: await readVersion(), escalated, projectRules })
705
- : options.format === 'json'
706
- ? formatJson(reported, { escalated, ...baselineNote })
707
- : options.format === 'markdown'
708
- ? formatMarkdown(reported, reporterOptions)
709
- : `${formatReport(reported, { color, ascii, ...reporterOptions })}\n`);
709
+ : options.format === 'gitlab'
710
+ ? formatGitlab(reported, { escalated })
711
+ : options.format === 'json'
712
+ ? formatJson(reported, { escalated, ...baselineNote })
713
+ : options.format === 'markdown'
714
+ ? formatMarkdown(reported, reporterOptions)
715
+ : `${formatReport(reported, { color, ascii, ...reporterOptions })}\n`);
710
716
  if (!reported.ok || looseBaseline)
711
717
  return EXIT_FAILED;
712
718
  if (options.maxWarnings >= 0 && reported.summary.warnings > options.maxWarnings)
@@ -738,7 +744,20 @@ function below(root, from) {
738
744
  function anchor(prefix, value) {
739
745
  if (prefix === '' || isAbsolutePath(value))
740
746
  return value;
741
- return value.startsWith('!') ? `!${prefix}/${value.slice(1)}` : `${prefix}/${value}`;
747
+ const negated = value.startsWith('!');
748
+ const directories = prefix.split('/');
749
+ let rest = toPosix(negated ? value.slice(1) : value);
750
+ // `../docs` typed one directory down is the root's `docs`. That is arithmetic
751
+ // on two relative paths, and this is the one place both are known; left in
752
+ // the pattern, it reads as a glob climbing out of the root, which is refused.
753
+ // A bare `..` typed in `docs/deep` leaves `docs/`: the directory's contents,
754
+ // which is what it named.
755
+ while (directories.length > 0 && (rest === '..' || rest.startsWith('../'))) {
756
+ directories.pop();
757
+ rest = rest.slice(3);
758
+ }
759
+ const joined = [...directories, rest].join('/');
760
+ return negated ? `!${joined}` : joined;
742
761
  }
743
762
  /** The same, for an ignore, where a bare name is a directory at any depth. */
744
763
  function anchorPath(prefix, pattern) {