@descent-vtt/spec-guard 0.6.0 → 0.7.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.
package/dist/rules.js ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Which rules govern a path, decided from the specs alone.
3
+ *
4
+ * Pure. Nothing here reads the codebase: whether an assertion's scope reaches a
5
+ * path is arithmetic on the assertion's own `target`, `exclude` and `glob`, the
6
+ * run's skipped directory names, and the path's extension. That is the whole
7
+ * reason a query can answer in milliseconds - and it is also a claim, that the
8
+ * arithmetic agrees with the walk the assertion would really make. The suite
9
+ * holds it to that claim by comparing the two over real trees; ADR-0012 lists
10
+ * the three things arithmetic cannot see.
11
+ *
12
+ * A file is governed or it is not. A directory is governed when some file under
13
+ * it could be, and "could" is doing work: whether `glob="*.py"` matches anything
14
+ * in `src/` is a question about files that are not read here. The rule is shown
15
+ * with its filters rather than being guessed away.
16
+ */
17
+ import path from 'node:path';
18
+ import { createExcludeMatcher, createGlobMatcher } from './glob.js';
19
+ import { isGraphFile } from './graph.js';
20
+ import { ANALYSABLE_EXTENSIONS } from './imports.js';
21
+ import { layerMatcher } from './layers.js';
22
+ /** Whether `child` is `parent` or lies under it. */
23
+ export function within(child, parent) {
24
+ return parent === '.' || child === parent || child.startsWith(`${parent}/`);
25
+ }
26
+ /** Whether the file-reading part of an assertion can read this file at all. */
27
+ function readable(kind, relativePath) {
28
+ if (kind === 'assert-import-cycle')
29
+ return isGraphFile(relativePath);
30
+ if (kind === 'assert-import-absence' || kind === 'assert-import-count' || kind === 'assert-layers') {
31
+ return ANALYSABLE_EXTENSIONS.has(path.posix.extname(relativePath));
32
+ }
33
+ return true;
34
+ }
35
+ /**
36
+ * Whether the walk from `target` would step into a skipped directory on its way
37
+ * to `relativePath`.
38
+ *
39
+ * Only the directories below the target count, which is how the walk behaves: it
40
+ * never checks the name of the directory it was started in, so
41
+ * `target="node_modules/pkg"` is searched and `target="src"` does not reach
42
+ * `src/node_modules`.
43
+ */
44
+ function skippedOnTheWay(target, relativePath, options, isFile) {
45
+ // A target that is the path itself leaves this empty, and it splits into one
46
+ // empty name - which no skipped directory has.
47
+ const directories = path.posix.relative(target, relativePath).split('/');
48
+ if (isFile)
49
+ directories.pop();
50
+ return directories.some((name) => options.scope.skippedDirectories.has(name));
51
+ }
52
+ /**
53
+ * Whether an assertion's scope reaches a path.
54
+ *
55
+ * For a file, exactly what the walk would do: under a target, not excluded, not
56
+ * inside a skipped directory, matched by `glob`, not one of the spec files, and
57
+ * in a language the assertion reads. For a directory, whether any of that could
58
+ * be true of something under it.
59
+ */
60
+ export function governs(assertion, query) {
61
+ if (assertion.kind === 'assert-present') {
62
+ return assertion.files.some((file) => (query.shape === 'file' ? file === query.path : within(file, query.path)));
63
+ }
64
+ const options = assertion.search;
65
+ const excluded = createExcludeMatcher(options.excludeGlobs);
66
+ if (query.shape === 'file') {
67
+ return (assertion.targets.some((target) => within(query.path, target) && !skippedOnTheWay(target, query.path, options, true)) &&
68
+ !excluded(query.path) &&
69
+ createGlobMatcher(options.globs)(query.path) &&
70
+ !options.excludeFiles.has(query.absolutePath) &&
71
+ readable(assertion.kind, query.path));
72
+ }
73
+ return assertion.targets.some((target) => {
74
+ // The directory is inside the target: everything under it is walked unless
75
+ // the directory itself is skipped or excluded - and an excluded directory
76
+ // excludes all of its contents, because `exclude` tests every ancestor.
77
+ if (within(query.path, target)) {
78
+ return !skippedOnTheWay(target, query.path, options, false) && !excluded(query.path);
79
+ }
80
+ // The target is inside the directory: its files are under the directory.
81
+ return within(target, query.path) && !excluded(target);
82
+ });
83
+ }
84
+ /**
85
+ * Where a path sits in a layer order.
86
+ *
87
+ * For a directory, the layer that matches the directory itself - which then
88
+ * matches every file under it, since a layer is an `exclude`-style pattern and
89
+ * those test every ancestor. A directory no layer matches may still hold files
90
+ * that one does, and is reported as belonging to none rather than guessed at.
91
+ */
92
+ export function layerPosition(order, relativePath) {
93
+ const indices = layerMatcher(order)(relativePath);
94
+ const matches = indices.map((index) => order[index]);
95
+ if (indices.length !== 1)
96
+ return { layer: null, position: null, matches, mayImport: [], mustNotImport: [] };
97
+ const index = indices[0];
98
+ return {
99
+ layer: order[index],
100
+ position: index + 1,
101
+ matches,
102
+ mayImport: order.slice(0, index + 1),
103
+ mustNotImport: order.slice(index + 1),
104
+ };
105
+ }
106
+ /**
107
+ * A rule as a report shows it, optionally as it applies to one path.
108
+ *
109
+ * Every field is what the directive said, resolved - never a default invented
110
+ * for display. A text rule reports its `glob` even when empty, because "every
111
+ * file" is a statement about scope a reader should not have to infer.
112
+ */
113
+ export function viewRule(assertion, document, query) {
114
+ const view = {
115
+ document: document.file,
116
+ line: assertion.location.line,
117
+ kind: assertion.kind,
118
+ description: assertion.description,
119
+ reason: assertion.reason ?? null,
120
+ inForce: document.inForce,
121
+ bounds: assertion.bounds,
122
+ };
123
+ if (assertion.kind === 'assert-present')
124
+ return { ...view, files: assertion.files };
125
+ const options = assertion.search;
126
+ const baseline = query ? assertion.baseline.filter((entry) => within(entry.path, query.path)) : assertion.baseline;
127
+ const scoped = { ...view, targets: assertion.targets, exclude: options.excludeGlobs };
128
+ if (assertion.imports === undefined) {
129
+ return {
130
+ ...scoped,
131
+ glob: options.globs,
132
+ symbol: assertion.symbol,
133
+ regex: options.regex,
134
+ word: options.word,
135
+ ignoreCase: options.ignoreCase,
136
+ comments: options.ignoreComments ? 'ignore' : 'include',
137
+ baseline,
138
+ };
139
+ }
140
+ const types = assertion.imports.includeTypes ? 'include' : 'ignore';
141
+ if (assertion.kind === 'assert-import-cycle')
142
+ return { ...scoped, types };
143
+ if (assertion.layers === undefined)
144
+ return { ...scoped, modules: assertion.imports.modules, types, baseline };
145
+ return {
146
+ ...scoped,
147
+ order: assertion.layers,
148
+ types,
149
+ baseline,
150
+ ...(query ? { position: layerPosition(assertion.layers, query.path) } : {}),
151
+ };
152
+ }
153
+ //# sourceMappingURL=rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"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"}
package/dist/runner.d.ts CHANGED
@@ -57,6 +57,15 @@ export interface RunOptions {
57
57
  * default, because the point of reading the status is to honour it.
58
58
  */
59
59
  ignoreStatus?: boolean;
60
+ /**
61
+ * Which resolved assertions to execute. Every one, when unset.
62
+ *
63
+ * For a caller that wants the rules governing a few paths rather than all of
64
+ * them - the MCP server's `check_architecture`. An assertion that is not
65
+ * selected is not executed and not counted; it is still resolved, so a
66
+ * malformed directive is an error whatever is selected.
67
+ */
68
+ select?: (assertion: Assertion) => boolean;
60
69
  }
61
70
  export interface RunResult extends RunReport {
62
71
  /** Spec files that were executed, relative to root. */
@@ -107,6 +116,13 @@ export declare function resolveDirective(directive: Directive, context: ResolveC
107
116
  } | {
108
117
  error: DirectiveError;
109
118
  };
119
+ /**
120
+ * The spec files a search must not count, as absolute paths.
121
+ *
122
+ * A spec names the symbol it forbids, so without this every absence rule would
123
+ * find itself. Empty under `--include-specs`.
124
+ */
125
+ export declare function specExclusions(specFiles: readonly string[], includeSpecs: boolean): ReadonlySet<string>;
110
126
  export interface ExecuteOptions {
111
127
  root: string;
112
128
  engine: Engine;
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
package/dist/runner.js CHANGED
@@ -8,11 +8,11 @@
8
8
  import { promises as fs } from "node:fs";
9
9
  import path from "node:path";
10
10
  import { comparePaths, createCachedEngine, enumerateCandidates, passKey, resolveEngine, runSearches, ANY_FILE_PROBE, ROOT_TARGETS, } from "./engine.js";
11
- import { createExcludeMatcher, expandSpecPatterns, toPosix } from "./glob.js";
11
+ import { createExcludeMatcher, toPosix } from "./glob.js";
12
12
  import { buildGraph, cyclicComponents, edgeKey, isGraphFile, witness, } from "./graph.js";
13
13
  import { ANALYSABLE_EXTENSIONS, createImportIndex, resolveModule, } from "./imports.js";
14
14
  import { checkLayers } from "./layers.js";
15
- import { parseDirectives } from "./parser.js";
15
+ import { readSpecs, specPath } from "./specs.js";
16
16
  import { createScope, DEFAULT_SCOPE, EMPTY_LEDGER, } from "./scope.js";
17
17
  export const DEFAULT_MAX_SNIPPETS = 5;
18
18
  export const DEFAULT_CONCURRENCY = 8;
@@ -477,6 +477,15 @@ export function resolveDirective(directive, context) {
477
477
  return fail(error instanceof Error ? error.message : String(error));
478
478
  }
479
479
  }
480
+ /**
481
+ * The spec files a search must not count, as absolute paths.
482
+ *
483
+ * A spec names the symbol it forbids, so without this every absence rule would
484
+ * find itself. Empty under `--include-specs`.
485
+ */
486
+ export function specExclusions(specFiles, includeSpecs) {
487
+ return new Set(includeSpecs ? undefined : specFiles.map((file) => path.resolve(file)));
488
+ }
480
489
  async function pathExists(candidate) {
481
490
  return (await fs.stat(candidate).catch(() => null)) !== null;
482
491
  }
@@ -551,61 +560,71 @@ async function prepareAssertion(assertion, options) {
551
560
  targets: existingTargets,
552
561
  options: assertion.search,
553
562
  };
554
- if (!options.allowEmptyScope && !assertion.allowEmpty && !(await options.hasFiles(request))) {
555
- {
556
- return {
557
- ...base,
558
- ok: false,
559
- actual: 0,
560
- message: `no files were inspected, so this assertion verified nothing (${EMPTY_SCOPE_HINT})`,
561
- matches: [],
562
- durationMs: elapsed(startedAt),
563
- };
564
- }
563
+ // Every target missing, and that tolerated, leaves nothing to search - and
564
+ // must not be handed to the engine as an empty target list, which the engine
565
+ // reads, by contract, as the root. It used to be: a rule on a deleted
566
+ // `src/auth` searched the whole repository, where `min="1"` could pass on a
567
+ // test file. Found by holding `spec-guard query` to the walk; see ADR-0012.
568
+ const nothingLeft = existingTargets.length === 0;
569
+ if (!options.allowEmptyScope && !assertion.allowEmpty && (nothingLeft || !(await options.hasFiles(request)))) {
570
+ return {
571
+ ...base,
572
+ ok: false,
573
+ actual: 0,
574
+ message: `no files were inspected, so this assertion verified nothing (${EMPTY_SCOPE_HINT})`,
575
+ matches: [],
576
+ durationMs: elapsed(startedAt),
577
+ };
565
578
  }
566
- return {
567
- request,
568
- finish: (search) => {
569
- // A file that could not be read, or whose bytes are not text but did
570
- // contain the symbol, is a hole in the answer rather than a detail of it.
571
- // The count is still reported, because it is still true of everything
572
- // that was read; --strict is for runs where "true of what we read" is not
573
- // good enough.
574
- //
575
- // No filter for uncertainty here any more. The ledger only ever holds
576
- // gaps: the walk drops a policy skip before recording it, and the only
577
- // other reasons the scanner adds are "unreadable" and "binary". Filtering
578
- // again was a second statement of an invariant that is enforced where it
579
- // is created, and being a no-op, nothing could tell it from its absence.
580
- const gaps = search.scope.skipped;
581
- const strictFailure = options.strictTargets && gaps.length > 0;
582
- const { excluded, stale, shows } = applyBaseline(assertion.baseline, search.fileCounts);
583
- const staleFailure = assertion.ratchet === "two-sided" && stale.length > 0;
584
- const actual = search.count - excluded;
585
- return {
586
- ...base,
587
- ok: satisfies(actual, assertion.bounds) && !strictFailure && !staleFailure,
588
- actual,
589
- message: describeOutcome(assertion.bounds, actual, {
590
- excluded,
591
- stale: staleFailure ? stale : [],
592
- gaps: strictFailure ? gaps.length : 0,
593
- }),
594
- baselinedMatches: excluded,
595
- staleBaseline: stale,
596
- fileMatches: [...search.fileCounts].map(([file, count]) => ({ file, count })),
597
- matches: search.matches.filter((match) => shows(match.file)).slice(0, options.maxSnippets),
598
- // Carried as numbers, not prose, so the reporter can total them across a
599
- // run and JSON consumers can act on them.
600
- commentMatches: search.commentMatches,
601
- unclassifiedFiles: search.unclassifiedFiles,
602
- scope: search.scope,
603
- engine: search.engine,
604
- durationMs: elapsed(startedAt),
605
- };
606
- },
579
+ const finish = (search) => {
580
+ // A file that could not be read, or whose bytes are not text but did
581
+ // contain the symbol, is a hole in the answer rather than a detail of it.
582
+ // The count is still reported, because it is still true of everything
583
+ // that was read; --strict is for runs where "true of what we read" is not
584
+ // good enough.
585
+ //
586
+ // No filter for uncertainty here any more. The ledger only ever holds
587
+ // gaps: the walk drops a policy skip before recording it, and the only
588
+ // other reasons the scanner adds are "unreadable" and "binary". Filtering
589
+ // again was a second statement of an invariant that is enforced where it
590
+ // is created, and being a no-op, nothing could tell it from its absence.
591
+ const gaps = search.scope.skipped;
592
+ const strictFailure = options.strictTargets && gaps.length > 0;
593
+ const { excluded, stale, shows } = applyBaseline(assertion.baseline, search.fileCounts);
594
+ const staleFailure = assertion.ratchet === "two-sided" && stale.length > 0;
595
+ const actual = search.count - excluded;
596
+ return {
597
+ ...base,
598
+ ok: satisfies(actual, assertion.bounds) && !strictFailure && !staleFailure,
599
+ actual,
600
+ message: describeOutcome(assertion.bounds, actual, {
601
+ excluded,
602
+ stale: staleFailure ? stale : [],
603
+ gaps: strictFailure ? gaps.length : 0,
604
+ }),
605
+ baselinedMatches: excluded,
606
+ staleBaseline: stale,
607
+ fileMatches: [...search.fileCounts].map(([file, count]) => ({ file, count })),
608
+ matches: search.matches.filter((match) => shows(match.file)).slice(0, options.maxSnippets),
609
+ // Carried as numbers, not prose, so the reporter can total them across a
610
+ // run and JSON consumers can act on them.
611
+ commentMatches: search.commentMatches,
612
+ unclassifiedFiles: search.unclassifiedFiles,
613
+ scope: search.scope,
614
+ engine: search.engine,
615
+ durationMs: elapsed(startedAt),
616
+ };
607
617
  };
618
+ return nothingLeft ? finish(NOTHING_SEARCHED) : { request, finish };
608
619
  }
620
+ const NOTHING_SEARCHED = {
621
+ count: 0,
622
+ commentMatches: 0,
623
+ unclassifiedFiles: 0,
624
+ matches: [],
625
+ fileCounts: new Map(),
626
+ scope: EMPTY_LEDGER,
627
+ };
609
628
  /** How each kind of reference is spelled in a report snippet. */
610
629
  const IMPORT_VERBS = {
611
630
  import: "import",
@@ -637,11 +656,22 @@ async function executeImportAssertion(assertion, options, base, warnings, starte
637
656
  // No symbol: an import assertion never runs a text search, and the walk does
638
657
  // not depend on one. This used to pass `symbol: ""`, an invented value that
639
658
  // reached nothing and that no test could therefore be wrong about.
640
- const enumeration = await enumerateCandidates({
641
- root: options.root,
642
- targets: existingTargets,
643
- options: assertion.search,
644
- });
659
+ //
660
+ // And no walk at all when every target is missing: an empty target list is
661
+ // the root to the engine, as the text rules above found out.
662
+ const enumeration = existingTargets.length === 0
663
+ ? { files: [] }
664
+ : await enumerateCandidates({
665
+ root: options.root,
666
+ targets: existingTargets,
667
+ options: assertion.search,
668
+ });
669
+ // Why a missing target fails the assertion, or null when it does not. Decided
670
+ // before scope, so a rule whose only target is gone says so rather than that
671
+ // its scope is empty - which is true, and not the thing to fix.
672
+ const missing = !options.allowMissingTargets && assertion.missingTargets.length > 0
673
+ ? `target path${assertion.missingTargets.length === 1 ? " does" : "s do"} not exist: ${assertion.missingTargets.join(", ")}`
674
+ : null;
645
675
  // A cycle needs files the resolver can name, which today means JavaScript
646
676
  // and TypeScript; every other import rule reads any language spec-guard
647
677
  // tokenizes. ADR-0011 has the reasons, language by language.
@@ -649,7 +679,7 @@ async function executeImportAssertion(assertion, options, base, warnings, starte
649
679
  const analysable = enumeration.files.filter((file) => cycles
650
680
  ? isGraphFile(file.relativePath)
651
681
  : ANALYSABLE_EXTENSIONS.has(path.posix.extname(file.relativePath)));
652
- if (analysable.length === 0 && !options.allowEmptyScope && !assertion.allowEmpty) {
682
+ if (analysable.length === 0 && missing === null && !options.allowEmptyScope && !assertion.allowEmpty) {
653
683
  // For an import rule the bar is higher than "some file exists": a directory
654
684
  // of YAML has nothing this can read, so a dependency claim about it is a
655
685
  // claim about nothing.
@@ -689,9 +719,7 @@ async function executeImportAssertion(assertion, options, base, warnings, starte
689
719
  walked: enumeration.files.map((file) => file.relativePath),
690
720
  targets: existingTargets,
691
721
  unresolved: unresolved.length,
692
- missing: !options.allowMissingTargets && assertion.missingTargets.length > 0
693
- ? `target path${assertion.missingTargets.length === 1 ? " does" : "s do"} not exist: ${assertion.missingTargets.join(", ")}`
694
- : null,
722
+ missing,
695
723
  };
696
724
  if (cycles)
697
725
  return finishCycles(assertion, options, base, warnings, startedAt, read);
@@ -891,50 +919,42 @@ export async function runSpecGuard(options) {
891
919
  const allowMissingTargets = options.allowMissingTargets ?? false;
892
920
  const allowEmptyScope = options.allowEmptyScope ?? false;
893
921
  const scope = createScope(options.defaultSkips ?? true);
894
- const specFiles = await expandSpecPatterns(options.patterns, root);
895
- const excludeFiles = new Set(options.includeSpecs ? [] : specFiles.map((file) => path.resolve(file)));
922
+ const specs = await readSpecs(options.patterns, root);
923
+ const specFiles = specs.files;
924
+ const excludeFiles = specExclusions(specFiles, options.includeSpecs ?? false);
896
925
  const directives = [];
897
926
  /** Parsed, validated, and then not run: see ADR-0010. */
898
927
  const withheld = [];
899
- const errors = [];
928
+ const errors = [...specs.errors];
900
929
  const inactiveSpecs = [];
901
- for (const file of specFiles) {
902
- const relativeFile = toPosix(path.relative(root, file)) || toPosix(file);
903
- const source = await fs.readFile(file, "utf8").catch((error) => {
904
- errors.push({
905
- location: { file, relativeFile, line: 1, column: 1 },
906
- raw: "",
907
- message: `Unable to read spec file: ${error instanceof Error ? error.message : String(error)}`,
908
- });
909
- return null;
910
- });
911
- if (source === null)
912
- continue;
913
- const parsed = parseDirectives(source, { file, relativeFile });
914
- errors.push(...parsed.errors);
915
- const status = parsed.status;
916
- if (status && !status.active && !(options.ignoreStatus ?? false)) {
917
- withheld.push(...parsed.directives);
930
+ for (const document of specs.documents) {
931
+ if (!document.inForce && !(options.ignoreStatus ?? false)) {
932
+ // A document is only ever out of force because of a status it declares.
933
+ const status = document.status;
934
+ withheld.push(...document.directives);
918
935
  // Recorded even when it held no directives. "docs/adr/0011.md is a
919
936
  // draft" is worth saying to someone wondering why their new rule has no
920
937
  // effect, and a report that only mentions the documents it happened to
921
938
  // find directives in cannot answer that.
922
939
  inactiveSpecs.push({
923
- file: relativeFile,
940
+ file: document.relativeFile,
924
941
  status: status.value,
925
942
  label: status.label,
926
- directives: parsed.directives.length,
943
+ directives: document.directives.length,
927
944
  });
928
945
  continue;
929
946
  }
930
- directives.push(...parsed.directives);
947
+ directives.push(...document.directives);
931
948
  }
932
949
  const assertions = [];
933
950
  for (const directive of directives) {
934
951
  const resolved = resolveDirective(directive, { root, excludeFiles, scope });
935
952
  if ("error" in resolved)
936
953
  errors.push(resolved.error);
937
- else
954
+ // Selection happens after resolution, so a directive that is not selected
955
+ // is still held to being well-formed - the same bargain ADR-0010 strikes
956
+ // for a document that is not in force.
957
+ else if (options.select?.(resolved.assertion) ?? true)
938
958
  assertions.push(resolved.assertion);
939
959
  }
940
960
  // Not in force is not the same as not checked. A withheld directive is still
@@ -1028,7 +1048,7 @@ export async function runSpecGuard(options) {
1028
1048
  errors,
1029
1049
  warnings,
1030
1050
  inactiveSpecs,
1031
- specFiles: specFiles.map((file) => toPosix(path.relative(root, file)) || toPosix(file)),
1051
+ specFiles: specFiles.map((file) => specPath(root, file)),
1032
1052
  };
1033
1053
  }
1034
1054
  //# sourceMappingURL=runner.js.map