@goodbones/core 0.1.0-beta.6 → 0.1.0-beta.8

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 (108) hide show
  1. package/build/dts/core/campaigns.d.ts +140 -0
  2. package/build/dts/core/campaigns.d.ts.map +1 -0
  3. package/build/dts/core/ledger.d.ts +61 -0
  4. package/build/dts/core/ledger.d.ts.map +1 -0
  5. package/build/dts/core/structure.d.ts +1 -0
  6. package/build/dts/core/structure.d.ts.map +1 -1
  7. package/build/dts/domain/architecture-config.d.ts +309 -1
  8. package/build/dts/domain/architecture-config.d.ts.map +1 -1
  9. package/build/dts/domain/facts.d.ts +1 -0
  10. package/build/dts/domain/facts.d.ts.map +1 -1
  11. package/build/dts/domain/report.d.ts +15 -0
  12. package/build/dts/domain/report.d.ts.map +1 -0
  13. package/build/dts/domain/snapshot.d.ts +55 -3
  14. package/build/dts/domain/snapshot.d.ts.map +1 -1
  15. package/build/dts/domain/violation.d.ts +1 -1
  16. package/build/dts/domain/violation.d.ts.map +1 -1
  17. package/build/dts/index.d.ts +11 -3
  18. package/build/dts/index.d.ts.map +1 -1
  19. package/build/dts/infrastructure/campaign-functions.d.ts +7 -0
  20. package/build/dts/infrastructure/campaign-functions.d.ts.map +1 -0
  21. package/build/dts/infrastructure/report-source-fake.d.ts +4 -0
  22. package/build/dts/infrastructure/report-source-fake.d.ts.map +1 -0
  23. package/build/dts/infrastructure/report-source-live.d.ts +3 -0
  24. package/build/dts/infrastructure/report-source-live.d.ts.map +1 -0
  25. package/build/dts/infrastructure/syntax-matcher-fake.d.ts +10 -0
  26. package/build/dts/infrastructure/syntax-matcher-fake.d.ts.map +1 -0
  27. package/build/dts/load/policy.d.ts +14 -0
  28. package/build/dts/load/policy.d.ts.map +1 -1
  29. package/build/dts/manifest/compile.d.ts +2 -1
  30. package/build/dts/manifest/compile.d.ts.map +1 -1
  31. package/build/dts/manifest/json-schema.d.ts.map +1 -1
  32. package/build/dts/manifest/manifest.d.ts +362 -0
  33. package/build/dts/manifest/manifest.d.ts.map +1 -1
  34. package/build/dts/ports/campaign-predicate.d.ts +24 -0
  35. package/build/dts/ports/campaign-predicate.d.ts.map +1 -0
  36. package/build/dts/ports/language.d.ts +2 -0
  37. package/build/dts/ports/language.d.ts.map +1 -1
  38. package/build/dts/ports/report-source.d.ts +12 -0
  39. package/build/dts/ports/report-source.d.ts.map +1 -0
  40. package/build/dts/ports/syntax-matcher.d.ts +21 -0
  41. package/build/dts/ports/syntax-matcher.d.ts.map +1 -0
  42. package/build/dts/testing.d.ts +2 -0
  43. package/build/dts/testing.d.ts.map +1 -1
  44. package/build/esm/core/campaigns.js +732 -0
  45. package/build/esm/core/campaigns.js.map +1 -0
  46. package/build/esm/core/ledger.js +171 -0
  47. package/build/esm/core/ledger.js.map +1 -0
  48. package/build/esm/core/structure.js +5 -2
  49. package/build/esm/core/structure.js.map +1 -1
  50. package/build/esm/domain/architecture-config.js +133 -1
  51. package/build/esm/domain/architecture-config.js.map +1 -1
  52. package/build/esm/domain/report.js +151 -0
  53. package/build/esm/domain/report.js.map +1 -0
  54. package/build/esm/domain/snapshot.js +21 -0
  55. package/build/esm/domain/snapshot.js.map +1 -1
  56. package/build/esm/domain/violation.js.map +1 -1
  57. package/build/esm/index.js +7 -1
  58. package/build/esm/index.js.map +1 -1
  59. package/build/esm/infrastructure/campaign-functions.js +67 -0
  60. package/build/esm/infrastructure/campaign-functions.js.map +1 -0
  61. package/build/esm/infrastructure/report-source-fake.js +6 -0
  62. package/build/esm/infrastructure/report-source-fake.js.map +1 -0
  63. package/build/esm/infrastructure/report-source-live.js +57 -0
  64. package/build/esm/infrastructure/report-source-live.js.map +1 -0
  65. package/build/esm/infrastructure/syntax-matcher-fake.js +33 -0
  66. package/build/esm/infrastructure/syntax-matcher-fake.js.map +1 -0
  67. package/build/esm/load/policy.js +121 -1
  68. package/build/esm/load/policy.js.map +1 -1
  69. package/build/esm/manifest/compile.js +186 -1
  70. package/build/esm/manifest/compile.js.map +1 -1
  71. package/build/esm/manifest/json-schema.js +7 -3
  72. package/build/esm/manifest/json-schema.js.map +1 -1
  73. package/build/esm/manifest/manifest.js +148 -1
  74. package/build/esm/manifest/manifest.js.map +1 -1
  75. package/build/esm/ports/campaign-predicate.js +2 -0
  76. package/build/esm/ports/campaign-predicate.js.map +1 -0
  77. package/build/esm/ports/report-source.js +2 -0
  78. package/build/esm/ports/report-source.js.map +1 -0
  79. package/build/esm/ports/syntax-matcher.js +12 -0
  80. package/build/esm/ports/syntax-matcher.js.map +1 -0
  81. package/build/esm/testing.js +2 -0
  82. package/build/esm/testing.js.map +1 -1
  83. package/package.json +1 -1
  84. package/schema/architecture-node.schema.json +843 -0
  85. package/schema/architecture.schema.json +1253 -0
  86. package/schema/conformance.schema.json +118 -3
  87. package/src/core/campaigns.ts +1006 -0
  88. package/src/core/ledger.ts +242 -0
  89. package/src/core/structure.ts +10 -5
  90. package/src/domain/architecture-config.ts +178 -1
  91. package/src/domain/facts.ts +9 -4
  92. package/src/domain/report.ts +186 -0
  93. package/src/domain/snapshot.ts +52 -0
  94. package/src/domain/violation.ts +4 -1
  95. package/src/index.ts +74 -0
  96. package/src/infrastructure/campaign-functions.ts +98 -0
  97. package/src/infrastructure/report-source-fake.ts +10 -0
  98. package/src/infrastructure/report-source-live.ts +66 -0
  99. package/src/infrastructure/syntax-matcher-fake.ts +51 -0
  100. package/src/load/policy.ts +187 -1
  101. package/src/manifest/compile.ts +221 -0
  102. package/src/manifest/json-schema.ts +7 -3
  103. package/src/manifest/manifest.ts +192 -1
  104. package/src/ports/campaign-predicate.ts +30 -0
  105. package/src/ports/language.ts +6 -0
  106. package/src/ports/report-source.ts +21 -0
  107. package/src/ports/syntax-matcher.ts +42 -0
  108. package/src/testing.ts +2 -0
@@ -2,7 +2,14 @@ import * as Result from "effect/Result";
2
2
  import * as Schema from "effect/Schema";
3
3
  import * as SchemaIssue from "effect/SchemaIssue";
4
4
 
5
- import { DeclarationKind, ResolveConfig } from "../domain/architecture-config.js";
5
+ import {
6
+ CampaignUnit,
7
+ DeclarationKind,
8
+ ImportProbeTarget,
9
+ ProbeDiagnostic,
10
+ ReportFormat,
11
+ ResolveConfig,
12
+ } from "../domain/architecture-config.js";
6
13
  import { ConfigInvalid } from "../domain/architecture-error.js";
7
14
  import {
8
15
  type ManifestLocator,
@@ -260,6 +267,175 @@ const Limits = Schema.Struct({
260
267
  coverage: Schema.optionalKey(CoverageFloors),
261
268
  });
262
269
 
270
+ // A campaign: a migration the repository is running, tracked as an object —
271
+ // a detector, a rationale, a guide, an owner, a definition of done, and a
272
+ // ledger of every place the pattern still occurs. Where a rule says what may
273
+ // never happen, a campaign names what the code is moving away from.
274
+ //
275
+ // The detector is a predicate algebra: `all`, `any` and `not` over leaf
276
+ // terms, the same three words ast-grep uses. Leaf terms reuse the other
277
+ // families' vocabularies where one exists.
278
+
279
+ // A regular expression over the whole repo-relative path, matched as the
280
+ // `structure` naming rules are: with `subject` and `convention`, the capture
281
+ // group named holds the name being judged, and the term holds when the name
282
+ // has the convention's shape.
283
+ const PathTerm = Schema.Struct({
284
+ file: Globs,
285
+ fileNot: Schema.optionalKey(Globs),
286
+ subject: Schema.optionalKey(Schema.Finite),
287
+ convention: Schema.optionalKey(SurfaceConvention),
288
+ });
289
+
290
+ // Holds when some import of the file resolves to the target — a path glob,
291
+ // `{ external: <package> }` or `{ builtin: <module> }` — and, with `symbols`,
292
+ // pulls one of those names across it.
293
+ const ImportsTerm = Schema.Struct({
294
+ resolves: ImportProbeTarget,
295
+ symbols: Schema.optionalKey(Schema.Array(Schema.String)),
296
+ });
297
+
298
+ // Holds for an export site the selectors admit — the `surface` selectors.
299
+ const ExportsTerm = Schema.Struct({
300
+ name: Schema.optionalKey(Globs),
301
+ kinds: Schema.optionalKey(Schema.Array(Schema.Literals(["named", "default", "namespace"]))),
302
+ declares: Schema.optionalKey(Schema.Array(DeclarationKind)),
303
+ reexport: Schema.optionalKey(Schema.Boolean),
304
+ });
305
+
306
+ // Holds for a member site the selectors admit — the `members` selectors.
307
+ const MembersTerm = Schema.Struct({
308
+ subject: Schema.Literals(["members", "calls"]),
309
+ name: Schema.optionalKey(Globs),
310
+ in: Schema.optionalKey(Globs),
311
+ declares: Schema.optionalKey(Schema.Array(DeclarationKind)),
312
+ });
313
+
314
+ // A regular expression over the file's text, multiline.
315
+ const ContentTerm = Schema.Struct({ regex: Schema.String });
316
+
317
+ // How a metavariable is narrowed: by the text it captured, or by what the
318
+ // identifier at its root is bound to — the module it resolves to and the
319
+ // name it was imported as (`member`), which is how `class $N extends $BASE`
320
+ // says "a React component" rather than "any class with a base".
321
+ const CaptureNarrowing = Schema.Struct({
322
+ regex: Schema.optionalKey(Schema.String),
323
+ binding: Schema.optionalKey(
324
+ Schema.Struct({
325
+ resolves: ImportProbeTarget,
326
+ member: Schema.optionalKey(Schema.Array(Schema.String)),
327
+ }),
328
+ ),
329
+ });
330
+
331
+ // An ast-grep rule object — `pattern`, `kind`, `regex`, `has`, `inside`,
332
+ // `precedes`, `follows`, `nthChild`, `all`, `any`, `not` — with `where`
333
+ // beside it. The rule is the engine's to validate; the manifest only knows
334
+ // which keys are a rule's and which is the narrowing. `kind` names are the
335
+ // engine's node kinds, which today are tree-sitter's.
336
+ const SyntaxTerm = Schema.Struct({
337
+ pattern: Schema.optionalKey(Schema.Unknown),
338
+ kind: Schema.optionalKey(Schema.Unknown),
339
+ regex: Schema.optionalKey(Schema.Unknown),
340
+ nthChild: Schema.optionalKey(Schema.Unknown),
341
+ inside: Schema.optionalKey(Schema.Unknown),
342
+ has: Schema.optionalKey(Schema.Unknown),
343
+ precedes: Schema.optionalKey(Schema.Unknown),
344
+ follows: Schema.optionalKey(Schema.Unknown),
345
+ all: Schema.optionalKey(Schema.Unknown),
346
+ any: Schema.optionalKey(Schema.Unknown),
347
+ not: Schema.optionalKey(Schema.Unknown),
348
+ where: Schema.optionalKey(Schema.Record(Schema.String, CaptureNarrowing)),
349
+ });
350
+
351
+ // A finding of another program, read from the output of `command` (run
352
+ // from the repository root, once per `check`) or from `file` (written by
353
+ // an earlier step) in one of the known formats — `tsc`, `eslint --format
354
+ // json`, `oxlint --format json` — or by a `regex` with named groups. Holds
355
+ // for each diagnostic on the file whose code the term speaks to.
356
+ const ReportTerm = Schema.Struct({
357
+ command: Schema.optionalKey(Schema.String),
358
+ file: Schema.optionalKey(Schema.String),
359
+ format: ReportFormat,
360
+ pattern: Schema.optionalKey(Schema.String),
361
+ codes: Schema.optionalKey(Schema.Array(Schema.String)),
362
+ codesNot: Schema.optionalKey(Schema.Array(Schema.String)),
363
+ });
364
+
365
+ export type DetectorSpec =
366
+ | { readonly all: ReadonlyArray<DetectorSpec> }
367
+ | { readonly any: ReadonlyArray<DetectorSpec> }
368
+ | { readonly not: DetectorSpec }
369
+ | { readonly path: typeof PathTerm.Type }
370
+ | { readonly imports: typeof ImportsTerm.Type }
371
+ | { readonly exports: typeof ExportsTerm.Type }
372
+ | { readonly members: typeof MembersTerm.Type }
373
+ // The `structure` parity strings: holds when every named sibling exists.
374
+ | { readonly requires: ReadonlyArray<string> }
375
+ | { readonly content: typeof ContentTerm.Type }
376
+ | { readonly syntax: typeof SyntaxTerm.Type }
377
+ | { readonly report: typeof ReportTerm.Type }
378
+ // `module#export`: a predicate function the host imports before loading.
379
+ | { readonly fn: string };
380
+
381
+ // Each object carries exactly one term key, so a misspelled one is a decode
382
+ // error that names the line rather than a term quietly dropped.
383
+ const DetectorRef = Schema.suspend((): Schema.Codec<DetectorSpec> => DetectorSpec);
384
+
385
+ const DetectorSpec = Schema.Union([
386
+ Schema.Struct({ all: Schema.Array(DetectorRef) }),
387
+ Schema.Struct({ any: Schema.Array(DetectorRef) }),
388
+ Schema.Struct({ not: DetectorRef }),
389
+ Schema.Struct({ path: PathTerm }),
390
+ Schema.Struct({ imports: ImportsTerm }),
391
+ Schema.Struct({ exports: ExportsTerm }),
392
+ Schema.Struct({ members: MembersTerm }),
393
+ Schema.Struct({ requires: Schema.Array(Schema.String) }),
394
+ Schema.Struct({ content: ContentTerm }),
395
+ Schema.Struct({ syntax: SyntaxTerm }),
396
+ Schema.Struct({ report: ReportTerm }),
397
+ Schema.Struct({ fn: Schema.String }),
398
+ ]);
399
+
400
+ // A source the campaign is proven against at load. `path` alone proves a
401
+ // path-shaped detector; `source` is parsed, `edges` answers the `imports`
402
+ // term and a binding narrowing in place of the live resolver, `files`
403
+ // answers `requires` in place of the file system, and `report` answers a
404
+ // `report` term in place of running anything — one-based positions, as a
405
+ // tool prints them.
406
+ const CampaignProbe = Schema.Struct({
407
+ path: Schema.String,
408
+ source: Schema.optionalKey(Schema.String),
409
+ edges: Schema.optionalKey(Schema.Record(Schema.String, ImportProbeTarget)),
410
+ files: Schema.optionalKey(Schema.Array(Schema.String)),
411
+ report: Schema.optionalKey(Schema.Array(ProbeDiagnostic)),
412
+ });
413
+
414
+ // `30d`, `12h`: how long a campaign may go without progress before the
415
+ // conformance report calls it stalled.
416
+ const Duration = Schema.String.check(Schema.isPattern(/^\d+[dh]$/));
417
+
418
+ const Campaign = Schema.Struct({
419
+ // The ledger file's name, and the rule name's tail: `campaign/<id>`.
420
+ id: Schema.String.check(Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)),
421
+ title: Schema.optionalKey(Schema.String),
422
+ why: Schema.String,
423
+ // What a reader at a hit does about it — the message every hit carries.
424
+ how: Schema.String,
425
+ owner: Schema.optionalKey(Schema.String),
426
+ // Which files the campaign selects. Alias-aware globs, as the graph rules
427
+ // take; a campaign's reach is its scope, so it joins no coverage row.
428
+ scope: Globs,
429
+ unit: CampaignUnit,
430
+ detect: DetectorSpec,
431
+ probes: Schema.Struct({
432
+ fires: Schema.Array(CampaignProbe),
433
+ ignores: Schema.optionalKey(Schema.Array(CampaignProbe)),
434
+ }),
435
+ staleAfter: Duration,
436
+ onComplete: Schema.optionalKey(Schema.Literals(["keep", "remove"])),
437
+ });
438
+
263
439
  export const Manifest = Schema.Struct({
264
440
  // How an import specifier becomes a file. Every pattern below is matched
265
441
  // against a resolved path, so this is what makes the rest of the file mean
@@ -274,6 +450,10 @@ export const Manifest = Schema.Struct({
274
450
  exports: Schema.optionalKey(Schema.Array(ExportRestriction)),
275
451
  graph: Schema.optionalKey(Graph),
276
452
  limits: Schema.optionalKey(Limits),
453
+ campaigns: Schema.optionalKey(Schema.Array(Campaign)),
454
+ // Where each campaign's ledger is written: `<ledger>/<id>.json`, relative
455
+ // to the manifest. Defaults to `.architecture-campaigns`.
456
+ ledger: Schema.optionalKey(Schema.String),
277
457
  // Shorthands expanded in every glob, so a pattern reads the way the repo's own
278
458
  // imports do rather than repeating `packages/server/src` on every line.
279
459
  aliases: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
@@ -289,6 +469,17 @@ export type GraphSpec = typeof Graph.Type;
289
469
  export type LimitsSpec = typeof Limits.Type;
290
470
  export type NamingSpec = typeof Naming.Type;
291
471
  export type ExportRestriction = typeof ExportRestriction.Type;
472
+ export type CampaignSpec = typeof Campaign.Type;
473
+ export type CampaignProbeSpec = typeof CampaignProbe.Type;
474
+ export type SyntaxTermSpec = typeof SyntaxTerm.Type;
475
+
476
+ export const DEFAULT_LEDGER_DIR = ".architecture-campaigns";
477
+
478
+ // `30d` → milliseconds. The schema has already refused any other shape.
479
+ export const durationMs = (duration: string): number => {
480
+ const amount = Number(duration.slice(0, -1));
481
+ return amount * (duration.endsWith("h") ? 3_600_000 : 86_400_000);
482
+ };
292
483
 
293
484
  export const globsOf = (globs: string | ReadonlyArray<string>): ReadonlyArray<string> =>
294
485
  typeof globs === "string" ? [globs] : globs;
@@ -0,0 +1,30 @@
1
+ import type { SourceFacts } from "../domain/facts.js";
2
+ import type { SyntaxTree } from "./syntax-matcher.js";
3
+
4
+ // The floor of a campaign's detector: a predicate function the repository
5
+ // writes, named from the manifest as `module#export` and handed to the
6
+ // policy by the host. It is given what the evaluator has about one file and
7
+ // answers either a verdict about the file or the subjects it found in it —
8
+ // a declaration name, or any key a `match` campaign should ledger — each
9
+ // optionally with where it sits. Exported as a public type so a referenced
10
+ // module typechecks on its own.
11
+
12
+ export type Range = {
13
+ // Zero-based.
14
+ readonly start: { readonly line: number; readonly column: number };
15
+ readonly end: { readonly line: number; readonly column: number };
16
+ };
17
+
18
+ export type CampaignPredicateInput = {
19
+ readonly file: string;
20
+ readonly text: string;
21
+ readonly facts: SourceFacts;
22
+ // The file parsed by the scope's syntax matcher, or `null` without one.
23
+ readonly syntax: SyntaxTree | null;
24
+ };
25
+
26
+ export type CampaignSubject = { readonly subject: string; readonly range?: Range };
27
+
28
+ export type CampaignPredicate = (
29
+ input: CampaignPredicateInput,
30
+ ) => boolean | ReadonlyArray<CampaignSubject>;
@@ -4,6 +4,7 @@ import type { ExportFix, ResolveScope } from "../domain/architecture-config.js";
4
4
  import type { ScopeInvalid } from "../domain/architecture-error.js";
5
5
  import type { FactExtractor } from "./fact-extractor.js";
6
6
  import type { ModuleResolver } from "./module-resolver.js";
7
+ import type { SyntaxMatcher } from "./syntax-matcher.js";
7
8
 
8
9
  // Everything the policy needs from one programming language, behind one port.
9
10
  //
@@ -39,6 +40,11 @@ export type Language = {
39
40
  // carry out. A rewrite is written in one module syntax; a rule naming one no
40
41
  // loaded language implements is refused at load.
41
42
  readonly fixes: ReadonlyArray<ExportFix>;
43
+ // Finds expressions of a given shape in one file, for a campaign's `syntax`
44
+ // term. Optional: a pack without one still answers every other family, and
45
+ // a campaign that needs one in its scope is refused at load with a
46
+ // sentence naming the language.
47
+ readonly syntax?: SyntaxMatcher;
42
48
  // A resolver for the files one scope covers. Built once per scope per run;
43
49
  // resolution is the expensive half of linting an architecture. The scope's
44
50
  // `options` are this language's to read, and anything it does not
@@ -0,0 +1,21 @@
1
+ import type { Diagnostic, ReportFormat } from "../domain/report.js";
2
+
3
+ // What a `report` term names: a program to run from the repository root, or
4
+ // a file some earlier step wrote, and the format its output is read in.
5
+ export type ReportSpec = {
6
+ readonly command?: string | undefined;
7
+ readonly file?: string | undefined;
8
+ readonly format: ReportFormat;
9
+ // `regex` only.
10
+ readonly pattern?: string | undefined;
11
+ };
12
+
13
+ // Answers a `report` term for one file. The live source runs the command
14
+ // once per process (a report is about the whole repository, and a campaign
15
+ // asks per file) and reads a file once; a fake answers from a table. The
16
+ // core never spawns anything.
17
+ export type ReportSource = {
18
+ readonly diagnosticsOf: (spec: ReportSpec, file: string) => ReadonlyArray<Diagnostic>;
19
+ };
20
+
21
+ export const NO_REPORTS: ReportSource = { diagnosticsOf: () => [] };
@@ -0,0 +1,42 @@
1
+ // A campaign's `syntax` term asks a question the facts cannot answer: does
2
+ // this file contain an expression of this shape? A syntax matcher parses one
3
+ // file and finds every node matching a rule, returning the matched text, what
4
+ // each metavariable captured, where it sits, and the named declaration it
5
+ // sits in. The rule object is the engine's own — ast-grep's, for the one
6
+ // matcher that exists — and is carried opaquely; the matcher validates it.
7
+ //
8
+ // The matcher knows nothing about imports. Narrowing a capture by what its
9
+ // identifier is bound to is done in the core, against the file's facts and
10
+ // the resolver, so a second engine has only this port to implement.
11
+
12
+ export type Position = {
13
+ // Zero-based line and column.
14
+ readonly line: number;
15
+ readonly column: number;
16
+ };
17
+
18
+ export type SyntaxMatch = {
19
+ readonly text: string;
20
+ // Each metavariable of the rule to the text it captured.
21
+ readonly captures: ReadonlyMap<string, string>;
22
+ readonly range: { readonly start: Position; readonly end: Position };
23
+ // The name of the nearest enclosing named declaration — the match itself
24
+ // when it is one — or `null` for a match at the top level of the file.
25
+ readonly anchor: string | null;
26
+ };
27
+
28
+ export type SyntaxTree = {
29
+ // Every node the rule matches, in source order. A rule the engine cannot
30
+ // read throws, with the engine's own sentence.
31
+ readonly findAll: (rule: unknown) => ReadonlyArray<SyntaxMatch>;
32
+ // The name of the innermost named declaration enclosing a position, or
33
+ // `null` at the top level — how a diagnostic another tool reported at a
34
+ // line is anchored on a declaration rather than on the line.
35
+ readonly anchorAt: (position: Position) => string | null;
36
+ };
37
+
38
+ export type SyntaxMatcher = {
39
+ // Parses one file's text. `null` when the matcher has no grammar for the
40
+ // file's extension, which a campaign treats as a file with no matches.
41
+ readonly parse: (file: string, text: string) => SyntaxTree | null;
42
+ };
package/src/testing.ts CHANGED
@@ -4,3 +4,5 @@
4
4
  export { makeFactExtractorFake } from "./infrastructure/fact-extractor-fake.js";
5
5
  export { makeFileSystemFake } from "./infrastructure/file-system-fake.js";
6
6
  export { makeModuleResolverFake } from "./infrastructure/module-resolver-fake.js";
7
+ export { makeReportSourceFake } from "./infrastructure/report-source-fake.js";
8
+ export { makeSyntaxMatcherFake, type StagedMatch } from "./infrastructure/syntax-matcher-fake.js";