@descent-vtt/spec-graph 0.2.3 → 0.4.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-rules.js","sourceRoot":"","sources":["../src/project-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Rules a repository writes for itself.\n *\n * [ADR-0005](../docs/adr/0005-rules-are-queries.md) claimed the selector\n * language was expressive enough that a team could express a convention\n * spec-graph never anticipated. For three releases that claim was only true at a\n * command prompt: `spec-graph query` could find the thing, and nothing could\n * make finding it fail a build. Three ADRs recorded the same gap and the same\n * reason - a query needs a severity and a sentence before it is a rule.\n *\n * This is the sentence. A project rule is a selector, a message, a hint and a\n * severity, written in `.spec-graph.json` and run beside the built-ins:\n *\n * ```json\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 the dependency\"\n * }\n * }\n * ```\n *\n * Everything here is a pure function of the parsed configuration. Reading the\n * file stays in `config.ts`, for the same reason extraction never touches a\n * disk.\n */\n\nimport { attributesOf, parseQuery, QueryError, SELECTOR_KEYS, type Match, type QuerySpec } from './select.js';\nimport type { SpecGraph } from './graph.js';\nimport type { ProjectRuleId, Severity } from './types.js';\nimport { PROJECT_RULE_PREFIX } from './types.js';\n\n/* -------------------------------------------------------------------------- */\n/* Shapes */\n/* -------------------------------------------------------------------------- */\n\n/** A project rule, compiled and known to be runnable. */\nexport interface ProjectRule {\n /** Namespaced, so it can never collide with a built-in. */\n readonly id: ProjectRuleId;\n /** The name as the repository wrote it, for error messages. */\n readonly name: string;\n /**\n * The selectors, in the order they were written.\n *\n * More than one because a convention is often a union - \"no live document may\n * depend on a draft *or* on a record\" - and a union of queries is clearer than\n * a disjunction inside the grammar. See ADR-0005.\n */\n readonly queries: readonly QuerySpec[];\n /**\n * The selectors as written, kept beside the compiled form.\n *\n * A report has to describe the rule, and the only honest description of a\n * project rule is the query behind it. Echoing the text back is exact and\n * free; rendering a `QuerySpec` would be a second grammar to keep correct.\n */\n readonly sources: readonly string[];\n readonly message: string;\n readonly hint: string;\n /**\n * What the repository declared, before `--rule` and `--strict` have their say.\n *\n * Defaults to `warn` rather than to `error`. A rule a team has just written\n * has not yet earned the right to stop their build, and the first run of a new\n * convention is exactly when it is most likely to be wrong.\n */\n readonly severity: Severity;\n}\n\nexport interface CompiledProjectRules {\n readonly rules: readonly ProjectRule[];\n readonly problems: readonly string[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Compilation */\n/* -------------------------------------------------------------------------- */\n\n/**\n * What a name may contain.\n *\n * No colon, because the namespace separator has to stay unambiguous. No\n * whitespace, because the id is printed in a column of a report. And no tab in\n * particular: a baseline key is three fields joined by one, and a rule id that\n * could contain a tab would be a rule whose accepted debt could be forged by\n * typing carefully. See ADR-0012.\n */\nconst NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nconst MAX_NAME_LENGTH = 64;\n\n/**\n * `{0}`, `{1.phase}`, `{2.fm.owner}`.\n *\n * Positional because a path is positional: step zero is where the finding\n * starts and the last step is where it ends. Naming them would mean inventing a\n * second vocabulary for something the selector already numbers.\n */\nconst PLACEHOLDER = /\\{(\\d+)(?:\\.([A-Za-z][A-Za-z0-9_.:-]*))?\\}/g;\n\n/**\n * Turns the `rules` section of a configuration into runnable rules.\n *\n * Every failure is collected rather than thrown. A repository whose eighth\n * custom rule has a typo in it should still get the findings from the other\n * seven, and should be told exactly which one stopped working - a configuration\n * that silently does less than it says is the failure mode this whole file\n * exists to avoid.\n */\nexport function compileProjectRules(raw: unknown, source: string): CompiledProjectRules {\n const problems: string[] = [];\n if (!isRecord(raw)) {\n return { rules: [], problems: [`${source}: \"rules\" must be an object of name to rule`] };\n }\n\n const rules: ProjectRule[] = [];\n for (const [name, body] of Object.entries(raw)) {\n const rule = compileOne(name, body, source, problems);\n if (rule !== null) rules.push(rule);\n }\n // Sorted by id, so two runs of one configuration produce findings in the same\n // order however the JSON was keyed. Two ids are never equal - they are object\n // keys - so there is no third case to write, and `<` against `<=` here is a\n // genuinely equivalent mutant with no test behind it for that reason.\n return { rules: rules.sort((a, b) => (a.id < b.id ? -1 : 1)), problems };\n}\n\nfunction compileOne(\n name: string,\n body: unknown,\n source: string,\n problems: string[],\n): ProjectRule | null {\n const where = `${source}: rules.${name}`;\n if (!NAME.test(name) || name.length > MAX_NAME_LENGTH) {\n problems.push(\n `${where}: a rule name must be letters, digits, \".\" \"_\" or \"-\", at most ${MAX_NAME_LENGTH} characters`,\n );\n return null;\n }\n if (!isRecord(body)) {\n problems.push(`${where} must be an object with \"query\" and \"message\"`);\n return null;\n }\n\n for (const key of Object.keys(body)) {\n if (!RULE_KEYS.has(key)) problems.push(`${where}: unknown key \"${key}\"`);\n }\n\n const written = body['query'];\n const listed: unknown[] = typeof written === 'string' ? [written] : Array.isArray(written) ? written : [];\n if (listed.length === 0 || listed.some((entry) => typeof entry !== 'string')) {\n problems.push(`${where}: \"query\" must be a selector, or an array of them`);\n return null;\n }\n const sources = listed as string[];\n\n const queries: QuerySpec[] = [];\n for (const selector of sources) {\n try {\n queries.push(parseQuery(selector));\n } catch (error) {\n const detail = error instanceof QueryError ? `${error.message} (at character ${error.offset + 1})` : String(error);\n problems.push(`${where}: ${detail}`);\n return null;\n }\n }\n\n const message = body['message'];\n if (typeof message !== 'string' || message.trim().length === 0) {\n problems.push(`${where}: \"message\" must be the one-line headline of the finding`);\n return null;\n }\n\n // A finding has to name a next action, so the fallback names the only thing\n // this file can be certain of: where the rule was written down.\n const hint = body['hint'] ?? `check this against rules.${name} in ${source}`;\n if (typeof hint !== 'string' || hint.trim().length === 0) {\n problems.push(`${where}: \"hint\" must be a concrete next action`);\n return null;\n }\n\n const declared = body['severity'] ?? 'warn';\n if (declared !== 'error' && declared !== 'warn' && declared !== 'info' && declared !== 'off') {\n problems.push(`${where}: \"severity\" must be error, warn, info or off`);\n return null;\n }\n\n // The shortest path a rule can produce, because a placeholder has to be\n // resolvable by *every* query behind the rule and not merely by the longest.\n const arity = Math.min(...queries.map((query) => query.steps.length + 1));\n let usable = true;\n for (const text of [message, hint]) {\n for (const problem of placeholderProblems(text, arity, where)) {\n problems.push(problem);\n usable = false;\n }\n }\n if (!usable) return null;\n\n return {\n id: `${PROJECT_RULE_PREFIX}${name}`,\n name,\n queries,\n sources,\n message: message.trim(),\n hint: hint.trim(),\n severity: declared,\n };\n}\n\nconst RULE_KEYS: ReadonlySet<string> = new Set(['query', 'message', 'hint', 'severity']);\n\n/**\n * Why a template would not render.\n *\n * Checked here rather than tolerated at render time, because both failures are\n * silent where it matters: an out-of-range index leaves `{3}` sitting in a\n * sentence a human has to act on, and a misspelled attribute renders as nothing\n * at all, which reads as a finding about an unnamed document.\n */\nfunction placeholderProblems(template: string, arity: number, where: string): string[] {\n const out: string[] = [];\n for (const match of template.matchAll(PLACEHOLDER)) {\n const index = Number(match[1]);\n if (index >= arity) {\n out.push(\n `${where}: \"${match[0]}\" names step ${index}, and the query has ${arity} ${arity === 1 ? 'step' : 'steps'} (0 to ${arity - 1})`,\n );\n continue;\n }\n const key = match[2];\n if (key === undefined) continue;\n const folded = key.toLowerCase();\n if (folded.startsWith('fm.') || SELECTOR_KEYS.includes(folded)) continue;\n out.push(`${where}: \"${match[0]}\" asks for an attribute nothing has - known: ${SELECTOR_KEYS.join(', ')}, fm.*`);\n }\n return out;\n}\n\n/**\n * Fills a validated template in from one path through the graph.\n *\n * Sharing one global regex between this and {@link placeholderProblems} is safe\n * and not an oversight: `matchAll` works on a clone, and `replace` resets\n * `lastIndex` on entry and on exit. The hazard `CLAUDE.md` warns about is a\n * `/g` regex whose cursor survives a call, and neither of these keeps one.\n *\n * The index cannot be out of range when `execute` produced the match - a path is\n * always one node longer than the query has steps, which is what validation\n * checks against. The guard is for the exported case: this takes any `Match`,\n * and a caller assembling one by hand gets the placeholder back rather than a\n * crash.\n */\nexport function renderTemplate(template: string, match: Match, graph: SpecGraph): string {\n return template.replace(PLACEHOLDER, (whole, index: string, key: string | undefined) => {\n const node = match.nodes[Number(index)];\n if (node === undefined) return whole;\n return attributesOf(node, (key ?? 'id').toLowerCase(), graph)[0] ?? whole;\n });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
package/dist/report.d.ts CHANGED
@@ -10,9 +10,11 @@
10
10
  * Colour is opt-out and auto-detected, and the glyphs degrade to ASCII when the
11
11
  * terminal cannot render them.
12
12
  */
13
+ import { type StaleEntry } from './baseline.js';
13
14
  import type { SpecGraph } from './graph.js';
15
+ import type { ProjectRule } from './project-rules.js';
14
16
  import type { AnalysisResult } from './runner.js';
15
- import type { RuleId } from './types.js';
17
+ import type { AnyRuleId } from './types.js';
16
18
  export interface ReporterOptions {
17
19
  readonly color?: boolean | undefined;
18
20
  readonly ascii?: boolean | undefined;
@@ -25,18 +27,25 @@ export interface ReporterOptions {
25
27
  * A finding that only became an error because of a flag has to say so, or the
26
28
  * reader cannot tell what would happen without it.
27
29
  */
28
- readonly escalated?: ReadonlySet<RuleId> | undefined;
30
+ readonly escalated?: ReadonlySet<AnyRuleId> | undefined;
29
31
  /**
30
32
  * What a baseline accounted for on this run.
31
33
  *
32
34
  * Reported as counts rather than as a list: the point of accepted debt is
33
- * that nobody has to read it every time. `stale` is the ratchet - debt that
34
- * has been paid and can be struck from the file.
35
+ * that nobody has to read it every time. `stale` is the ratchet - debt the
36
+ * run did not spend and which can be struck from the file - and `ratchet`
37
+ * says whether this run was asked to fail over it.
38
+ *
39
+ * `entries` carries the stale ones themselves, for the structured formats. A
40
+ * number is enough to know the file has slack and never enough to strike it,
41
+ * and a bot that wants to open the pull request needs the rows.
35
42
  */
36
43
  readonly baseline?: {
37
44
  readonly source: string;
38
45
  readonly suppressed: number;
39
46
  readonly stale: number;
47
+ readonly ratchet?: boolean | undefined;
48
+ readonly entries?: readonly StaleEntry[] | undefined;
40
49
  } | undefined;
41
50
  }
42
51
  export interface ColorEnvironment {
@@ -70,13 +79,29 @@ export declare function createPainter(color: boolean): Painter;
70
79
  /** Renders the full terminal report. */
71
80
  export declare function formatReport(result: AnalysisResult, options?: ReporterOptions): string;
72
81
  /**
73
- * The machine-readable report.
82
+ * The static-analysis interchange format, for the tools that already read it.
83
+ *
84
+ * The reason to emit it is not the format, it is what consumes the format:
85
+ * GitHub code scanning turns a SARIF upload into annotations on the diff, and
86
+ * an editor turns it into the problems pane, neither of which needs a line of
87
+ * spec-graph-specific integration.
74
88
  *
75
- * Deliberately flat and versioned: this is the contract a CI annotator or a
76
- * dashboard builds against, and it must be safe to add fields to it later.
89
+ * One field earns the whole exercise. `partialFingerprints` is how a consumer
90
+ * tracks a finding across commits without keying on a line number, and
91
+ * spec-graph already has exactly that identifier for exactly that reason - the
92
+ * `rule/document/subject` triple a baseline is keyed on (ADR-0012). The two
93
+ * problems turned out to be the same problem, so the answer is the same answer.
94
+ *
95
+ * Deterministic and timestamp-free, like every other output here. Nothing about
96
+ * a run that has the same findings should produce a different file.
77
97
  */
98
+ export declare function formatSarif(result: AnalysisResult, graph: SpecGraph, options?: {
99
+ version?: string | undefined;
100
+ escalated?: ReadonlySet<AnyRuleId> | undefined;
101
+ projectRules?: readonly ProjectRule[] | undefined;
102
+ }): string;
78
103
  export declare function formatJson(result: AnalysisResult, options?: {
79
- escalated?: ReadonlySet<RuleId>;
104
+ escalated?: ReadonlySet<AnyRuleId>;
80
105
  baseline?: ReporterOptions['baseline'];
81
106
  }): string;
82
107
  export type GraphFormat = 'dot' | 'mermaid' | 'json';
package/dist/report.js CHANGED
@@ -10,7 +10,9 @@
10
10
  * Colour is opt-out and auto-detected, and the glyphs degrade to ASCII when the
11
11
  * terminal cannot render them.
12
12
  */
13
+ import { fingerprintOf } from './baseline.js';
13
14
  import { formatRef } from './source.js';
15
+ import { DEFAULT_SEVERITIES, RULE_DESCRIPTIONS, RULE_IDS } from './rules.js';
14
16
  /** Honours `NO_COLOR`, `FORCE_COLOR` and `TERM=dumb` before falling back to TTY. */
15
17
  export function shouldUseColor(environment = {}) {
16
18
  const env = environment.env ?? process.env;
@@ -89,6 +91,18 @@ export function formatReport(result, options = {}) {
89
91
  }
90
92
  lines.push('');
91
93
  }
94
+ // What the repository told spec-graph to stop looking at. Only under
95
+ // --verbose, and never as a finding: these are not defects, they are the one
96
+ // place a configuration can make the check quieter, and a single over-broad
97
+ // glob looks exactly like a clean repository from the outside. See ADR-0008.
98
+ if (options.verbose && result.corpus.suppressed.length > 0) {
99
+ for (const [target, entry] of groupSuppressed(result.corpus.suppressed)) {
100
+ const where = entry.by === 'family' ? 'ignored family' : 'ignored reference';
101
+ const times = entry.count === 1 ? '' : ` (${entry.count} times)`;
102
+ lines.push(`${paint.dim(marks.info)} ${paint.location(formatRef(entry.at))} ${paint.dim(`${where}: ${target}${times}`)}`);
103
+ }
104
+ lines.push('');
105
+ }
92
106
  const tally = [];
93
107
  if (summary.errors > 0)
94
108
  tally.push(paint.error(`${summary.errors} ${plural(summary.errors, 'error')}`));
@@ -106,14 +120,27 @@ export function formatReport(result, options = {}) {
106
120
  tally.push(paint.dim(`${baseline.suppressed} accepted by ${baseline.source}`));
107
121
  }
108
122
  lines.push(tally.join(` ${marks.separator} `));
123
+ // Under `--ratchet` a paid entry is a failure rather than a note, so it is
124
+ // painted and worded as one.
125
+ const ratcheted = ratchetFailed(baseline);
109
126
  if (baseline !== undefined && baseline.stale > 0) {
110
- lines.push(paint.dim(`${baseline.stale} baseline ${plural(baseline.stale, 'entry', 'entries')} no longer ${baseline.stale === 1 ? 'occurs' : 'occur'} - tighten it: spec-graph check --record-baseline ${baseline.source}`));
127
+ // A "gone" entry is not the ratchet working, and saying so matters more
128
+ // than the count does: the usual way to produce one is to narrow an include
129
+ // pattern, which loses sight of a defect rather than fixing it.
130
+ const gone = (baseline.entries ?? []).filter((entry) => entry.reason === 'gone').length;
131
+ const because = gone === 0
132
+ ? `tighten it: spec-graph check --record-baseline ${baseline.source}`
133
+ : `${gone} of them ${gone === 1 ? 'names a document' : 'name documents'} this run did not see - check the include patterns before re-recording`;
134
+ const text = `${baseline.stale} baseline ${plural(baseline.stale, 'entry', 'entries')} no longer ${baseline.stale === 1 ? 'occurs' : 'occur'} - ${because}`;
135
+ lines.push(ratcheted ? `${paint.error(marks.error)} ${text}` : paint.dim(text));
111
136
  }
112
- lines.push(result.ok
113
- ? `${paint.hint(marks.hint === '>' ? 'ok' : '\u2714')} ${summary.errors + summary.warnings === 0
114
- ? 'the specification graph is consistent'
115
- : 'no errors - the specification graph holds'}`
116
- : `${paint.error(marks.error)} the specification graph is inconsistent`);
137
+ lines.push(ratcheted
138
+ ? `${paint.error(marks.error)} ${result.ok ? 'the baseline is looser than the repository' : 'the specification graph is inconsistent'}`
139
+ : result.ok
140
+ ? `${paint.hint(marks.hint === '>' ? 'ok' : '\u2714')} ${summary.errors + summary.warnings === 0
141
+ ? 'the specification graph is consistent'
142
+ : 'no errors - the specification graph holds'}`
143
+ : `${paint.error(marks.error)} the specification graph is inconsistent`);
117
144
  return lines.join('\n');
118
145
  }
119
146
  function formatDiagnostic(diagnostic, paint, marks, escalated) {
@@ -157,15 +184,162 @@ function plural(count, word, plural) {
157
184
  * Deliberately flat and versioned: this is the contract a CI annotator or a
158
185
  * dashboard builds against, and it must be safe to add fields to it later.
159
186
  */
187
+ /**
188
+ * One row per silenced target, with the first place it was written.
189
+ *
190
+ * Grouped because a concept tag used forty times is one decision, and forty
191
+ * lines of it would bury the one entry somebody needs to see. Insertion order
192
+ * is document order, which is already deterministic.
193
+ */
194
+ function groupSuppressed(suppressed) {
195
+ const out = new Map();
196
+ for (const entry of suppressed) {
197
+ const seen = out.get(entry.target);
198
+ if (seen === undefined)
199
+ out.set(entry.target, { at: entry.at, by: entry.by, count: 1 });
200
+ else
201
+ seen.count += 1;
202
+ }
203
+ return out;
204
+ }
205
+ /**
206
+ * True when a baseline was asked to ratchet and has slack left in it.
207
+ *
208
+ * Both reporters ask, because both have to agree with the exit code: a report
209
+ * that prints "ok" above a failing build is worse than no report.
210
+ */
211
+ function ratchetFailed(baseline) {
212
+ return baseline !== undefined && baseline.ratchet === true && baseline.stale > 0;
213
+ }
214
+ /* -------------------------------------------------------------------------- */
215
+ /* SARIF */
216
+ /* -------------------------------------------------------------------------- */
217
+ /**
218
+ * The static-analysis interchange format, for the tools that already read it.
219
+ *
220
+ * The reason to emit it is not the format, it is what consumes the format:
221
+ * GitHub code scanning turns a SARIF upload into annotations on the diff, and
222
+ * an editor turns it into the problems pane, neither of which needs a line of
223
+ * spec-graph-specific integration.
224
+ *
225
+ * One field earns the whole exercise. `partialFingerprints` is how a consumer
226
+ * tracks a finding across commits without keying on a line number, and
227
+ * spec-graph already has exactly that identifier for exactly that reason - the
228
+ * `rule/document/subject` triple a baseline is keyed on (ADR-0012). The two
229
+ * problems turned out to be the same problem, so the answer is the same answer.
230
+ *
231
+ * Deterministic and timestamp-free, like every other output here. Nothing about
232
+ * a run that has the same findings should produce a different file.
233
+ */
234
+ export function formatSarif(result, graph, options = {}) {
235
+ const escalated = options.escalated ?? EMPTY_RULES;
236
+ // A project rule describes itself by the selector it is, because that is the
237
+ // only honest answer: its message is a template, and a template with `{0}` in
238
+ // it is not a description of anything. See ADR-0016.
239
+ const known = [
240
+ ...RULE_IDS.map((id) => [id, RULE_DESCRIPTIONS[id], DEFAULT_SEVERITIES[id]]),
241
+ ...(options.projectRules ?? []).map((rule) => [
242
+ rule.id,
243
+ rule.sources.join(' | '),
244
+ rule.severity,
245
+ ]),
246
+ ];
247
+ const fired = known.filter(([id]) => result.diagnostics.some((diagnostic) => diagnostic.rule === id));
248
+ return `${JSON.stringify({
249
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
250
+ version: '2.1.0',
251
+ runs: [
252
+ {
253
+ tool: {
254
+ driver: {
255
+ name: 'spec-graph',
256
+ informationUri: 'https://github.com/DescentVTT/spec-graph',
257
+ ...(options.version === undefined ? {} : { version: options.version, semanticVersion: options.version }),
258
+ // Only the rules that fired. A driver listing all of them
259
+ // describes the tool; this file describes the run.
260
+ rules: fired.map(([id, description, level]) => ({
261
+ id,
262
+ shortDescription: { text: description },
263
+ defaultConfiguration: { level: sarifLevel(level) },
264
+ })),
265
+ },
266
+ },
267
+ results: result.diagnostics.map((diagnostic) => {
268
+ const print = fingerprintOf(graph, diagnostic);
269
+ return {
270
+ ruleId: diagnostic.rule,
271
+ level: sarifLevel(diagnostic.severity),
272
+ message: { text: `${diagnostic.message}. ${diagnostic.hint}` },
273
+ locations: [sarifLocation(diagnostic.at)],
274
+ ...(diagnostic.related.length === 0
275
+ ? {}
276
+ : {
277
+ relatedLocations: diagnostic.related.map((entry) => ({
278
+ ...sarifLocation(entry.at),
279
+ message: { text: entry.note },
280
+ })),
281
+ }),
282
+ partialFingerprints: {
283
+ specGraphIdentity: `${diagnostic.rule}/${print.document}/${print.subject}`,
284
+ },
285
+ properties: {
286
+ nodes: diagnostic.nodes,
287
+ target: diagnostic.target,
288
+ // A finding that is only an error because of a flag says so
289
+ // here too, so a reviewer reading an annotation is not misled.
290
+ escalated: escalated.has(diagnostic.rule),
291
+ },
292
+ };
293
+ }),
294
+ },
295
+ ],
296
+ }, null, 2)}\n`;
297
+ }
298
+ function sarifLevel(severity) {
299
+ switch (severity) {
300
+ case 'error':
301
+ return 'error';
302
+ case 'warn':
303
+ return 'warning';
304
+ case 'info':
305
+ return 'note';
306
+ default:
307
+ return 'none';
308
+ }
309
+ }
310
+ function sarifLocation(at) {
311
+ return {
312
+ physicalLocation: {
313
+ // Already repository-relative and POSIX, which is what SARIF asks for.
314
+ artifactLocation: { uri: at.file },
315
+ region: {
316
+ startLine: at.span.start.line,
317
+ startColumn: at.span.start.column,
318
+ endLine: at.span.end.line,
319
+ endColumn: at.span.end.column,
320
+ },
321
+ },
322
+ };
323
+ }
160
324
  export function formatJson(result, options = {}) {
161
325
  const escalated = options.escalated ?? EMPTY_RULES;
162
326
  return `${JSON.stringify({
163
327
  version: 1,
164
- ok: result.ok,
328
+ ok: result.ok && !ratchetFailed(options.baseline),
165
329
  strict: escalated.size > 0,
166
330
  ...(options.baseline === undefined ? {} : { baseline: options.baseline }),
167
331
  summary: result.summary,
168
332
  files: result.files,
333
+ // The audit trail for what configuration silenced. Always present, unlike
334
+ // the human report's --verbose gate: a machine reader that has to ask for
335
+ // it twice will not ask. See ADR-0008.
336
+ suppressed: result.corpus.suppressed.map((entry) => ({
337
+ target: entry.target,
338
+ from: entry.from,
339
+ by: entry.by,
340
+ file: entry.at.file,
341
+ line: entry.at.span.start.line,
342
+ })),
169
343
  diagnostics: result.diagnostics.map((diagnostic) => ({
170
344
  rule: diagnostic.rule,
171
345
  severity: diagnostic.severity,
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Output formatting.\n *\n * Two audiences, one set of facts. In a terminal the report leads with the file\n * and line so an editor can jump there, indents the supporting evidence, and\n * ends with a concrete next action - because a finding a reader cannot act on\n * is a finding they will learn to scroll past. In CI it emits JSON with every\n * position intact, so a bot can annotate a diff.\n *\n * Colour is opt-out and auto-detected, and the glyphs degrade to ASCII when the\n * terminal cannot render them.\n */\n\nimport type { SpecGraph } from './graph.js';\nimport { formatRef } from './source.js';\nimport type { AnalysisResult } from './runner.js';\nimport type { Diagnostic, Edge, RuleId, Severity, SpecNode } from './types.js';\n\nexport interface ReporterOptions {\n readonly color?: boolean | undefined;\n readonly ascii?: boolean | undefined;\n readonly verbose?: boolean | undefined;\n /** Stop after this many findings. `0` means no limit. */\n readonly max?: number | undefined;\n /**\n * Rules whose severity `--strict` raised.\n *\n * A finding that only became an error because of a flag has to say so, or the\n * reader cannot tell what would happen without it.\n */\n readonly escalated?: ReadonlySet<RuleId> | undefined;\n /**\n * What a baseline accounted for on this run.\n *\n * Reported as counts rather than as a list: the point of accepted debt is\n * that nobody has to read it every time. `stale` is the ratchet - debt that\n * has been paid and can be struck from the file.\n */\n readonly baseline?: { readonly source: string; readonly suppressed: number; readonly stale: number } | undefined;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Environment detection */\n/* -------------------------------------------------------------------------- */\n\nexport interface ColorEnvironment {\n readonly isTTY?: boolean | undefined;\n readonly env?: Readonly<Record<string, string | undefined>> | undefined;\n /**\n * Host platform, defaulting to the real one.\n *\n * Injected for the same reason `env` is: without it half of\n * {@link shouldUseAscii} is unreachable from a test, and a rule about\n * Windows consoles that only Windows can exercise is a rule that breaks on\n * Windows and nowhere else.\n */\n readonly platform?: NodeJS.Platform | undefined;\n}\n\n/** Honours `NO_COLOR`, `FORCE_COLOR` and `TERM=dumb` before falling back to TTY. */\nexport function shouldUseColor(environment: ColorEnvironment = {}): boolean {\n const env = environment.env ?? process.env;\n if (env['NO_COLOR'] !== undefined && env['NO_COLOR'] !== '') return false;\n // `FORCE_COLOR=0` is the conventional way to turn colour off, so it has to\n // win over an attached TTY rather than merely failing to force it on.\n if (env['FORCE_COLOR'] === '0') return false;\n if (env['FORCE_COLOR'] !== undefined && env['FORCE_COLOR'] !== '') return true;\n if (env['TERM'] === 'dumb') return false;\n if (env['CI'] !== undefined && env['CI'] !== '') return false;\n return environment.isTTY === true;\n}\n\n/** True when the terminal is unlikely to render box-drawing and arrows. */\nexport function shouldUseAscii(environment: ColorEnvironment = {}): boolean {\n const env = environment.env ?? process.env;\n if (env['SPEC_GRAPH_ASCII'] !== undefined && env['SPEC_GRAPH_ASCII'] !== '') return true;\n if ((environment.platform ?? process.platform) !== 'win32') return false;\n // Windows Terminal and modern shells set these; the legacy console does not.\n return env['WT_SESSION'] === undefined && env['TERM_PROGRAM'] === undefined;\n}\n\nconst EMPTY_RULES: ReadonlySet<RuleId> = new Set<RuleId>();\n\ntype Paint = (text: string) => string;\n\nexport interface Painter {\n readonly error: Paint;\n readonly warn: Paint;\n readonly info: Paint;\n readonly dim: Paint;\n readonly bold: Paint;\n readonly hint: Paint;\n readonly location: Paint;\n}\n\nexport function createPainter(color: boolean): Painter {\n const wrap = (open: string): Paint => (text) => (color ? `\\u001b[${open}m${text}\\u001b[0m` : text);\n return {\n error: wrap('31'),\n warn: wrap('33'),\n info: wrap('36'),\n dim: wrap('2'),\n bold: wrap('1'),\n hint: wrap('32'),\n location: wrap('4'),\n };\n}\n\ninterface Glyphs {\n readonly error: string;\n readonly warn: string;\n readonly info: string;\n readonly related: string;\n readonly hint: string;\n readonly separator: string;\n}\n\nfunction glyphs(ascii: boolean): Glyphs {\n return ascii\n ? { error: 'x', warn: '!', info: 'i', related: '|', hint: '>', separator: '-' }\n : { error: '\\u2716', warn: '\\u26a0', info: '\\u2139', related: '\\u21b3', hint: '\\u2192', separator: '\\u00b7' };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Human report */\n/* -------------------------------------------------------------------------- */\n\n/** Renders the full terminal report. */\nexport function formatReport(result: AnalysisResult, options: ReporterOptions = {}): string {\n const paint = createPainter(options.color ?? false);\n const marks = glyphs(options.ascii ?? false);\n const lines: string[] = [];\n const { summary } = result;\n\n lines.push(\n `${paint.bold('spec-graph')} ${paint.dim(\n [\n `${summary.documents} ${plural(summary.documents, 'document')}`,\n `${summary.items} ${plural(summary.items, 'item')}`,\n `${summary.edges} ${plural(summary.edges, 'relation')}`,\n `${summary.openObligations} open`,\n ].join(` ${marks.separator} `),\n )}`,\n );\n lines.push('');\n\n const limit = options.max && options.max > 0 ? options.max : result.diagnostics.length;\n const shown = result.diagnostics.slice(0, limit);\n\n const escalated = options.escalated ?? EMPTY_RULES;\n for (const diagnostic of shown) {\n lines.push(...formatDiagnostic(diagnostic, paint, marks, escalated));\n lines.push('');\n }\n\n if (result.diagnostics.length > shown.length) {\n lines.push(paint.dim(` ... and ${result.diagnostics.length - shown.length} more`));\n lines.push('');\n }\n\n if (options.verbose && result.problems.length > 0) {\n for (const problem of result.problems) {\n lines.push(`${paint.warn(marks.warn)} ${paint.location(formatRef(problem.at))} ${problem.message}`);\n }\n lines.push('');\n }\n\n const tally: string[] = [];\n if (summary.errors > 0) tally.push(paint.error(`${summary.errors} ${plural(summary.errors, 'error')}`));\n if (summary.warnings > 0) tally.push(paint.warn(`${summary.warnings} ${plural(summary.warnings, 'warning')}`));\n if (summary.infos > 0) tally.push(paint.info(`${summary.infos} ${plural(summary.infos, 'note')}`));\n tally.push(paint.dim(`${summary.durationMs}ms`));\n const raised = result.diagnostics.filter((diagnostic) => escalated.has(diagnostic.rule)).length;\n if (raised > 0) {\n tally.push(paint.warn(`${raised} raised by --strict`));\n }\n const baseline = options.baseline;\n if (baseline !== undefined && baseline.suppressed > 0) {\n tally.push(paint.dim(`${baseline.suppressed} accepted by ${baseline.source}`));\n }\n lines.push(tally.join(` ${marks.separator} `));\n\n if (baseline !== undefined && baseline.stale > 0) {\n lines.push(\n paint.dim(\n `${baseline.stale} baseline ${plural(baseline.stale, 'entry', 'entries')} no longer ${\n baseline.stale === 1 ? 'occurs' : 'occur'\n } - tighten it: spec-graph check --record-baseline ${baseline.source}`,\n ),\n );\n }\n\n lines.push(\n result.ok\n ? `${paint.hint(marks.hint === '>' ? 'ok' : '\\u2714')} ${\n summary.errors + summary.warnings === 0\n ? 'the specification graph is consistent'\n : 'no errors - the specification graph holds'\n }`\n : `${paint.error(marks.error)} the specification graph is inconsistent`,\n );\n\n return lines.join('\\n');\n}\n\nfunction formatDiagnostic(\n diagnostic: Diagnostic,\n paint: Painter,\n marks: Glyphs,\n escalated: ReadonlySet<RuleId>,\n): string[] {\n const mark = severityMark(diagnostic.severity, paint, marks);\n const label = escalated.has(diagnostic.rule)\n ? `${diagnostic.rule} ${paint.warn('(strict: warn -> error)')}`\n : diagnostic.rule;\n const lines: string[] = [\n `${mark} ${paint.location(formatRef(diagnostic.at))} ${paint.dim(label)}`,\n ` ${diagnostic.message}`,\n ];\n\n const width = Math.max(0, ...diagnostic.related.map((entry) => formatRef(entry.at).length));\n for (const entry of diagnostic.related) {\n const reference = formatRef(entry.at);\n lines.push(` ${paint.dim(marks.related)} ${paint.dim(reference.padEnd(width))} ${paint.dim(entry.note)}`);\n }\n\n lines.push(` ${paint.hint(marks.hint)} ${paint.hint(diagnostic.hint)}`);\n return lines;\n}\n\nfunction severityMark(severity: Exclude<Severity, 'off'>, paint: Painter, marks: Glyphs): string {\n switch (severity) {\n case 'error':\n return paint.error(marks.error);\n case 'warn':\n return paint.warn(marks.warn);\n case 'info':\n return paint.info(marks.info);\n }\n}\n\nfunction plural(count: number, word: string, plural?: string): string {\n if (count === 1) return word;\n return plural ?? `${word}s`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* JSON */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The machine-readable report.\n *\n * Deliberately flat and versioned: this is the contract a CI annotator or a\n * dashboard builds against, and it must be safe to add fields to it later.\n */\nexport function formatJson(\n result: AnalysisResult,\n options: { escalated?: ReadonlySet<RuleId>; baseline?: ReporterOptions['baseline'] } = {},\n): string {\n const escalated = options.escalated ?? EMPTY_RULES;\n return `${JSON.stringify(\n {\n version: 1,\n ok: result.ok,\n strict: escalated.size > 0,\n ...(options.baseline === undefined ? {} : { baseline: options.baseline }),\n summary: result.summary,\n files: result.files,\n diagnostics: result.diagnostics.map((diagnostic) => ({\n rule: diagnostic.rule,\n severity: diagnostic.severity,\n // True when this finding is only an error because of --strict.\n escalated: escalated.has(diagnostic.rule),\n message: diagnostic.message,\n hint: diagnostic.hint,\n nodes: diagnostic.nodes,\n target: diagnostic.target,\n file: diagnostic.at.file,\n line: diagnostic.at.span.start.line,\n column: diagnostic.at.span.start.column,\n endLine: diagnostic.at.span.end.line,\n endColumn: diagnostic.at.span.end.column,\n related: diagnostic.related.map((entry) => ({\n file: entry.at.file,\n line: entry.at.span.start.line,\n column: entry.at.span.start.column,\n note: entry.note,\n })),\n })),\n problems: result.problems.map((problem) => ({\n message: problem.message,\n file: problem.at.file,\n line: problem.at.span.start.line,\n })),\n },\n null,\n 2,\n )}\\n`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Graph export */\n/* -------------------------------------------------------------------------- */\n\nexport type GraphFormat = 'dot' | 'mermaid' | 'json';\n\nexport interface GraphExportOptions {\n /** Leave out `contains` edges and item nodes. */\n readonly documentsOnly?: boolean | undefined;\n}\n\n/** Serialises the graph for Graphviz, Mermaid, or a downstream tool. */\nexport function formatGraph(graph: SpecGraph, format: GraphFormat, options: GraphExportOptions = {}): string {\n const skipItems = options.documentsOnly ?? false;\n const nodes = [...graph.nodes.values()].filter((node) => !skipItems || node.kind === 'document');\n const visible = new Set(nodes.map((node) => node.id));\n\n // Hiding items must not hide what they say. An obligation delegated from an\n // item to a document is lifted onto the item's owner, so the document-level\n // view still shows the handover rather than silently losing it.\n const lifted = skipItems\n ? dedupeEdges(\n graph.edges\n // Containment of an *item* is what hiding items makes redundant.\n // Containment of a specification by the register that holds it is\n // structure between documents, and is what this view exists to show.\n .filter((edge) => edge.kind !== 'contains' || visible.has(edge.to))\n .map((edge) => {\n const from = graph.owningDocument(edge.from)?.id ?? edge.from;\n const to = graph.owningDocument(edge.to)?.id ?? edge.to;\n return from === edge.from && to === edge.to ? edge : { ...edge, from, to, reflexive: from === to };\n })\n .filter((edge) => !edge.reflexive),\n )\n : graph.edges;\n\n const edges = lifted.filter((edge) => visible.has(edge.from) && visible.has(edge.to));\n\n switch (format) {\n case 'dot':\n return toDot(nodes, edges);\n case 'mermaid':\n return toMermaid(nodes, edges);\n case 'json':\n return `${JSON.stringify(\n {\n version: 1,\n nodes: nodes.map(serialiseNode),\n edges: edges.map((edge) => ({\n kind: edge.kind,\n from: edge.from,\n to: edge.to,\n origin: edge.origin,\n declaredIn: edge.declaredIn,\n file: edge.declaredAt.file,\n line: edge.declaredAt.span.start.line,\n })),\n },\n null,\n 2,\n )}\\n`;\n }\n}\n\n/** Collapses duplicate relations produced by lifting item edges onto documents. */\nfunction dedupeEdges(edges: readonly Edge[]): Edge[] {\n const seen = new Set<string>();\n const out: Edge[] = [];\n for (const edge of edges) {\n const key = `${edge.kind} ${edge.from} ${edge.to}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(edge);\n }\n return out;\n}\n\nfunction serialiseNode(node: SpecNode): Record<string, unknown> {\n const base = {\n id: node.id,\n kind: node.kind,\n title: node.title,\n file: node.at.file,\n line: node.at.span.start.line,\n };\n return node.kind === 'document'\n ? { ...base, path: node.path, phase: node.phase, status: node.rawStatus, aliases: node.aliases }\n : {\n ...base,\n document: node.document,\n section: node.section,\n disposition: node.disposition,\n openness: node.openness,\n };\n}\n\n/** Phase colours, chosen to stay legible in both light and dark renderings. */\nconst PHASE_FILL: Readonly<Record<string, string>> = {\n draft: '#e8f0fe',\n active: '#e6f4ea',\n frozen: '#e8eaed',\n retired: '#fce8e6',\n record: '#f3e8fd',\n unknown: '#ffffff',\n};\n\nfunction toDot(nodes: readonly SpecNode[], edges: readonly Edge[]): string {\n const lines = ['digraph spec {', ' rankdir=LR;', ' node [shape=box, style=\"rounded,filled\", fontname=\"sans\"];'];\n for (const node of nodes) {\n const fill = node.kind === 'document' ? (PHASE_FILL[node.phase] ?? '#ffffff') : '#fffbe6';\n const label = node.kind === 'document' ? `${node.id}\\\\n${escapeDot(node.title)}` : escapeDot(node.text);\n const shape = node.kind === 'document' ? 'box' : 'note';\n lines.push(` \"${escapeDot(node.id)}\" [label=\"${label}\", fillcolor=\"${fill}\", shape=${shape}];`);\n }\n for (const edge of edges) {\n const style = edge.kind === 'contains' ? ', style=dotted' : '';\n lines.push(` \"${escapeDot(edge.from)}\" -> \"${escapeDot(edge.to)}\" [label=\"${edge.kind}\"${style}];`);\n }\n lines.push('}');\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction toMermaid(nodes: readonly SpecNode[], edges: readonly Edge[]): string {\n const lines = ['graph LR'];\n const alias = new Map<string, string>();\n nodes.forEach((node, position) => alias.set(node.id, `n${position}`));\n\n for (const node of nodes) {\n const key = alias.get(node.id) as string;\n const label = escapeMermaid(node.kind === 'document' ? node.id : node.text);\n lines.push(node.kind === 'document' ? ` ${key}[\"${label}\"]` : ` ${key}([\"${label}\"])`);\n if (node.kind === 'document' && node.phase !== 'unknown') lines.push(` class ${key} ${node.phase};`);\n }\n for (const edge of edges) {\n const from = alias.get(edge.from);\n const to = alias.get(edge.to);\n if (!from || !to) continue;\n lines.push(` ${from} -->|${edge.kind}| ${to}`);\n }\n for (const [phase, fill] of Object.entries(PHASE_FILL)) {\n if (phase === 'unknown') continue;\n lines.push(` classDef ${phase} fill:${fill},stroke:#5f6368;`);\n }\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction escapeDot(value: string): string {\n return value.replace(/[\"\\\\]/g, '\\\\$&').replace(/\\n/g, '\\\\n');\n}\n\nfunction escapeMermaid(value: string): string {\n return value.replace(/[\"#]/g, ' ').replace(/\\s+/g, ' ').trim();\n}\n"]}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Output formatting.\n *\n * Two audiences, one set of facts. In a terminal the report leads with the file\n * and line so an editor can jump there, indents the supporting evidence, and\n * ends with a concrete next action - because a finding a reader cannot act on\n * is a finding they will learn to scroll past. In CI it emits JSON with every\n * position intact, so a bot can annotate a diff.\n *\n * Colour is opt-out and auto-detected, and the glyphs degrade to ASCII when the\n * terminal cannot render them.\n */\n\nimport { fingerprintOf, type StaleEntry } from './baseline.js';\nimport type { SpecGraph } from './graph.js';\nimport type { ProjectRule } from './project-rules.js';\nimport { formatRef } from './source.js';\nimport { DEFAULT_SEVERITIES, RULE_DESCRIPTIONS, RULE_IDS } from './rules.js';\nimport type { AnalysisResult } from './runner.js';\nimport type {\n AnyRuleId,\n Diagnostic,\n Edge,\n RuleId,\n Severity,\n SourceRef,\n SpecNode,\n SuppressedRef,\n} from './types.js';\n\nexport interface ReporterOptions {\n readonly color?: boolean | undefined;\n readonly ascii?: boolean | undefined;\n readonly verbose?: boolean | undefined;\n /** Stop after this many findings. `0` means no limit. */\n readonly max?: number | undefined;\n /**\n * Rules whose severity `--strict` raised.\n *\n * A finding that only became an error because of a flag has to say so, or the\n * reader cannot tell what would happen without it.\n */\n readonly escalated?: ReadonlySet<AnyRuleId> | undefined;\n /**\n * What a baseline accounted for on this run.\n *\n * Reported as counts rather than as a list: the point of accepted debt is\n * that nobody has to read it every time. `stale` is the ratchet - debt the\n * run did not spend and which can be struck from the file - and `ratchet`\n * says whether this run was asked to fail over it.\n *\n * `entries` carries the stale ones themselves, for the structured formats. A\n * number is enough to know the file has slack and never enough to strike it,\n * and a bot that wants to open the pull request needs the rows.\n */\n readonly baseline?:\n | {\n readonly source: string;\n readonly suppressed: number;\n readonly stale: number;\n readonly ratchet?: boolean | undefined;\n readonly entries?: readonly StaleEntry[] | undefined;\n }\n | undefined;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Environment detection */\n/* -------------------------------------------------------------------------- */\n\nexport interface ColorEnvironment {\n readonly isTTY?: boolean | undefined;\n readonly env?: Readonly<Record<string, string | undefined>> | undefined;\n /**\n * Host platform, defaulting to the real one.\n *\n * Injected for the same reason `env` is: without it half of\n * {@link shouldUseAscii} is unreachable from a test, and a rule about\n * Windows consoles that only Windows can exercise is a rule that breaks on\n * Windows and nowhere else.\n */\n readonly platform?: NodeJS.Platform | undefined;\n}\n\n/** Honours `NO_COLOR`, `FORCE_COLOR` and `TERM=dumb` before falling back to TTY. */\nexport function shouldUseColor(environment: ColorEnvironment = {}): boolean {\n const env = environment.env ?? process.env;\n if (env['NO_COLOR'] !== undefined && env['NO_COLOR'] !== '') return false;\n // `FORCE_COLOR=0` is the conventional way to turn colour off, so it has to\n // win over an attached TTY rather than merely failing to force it on.\n if (env['FORCE_COLOR'] === '0') return false;\n if (env['FORCE_COLOR'] !== undefined && env['FORCE_COLOR'] !== '') return true;\n if (env['TERM'] === 'dumb') return false;\n if (env['CI'] !== undefined && env['CI'] !== '') return false;\n return environment.isTTY === true;\n}\n\n/** True when the terminal is unlikely to render box-drawing and arrows. */\nexport function shouldUseAscii(environment: ColorEnvironment = {}): boolean {\n const env = environment.env ?? process.env;\n if (env['SPEC_GRAPH_ASCII'] !== undefined && env['SPEC_GRAPH_ASCII'] !== '') return true;\n if ((environment.platform ?? process.platform) !== 'win32') return false;\n // Windows Terminal and modern shells set these; the legacy console does not.\n return env['WT_SESSION'] === undefined && env['TERM_PROGRAM'] === undefined;\n}\n\nconst EMPTY_RULES: ReadonlySet<RuleId> = new Set<RuleId>();\n\ntype Paint = (text: string) => string;\n\nexport interface Painter {\n readonly error: Paint;\n readonly warn: Paint;\n readonly info: Paint;\n readonly dim: Paint;\n readonly bold: Paint;\n readonly hint: Paint;\n readonly location: Paint;\n}\n\nexport function createPainter(color: boolean): Painter {\n const wrap = (open: string): Paint => (text) => (color ? `\\u001b[${open}m${text}\\u001b[0m` : text);\n return {\n error: wrap('31'),\n warn: wrap('33'),\n info: wrap('36'),\n dim: wrap('2'),\n bold: wrap('1'),\n hint: wrap('32'),\n location: wrap('4'),\n };\n}\n\ninterface Glyphs {\n readonly error: string;\n readonly warn: string;\n readonly info: string;\n readonly related: string;\n readonly hint: string;\n readonly separator: string;\n}\n\nfunction glyphs(ascii: boolean): Glyphs {\n return ascii\n ? { error: 'x', warn: '!', info: 'i', related: '|', hint: '>', separator: '-' }\n : { error: '\\u2716', warn: '\\u26a0', info: '\\u2139', related: '\\u21b3', hint: '\\u2192', separator: '\\u00b7' };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Human report */\n/* -------------------------------------------------------------------------- */\n\n/** Renders the full terminal report. */\nexport function formatReport(result: AnalysisResult, options: ReporterOptions = {}): string {\n const paint = createPainter(options.color ?? false);\n const marks = glyphs(options.ascii ?? false);\n const lines: string[] = [];\n const { summary } = result;\n\n lines.push(\n `${paint.bold('spec-graph')} ${paint.dim(\n [\n `${summary.documents} ${plural(summary.documents, 'document')}`,\n `${summary.items} ${plural(summary.items, 'item')}`,\n `${summary.edges} ${plural(summary.edges, 'relation')}`,\n `${summary.openObligations} open`,\n ].join(` ${marks.separator} `),\n )}`,\n );\n lines.push('');\n\n const limit = options.max && options.max > 0 ? options.max : result.diagnostics.length;\n const shown = result.diagnostics.slice(0, limit);\n\n const escalated = options.escalated ?? EMPTY_RULES;\n for (const diagnostic of shown) {\n lines.push(...formatDiagnostic(diagnostic, paint, marks, escalated));\n lines.push('');\n }\n\n if (result.diagnostics.length > shown.length) {\n lines.push(paint.dim(` ... and ${result.diagnostics.length - shown.length} more`));\n lines.push('');\n }\n\n if (options.verbose && result.problems.length > 0) {\n for (const problem of result.problems) {\n lines.push(`${paint.warn(marks.warn)} ${paint.location(formatRef(problem.at))} ${problem.message}`);\n }\n lines.push('');\n }\n\n // What the repository told spec-graph to stop looking at. Only under\n // --verbose, and never as a finding: these are not defects, they are the one\n // place a configuration can make the check quieter, and a single over-broad\n // glob looks exactly like a clean repository from the outside. See ADR-0008.\n if (options.verbose && result.corpus.suppressed.length > 0) {\n for (const [target, entry] of groupSuppressed(result.corpus.suppressed)) {\n const where = entry.by === 'family' ? 'ignored family' : 'ignored reference';\n const times = entry.count === 1 ? '' : ` (${entry.count} times)`;\n lines.push(`${paint.dim(marks.info)} ${paint.location(formatRef(entry.at))} ${paint.dim(`${where}: ${target}${times}`)}`);\n }\n lines.push('');\n }\n\n const tally: string[] = [];\n if (summary.errors > 0) tally.push(paint.error(`${summary.errors} ${plural(summary.errors, 'error')}`));\n if (summary.warnings > 0) tally.push(paint.warn(`${summary.warnings} ${plural(summary.warnings, 'warning')}`));\n if (summary.infos > 0) tally.push(paint.info(`${summary.infos} ${plural(summary.infos, 'note')}`));\n tally.push(paint.dim(`${summary.durationMs}ms`));\n const raised = result.diagnostics.filter((diagnostic) => escalated.has(diagnostic.rule)).length;\n if (raised > 0) {\n tally.push(paint.warn(`${raised} raised by --strict`));\n }\n const baseline = options.baseline;\n if (baseline !== undefined && baseline.suppressed > 0) {\n tally.push(paint.dim(`${baseline.suppressed} accepted by ${baseline.source}`));\n }\n lines.push(tally.join(` ${marks.separator} `));\n\n // Under `--ratchet` a paid entry is a failure rather than a note, so it is\n // painted and worded as one.\n const ratcheted = ratchetFailed(baseline);\n if (baseline !== undefined && baseline.stale > 0) {\n // A \"gone\" entry is not the ratchet working, and saying so matters more\n // than the count does: the usual way to produce one is to narrow an include\n // pattern, which loses sight of a defect rather than fixing it.\n const gone = (baseline.entries ?? []).filter((entry) => entry.reason === 'gone').length;\n const because =\n gone === 0\n ? `tighten it: spec-graph check --record-baseline ${baseline.source}`\n : `${gone} of them ${gone === 1 ? 'names a document' : 'name documents'} this run did not see - check the include patterns before re-recording`;\n const text = `${baseline.stale} baseline ${plural(baseline.stale, 'entry', 'entries')} no longer ${\n baseline.stale === 1 ? 'occurs' : 'occur'\n } - ${because}`;\n lines.push(ratcheted ? `${paint.error(marks.error)} ${text}` : paint.dim(text));\n }\n\n lines.push(\n ratcheted\n ? `${paint.error(marks.error)} ${\n result.ok ? 'the baseline is looser than the repository' : 'the specification graph is inconsistent'\n }`\n : result.ok\n ? `${paint.hint(marks.hint === '>' ? 'ok' : '\\u2714')} ${\n summary.errors + summary.warnings === 0\n ? 'the specification graph is consistent'\n : 'no errors - the specification graph holds'\n }`\n : `${paint.error(marks.error)} the specification graph is inconsistent`,\n );\n\n return lines.join('\\n');\n}\n\nfunction formatDiagnostic(\n diagnostic: Diagnostic,\n paint: Painter,\n marks: Glyphs,\n escalated: ReadonlySet<AnyRuleId>,\n): string[] {\n const mark = severityMark(diagnostic.severity, paint, marks);\n const label = escalated.has(diagnostic.rule)\n ? `${diagnostic.rule} ${paint.warn('(strict: warn -> error)')}`\n : diagnostic.rule;\n const lines: string[] = [\n `${mark} ${paint.location(formatRef(diagnostic.at))} ${paint.dim(label)}`,\n ` ${diagnostic.message}`,\n ];\n\n const width = Math.max(0, ...diagnostic.related.map((entry) => formatRef(entry.at).length));\n for (const entry of diagnostic.related) {\n const reference = formatRef(entry.at);\n lines.push(` ${paint.dim(marks.related)} ${paint.dim(reference.padEnd(width))} ${paint.dim(entry.note)}`);\n }\n\n lines.push(` ${paint.hint(marks.hint)} ${paint.hint(diagnostic.hint)}`);\n return lines;\n}\n\nfunction severityMark(severity: Exclude<Severity, 'off'>, paint: Painter, marks: Glyphs): string {\n switch (severity) {\n case 'error':\n return paint.error(marks.error);\n case 'warn':\n return paint.warn(marks.warn);\n case 'info':\n return paint.info(marks.info);\n }\n}\n\nfunction plural(count: number, word: string, plural?: string): string {\n if (count === 1) return word;\n return plural ?? `${word}s`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* JSON */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The machine-readable report.\n *\n * Deliberately flat and versioned: this is the contract a CI annotator or a\n * dashboard builds against, and it must be safe to add fields to it later.\n */\n/**\n * One row per silenced target, with the first place it was written.\n *\n * Grouped because a concept tag used forty times is one decision, and forty\n * lines of it would bury the one entry somebody needs to see. Insertion order\n * is document order, which is already deterministic.\n */\nfunction groupSuppressed(\n suppressed: readonly SuppressedRef[],\n): Map<string, { at: SourceRef; by: SuppressedRef['by']; count: number }> {\n const out = new Map<string, { at: SourceRef; by: SuppressedRef['by']; count: number }>();\n for (const entry of suppressed) {\n const seen = out.get(entry.target);\n if (seen === undefined) out.set(entry.target, { at: entry.at, by: entry.by, count: 1 });\n else seen.count += 1;\n }\n return out;\n}\n\n/**\n * True when a baseline was asked to ratchet and has slack left in it.\n *\n * Both reporters ask, because both have to agree with the exit code: a report\n * that prints \"ok\" above a failing build is worse than no report.\n */\nfunction ratchetFailed(baseline: ReporterOptions['baseline']): boolean {\n return baseline !== undefined && baseline.ratchet === true && baseline.stale > 0;\n}\n\n/* -------------------------------------------------------------------------- */\n/* SARIF */\n/* -------------------------------------------------------------------------- */\n\n/**\n * The static-analysis interchange format, for the tools that already read it.\n *\n * The reason to emit it is not the format, it is what consumes the format:\n * GitHub code scanning turns a SARIF upload into annotations on the diff, and\n * an editor turns it into the problems pane, neither of which needs a line of\n * spec-graph-specific integration.\n *\n * One field earns the whole exercise. `partialFingerprints` is how a consumer\n * tracks a finding across commits without keying on a line number, and\n * spec-graph already has exactly that identifier for exactly that reason - the\n * `rule/document/subject` triple a baseline is keyed on (ADR-0012). The two\n * problems turned out to be the same problem, so the answer is the same answer.\n *\n * Deterministic and timestamp-free, like every other output here. Nothing about\n * a run that has the same findings should produce a different file.\n */\nexport function formatSarif(\n result: AnalysisResult,\n graph: SpecGraph,\n options: {\n version?: string | undefined;\n escalated?: ReadonlySet<AnyRuleId> | undefined;\n projectRules?: readonly ProjectRule[] | undefined;\n } = {},\n): string {\n const escalated = options.escalated ?? EMPTY_RULES;\n // A project rule describes itself by the selector it is, because that is the\n // only honest answer: its message is a template, and a template with `{0}` in\n // it is not a description of anything. See ADR-0016.\n const known: [AnyRuleId, string, Severity][] = [\n ...RULE_IDS.map((id): [AnyRuleId, string, Severity] => [id, RULE_DESCRIPTIONS[id], DEFAULT_SEVERITIES[id]]),\n ...(options.projectRules ?? []).map((rule): [AnyRuleId, string, Severity] => [\n rule.id,\n rule.sources.join(' | '),\n rule.severity,\n ]),\n ];\n const fired = known.filter(([id]) => result.diagnostics.some((diagnostic) => diagnostic.rule === id));\n return `${JSON.stringify(\n {\n $schema: 'https://json.schemastore.org/sarif-2.1.0.json',\n version: '2.1.0',\n runs: [\n {\n tool: {\n driver: {\n name: 'spec-graph',\n informationUri: 'https://github.com/DescentVTT/spec-graph',\n ...(options.version === undefined ? {} : { version: options.version, semanticVersion: options.version }),\n // Only the rules that fired. A driver listing all of them\n // describes the tool; this file describes the run.\n rules: fired.map(([id, description, level]) => ({\n id,\n shortDescription: { text: description },\n defaultConfiguration: { level: sarifLevel(level) },\n })),\n },\n },\n results: result.diagnostics.map((diagnostic) => {\n const print = fingerprintOf(graph, diagnostic);\n return {\n ruleId: diagnostic.rule,\n level: sarifLevel(diagnostic.severity),\n message: { text: `${diagnostic.message}. ${diagnostic.hint}` },\n locations: [sarifLocation(diagnostic.at)],\n ...(diagnostic.related.length === 0\n ? {}\n : {\n relatedLocations: diagnostic.related.map((entry) => ({\n ...sarifLocation(entry.at),\n message: { text: entry.note },\n })),\n }),\n partialFingerprints: {\n specGraphIdentity: `${diagnostic.rule}/${print.document}/${print.subject}`,\n },\n properties: {\n nodes: diagnostic.nodes,\n target: diagnostic.target,\n // A finding that is only an error because of a flag says so\n // here too, so a reviewer reading an annotation is not misled.\n escalated: escalated.has(diagnostic.rule),\n },\n };\n }),\n },\n ],\n },\n null,\n 2,\n )}\\n`;\n}\n\nfunction sarifLevel(severity: Severity): string {\n switch (severity) {\n case 'error':\n return 'error';\n case 'warn':\n return 'warning';\n case 'info':\n return 'note';\n default:\n return 'none';\n }\n}\n\nfunction sarifLocation(at: SourceRef): {\n physicalLocation: {\n artifactLocation: { uri: string };\n region: { startLine: number; startColumn: number; endLine: number; endColumn: number };\n };\n} {\n return {\n physicalLocation: {\n // Already repository-relative and POSIX, which is what SARIF asks for.\n artifactLocation: { uri: at.file },\n region: {\n startLine: at.span.start.line,\n startColumn: at.span.start.column,\n endLine: at.span.end.line,\n endColumn: at.span.end.column,\n },\n },\n };\n}\n\nexport function formatJson(\n result: AnalysisResult,\n options: { escalated?: ReadonlySet<AnyRuleId>; baseline?: ReporterOptions['baseline'] } = {},\n): string {\n const escalated = options.escalated ?? EMPTY_RULES;\n return `${JSON.stringify(\n {\n version: 1,\n ok: result.ok && !ratchetFailed(options.baseline),\n strict: escalated.size > 0,\n ...(options.baseline === undefined ? {} : { baseline: options.baseline }),\n summary: result.summary,\n files: result.files,\n // The audit trail for what configuration silenced. Always present, unlike\n // the human report's --verbose gate: a machine reader that has to ask for\n // it twice will not ask. See ADR-0008.\n suppressed: result.corpus.suppressed.map((entry) => ({\n target: entry.target,\n from: entry.from,\n by: entry.by,\n file: entry.at.file,\n line: entry.at.span.start.line,\n })),\n diagnostics: result.diagnostics.map((diagnostic) => ({\n rule: diagnostic.rule,\n severity: diagnostic.severity,\n // True when this finding is only an error because of --strict.\n escalated: escalated.has(diagnostic.rule),\n message: diagnostic.message,\n hint: diagnostic.hint,\n nodes: diagnostic.nodes,\n target: diagnostic.target,\n file: diagnostic.at.file,\n line: diagnostic.at.span.start.line,\n column: diagnostic.at.span.start.column,\n endLine: diagnostic.at.span.end.line,\n endColumn: diagnostic.at.span.end.column,\n related: diagnostic.related.map((entry) => ({\n file: entry.at.file,\n line: entry.at.span.start.line,\n column: entry.at.span.start.column,\n note: entry.note,\n })),\n })),\n problems: result.problems.map((problem) => ({\n message: problem.message,\n file: problem.at.file,\n line: problem.at.span.start.line,\n })),\n },\n null,\n 2,\n )}\\n`;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Graph export */\n/* -------------------------------------------------------------------------- */\n\nexport type GraphFormat = 'dot' | 'mermaid' | 'json';\n\nexport interface GraphExportOptions {\n /** Leave out `contains` edges and item nodes. */\n readonly documentsOnly?: boolean | undefined;\n}\n\n/** Serialises the graph for Graphviz, Mermaid, or a downstream tool. */\nexport function formatGraph(graph: SpecGraph, format: GraphFormat, options: GraphExportOptions = {}): string {\n const skipItems = options.documentsOnly ?? false;\n const nodes = [...graph.nodes.values()].filter((node) => !skipItems || node.kind === 'document');\n const visible = new Set(nodes.map((node) => node.id));\n\n // Hiding items must not hide what they say. An obligation delegated from an\n // item to a document is lifted onto the item's owner, so the document-level\n // view still shows the handover rather than silently losing it.\n const lifted = skipItems\n ? dedupeEdges(\n graph.edges\n // Containment of an *item* is what hiding items makes redundant.\n // Containment of a specification by the register that holds it is\n // structure between documents, and is what this view exists to show.\n .filter((edge) => edge.kind !== 'contains' || visible.has(edge.to))\n .map((edge) => {\n const from = graph.owningDocument(edge.from)?.id ?? edge.from;\n const to = graph.owningDocument(edge.to)?.id ?? edge.to;\n return from === edge.from && to === edge.to ? edge : { ...edge, from, to, reflexive: from === to };\n })\n .filter((edge) => !edge.reflexive),\n )\n : graph.edges;\n\n const edges = lifted.filter((edge) => visible.has(edge.from) && visible.has(edge.to));\n\n switch (format) {\n case 'dot':\n return toDot(nodes, edges);\n case 'mermaid':\n return toMermaid(nodes, edges);\n case 'json':\n return `${JSON.stringify(\n {\n version: 1,\n nodes: nodes.map(serialiseNode),\n edges: edges.map((edge) => ({\n kind: edge.kind,\n from: edge.from,\n to: edge.to,\n origin: edge.origin,\n declaredIn: edge.declaredIn,\n file: edge.declaredAt.file,\n line: edge.declaredAt.span.start.line,\n })),\n },\n null,\n 2,\n )}\\n`;\n }\n}\n\n/** Collapses duplicate relations produced by lifting item edges onto documents. */\nfunction dedupeEdges(edges: readonly Edge[]): Edge[] {\n const seen = new Set<string>();\n const out: Edge[] = [];\n for (const edge of edges) {\n const key = `${edge.kind} ${edge.from} ${edge.to}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(edge);\n }\n return out;\n}\n\nfunction serialiseNode(node: SpecNode): Record<string, unknown> {\n const base = {\n id: node.id,\n kind: node.kind,\n title: node.title,\n file: node.at.file,\n line: node.at.span.start.line,\n };\n return node.kind === 'document'\n ? { ...base, path: node.path, phase: node.phase, status: node.rawStatus, aliases: node.aliases }\n : {\n ...base,\n document: node.document,\n section: node.section,\n disposition: node.disposition,\n openness: node.openness,\n };\n}\n\n/** Phase colours, chosen to stay legible in both light and dark renderings. */\nconst PHASE_FILL: Readonly<Record<string, string>> = {\n draft: '#e8f0fe',\n active: '#e6f4ea',\n frozen: '#e8eaed',\n retired: '#fce8e6',\n record: '#f3e8fd',\n unknown: '#ffffff',\n};\n\nfunction toDot(nodes: readonly SpecNode[], edges: readonly Edge[]): string {\n const lines = ['digraph spec {', ' rankdir=LR;', ' node [shape=box, style=\"rounded,filled\", fontname=\"sans\"];'];\n for (const node of nodes) {\n const fill = node.kind === 'document' ? (PHASE_FILL[node.phase] ?? '#ffffff') : '#fffbe6';\n const label = node.kind === 'document' ? `${node.id}\\\\n${escapeDot(node.title)}` : escapeDot(node.text);\n const shape = node.kind === 'document' ? 'box' : 'note';\n lines.push(` \"${escapeDot(node.id)}\" [label=\"${label}\", fillcolor=\"${fill}\", shape=${shape}];`);\n }\n for (const edge of edges) {\n const style = edge.kind === 'contains' ? ', style=dotted' : '';\n lines.push(` \"${escapeDot(edge.from)}\" -> \"${escapeDot(edge.to)}\" [label=\"${edge.kind}\"${style}];`);\n }\n lines.push('}');\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction toMermaid(nodes: readonly SpecNode[], edges: readonly Edge[]): string {\n const lines = ['graph LR'];\n const alias = new Map<string, string>();\n nodes.forEach((node, position) => alias.set(node.id, `n${position}`));\n\n for (const node of nodes) {\n const key = alias.get(node.id) as string;\n const label = escapeMermaid(node.kind === 'document' ? node.id : node.text);\n lines.push(node.kind === 'document' ? ` ${key}[\"${label}\"]` : ` ${key}([\"${label}\"])`);\n if (node.kind === 'document' && node.phase !== 'unknown') lines.push(` class ${key} ${node.phase};`);\n }\n for (const edge of edges) {\n const from = alias.get(edge.from);\n const to = alias.get(edge.to);\n if (!from || !to) continue;\n lines.push(` ${from} -->|${edge.kind}| ${to}`);\n }\n for (const [phase, fill] of Object.entries(PHASE_FILL)) {\n if (phase === 'unknown') continue;\n lines.push(` classDef ${phase} fill:${fill},stroke:#5f6368;`);\n }\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction escapeDot(value: string): string {\n return value.replace(/[\"\\\\]/g, '\\\\$&').replace(/\\n/g, '\\\\n');\n}\n\nfunction escapeMermaid(value: string): string {\n return value.replace(/[\"#]/g, ' ').replace(/\\s+/g, ' ').trim();\n}\n"]}
package/dist/resolve.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * one happened to be indexed first.
19
19
  */
20
20
  import type { ExtractedDocument } from './extract.js';
21
- import type { DanglingRef, DocumentNode, Edge, ItemNode, ParseProblem, SpecNode } from './types.js';
21
+ import type { DanglingRef, DocumentNode, Edge, ItemNode, MisreadKey, ParseProblem, SpecNode, SuppressedRef } from './types.js';
22
22
  export interface ResolveOptions {
23
23
  /**
24
24
  * Whether a repository-relative path exists on disk.
@@ -54,6 +54,14 @@ export interface ResolvedCorpus {
54
54
  readonly edges: readonly Edge[];
55
55
  readonly dangling: readonly DanglingRef[];
56
56
  readonly problems: readonly ParseProblem[];
57
+ readonly misreadKeys: readonly MisreadKey[];
58
+ /**
59
+ * References this repository declared none of its business.
60
+ *
61
+ * Not findings, and never reported as such - they are the audit trail for
62
+ * the two settings that can silence one. See ADR-0008.
63
+ */
64
+ readonly suppressed: readonly SuppressedRef[];
57
65
  }
58
66
  /** Resolves every extracted reference against the whole corpus. */
59
67
  export declare function resolveCorpus(extracted: readonly ExtractedDocument[], options?: ResolveOptions): ResolvedCorpus;