semantifold 0.9.0 → 0.11.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/kotlin.d.ts +1 -1
- package/build/src/backends/kotlin.js +2 -2
- 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/shared.d.ts.map +1 -1
- package/build/src/backends/shared.js +29 -1
- 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/java.d.ts.map +1 -1
- package/build/src/frontends/java.js +23 -1
- package/build/src/frontends/javascript-typescript.d.ts.map +1 -1
- package/build/src/frontends/javascript-typescript.js +45 -4
- package/build/src/frontends/php.d.ts.map +1 -1
- package/build/src/frontends/php.js +54 -3
- 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/frontends/ruby.d.ts.map +1 -1
- package/build/src/frontends/ruby.js +55 -5
- 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 +124 -10
- 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/effects.js +3 -1
- package/build/src/semantic/provenance.d.ts.map +1 -1
- package/build/src/semantic/provenance.js +11 -1
- package/build/src/semantic/types.d.ts +355 -4
- package/build/src/semantic/types.d.ts.map +1 -1
- package/build/src/semantic/types.js +92 -3
- package/build/src/semantic/validate.js +18 -1
- 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/adr-expression-breadth.md +186 -0
- 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 +40 -6
- package/docs/parser-qualification.md +1 -1
- 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/kotlin.js +1 -1
- 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/shared.js +30 -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/java.js +24 -0
- package/src/frontends/javascript-typescript.js +49 -2
- package/src/frontends/php.js +61 -2
- package/src/frontends/program.js +9 -0
- package/src/frontends/ruby.js +62 -2
- package/src/language-registry.js +130 -9
- 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/effects.js +1 -0
- package/src/semantic/provenance.js +8 -0
- package/src/semantic/types.js +101 -2
- package/src/semantic/validate.js +26 -0
- package/src/subprocess.js +49 -8
- package/src/target-check.js +844 -0
- package/src/toolchains.js +3 -2
- package/tsconfig.json +1 -1
package/src/publication.js
CHANGED
|
@@ -29,6 +29,26 @@ const publicationBoundaries = new Set([
|
|
|
29
29
|
const injectedPublicationFailures = new WeakMap()
|
|
30
30
|
/** @type {WeakMap<GeneratedArtifactPublisher, (operation: string) => void>} */
|
|
31
31
|
const publicationFilesystemObservers = new WeakMap()
|
|
32
|
+
/** @type {WeakMap<GeneratedArtifactPublisher, (() => Promise<void> | void)[]>} */
|
|
33
|
+
const publicationCommitGuards = new WeakMap()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Queues one internal caller-owned currentness guard for the publisher's next transaction.
|
|
37
|
+
* The guard runs after every validator closes and immediately before the active-pointer commit.
|
|
38
|
+
* This helper is intentionally not part of the package root API.
|
|
39
|
+
* @param {GeneratedArtifactPublisher} publisher - Publisher instance.
|
|
40
|
+
* @param {() => Promise<void> | void} guard - Exact pre-commit guard.
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
export function guardNextPublicationCommit(publisher, guard) {
|
|
44
|
+
if (!(publisher instanceof GeneratedArtifactPublisher) || typeof guard != "function") {
|
|
45
|
+
throw new TypeError("Invalid publication commit guard.")
|
|
46
|
+
}
|
|
47
|
+
const queued = publicationCommitGuards.get(publisher) ?? []
|
|
48
|
+
|
|
49
|
+
queued.push(guard)
|
|
50
|
+
publicationCommitGuards.set(publisher, queued)
|
|
51
|
+
}
|
|
32
52
|
|
|
33
53
|
/**
|
|
34
54
|
* Installs one internal, one-shot deterministic failure used by real-filesystem interruption specs.
|
|
@@ -71,6 +91,8 @@ export class GeneratedArtifactPublisher {
|
|
|
71
91
|
#publicationRoot
|
|
72
92
|
/** @type {readonly string[]} */
|
|
73
93
|
#sourceRoots
|
|
94
|
+
/** @type {readonly string[]} */
|
|
95
|
+
#sourceFiles
|
|
74
96
|
/** @type {Promise<void>} */
|
|
75
97
|
#publicationTail = Promise.resolve()
|
|
76
98
|
|
|
@@ -80,13 +102,14 @@ export class GeneratedArtifactPublisher {
|
|
|
80
102
|
* @param {string} options.projectId - Stable lowercase project identity.
|
|
81
103
|
* @param {string} options.projectRoot - Canonical absolute project root.
|
|
82
104
|
* @param {string} options.publicationRoot - Canonical absolute publisher-owned directory beneath the project root.
|
|
83
|
-
* @param {readonly string[]} options.sourceRoots - Canonical absolute project source
|
|
105
|
+
* @param {readonly string[]} [options.sourceRoots] - Canonical absolute project source directories kept disjoint from publication.
|
|
106
|
+
* @param {readonly string[]} [options.sourceFiles] - Canonical absolute project source files kept disjoint from publication.
|
|
84
107
|
*/
|
|
85
108
|
constructor(options) {
|
|
86
109
|
if (!isPlainObject(options)) {
|
|
87
110
|
publicationFailure("INVALID_PUBLICATION_REQUEST", "publication", "Publisher options must be a plain object.")
|
|
88
111
|
}
|
|
89
|
-
const {projectId, projectRoot, publicationRoot, sourceRoots} = options
|
|
112
|
+
const {projectId, projectRoot, publicationRoot, sourceFiles = [], sourceRoots = []} = options
|
|
90
113
|
|
|
91
114
|
if (!identityPattern.test(projectId ?? "")) {
|
|
92
115
|
publicationFailure("INVALID_PUBLICATION_REQUEST", "publication", "Project identity must be a stable lowercase ID.")
|
|
@@ -96,8 +119,9 @@ export class GeneratedArtifactPublisher {
|
|
|
96
119
|
publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
|
|
97
120
|
"Project and publication roots must be canonical absolute paths with publication strictly beneath the project root.")
|
|
98
121
|
}
|
|
99
|
-
if (!isDenseArray(sourceRoots) || sourceRoots.length == 0) {
|
|
100
|
-
publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
|
|
122
|
+
if (!isDenseArray(sourceRoots) || !isDenseArray(sourceFiles) || sourceRoots.length + sourceFiles.length == 0) {
|
|
123
|
+
publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
|
|
124
|
+
"Publisher requires canonical absolute source directories or source files.")
|
|
101
125
|
}
|
|
102
126
|
/** @type {string[]} */
|
|
103
127
|
const validatedSourceRoots = []
|
|
@@ -119,20 +143,43 @@ export class GeneratedArtifactPublisher {
|
|
|
119
143
|
}
|
|
120
144
|
validatedSourceRoots.push(sourceRoot)
|
|
121
145
|
}
|
|
146
|
+
/** @type {string[]} */
|
|
147
|
+
const validatedSourceFiles = []
|
|
148
|
+
|
|
149
|
+
for (let index = 0; index < sourceFiles.length; index += 1) {
|
|
150
|
+
const sourceFile = sourceFiles[index]
|
|
151
|
+
|
|
152
|
+
if (!isCanonicalAbsolutePath(sourceFile) || !isPathWithin(projectRoot, sourceFile)) {
|
|
153
|
+
publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
|
|
154
|
+
"Source files must be canonical absolute paths within the project root.")
|
|
155
|
+
}
|
|
156
|
+
if (pathsOverlap(publicationRoot, sourceFile)) {
|
|
157
|
+
publicationFailure("PUBLICATION_SOURCE_OVERLAP", projectId,
|
|
158
|
+
"Publication root and source files must be disjoint.")
|
|
159
|
+
}
|
|
160
|
+
validatedSourceFiles.push(sourceFile)
|
|
161
|
+
}
|
|
122
162
|
|
|
123
163
|
this.#projectId = projectId
|
|
124
164
|
this.#projectRoot = projectRoot
|
|
125
165
|
this.#publicationRoot = publicationRoot
|
|
126
166
|
this.#sourceRoots = Object.freeze(validatedSourceRoots)
|
|
167
|
+
this.#sourceFiles = Object.freeze(validatedSourceFiles)
|
|
127
168
|
}
|
|
128
169
|
|
|
129
170
|
/**
|
|
130
171
|
* Stages, validates, and publishes one complete immutable project generation.
|
|
131
172
|
* Calls on one publisher instance are serialized in invocation order.
|
|
132
173
|
* @param {import("./semantic/types.js").PublicationRequest} request - Complete publication request.
|
|
174
|
+
* @param {import("./semantic/types.js").PublicationOptions} [options] - Transaction lifecycle controls.
|
|
133
175
|
* @returns {Promise<import("./semantic/types.js").PublishedGeneration>} Committed immutable snapshot.
|
|
134
176
|
*/
|
|
135
|
-
publish(request) {
|
|
177
|
+
publish(request, options = {}) {
|
|
178
|
+
const signal = validatePublicationOptions(options, this.#projectId)
|
|
179
|
+
const queuedGuards = publicationCommitGuards.get(this)
|
|
180
|
+
const commitGuard = queuedGuards?.shift()
|
|
181
|
+
|
|
182
|
+
if (queuedGuards?.length == 0) publicationCommitGuards.delete(this)
|
|
136
183
|
/** @type {(value: import("./semantic/types.js").PublishedGeneration) => void} */
|
|
137
184
|
let resolveResult
|
|
138
185
|
/** @type {(reason?: unknown) => void} */
|
|
@@ -143,7 +190,7 @@ export class GeneratedArtifactPublisher {
|
|
|
143
190
|
})
|
|
144
191
|
const operation = this.#publicationTail.then(async () => {
|
|
145
192
|
try {
|
|
146
|
-
resolveResult(await this.#publish(request))
|
|
193
|
+
resolveResult(await this.#publish(request, signal, commitGuard))
|
|
147
194
|
} catch (error) {
|
|
148
195
|
rejectResult(error)
|
|
149
196
|
}
|
|
@@ -173,11 +220,14 @@ export class GeneratedArtifactPublisher {
|
|
|
173
220
|
/**
|
|
174
221
|
* Performs one serialized publication.
|
|
175
222
|
* @param {import("./semantic/types.js").PublicationRequest} request - Complete request.
|
|
223
|
+
* @param {AbortSignal | undefined} signal - Optional transaction cancellation authority.
|
|
224
|
+
* @param {(() => Promise<void> | void) | undefined} commitGuard - Optional currentness guard.
|
|
176
225
|
* @returns {Promise<import("./semantic/types.js").PublishedGeneration>} Publication result.
|
|
177
226
|
*/
|
|
178
|
-
async #publish(request) {
|
|
227
|
+
async #publish(request, signal, commitGuard) {
|
|
179
228
|
const validated = validatePublicationRequest(request, this.#projectId)
|
|
180
229
|
|
|
230
|
+
assertPublicationActive(signal, this.#projectId)
|
|
181
231
|
await this.#prepareRoot()
|
|
182
232
|
await reconcilePublication(this.#publicationRoot, this.#projectId, this)
|
|
183
233
|
await verifyAtomicReplacement(this.#publicationRoot, this.#projectId)
|
|
@@ -188,55 +238,80 @@ export class GeneratedArtifactPublisher {
|
|
|
188
238
|
const tempPointerName = `.active-generation.${validated.generationId}.tmp`
|
|
189
239
|
const tempPointerPath = path.join(this.#publicationRoot, tempPointerName)
|
|
190
240
|
const journalPath = path.join(this.#publicationRoot, journalName, `${validated.generationId}.json`)
|
|
241
|
+
const retainedGeneration = await optionalStatus(generationPath, this.#projectId, "retained generation") !== null
|
|
191
242
|
let committed = false
|
|
192
243
|
let journalCreated = false
|
|
193
244
|
let generationCreated = false
|
|
194
245
|
let tempPointerCreated = false
|
|
195
246
|
let failureCode = "PUBLICATION_STAGE_WRITE_FAILED"
|
|
247
|
+
/** @type {import("./semantic/types.js").GenerationManifest | null} */
|
|
248
|
+
let manifest = null
|
|
249
|
+
/** @type {Buffer | null} */
|
|
250
|
+
let manifestBytes = null
|
|
196
251
|
|
|
197
252
|
try {
|
|
198
|
-
await assertAbsent(generationPath, "PUBLICATION_UNOWNED_CONFLICT", this.#projectId,
|
|
199
|
-
"Generation identity is already owned or ambiguous.")
|
|
200
253
|
await assertAbsent(tempPointerPath, "PUBLICATION_UNOWNED_CONFLICT", this.#projectId,
|
|
201
254
|
"Temporary active pointer path is already owned or ambiguous.")
|
|
202
255
|
await assertAbsent(journalPath, "PUBLICATION_UNOWNED_CONFLICT", this.#projectId,
|
|
203
256
|
"Publication journal identity is already owned or ambiguous.")
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
version: 1
|
|
257
|
+
if (retainedGeneration) {
|
|
258
|
+
const retained = await verifyRetainedGeneration(generationPath, validated, this.#projectId)
|
|
259
|
+
|
|
260
|
+
manifest = retained.manifest
|
|
261
|
+
manifestBytes = retained.manifestBytes
|
|
210
262
|
}
|
|
263
|
+
const journal = retainedGeneration
|
|
264
|
+
? {
|
|
265
|
+
generationId: validated.generationId,
|
|
266
|
+
operation: "reactivate",
|
|
267
|
+
projectId: this.#projectId,
|
|
268
|
+
schema: "SemantifoldPublicationJournal",
|
|
269
|
+
tempPointer: tempPointerName,
|
|
270
|
+
version: 2
|
|
271
|
+
}
|
|
272
|
+
: {
|
|
273
|
+
generationId: validated.generationId,
|
|
274
|
+
projectId: this.#projectId,
|
|
275
|
+
schema: "SemantifoldPublicationJournal",
|
|
276
|
+
tempPointer: tempPointerName,
|
|
277
|
+
version: 1
|
|
278
|
+
}
|
|
211
279
|
|
|
212
280
|
await writeSyncedFile(journalPath, serializeJson(journal), "wx")
|
|
213
281
|
journalCreated = true
|
|
214
282
|
await syncDirectory(path.dirname(journalPath))
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
283
|
+
if (!retainedGeneration) {
|
|
284
|
+
await mkdir(generationPath, {mode: 0o700})
|
|
285
|
+
generationCreated = true
|
|
286
|
+
await syncDirectory(path.dirname(generationPath))
|
|
287
|
+
|
|
288
|
+
/** @type {import("./semantic/types.js").GenerationTargetManifest[]} */
|
|
289
|
+
const targetManifests = []
|
|
290
|
+
|
|
291
|
+
for (const target of validated.targets) {
|
|
292
|
+
targetManifests.push(await stageTarget(generationPath, target, this.#projectId))
|
|
293
|
+
}
|
|
294
|
+
const manifestCandidate = /** @type {import("./semantic/types.js").GenerationManifest} */ ({
|
|
295
|
+
generationId: validated.generationId,
|
|
296
|
+
projectId: this.#projectId,
|
|
297
|
+
schema: /** @type {const} */ ("SemantifoldGenerationManifest"),
|
|
298
|
+
targets: targetManifests,
|
|
299
|
+
version: /** @type {const} */ (1)
|
|
300
|
+
})
|
|
218
301
|
|
|
219
|
-
|
|
220
|
-
|
|
302
|
+
manifestBytes = serializeJson(manifestCandidate)
|
|
303
|
+
manifest = validateGenerationManifest(JSON.parse(manifestBytes.toString("utf8")), this.#projectId,
|
|
304
|
+
validated.generationId)
|
|
221
305
|
|
|
222
|
-
|
|
223
|
-
|
|
306
|
+
await writeSyncedFile(manifestPath, manifestBytes, "wx")
|
|
307
|
+
await syncTreeDirectories(generationPath)
|
|
308
|
+
await verifyManifestFiles(generationPath, manifest, this.#projectId)
|
|
309
|
+
}
|
|
310
|
+
if (manifest === null || manifestBytes === null) {
|
|
311
|
+
publicationFailure("PUBLICATION_STAGE_WRITE_FAILED", this.#projectId,
|
|
312
|
+
"Candidate generation manifest could not be prepared.")
|
|
224
313
|
}
|
|
225
314
|
reachPublicationBoundary(this, "after-stage")
|
|
226
|
-
const manifestCandidate = /** @type {import("./semantic/types.js").GenerationManifest} */ ({
|
|
227
|
-
generationId: validated.generationId,
|
|
228
|
-
projectId: this.#projectId,
|
|
229
|
-
schema: /** @type {const} */ ("SemantifoldGenerationManifest"),
|
|
230
|
-
targets: targetManifests,
|
|
231
|
-
version: /** @type {const} */ (1)
|
|
232
|
-
})
|
|
233
|
-
const manifestBytes = serializeJson(manifestCandidate)
|
|
234
|
-
const manifest = validateGenerationManifest(JSON.parse(manifestBytes.toString("utf8")), this.#projectId,
|
|
235
|
-
validated.generationId)
|
|
236
|
-
|
|
237
|
-
await writeSyncedFile(manifestPath, manifestBytes, "wx")
|
|
238
|
-
await syncTreeDirectories(generationPath)
|
|
239
|
-
await verifyManifestFiles(generationPath, manifest, this.#projectId)
|
|
240
315
|
failureCode = "PUBLICATION_POINTER_FAILED"
|
|
241
316
|
|
|
242
317
|
const pointer = {
|
|
@@ -251,6 +326,8 @@ export class GeneratedArtifactPublisher {
|
|
|
251
326
|
tempPointerCreated = true
|
|
252
327
|
})
|
|
253
328
|
reachPublicationBoundary(this, "before-pointer-replace")
|
|
329
|
+
await commitGuard?.()
|
|
330
|
+
assertPublicationActive(signal, this.#projectId)
|
|
254
331
|
await rename(tempPointerPath, path.join(this.#publicationRoot, activePointerName))
|
|
255
332
|
tempPointerCreated = false
|
|
256
333
|
committed = true
|
|
@@ -339,11 +416,65 @@ export class GeneratedArtifactPublisher {
|
|
|
339
416
|
"Publication root and source roots resolve to overlapping paths.")
|
|
340
417
|
}
|
|
341
418
|
}
|
|
419
|
+
for (const sourceFile of this.#sourceFiles) {
|
|
420
|
+
await ensureDirectoryWithoutSymlinks(path.dirname(sourceFile), false, this.#projectId)
|
|
421
|
+
let status
|
|
422
|
+
let exactSource
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
status = await lstat(sourceFile)
|
|
426
|
+
exactSource = await realpath(sourceFile)
|
|
427
|
+
} catch (error) {
|
|
428
|
+
publicationFailure("INVALID_PUBLICATION_ROOT", this.#projectId,
|
|
429
|
+
"Source file could not be resolved safely.", error)
|
|
430
|
+
}
|
|
431
|
+
if (status.isSymbolicLink()) {
|
|
432
|
+
publicationFailure("PUBLICATION_SYMLINK_TRAVERSAL", this.#projectId,
|
|
433
|
+
"Source file traversal encountered a symbolic link.")
|
|
434
|
+
}
|
|
435
|
+
if (!status.isFile() || exactSource != sourceFile || !isPathWithin(exactProject, exactSource)) {
|
|
436
|
+
publicationFailure("INVALID_PUBLICATION_ROOT", this.#projectId,
|
|
437
|
+
"Source files must resolve to real regular files within the project root.")
|
|
438
|
+
}
|
|
439
|
+
if (pathsOverlap(exactPublication, exactSource)) {
|
|
440
|
+
publicationFailure("PUBLICATION_SOURCE_OVERLAP", this.#projectId,
|
|
441
|
+
"Publication root and source files resolve to overlapping paths.")
|
|
442
|
+
}
|
|
443
|
+
}
|
|
342
444
|
await ensureOwnedDirectory(path.join(this.#publicationRoot, generationsName), this.#projectId)
|
|
343
445
|
await ensureOwnedDirectory(path.join(this.#publicationRoot, journalName), this.#projectId)
|
|
344
446
|
}
|
|
345
447
|
}
|
|
346
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Validates and snapshots transaction lifecycle controls before publication is queued.
|
|
451
|
+
* @param {unknown} options - Candidate options.
|
|
452
|
+
* @param {string} projectId - Project diagnostic identity.
|
|
453
|
+
* @returns {AbortSignal | undefined} Optional cancellation signal.
|
|
454
|
+
*/
|
|
455
|
+
function validatePublicationOptions(options, projectId) {
|
|
456
|
+
if (!isPlainObject(options) || Object.keys(options).some(key => key != "signal") ||
|
|
457
|
+
options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
|
|
458
|
+
publicationFailure("INVALID_PUBLICATION_REQUEST", projectId,
|
|
459
|
+
"Publication options require only an optional AbortSignal.")
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return /** @type {AbortSignal | undefined} */ (options.signal)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Rejects cancellation before the atomic active-pointer commit begins.
|
|
467
|
+
* @param {AbortSignal | undefined} signal - Optional transaction cancellation authority.
|
|
468
|
+
* @param {string} projectId - Project diagnostic identity.
|
|
469
|
+
* @returns {void}
|
|
470
|
+
*/
|
|
471
|
+
function assertPublicationActive(signal, projectId) {
|
|
472
|
+
if (signal?.aborted) {
|
|
473
|
+
publicationFailure("PUBLICATION_CANCELLED", projectId,
|
|
474
|
+
"Publication was cancelled before the active generation pointer commit.")
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
347
478
|
/**
|
|
348
479
|
* Throws and consumes a configured internal failure at an exact lifecycle boundary.
|
|
349
480
|
* @param {GeneratedArtifactPublisher} publisher - Active publisher.
|
|
@@ -388,8 +519,9 @@ async function reconcilePublication(publicationRoot, projectId, publisher) {
|
|
|
388
519
|
const candidatePath = ownedPath(path.join(publicationRoot, generationsName), journal.generationId, projectId)
|
|
389
520
|
const temporaryPointerPath = path.join(publicationRoot, journal.tempPointer)
|
|
390
521
|
const committed = pointer?.generationId == journal.generationId
|
|
522
|
+
const candidateOwned = journal.version == 1
|
|
391
523
|
|
|
392
|
-
if (!committed) {
|
|
524
|
+
if (candidateOwned && !committed) {
|
|
393
525
|
const candidateStatus = await optionalStatus(candidatePath, projectId, "candidate generation")
|
|
394
526
|
|
|
395
527
|
if (candidateStatus) {
|
|
@@ -408,7 +540,7 @@ async function reconcilePublication(publicationRoot, projectId, publisher) {
|
|
|
408
540
|
}
|
|
409
541
|
}
|
|
410
542
|
await removeJournalOwnedState({
|
|
411
|
-
...(!committed ? {candidatePath} : {}),
|
|
543
|
+
...(candidateOwned && !committed ? {candidatePath} : {}),
|
|
412
544
|
journalPath,
|
|
413
545
|
projectId,
|
|
414
546
|
publicationRoot,
|
|
@@ -542,7 +674,7 @@ async function optionalStatus(filename, projectId, subject) {
|
|
|
542
674
|
* Validates and snapshots one publication request before staging.
|
|
543
675
|
* @param {unknown} request - Request candidate.
|
|
544
676
|
* @param {string} projectId - Project diagnostic identity.
|
|
545
|
-
* @returns {
|
|
677
|
+
* @returns {ValidatedPublicationRequest} Validated request.
|
|
546
678
|
*/
|
|
547
679
|
function validatePublicationRequest(request, projectId) {
|
|
548
680
|
if (!isPlainObject(request)) {
|
|
@@ -661,6 +793,56 @@ function validatePublicationRequest(request, projectId) {
|
|
|
661
793
|
return {generationId, targets: Object.freeze(targets)}
|
|
662
794
|
}
|
|
663
795
|
|
|
796
|
+
/**
|
|
797
|
+
* Verifies that one retained immutable generation exactly represents a validator-free request.
|
|
798
|
+
* The retained directory is never modified or removed on either success or failure.
|
|
799
|
+
* @param {string} generationPath - Existing generation path.
|
|
800
|
+
* @param {ValidatedPublicationRequest} request - Validated candidate request.
|
|
801
|
+
* @param {string} projectId - Project diagnostic identity.
|
|
802
|
+
* @returns {Promise<{manifest: import("./semantic/types.js").GenerationManifest, manifestBytes: Buffer}>} Exact retained manifest.
|
|
803
|
+
*/
|
|
804
|
+
async function verifyRetainedGeneration(generationPath, request, projectId) {
|
|
805
|
+
if (request.targets.some(target => target.validators.length != 0)) {
|
|
806
|
+
publicationFailure("PUBLICATION_UNOWNED_CONFLICT", projectId,
|
|
807
|
+
"Retained generations can be reactivated only for exact validator-free candidates.")
|
|
808
|
+
}
|
|
809
|
+
const targetManifests = request.targets.map(target => Object.freeze({
|
|
810
|
+
artifacts: Object.freeze(target.artifactSet.artifacts.map(artifact => describeGeneratedArtifact(artifact).manifest)),
|
|
811
|
+
buildArtifacts: Object.freeze([]),
|
|
812
|
+
id: target.id,
|
|
813
|
+
...(target.artifactSet.metadata === undefined ? {} : {metadata: target.artifactSet.metadata}),
|
|
814
|
+
projections: Object.freeze({build: target.buildProjection, source: target.sourceProjection}),
|
|
815
|
+
role: target.role,
|
|
816
|
+
target: target.artifactSet.target
|
|
817
|
+
}))
|
|
818
|
+
const candidate = {
|
|
819
|
+
generationId: request.generationId,
|
|
820
|
+
projectId,
|
|
821
|
+
schema: "SemantifoldGenerationManifest",
|
|
822
|
+
targets: targetManifests,
|
|
823
|
+
version: 1
|
|
824
|
+
}
|
|
825
|
+
const expectedBytes = serializeJson(candidate)
|
|
826
|
+
const manifest = validateGenerationManifest(JSON.parse(expectedBytes.toString("utf8")), projectId, request.generationId)
|
|
827
|
+
let manifestBytes
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
manifestBytes = await readAndSyncRegularFile(path.join(generationPath, "manifest.json"), projectId,
|
|
831
|
+
"MALFORMED_GENERATION_MANIFEST", "Retained generation manifest must be a regular file.")
|
|
832
|
+
if (!manifestBytes.equals(expectedBytes)) {
|
|
833
|
+
publicationFailure("PUBLICATION_UNOWNED_CONFLICT", projectId,
|
|
834
|
+
"Retained generation does not exactly match the requested candidate.")
|
|
835
|
+
}
|
|
836
|
+
await verifyManifestFiles(generationPath, manifest, projectId)
|
|
837
|
+
} catch (error) {
|
|
838
|
+
if (error instanceof SemantifoldDiagnostic && error.code == "PUBLICATION_UNOWNED_CONFLICT") throw error
|
|
839
|
+
publicationFailure("PUBLICATION_UNOWNED_CONFLICT", projectId,
|
|
840
|
+
"Retained generation could not be verified as the exact requested candidate.", error)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return {manifest, manifestBytes}
|
|
844
|
+
}
|
|
845
|
+
|
|
664
846
|
/**
|
|
665
847
|
* Stages one validated target and invokes its ordered validators.
|
|
666
848
|
* @param {string} generationPath - Candidate generation path.
|
|
@@ -679,27 +861,17 @@ async function stageTarget(generationPath, target, projectId) {
|
|
|
679
861
|
|
|
680
862
|
for (const artifact of target.artifactSet.artifacts) {
|
|
681
863
|
const filename = ownedPath(sourcePath, artifact.path, projectId)
|
|
682
|
-
const
|
|
683
|
-
? Buffer.from(/** @type {string} */ (artifact.content), "utf8")
|
|
684
|
-
: Buffer.from(/** @type {Uint8Array} */ (artifact.content))
|
|
864
|
+
const described = describeGeneratedArtifact(artifact)
|
|
685
865
|
|
|
686
866
|
await mkdir(path.dirname(filename), {mode: 0o700, recursive: true})
|
|
687
867
|
try {
|
|
688
|
-
await writeSyncedFile(filename, bytes, "wx")
|
|
868
|
+
await writeSyncedFile(filename, described.bytes, "wx")
|
|
689
869
|
} catch (error) {
|
|
690
870
|
if (error instanceof SemantifoldDiagnostic) throw error
|
|
691
871
|
publicationFailure("PUBLICATION_STAGE_WRITE_FAILED", projectId,
|
|
692
872
|
`Generated artifact '${artifact.path}' could not be staged.`, error)
|
|
693
873
|
}
|
|
694
|
-
artifacts.push(
|
|
695
|
-
byteLength: bytes.byteLength,
|
|
696
|
-
contentKind: artifact.contentKind,
|
|
697
|
-
hash: Object.freeze({algorithm: /** @type {const} */ ("sha256"), value: sha256(bytes)}),
|
|
698
|
-
mediaType: artifact.mediaType,
|
|
699
|
-
path: artifact.path,
|
|
700
|
-
provenance: artifact.provenance,
|
|
701
|
-
role: artifact.role
|
|
702
|
-
}))
|
|
874
|
+
artifacts.push(described.manifest)
|
|
703
875
|
}
|
|
704
876
|
|
|
705
877
|
/** @type {import("./semantic/types.js").PublicationBuildArtifactInput[]} */
|
|
@@ -765,6 +937,30 @@ async function stageTarget(generationPath, target, projectId) {
|
|
|
765
937
|
})
|
|
766
938
|
}
|
|
767
939
|
|
|
940
|
+
/**
|
|
941
|
+
* Derives exact staged bytes and their deterministic generation-manifest record.
|
|
942
|
+
* @param {import("./semantic/types.js").GeneratedSetArtifact} artifact - Validated generated artifact.
|
|
943
|
+
* @returns {{bytes: Buffer, manifest: import("./semantic/types.js").GenerationArtifactManifest}} Bytes and manifest record.
|
|
944
|
+
*/
|
|
945
|
+
function describeGeneratedArtifact(artifact) {
|
|
946
|
+
const bytes = artifact.contentKind == "text"
|
|
947
|
+
? Buffer.from(/** @type {string} */ (artifact.content), "utf8")
|
|
948
|
+
: Buffer.from(/** @type {Uint8Array} */ (artifact.content))
|
|
949
|
+
|
|
950
|
+
return {
|
|
951
|
+
bytes,
|
|
952
|
+
manifest: Object.freeze({
|
|
953
|
+
byteLength: bytes.byteLength,
|
|
954
|
+
contentKind: artifact.contentKind,
|
|
955
|
+
hash: Object.freeze({algorithm: /** @type {const} */ ("sha256"), value: sha256(bytes)}),
|
|
956
|
+
mediaType: artifact.mediaType,
|
|
957
|
+
path: artifact.path,
|
|
958
|
+
provenance: artifact.provenance,
|
|
959
|
+
role: artifact.role
|
|
960
|
+
})
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
768
964
|
/**
|
|
769
965
|
* Validates validator/compiler output metadata.
|
|
770
966
|
* @param {unknown} descriptor - Metadata candidate.
|
|
@@ -1748,11 +1944,14 @@ function isActivePointer(value) {
|
|
|
1748
1944
|
* @returns {value is PublicationJournal} Whether the value is strict and versioned.
|
|
1749
1945
|
*/
|
|
1750
1946
|
function isPublicationJournal(value) {
|
|
1751
|
-
|
|
1752
|
-
value.
|
|
1753
|
-
typeof value.
|
|
1754
|
-
|
|
1755
|
-
|
|
1947
|
+
if (!isPlainObject(value) || value.schema != "SemantifoldPublicationJournal" ||
|
|
1948
|
+
typeof value.projectId != "string" || !identityPattern.test(value.projectId) ||
|
|
1949
|
+
typeof value.generationId != "string" || !generationPattern.test(value.generationId) ||
|
|
1950
|
+
value.tempPointer != `.active-generation.${value.generationId}.tmp`) return false
|
|
1951
|
+
|
|
1952
|
+
return value.version == 1 && hasExactKeys(value, ["generationId", "projectId", "schema", "tempPointer", "version"]) ||
|
|
1953
|
+
value.version == 2 && value.operation == "reactivate" &&
|
|
1954
|
+
hasExactKeys(value, ["generationId", "operation", "projectId", "schema", "tempPointer", "version"])
|
|
1756
1955
|
}
|
|
1757
1956
|
|
|
1758
1957
|
/**
|
|
@@ -1835,6 +2034,12 @@ function publicationFailure(code, projectId, message, error) {
|
|
|
1835
2034
|
* @property {readonly import("./semantic/types.js").PublicationValidator[]} validators - Ordered validators.
|
|
1836
2035
|
*/
|
|
1837
2036
|
|
|
2037
|
+
/**
|
|
2038
|
+
* @typedef ValidatedPublicationRequest
|
|
2039
|
+
* @property {string} generationId - Snapshotted generation identity.
|
|
2040
|
+
* @property {readonly ValidatedPublicationTarget[]} targets - Snapshotted ordered targets.
|
|
2041
|
+
*/
|
|
2042
|
+
|
|
1838
2043
|
/**
|
|
1839
2044
|
* @typedef ActivePointer
|
|
1840
2045
|
* @property {"SemantifoldActiveGeneration"} schema - Schema discriminator.
|
|
@@ -1845,10 +2050,22 @@ function publicationFailure(code, projectId, message, error) {
|
|
|
1845
2050
|
*/
|
|
1846
2051
|
|
|
1847
2052
|
/**
|
|
1848
|
-
* @typedef
|
|
2053
|
+
* @typedef CandidatePublicationJournal
|
|
1849
2054
|
* @property {"SemantifoldPublicationJournal"} schema - Schema discriminator.
|
|
1850
2055
|
* @property {1} version - Schema version.
|
|
1851
2056
|
* @property {string} projectId - Project identity.
|
|
1852
2057
|
* @property {string} generationId - Candidate generation identity.
|
|
1853
2058
|
* @property {string} tempPointer - Owned sibling temporary-pointer filename.
|
|
1854
2059
|
*/
|
|
2060
|
+
|
|
2061
|
+
/**
|
|
2062
|
+
* @typedef ReactivationPublicationJournal
|
|
2063
|
+
* @property {"SemantifoldPublicationJournal"} schema - Schema discriminator.
|
|
2064
|
+
* @property {2} version - Schema version.
|
|
2065
|
+
* @property {"reactivate"} operation - Retained-generation activation without candidate ownership.
|
|
2066
|
+
* @property {string} projectId - Project identity.
|
|
2067
|
+
* @property {string} generationId - Retained generation identity.
|
|
2068
|
+
* @property {string} tempPointer - Owned sibling temporary-pointer filename.
|
|
2069
|
+
*/
|
|
2070
|
+
|
|
2071
|
+
/** @typedef {CandidatePublicationJournal | ReactivationPublicationJournal} PublicationJournal */
|
package/src/semantic/effects.js
CHANGED
|
@@ -722,6 +722,7 @@ function expressionChildren(expression) {
|
|
|
722
722
|
if (expression.kind == "MethodCallExpression") return [expression.receiver, ...expression.arguments]
|
|
723
723
|
if (expression.kind == "OwnedBorrowExpression" || expression.kind == "OwnedMoveExpression") return [expression.expression]
|
|
724
724
|
if (expression.kind == "BinaryExpression") return [expression.left, expression.right]
|
|
725
|
+
if (expression.kind == "ConditionalExpression") return [expression.condition, expression.consequent, expression.alternate]
|
|
725
726
|
if (expression.kind == "UnaryExpression") return [expression.operand]
|
|
726
727
|
if (expression.kind == "OptionalSome") return [expression.value]
|
|
727
728
|
if (expression.kind == "OptionalIsPresent" || expression.kind == "OptionalUnwrap") return [expression.operand]
|
|
@@ -592,6 +592,10 @@ export function semanticEntries(module) {
|
|
|
592
592
|
} else if (node.kind == "BinaryExpression") {
|
|
593
593
|
visit(node.left, `${path}/left`, location)
|
|
594
594
|
visit(node.right, `${path}/right`, location)
|
|
595
|
+
} else if (node.kind == "ConditionalExpression") {
|
|
596
|
+
visit(node.condition, `${path}/condition`, location)
|
|
597
|
+
visit(node.consequent, `${path}/consequent`, location)
|
|
598
|
+
visit(node.alternate, `${path}/alternate`, location)
|
|
595
599
|
} else if (node.kind == "CallExpression" || node.kind == "EffectCallExpression") {
|
|
596
600
|
node.arguments.forEach((child, index) => visit(child, `${path}/arguments/${index}`, location))
|
|
597
601
|
} else if (node.kind == "OwnedMoveExpression" || node.kind == "OwnedBorrowExpression") {
|
|
@@ -951,6 +955,10 @@ function resolveSymbols(module, records) {
|
|
|
951
955
|
} else if (expression.kind == "BinaryExpression") {
|
|
952
956
|
visitExpression(expression.left, scope, `${path}/left`)
|
|
953
957
|
visitExpression(expression.right, scope, `${path}/right`)
|
|
958
|
+
} else if (expression.kind == "ConditionalExpression") {
|
|
959
|
+
visitExpression(expression.condition, scope, `${path}/condition`)
|
|
960
|
+
visitExpression(expression.consequent, scope, `${path}/consequent`)
|
|
961
|
+
visitExpression(expression.alternate, scope, `${path}/alternate`)
|
|
954
962
|
} else if (expression.kind == "ListLiteral") {
|
|
955
963
|
for (const [index, element] of expression.elements.entries()) visitExpression(element, scope, `${path}/elements/${index}`)
|
|
956
964
|
} else if (expression.kind == "MapLiteral" || expression.kind == "OrderedMapLiteral") {
|