semantifold 0.8.0 → 0.10.0

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 (171) hide show
  1. package/README.md +11 -5
  2. package/bin/semantifold.js +6 -0
  3. package/build/bin/semantifold.d.ts +3 -0
  4. package/build/bin/semantifold.d.ts.map +1 -0
  5. package/build/bin/semantifold.js +5 -0
  6. package/build/index.d.ts +9 -0
  7. package/build/index.d.ts.map +1 -1
  8. package/build/index.js +10 -1
  9. package/build/src/artifact-path.d.ts +16 -0
  10. package/build/src/artifact-path.d.ts.map +1 -1
  11. package/build/src/artifact-path.js +29 -3
  12. package/build/src/backends/c-check.d.ts +7 -0
  13. package/build/src/backends/c-check.d.ts.map +1 -0
  14. package/build/src/backends/c-check.js +65 -0
  15. package/build/src/backends/check-plan.d.ts +96 -0
  16. package/build/src/backends/check-plan.d.ts.map +1 -0
  17. package/build/src/backends/check-plan.js +135 -0
  18. package/build/src/backends/cpp-check.d.ts +7 -0
  19. package/build/src/backends/cpp-check.d.ts.map +1 -0
  20. package/build/src/backends/cpp-check.js +59 -0
  21. package/build/src/backends/csharp-check.d.ts +7 -0
  22. package/build/src/backends/csharp-check.d.ts.map +1 -0
  23. package/build/src/backends/csharp-check.js +125 -0
  24. package/build/src/backends/csharp.d.ts +1 -0
  25. package/build/src/backends/csharp.d.ts.map +1 -1
  26. package/build/src/backends/csharp.js +5 -4
  27. package/build/src/backends/dart-check.d.ts +7 -0
  28. package/build/src/backends/dart-check.d.ts.map +1 -0
  29. package/build/src/backends/dart-check.js +103 -0
  30. package/build/src/backends/dart.d.ts +2 -0
  31. package/build/src/backends/dart.d.ts.map +1 -1
  32. package/build/src/backends/dart.js +5 -5
  33. package/build/src/backends/go-check.d.ts +7 -0
  34. package/build/src/backends/go-check.d.ts.map +1 -0
  35. package/build/src/backends/go-check.js +77 -0
  36. package/build/src/backends/go.d.ts +1 -0
  37. package/build/src/backends/go.d.ts.map +1 -1
  38. package/build/src/backends/go.js +3 -3
  39. package/build/src/backends/java-check.d.ts +21 -0
  40. package/build/src/backends/java-check.d.ts.map +1 -0
  41. package/build/src/backends/java-check.js +73 -0
  42. package/build/src/backends/javascript-check.d.ts +7 -0
  43. package/build/src/backends/javascript-check.d.ts.map +1 -0
  44. package/build/src/backends/javascript-check.js +29 -0
  45. package/build/src/backends/kotlin-check.d.ts +7 -0
  46. package/build/src/backends/kotlin-check.d.ts.map +1 -0
  47. package/build/src/backends/kotlin-check.js +31 -0
  48. package/build/src/backends/php-check.d.ts +7 -0
  49. package/build/src/backends/php-check.d.ts.map +1 -0
  50. package/build/src/backends/php-check.js +21 -0
  51. package/build/src/backends/program.d.ts +7 -0
  52. package/build/src/backends/program.d.ts.map +1 -1
  53. package/build/src/backends/program.js +16 -1
  54. package/build/src/backends/python-check.d.ts +7 -0
  55. package/build/src/backends/python-check.d.ts.map +1 -0
  56. package/build/src/backends/python-check.js +28 -0
  57. package/build/src/backends/ruby-check.d.ts +7 -0
  58. package/build/src/backends/ruby-check.d.ts.map +1 -0
  59. package/build/src/backends/ruby-check.js +21 -0
  60. package/build/src/backends/rust-check.d.ts +7 -0
  61. package/build/src/backends/rust-check.d.ts.map +1 -0
  62. package/build/src/backends/rust-check.js +65 -0
  63. package/build/src/backends/swift-check.d.ts +7 -0
  64. package/build/src/backends/swift-check.d.ts.map +1 -0
  65. package/build/src/backends/swift-check.js +77 -0
  66. package/build/src/backends/typescript-check.d.ts +7 -0
  67. package/build/src/backends/typescript-check.d.ts.map +1 -0
  68. package/build/src/backends/typescript-check.js +33 -0
  69. package/build/src/backends/zig-check.d.ts +7 -0
  70. package/build/src/backends/zig-check.d.ts.map +1 -0
  71. package/build/src/backends/zig-check.js +70 -0
  72. package/build/src/diagnostic.d.ts +13 -1
  73. package/build/src/diagnostic.d.ts.map +1 -1
  74. package/build/src/diagnostic.js +14 -2
  75. package/build/src/frontends/program.d.ts +6 -0
  76. package/build/src/frontends/program.d.ts.map +1 -1
  77. package/build/src/frontends/program.js +9 -1
  78. package/build/src/language-registry.d.ts +54 -0
  79. package/build/src/language-registry.d.ts.map +1 -1
  80. package/build/src/language-registry.js +107 -5
  81. package/build/src/project-build.d.ts +172 -0
  82. package/build/src/project-build.d.ts.map +1 -0
  83. package/build/src/project-build.js +320 -0
  84. package/build/src/project-cli.d.ts +40 -0
  85. package/build/src/project-cli.d.ts.map +1 -0
  86. package/build/src/project-cli.js +145 -0
  87. package/build/src/project-manifest.d.ts +148 -0
  88. package/build/src/project-manifest.d.ts.map +1 -0
  89. package/build/src/project-manifest.js +432 -0
  90. package/build/src/project-reporter.d.ts +81 -0
  91. package/build/src/project-reporter.d.ts.map +1 -0
  92. package/build/src/project-reporter.js +293 -0
  93. package/build/src/project-snapshot.d.ts +129 -0
  94. package/build/src/project-snapshot.d.ts.map +1 -0
  95. package/build/src/project-snapshot.js +362 -0
  96. package/build/src/project-watch-reporter.d.ts +125 -0
  97. package/build/src/project-watch-reporter.d.ts.map +1 -0
  98. package/build/src/project-watch-reporter.js +280 -0
  99. package/build/src/project-watch.d.ts +133 -0
  100. package/build/src/project-watch.d.ts.map +1 -0
  101. package/build/src/project-watch.js +839 -0
  102. package/build/src/publication.d.ts +218 -0
  103. package/build/src/publication.d.ts.map +1 -0
  104. package/build/src/publication.js +1832 -0
  105. package/build/src/semantic/types.d.ts +613 -0
  106. package/build/src/semantic/types.d.ts.map +1 -1
  107. package/build/src/semantic/types.js +163 -1
  108. package/build/src/subprocess.d.ts +8 -0
  109. package/build/src/subprocess.d.ts.map +1 -1
  110. package/build/src/subprocess.js +50 -9
  111. package/build/src/target-check.d.ts +52 -0
  112. package/build/src/target-check.d.ts.map +1 -0
  113. package/build/src/target-check.js +796 -0
  114. package/build/src/toolchains.d.ts.map +1 -1
  115. package/build/src/toolchains.js +3 -3
  116. package/docs/architecture.md +3 -1
  117. package/docs/c.md +4 -0
  118. package/docs/cpp.md +4 -0
  119. package/docs/dart.md +4 -0
  120. package/docs/generated-artifact-publication.md +98 -0
  121. package/docs/kotlin.md +2 -2
  122. package/docs/language-support.md +29 -3
  123. package/docs/project-build-cli.md +147 -0
  124. package/docs/project-watch.md +60 -0
  125. package/docs/rust.md +4 -0
  126. package/docs/swift.md +2 -0
  127. package/docs/testing.md +70 -3
  128. package/docs/watch-build-pipeline.md +42 -26
  129. package/docs/zig.md +2 -0
  130. package/examples/jsdoc-java-watch/expected-output.txt +3 -0
  131. package/examples/jsdoc-java-watch/semantifold.json +23 -0
  132. package/examples/jsdoc-java-watch/src/main.js +21 -0
  133. package/index.js +9 -0
  134. package/package.json +6 -1
  135. package/src/artifact-path.js +31 -2
  136. package/src/backends/c-check.js +80 -0
  137. package/src/backends/check-plan.js +147 -0
  138. package/src/backends/cpp-check.js +73 -0
  139. package/src/backends/csharp-check.js +139 -0
  140. package/src/backends/csharp.js +4 -3
  141. package/src/backends/dart-check.js +117 -0
  142. package/src/backends/dart.js +4 -4
  143. package/src/backends/go-check.js +89 -0
  144. package/src/backends/go.js +2 -2
  145. package/src/backends/java-check.js +77 -0
  146. package/src/backends/javascript-check.js +33 -0
  147. package/src/backends/kotlin-check.js +41 -0
  148. package/src/backends/php-check.js +23 -0
  149. package/src/backends/program.js +17 -0
  150. package/src/backends/python-check.js +30 -0
  151. package/src/backends/ruby-check.js +23 -0
  152. package/src/backends/rust-check.js +78 -0
  153. package/src/backends/swift-check.js +89 -0
  154. package/src/backends/typescript-check.js +37 -0
  155. package/src/backends/zig-check.js +81 -0
  156. package/src/diagnostic.js +9 -1
  157. package/src/frontends/program.js +9 -0
  158. package/src/language-registry.js +113 -4
  159. package/src/project-build.js +353 -0
  160. package/src/project-cli.js +155 -0
  161. package/src/project-manifest.js +479 -0
  162. package/src/project-reporter.js +320 -0
  163. package/src/project-snapshot.js +403 -0
  164. package/src/project-watch-reporter.js +303 -0
  165. package/src/project-watch.js +870 -0
  166. package/src/publication.js +2071 -0
  167. package/src/semantic/types.js +184 -0
  168. package/src/subprocess.js +49 -8
  169. package/src/target-check.js +844 -0
  170. package/src/toolchains.js +3 -2
  171. package/tsconfig.json +1 -1
@@ -0,0 +1,353 @@
1
+ // @ts-check
2
+
3
+ import {randomUUID} from "node:crypto"
4
+ import {generateProgramArtifacts} from "./backends/program.js"
5
+ import {SemantifoldDiagnostic} from "./diagnostic.js"
6
+ import {parseProgramSource} from "./frontends/program.js"
7
+ import {ProjectManifestLoader} from "./project-manifest.js"
8
+ import {ProjectSnapshotBuilder} from "./project-snapshot.js"
9
+ import {GeneratedArtifactPublisher, guardNextPublicationCommit} from "./publication.js"
10
+ import {languageRegistry} from "./language-registry.js"
11
+ import {createTargetCheckPlan, TargetCheckRunner} from "./target-check.js"
12
+ import {canonicalToolchains, discoverCanonicalToolchain} from "./toolchains.js"
13
+
14
+ /** @type {WeakMap<ProjectBuilder, (operation: string) => void>} */
15
+ const projectBuildObservers = new WeakMap()
16
+ /** @type {WeakMap<ProjectBuilder, ProjectWatchBuildControls[]>} */
17
+ const projectWatchBuildControls = new WeakMap()
18
+
19
+ /**
20
+ * Installs an internal synchronous orchestration observer for focused specs.
21
+ * This helper is intentionally not part of the package root API.
22
+ * @param {ProjectBuilder} builder - Project builder instance.
23
+ * @param {(operation: string) => void} observer - Ordered operation observer.
24
+ * @returns {void}
25
+ */
26
+ export function observeProjectBuild(builder, observer) {
27
+ if (!(builder instanceof ProjectBuilder) || typeof observer != "function") {
28
+ throw new TypeError("Invalid project build observer.")
29
+ }
30
+ projectBuildObservers.set(builder, observer)
31
+ }
32
+
33
+ /**
34
+ * Runs one builder invocation with the narrow snapshot-currentness controls owned by the watch coordinator.
35
+ * This helper is intentionally not part of the package root API.
36
+ * @param {ProjectBuilder} builder - Project builder instance.
37
+ * @param {string} projectPath - Exact project manifest path.
38
+ * @param {ProjectBuildReporterLike} reporter - Watch-cycle reporter.
39
+ * @param {ProjectWatchBuildOptions} options - Checked-cycle and currentness controls.
40
+ * @returns {Promise<Readonly<ProjectBuildSuccess>>} Committed build result.
41
+ */
42
+ export function buildWatchedProject(builder, projectPath, reporter, options) {
43
+ if (!(builder instanceof ProjectBuilder) || typeof projectPath != "string" || projectPath.length == 0 ||
44
+ !isPlainObject(options) || Object.keys(options).some(key =>
45
+ !["check", "expectedSnapshotHash", "publicationSignal", "signal", "validateCurrentSnapshot"].includes(key)) ||
46
+ typeof options.check != "boolean" || typeof options.expectedSnapshotHash != "string" ||
47
+ !/^[a-f0-9]{64}$/u.test(options.expectedSnapshotHash) || !(options.publicationSignal instanceof AbortSignal) ||
48
+ !(options.signal instanceof AbortSignal) || typeof options.validateCurrentSnapshot != "function") {
49
+ throw new TypeError("Invalid watched project build controls.")
50
+ }
51
+ const queued = projectWatchBuildControls.get(builder) ?? []
52
+
53
+ queued.push({
54
+ expectedSnapshotHash: options.expectedSnapshotHash,
55
+ publicationSignal: options.publicationSignal,
56
+ validateCurrentSnapshot: options.validateCurrentSnapshot
57
+ })
58
+ projectWatchBuildControls.set(builder, queued)
59
+
60
+ return builder.build(projectPath, reporter, {check: options.check, signal: options.signal})
61
+ }
62
+
63
+ /**
64
+ * Orchestrates one complete immutable project build and one transactional publication.
65
+ */
66
+ export class ProjectBuilder {
67
+ /** @type {ProjectManifestLoader} */
68
+ #manifestLoader
69
+ /** @type {ProjectSnapshotBuilder} */
70
+ #snapshotBuilder
71
+ /** @type {TargetCheckRunner} */
72
+ #checkRunner
73
+ /** @type {Readonly<Record<string, string | undefined>> | undefined} */
74
+ #environment
75
+
76
+ /**
77
+ * Creates a one-shot project builder.
78
+ * @param {{checkRunner?: TargetCheckRunner, environment?: Readonly<Record<string, string | undefined>>, manifestLoader?: ProjectManifestLoader, snapshotBuilder?: ProjectSnapshotBuilder}} [options] - Lifecycle collaborators.
79
+ */
80
+ constructor(options = {}) {
81
+ if (options.checkRunner !== undefined && !(options.checkRunner instanceof TargetCheckRunner)) {
82
+ throw new TypeError("Project builder check runner must be a TargetCheckRunner.")
83
+ }
84
+ this.#manifestLoader = options.manifestLoader ?? new ProjectManifestLoader()
85
+ this.#snapshotBuilder = options.snapshotBuilder ?? new ProjectSnapshotBuilder()
86
+ this.#checkRunner = options.checkRunner ?? new TargetCheckRunner()
87
+ this.#environment = options.environment === undefined ? undefined : snapshotEnvironment(options.environment)
88
+ }
89
+
90
+ /**
91
+ * Builds and atomically publishes one project generation.
92
+ * @param {string} [projectPath] - Project manifest path.
93
+ * @param {ProjectBuildReporterLike} [reporter] - Optional state reporter.
94
+ * @param {ProjectBuildOptions} [options] - Optional developer-check and publication lifecycle.
95
+ * @returns {Promise<Readonly<ProjectBuildSuccess>>} Committed build result.
96
+ */
97
+ async build(projectPath = "./semantifold.json", reporter, options = {}) {
98
+ const queuedWatchControls = projectWatchBuildControls.get(this)
99
+ const watchControls = queuedWatchControls?.shift()
100
+
101
+ if (queuedWatchControls?.length == 0) projectWatchBuildControls.delete(this)
102
+ if (typeof options != "object" || options == null || Array.isArray(options) || Object.getPrototypeOf(options) != Object.prototype ||
103
+ Object.keys(options).some(key => !["check", "signal", "timeoutMs"].includes(key)) ||
104
+ options.check !== undefined && typeof options.check != "boolean" ||
105
+ options.signal !== undefined && !(options.signal instanceof AbortSignal) ||
106
+ options.timeoutMs !== undefined && (typeof options.timeoutMs != "number" || !Number.isInteger(options.timeoutMs))) {
107
+ throw new SemantifoldDiagnostic({
108
+ code: "INVALID_PROJECT_BUILD",
109
+ language: "project",
110
+ message: "Project build options require valid check, snapshot-currentness, cancellation, and timeout controls."
111
+ })
112
+ }
113
+ const check = options.check === true
114
+ const signal = options.signal
115
+ const project = await this.#manifestLoader.load(projectPath)
116
+
117
+ reporter?.projectLoaded(project.id)
118
+ const snapshot = await this.#snapshotBuilder.build(project)
119
+
120
+ if (watchControls !== undefined && snapshot.hash != watchControls.expectedSnapshotHash) {
121
+ throw new SemantifoldDiagnostic({
122
+ code: "WATCH_SNAPSHOT_CHANGED",
123
+ language: project.id,
124
+ message: "The complete project snapshot changed before its watch cycle began."
125
+ })
126
+ }
127
+
128
+ reporter?.snapshotLoaded(snapshot.hash)
129
+ projectBuildObservers.get(this)?.("snapshot")
130
+ const program = parseProgramSource({
131
+ entryModule: project.entryModule,
132
+ sources: snapshot.sources.map(source => ({
133
+ filename: source.path,
134
+ id: source.id,
135
+ language: source.language,
136
+ source: source.source
137
+ }))
138
+ })
139
+
140
+ projectBuildObservers.get(this)?.("parse")
141
+ /** @type {import("./semantic/types.js").PublicationTargetInput[]} */
142
+ const publicationTargets = []
143
+ /** @type {ProjectBuildTargetResult[]} */
144
+ const targetResults = []
145
+ /** @type {Map<string, import("./semantic/types.js").TargetCheckResult>} */
146
+ const checkResults = new Map()
147
+
148
+ for (const target of project.targets) {
149
+ if (target.role == "binary") {
150
+ throw new SemantifoldDiagnostic({
151
+ code: "UNSUPPORTED_PROJECT_TARGET",
152
+ language: target.language,
153
+ message: "Complete semantic programs do not support binary project generation."
154
+ })
155
+ }
156
+ const artifactSet = generateProgramArtifacts({
157
+ language: target.language,
158
+ program,
159
+ role: target.role
160
+ })
161
+ const checkCapability = languageRegistry.record(target.language).check
162
+
163
+ if (check && !checkCapability.supported) {
164
+ throw new SemantifoldDiagnostic({
165
+ code: "UNSUPPORTED_TARGET_CHECK",
166
+ language: target.language,
167
+ message: `Project target '${target.id}' does not provide a developer check plan.`
168
+ })
169
+ }
170
+ /**
171
+ * Ordered candidate validators when checking is enabled.
172
+ * @type {import("./semantic/types.js").PublicationValidator[] | undefined}
173
+ */
174
+ const validators = check ? [async ({buildPath, sourcePath, targetId}) => {
175
+ /** @type {import("./semantic/types.js").DiscoveredToolchain[]} */
176
+ const tools = []
177
+
178
+ for (const toolchain of checkCapability.toolchains) {
179
+ tools.push(await discoverCanonicalToolchain(/** @type {keyof typeof canonicalToolchains} */ (toolchain), {
180
+ ...(this.#environment === undefined ? {} : {environment: this.#environment})
181
+ }))
182
+ }
183
+ const plan = createTargetCheckPlan({
184
+ artifacts: artifactSet,
185
+ buildPath,
186
+ projectId: project.id,
187
+ sourcePath,
188
+ targetId,
189
+ tools: Object.freeze(tools)
190
+ })
191
+ const result = await this.#checkRunner.run(plan, {
192
+ onStage: stage => {
193
+ reporter?.targetChecked({id: target.id, language: target.language, stage})
194
+ projectBuildObservers.get(this)?.(`check:${target.id}:${stage.stage}`)
195
+ },
196
+ ...(signal === undefined ? {} : {signal}),
197
+ ...(options.timeoutMs === undefined ? {} : {timeoutMs: options.timeoutMs})
198
+ })
199
+
200
+ checkResults.set(target.id, result)
201
+
202
+ return [...result.outputs]
203
+ }] : undefined
204
+
205
+ publicationTargets.push({
206
+ artifactSet,
207
+ buildProjection: target.buildProjection,
208
+ id: target.id,
209
+ role: target.role,
210
+ sourceProjection: target.sourceProjection,
211
+ ...(validators === undefined ? {} : {validators})
212
+ })
213
+ targetResults.push(Object.freeze({
214
+ artifactCount: artifactSet.artifacts.length,
215
+ id: target.id,
216
+ language: target.language,
217
+ role: target.role
218
+ }))
219
+ reporter?.targetGenerated(targetResults[targetResults.length - 1])
220
+ projectBuildObservers.get(this)?.(`target:${target.id}`)
221
+ }
222
+ const publisher = new GeneratedArtifactPublisher({
223
+ projectId: project.id,
224
+ projectRoot: project.projectRoot,
225
+ publicationRoot: project.publicationRoot,
226
+ sourceFiles: project.sources.map(({absolutePath}) => absolutePath)
227
+ })
228
+ const generationId = check
229
+ ? `g-${snapshot.hash}-checked-${randomUUID()}`
230
+ : `g-${snapshot.hash}`
231
+ if (watchControls !== undefined) {
232
+ guardNextPublicationCommit(publisher, watchControls.validateCurrentSnapshot)
233
+ }
234
+ const publicationSignal = signal === undefined
235
+ ? watchControls?.publicationSignal
236
+ : watchControls === undefined
237
+ ? signal
238
+ : AbortSignal.any([signal, watchControls.publicationSignal])
239
+ const generation = await publisher.publish({generationId, targets: publicationTargets}, {
240
+ ...(publicationSignal === undefined ? {} : {signal: publicationSignal})
241
+ })
242
+
243
+ projectBuildObservers.get(this)?.("publish")
244
+
245
+ return Object.freeze({
246
+ generation,
247
+ generationId,
248
+ checked: check,
249
+ projectId: project.id,
250
+ snapshotHash: snapshot.hash,
251
+ status: /** @type {const} */ ("succeeded"),
252
+ targets: Object.freeze(targetResults.map(target => {
253
+ const result = checkResults.get(target.id)
254
+
255
+ return Object.freeze({
256
+ ...target,
257
+ ...(result === undefined ? {} : {check: Object.freeze({stages: result.stages})})
258
+ })
259
+ }))
260
+ })
261
+ }
262
+ }
263
+
264
+ /**
265
+ * @typedef ProjectBuildOptions
266
+ * @property {boolean} [check] - Whether every target must run its developer check.
267
+ * @property {AbortSignal} [signal] - Active check and publication cancellation authority.
268
+ * @property {number} [timeoutMs] - Per-check-stage deadline.
269
+ */
270
+
271
+ /**
272
+ * @typedef ProjectWatchBuildOptions
273
+ * @property {boolean} check - Whether every target must run its developer check.
274
+ * @property {string} expectedSnapshotHash - Coordinator-reconciled complete graph hash.
275
+ * @property {AbortSignal} publicationSignal - Publication-only cancellation authority.
276
+ * @property {AbortSignal} signal - Active check and publication cancellation authority.
277
+ * @property {() => Promise<void> | void} validateCurrentSnapshot - Complete graph guard at the pointer boundary.
278
+ */
279
+
280
+ /**
281
+ * @typedef ProjectWatchBuildControls
282
+ * @property {string} expectedSnapshotHash - Coordinator-reconciled complete graph hash.
283
+ * @property {AbortSignal} publicationSignal - Publication-only cancellation authority.
284
+ * @property {() => Promise<void> | void} validateCurrentSnapshot - Complete graph guard at the pointer boundary.
285
+ */
286
+
287
+ /**
288
+ * @typedef ProjectBuildReporterLike
289
+ * @property {(projectId: string) => void} projectLoaded - Reports validated configuration.
290
+ * @property {(snapshotHash: string) => void} snapshotLoaded - Reports a complete snapshot.
291
+ * @property {(target: ProjectBuildTargetResult) => void} targetGenerated - Reports generated target output.
292
+ * @property {(checked: {id: string, language: string, stage: import("./semantic/types.js").TargetCheckStageResult}) => void} targetChecked - Reports a successful check stage.
293
+ */
294
+
295
+ /**
296
+ * @typedef ProjectBuildTargetResult
297
+ * @property {number} artifactCount - Generated artifact count.
298
+ * @property {string} id - Project target identity.
299
+ * @property {import("./semantic/types.js").BackendLanguage} language - Backend target identity.
300
+ * @property {import("./semantic/types.js").PublicationTargetRole} role - Explicit target role.
301
+ * @property {Readonly<{stages: readonly import("./semantic/types.js").TargetCheckStageResult[]}>} [check] - Successful developer-check evidence.
302
+ */
303
+
304
+ /**
305
+ * @typedef ProjectBuildSuccess
306
+ * @property {"succeeded"} status - Terminal success state.
307
+ * @property {string} projectId - Stable project identity.
308
+ * @property {string} snapshotHash - Complete ordered input hash.
309
+ * @property {string} generationId - Deterministic immutable generation identity.
310
+ * @property {boolean} checked - Whether developer checks completed for every target.
311
+ * @property {readonly ProjectBuildTargetResult[]} targets - Ordered generated target results.
312
+ * @property {import("./semantic/types.js").PublishedGeneration} generation - Committed publisher snapshot.
313
+ */
314
+
315
+ /**
316
+ * Detaches explicit tool discovery configuration from caller mutation.
317
+ * @param {unknown} candidate - Candidate environment.
318
+ * @returns {Readonly<Record<string, string | undefined>>} Immutable snapshot.
319
+ */
320
+ function snapshotEnvironment(candidate) {
321
+ if (typeof candidate != "object" || candidate == null || Array.isArray(candidate)) {
322
+ throw new TypeError("Project builder environment must be a plain string record.")
323
+ }
324
+ const prototype = Object.getPrototypeOf(candidate)
325
+
326
+ if (prototype !== Object.prototype && prototype !== null) {
327
+ throw new TypeError("Project builder environment must be a plain string record.")
328
+ }
329
+ /** @type {Record<string, string | undefined>} */
330
+ const snapshot = Object.create(null)
331
+
332
+ for (const key of Reflect.ownKeys(candidate)) {
333
+ if (typeof key != "string") throw new TypeError("Project builder environment must use string keys.")
334
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, key)
335
+
336
+ if (descriptor == undefined || !descriptor.enumerable || !("value" in descriptor) ||
337
+ descriptor.value !== undefined && typeof descriptor.value != "string") {
338
+ throw new TypeError("Project builder environment must contain string or undefined data values.")
339
+ }
340
+ snapshot[key] = descriptor.value
341
+ }
342
+
343
+ return Object.freeze(snapshot)
344
+ }
345
+
346
+ /**
347
+ * Checks for an ordinary object-prototype record.
348
+ * @param {unknown} value - Candidate value.
349
+ * @returns {value is Record<string, unknown>} Whether the value is a plain object.
350
+ */
351
+ function isPlainObject(value) {
352
+ return typeof value == "object" && value != null && !Array.isArray(value) && Object.getPrototypeOf(value) == Object.prototype
353
+ }
@@ -0,0 +1,155 @@
1
+ // @ts-check
2
+
3
+ import {SemantifoldDiagnostic} from "./diagnostic.js"
4
+ import {ProjectBuilder} from "./project-build.js"
5
+ import {ProjectBuildReporter} from "./project-reporter.js"
6
+ import {ProjectWatchCoordinator} from "./project-watch.js"
7
+ import {ProjectWatchReporter} from "./project-watch-reporter.js"
8
+
9
+ /**
10
+ * Owns one strict command invocation while delegating durable build behavior.
11
+ */
12
+ export class SemantifoldCli {
13
+ /** @type {ProjectBuilder} */
14
+ #builder
15
+ /** @type {ProjectWatchCoordinator} */
16
+ #coordinator
17
+ /** @type {import("./project-reporter.js").WritableOutput} */
18
+ #stdout
19
+ /** @type {import("./project-reporter.js").WritableOutput} */
20
+ #stderr
21
+
22
+ /**
23
+ * Creates an importable CLI lifecycle.
24
+ * @param {{builder?: ProjectBuilder, coordinator?: ProjectWatchCoordinator, stderr?: import("./project-reporter.js").WritableOutput, stdout?: import("./project-reporter.js").WritableOutput}} [options] - Collaborators and sinks.
25
+ */
26
+ constructor(options = {}) {
27
+ this.#builder = options.builder ?? new ProjectBuilder()
28
+ this.#coordinator = options.coordinator ?? new ProjectWatchCoordinator({builder: this.#builder})
29
+ this.#stdout = options.stdout ?? process.stdout
30
+ this.#stderr = options.stderr ?? process.stderr
31
+ }
32
+
33
+ /**
34
+ * Runs one command without mutating process exit state.
35
+ * @param {readonly string[]} arguments_ - Arguments after the executable name.
36
+ * @returns {Promise<0 | 1>} Exact process status.
37
+ */
38
+ async run(arguments_) {
39
+ const hintedFormat = arguments_.includes("--ndjson") ? "ndjson" : "human"
40
+ const watchHint = arguments_[0] == "watch"
41
+ let reporter = watchHint
42
+ ? new ProjectWatchReporter({format: hintedFormat, stderr: this.#stderr, stdout: this.#stdout})
43
+ : new ProjectBuildReporter({format: hintedFormat, stderr: this.#stderr, stdout: this.#stdout})
44
+
45
+ try {
46
+ const options = parseSemantifoldCliArguments(arguments_)
47
+ const command = "command" in options ? options.command : "build"
48
+
49
+ if (options.format != hintedFormat) {
50
+ reporter = command == "watch"
51
+ ? new ProjectWatchReporter({format: options.format, stderr: this.#stderr, stdout: this.#stdout})
52
+ : new ProjectBuildReporter({format: options.format, stderr: this.#stderr, stdout: this.#stdout})
53
+ }
54
+ if (command == "watch") {
55
+ const result = await this.#coordinator.run(options.projectPath, /** @type {ProjectWatchReporter} */ (reporter), {
56
+ check: options.check
57
+ })
58
+
59
+ return result.status == "stopped" ? 0 : 1
60
+ }
61
+ const controller = new AbortController()
62
+ const forwardSignal = () => controller.abort("Semantifold CLI received a termination signal.")
63
+
64
+ if (options.check) {
65
+ process.once("SIGINT", forwardSignal)
66
+ process.once("SIGTERM", forwardSignal)
67
+ }
68
+ let result
69
+
70
+ try {
71
+ result = await this.#builder.build(options.projectPath, /** @type {ProjectBuildReporter} */ (reporter), {
72
+ check: options.check,
73
+ ...(options.check ? {signal: controller.signal} : {})
74
+ })
75
+ } finally {
76
+ if (options.check) {
77
+ process.removeListener("SIGINT", forwardSignal)
78
+ process.removeListener("SIGTERM", forwardSignal)
79
+ }
80
+ }
81
+
82
+ /** @type {ProjectBuildReporter} */ (reporter).succeeded(result)
83
+
84
+ return 0
85
+ } catch (error) {
86
+ if (reporter instanceof ProjectWatchReporter) reporter.watchFailed(error)
87
+ else reporter.failed(error)
88
+
89
+ return 1
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Parses the intentionally narrow one-shot CLI grammar.
96
+ * @param {readonly string[]} arguments_ - Arguments after the executable name.
97
+ * @returns {Readonly<{check: boolean, format: "human" | "ndjson", projectPath: string}> | Readonly<{command: "watch", check: boolean, format: "human" | "ndjson", projectPath: string}>} Validated command options.
98
+ */
99
+ export function parseSemantifoldCliArguments(arguments_) {
100
+ if (!Array.isArray(arguments_) || arguments_.length == 0 || arguments_[0] != "build" && arguments_[0] != "watch") {
101
+ invalidArguments("Expected the command 'semantifold build' or 'semantifold watch'.")
102
+ }
103
+ const command = /** @type {"build" | "watch"} */ (arguments_[0])
104
+ let format = /** @type {"human" | "ndjson"} */ ("human")
105
+ let projectPath = "./semantifold.json"
106
+ let projectSeen = false
107
+ let ndjsonSeen = false
108
+ let checkSeen = false
109
+
110
+ for (let index = 1; index < arguments_.length; index += 1) {
111
+ const argument = arguments_[index]
112
+
113
+ if (argument == "--check") {
114
+ if (checkSeen) invalidArguments("Option '--check' may be supplied only once.")
115
+ checkSeen = true
116
+ continue
117
+ }
118
+ if (argument == "--ndjson") {
119
+ if (ndjsonSeen) invalidArguments("Option '--ndjson' may be supplied only once.")
120
+ ndjsonSeen = true
121
+ format = "ndjson"
122
+ continue
123
+ }
124
+ if (argument == "--project") {
125
+ if (projectSeen) invalidArguments("Option '--project' may be supplied only once.")
126
+ const value = arguments_[index + 1]
127
+
128
+ if (typeof value != "string" || value.length == 0 || value.startsWith("--")) {
129
+ invalidArguments("Option '--project' requires one path value.")
130
+ }
131
+ projectSeen = true
132
+ projectPath = value
133
+ index += 1
134
+ continue
135
+ }
136
+ invalidArguments(`Unknown command argument '${argument}'.`)
137
+ }
138
+
139
+ return command == "watch"
140
+ ? Object.freeze({command, check: checkSeen, format, projectPath})
141
+ : Object.freeze({check: checkSeen, format, projectPath})
142
+ }
143
+
144
+ /**
145
+ * Throws one stable CLI grammar diagnostic.
146
+ * @param {string} message - Failure detail.
147
+ * @returns {never} Always throws.
148
+ */
149
+ function invalidArguments(message) {
150
+ throw new SemantifoldDiagnostic({
151
+ code: "INVALID_CLI_ARGUMENTS",
152
+ language: "cli",
153
+ message
154
+ })
155
+ }