@goodbones/core 0.1.0-beta.1 → 0.1.0-beta.10

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 (164) hide show
  1. package/build/dts/core/campaigns.d.ts +141 -0
  2. package/build/dts/core/campaigns.d.ts.map +1 -0
  3. package/build/dts/core/coverage.d.ts +20 -0
  4. package/build/dts/core/coverage.d.ts.map +1 -1
  5. package/build/dts/core/graph.d.ts +2 -0
  6. package/build/dts/core/graph.d.ts.map +1 -1
  7. package/build/dts/core/imports.d.ts +3 -1
  8. package/build/dts/core/imports.d.ts.map +1 -1
  9. package/build/dts/core/ledger.d.ts +61 -0
  10. package/build/dts/core/ledger.d.ts.map +1 -0
  11. package/build/dts/core/slack.d.ts +22 -0
  12. package/build/dts/core/slack.d.ts.map +1 -0
  13. package/build/dts/core/structure.d.ts +1 -0
  14. package/build/dts/core/structure.d.ts.map +1 -1
  15. package/build/dts/domain/architecture-config.d.ts +324 -1
  16. package/build/dts/domain/architecture-config.d.ts.map +1 -1
  17. package/build/dts/domain/architecture-error.d.ts +8 -0
  18. package/build/dts/domain/architecture-error.d.ts.map +1 -1
  19. package/build/dts/domain/facts.d.ts +1 -0
  20. package/build/dts/domain/facts.d.ts.map +1 -1
  21. package/build/dts/domain/manifest-location.d.ts +9 -0
  22. package/build/dts/domain/manifest-location.d.ts.map +1 -0
  23. package/build/dts/domain/report.d.ts +16 -0
  24. package/build/dts/domain/report.d.ts.map +1 -0
  25. package/build/dts/domain/snapshot.d.ts +224 -0
  26. package/build/dts/domain/snapshot.d.ts.map +1 -0
  27. package/build/dts/domain/violation.d.ts +1 -1
  28. package/build/dts/domain/violation.d.ts.map +1 -1
  29. package/build/dts/index.d.ts +24 -9
  30. package/build/dts/index.d.ts.map +1 -1
  31. package/build/dts/infrastructure/campaign-functions.d.ts +7 -0
  32. package/build/dts/infrastructure/campaign-functions.d.ts.map +1 -0
  33. package/build/dts/infrastructure/manifest-file.d.ts +11 -2
  34. package/build/dts/infrastructure/manifest-file.d.ts.map +1 -1
  35. package/build/dts/infrastructure/manifest-include.d.ts +16 -0
  36. package/build/dts/infrastructure/manifest-include.d.ts.map +1 -0
  37. package/build/dts/infrastructure/report-source-fake.d.ts +4 -0
  38. package/build/dts/infrastructure/report-source-fake.d.ts.map +1 -0
  39. package/build/dts/infrastructure/report-source-live.d.ts +3 -0
  40. package/build/dts/infrastructure/report-source-live.d.ts.map +1 -0
  41. package/build/dts/infrastructure/syntax-matcher-fake.d.ts +10 -0
  42. package/build/dts/infrastructure/syntax-matcher-fake.d.ts.map +1 -0
  43. package/build/dts/infrastructure/walk.d.ts +6 -0
  44. package/build/dts/infrastructure/walk.d.ts.map +1 -1
  45. package/build/dts/load/policy.d.ts +16 -0
  46. package/build/dts/load/policy.d.ts.map +1 -1
  47. package/build/dts/manifest/compile.d.ts +7 -2
  48. package/build/dts/manifest/compile.d.ts.map +1 -1
  49. package/build/dts/manifest/expand.d.ts +27 -0
  50. package/build/dts/manifest/expand.d.ts.map +1 -0
  51. package/build/dts/manifest/infer.d.ts +55 -0
  52. package/build/dts/manifest/infer.d.ts.map +1 -0
  53. package/build/dts/manifest/json-schema.d.ts +10 -0
  54. package/build/dts/manifest/json-schema.d.ts.map +1 -0
  55. package/build/dts/manifest/manifest.d.ts +381 -1
  56. package/build/dts/manifest/manifest.d.ts.map +1 -1
  57. package/build/dts/ports/campaign-predicate.d.ts +24 -0
  58. package/build/dts/ports/campaign-predicate.d.ts.map +1 -0
  59. package/build/dts/ports/language.d.ts +4 -0
  60. package/build/dts/ports/language.d.ts.map +1 -1
  61. package/build/dts/ports/report-source.d.ts +14 -0
  62. package/build/dts/ports/report-source.d.ts.map +1 -0
  63. package/build/dts/ports/syntax-matcher.d.ts +21 -0
  64. package/build/dts/ports/syntax-matcher.d.ts.map +1 -0
  65. package/build/dts/testing.d.ts +2 -0
  66. package/build/dts/testing.d.ts.map +1 -1
  67. package/build/esm/core/campaigns.js +778 -0
  68. package/build/esm/core/campaigns.js.map +1 -0
  69. package/build/esm/core/coverage.js +107 -32
  70. package/build/esm/core/coverage.js.map +1 -1
  71. package/build/esm/core/graph.js +45 -0
  72. package/build/esm/core/graph.js.map +1 -1
  73. package/build/esm/core/imports.js +14 -9
  74. package/build/esm/core/imports.js.map +1 -1
  75. package/build/esm/core/ledger.js +171 -0
  76. package/build/esm/core/ledger.js.map +1 -0
  77. package/build/esm/core/slack.js +76 -0
  78. package/build/esm/core/slack.js.map +1 -0
  79. package/build/esm/core/structure.js +5 -2
  80. package/build/esm/core/structure.js.map +1 -1
  81. package/build/esm/domain/architecture-config.js +158 -1
  82. package/build/esm/domain/architecture-config.js.map +1 -1
  83. package/build/esm/domain/architecture-error.js +27 -0
  84. package/build/esm/domain/architecture-error.js.map +1 -1
  85. package/build/esm/domain/manifest-location.js +21 -0
  86. package/build/esm/domain/manifest-location.js.map +1 -0
  87. package/build/esm/domain/report.js +168 -0
  88. package/build/esm/domain/report.js.map +1 -0
  89. package/build/esm/domain/snapshot.js +141 -0
  90. package/build/esm/domain/snapshot.js.map +1 -0
  91. package/build/esm/domain/violation.js.map +1 -1
  92. package/build/esm/index.js +21 -8
  93. package/build/esm/index.js.map +1 -1
  94. package/build/esm/infrastructure/campaign-functions.js +67 -0
  95. package/build/esm/infrastructure/campaign-functions.js.map +1 -0
  96. package/build/esm/infrastructure/manifest-file.js +158 -7
  97. package/build/esm/infrastructure/manifest-file.js.map +1 -1
  98. package/build/esm/infrastructure/manifest-include.js +187 -0
  99. package/build/esm/infrastructure/manifest-include.js.map +1 -0
  100. package/build/esm/infrastructure/report-source-fake.js +6 -0
  101. package/build/esm/infrastructure/report-source-fake.js.map +1 -0
  102. package/build/esm/infrastructure/report-source-live.js +165 -0
  103. package/build/esm/infrastructure/report-source-live.js.map +1 -0
  104. package/build/esm/infrastructure/syntax-matcher-fake.js +33 -0
  105. package/build/esm/infrastructure/syntax-matcher-fake.js.map +1 -0
  106. package/build/esm/infrastructure/walk.js +57 -1
  107. package/build/esm/infrastructure/walk.js.map +1 -1
  108. package/build/esm/load/policy.js +123 -3
  109. package/build/esm/load/policy.js.map +1 -1
  110. package/build/esm/manifest/compile.js +228 -26
  111. package/build/esm/manifest/compile.js.map +1 -1
  112. package/build/esm/manifest/expand.js +116 -0
  113. package/build/esm/manifest/expand.js.map +1 -0
  114. package/build/esm/manifest/infer.js +455 -0
  115. package/build/esm/manifest/infer.js.map +1 -0
  116. package/build/esm/manifest/json-schema.js +135 -0
  117. package/build/esm/manifest/json-schema.js.map +1 -0
  118. package/build/esm/manifest/manifest.js +243 -5
  119. package/build/esm/manifest/manifest.js.map +1 -1
  120. package/build/esm/ports/campaign-predicate.js +2 -0
  121. package/build/esm/ports/campaign-predicate.js.map +1 -0
  122. package/build/esm/ports/report-source.js +7 -0
  123. package/build/esm/ports/report-source.js.map +1 -0
  124. package/build/esm/ports/syntax-matcher.js +12 -0
  125. package/build/esm/ports/syntax-matcher.js.map +1 -0
  126. package/build/esm/testing.js +2 -0
  127. package/build/esm/testing.js.map +1 -1
  128. package/package.json +9 -3
  129. package/schema/architecture-node.schema.json +2405 -0
  130. package/schema/architecture.schema.json +2825 -0
  131. package/schema/conformance.schema.json +775 -0
  132. package/src/core/campaigns.ts +1056 -0
  133. package/src/core/coverage.ts +164 -34
  134. package/src/core/graph.ts +48 -0
  135. package/src/core/imports.ts +29 -13
  136. package/src/core/ledger.ts +242 -0
  137. package/src/core/slack.ts +135 -0
  138. package/src/core/structure.ts +10 -5
  139. package/src/domain/architecture-config.ts +205 -1
  140. package/src/domain/architecture-error.ts +30 -0
  141. package/src/domain/facts.ts +9 -4
  142. package/src/domain/manifest-location.ts +41 -0
  143. package/src/domain/report.ts +203 -0
  144. package/src/domain/snapshot.ts +302 -0
  145. package/src/domain/violation.ts +4 -1
  146. package/src/index.ts +172 -3
  147. package/src/infrastructure/campaign-functions.ts +98 -0
  148. package/src/infrastructure/manifest-file.ts +204 -8
  149. package/src/infrastructure/manifest-include.ts +318 -0
  150. package/src/infrastructure/report-source-fake.ts +10 -0
  151. package/src/infrastructure/report-source-live.ts +192 -0
  152. package/src/infrastructure/syntax-matcher-fake.ts +51 -0
  153. package/src/infrastructure/walk.ts +70 -1
  154. package/src/load/policy.ts +193 -3
  155. package/src/manifest/compile.ts +290 -28
  156. package/src/manifest/expand.ts +183 -0
  157. package/src/manifest/infer.ts +643 -0
  158. package/src/manifest/json-schema.ts +168 -0
  159. package/src/manifest/manifest.ts +339 -11
  160. package/src/ports/campaign-predicate.ts +30 -0
  161. package/src/ports/language.ts +17 -0
  162. package/src/ports/report-source.ts +35 -0
  163. package/src/ports/syntax-matcher.ts +42 -0
  164. package/src/testing.ts +2 -0
@@ -7,6 +7,13 @@ import {
7
7
  EMPTY_BASELINE,
8
8
  makeBaselineFilter,
9
9
  } from "../core/baseline.js";
10
+ import {
11
+ type CampaignPredicate,
12
+ campaignsFailingTheirProbe,
13
+ compileCampaignRules,
14
+ type CompiledCampaign,
15
+ leafTermsOf,
16
+ } from "../core/campaigns.js";
10
17
  import {
11
18
  type CompiledExportRule,
12
19
  compileExportRules,
@@ -22,6 +29,7 @@ import {
22
29
  compileImportRules,
23
30
  rulesFailingTheirProbe,
24
31
  } from "../core/imports.js";
32
+ import { decodeLedger, type Ledger } from "../core/ledger.js";
25
33
  import {
26
34
  type CompiledMemberRule,
27
35
  compileMemberRules,
@@ -44,12 +52,15 @@ import {
44
52
  type PatternInvalid,
45
53
  } from "../domain/architecture-error.js";
46
54
  import type { SourceFacts } from "../domain/facts.js";
55
+ import type { ManifestLocator } from "../domain/manifest-location.js";
47
56
  import { type LoweredRules, lowerManifest } from "../manifest/compile.js";
48
- import { decodeManifest, type Manifest } from "../manifest/manifest.js";
57
+ import { decodeManifest, DEFAULT_LEDGER_DIR, type Manifest } from "../manifest/manifest.js";
49
58
  import type { FactExtractor } from "../ports/fact-extractor.js";
50
59
  import type { FileSystem } from "../ports/file-system.js";
51
60
  import type { Language } from "../ports/language.js";
52
61
  import type { ModuleResolver } from "../ports/module-resolver.js";
62
+ import { NO_REPORTS, type ReportSource } from "../ports/report-source.js";
63
+ import type { SyntaxMatcher } from "../ports/syntax-matcher.js";
53
64
 
54
65
  // A manifest, read by a host, turned into the policy both adapters evaluate.
55
66
  // Decoding, lowering, compiling and probing happen here, once, with whatever
@@ -69,6 +80,19 @@ export type LoadedPolicy = {
69
80
  readonly graph: CompiledGraph;
70
81
  readonly adoption: LoweredRules["adoption"];
71
82
  readonly structure: CompiledStructure;
83
+ // Evaluated by both hosts, one file at a time; the ledgers say which hits
84
+ // are already counted. A campaign with no ledger is one `campaigns init`
85
+ // has not been run for, and `check` says so.
86
+ readonly campaignRules: ReadonlyArray<CompiledCampaign>;
87
+ readonly ledgers: ReadonlyMap<string, Ledger>;
88
+ // Repo-relative; `<ledgerDir>/<id>.json` is a campaign's ledger.
89
+ readonly ledgerDir: string;
90
+ // The predicate functions the host imported for the `fn` terms.
91
+ readonly functions: ReadonlyMap<string, CampaignPredicate>;
92
+ // Answers the `report` terms: the host's live source, or nothing.
93
+ readonly reports: ReportSource;
94
+ // The clock the campaigns are judged by — stalls, timestamps.
95
+ readonly now: number;
72
96
  readonly fileSystem: FileSystem;
73
97
  // The language packs this policy is evaluated with. The walker takes its
74
98
  // extensions from them, and lowering the shape of its probes.
@@ -81,6 +105,9 @@ export type LoadedPolicy = {
81
105
  // Routes each file to the extractor of the language whose scope covers it.
82
106
  // The CLI reads every file through this; the plugin reads oxlint's tree.
83
107
  readonly extractor: FactExtractor;
108
+ // Routes each file to the syntax matcher of the language whose scope
109
+ // covers it; `parse` answers `null` for a file whose language has none.
110
+ readonly syntax: SyntaxMatcher;
84
111
  readonly ignoreUnresolved: ReadonlyArray<RegExp>;
85
112
  // Deprecation notices from reading the manifest. The host prints them once.
86
113
  readonly notices: ReadonlyArray<string>;
@@ -93,8 +120,20 @@ export type LoadPolicyInput = {
93
120
  // The manifest as the host read it — a module's default export, a parsed
94
121
  // document — before decoding.
95
122
  readonly manifest: unknown;
123
+ // From a data file, where a path in the manifest was written, so a decode
124
+ // error names a line. A module manifest has none to give.
125
+ readonly locate?: ManifestLocator | undefined;
96
126
  readonly languages: ReadonlyArray<Language>;
97
127
  readonly fileSystem: FileSystem;
128
+ // The predicate functions the manifest's `fn` terms name, imported by the
129
+ // host before loading — the core touches no module loader.
130
+ readonly functions?: ReadonlyMap<string, CampaignPredicate> | undefined;
131
+ // The host's report source, for the `report` terms. Absent, a term
132
+ // answers nothing — a policy with one is refused, since it would be a
133
+ // campaign that can never fire.
134
+ readonly reports?: ReportSource | undefined;
135
+ // For tests and for a CI that pins the clock; defaults to `Date.now()`.
136
+ readonly now?: number | undefined;
98
137
  };
99
138
 
100
139
  const NOTHING: SourceFacts = {
@@ -140,6 +179,20 @@ const routesOf = (
140
179
  const routeFor = (routes: ReadonlyArray<Route>, file: string): Route | undefined =>
141
180
  routes.find((route) => route.matches.test(file));
142
181
 
182
+ const referencedFunctions = (detect: CompiledCampaign["detect"]): ReadonlyArray<string> => {
183
+ switch (detect.kind) {
184
+ case "all":
185
+ case "any":
186
+ return detect.terms.flatMap(referencedFunctions);
187
+ case "not":
188
+ return referencedFunctions(detect.term);
189
+ case "fn":
190
+ return [detect.name];
191
+ default:
192
+ return [];
193
+ }
194
+ };
195
+
143
196
  // One resolver per scope, built by the scope's language, behind one port that
144
197
  // picks the scope by the importing file.
145
198
  const makeRouter = (
@@ -180,6 +233,58 @@ const makeRoutingExtractor = (routes: ReadonlyArray<Route>): FactExtractor => ({
180
233
  routeFor(routes, file)?.language.extractor.factsOf(file, text) ?? NOTHING,
181
234
  });
182
235
 
236
+ // One matcher per scope, behind one port that picks the scope by the file. A
237
+ // language without one parses nothing, which a `syntax` term reads as no
238
+ // matches — and which the probe check below refuses for a campaign that
239
+ // depends on it.
240
+ const makeRoutingMatcher = (routes: ReadonlyArray<Route>): SyntaxMatcher => ({
241
+ parse: (file, text) => routeFor(routes, file)?.language.syntax?.parse(file, text) ?? null,
242
+ });
243
+
244
+ // The ledgers, read through the port. An absent file is a campaign with no
245
+ // ledger yet; a malformed one is refused, since reading it as empty would
246
+ // report every hit as unrecorded growth.
247
+ const readLedgers = (
248
+ configPath: string,
249
+ fileSystem: FileSystem,
250
+ ledgerDir: string,
251
+ campaigns: ReadonlyArray<CompiledCampaign>,
252
+ ): Result.Result<ReadonlyMap<string, Ledger>, ConfigInvalid> => {
253
+ const ledgers = new Map<string, Ledger>();
254
+ for (const campaign of campaigns) {
255
+ const at = `${ledgerDir}/${campaign.id}.json`;
256
+ const text = fileSystem.readText(at);
257
+ if (text === null) continue;
258
+ let raw: unknown;
259
+ try {
260
+ raw = JSON.parse(text);
261
+ } catch (cause) {
262
+ return Result.fail(
263
+ new ConfigInvalid({ configPath, detail: `the ledger ${at} is not JSON: ${String(cause)}` }),
264
+ );
265
+ }
266
+ const decoded = decodeLedger(raw);
267
+ if (Result.isFailure(decoded)) {
268
+ return Result.fail(
269
+ new ConfigInvalid({
270
+ configPath,
271
+ detail: `the ledger ${at} does not decode:\n${decoded.failure}`,
272
+ }),
273
+ );
274
+ }
275
+ if (decoded.success.id !== campaign.id) {
276
+ return Result.fail(
277
+ new ConfigInvalid({
278
+ configPath,
279
+ detail: `the ledger ${at} says it belongs to "${decoded.success.id}", not "${campaign.id}".`,
280
+ }),
281
+ );
282
+ }
283
+ ledgers.set(campaign.id, decoded.success);
284
+ }
285
+ return Result.succeed(ledgers);
286
+ };
287
+
183
288
  // The baseline is read through the port, so this tier touches no file itself.
184
289
  // An absent or unreadable one carries nothing, which is the safe direction:
185
290
  // every violation reports.
@@ -202,7 +307,7 @@ export const loadPolicy = (
202
307
  ): Result.Result<LoadedPolicy, ConfigInvalid | PatternInvalid> => {
203
308
  const { configPath, fileSystem, languages, repoRoot } = input;
204
309
 
205
- const decoded = decodeManifest(configPath, input.manifest);
310
+ const decoded = decodeManifest(configPath, input.manifest, { locate: input.locate });
206
311
  if (Result.isFailure(decoded)) return Result.fail(decoded.failure);
207
312
  const config = decoded.success.manifest;
208
313
 
@@ -212,7 +317,7 @@ export const loadPolicy = (
212
317
  // The manifest is the authoring surface; these flat rules are the machine's.
213
318
  // The languages tell lowering what a source file in each scope is called, so
214
319
  // a synthetic probe is a file of the scope's language.
215
- const rules = lowerManifest(config, languages);
320
+ const rules = lowerManifest(config, languages, { substitutions: decoded.success.substitutions });
216
321
 
217
322
  // The ceilings. A tier that says "not tightened yet" is a sentence someone
218
323
  // wrote; a ceiling on how many may say so is what keeps the backlog from
@@ -276,6 +381,67 @@ export const loadPolicy = (
276
381
  const structure = compileStructure(rules.structure);
277
382
  if (Result.isFailure(structure)) return Result.fail(structure.failure);
278
383
 
384
+ const campaignRules = compileCampaignRules(rules.campaigns);
385
+ if (Result.isFailure(campaignRules)) return Result.fail(campaignRules.failure);
386
+
387
+ // A `fn` term names a function the host was to import. One it did not is
388
+ // a term that would answer false for every file, and is refused.
389
+ const functions = input.functions ?? new Map<string, CampaignPredicate>();
390
+ const missing = campaignRules.success.flatMap((rule) =>
391
+ referencedFunctions(rule.detect).filter((name) => !functions.has(name)),
392
+ );
393
+ if (missing.length > 0) {
394
+ return Result.fail(
395
+ new ConfigInvalid({
396
+ configPath,
397
+ detail:
398
+ `these campaigns name a predicate function the host did not load: ` +
399
+ `${[...new Set(missing)].join(", ")}. A \`fn\` term is \`module#export\`, resolved ` +
400
+ `relative to the manifest, and the export must be a function.`,
401
+ }),
402
+ );
403
+ }
404
+
405
+ // A `report` term is answered by the host's source. Without one every
406
+ // such campaign would report nothing and look complete.
407
+ const reporting = campaignRules.success.filter((rule) =>
408
+ leafTermsOf(rule.detect).includes("report"),
409
+ );
410
+ if (input.reports === undefined && reporting.length > 0) {
411
+ return Result.fail(
412
+ new ConfigInvalid({
413
+ configPath,
414
+ detail:
415
+ `these campaigns hold a \`report\` term and the host provided no report source: ` +
416
+ `${reporting.map((rule) => rule.name).join(", ")}.`,
417
+ }),
418
+ );
419
+ }
420
+
421
+ // A `syntax` term needs a matcher for the language of every file in its
422
+ // scope. The probes are the files the campaign is proven on; a probe whose
423
+ // language has none would pass no probe and mean nothing.
424
+ const syntaxless = campaignRules.success.flatMap((rule) => {
425
+ if (!leafTermsOf(rule.detect).includes("syntax")) return [];
426
+ return [...rule.probes.fires, ...rule.probes.ignores].flatMap((probe) => {
427
+ const route = routeFor(routes.success, probe.path);
428
+ return route === undefined || route.language.syntax !== undefined
429
+ ? []
430
+ : [`${rule.name} (${probe.path}: ${route.language.id} carries no syntax matcher)`];
431
+ });
432
+ });
433
+ if (syntaxless.length > 0) {
434
+ return Result.fail(
435
+ new ConfigInvalid({
436
+ configPath,
437
+ detail:
438
+ `these campaigns hold a \`syntax\` term in a scope whose language has no syntax ` +
439
+ `matcher: ${[...new Set(syntaxless)].join(", ")}. Compose the language pack with ` +
440
+ `one (\`@goodbones/ast-grep\` for TypeScript), or write the detector without it.`,
441
+ }),
442
+ );
443
+ }
444
+
279
445
  // A probe carrying a source snippet is parsed by the extractor of the
280
446
  // language whose scope covers the probe's file — the same extractor the CLI
281
447
  // reads that file through. The plugin reads through oxlint's tree instead,
@@ -300,6 +466,19 @@ export const loadPolicy = (
300
466
  ),
301
467
  ...structureRulesFailingTheirProbe(structure.success),
302
468
  ...graphRulesFailingTheirProbe(graph.success),
469
+ ...campaignsFailingTheirProbe(
470
+ campaignRules.success,
471
+ extractor,
472
+ (file) => routeFor(routes.success, file)?.language.syntax ?? null,
473
+ functions,
474
+ ).map((failed) =>
475
+ failed.outOfScope === true
476
+ ? `${failed.name} (its probe ${failed.probe.path} is outside the campaign's own scope)`
477
+ : failed.expected === "fires"
478
+ ? `${failed.name} (fires probe ${failed.probe.path} did not fire)`
479
+ : `${failed.name} (ignores probe ${failed.probe.path} fired` +
480
+ (failed.admittedBy === undefined ? ")" : `, admitted by \`${failed.admittedBy}\`)`),
481
+ ),
303
482
  ];
304
483
  if (vacuous.length > 0) {
305
484
  return Result.fail(
@@ -321,6 +500,10 @@ export const loadPolicy = (
321
500
  const resolver = makeRouter(configPath, repoRoot, routes.success);
322
501
  if (Result.isFailure(resolver)) return Result.fail(resolver.failure);
323
502
 
503
+ const ledgerDir = config.ledger ?? DEFAULT_LEDGER_DIR;
504
+ const ledgers = readLedgers(configPath, fileSystem, ledgerDir, campaignRules.success);
505
+ if (Result.isFailure(ledgers)) return Result.fail(ledgers.failure);
506
+
324
507
  return Result.succeed({
325
508
  repoRoot,
326
509
  config,
@@ -331,11 +514,18 @@ export const loadPolicy = (
331
514
  graph: graph.success,
332
515
  adoption: rules.adoption,
333
516
  structure: structure.success,
517
+ campaignRules: campaignRules.success,
518
+ ledgers: ledgers.success,
519
+ ledgerDir,
520
+ functions,
521
+ reports: input.reports ?? NO_REPORTS,
522
+ now: input.now ?? Date.now(),
334
523
  fileSystem,
335
524
  languages,
336
525
  baseline: makeBaselineFilter(readBaseline(fileSystem, config.baseline)),
337
526
  resolver: resolver.success,
338
527
  extractor,
528
+ syntax: makeRoutingMatcher(routes.success),
339
529
  ignoreUnresolved: (config.resolve.ignoreUnresolved ?? []).map(
340
530
  (pattern: string) => new RegExp(pattern),
341
531
  ),