@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.
- package/build/dts/core/campaigns.d.ts +141 -0
- package/build/dts/core/campaigns.d.ts.map +1 -0
- package/build/dts/core/coverage.d.ts +20 -0
- package/build/dts/core/coverage.d.ts.map +1 -1
- package/build/dts/core/graph.d.ts +2 -0
- package/build/dts/core/graph.d.ts.map +1 -1
- package/build/dts/core/imports.d.ts +3 -1
- package/build/dts/core/imports.d.ts.map +1 -1
- package/build/dts/core/ledger.d.ts +61 -0
- package/build/dts/core/ledger.d.ts.map +1 -0
- package/build/dts/core/slack.d.ts +22 -0
- package/build/dts/core/slack.d.ts.map +1 -0
- package/build/dts/core/structure.d.ts +1 -0
- package/build/dts/core/structure.d.ts.map +1 -1
- package/build/dts/domain/architecture-config.d.ts +324 -1
- package/build/dts/domain/architecture-config.d.ts.map +1 -1
- package/build/dts/domain/architecture-error.d.ts +8 -0
- package/build/dts/domain/architecture-error.d.ts.map +1 -1
- package/build/dts/domain/facts.d.ts +1 -0
- package/build/dts/domain/facts.d.ts.map +1 -1
- package/build/dts/domain/manifest-location.d.ts +9 -0
- package/build/dts/domain/manifest-location.d.ts.map +1 -0
- package/build/dts/domain/report.d.ts +16 -0
- package/build/dts/domain/report.d.ts.map +1 -0
- package/build/dts/domain/snapshot.d.ts +224 -0
- package/build/dts/domain/snapshot.d.ts.map +1 -0
- package/build/dts/domain/violation.d.ts +1 -1
- package/build/dts/domain/violation.d.ts.map +1 -1
- package/build/dts/index.d.ts +24 -9
- package/build/dts/index.d.ts.map +1 -1
- package/build/dts/infrastructure/campaign-functions.d.ts +7 -0
- package/build/dts/infrastructure/campaign-functions.d.ts.map +1 -0
- package/build/dts/infrastructure/manifest-file.d.ts +11 -2
- package/build/dts/infrastructure/manifest-file.d.ts.map +1 -1
- package/build/dts/infrastructure/manifest-include.d.ts +16 -0
- package/build/dts/infrastructure/manifest-include.d.ts.map +1 -0
- package/build/dts/infrastructure/report-source-fake.d.ts +4 -0
- package/build/dts/infrastructure/report-source-fake.d.ts.map +1 -0
- package/build/dts/infrastructure/report-source-live.d.ts +3 -0
- package/build/dts/infrastructure/report-source-live.d.ts.map +1 -0
- package/build/dts/infrastructure/syntax-matcher-fake.d.ts +10 -0
- package/build/dts/infrastructure/syntax-matcher-fake.d.ts.map +1 -0
- package/build/dts/infrastructure/walk.d.ts +6 -0
- package/build/dts/infrastructure/walk.d.ts.map +1 -1
- package/build/dts/load/policy.d.ts +16 -0
- package/build/dts/load/policy.d.ts.map +1 -1
- package/build/dts/manifest/compile.d.ts +7 -2
- package/build/dts/manifest/compile.d.ts.map +1 -1
- package/build/dts/manifest/expand.d.ts +27 -0
- package/build/dts/manifest/expand.d.ts.map +1 -0
- package/build/dts/manifest/infer.d.ts +55 -0
- package/build/dts/manifest/infer.d.ts.map +1 -0
- package/build/dts/manifest/json-schema.d.ts +10 -0
- package/build/dts/manifest/json-schema.d.ts.map +1 -0
- package/build/dts/manifest/manifest.d.ts +381 -1
- package/build/dts/manifest/manifest.d.ts.map +1 -1
- package/build/dts/ports/campaign-predicate.d.ts +24 -0
- package/build/dts/ports/campaign-predicate.d.ts.map +1 -0
- package/build/dts/ports/language.d.ts +4 -0
- package/build/dts/ports/language.d.ts.map +1 -1
- package/build/dts/ports/report-source.d.ts +14 -0
- package/build/dts/ports/report-source.d.ts.map +1 -0
- package/build/dts/ports/syntax-matcher.d.ts +21 -0
- package/build/dts/ports/syntax-matcher.d.ts.map +1 -0
- package/build/dts/testing.d.ts +2 -0
- package/build/dts/testing.d.ts.map +1 -1
- package/build/esm/core/campaigns.js +778 -0
- package/build/esm/core/campaigns.js.map +1 -0
- package/build/esm/core/coverage.js +107 -32
- package/build/esm/core/coverage.js.map +1 -1
- package/build/esm/core/graph.js +45 -0
- package/build/esm/core/graph.js.map +1 -1
- package/build/esm/core/imports.js +14 -9
- package/build/esm/core/imports.js.map +1 -1
- package/build/esm/core/ledger.js +171 -0
- package/build/esm/core/ledger.js.map +1 -0
- package/build/esm/core/slack.js +76 -0
- package/build/esm/core/slack.js.map +1 -0
- package/build/esm/core/structure.js +5 -2
- package/build/esm/core/structure.js.map +1 -1
- package/build/esm/domain/architecture-config.js +158 -1
- package/build/esm/domain/architecture-config.js.map +1 -1
- package/build/esm/domain/architecture-error.js +27 -0
- package/build/esm/domain/architecture-error.js.map +1 -1
- package/build/esm/domain/manifest-location.js +21 -0
- package/build/esm/domain/manifest-location.js.map +1 -0
- package/build/esm/domain/report.js +168 -0
- package/build/esm/domain/report.js.map +1 -0
- package/build/esm/domain/snapshot.js +141 -0
- package/build/esm/domain/snapshot.js.map +1 -0
- package/build/esm/domain/violation.js.map +1 -1
- package/build/esm/index.js +21 -8
- package/build/esm/index.js.map +1 -1
- package/build/esm/infrastructure/campaign-functions.js +67 -0
- package/build/esm/infrastructure/campaign-functions.js.map +1 -0
- package/build/esm/infrastructure/manifest-file.js +158 -7
- package/build/esm/infrastructure/manifest-file.js.map +1 -1
- package/build/esm/infrastructure/manifest-include.js +187 -0
- package/build/esm/infrastructure/manifest-include.js.map +1 -0
- package/build/esm/infrastructure/report-source-fake.js +6 -0
- package/build/esm/infrastructure/report-source-fake.js.map +1 -0
- package/build/esm/infrastructure/report-source-live.js +165 -0
- package/build/esm/infrastructure/report-source-live.js.map +1 -0
- package/build/esm/infrastructure/syntax-matcher-fake.js +33 -0
- package/build/esm/infrastructure/syntax-matcher-fake.js.map +1 -0
- package/build/esm/infrastructure/walk.js +57 -1
- package/build/esm/infrastructure/walk.js.map +1 -1
- package/build/esm/load/policy.js +123 -3
- package/build/esm/load/policy.js.map +1 -1
- package/build/esm/manifest/compile.js +228 -26
- package/build/esm/manifest/compile.js.map +1 -1
- package/build/esm/manifest/expand.js +116 -0
- package/build/esm/manifest/expand.js.map +1 -0
- package/build/esm/manifest/infer.js +455 -0
- package/build/esm/manifest/infer.js.map +1 -0
- package/build/esm/manifest/json-schema.js +135 -0
- package/build/esm/manifest/json-schema.js.map +1 -0
- package/build/esm/manifest/manifest.js +243 -5
- package/build/esm/manifest/manifest.js.map +1 -1
- package/build/esm/ports/campaign-predicate.js +2 -0
- package/build/esm/ports/campaign-predicate.js.map +1 -0
- package/build/esm/ports/report-source.js +7 -0
- package/build/esm/ports/report-source.js.map +1 -0
- package/build/esm/ports/syntax-matcher.js +12 -0
- package/build/esm/ports/syntax-matcher.js.map +1 -0
- package/build/esm/testing.js +2 -0
- package/build/esm/testing.js.map +1 -1
- package/package.json +9 -3
- package/schema/architecture-node.schema.json +2405 -0
- package/schema/architecture.schema.json +2825 -0
- package/schema/conformance.schema.json +775 -0
- package/src/core/campaigns.ts +1056 -0
- package/src/core/coverage.ts +164 -34
- package/src/core/graph.ts +48 -0
- package/src/core/imports.ts +29 -13
- package/src/core/ledger.ts +242 -0
- package/src/core/slack.ts +135 -0
- package/src/core/structure.ts +10 -5
- package/src/domain/architecture-config.ts +205 -1
- package/src/domain/architecture-error.ts +30 -0
- package/src/domain/facts.ts +9 -4
- package/src/domain/manifest-location.ts +41 -0
- package/src/domain/report.ts +203 -0
- package/src/domain/snapshot.ts +302 -0
- package/src/domain/violation.ts +4 -1
- package/src/index.ts +172 -3
- package/src/infrastructure/campaign-functions.ts +98 -0
- package/src/infrastructure/manifest-file.ts +204 -8
- package/src/infrastructure/manifest-include.ts +318 -0
- package/src/infrastructure/report-source-fake.ts +10 -0
- package/src/infrastructure/report-source-live.ts +192 -0
- package/src/infrastructure/syntax-matcher-fake.ts +51 -0
- package/src/infrastructure/walk.ts +70 -1
- package/src/load/policy.ts +193 -3
- package/src/manifest/compile.ts +290 -28
- package/src/manifest/expand.ts +183 -0
- package/src/manifest/infer.ts +643 -0
- package/src/manifest/json-schema.ts +168 -0
- package/src/manifest/manifest.ts +339 -11
- package/src/ports/campaign-predicate.ts +30 -0
- package/src/ports/language.ts +17 -0
- package/src/ports/report-source.ts +35 -0
- package/src/ports/syntax-matcher.ts +42 -0
- 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 {
|
|
119
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
? module.default
|
|
16
|
-
|
|
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" });
|