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.
Files changed (171) hide show
  1. package/README.md +6 -3
  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 +8 -0
  7. package/build/index.d.ts.map +1 -1
  8. package/build/index.js +9 -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 +69 -5
  103. package/build/src/publication.d.ts.map +1 -1
  104. package/build/src/publication.js +246 -57
  105. package/build/src/semantic/types.d.ts +303 -2
  106. package/build/src/semantic/types.d.ts.map +1 -1
  107. package/build/src/semantic/types.js +79 -2
  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 +11 -8
  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 +39 -23
  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 +8 -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 +275 -58
  167. package/src/semantic/types.js +87 -1
  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
@@ -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 roots kept disjoint from publication.
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, "Source roots must be a non-empty array of canonical absolute paths.")
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
- const journal = {
205
- generationId: validated.generationId,
206
- projectId: this.#projectId,
207
- schema: "SemantifoldPublicationJournal",
208
- tempPointer: tempPointerName,
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
- await mkdir(generationPath, {mode: 0o700})
216
- generationCreated = true
217
- await syncDirectory(path.dirname(generationPath))
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
- /** @type {import("./semantic/types.js").GenerationTargetManifest[]} */
220
- const targetManifests = []
302
+ manifestBytes = serializeJson(manifestCandidate)
303
+ manifest = validateGenerationManifest(JSON.parse(manifestBytes.toString("utf8")), this.#projectId,
304
+ validated.generationId)
221
305
 
222
- for (const target of validated.targets) {
223
- targetManifests.push(await stageTarget(generationPath, target, this.#projectId))
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 {{generationId: string, targets: readonly ValidatedPublicationTarget[]}} Validated request.
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 bytes = artifact.contentKind == "text"
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(Object.freeze({
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
- return isPlainObject(value) && hasExactKeys(value, ["generationId", "projectId", "schema", "tempPointer", "version"]) &&
1752
- value.schema == "SemantifoldPublicationJournal" && value.version == 1 &&
1753
- typeof value.projectId == "string" && identityPattern.test(value.projectId) &&
1754
- typeof value.generationId == "string" && generationPattern.test(value.generationId) &&
1755
- value.tempPointer == `.active-generation.${value.generationId}.tmp`
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 PublicationJournal
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 */
@@ -165,6 +165,13 @@
165
165
  * @property {readonly string[]} toolchains - Required canonical toolchain IDs.
166
166
  */
167
167
 
168
+ /**
169
+ * @typedef LanguageCheckCapabilities
170
+ * @property {boolean} supported - Whether the target owns a developer check plan.
171
+ * @property {readonly AcceptanceStage[]} stages - Ordered non-executing developer-check stages.
172
+ * @property {readonly string[]} toolchains - Exact canonical tools required to construct the plan.
173
+ */
174
+
168
175
  /**
169
176
  * @typedef LanguageFeatureCapabilities
170
177
  * @property {boolean} generalFunctionsAndCalls - Task 005 arbitrary required signatures, resolved direct calls, and void functions.
@@ -189,6 +196,7 @@
189
196
  * @property {Readonly<LanguageFeatureCapabilities>} features - Semantic feature capabilities.
190
197
  * @property {Readonly<LanguageMappingCapabilities>} mapping - Mapping forms.
191
198
  * @property {Readonly<LanguageAcceptanceCapabilities>} acceptance - Stages and toolchains.
199
+ * @property {Readonly<LanguageCheckCapabilities>} check - Non-executing developer-check capability.
192
200
  */
193
201
 
194
202
  /**
@@ -425,10 +433,15 @@
425
433
 
426
434
  /**
427
435
  * @typedef PublicationRequest
428
- * @property {string} generationId - Unique explicit generation/cycle identity.
436
+ * @property {string} generationId - Explicit generation/cycle identity, fresh or an exact retained validator-free candidate.
429
437
  * @property {readonly PublicationTargetInput[]} targets - Complete ordered project targets.
430
438
  */
431
439
 
440
+ /**
441
+ * @typedef PublicationOptions
442
+ * @property {AbortSignal} [signal] - Cancellation authority through the active-pointer commit boundary.
443
+ */
444
+
432
445
  /**
433
446
  * @typedef GenerationContentHash
434
447
  * @property {"sha256"} algorithm - Hash algorithm.
@@ -504,6 +517,79 @@
504
517
  * @property {readonly string[]} versionArguments - Exact version argument array.
505
518
  */
506
519
 
520
+ /**
521
+ * @typedef TargetCheckPathArgument
522
+ * @property {number} index - Exact argv index containing the path.
523
+ * @property {"source" | "build"} ownership - Candidate subtree that owns the path.
524
+ * @property {string} [prefix] - Exact non-path argv prefix preceding an owned absolute path.
525
+ */
526
+
527
+ /**
528
+ * @typedef TargetCheckEnvironmentPath
529
+ * @property {string} name - Exact environment variable containing an owned absolute path.
530
+ * @property {"source" | "build" | "tool"} ownership - Candidate subtree or discovered tool that owns the path.
531
+ * @property {DiscoveredToolchain} [tool] - Exact tool executable bound by a tool-owned environment value.
532
+ */
533
+
534
+ /**
535
+ * @typedef TargetCheckOutputOwnership
536
+ * @property {"build"} ownership - Compiler outputs belong only to the candidate build subtree.
537
+ * @property {string} path - Exact generation-scoped build root.
538
+ * @property {string} mediaType - Media type assigned to every produced regular file.
539
+ * @property {string} role - Language-neutral compiler-output role.
540
+ */
541
+
542
+ /**
543
+ * @typedef TargetCheckStageRequest
544
+ * @property {AcceptanceStage} stage - Declared non-executing acceptance stage.
545
+ * @property {DiscoveredToolchain} tool - Exact immutable discovered tool record.
546
+ * @property {string} executable - Exact executable copied from the tool record.
547
+ * @property {readonly string[]} argv - Exact shell-free argument vector.
548
+ * @property {readonly TargetCheckPathArgument[]} pathArguments - Complete ownership declaration for argv paths.
549
+ * @property {string} cwd - Exact candidate-scoped working directory.
550
+ * @property {Readonly<Record<string, string>>} environment - Exact deterministic environment.
551
+ * @property {readonly TargetCheckEnvironmentPath[]} environmentPaths - Owned absolute environment paths.
552
+ * @property {string | null} inputHash - SHA-256 of declared restore inputs, or null for unconditional stages.
553
+ * @property {readonly string[]} inputs - Complete staged artifact inputs read by the stage.
554
+ * @property {Readonly<TargetCheckOutputOwnership> | null} output - Build output ownership, or null for non-producing checks.
555
+ * @property {readonly string[]} transientPaths - Candidate source/build cache or intermediate directories removed after checking.
556
+ */
557
+
558
+ /**
559
+ * @typedef TargetCheckPlan
560
+ * @property {"SemantifoldTargetCheckPlan"} schema - Plan schema discriminator.
561
+ * @property {1} version - Plan schema version.
562
+ * @property {"developer-check"} mode - Non-executing developer-check mode.
563
+ * @property {string} projectId - Exact project identity.
564
+ * @property {string} targetId - Exact project target identity.
565
+ * @property {string} target - Registered backend target identity.
566
+ * @property {string} sourcePath - Exact staged generated-source root.
567
+ * @property {string} buildPath - Exact staged compiler-output root.
568
+ * @property {readonly string[]} artifactPaths - Exact staged artifact paths.
569
+ * @property {readonly TargetCheckStageRequest[]} stages - Ordered target-owned requests.
570
+ */
571
+
572
+ /**
573
+ * @typedef TargetCheckStageResult
574
+ * @property {AcceptanceStage} stage - Completed stage identity.
575
+ * @property {Readonly<{command: string, executable: string, id: string, source: "override" | "canonical", version: string}>} tool - Exact tool identity.
576
+ * @property {readonly string[]} argv - Exact executed argument vector.
577
+ * @property {number} exitCode - Successful process exit code.
578
+ * @property {null} signal - Successful process signal state.
579
+ * @property {string} stdout - Bounded captured standard output.
580
+ * @property {string} stderr - Bounded captured standard error.
581
+ * @property {number} durationMs - Non-negative stage duration.
582
+ */
583
+
584
+ /**
585
+ * @typedef TargetCheckResult
586
+ * @property {string} projectId - Exact project identity.
587
+ * @property {string} targetId - Exact project target identity.
588
+ * @property {string} target - Registered backend target identity.
589
+ * @property {readonly TargetCheckStageResult[]} stages - Ordered successful stage evidence.
590
+ * @property {readonly PublicationBuildArtifactInput[]} outputs - Complete compiler-owned build files.
591
+ */
592
+
507
593
  /**
508
594
  * @typedef AcceptanceStageResult
509
595
  * @property {AcceptanceStage} stage - Completed stage.