semantifold 0.9.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.
- package/README.md +6 -3
- package/bin/semantifold.js +6 -0
- package/build/bin/semantifold.d.ts +3 -0
- package/build/bin/semantifold.d.ts.map +1 -0
- package/build/bin/semantifold.js +5 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9 -1
- package/build/src/artifact-path.d.ts +16 -0
- package/build/src/artifact-path.d.ts.map +1 -1
- package/build/src/artifact-path.js +29 -3
- package/build/src/backends/c-check.d.ts +7 -0
- package/build/src/backends/c-check.d.ts.map +1 -0
- package/build/src/backends/c-check.js +65 -0
- package/build/src/backends/check-plan.d.ts +96 -0
- package/build/src/backends/check-plan.d.ts.map +1 -0
- package/build/src/backends/check-plan.js +135 -0
- package/build/src/backends/cpp-check.d.ts +7 -0
- package/build/src/backends/cpp-check.d.ts.map +1 -0
- package/build/src/backends/cpp-check.js +59 -0
- package/build/src/backends/csharp-check.d.ts +7 -0
- package/build/src/backends/csharp-check.d.ts.map +1 -0
- package/build/src/backends/csharp-check.js +125 -0
- package/build/src/backends/csharp.d.ts +1 -0
- package/build/src/backends/csharp.d.ts.map +1 -1
- package/build/src/backends/csharp.js +5 -4
- package/build/src/backends/dart-check.d.ts +7 -0
- package/build/src/backends/dart-check.d.ts.map +1 -0
- package/build/src/backends/dart-check.js +103 -0
- package/build/src/backends/dart.d.ts +2 -0
- package/build/src/backends/dart.d.ts.map +1 -1
- package/build/src/backends/dart.js +5 -5
- package/build/src/backends/go-check.d.ts +7 -0
- package/build/src/backends/go-check.d.ts.map +1 -0
- package/build/src/backends/go-check.js +77 -0
- package/build/src/backends/go.d.ts +1 -0
- package/build/src/backends/go.d.ts.map +1 -1
- package/build/src/backends/go.js +3 -3
- package/build/src/backends/java-check.d.ts +21 -0
- package/build/src/backends/java-check.d.ts.map +1 -0
- package/build/src/backends/java-check.js +73 -0
- package/build/src/backends/javascript-check.d.ts +7 -0
- package/build/src/backends/javascript-check.d.ts.map +1 -0
- package/build/src/backends/javascript-check.js +29 -0
- package/build/src/backends/kotlin-check.d.ts +7 -0
- package/build/src/backends/kotlin-check.d.ts.map +1 -0
- package/build/src/backends/kotlin-check.js +31 -0
- package/build/src/backends/php-check.d.ts +7 -0
- package/build/src/backends/php-check.d.ts.map +1 -0
- package/build/src/backends/php-check.js +21 -0
- package/build/src/backends/program.d.ts +7 -0
- package/build/src/backends/program.d.ts.map +1 -1
- package/build/src/backends/program.js +16 -1
- package/build/src/backends/python-check.d.ts +7 -0
- package/build/src/backends/python-check.d.ts.map +1 -0
- package/build/src/backends/python-check.js +28 -0
- package/build/src/backends/ruby-check.d.ts +7 -0
- package/build/src/backends/ruby-check.d.ts.map +1 -0
- package/build/src/backends/ruby-check.js +21 -0
- package/build/src/backends/rust-check.d.ts +7 -0
- package/build/src/backends/rust-check.d.ts.map +1 -0
- package/build/src/backends/rust-check.js +65 -0
- package/build/src/backends/swift-check.d.ts +7 -0
- package/build/src/backends/swift-check.d.ts.map +1 -0
- package/build/src/backends/swift-check.js +77 -0
- package/build/src/backends/typescript-check.d.ts +7 -0
- package/build/src/backends/typescript-check.d.ts.map +1 -0
- package/build/src/backends/typescript-check.js +33 -0
- package/build/src/backends/zig-check.d.ts +7 -0
- package/build/src/backends/zig-check.d.ts.map +1 -0
- package/build/src/backends/zig-check.js +70 -0
- package/build/src/diagnostic.d.ts +13 -1
- package/build/src/diagnostic.d.ts.map +1 -1
- package/build/src/diagnostic.js +14 -2
- package/build/src/frontends/program.d.ts +6 -0
- package/build/src/frontends/program.d.ts.map +1 -1
- package/build/src/frontends/program.js +9 -1
- package/build/src/language-registry.d.ts +54 -0
- package/build/src/language-registry.d.ts.map +1 -1
- package/build/src/language-registry.js +107 -5
- package/build/src/project-build.d.ts +172 -0
- package/build/src/project-build.d.ts.map +1 -0
- package/build/src/project-build.js +320 -0
- package/build/src/project-cli.d.ts +40 -0
- package/build/src/project-cli.d.ts.map +1 -0
- package/build/src/project-cli.js +145 -0
- package/build/src/project-manifest.d.ts +148 -0
- package/build/src/project-manifest.d.ts.map +1 -0
- package/build/src/project-manifest.js +432 -0
- package/build/src/project-reporter.d.ts +81 -0
- package/build/src/project-reporter.d.ts.map +1 -0
- package/build/src/project-reporter.js +293 -0
- package/build/src/project-snapshot.d.ts +129 -0
- package/build/src/project-snapshot.d.ts.map +1 -0
- package/build/src/project-snapshot.js +362 -0
- package/build/src/project-watch-reporter.d.ts +125 -0
- package/build/src/project-watch-reporter.d.ts.map +1 -0
- package/build/src/project-watch-reporter.js +280 -0
- package/build/src/project-watch.d.ts +133 -0
- package/build/src/project-watch.d.ts.map +1 -0
- package/build/src/project-watch.js +839 -0
- package/build/src/publication.d.ts +69 -5
- package/build/src/publication.d.ts.map +1 -1
- package/build/src/publication.js +246 -57
- package/build/src/semantic/types.d.ts +303 -2
- package/build/src/semantic/types.d.ts.map +1 -1
- package/build/src/semantic/types.js +79 -2
- package/build/src/subprocess.d.ts +8 -0
- package/build/src/subprocess.d.ts.map +1 -1
- package/build/src/subprocess.js +50 -9
- package/build/src/target-check.d.ts +52 -0
- package/build/src/target-check.d.ts.map +1 -0
- package/build/src/target-check.js +796 -0
- package/build/src/toolchains.d.ts.map +1 -1
- package/build/src/toolchains.js +3 -3
- package/docs/architecture.md +3 -1
- package/docs/c.md +4 -0
- package/docs/cpp.md +4 -0
- package/docs/dart.md +4 -0
- package/docs/generated-artifact-publication.md +11 -8
- package/docs/kotlin.md +2 -2
- package/docs/language-support.md +29 -3
- package/docs/project-build-cli.md +147 -0
- package/docs/project-watch.md +60 -0
- package/docs/rust.md +4 -0
- package/docs/swift.md +2 -0
- package/docs/testing.md +70 -3
- package/docs/watch-build-pipeline.md +39 -23
- package/docs/zig.md +2 -0
- package/examples/jsdoc-java-watch/expected-output.txt +3 -0
- package/examples/jsdoc-java-watch/semantifold.json +23 -0
- package/examples/jsdoc-java-watch/src/main.js +21 -0
- package/index.js +8 -0
- package/package.json +6 -1
- package/src/artifact-path.js +31 -2
- package/src/backends/c-check.js +80 -0
- package/src/backends/check-plan.js +147 -0
- package/src/backends/cpp-check.js +73 -0
- package/src/backends/csharp-check.js +139 -0
- package/src/backends/csharp.js +4 -3
- package/src/backends/dart-check.js +117 -0
- package/src/backends/dart.js +4 -4
- package/src/backends/go-check.js +89 -0
- package/src/backends/go.js +2 -2
- package/src/backends/java-check.js +77 -0
- package/src/backends/javascript-check.js +33 -0
- package/src/backends/kotlin-check.js +41 -0
- package/src/backends/php-check.js +23 -0
- package/src/backends/program.js +17 -0
- package/src/backends/python-check.js +30 -0
- package/src/backends/ruby-check.js +23 -0
- package/src/backends/rust-check.js +78 -0
- package/src/backends/swift-check.js +89 -0
- package/src/backends/typescript-check.js +37 -0
- package/src/backends/zig-check.js +81 -0
- package/src/diagnostic.js +9 -1
- package/src/frontends/program.js +9 -0
- package/src/language-registry.js +113 -4
- package/src/project-build.js +353 -0
- package/src/project-cli.js +155 -0
- package/src/project-manifest.js +479 -0
- package/src/project-reporter.js +320 -0
- package/src/project-snapshot.js +403 -0
- package/src/project-watch-reporter.js +303 -0
- package/src/project-watch.js +870 -0
- package/src/publication.js +275 -58
- package/src/semantic/types.js +87 -1
- package/src/subprocess.js +49 -8
- package/src/target-check.js +844 -0
- package/src/toolchains.js +3 -2
- 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
|
+
}
|