@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
package/src/index.ts CHANGED
@@ -14,6 +14,25 @@ export {
14
14
  staleEntriesOf,
15
15
  unbaselined,
16
16
  } from "./core/baseline.js";
17
+ export {
18
+ type CampaignHit,
19
+ type CampaignInput,
20
+ campaignsFailingTheirProbe,
21
+ campaignsSelecting,
22
+ compileCampaignRule,
23
+ compileCampaignRules,
24
+ type CompiledCampaign,
25
+ type CompiledDetector,
26
+ evaluateCampaign,
27
+ evaluateCampaigns,
28
+ explainCampaign,
29
+ type FailedProbe,
30
+ leafTermsOf,
31
+ matchKeyOf,
32
+ probeInputOf,
33
+ reportSpecsOf,
34
+ type TermAnswer,
35
+ } from "./core/campaigns.js";
17
36
  export {
18
37
  type Coverage,
19
38
  type CoverageFamily,
@@ -21,6 +40,12 @@ export {
21
40
  coverageOf,
22
41
  coverageShortfalls,
23
42
  fractionsOf,
43
+ type Reach,
44
+ reachOf,
45
+ type Residue,
46
+ residueOf,
47
+ type Vacancy,
48
+ vacancyOf,
24
49
  } from "./core/coverage.js";
25
50
  export {
26
51
  type BindingEdge,
@@ -37,17 +62,20 @@ export {
37
62
  export {
38
63
  type CompiledGraph,
39
64
  compileGraphRules,
65
+ cyclesIn,
40
66
  EMPTY_GRAPH_RULES,
41
67
  evaluateGraph,
42
68
  type Graph,
43
69
  graphRulesFailingTheirProbe,
44
70
  hasGraphRules,
71
+ heightOf,
45
72
  } from "./core/graph.js";
46
73
  export {
47
74
  type CompiledImportRule,
48
75
  compileImportRule,
49
76
  compileImportRules,
50
77
  evaluateImportEdge,
78
+ evaluateResolvedEdge,
51
79
  evaluateSelectedEdge,
52
80
  type ImportEdge,
53
81
  probeTargetOf,
@@ -55,6 +83,27 @@ export {
55
83
  rulesSelecting,
56
84
  type SelectedRule,
57
85
  } from "./core/imports.js";
86
+ export {
87
+ allowed,
88
+ decodeLedger,
89
+ EMPTY_LEDGER,
90
+ entryOf,
91
+ isComplete,
92
+ isStalled,
93
+ type Ledger,
94
+ ledgerArithmeticHolds,
95
+ ledgerOf,
96
+ Ledger as LedgerSchema,
97
+ newEntriesOf,
98
+ progressOf,
99
+ pruned,
100
+ reconcile,
101
+ type Reconciliation,
102
+ type Regression,
103
+ type RegressionRecord,
104
+ serializeLedger,
105
+ staleEntriesOf as staleLedgerEntriesOf,
106
+ } from "./core/ledger.js";
58
107
  export {
59
108
  type CompiledMemberRule,
60
109
  compileMemberRules,
@@ -62,6 +111,13 @@ export {
62
111
  memberRulesFailingTheirProbe,
63
112
  memberRulesSelecting,
64
113
  } from "./core/members.js";
114
+ export {
115
+ type Concentration,
116
+ type ObservedEdge,
117
+ type Slack,
118
+ slackOf,
119
+ type SlackReport,
120
+ } from "./core/slack.js";
65
121
  export {
66
122
  type CompiledStructure,
67
123
  compileStructure,
@@ -78,8 +134,13 @@ export {
78
134
  surfaceRulesSelecting,
79
135
  } from "./core/surface.js";
80
136
  export {
137
+ type Allowance,
81
138
  type BindingKind,
139
+ type CampaignProbe,
140
+ type CampaignRule,
141
+ type CampaignUnit,
82
142
  type DeclarationKind,
143
+ type Detector,
83
144
  type ExportFix,
84
145
  type ExportRule,
85
146
  type GraphConfig,
@@ -91,6 +152,7 @@ export {
91
152
  type ImportRule,
92
153
  type MemberRule,
93
154
  type MemberSubject,
155
+ type ProbeDiagnostic,
94
156
  type ResolveConfig,
95
157
  type ResolveScope,
96
158
  type StructureConfig,
@@ -100,6 +162,7 @@ export {
100
162
  ConfigInvalid,
101
163
  ImportUnresolved,
102
164
  PatternInvalid,
165
+ ReportUnavailable,
103
166
  ScopeInvalid,
104
167
  } from "./domain/architecture-error.js";
105
168
  export {
@@ -108,24 +171,118 @@ export {
108
171
  type MemberSite,
109
172
  type SourceFacts,
110
173
  } from "./domain/facts.js";
174
+ export {
175
+ type ManifestLocator,
176
+ type ManifestPath,
177
+ type ManifestPosition,
178
+ renderManifestPath,
179
+ } from "./domain/manifest-location.js";
180
+ export {
181
+ type Diagnostic,
182
+ indexByFile,
183
+ parseReport,
184
+ type ParseReportOptions,
185
+ type ReportFormat,
186
+ uniqueDiagnostics,
187
+ } from "./domain/report.js";
188
+ export {
189
+ CONFORMANCE_MEASURES,
190
+ type ConformanceMeasure,
191
+ decodeSnapshot,
192
+ type Snapshot,
193
+ SNAPSHOT_SCHEMA_ID,
194
+ SNAPSHOT_VERSION,
195
+ type SnapshotCampaign,
196
+ type SnapshotConcentration,
197
+ snapshotJsonSchema,
198
+ Snapshot as SnapshotSchema,
199
+ type SnapshotSlack,
200
+ type SnapshotVacancy,
201
+ type SnapshotViolation,
202
+ } from "./domain/snapshot.js";
111
203
  export {
112
204
  fingerprintOf,
113
205
  formatMessage,
114
206
  type Violation,
115
207
  type ViolationKind,
116
208
  } from "./domain/violation.js";
209
+ export {
210
+ loadCampaignFunctions,
211
+ type LoadedCampaignFunctions,
212
+ } from "./infrastructure/campaign-functions.js";
117
213
  export { makeFileSystemLive } from "./infrastructure/file-system-live.js";
118
- export { DEFAULT_CONFIG_FILENAME, readManifestFile } from "./infrastructure/manifest-file.js";
119
- export { listSourceFiles, type WalkedLanguage } from "./infrastructure/walk.js";
214
+ export {
215
+ findManifestFile,
216
+ formatManifestYaml,
217
+ MANIFEST_FILENAMES,
218
+ type ManifestFile,
219
+ readManifestFile,
220
+ } from "./infrastructure/manifest-file.js";
221
+ export {
222
+ expandIncludes,
223
+ type IncludedManifest,
224
+ type IncludeReader,
225
+ type SourceDocument,
226
+ } from "./infrastructure/manifest-include.js";
227
+ export { makeReportSourceLive } from "./infrastructure/report-source-live.js";
228
+ export {
229
+ listPackageRoots,
230
+ listSourceFiles,
231
+ type PackagedLanguage,
232
+ type PackageRoot,
233
+ type WalkedLanguage,
234
+ } from "./infrastructure/walk.js";
120
235
  export { type LoadedPolicy, loadPolicy, type LoadPolicyInput } from "./load/policy.js";
121
- export { type LoweredRules, lowerManifest, type ProbeLanguage } from "./manifest/compile.js";
122
236
  export {
237
+ type LoweredRules,
238
+ lowerManifest,
239
+ type LowerOptions,
240
+ type ProbeLanguage,
241
+ } from "./manifest/compile.js";
242
+ export {
243
+ type ExpandedManifest,
244
+ type ExpandIssue,
245
+ expandManifest,
246
+ type Origin,
247
+ originOf,
248
+ type Substitution,
249
+ } from "./manifest/expand.js";
250
+ export {
251
+ type Candidate,
252
+ candidatesOf,
253
+ type Generalization,
254
+ type InferInput,
255
+ inferManifest,
256
+ type InferOptions,
257
+ type InferPackage,
258
+ type Inferred,
259
+ type InferredTarget,
260
+ singularOf,
261
+ } from "./manifest/infer.js";
262
+ export {
263
+ MANIFEST_NODE_SCHEMA_ID,
264
+ MANIFEST_SCHEMA_ID,
265
+ manifestJsonSchema,
266
+ manifestNodeJsonSchema,
267
+ } from "./manifest/json-schema.js";
268
+ export {
269
+ type CampaignSpec,
123
270
  type DecodedManifest,
124
271
  decodeManifest,
272
+ type DecodeManifestOptions,
273
+ DEFAULT_LEDGER_DIR,
274
+ type DetectorSpec,
275
+ durationMs,
125
276
  type Manifest,
126
277
  type ManifestNode,
127
278
  Manifest as ManifestSchema,
128
279
  } from "./manifest/manifest.js";
280
+ export {
281
+ type CampaignPredicate,
282
+ type CampaignPredicateInput,
283
+ type CampaignSubject,
284
+ type Range,
285
+ } from "./ports/campaign-predicate.js";
129
286
  export { type FactExtractor } from "./ports/fact-extractor.js";
130
287
  export { type FileSystem } from "./ports/file-system.js";
131
288
  export { type Language } from "./ports/language.js";
@@ -134,3 +291,15 @@ export {
134
291
  type ModuleResolver,
135
292
  type ResolvedTarget,
136
293
  } from "./ports/module-resolver.js";
294
+ export {
295
+ NO_REPORTS,
296
+ type ReportSource,
297
+ reportSourcesOf,
298
+ type ReportSpec,
299
+ } from "./ports/report-source.js";
300
+ export {
301
+ type Position,
302
+ type SyntaxMatch,
303
+ type SyntaxMatcher,
304
+ type SyntaxTree,
305
+ } from "./ports/syntax-matcher.js";
@@ -0,0 +1,98 @@
1
+ import * as path from "node:path";
2
+ import { pathToFileURL } from "node:url";
3
+
4
+ import { ConfigInvalid } from "../domain/architecture-error.js";
5
+ import type { CampaignPredicate } from "../ports/campaign-predicate.js";
6
+
7
+ // A campaign's `fn` term names a predicate function as `module#export`,
8
+ // resolved relative to the root manifest. Importing it touches the module
9
+ // loader, which is why this lives in infrastructure and both hosts call it
10
+ // before `loadPolicy`: the core receives the functions as a map and never
11
+ // imports anything.
12
+ //
13
+ // A `.mjs` manifest may write the function itself in place of the string.
14
+ // It is lifted out here, keyed by a synthesized name, and the manifest handed
15
+ // back with the string in its place — so the schema, which decodes a data
16
+ // file and a module alike, sees one shape. The whole value is walked, not
17
+ // only `campaigns`, so a term written in a `defs` fragment is found too.
18
+
19
+ export type LoadedCampaignFunctions = {
20
+ readonly functions: ReadonlyMap<string, CampaignPredicate>;
21
+ // The manifest with every function value replaced by its synthesized name.
22
+ readonly manifest: unknown;
23
+ };
24
+
25
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
26
+ typeof value === "object" && value !== null && !Array.isArray(value);
27
+
28
+ const REFERENCE = /^(.+)#([A-Za-z_$][\w$]*)$/;
29
+
30
+ // A `fn` term is an object whose one key is `fn`.
31
+ const isFnTerm = (value: Record<string, unknown>): boolean =>
32
+ "fn" in value && Object.keys(value).length === 1;
33
+
34
+ export const loadCampaignFunctions = async (
35
+ manifestPath: string,
36
+ rawManifest: unknown,
37
+ ): Promise<LoadedCampaignFunctions> => {
38
+ const functions = new Map<string, CampaignPredicate>();
39
+ const root = path.dirname(manifestPath);
40
+ const refuse = (detail: string): never => {
41
+ throw new ConfigInvalid({ configPath: manifestPath, detail });
42
+ };
43
+
44
+ const imported = async (reference: string): Promise<void> => {
45
+ if (functions.has(reference)) return;
46
+ const parsed = REFERENCE.exec(reference);
47
+ if (parsed === null) {
48
+ return refuse(
49
+ `the \`fn\` term ${JSON.stringify(reference)} is not \`module#export\`: name the ` +
50
+ `module (relative to the manifest) and the export, joined by \`#\`.`,
51
+ );
52
+ }
53
+ const [, module = "", exportName = ""] = parsed;
54
+ const at = path.resolve(root, module);
55
+ let loaded: unknown;
56
+ try {
57
+ loaded = await import(pathToFileURL(at).href);
58
+ } catch (cause) {
59
+ return refuse(
60
+ `the \`fn\` term ${JSON.stringify(reference)} names a module that does not load: ${String(cause)}`,
61
+ );
62
+ }
63
+ const found = isRecord(loaded) ? loaded[exportName] : undefined;
64
+ if (typeof found !== "function") {
65
+ return refuse(
66
+ `the \`fn\` term ${JSON.stringify(reference)} names an export that is not a function ` +
67
+ `(${found === undefined ? "absent" : typeof found}).`,
68
+ );
69
+ }
70
+ functions.set(reference, found as CampaignPredicate);
71
+ };
72
+
73
+ let synthesized = 0;
74
+ const lift = async (value: unknown): Promise<unknown> => {
75
+ if (Array.isArray(value)) {
76
+ const items: Array<unknown> = [];
77
+ for (const item of value) items.push(await lift(item));
78
+ return items;
79
+ }
80
+ if (!isRecord(value)) return value;
81
+ if (isFnTerm(value)) {
82
+ const fn = value.fn;
83
+ if (typeof fn === "function") {
84
+ synthesized += 1;
85
+ const name = `<manifest>#fn${String(synthesized)}`;
86
+ functions.set(name, fn as CampaignPredicate);
87
+ return { fn: name };
88
+ }
89
+ if (typeof fn === "string") await imported(fn);
90
+ return value;
91
+ }
92
+ const rebuilt: Record<string, unknown> = {};
93
+ for (const [key, entry] of Object.entries(value)) rebuilt[key] = await lift(entry);
94
+ return rebuilt;
95
+ };
96
+
97
+ return { functions, manifest: await lift(rawManifest) };
98
+ };
@@ -1,17 +1,213 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import * as path from "node:path";
1
3
  import { pathToFileURL } from "node:url";
2
4
 
5
+ import {
6
+ type Document,
7
+ isAlias,
8
+ isMap,
9
+ isScalar,
10
+ isSeq,
11
+ LineCounter,
12
+ type Node,
13
+ type Pair,
14
+ parseDocument,
15
+ stringify,
16
+ } from "yaml";
17
+
3
18
  import { ConfigInvalid } from "../domain/architecture-error.js";
19
+ import type {
20
+ ManifestLocator,
21
+ ManifestPath,
22
+ ManifestPosition,
23
+ } from "../domain/manifest-location.js";
24
+ import { expandIncludes, type SourceDocument } from "./manifest-include.js";
25
+
26
+ // The manifest, as the files on disk state it, before any decoding. A data
27
+ // file — YAML, or JSON, which YAML 1.2 contains — is what any host in any
28
+ // language can read, and is the form the docs are written in. A JavaScript
29
+ // module is the escape hatch a Node host alone honours: for a manifest
30
+ // generated from other data, and for what the ecosystem expects of a config
31
+ // file. Either may `include` further data files, so a monorepo's policy can
32
+ // live beside the packages it governs. Whichever it is, what comes out is one
33
+ // `unknown` value for the decoder, and, from the data formats, a way to turn
34
+ // a path in it back into a file and a line.
35
+
36
+ // In discovery order. A repository with none of these is told all four; one
37
+ // with more than one is refused, so nobody edits the wrong file for a week.
38
+ export const MANIFEST_FILENAMES = [
39
+ "architecture.yaml",
40
+ "architecture.yml",
41
+ "architecture.json",
42
+ "architecture.config.mjs",
43
+ ] as const;
4
44
 
5
- export const DEFAULT_CONFIG_FILENAME = "architecture.config.mjs";
45
+ export type ManifestFile = {
46
+ readonly configPath: string;
47
+ readonly manifest: unknown;
48
+ // Absent for a JavaScript module that includes nothing, which has no
49
+ // positions to give.
50
+ readonly locate: ManifestLocator | undefined;
51
+ // Every file the manifest was read from, the root first: what a host that
52
+ // caches or watches the policy has to look at.
53
+ readonly files: ReadonlyArray<string>;
54
+ };
55
+
56
+ export const findManifestFile = (repoRoot: string): string => {
57
+ const present = MANIFEST_FILENAMES.filter((name) => existsSync(path.resolve(repoRoot, name)));
58
+ const [found] = present;
59
+ if (found === undefined) {
60
+ throw new ConfigInvalid({
61
+ configPath: repoRoot,
62
+ detail:
63
+ `no architecture manifest found. Looked for ${MANIFEST_FILENAMES.join(", ")} ` +
64
+ `in this directory; \`architecture init\` writes a starter architecture.yaml.`,
65
+ });
66
+ }
67
+ if (present.length > 1) {
68
+ throw new ConfigInvalid({
69
+ configPath: repoRoot,
70
+ detail:
71
+ `more than one architecture manifest is present (${present.join(", ")}), and only one ` +
72
+ `can be the policy. Delete the others, or name one with ARCHITECTURE_CONFIG.`,
73
+ });
74
+ }
75
+ return path.resolve(repoRoot, found);
76
+ };
77
+
78
+ const extensionOf = (configPath: string): string => path.extname(configPath).toLowerCase();
79
+
80
+ const isDataManifest = (configPath: string): boolean =>
81
+ [".yaml", ".yml", ".json"].includes(extensionOf(configPath));
82
+
83
+ const isModuleManifest = (configPath: string): boolean =>
84
+ [".mjs", ".js", ".cjs"].includes(extensionOf(configPath));
85
+
86
+ // Whichever form the root takes, an `include` in it names a data file, read
87
+ // by the same parser; the locator that comes back answers across every file.
88
+ export const readManifestFile = async (configPath: string): Promise<ManifestFile> => {
89
+ const root = isDataManifest(configPath)
90
+ ? parseDataFile(configPath)
91
+ : isModuleManifest(configPath)
92
+ ? await readModule(configPath)
93
+ : undefined;
94
+ if (root === undefined) {
95
+ throw new ConfigInvalid({
96
+ configPath,
97
+ detail:
98
+ "a manifest is a .yaml, .yml or .json file, or a .mjs/.js module — " +
99
+ `not ${JSON.stringify(path.basename(configPath))}.`,
100
+ });
101
+ }
102
+ const assembled = expandIncludes(configPath, root, { exists: existsSync, read: parseDataFile });
103
+ return {
104
+ configPath,
105
+ manifest: assembled.value,
106
+ locate: assembled.locate,
107
+ files: assembled.files,
108
+ };
109
+ };
6
110
 
7
- // The manifest, as the file on disk states it, before any decoding. Today that
8
- // is a JavaScript module and its default export; a manifest in another format
9
- // changes this function and nothing behind it.
10
- export const readManifestFile = async (configPath: string): Promise<unknown> => {
111
+ const readModule = async (configPath: string): Promise<SourceDocument> => {
11
112
  const module: unknown = await import(pathToFileURL(configPath).href).catch((cause: unknown) => {
12
113
  throw new ConfigInvalid({ configPath, detail: String(cause) });
13
114
  });
14
- return typeof module === "object" && module !== null && "default" in module
15
- ? module.default
16
- : module;
115
+ const value =
116
+ typeof module === "object" && module !== null && "default" in module ? module.default : module;
117
+ return { value, locate: undefined };
17
118
  };
119
+
120
+ // YAML 1.2 core schema, which is what a reader without a YAML background
121
+ // expects: `on` and `no` are strings, not booleans. Merge keys (`<<`) resolve,
122
+ // because the parser handles them before the manifest sees the document, and
123
+ // duplicate keys are refused rather than last-one-wins. A tag the parser does
124
+ // not know is refused too — a manifest is data, and a `!!js/function` in it
125
+ // would be a manifest only one runtime could read.
126
+ const parseDataFile = (configPath: string): SourceDocument => {
127
+ let text: string;
128
+ try {
129
+ text = readFileSync(configPath, "utf8");
130
+ } catch (cause) {
131
+ throw new ConfigInvalid({ configPath, detail: String(cause) });
132
+ }
133
+
134
+ const lines = new LineCounter();
135
+ const document = parseDocument(text, {
136
+ merge: true,
137
+ uniqueKeys: true,
138
+ customTags: [],
139
+ lineCounter: lines,
140
+ // A JSON file is read by the same parser; strict JSON is valid YAML.
141
+ schema: "core",
142
+ });
143
+ const problems = [...document.errors, ...document.warnings];
144
+ if (problems.length > 0) {
145
+ const file = path.basename(configPath);
146
+ throw new ConfigInvalid({
147
+ configPath,
148
+ detail:
149
+ "the manifest does not parse:\n" +
150
+ problems
151
+ .map((problem) => {
152
+ const [position] = problem.linePos ?? [];
153
+ const at =
154
+ position === undefined
155
+ ? ""
156
+ : `${file}:${String(position.line)}:${String(position.col)} `;
157
+ return ` ${at}${problem.code}: ${problem.message.split("\n")[0] ?? problem.message}`;
158
+ })
159
+ .join("\n"),
160
+ });
161
+ }
162
+
163
+ return {
164
+ value: document.toJS({ mapAsMap: false }) as unknown,
165
+ locate: makeLocator(document, lines),
166
+ };
167
+ };
168
+
169
+ const keyMatches = (pair: Pair, segment: PropertyKey): boolean =>
170
+ isScalar(pair.key) && String(pair.key.value) === String(segment);
171
+
172
+ // Walks the document's own tree by the decoder's path and answers with the
173
+ // position of the deepest thing that exists along it. A map key is reported
174
+ // at the key, where the reader's eye lands; a sequence item at the item. A
175
+ // key merged in through `<<` lives in the fragment it came from, which the
176
+ // walk cannot see — the reader then gets the position of the map instead.
177
+ const makeLocator = (document: Document, lines: LineCounter): ManifestLocator => {
178
+ const positionOf = (node: Node | null | undefined): ManifestPosition | null => {
179
+ const offset = node?.range?.[0];
180
+ if (offset === undefined) return null;
181
+ const { col, line } = lines.linePos(offset);
182
+ return { line, column: col };
183
+ };
184
+ const resolved = (node: unknown): unknown => (isAlias(node) ? node.resolve(document) : node);
185
+
186
+ return (manifestPath: ManifestPath) => {
187
+ let current: unknown = resolved(document.contents);
188
+ let position = positionOf(document.contents);
189
+ for (const segment of manifestPath) {
190
+ if (isMap(current)) {
191
+ const pair = current.items.find((one) => keyMatches(one, segment));
192
+ if (pair === undefined) break;
193
+ position = positionOf(isScalar(pair.key) ? pair.key : null) ?? position;
194
+ current = resolved(pair.value);
195
+ } else if (isSeq(current) && typeof segment === "number") {
196
+ const item = current.items[segment];
197
+ if (item === undefined) break;
198
+ position = positionOf(item as Node) ?? position;
199
+ current = resolved(item);
200
+ } else {
201
+ break;
202
+ }
203
+ }
204
+ return position;
205
+ };
206
+ };
207
+
208
+ // The value as a YAML document, for `architecture migrate` and anything else
209
+ // that writes a manifest rather than reads one. Strings are quoted whenever
210
+ // the core schema would read them as something else, and a multi-line one —
211
+ // a probe source — becomes a block scalar.
212
+ export const formatManifestYaml = (manifest: unknown): string =>
213
+ stringify(manifest, { lineWidth: 100, blockQuote: "literal", schema: "core" });