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

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 (150) hide show
  1. package/build/dts/core/coverage.d.ts +20 -0
  2. package/build/dts/core/coverage.d.ts.map +1 -1
  3. package/build/dts/core/graph.d.ts +2 -0
  4. package/build/dts/core/graph.d.ts.map +1 -1
  5. package/build/dts/core/imports.d.ts +3 -1
  6. package/build/dts/core/imports.d.ts.map +1 -1
  7. package/build/dts/core/slack.d.ts +22 -0
  8. package/build/dts/core/slack.d.ts.map +1 -0
  9. package/build/dts/core/structure.d.ts +1 -0
  10. package/build/dts/core/structure.d.ts.map +1 -1
  11. package/build/dts/domain/architecture-config.d.ts +19 -3
  12. package/build/dts/domain/architecture-config.d.ts.map +1 -1
  13. package/build/dts/domain/architecture-error.d.ts +8 -0
  14. package/build/dts/domain/architecture-error.d.ts.map +1 -1
  15. package/build/dts/domain/facts.d.ts +1 -0
  16. package/build/dts/domain/facts.d.ts.map +1 -1
  17. package/build/dts/domain/manifest-location.d.ts +9 -0
  18. package/build/dts/domain/manifest-location.d.ts.map +1 -0
  19. package/build/dts/domain/snapshot.d.ts +356 -0
  20. package/build/dts/domain/snapshot.d.ts.map +1 -0
  21. package/build/dts/domain/violation.d.ts +1 -1
  22. package/build/dts/domain/violation.d.ts.map +1 -1
  23. package/build/dts/index.d.ts +22 -10
  24. package/build/dts/index.d.ts.map +1 -1
  25. package/build/dts/infrastructure/file-system-fake.d.ts.map +1 -1
  26. package/build/dts/infrastructure/file-system-live.d.ts.map +1 -1
  27. package/build/dts/infrastructure/manifest-file.d.ts +11 -2
  28. package/build/dts/infrastructure/manifest-file.d.ts.map +1 -1
  29. package/build/dts/infrastructure/manifest-include.d.ts +16 -0
  30. package/build/dts/infrastructure/manifest-include.d.ts.map +1 -0
  31. package/build/dts/infrastructure/syntax-matcher-fake.d.ts +10 -0
  32. package/build/dts/infrastructure/syntax-matcher-fake.d.ts.map +1 -0
  33. package/build/dts/infrastructure/walk.d.ts +12 -1
  34. package/build/dts/infrastructure/walk.d.ts.map +1 -1
  35. package/build/dts/load/extension.d.ts +33 -0
  36. package/build/dts/load/extension.d.ts.map +1 -0
  37. package/build/dts/load/policy.d.ts +10 -0
  38. package/build/dts/load/policy.d.ts.map +1 -1
  39. package/build/dts/manifest/compile.d.ts +11 -1
  40. package/build/dts/manifest/compile.d.ts.map +1 -1
  41. package/build/dts/manifest/expand.d.ts +27 -0
  42. package/build/dts/manifest/expand.d.ts.map +1 -0
  43. package/build/dts/manifest/extension.d.ts +8 -0
  44. package/build/dts/manifest/extension.d.ts.map +1 -0
  45. package/build/dts/manifest/glob.d.ts +1 -0
  46. package/build/dts/manifest/glob.d.ts.map +1 -1
  47. package/build/dts/manifest/infer.d.ts +55 -0
  48. package/build/dts/manifest/infer.d.ts.map +1 -0
  49. package/build/dts/manifest/json-schema.d.ts +14 -0
  50. package/build/dts/manifest/json-schema.d.ts.map +1 -0
  51. package/build/dts/manifest/manifest.d.ts +26 -1
  52. package/build/dts/manifest/manifest.d.ts.map +1 -1
  53. package/build/dts/ports/file-system.d.ts +1 -0
  54. package/build/dts/ports/file-system.d.ts.map +1 -1
  55. package/build/dts/ports/language.d.ts +4 -0
  56. package/build/dts/ports/language.d.ts.map +1 -1
  57. package/build/dts/ports/syntax-matcher.d.ts +21 -0
  58. package/build/dts/ports/syntax-matcher.d.ts.map +1 -0
  59. package/build/dts/testing.d.ts +1 -0
  60. package/build/dts/testing.d.ts.map +1 -1
  61. package/build/esm/core/coverage.js +107 -32
  62. package/build/esm/core/coverage.js.map +1 -1
  63. package/build/esm/core/graph.js +45 -0
  64. package/build/esm/core/graph.js.map +1 -1
  65. package/build/esm/core/imports.js +14 -9
  66. package/build/esm/core/imports.js.map +1 -1
  67. package/build/esm/core/slack.js +76 -0
  68. package/build/esm/core/slack.js.map +1 -0
  69. package/build/esm/core/structure.js +6 -3
  70. package/build/esm/core/structure.js.map +1 -1
  71. package/build/esm/domain/architecture-config.js +28 -4
  72. package/build/esm/domain/architecture-config.js.map +1 -1
  73. package/build/esm/domain/architecture-error.js +27 -0
  74. package/build/esm/domain/architecture-error.js.map +1 -1
  75. package/build/esm/domain/manifest-location.js +21 -0
  76. package/build/esm/domain/manifest-location.js.map +1 -0
  77. package/build/esm/domain/snapshot.js +181 -0
  78. package/build/esm/domain/snapshot.js.map +1 -0
  79. package/build/esm/domain/violation.js.map +1 -1
  80. package/build/esm/index.js +28 -9
  81. package/build/esm/index.js.map +1 -1
  82. package/build/esm/infrastructure/file-system-fake.js +10 -0
  83. package/build/esm/infrastructure/file-system-fake.js.map +1 -1
  84. package/build/esm/infrastructure/file-system-live.js +9 -1
  85. package/build/esm/infrastructure/file-system-live.js.map +1 -1
  86. package/build/esm/infrastructure/manifest-file.js +158 -7
  87. package/build/esm/infrastructure/manifest-file.js.map +1 -1
  88. package/build/esm/infrastructure/manifest-include.js +187 -0
  89. package/build/esm/infrastructure/manifest-include.js.map +1 -0
  90. package/build/esm/infrastructure/syntax-matcher-fake.js +33 -0
  91. package/build/esm/infrastructure/syntax-matcher-fake.js.map +1 -0
  92. package/build/esm/infrastructure/walk.js +106 -4
  93. package/build/esm/infrastructure/walk.js.map +1 -1
  94. package/build/esm/load/extension.js +2 -0
  95. package/build/esm/load/extension.js.map +1 -0
  96. package/build/esm/load/policy.js +49 -2
  97. package/build/esm/load/policy.js.map +1 -1
  98. package/build/esm/manifest/compile.js +50 -27
  99. package/build/esm/manifest/compile.js.map +1 -1
  100. package/build/esm/manifest/expand.js +116 -0
  101. package/build/esm/manifest/expand.js.map +1 -0
  102. package/build/esm/manifest/extension.js +2 -0
  103. package/build/esm/manifest/extension.js.map +1 -0
  104. package/build/esm/manifest/glob.js +3 -0
  105. package/build/esm/manifest/glob.js.map +1 -1
  106. package/build/esm/manifest/infer.js +455 -0
  107. package/build/esm/manifest/infer.js.map +1 -0
  108. package/build/esm/manifest/json-schema.js +169 -0
  109. package/build/esm/manifest/json-schema.js.map +1 -0
  110. package/build/esm/manifest/manifest.js +127 -5
  111. package/build/esm/manifest/manifest.js.map +1 -1
  112. package/build/esm/ports/syntax-matcher.js +12 -0
  113. package/build/esm/ports/syntax-matcher.js.map +1 -0
  114. package/build/esm/testing.js +1 -0
  115. package/build/esm/testing.js.map +1 -1
  116. package/package.json +9 -3
  117. package/schema/architecture-node.schema.json +1518 -0
  118. package/schema/architecture.schema.json +4498 -0
  119. package/schema/conformance.schema.json +1019 -0
  120. package/src/core/coverage.ts +164 -34
  121. package/src/core/graph.ts +48 -0
  122. package/src/core/imports.ts +29 -13
  123. package/src/core/slack.ts +135 -0
  124. package/src/core/structure.ts +15 -8
  125. package/src/domain/architecture-config.ts +31 -4
  126. package/src/domain/architecture-error.ts +30 -0
  127. package/src/domain/facts.ts +9 -4
  128. package/src/domain/manifest-location.ts +41 -0
  129. package/src/domain/snapshot.ts +402 -0
  130. package/src/domain/violation.ts +4 -1
  131. package/src/index.ts +149 -7
  132. package/src/infrastructure/file-system-fake.ts +12 -0
  133. package/src/infrastructure/file-system-live.ts +8 -1
  134. package/src/infrastructure/manifest-file.ts +204 -8
  135. package/src/infrastructure/manifest-include.ts +318 -0
  136. package/src/infrastructure/syntax-matcher-fake.ts +51 -0
  137. package/src/infrastructure/walk.ts +127 -3
  138. package/src/load/extension.ts +66 -0
  139. package/src/load/policy.ts +77 -2
  140. package/src/manifest/compile.ts +81 -30
  141. package/src/manifest/expand.ts +183 -0
  142. package/src/manifest/extension.ts +41 -0
  143. package/src/manifest/glob.ts +5 -0
  144. package/src/manifest/infer.ts +643 -0
  145. package/src/manifest/json-schema.ts +210 -0
  146. package/src/manifest/manifest.ts +206 -10
  147. package/src/ports/file-system.ts +3 -0
  148. package/src/ports/language.ts +17 -0
  149. package/src/ports/syntax-matcher.ts +42 -0
  150. package/src/testing.ts +1 -0
package/src/index.ts CHANGED
@@ -3,6 +3,15 @@
3
3
  // manifest into a policy. A host composes these with a language pack and the
4
4
  // live file system; a language pack implements the ports. Nothing here names a
5
5
  // language. The fakes are under `@goodbones/core/testing`.
6
+ //
7
+ // A family the core does not own — `campaigns`, in @goodbones/campaigns —
8
+ // arrives as a `PolicyExtension` the host passes to `loadPolicy`: it claims
9
+ // its own manifest keys, decodes them with its own codec, and reads its state
10
+ // back off `LoadedPolicy.extensions`. What such a family needs from here is
11
+ // exported deliberately: the glob primitives its lowering must match
12
+ // character for character, the pattern vocabulary its detectors share with
13
+ // the other families, and `lowerManifest` itself, which an `endState` runs a
14
+ // sector-relative tree through.
6
15
  export {
7
16
  type Baseline,
8
17
  type BaselineFilter,
@@ -21,6 +30,12 @@ export {
21
30
  coverageOf,
22
31
  coverageShortfalls,
23
32
  fractionsOf,
33
+ type Reach,
34
+ reachOf,
35
+ type Residue,
36
+ residueOf,
37
+ type Vacancy,
38
+ vacancyOf,
24
39
  } from "./core/coverage.js";
25
40
  export {
26
41
  type BindingEdge,
@@ -37,17 +52,20 @@ export {
37
52
  export {
38
53
  type CompiledGraph,
39
54
  compileGraphRules,
55
+ cyclesIn,
40
56
  EMPTY_GRAPH_RULES,
41
57
  evaluateGraph,
42
58
  type Graph,
43
59
  graphRulesFailingTheirProbe,
44
60
  hasGraphRules,
61
+ heightOf,
45
62
  } from "./core/graph.js";
46
63
  export {
47
64
  type CompiledImportRule,
48
65
  compileImportRule,
49
66
  compileImportRules,
50
67
  evaluateImportEdge,
68
+ evaluateResolvedEdge,
51
69
  evaluateSelectedEdge,
52
70
  type ImportEdge,
53
71
  probeTargetOf,
@@ -62,12 +80,30 @@ export {
62
80
  memberRulesFailingTheirProbe,
63
81
  memberRulesSelecting,
64
82
  } from "./core/members.js";
83
+ export {
84
+ compilePatterns,
85
+ firstFromMatch,
86
+ matchesAny,
87
+ type Selectable,
88
+ sourcesOf,
89
+ targetAllowed,
90
+ type Targeted,
91
+ validateTargetPatterns,
92
+ } from "./core/patterns.js";
93
+ export {
94
+ type Concentration,
95
+ type ObservedEdge,
96
+ type Slack,
97
+ slackOf,
98
+ type SlackReport,
99
+ } from "./core/slack.js";
65
100
  export {
66
101
  type CompiledStructure,
67
102
  compileStructure,
68
103
  EMPTY_STRUCTURE,
69
104
  evaluateStructure,
70
105
  requiredSiblingsOf,
106
+ siblingsOf,
71
107
  structureRulesFailingTheirProbe,
72
108
  } from "./core/structure.js";
73
109
  export {
@@ -78,8 +114,9 @@ export {
78
114
  surfaceRulesSelecting,
79
115
  } from "./core/surface.js";
80
116
  export {
81
- type BindingKind,
82
- type DeclarationKind,
117
+ type Allowance,
118
+ BindingKind,
119
+ DeclarationKind,
83
120
  type ExportFix,
84
121
  type ExportRule,
85
122
  type GraphConfig,
@@ -87,10 +124,11 @@ export {
87
124
  type GraphOrphanRule,
88
125
  type GraphReachRule,
89
126
  type ImportProbe,
90
- type ImportProbeTarget,
127
+ ImportProbeTarget,
91
128
  type ImportRule,
92
129
  type MemberRule,
93
- type MemberSubject,
130
+ MemberSubject,
131
+ PatternList,
94
132
  type ResolveConfig,
95
133
  type ResolveScope,
96
134
  type StructureConfig,
@@ -100,6 +138,7 @@ export {
100
138
  ConfigInvalid,
101
139
  ImportUnresolved,
102
140
  PatternInvalid,
141
+ ReportUnavailable,
103
142
  ScopeInvalid,
104
143
  } from "./domain/architecture-error.js";
105
144
  export {
@@ -108,6 +147,30 @@ export {
108
147
  type MemberSite,
109
148
  type SourceFacts,
110
149
  } from "./domain/facts.js";
150
+ export {
151
+ type ManifestLocator,
152
+ type ManifestPath,
153
+ type ManifestPosition,
154
+ renderManifestPath,
155
+ } from "./domain/manifest-location.js";
156
+ export {
157
+ CONFORMANCE_MEASURES,
158
+ type ConformanceMeasure,
159
+ decodeSnapshot,
160
+ type Snapshot,
161
+ SNAPSHOT_SCHEMA_ID,
162
+ SNAPSHOT_VERSION,
163
+ type SnapshotCampaign,
164
+ type SnapshotConcentration,
165
+ snapshotJsonSchema,
166
+ type SnapshotObjective,
167
+ type SnapshotPhase,
168
+ Snapshot as SnapshotSchema,
169
+ type SnapshotSector,
170
+ type SnapshotSlack,
171
+ type SnapshotVacancy,
172
+ type SnapshotViolation,
173
+ } from "./domain/snapshot.js";
111
174
  export {
112
175
  fingerprintOf,
113
176
  formatMessage,
@@ -115,16 +178,89 @@ export {
115
178
  type ViolationKind,
116
179
  } from "./domain/violation.js";
117
180
  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";
181
+ export {
182
+ findManifestFile,
183
+ formatManifestYaml,
184
+ MANIFEST_FILENAMES,
185
+ type ManifestFile,
186
+ readManifestFile,
187
+ } from "./infrastructure/manifest-file.js";
188
+ export {
189
+ expandIncludes,
190
+ type IncludedManifest,
191
+ type IncludeReader,
192
+ type SourceDocument,
193
+ } from "./infrastructure/manifest-include.js";
194
+ export {
195
+ listPackageRoots,
196
+ listSourceFiles,
197
+ listWorkspaceProjects,
198
+ type PackagedLanguage,
199
+ type PackageRoot,
200
+ type WalkedLanguage,
201
+ type WorkspaceProject,
202
+ } from "./infrastructure/walk.js";
203
+ export {
204
+ type ExtensionContext,
205
+ type ExtensionRoute,
206
+ type LoadedExtension,
207
+ type PolicyExtension,
208
+ } from "./load/extension.js";
120
209
  export { type LoadedPolicy, loadPolicy, type LoadPolicyInput } from "./load/policy.js";
121
- export { type LoweredRules, lowerManifest, type ProbeLanguage } from "./manifest/compile.js";
210
+ export {
211
+ CONVENTIONS,
212
+ expandAliases,
213
+ type LoweredRules,
214
+ lowerManifest,
215
+ type LowerOptions,
216
+ type ProbeLanguage,
217
+ } from "./manifest/compile.js";
218
+ export {
219
+ type ExpandedManifest,
220
+ type ExpandIssue,
221
+ expandManifest,
222
+ type Origin,
223
+ originOf,
224
+ type Substitution,
225
+ } from "./manifest/expand.js";
226
+ export { type ManifestExtension } from "./manifest/extension.js";
227
+ export {
228
+ anchored,
229
+ type CaptureIndex,
230
+ type GlobCompilation,
231
+ globToRegExp,
232
+ globToRegexSource,
233
+ prefixed,
234
+ } from "./manifest/glob.js";
235
+ export {
236
+ type Candidate,
237
+ candidatesOf,
238
+ type Generalization,
239
+ type InferInput,
240
+ inferManifest,
241
+ type InferOptions,
242
+ type InferPackage,
243
+ type Inferred,
244
+ type InferredTarget,
245
+ singularOf,
246
+ } from "./manifest/infer.js";
247
+ export {
248
+ MANIFEST_NODE_SCHEMA_ID,
249
+ MANIFEST_SCHEMA_ID,
250
+ manifestJsonSchema,
251
+ type ManifestJsonSchemaOptions,
252
+ manifestNodeJsonSchema,
253
+ } from "./manifest/json-schema.js";
122
254
  export {
123
255
  type DecodedManifest,
124
256
  decodeManifest,
257
+ type DecodeManifestOptions,
258
+ decodeManifestTree,
259
+ globsOf,
125
260
  type Manifest,
126
261
  type ManifestNode,
127
262
  Manifest as ManifestSchema,
263
+ SurfaceConvention,
128
264
  } from "./manifest/manifest.js";
129
265
  export { type FactExtractor } from "./ports/fact-extractor.js";
130
266
  export { type FileSystem } from "./ports/file-system.js";
@@ -134,3 +270,9 @@ export {
134
270
  type ModuleResolver,
135
271
  type ResolvedTarget,
136
272
  } from "./ports/module-resolver.js";
273
+ export {
274
+ type Position,
275
+ type SyntaxMatch,
276
+ type SyntaxMatcher,
277
+ type SyntaxTree,
278
+ } from "./ports/syntax-matcher.js";
@@ -10,5 +10,17 @@ export const makeFileSystemFake = (
10
10
  return {
11
11
  exists: (repoRelativePath) => paths.has(repoRelativePath),
12
12
  readText: (repoRelativePath) => contents[repoRelativePath] ?? null,
13
+ list: (repoRelativeDir) => {
14
+ const prefix = repoRelativeDir === "" ? "" : `${repoRelativeDir}/`;
15
+ return [
16
+ ...new Set(
17
+ [...paths]
18
+ .filter((one) => one.startsWith(prefix))
19
+ .map((one) => one.slice(prefix.length).split("/")[0] ?? ""),
20
+ ),
21
+ ]
22
+ .filter((one) => one !== "")
23
+ .sort();
24
+ },
13
25
  };
14
26
  };
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
2
  import * as path from "node:path";
3
3
 
4
4
  import type { FileSystem } from "../ports/file-system.js";
@@ -23,5 +23,12 @@ export const makeFileSystemLive = (repoRoot: string): FileSystem => {
23
23
  return null;
24
24
  }
25
25
  },
26
+ list: (repoRelativeDir) => {
27
+ try {
28
+ return readdirSync(path.resolve(repoRoot, repoRelativeDir)).sort();
29
+ } catch {
30
+ return [];
31
+ }
32
+ },
26
33
  };
27
34
  };
@@ -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" });