semantifold 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +11 -5
  2. package/bin/semantifold.js +6 -0
  3. package/build/bin/semantifold.d.ts +3 -0
  4. package/build/bin/semantifold.d.ts.map +1 -0
  5. package/build/bin/semantifold.js +5 -0
  6. package/build/index.d.ts +9 -0
  7. package/build/index.d.ts.map +1 -1
  8. package/build/index.js +10 -1
  9. package/build/src/artifact-path.d.ts +16 -0
  10. package/build/src/artifact-path.d.ts.map +1 -1
  11. package/build/src/artifact-path.js +29 -3
  12. package/build/src/backends/c-check.d.ts +7 -0
  13. package/build/src/backends/c-check.d.ts.map +1 -0
  14. package/build/src/backends/c-check.js +65 -0
  15. package/build/src/backends/check-plan.d.ts +96 -0
  16. package/build/src/backends/check-plan.d.ts.map +1 -0
  17. package/build/src/backends/check-plan.js +135 -0
  18. package/build/src/backends/cpp-check.d.ts +7 -0
  19. package/build/src/backends/cpp-check.d.ts.map +1 -0
  20. package/build/src/backends/cpp-check.js +59 -0
  21. package/build/src/backends/csharp-check.d.ts +7 -0
  22. package/build/src/backends/csharp-check.d.ts.map +1 -0
  23. package/build/src/backends/csharp-check.js +125 -0
  24. package/build/src/backends/csharp.d.ts +1 -0
  25. package/build/src/backends/csharp.d.ts.map +1 -1
  26. package/build/src/backends/csharp.js +5 -4
  27. package/build/src/backends/dart-check.d.ts +7 -0
  28. package/build/src/backends/dart-check.d.ts.map +1 -0
  29. package/build/src/backends/dart-check.js +103 -0
  30. package/build/src/backends/dart.d.ts +2 -0
  31. package/build/src/backends/dart.d.ts.map +1 -1
  32. package/build/src/backends/dart.js +5 -5
  33. package/build/src/backends/go-check.d.ts +7 -0
  34. package/build/src/backends/go-check.d.ts.map +1 -0
  35. package/build/src/backends/go-check.js +77 -0
  36. package/build/src/backends/go.d.ts +1 -0
  37. package/build/src/backends/go.d.ts.map +1 -1
  38. package/build/src/backends/go.js +3 -3
  39. package/build/src/backends/java-check.d.ts +21 -0
  40. package/build/src/backends/java-check.d.ts.map +1 -0
  41. package/build/src/backends/java-check.js +73 -0
  42. package/build/src/backends/javascript-check.d.ts +7 -0
  43. package/build/src/backends/javascript-check.d.ts.map +1 -0
  44. package/build/src/backends/javascript-check.js +29 -0
  45. package/build/src/backends/kotlin-check.d.ts +7 -0
  46. package/build/src/backends/kotlin-check.d.ts.map +1 -0
  47. package/build/src/backends/kotlin-check.js +31 -0
  48. package/build/src/backends/php-check.d.ts +7 -0
  49. package/build/src/backends/php-check.d.ts.map +1 -0
  50. package/build/src/backends/php-check.js +21 -0
  51. package/build/src/backends/program.d.ts +7 -0
  52. package/build/src/backends/program.d.ts.map +1 -1
  53. package/build/src/backends/program.js +16 -1
  54. package/build/src/backends/python-check.d.ts +7 -0
  55. package/build/src/backends/python-check.d.ts.map +1 -0
  56. package/build/src/backends/python-check.js +28 -0
  57. package/build/src/backends/ruby-check.d.ts +7 -0
  58. package/build/src/backends/ruby-check.d.ts.map +1 -0
  59. package/build/src/backends/ruby-check.js +21 -0
  60. package/build/src/backends/rust-check.d.ts +7 -0
  61. package/build/src/backends/rust-check.d.ts.map +1 -0
  62. package/build/src/backends/rust-check.js +65 -0
  63. package/build/src/backends/swift-check.d.ts +7 -0
  64. package/build/src/backends/swift-check.d.ts.map +1 -0
  65. package/build/src/backends/swift-check.js +77 -0
  66. package/build/src/backends/typescript-check.d.ts +7 -0
  67. package/build/src/backends/typescript-check.d.ts.map +1 -0
  68. package/build/src/backends/typescript-check.js +33 -0
  69. package/build/src/backends/zig-check.d.ts +7 -0
  70. package/build/src/backends/zig-check.d.ts.map +1 -0
  71. package/build/src/backends/zig-check.js +70 -0
  72. package/build/src/diagnostic.d.ts +13 -1
  73. package/build/src/diagnostic.d.ts.map +1 -1
  74. package/build/src/diagnostic.js +14 -2
  75. package/build/src/frontends/program.d.ts +6 -0
  76. package/build/src/frontends/program.d.ts.map +1 -1
  77. package/build/src/frontends/program.js +9 -1
  78. package/build/src/language-registry.d.ts +54 -0
  79. package/build/src/language-registry.d.ts.map +1 -1
  80. package/build/src/language-registry.js +107 -5
  81. package/build/src/project-build.d.ts +172 -0
  82. package/build/src/project-build.d.ts.map +1 -0
  83. package/build/src/project-build.js +320 -0
  84. package/build/src/project-cli.d.ts +40 -0
  85. package/build/src/project-cli.d.ts.map +1 -0
  86. package/build/src/project-cli.js +145 -0
  87. package/build/src/project-manifest.d.ts +148 -0
  88. package/build/src/project-manifest.d.ts.map +1 -0
  89. package/build/src/project-manifest.js +432 -0
  90. package/build/src/project-reporter.d.ts +81 -0
  91. package/build/src/project-reporter.d.ts.map +1 -0
  92. package/build/src/project-reporter.js +293 -0
  93. package/build/src/project-snapshot.d.ts +129 -0
  94. package/build/src/project-snapshot.d.ts.map +1 -0
  95. package/build/src/project-snapshot.js +362 -0
  96. package/build/src/project-watch-reporter.d.ts +125 -0
  97. package/build/src/project-watch-reporter.d.ts.map +1 -0
  98. package/build/src/project-watch-reporter.js +280 -0
  99. package/build/src/project-watch.d.ts +133 -0
  100. package/build/src/project-watch.d.ts.map +1 -0
  101. package/build/src/project-watch.js +839 -0
  102. package/build/src/publication.d.ts +218 -0
  103. package/build/src/publication.d.ts.map +1 -0
  104. package/build/src/publication.js +1832 -0
  105. package/build/src/semantic/types.d.ts +613 -0
  106. package/build/src/semantic/types.d.ts.map +1 -1
  107. package/build/src/semantic/types.js +163 -1
  108. package/build/src/subprocess.d.ts +8 -0
  109. package/build/src/subprocess.d.ts.map +1 -1
  110. package/build/src/subprocess.js +50 -9
  111. package/build/src/target-check.d.ts +52 -0
  112. package/build/src/target-check.d.ts.map +1 -0
  113. package/build/src/target-check.js +796 -0
  114. package/build/src/toolchains.d.ts.map +1 -1
  115. package/build/src/toolchains.js +3 -3
  116. package/docs/architecture.md +3 -1
  117. package/docs/c.md +4 -0
  118. package/docs/cpp.md +4 -0
  119. package/docs/dart.md +4 -0
  120. package/docs/generated-artifact-publication.md +98 -0
  121. package/docs/kotlin.md +2 -2
  122. package/docs/language-support.md +29 -3
  123. package/docs/project-build-cli.md +147 -0
  124. package/docs/project-watch.md +60 -0
  125. package/docs/rust.md +4 -0
  126. package/docs/swift.md +2 -0
  127. package/docs/testing.md +70 -3
  128. package/docs/watch-build-pipeline.md +42 -26
  129. package/docs/zig.md +2 -0
  130. package/examples/jsdoc-java-watch/expected-output.txt +3 -0
  131. package/examples/jsdoc-java-watch/semantifold.json +23 -0
  132. package/examples/jsdoc-java-watch/src/main.js +21 -0
  133. package/index.js +9 -0
  134. package/package.json +6 -1
  135. package/src/artifact-path.js +31 -2
  136. package/src/backends/c-check.js +80 -0
  137. package/src/backends/check-plan.js +147 -0
  138. package/src/backends/cpp-check.js +73 -0
  139. package/src/backends/csharp-check.js +139 -0
  140. package/src/backends/csharp.js +4 -3
  141. package/src/backends/dart-check.js +117 -0
  142. package/src/backends/dart.js +4 -4
  143. package/src/backends/go-check.js +89 -0
  144. package/src/backends/go.js +2 -2
  145. package/src/backends/java-check.js +77 -0
  146. package/src/backends/javascript-check.js +33 -0
  147. package/src/backends/kotlin-check.js +41 -0
  148. package/src/backends/php-check.js +23 -0
  149. package/src/backends/program.js +17 -0
  150. package/src/backends/python-check.js +30 -0
  151. package/src/backends/ruby-check.js +23 -0
  152. package/src/backends/rust-check.js +78 -0
  153. package/src/backends/swift-check.js +89 -0
  154. package/src/backends/typescript-check.js +37 -0
  155. package/src/backends/zig-check.js +81 -0
  156. package/src/diagnostic.js +9 -1
  157. package/src/frontends/program.js +9 -0
  158. package/src/language-registry.js +113 -4
  159. package/src/project-build.js +353 -0
  160. package/src/project-cli.js +155 -0
  161. package/src/project-manifest.js +479 -0
  162. package/src/project-reporter.js +320 -0
  163. package/src/project-snapshot.js +403 -0
  164. package/src/project-watch-reporter.js +303 -0
  165. package/src/project-watch.js +870 -0
  166. package/src/publication.js +2071 -0
  167. package/src/semantic/types.js +184 -0
  168. package/src/subprocess.js +49 -8
  169. package/src/target-check.js +844 -0
  170. package/src/toolchains.js +3 -2
  171. package/tsconfig.json +1 -1
@@ -0,0 +1,2071 @@
1
+ // @ts-check
2
+
3
+ import {createHash, randomUUID} from "node:crypto"
4
+ import {constants} from "node:fs"
5
+ import {lstat, mkdir, open, readFile, readdir, realpath, rename, rm} from "node:fs/promises"
6
+ import path from "node:path"
7
+ import {findPortableArtifactPathConflict, isSafeArtifactPath} from "./artifact-path.js"
8
+ import {createGeneratedArtifactSet} from "./artifacts.js"
9
+ import {finalizeByteMapping} from "./binary-mapping.js"
10
+ import {SemantifoldDiagnostic} from "./diagnostic.js"
11
+ import {finalizeMapping, toSourceMapV3} from "./mapping.js"
12
+
13
+ const activePointerName = "active-generation.json"
14
+ const generationsName = "generations"
15
+ const journalName = ".semantifold-publication-journal"
16
+ const identityPattern = /^[a-z][a-z0-9-]*$/u
17
+ const generationPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u
18
+ const buildRolePattern = /^[a-z][a-z0-9-]*$/u
19
+ const mediaTypePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+(?:;[\u0020-\u007e]+)?$/u
20
+ const targetRoles = new Set(["application", "binary", "text"])
21
+ const publicationBoundaries = new Set([
22
+ "after-stage",
23
+ "before-pointer-replace",
24
+ "after-pointer-replace",
25
+ "before-cleanup",
26
+ "before-journal-remove"
27
+ ])
28
+ /** @type {WeakMap<GeneratedArtifactPublisher, {boundary: string, error: Error}>} */
29
+ const injectedPublicationFailures = new WeakMap()
30
+ /** @type {WeakMap<GeneratedArtifactPublisher, (operation: string) => void>} */
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
+ }
52
+
53
+ /**
54
+ * Installs one internal, one-shot deterministic failure used by real-filesystem interruption specs.
55
+ * This helper is intentionally not part of the package root API.
56
+ * @param {GeneratedArtifactPublisher} publisher - Publisher instance.
57
+ * @param {"after-stage" | "before-pointer-replace" | "after-pointer-replace" | "before-cleanup" | "before-journal-remove"} boundary - Exact lifecycle boundary.
58
+ * @param {Error} error - Injected failure.
59
+ * @returns {void}
60
+ */
61
+ export function injectPublicationFailure(publisher, boundary, error) {
62
+ if (!(publisher instanceof GeneratedArtifactPublisher) || !publicationBoundaries.has(boundary) || !(error instanceof Error)) {
63
+ throw new TypeError("Invalid publication failure injection.")
64
+ }
65
+ injectedPublicationFailures.set(publisher, {boundary, error})
66
+ }
67
+
68
+ /**
69
+ * Installs an internal deterministic filesystem-operation observer for durability specs.
70
+ * This helper is intentionally not part of the package root API.
71
+ * @param {GeneratedArtifactPublisher} publisher - Publisher instance.
72
+ * @param {(operation: string) => void} observer - Synchronous operation observer.
73
+ * @returns {void}
74
+ */
75
+ export function observePublicationFilesystem(publisher, observer) {
76
+ if (!(publisher instanceof GeneratedArtifactPublisher) || typeof observer != "function") {
77
+ throw new TypeError("Invalid publication filesystem observer.")
78
+ }
79
+ publicationFilesystemObservers.set(publisher, observer)
80
+ }
81
+
82
+ /**
83
+ * Owns transactional publication in one dedicated directory beneath a canonical project root.
84
+ */
85
+ export class GeneratedArtifactPublisher {
86
+ /** @type {string} */
87
+ #projectId
88
+ /** @type {string} */
89
+ #projectRoot
90
+ /** @type {string} */
91
+ #publicationRoot
92
+ /** @type {readonly string[]} */
93
+ #sourceRoots
94
+ /** @type {readonly string[]} */
95
+ #sourceFiles
96
+ /** @type {Promise<void>} */
97
+ #publicationTail = Promise.resolve()
98
+
99
+ /**
100
+ * Creates a project-scoped publisher without touching the filesystem.
101
+ * @param {object} options - Publisher identity and owned paths.
102
+ * @param {string} options.projectId - Stable lowercase project identity.
103
+ * @param {string} options.projectRoot - Canonical absolute project root.
104
+ * @param {string} options.publicationRoot - Canonical absolute publisher-owned directory beneath the project root.
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.
107
+ */
108
+ constructor(options) {
109
+ if (!isPlainObject(options)) {
110
+ publicationFailure("INVALID_PUBLICATION_REQUEST", "publication", "Publisher options must be a plain object.")
111
+ }
112
+ const {projectId, projectRoot, publicationRoot, sourceFiles = [], sourceRoots = []} = options
113
+
114
+ if (!identityPattern.test(projectId ?? "")) {
115
+ publicationFailure("INVALID_PUBLICATION_REQUEST", "publication", "Project identity must be a stable lowercase ID.")
116
+ }
117
+ if (!isCanonicalAbsolutePath(projectRoot) || !isCanonicalAbsolutePath(publicationRoot) ||
118
+ !isStrictPathDescendant(projectRoot, publicationRoot)) {
119
+ publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
120
+ "Project and publication roots must be canonical absolute paths with publication strictly beneath the project root.")
121
+ }
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.")
125
+ }
126
+ /** @type {string[]} */
127
+ const validatedSourceRoots = []
128
+
129
+ for (let index = 0; index < sourceRoots.length; index += 1) {
130
+ const sourceRoot = sourceRoots[index]
131
+
132
+ if (!isCanonicalAbsolutePath(sourceRoot)) {
133
+ publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
134
+ "Source roots must be a non-empty array of canonical absolute paths.")
135
+ }
136
+ if (!isPathWithin(projectRoot, sourceRoot)) {
137
+ publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
138
+ "Source roots must remain within the canonical project root.")
139
+ }
140
+ if (pathsOverlap(publicationRoot, sourceRoot)) {
141
+ publicationFailure("PUBLICATION_SOURCE_OVERLAP", projectId,
142
+ "Publication root and source roots must be disjoint.")
143
+ }
144
+ validatedSourceRoots.push(sourceRoot)
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
+ }
162
+
163
+ this.#projectId = projectId
164
+ this.#projectRoot = projectRoot
165
+ this.#publicationRoot = publicationRoot
166
+ this.#sourceRoots = Object.freeze(validatedSourceRoots)
167
+ this.#sourceFiles = Object.freeze(validatedSourceFiles)
168
+ }
169
+
170
+ /**
171
+ * Stages, validates, and publishes one complete immutable project generation.
172
+ * Calls on one publisher instance are serialized in invocation order.
173
+ * @param {import("./semantic/types.js").PublicationRequest} request - Complete publication request.
174
+ * @param {import("./semantic/types.js").PublicationOptions} [options] - Transaction lifecycle controls.
175
+ * @returns {Promise<import("./semantic/types.js").PublishedGeneration>} Committed immutable snapshot.
176
+ */
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)
183
+ /** @type {(value: import("./semantic/types.js").PublishedGeneration) => void} */
184
+ let resolveResult
185
+ /** @type {(reason?: unknown) => void} */
186
+ let rejectResult
187
+ const result = new Promise((resolve, reject) => {
188
+ resolveResult = resolve
189
+ rejectResult = reject
190
+ })
191
+ const operation = this.#publicationTail.then(async () => {
192
+ try {
193
+ resolveResult(await this.#publish(request, signal, commitGuard))
194
+ } catch (error) {
195
+ rejectResult(error)
196
+ }
197
+ })
198
+
199
+ this.#publicationTail = operation.catch(() => {})
200
+
201
+ return result
202
+ }
203
+
204
+ /**
205
+ * Resolves the active pointer exactly once and verifies the referenced immutable generation.
206
+ * @returns {Promise<import("./semantic/types.js").PublishedGeneration>} Verified pointer-once snapshot.
207
+ */
208
+ async resolveActive() {
209
+ await this.#prepareRoot()
210
+ const pointerPath = path.join(this.#publicationRoot, activePointerName)
211
+ const pointer = await readActivePointer(pointerPath, this.#projectId, false)
212
+
213
+ if (!pointer) publicationFailure("ACTIVE_GENERATION_MISSING", this.#projectId, "No active generation has been published.")
214
+ const generation = await verifyPublishedGeneration(this.#publicationRoot, this.#projectId, pointer, false)
215
+ const cleanupPending = await hasCommittedCleanupJournal(this.#publicationRoot, this.#projectId, pointer.generationId)
216
+
217
+ return publishedGeneration(this.#publicationRoot, generation.manifest, cleanupPending)
218
+ }
219
+
220
+ /**
221
+ * Performs one serialized publication.
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.
225
+ * @returns {Promise<import("./semantic/types.js").PublishedGeneration>} Publication result.
226
+ */
227
+ async #publish(request, signal, commitGuard) {
228
+ const validated = validatePublicationRequest(request, this.#projectId)
229
+
230
+ assertPublicationActive(signal, this.#projectId)
231
+ await this.#prepareRoot()
232
+ await reconcilePublication(this.#publicationRoot, this.#projectId, this)
233
+ await verifyAtomicReplacement(this.#publicationRoot, this.#projectId)
234
+ await validateExistingPointer(this.#publicationRoot, this.#projectId)
235
+
236
+ const generationPath = path.join(this.#publicationRoot, generationsName, validated.generationId)
237
+ const manifestPath = path.join(generationPath, "manifest.json")
238
+ const tempPointerName = `.active-generation.${validated.generationId}.tmp`
239
+ const tempPointerPath = path.join(this.#publicationRoot, tempPointerName)
240
+ const journalPath = path.join(this.#publicationRoot, journalName, `${validated.generationId}.json`)
241
+ const retainedGeneration = await optionalStatus(generationPath, this.#projectId, "retained generation") !== null
242
+ let committed = false
243
+ let journalCreated = false
244
+ let generationCreated = false
245
+ let tempPointerCreated = false
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
251
+
252
+ try {
253
+ await assertAbsent(tempPointerPath, "PUBLICATION_UNOWNED_CONFLICT", this.#projectId,
254
+ "Temporary active pointer path is already owned or ambiguous.")
255
+ await assertAbsent(journalPath, "PUBLICATION_UNOWNED_CONFLICT", this.#projectId,
256
+ "Publication journal identity is already owned or ambiguous.")
257
+ if (retainedGeneration) {
258
+ const retained = await verifyRetainedGeneration(generationPath, validated, this.#projectId)
259
+
260
+ manifest = retained.manifest
261
+ manifestBytes = retained.manifestBytes
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
+ }
279
+
280
+ await writeSyncedFile(journalPath, serializeJson(journal), "wx")
281
+ journalCreated = true
282
+ await syncDirectory(path.dirname(journalPath))
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
+ })
301
+
302
+ manifestBytes = serializeJson(manifestCandidate)
303
+ manifest = validateGenerationManifest(JSON.parse(manifestBytes.toString("utf8")), this.#projectId,
304
+ validated.generationId)
305
+
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.")
313
+ }
314
+ reachPublicationBoundary(this, "after-stage")
315
+ failureCode = "PUBLICATION_POINTER_FAILED"
316
+
317
+ const pointer = {
318
+ generationId: validated.generationId,
319
+ manifestHash: sha256(manifestBytes),
320
+ projectId: this.#projectId,
321
+ schema: "SemantifoldActiveGeneration",
322
+ version: 1
323
+ }
324
+
325
+ await writeSyncedFile(tempPointerPath, serializeJson(pointer), "wx", () => {
326
+ tempPointerCreated = true
327
+ })
328
+ reachPublicationBoundary(this, "before-pointer-replace")
329
+ await commitGuard?.()
330
+ assertPublicationActive(signal, this.#projectId)
331
+ await rename(tempPointerPath, path.join(this.#publicationRoot, activePointerName))
332
+ tempPointerCreated = false
333
+ committed = true
334
+ reachPublicationBoundary(this, "after-pointer-replace")
335
+ await syncDirectory(this.#publicationRoot)
336
+
337
+ let cleanupPending = false
338
+
339
+ try {
340
+ reachPublicationBoundary(this, "before-cleanup")
341
+ await removeJournalOwnedState({
342
+ journalPath,
343
+ projectId: this.#projectId,
344
+ publicationRoot: this.#publicationRoot,
345
+ publisher: this
346
+ })
347
+ } catch (_error) {
348
+ cleanupPending = true
349
+ }
350
+
351
+ return publishedGeneration(this.#publicationRoot, deepFreeze(manifest), cleanupPending)
352
+ } catch (error) {
353
+ if (!committed) {
354
+ try {
355
+ await removeJournalOwnedState({
356
+ ...(generationCreated ? {candidatePath: generationPath} : {}),
357
+ ...(journalCreated ? {journalPath} : {}),
358
+ projectId: this.#projectId,
359
+ publicationRoot: this.#publicationRoot,
360
+ publisher: this,
361
+ ...(tempPointerCreated ? {temporaryPointerPath: tempPointerPath} : {})
362
+ })
363
+ } catch (cleanupError) {
364
+ if (cleanupError instanceof SemantifoldDiagnostic) throw cleanupError
365
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", this.#projectId,
366
+ "Failed publication left state that requires recovery.", cleanupError)
367
+ }
368
+ }
369
+ if (error instanceof SemantifoldDiagnostic) throw error
370
+
371
+ publicationFailure(failureCode, this.#projectId,
372
+ committed
373
+ ? "Active generation changed, but durable pointer publication did not complete."
374
+ : "Candidate generation could not be staged.", error)
375
+ }
376
+ }
377
+
378
+ /**
379
+ * Creates and validates owned root directories and source-root separation.
380
+ * @returns {Promise<void>} Completion.
381
+ */
382
+ async #prepareRoot() {
383
+ await ensureDirectoryWithoutSymlinks(this.#projectRoot, false, this.#projectId)
384
+ await ensureDirectoryWithoutSymlinks(this.#publicationRoot, true, this.#projectId)
385
+ let exactProject
386
+ let exactPublication
387
+
388
+ try {
389
+ exactProject = await realpath(this.#projectRoot)
390
+ exactPublication = await realpath(this.#publicationRoot)
391
+ } catch (error) {
392
+ publicationFailure("INVALID_PUBLICATION_ROOT", this.#projectId,
393
+ "Publication root could not be resolved safely.", error)
394
+ }
395
+ if (!isStrictPathDescendant(exactProject, exactPublication)) {
396
+ publicationFailure("INVALID_PUBLICATION_ROOT", this.#projectId,
397
+ "Publication root must resolve strictly beneath the project root.")
398
+ }
399
+ for (const sourceRoot of this.#sourceRoots) {
400
+ await ensureDirectoryWithoutSymlinks(sourceRoot, false, this.#projectId)
401
+ let exactSource
402
+
403
+ try {
404
+ exactSource = await realpath(sourceRoot)
405
+ } catch (error) {
406
+ publicationFailure("INVALID_PUBLICATION_ROOT", this.#projectId,
407
+ "Source root could not be resolved safely.", error)
408
+ }
409
+
410
+ if (!isPathWithin(exactProject, exactSource)) {
411
+ publicationFailure("INVALID_PUBLICATION_ROOT", this.#projectId,
412
+ "Source roots must resolve within the project root.")
413
+ }
414
+ if (pathsOverlap(exactPublication, exactSource)) {
415
+ publicationFailure("PUBLICATION_SOURCE_OVERLAP", this.#projectId,
416
+ "Publication root and source roots resolve to overlapping paths.")
417
+ }
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
+ }
444
+ await ensureOwnedDirectory(path.join(this.#publicationRoot, generationsName), this.#projectId)
445
+ await ensureOwnedDirectory(path.join(this.#publicationRoot, journalName), this.#projectId)
446
+ }
447
+ }
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
+
478
+ /**
479
+ * Throws and consumes a configured internal failure at an exact lifecycle boundary.
480
+ * @param {GeneratedArtifactPublisher} publisher - Active publisher.
481
+ * @param {string} boundary - Reached boundary.
482
+ * @returns {void}
483
+ */
484
+ function reachPublicationBoundary(publisher, boundary) {
485
+ const injection = injectedPublicationFailures.get(publisher)
486
+
487
+ if (injection?.boundary == boundary) {
488
+ injectedPublicationFailures.delete(publisher)
489
+ throw injection.error
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Reports one completed filesystem operation to the internal deterministic seam.
495
+ * @param {GeneratedArtifactPublisher} publisher - Active publisher.
496
+ * @param {string} operation - Completed operation.
497
+ * @returns {void}
498
+ */
499
+ function observePublicationFilesystemOperation(publisher, operation) {
500
+ publicationFilesystemObservers.get(publisher)?.(operation)
501
+ }
502
+
503
+ /**
504
+ * Reconciles only candidates and temporary pointers whose ownership is proven by a strict journal.
505
+ * The active pointer remains the sole commit authority.
506
+ * @param {string} publicationRoot - Publication root.
507
+ * @param {string} projectId - Project identity.
508
+ * @param {GeneratedArtifactPublisher} publisher - Active publisher.
509
+ * @returns {Promise<void>} Completion.
510
+ */
511
+ async function reconcilePublication(publicationRoot, projectId, publisher) {
512
+ const journals = await readPublicationJournals(publicationRoot, projectId)
513
+
514
+ if (journals.length == 0) return
515
+ const pointer = await readActivePointer(path.join(publicationRoot, activePointerName), projectId, true)
516
+
517
+ if (pointer) await verifyPublishedGeneration(publicationRoot, projectId, pointer, true)
518
+ for (const {journal, journalPath} of journals) {
519
+ const candidatePath = ownedPath(path.join(publicationRoot, generationsName), journal.generationId, projectId)
520
+ const temporaryPointerPath = path.join(publicationRoot, journal.tempPointer)
521
+ const committed = pointer?.generationId == journal.generationId
522
+ const candidateOwned = journal.version == 1
523
+
524
+ if (candidateOwned && !committed) {
525
+ const candidateStatus = await optionalStatus(candidatePath, projectId, "candidate generation")
526
+
527
+ if (candidateStatus) {
528
+ if (!candidateStatus.isDirectory() || candidateStatus.isSymbolicLink()) {
529
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", projectId,
530
+ "Journal-owned candidate is not a real directory.")
531
+ }
532
+ }
533
+ }
534
+ const temporaryStatus = await optionalStatus(temporaryPointerPath, projectId, "temporary pointer")
535
+
536
+ if (temporaryStatus) {
537
+ if (!temporaryStatus.isFile() || temporaryStatus.isSymbolicLink()) {
538
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", projectId,
539
+ "Journal-owned temporary pointer is not a regular file.")
540
+ }
541
+ }
542
+ await removeJournalOwnedState({
543
+ ...(candidateOwned && !committed ? {candidatePath} : {}),
544
+ journalPath,
545
+ projectId,
546
+ publicationRoot,
547
+ publisher,
548
+ temporaryPointerPath
549
+ })
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Reads and validates every recovery journal without changing publication state.
555
+ * @param {string} publicationRoot - Publication root.
556
+ * @param {string} projectId - Project identity.
557
+ * @returns {Promise<readonly {journal: PublicationJournal, journalPath: string}[]>} Ordered journals.
558
+ */
559
+ async function readPublicationJournals(publicationRoot, projectId) {
560
+ const journalDirectory = path.join(publicationRoot, journalName)
561
+ let entries
562
+
563
+ try {
564
+ entries = await readdir(journalDirectory)
565
+ } catch (error) {
566
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", projectId,
567
+ "Publication journal directory could not be inspected.", error)
568
+ }
569
+ entries.sort(compareStrings)
570
+ /** @type {{journal: PublicationJournal, journalPath: string}[]} */
571
+ const journals = []
572
+
573
+ for (const entry of entries) {
574
+ if (!entry.endsWith(".json") || !generationPattern.test(entry.slice(0, -5))) {
575
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", projectId,
576
+ "Publication journal contains an unrecognized entry.")
577
+ }
578
+ const journalPath = path.join(journalDirectory, entry)
579
+ let journal
580
+
581
+ try {
582
+ const status = await lstat(journalPath)
583
+
584
+ if (!status.isFile() || status.isSymbolicLink()) {
585
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", projectId,
586
+ "Publication journal entry must be a regular file.")
587
+ }
588
+ journal = JSON.parse(await readFile(journalPath, "utf8"))
589
+ } catch (error) {
590
+ if (error instanceof SemantifoldDiagnostic) throw error
591
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", projectId,
592
+ "Publication journal entry is not valid JSON.", error)
593
+ }
594
+ if (!isPublicationJournal(journal) || journal.projectId != projectId || `${journal.generationId}.json` != entry) {
595
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", projectId,
596
+ "Publication journal entry does not match the versioned project contract.")
597
+ }
598
+ journals.push({journal, journalPath})
599
+ }
600
+
601
+ return Object.freeze(journals)
602
+ }
603
+
604
+ /**
605
+ * Reports committed cleanup state while failing closed on every journal entry.
606
+ * @param {string} publicationRoot - Publication root.
607
+ * @param {string} projectId - Project identity.
608
+ * @param {string} generationId - Pointer-selected generation identity.
609
+ * @returns {Promise<boolean>} Whether committed cleanup remains.
610
+ */
611
+ async function hasCommittedCleanupJournal(publicationRoot, projectId, generationId) {
612
+ const journals = await readPublicationJournals(publicationRoot, projectId)
613
+
614
+ return journals.some(({journal}) => journal.generationId == generationId)
615
+ }
616
+
617
+ /**
618
+ * Removes proven journal-owned state and durably synchronizes each parent before discarding its ownership proof.
619
+ * @param {object} options - Exact cleanup state.
620
+ * @param {string} [options.candidatePath] - Proven candidate generation path.
621
+ * @param {string} [options.temporaryPointerPath] - Proven temporary pointer path.
622
+ * @param {string} [options.journalPath] - Ownership journal path.
623
+ * @param {string} options.projectId - Project identity.
624
+ * @param {string} options.publicationRoot - Publication root.
625
+ * @param {GeneratedArtifactPublisher} options.publisher - Active publisher.
626
+ * @returns {Promise<void>} Completion.
627
+ */
628
+ async function removeJournalOwnedState({candidatePath, journalPath, projectId, publicationRoot, publisher, temporaryPointerPath}) {
629
+ try {
630
+ if (candidatePath) {
631
+ await rm(candidatePath, {force: true, recursive: true})
632
+ observePublicationFilesystemOperation(publisher, "remove-candidate")
633
+ await syncDirectory(path.join(publicationRoot, generationsName))
634
+ observePublicationFilesystemOperation(publisher, "sync-candidate-parent")
635
+ }
636
+ if (temporaryPointerPath) {
637
+ await rm(temporaryPointerPath, {force: true})
638
+ observePublicationFilesystemOperation(publisher, "remove-temporary-pointer")
639
+ await syncDirectory(publicationRoot)
640
+ observePublicationFilesystemOperation(publisher, "sync-temporary-pointer-parent")
641
+ }
642
+ if (journalPath) {
643
+ reachPublicationBoundary(publisher, "before-journal-remove")
644
+ await rm(journalPath)
645
+ observePublicationFilesystemOperation(publisher, "remove-journal")
646
+ await syncDirectory(path.dirname(journalPath))
647
+ observePublicationFilesystemOperation(publisher, "sync-journal-parent")
648
+ }
649
+ } catch (error) {
650
+ if (error instanceof SemantifoldDiagnostic) throw error
651
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", projectId,
652
+ "Journal-owned publication state could not be durably reconciled.", error)
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Reads optional metadata without following symbolic links.
658
+ * @param {string} filename - Exact owned path.
659
+ * @param {string} projectId - Project identity.
660
+ * @param {string} subject - Stable subject description.
661
+ * @returns {Promise<import("node:fs").Stats | null>} Status or absence.
662
+ */
663
+ async function optionalStatus(filename, projectId, subject) {
664
+ try {
665
+ return await lstat(filename)
666
+ } catch (error) {
667
+ if (isErrorCode(error, "ENOENT")) return null
668
+ publicationFailure("PUBLICATION_RECOVERY_FAILED", projectId,
669
+ `Journal-owned ${subject} could not be inspected.`, error)
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Validates and snapshots one publication request before staging.
675
+ * @param {unknown} request - Request candidate.
676
+ * @param {string} projectId - Project diagnostic identity.
677
+ * @returns {ValidatedPublicationRequest} Validated request.
678
+ */
679
+ function validatePublicationRequest(request, projectId) {
680
+ if (!isPlainObject(request)) {
681
+ publicationFailure("INVALID_PUBLICATION_REQUEST", projectId,
682
+ "Publication requires a unique generation ID and a non-empty ordered target array.")
683
+ }
684
+ const generationId = request.generationId
685
+ const requestedTargets = request.targets
686
+
687
+ if (typeof generationId != "string" || !generationPattern.test(generationId) ||
688
+ !isDenseArray(requestedTargets) || requestedTargets.length == 0) {
689
+ publicationFailure("INVALID_PUBLICATION_REQUEST", projectId,
690
+ "Publication requires a unique generation ID and a non-empty ordered target array.")
691
+ }
692
+ /** @type {ValidatedPublicationTarget[]} */
693
+ const targets = []
694
+ const targetIds = new Set()
695
+ /** @type {string[]} */
696
+ const projections = []
697
+ /** @type {string[]} */
698
+ const completeArtifactPaths = []
699
+
700
+ for (let index = 0; index < requestedTargets.length; index += 1) {
701
+ const target = requestedTargets[index]
702
+
703
+ if (!isPlainObject(target)) {
704
+ publicationFailure("INVALID_PUBLICATION_REQUEST", projectId,
705
+ `Target ${index} requires a stable lowercase identity.`)
706
+ }
707
+ const targetId = target.id
708
+ const role = target.role
709
+ const sourceProjection = target.sourceProjection
710
+ const buildProjection = target.buildProjection
711
+ const artifactSetCandidate = target.artifactSet
712
+ const validatorsCandidate = target.validators
713
+
714
+ if (typeof targetId != "string" || !identityPattern.test(targetId)) {
715
+ publicationFailure("INVALID_PUBLICATION_REQUEST", projectId,
716
+ `Target ${index} requires a stable lowercase identity.`)
717
+ }
718
+ if (typeof role != "string" || !targetRoles.has(role)) {
719
+ publicationFailure("INVALID_PUBLICATION_REQUEST", projectId,
720
+ `Target '${targetId}' requires an explicit language-neutral role.`)
721
+ }
722
+
723
+ if (targetIds.has(targetId)) {
724
+ publicationFailure("PUBLICATION_PATH_COLLISION", projectId, `Duplicate target identity '${targetId}'.`)
725
+ }
726
+ targetIds.add(targetId)
727
+ if (!isSafeArtifactPath(sourceProjection) || !isSafeArtifactPath(buildProjection)) {
728
+ publicationFailure("INVALID_PUBLICATION_PROJECTION", projectId,
729
+ `Target '${targetId}' requires canonical relative source and build projections.`)
730
+ }
731
+ projections.push(sourceProjection, buildProjection)
732
+ let artifactSet
733
+
734
+ try {
735
+ artifactSet = createGeneratedArtifactSet(artifactSetCandidate)
736
+ } catch (error) {
737
+ publicationFailure("INVALID_PUBLICATION_REQUEST", projectId,
738
+ `Target '${targetId}' does not contain a valid generated artifact set.`, error)
739
+ }
740
+ const validators = validatorsCandidate === undefined ? [] : validatorsCandidate
741
+
742
+ if (!isDenseArray(validators)) {
743
+ publicationFailure("INVALID_PUBLICATION_REQUEST", projectId,
744
+ `Target '${targetId}' validators must be a dense function array.`)
745
+ }
746
+ /** @type {import("./semantic/types.js").PublicationValidator[]} */
747
+ const validatedValidators = []
748
+
749
+ for (let validatorIndex = 0; validatorIndex < validators.length; validatorIndex += 1) {
750
+ const validator = validators[validatorIndex]
751
+
752
+ if (typeof validator != "function") {
753
+ publicationFailure("INVALID_PUBLICATION_REQUEST", projectId,
754
+ `Target '${targetId}' validators must be a dense function array.`)
755
+ }
756
+ validatedValidators.push(/** @type {import("./semantic/types.js").PublicationValidator} */ (validator))
757
+ }
758
+ const artifactConflict = findPortableArtifactPathConflict(artifactSet.artifacts.map(({path: artifactPath}) => artifactPath))
759
+
760
+ if (artifactConflict) {
761
+ publicationFailure("PUBLICATION_PATH_COLLISION", projectId,
762
+ `Target '${targetId}' artifact paths have a portable ${artifactConflict.kind} conflict.`)
763
+ }
764
+ for (const artifact of artifactSet.artifacts) {
765
+ completeArtifactPaths.push(`${sourceProjection}/${artifact.path}`)
766
+ }
767
+ targets.push(Object.freeze({
768
+ artifactSet,
769
+ buildProjection,
770
+ id: targetId,
771
+ role: /** @type {import("./semantic/types.js").PublicationTargetRole} */ (role),
772
+ sourceProjection,
773
+ validators: Object.freeze(validatedValidators)
774
+ }))
775
+ }
776
+ const projectionConflict = findPortableArtifactPathConflict(projections)
777
+
778
+ if (projectionConflict) {
779
+ publicationFailure("PUBLICATION_PROJECTION_OVERLAP", projectId,
780
+ "Target source/build projections must be distinct and non-nested under portable comparison.")
781
+ }
782
+ if (findPortableArtifactPathConflict([...projections, "manifest.json"])) {
783
+ publicationFailure("INVALID_PUBLICATION_PROJECTION", projectId,
784
+ "Target source/build projections must not overlap the generation manifest path.")
785
+ }
786
+ const artifactConflict = findPortableArtifactPathConflict(completeArtifactPaths)
787
+
788
+ if (artifactConflict) {
789
+ publicationFailure("PUBLICATION_PATH_COLLISION", projectId,
790
+ "Projected artifact paths collide under portable comparison.")
791
+ }
792
+
793
+ return {generationId, targets: Object.freeze(targets)}
794
+ }
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
+
846
+ /**
847
+ * Stages one validated target and invokes its ordered validators.
848
+ * @param {string} generationPath - Candidate generation path.
849
+ * @param {ValidatedPublicationTarget} target - Validated target.
850
+ * @param {string} projectId - Project diagnostic identity.
851
+ * @returns {Promise<import("./semantic/types.js").GenerationTargetManifest>} Target manifest.
852
+ */
853
+ async function stageTarget(generationPath, target, projectId) {
854
+ const sourcePath = ownedPath(generationPath, target.sourceProjection, projectId)
855
+ const buildPath = ownedPath(generationPath, target.buildProjection, projectId)
856
+
857
+ await mkdir(sourcePath, {mode: 0o700, recursive: true})
858
+ await mkdir(buildPath, {mode: 0o700, recursive: true})
859
+ /** @type {import("./semantic/types.js").GenerationArtifactManifest[]} */
860
+ const artifacts = []
861
+
862
+ for (const artifact of target.artifactSet.artifacts) {
863
+ const filename = ownedPath(sourcePath, artifact.path, projectId)
864
+ const described = describeGeneratedArtifact(artifact)
865
+
866
+ await mkdir(path.dirname(filename), {mode: 0o700, recursive: true})
867
+ try {
868
+ await writeSyncedFile(filename, described.bytes, "wx")
869
+ } catch (error) {
870
+ if (error instanceof SemantifoldDiagnostic) throw error
871
+ publicationFailure("PUBLICATION_STAGE_WRITE_FAILED", projectId,
872
+ `Generated artifact '${artifact.path}' could not be staged.`, error)
873
+ }
874
+ artifacts.push(described.manifest)
875
+ }
876
+
877
+ /** @type {import("./semantic/types.js").PublicationBuildArtifactInput[]} */
878
+ const buildDescriptors = []
879
+ const context = Object.freeze({buildPath, sourcePath, targetId: target.id})
880
+
881
+ for (let index = 0; index < target.validators.length; index += 1) {
882
+ let produced
883
+
884
+ try {
885
+ produced = await target.validators[index](context)
886
+ } catch (error) {
887
+ publicationFailure("PUBLICATION_VALIDATION_FAILED", projectId,
888
+ `Validator ${index} failed for target '${target.id}'.`, error)
889
+ }
890
+ if (produced === undefined) continue
891
+ if (!isDenseArray(produced)) {
892
+ publicationFailure("PUBLICATION_VALIDATION_FAILED", projectId,
893
+ `Validator ${index} returned invalid build-artifact metadata for target '${target.id}'.`)
894
+ }
895
+ for (const descriptor of produced) {
896
+ buildDescriptors.push(validateBuildArtifactDescriptor(descriptor, target.id, projectId))
897
+ }
898
+ }
899
+
900
+ const descriptorConflict = findPortableArtifactPathConflict(buildDescriptors.map(({path: artifactPath}) => artifactPath))
901
+
902
+ if (descriptorConflict) {
903
+ publicationFailure("PUBLICATION_PATH_COLLISION", projectId,
904
+ `Target '${target.id}' build artifacts have a portable ${descriptorConflict.kind} conflict.`)
905
+ }
906
+ const stagedBuildFiles = await listRegularFiles(buildPath, projectId)
907
+ const declared = new Set(buildDescriptors.map(({path: artifactPath}) => artifactPath))
908
+
909
+ if (stagedBuildFiles.length != declared.size || stagedBuildFiles.some(filename => !declared.has(filename))) {
910
+ publicationFailure("PUBLICATION_VALIDATION_FAILED", projectId,
911
+ `Target '${target.id}' validators must declare every staged build artifact exactly once.`)
912
+ }
913
+ /** @type {import("./semantic/types.js").GenerationBuildArtifactManifest[]} */
914
+ const finalizedBuildArtifacts = []
915
+
916
+ for (const descriptor of buildDescriptors) {
917
+ const bytes = await readAndSyncRegularFile(ownedPath(buildPath, descriptor.path, projectId), projectId,
918
+ "PUBLICATION_STAGE_HASH_MISMATCH", `Build artifact '${descriptor.path}' is not a stable regular file.`)
919
+
920
+ finalizedBuildArtifacts.push(Object.freeze({
921
+ byteLength: bytes.byteLength,
922
+ hash: Object.freeze({algorithm: /** @type {const} */ ("sha256"), value: sha256(bytes)}),
923
+ mediaType: descriptor.mediaType,
924
+ path: descriptor.path,
925
+ role: descriptor.role
926
+ }))
927
+ }
928
+
929
+ return Object.freeze({
930
+ artifacts: Object.freeze(artifacts),
931
+ buildArtifacts: Object.freeze(finalizedBuildArtifacts),
932
+ id: target.id,
933
+ ...(target.artifactSet.metadata === undefined ? {} : {metadata: target.artifactSet.metadata}),
934
+ projections: Object.freeze({build: target.buildProjection, source: target.sourceProjection}),
935
+ role: target.role,
936
+ target: target.artifactSet.target
937
+ })
938
+ }
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
+
964
+ /**
965
+ * Validates validator/compiler output metadata.
966
+ * @param {unknown} descriptor - Metadata candidate.
967
+ * @param {string} targetId - Target identity.
968
+ * @param {string} projectId - Project diagnostic identity.
969
+ * @returns {import("./semantic/types.js").PublicationBuildArtifactInput} Validated metadata.
970
+ */
971
+ function validateBuildArtifactDescriptor(descriptor, targetId, projectId) {
972
+ if (!isPlainObject(descriptor) || !hasExactKeys(descriptor, ["mediaType", "path", "role"])) {
973
+ publicationFailure("PUBLICATION_VALIDATION_FAILED", projectId,
974
+ `Target '${targetId}' validator returned invalid build-artifact metadata.`)
975
+ }
976
+ const descriptorPath = descriptor.path
977
+ const mediaType = descriptor.mediaType
978
+ const role = descriptor.role
979
+
980
+ if (!isSafeArtifactPath(descriptorPath) || typeof mediaType != "string" ||
981
+ !mediaTypePattern.test(mediaType) || /[\r\n]/u.test(mediaType) ||
982
+ typeof role != "string" || !buildRolePattern.test(role)) {
983
+ publicationFailure("PUBLICATION_VALIDATION_FAILED", projectId,
984
+ `Target '${targetId}' validator returned invalid build-artifact metadata.`)
985
+ }
986
+
987
+ return Object.freeze({mediaType, path: descriptorPath, role})
988
+ }
989
+
990
+ /**
991
+ * Verifies every manifest reference and staged byte before pointer reachability.
992
+ * @param {string} generationPath - Candidate generation path.
993
+ * @param {import("./semantic/types.js").GenerationManifest} manifest - Candidate manifest.
994
+ * @param {string} projectId - Project diagnostic identity.
995
+ * @returns {Promise<void>} Completion.
996
+ */
997
+ async function verifyManifestFiles(generationPath, manifest, projectId) {
998
+ for (const target of manifest.targets) {
999
+ const sourcePath = ownedPath(generationPath, target.projections.source, projectId)
1000
+ const buildPath = ownedPath(generationPath, target.projections.build, projectId)
1001
+ const sourceFiles = await listRegularFiles(sourcePath, projectId)
1002
+
1003
+ if (sourceFiles.length != target.artifacts.length ||
1004
+ sourceFiles.some(filename => !target.artifacts.some(artifact => artifact.path == filename))) {
1005
+ publicationFailure("PUBLICATION_STAGE_HASH_MISMATCH", projectId,
1006
+ `Target '${target.id}' generated-source files do not match its manifest.`)
1007
+ }
1008
+ for (const artifact of target.artifacts) {
1009
+ await verifyManifestFile(sourcePath, artifact, projectId)
1010
+ }
1011
+ const buildFiles = await listRegularFiles(buildPath, projectId)
1012
+
1013
+ if (buildFiles.length != target.buildArtifacts.length ||
1014
+ buildFiles.some(filename => !target.buildArtifacts.some(artifact => artifact.path == filename))) {
1015
+ publicationFailure("PUBLICATION_STAGE_HASH_MISMATCH", projectId,
1016
+ `Target '${target.id}' build files do not match its manifest.`)
1017
+ }
1018
+ for (const artifact of target.buildArtifacts) {
1019
+ await verifyManifestFile(buildPath, artifact, projectId)
1020
+ }
1021
+ try {
1022
+ const artifacts = []
1023
+
1024
+ for (const artifact of target.artifacts) {
1025
+ const bytes = await readAndSyncRegularFile(ownedPath(sourcePath, artifact.path, projectId), projectId,
1026
+ "PUBLICATION_STAGE_HASH_MISMATCH", `Manifest artifact '${artifact.path}' is not a stable regular file.`)
1027
+ let content
1028
+
1029
+ if (artifact.contentKind == "text") {
1030
+ try {
1031
+ content = new TextDecoder("utf-8", {fatal: true}).decode(bytes)
1032
+ } catch (error) {
1033
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1034
+ `Text artifact '${artifact.path}' is not valid UTF-8.`, error)
1035
+ }
1036
+ } else content = new Uint8Array(bytes)
1037
+ artifacts.push({
1038
+ content,
1039
+ contentKind: artifact.contentKind,
1040
+ mediaType: artifact.mediaType,
1041
+ ownership: /** @type {const} */ ("generated"),
1042
+ path: artifact.path,
1043
+ provenance: rehydrateProvenance(artifact.provenance, projectId),
1044
+ role: artifact.role
1045
+ })
1046
+ }
1047
+ createGeneratedArtifactSet({
1048
+ artifacts,
1049
+ ...(target.metadata === undefined ? {} : {metadata: target.metadata}),
1050
+ target: target.target
1051
+ })
1052
+ } catch (error) {
1053
+ if (error instanceof SemantifoldDiagnostic && error.code == "MALFORMED_GENERATION_MANIFEST") throw error
1054
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1055
+ `Target '${target.id}' artifact provenance does not match its staged bytes.`, error)
1056
+ }
1057
+ }
1058
+ await verifyGenerationInventory(generationPath, manifest, projectId)
1059
+ }
1060
+
1061
+ /**
1062
+ * Restores JSON-omitted optional mapping fields and verifies persisted mapping projections.
1063
+ * @param {import("./semantic/types.js").ArtifactProvenance} provenance - Persisted provenance.
1064
+ * @param {string} projectId - Project identity.
1065
+ * @returns {import("./semantic/types.js").ArtifactProvenance} Canonical provenance.
1066
+ */
1067
+ function rehydrateProvenance(provenance, projectId) {
1068
+ if (provenance.kind == "text") {
1069
+ const mapping = finalizeMapping(provenance.mapping)
1070
+ const sourceMap = toSourceMapV3(mapping)
1071
+ const jsonSourceMap = JSON.parse(JSON.stringify(sourceMap))
1072
+
1073
+ if (!sameJsonValue(jsonSourceMap, provenance.sourceMap)) {
1074
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1075
+ "Persisted Source Map projection contradicts its rich mapping.")
1076
+ }
1077
+
1078
+ return Object.freeze({
1079
+ kind: /** @type {const} */ ("text"),
1080
+ mapping,
1081
+ sourceMap,
1082
+ ...(provenance.sourceMapFilename === undefined ? {} : {sourceMapFilename: provenance.sourceMapFilename})
1083
+ })
1084
+ }
1085
+ if (provenance.kind == "bytes") {
1086
+ return Object.freeze({kind: /** @type {const} */ ("bytes"), mapping: finalizeByteMapping(provenance.mapping)})
1087
+ }
1088
+
1089
+ return provenance
1090
+ }
1091
+
1092
+ /**
1093
+ * Verifies one referenced file.
1094
+ * @param {string} projectionPath - Owning projection path.
1095
+ * @param {{byteLength: number, hash: import("./semantic/types.js").GenerationContentHash, path: string}} artifact - Manifest record.
1096
+ * @param {string} projectId - Project diagnostic identity.
1097
+ * @returns {Promise<Buffer>} Exact verified bytes.
1098
+ */
1099
+ async function verifyManifestFile(projectionPath, artifact, projectId) {
1100
+ const bytes = await readAndSyncRegularFile(ownedPath(projectionPath, artifact.path, projectId), projectId,
1101
+ "PUBLICATION_STAGE_HASH_MISMATCH", `Manifest artifact '${artifact.path}' is not a stable regular file.`)
1102
+
1103
+ if (bytes.byteLength != artifact.byteLength || sha256(bytes) != artifact.hash.value) {
1104
+ publicationFailure("PUBLICATION_STAGE_HASH_MISMATCH", projectId,
1105
+ `Manifest artifact '${artifact.path}' bytes do not match its recorded hash.`)
1106
+ }
1107
+
1108
+ return bytes
1109
+ }
1110
+
1111
+ /**
1112
+ * Validates an existing active pointer before a new candidate is staged.
1113
+ * @param {string} publicationRoot - Publication root.
1114
+ * @param {string} projectId - Project identity.
1115
+ * @returns {Promise<void>} Completion.
1116
+ */
1117
+ async function validateExistingPointer(publicationRoot, projectId) {
1118
+ const pointerPath = path.join(publicationRoot, activePointerName)
1119
+ const pointer = await readActivePointer(pointerPath, projectId, true)
1120
+
1121
+ if (pointer) await verifyPublishedGeneration(publicationRoot, projectId, pointer, false)
1122
+ }
1123
+
1124
+ /**
1125
+ * Reads and strictly validates one active pointer.
1126
+ * @param {string} pointerPath - Pointer filename.
1127
+ * @param {string} projectId - Expected project identity.
1128
+ * @param {boolean} optional - Whether absence is allowed.
1129
+ * @returns {Promise<ActivePointer | null>} Pointer or absent value.
1130
+ */
1131
+ async function readActivePointer(pointerPath, projectId, optional) {
1132
+ let pointerBytes
1133
+
1134
+ try {
1135
+ const status = await lstat(pointerPath)
1136
+
1137
+ if (!status.isFile() || status.isSymbolicLink()) {
1138
+ publicationFailure("MALFORMED_ACTIVE_GENERATION", projectId,
1139
+ "Active-generation pointer must be a regular file.")
1140
+ }
1141
+ pointerBytes = await readFile(pointerPath)
1142
+ } catch (error) {
1143
+ if (optional && isErrorCode(error, "ENOENT")) return null
1144
+ if (error instanceof SemantifoldDiagnostic) throw error
1145
+ publicationFailure(optional ? "MALFORMED_ACTIVE_GENERATION" : "ACTIVE_GENERATION_MISSING", projectId,
1146
+ optional ? "Active-generation pointer could not be read." : "No active generation has been published.", error)
1147
+ }
1148
+ let value
1149
+
1150
+ try {
1151
+ value = JSON.parse(pointerBytes.toString("utf8"))
1152
+ } catch (error) {
1153
+ publicationFailure("MALFORMED_ACTIVE_GENERATION", projectId,
1154
+ "Active-generation pointer is not valid JSON.", error)
1155
+ }
1156
+ if (!isActivePointer(value) || value.projectId != projectId) {
1157
+ publicationFailure("MALFORMED_ACTIVE_GENERATION", projectId,
1158
+ "Active-generation pointer does not match the versioned project contract.")
1159
+ }
1160
+
1161
+ return value
1162
+ }
1163
+
1164
+ /**
1165
+ * Verifies a pointer-selected generation without consulting the pointer again.
1166
+ * @param {string} publicationRoot - Publication root.
1167
+ * @param {string} projectId - Project identity.
1168
+ * @param {ActivePointer} pointer - Pointer snapshot.
1169
+ * @param {boolean} cleanupPending - Recovery state.
1170
+ * @returns {Promise<import("./semantic/types.js").PublishedGeneration>} Verified generation.
1171
+ */
1172
+ async function verifyPublishedGeneration(publicationRoot, projectId, pointer, cleanupPending) {
1173
+ const generationPath = ownedPath(path.join(publicationRoot, generationsName), pointer.generationId, projectId)
1174
+ const manifestPath = path.join(generationPath, "manifest.json")
1175
+ let manifestBytes
1176
+
1177
+ try {
1178
+ manifestBytes = await readAndSyncRegularFile(manifestPath, projectId, "MALFORMED_GENERATION_MANIFEST",
1179
+ "Generation manifest must be a regular file.")
1180
+ } catch (error) {
1181
+ if (error instanceof SemantifoldDiagnostic) throw error
1182
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1183
+ "Generation manifest could not be read.", error)
1184
+ }
1185
+ if (sha256(manifestBytes) != pointer.manifestHash) {
1186
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1187
+ "Generation manifest hash does not match the active pointer.")
1188
+ }
1189
+ let value
1190
+
1191
+ try {
1192
+ value = JSON.parse(manifestBytes.toString("utf8"))
1193
+ } catch (error) {
1194
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1195
+ "Generation manifest is not valid JSON.", error)
1196
+ }
1197
+ const manifest = validateGenerationManifest(value, projectId, pointer.generationId)
1198
+
1199
+ await verifyManifestFiles(generationPath, manifest, projectId)
1200
+
1201
+ return publishedGeneration(publicationRoot, manifest, cleanupPending)
1202
+ }
1203
+
1204
+ /**
1205
+ * Strictly validates a persisted generation manifest.
1206
+ * @param {unknown} value - Parsed manifest value.
1207
+ * @param {string} projectId - Expected project identity.
1208
+ * @param {string} generationId - Expected generation identity.
1209
+ * @returns {import("./semantic/types.js").GenerationManifest} Frozen manifest.
1210
+ */
1211
+ function validateGenerationManifest(value, projectId, generationId) {
1212
+ if (!isPlainObject(value) || !hasExactKeys(value, ["generationId", "projectId", "schema", "targets", "version"]) ||
1213
+ value.schema != "SemantifoldGenerationManifest" || value.version != 1 || value.projectId != projectId ||
1214
+ value.generationId != generationId || !isDenseArray(value.targets) || value.targets.length == 0) {
1215
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1216
+ "Generation manifest does not match the versioned project contract.")
1217
+ }
1218
+ /** @type {import("./semantic/types.js").GenerationTargetManifest[]} */
1219
+ const targets = []
1220
+ /** @type {string[]} */
1221
+ const projections = []
1222
+ const ids = new Set()
1223
+
1224
+ for (const target of value.targets) {
1225
+ const targetKeys = target && typeof target == "object" && "metadata" in target
1226
+ ? ["artifacts", "buildArtifacts", "id", "metadata", "projections", "role", "target"]
1227
+ : ["artifacts", "buildArtifacts", "id", "projections", "role", "target"]
1228
+
1229
+ if (!isPlainObject(target) || !hasExactKeys(target, targetKeys) ||
1230
+ !identityPattern.test(typeof target.id == "string" ? target.id : "") || ids.has(target.id) ||
1231
+ typeof target.role != "string" || !targetRoles.has(target.role) ||
1232
+ typeof target.target != "string" || !identityPattern.test(target.target) ||
1233
+ !isPlainObject(target.projections) || !hasExactKeys(target.projections, ["build", "source"]) ||
1234
+ !isSafeArtifactPath(target.projections.source) || !isSafeArtifactPath(target.projections.build) ||
1235
+ !isDenseArray(target.artifacts) || target.artifacts.length == 0 || !isDenseArray(target.buildArtifacts)) {
1236
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1237
+ "Generation manifest contains an invalid target record.")
1238
+ }
1239
+ const targetId = /** @type {string} */ (target.id)
1240
+
1241
+ ids.add(targetId)
1242
+ projections.push(target.projections.source, target.projections.build)
1243
+ const artifacts = target.artifacts.map(artifact => validatePersistedArtifact(artifact, true, projectId))
1244
+ const buildArtifacts = target.buildArtifacts.map(artifact => validatePersistedArtifact(artifact, false, projectId))
1245
+
1246
+ if (findPortableArtifactPathConflict(artifacts.map(({path: artifactPath}) => artifactPath)) ||
1247
+ findPortableArtifactPathConflict(buildArtifacts.map(({path: artifactPath}) => artifactPath))) {
1248
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1249
+ "Generation manifest contains colliding artifact paths.")
1250
+ }
1251
+ targets.push(Object.freeze({
1252
+ artifacts: Object.freeze(/** @type {import("./semantic/types.js").GenerationArtifactManifest[]} */ (artifacts)),
1253
+ buildArtifacts: Object.freeze(/** @type {import("./semantic/types.js").GenerationBuildArtifactManifest[]} */ (buildArtifacts)),
1254
+ id: targetId,
1255
+ ...(target.metadata === undefined ? {} : {metadata: deepFreeze(/** @type {Record<string, unknown>} */ (target.metadata))}),
1256
+ projections: Object.freeze({build: target.projections.build, source: target.projections.source}),
1257
+ role: /** @type {import("./semantic/types.js").PublicationTargetRole} */ (target.role),
1258
+ target: target.target
1259
+ }))
1260
+ }
1261
+ if (findPortableArtifactPathConflict(projections) ||
1262
+ findPortableArtifactPathConflict([...projections, "manifest.json"])) {
1263
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1264
+ "Generation manifest contains overlapping projections.")
1265
+ }
1266
+
1267
+ return Object.freeze({
1268
+ generationId,
1269
+ projectId,
1270
+ schema: /** @type {const} */ ("SemantifoldGenerationManifest"),
1271
+ targets: Object.freeze(targets),
1272
+ version: /** @type {const} */ (1)
1273
+ })
1274
+ }
1275
+
1276
+ /**
1277
+ * Validates one persisted artifact record.
1278
+ * @param {unknown} value - Record candidate.
1279
+ * @param {boolean} generated - Whether this is a generated-source artifact.
1280
+ * @param {string} projectId - Project identity.
1281
+ * @returns {import("./semantic/types.js").GenerationArtifactManifest | import("./semantic/types.js").GenerationBuildArtifactManifest} Frozen record.
1282
+ */
1283
+ function validatePersistedArtifact(value, generated, projectId) {
1284
+ const keys = generated
1285
+ ? ["byteLength", "contentKind", "hash", "mediaType", "path", "provenance", "role"]
1286
+ : ["byteLength", "hash", "mediaType", "path", "role"]
1287
+
1288
+ if (!isPlainObject(value) || !hasExactKeys(value, keys) || !isSafeArtifactPath(value.path) ||
1289
+ typeof value.byteLength != "number" || !Number.isSafeInteger(value.byteLength) || value.byteLength < 0 || typeof value.mediaType != "string" ||
1290
+ !mediaTypePattern.test(value.mediaType) || typeof value.role != "string" ||
1291
+ !(generated ? ["entry", "source", "manifest", "support", "mapping", "resource", "loader"].includes(value.role) :
1292
+ buildRolePattern.test(value.role)) ||
1293
+ (generated && value.contentKind != "text" && value.contentKind != "binary") ||
1294
+ (generated && !isPlainObject(value.provenance)) ||
1295
+ !isPlainObject(value.hash) || !hasExactKeys(value.hash, ["algorithm", "value"]) || value.hash.algorithm != "sha256" ||
1296
+ typeof value.hash.value != "string" || !/^[a-f0-9]{64}$/u.test(value.hash.value)) {
1297
+ publicationFailure("MALFORMED_GENERATION_MANIFEST", projectId,
1298
+ "Generation manifest contains an invalid artifact record.")
1299
+ }
1300
+
1301
+ return Object.freeze(/** @type {import("./semantic/types.js").GenerationArtifactManifest | import("./semantic/types.js").GenerationBuildArtifactManifest} */ ({
1302
+ byteLength: value.byteLength,
1303
+ ...(generated ? {contentKind: value.contentKind} : {}),
1304
+ hash: Object.freeze({algorithm: /** @type {const} */ ("sha256"), value: value.hash.value}),
1305
+ mediaType: value.mediaType,
1306
+ path: value.path,
1307
+ ...(generated ? {provenance: deepFreeze(/** @type {import("./semantic/types.js").ArtifactProvenance} */ (value.provenance))} : {}),
1308
+ role: value.role
1309
+ }))
1310
+ }
1311
+
1312
+ /**
1313
+ * Constructs a frozen resolved-generation result.
1314
+ * @param {string} publicationRoot - Publication root.
1315
+ * @param {import("./semantic/types.js").GenerationManifest} manifest - Verified manifest.
1316
+ * @param {boolean} cleanupPending - Whether cleanup remains.
1317
+ * @returns {import("./semantic/types.js").PublishedGeneration} Frozen result.
1318
+ */
1319
+ function publishedGeneration(publicationRoot, manifest, cleanupPending) {
1320
+ const generationPath = path.join(publicationRoot, generationsName, manifest.generationId)
1321
+ const targets = manifest.targets.map(target => Object.freeze({
1322
+ buildPath: path.join(generationPath, target.projections.build),
1323
+ id: target.id,
1324
+ role: target.role,
1325
+ sourcePath: path.join(generationPath, target.projections.source)
1326
+ }))
1327
+
1328
+ return Object.freeze({
1329
+ cleanupPending,
1330
+ generationId: manifest.generationId,
1331
+ generationPath,
1332
+ manifest,
1333
+ manifestPath: path.join(generationPath, "manifest.json"),
1334
+ targets: Object.freeze(targets)
1335
+ })
1336
+ }
1337
+
1338
+ /**
1339
+ * Probes the exact same-directory regular-file replacement primitive used by the active pointer.
1340
+ * @param {string} publicationRoot - Publication root.
1341
+ * @param {string} projectId - Project identity.
1342
+ * @returns {Promise<void>} Completion.
1343
+ */
1344
+ async function verifyAtomicReplacement(publicationRoot, projectId) {
1345
+ const identity = randomUUID()
1346
+ const source = path.join(publicationRoot, `.semantifold-atomic-${identity}.source`)
1347
+ const destination = path.join(publicationRoot, `.semantifold-atomic-${identity}.destination`)
1348
+ const expected = Buffer.from("candidate\n")
1349
+ let sourceCreated = false
1350
+ let destinationCreated = false
1351
+
1352
+ try {
1353
+ await writeSyncedFile(source, Buffer.from("candidate\n"), "wx")
1354
+ sourceCreated = true
1355
+ await writeSyncedFile(destination, Buffer.from("previous\n"), "wx")
1356
+ destinationCreated = true
1357
+ await rename(source, destination)
1358
+ sourceCreated = false
1359
+ await syncDirectory(publicationRoot)
1360
+ const destinationStatus = await lstat(destination)
1361
+ const replaced = await readFile(destination)
1362
+
1363
+ if (!destinationStatus.isFile() || destinationStatus.isSymbolicLink() || !replaced.equals(expected)) {
1364
+ publicationFailure("PUBLICATION_ATOMIC_REPLACE_UNSUPPORTED", projectId,
1365
+ "Filesystem did not provide same-directory regular-file replacement.")
1366
+ }
1367
+ try {
1368
+ await lstat(source)
1369
+ publicationFailure("PUBLICATION_ATOMIC_REPLACE_UNSUPPORTED", projectId,
1370
+ "Filesystem did not remove the replaced temporary name atomically.")
1371
+ } catch (error) {
1372
+ if (!isErrorCode(error, "ENOENT")) throw error
1373
+ }
1374
+ } catch (error) {
1375
+ if (error instanceof SemantifoldDiagnostic) throw error
1376
+ publicationFailure("PUBLICATION_ATOMIC_REPLACE_UNSUPPORTED", projectId,
1377
+ "Filesystem did not provide same-directory regular-file replacement.", error)
1378
+ } finally {
1379
+ await Promise.allSettled([
1380
+ ...(sourceCreated ? [rm(source, {force: true})] : []),
1381
+ ...(destinationCreated ? [rm(destination, {force: true})] : [])
1382
+ ])
1383
+ }
1384
+ }
1385
+
1386
+ /**
1387
+ * Writes and synchronizes one newly owned file.
1388
+ * @param {string} filename - Exact filename.
1389
+ * @param {Uint8Array} bytes - Exact bytes.
1390
+ * @param {"wx"} flag - Exclusive creation mode.
1391
+ * @param {() => void} [onCreated] - Called immediately after exclusive creation succeeds.
1392
+ * @returns {Promise<void>} Completion.
1393
+ */
1394
+ async function writeSyncedFile(filename, bytes, flag, onCreated) {
1395
+ const handle = await open(filename, flag, 0o600)
1396
+
1397
+ onCreated?.()
1398
+ /** @type {unknown} */
1399
+ let failure
1400
+
1401
+ try {
1402
+ await handle.writeFile(bytes)
1403
+ await handle.sync()
1404
+ } catch (error) {
1405
+ failure = error
1406
+ } finally {
1407
+ try {
1408
+ await handle.close()
1409
+ } catch (error) {
1410
+ if (failure === undefined) failure = error
1411
+ }
1412
+ }
1413
+ if (failure !== undefined) {
1414
+ await rm(filename, {force: true}).catch(() => {})
1415
+ throw failure
1416
+ }
1417
+ }
1418
+
1419
+ /**
1420
+ * Reads and synchronizes one regular non-symlink file.
1421
+ * @param {string} filename - Exact filename.
1422
+ * @param {string} projectId - Project identity.
1423
+ * @param {string} code - Failure code.
1424
+ * @param {string} message - Stable failure detail.
1425
+ * @returns {Promise<Buffer>} Exact bytes.
1426
+ */
1427
+ async function readAndSyncRegularFile(filename, projectId, code, message) {
1428
+ try {
1429
+ const status = await lstat(filename)
1430
+
1431
+ if (!status.isFile() || status.isSymbolicLink()) publicationFailure(code, projectId, message)
1432
+ assertRegularFileImmutability(status, new Set(), projectId)
1433
+ const handle = await open(filename, constants.O_RDONLY)
1434
+
1435
+ try {
1436
+ const bytes = await handle.readFile()
1437
+
1438
+ await handle.sync()
1439
+
1440
+ return bytes
1441
+ } finally {
1442
+ await handle.close()
1443
+ }
1444
+ } catch (error) {
1445
+ if (error instanceof SemantifoldDiagnostic) throw error
1446
+ publicationFailure(code, projectId, message, error)
1447
+ }
1448
+ }
1449
+
1450
+ /**
1451
+ * Lists every regular file beneath one projection and rejects symlinks or special nodes.
1452
+ * @param {string} directory - Projection root.
1453
+ * @param {string} projectId - Project identity.
1454
+ * @returns {Promise<string[]>} Sorted relative POSIX paths.
1455
+ */
1456
+ async function listRegularFiles(directory, projectId) {
1457
+ /** @type {string[]} */
1458
+ const files = []
1459
+ const identities = new Set()
1460
+
1461
+ try {
1462
+ await visit(directory, "")
1463
+ } catch (error) {
1464
+ if (error instanceof SemantifoldDiagnostic) throw error
1465
+ publicationFailure("PUBLICATION_GENERATION_VERIFICATION_FAILED", projectId,
1466
+ "Immutable generation filesystem inventory could not be verified.", error)
1467
+ }
1468
+
1469
+ return files.sort(compareStrings)
1470
+
1471
+ /**
1472
+ * Visits one owned directory.
1473
+ * @param {string} current - Exact directory.
1474
+ * @param {string} relative - Projection-relative path.
1475
+ * @returns {Promise<void>} Completion.
1476
+ */
1477
+ async function visit(current, relative) {
1478
+ await assertRealInventoryDirectory(current, projectId)
1479
+ const entries = await readdir(current)
1480
+
1481
+ entries.sort(compareStrings)
1482
+ for (const entry of entries) {
1483
+ const childRelative = relative.length == 0 ? entry : `${relative}/${entry}`
1484
+
1485
+ if (!isSafeArtifactPath(childRelative)) {
1486
+ publicationFailure("INVALID_PUBLICATION_PROJECTION", projectId,
1487
+ "Validator output contains a non-canonical relative path.")
1488
+ }
1489
+ const child = path.join(current, entry)
1490
+ const status = await lstat(child)
1491
+
1492
+ if (status.isSymbolicLink()) {
1493
+ publicationFailure("PUBLICATION_SYMLINK_TRAVERSAL", projectId,
1494
+ "Candidate generation contains a symbolic link.")
1495
+ }
1496
+ if (status.isDirectory()) await visit(child, childRelative)
1497
+ else if (status.isFile()) {
1498
+ assertRegularFileImmutability(status, identities, projectId)
1499
+ files.push(childRelative)
1500
+ } else publicationFailure("PUBLICATION_GENERATION_INVENTORY_MISMATCH", projectId,
1501
+ "Immutable generation contains an undeclared filesystem entry.")
1502
+ }
1503
+ }
1504
+ }
1505
+
1506
+ /**
1507
+ * Verifies that the manifest describes every directory and regular file in the immutable generation.
1508
+ * @param {string} generationPath - Exact generation root.
1509
+ * @param {import("./semantic/types.js").GenerationManifest} manifest - Verified manifest.
1510
+ * @param {string} projectId - Project identity.
1511
+ * @returns {Promise<void>} Completion.
1512
+ */
1513
+ async function verifyGenerationInventory(generationPath, manifest, projectId) {
1514
+ const expectedFiles = new Set(["manifest.json"])
1515
+ const expectedDirectories = new Set()
1516
+
1517
+ for (const target of manifest.targets) {
1518
+ addExpectedDirectory(target.projections.source)
1519
+ addExpectedDirectory(target.projections.build)
1520
+ for (const artifact of target.artifacts) {
1521
+ addExpectedFile(`${target.projections.source}/${artifact.path}`)
1522
+ }
1523
+ for (const artifact of target.buildArtifacts) {
1524
+ addExpectedFile(`${target.projections.build}/${artifact.path}`)
1525
+ }
1526
+ }
1527
+ /** @type {Set<string>} */
1528
+ const observedFiles = new Set()
1529
+ /** @type {Set<string>} */
1530
+ const observedDirectories = new Set()
1531
+ const identities = new Set()
1532
+
1533
+ try {
1534
+ await visit(generationPath, "")
1535
+ } catch (error) {
1536
+ if (error instanceof SemantifoldDiagnostic) throw error
1537
+ publicationFailure("PUBLICATION_GENERATION_VERIFICATION_FAILED", projectId,
1538
+ "Immutable generation filesystem inventory could not be verified.", error)
1539
+ }
1540
+ if (!sameStringSet(expectedFiles, observedFiles) || !sameStringSet(expectedDirectories, observedDirectories)) {
1541
+ publicationFailure("PUBLICATION_GENERATION_INVENTORY_MISMATCH", projectId,
1542
+ "Immutable generation contains an undeclared filesystem entry.")
1543
+ }
1544
+
1545
+ /**
1546
+ * Adds one expected directory and each generation-relative parent.
1547
+ * @param {string} relative - POSIX-style generation-relative directory.
1548
+ * @returns {void}
1549
+ */
1550
+ function addExpectedDirectory(relative) {
1551
+ const parts = relative.split("/")
1552
+
1553
+ for (let index = 1; index <= parts.length; index += 1) {
1554
+ expectedDirectories.add(parts.slice(0, index).join("/"))
1555
+ }
1556
+ }
1557
+
1558
+ /**
1559
+ * Adds one expected regular file and its parent directories.
1560
+ * @param {string} relative - POSIX-style generation-relative filename.
1561
+ * @returns {void}
1562
+ */
1563
+ function addExpectedFile(relative) {
1564
+ expectedFiles.add(relative)
1565
+ const separator = relative.lastIndexOf("/")
1566
+
1567
+ if (separator >= 0) addExpectedDirectory(relative.slice(0, separator))
1568
+ }
1569
+
1570
+ /**
1571
+ * Visits every entry in one generation directory without following links.
1572
+ * @param {string} current - Exact directory.
1573
+ * @param {string} relative - Generation-relative directory.
1574
+ * @returns {Promise<void>} Completion.
1575
+ */
1576
+ async function visit(current, relative) {
1577
+ await assertRealInventoryDirectory(current, projectId)
1578
+ const entries = await readdir(current)
1579
+
1580
+ entries.sort(compareStrings)
1581
+ for (const entry of entries) {
1582
+ const childRelative = relative.length == 0 ? entry : `${relative}/${entry}`
1583
+
1584
+ if (!isSafeArtifactPath(childRelative)) {
1585
+ publicationFailure("PUBLICATION_GENERATION_INVENTORY_MISMATCH", projectId,
1586
+ "Immutable generation contains an undeclared filesystem entry.")
1587
+ }
1588
+ const child = path.join(current, entry)
1589
+ const status = await lstat(child)
1590
+
1591
+ if (status.isSymbolicLink()) {
1592
+ publicationFailure("PUBLICATION_SYMLINK_TRAVERSAL", projectId,
1593
+ "Candidate generation contains a symbolic link.")
1594
+ }
1595
+ if (status.isDirectory()) {
1596
+ observedDirectories.add(childRelative)
1597
+ await visit(child, childRelative)
1598
+ } else if (status.isFile()) {
1599
+ assertRegularFileImmutability(status, identities, projectId)
1600
+ observedFiles.add(childRelative)
1601
+ } else {
1602
+ publicationFailure("PUBLICATION_GENERATION_INVENTORY_MISMATCH", projectId,
1603
+ "Immutable generation contains an undeclared filesystem entry.")
1604
+ }
1605
+ }
1606
+ }
1607
+ }
1608
+
1609
+ /**
1610
+ * Verifies one inventory traversal root without following symbolic links.
1611
+ * @param {string} directory - Exact directory about to be read.
1612
+ * @param {string} projectId - Project identity.
1613
+ * @returns {Promise<void>} Completion.
1614
+ */
1615
+ async function assertRealInventoryDirectory(directory, projectId) {
1616
+ const status = await lstat(directory)
1617
+
1618
+ if (status.isSymbolicLink()) {
1619
+ publicationFailure("PUBLICATION_SYMLINK_TRAVERSAL", projectId,
1620
+ "Candidate generation contains a symbolic link.")
1621
+ }
1622
+ if (!status.isDirectory()) {
1623
+ publicationFailure("PUBLICATION_GENERATION_INVENTORY_MISMATCH", projectId,
1624
+ "Immutable generation contains an undeclared filesystem entry.")
1625
+ }
1626
+ }
1627
+
1628
+ /**
1629
+ * Rejects external hard links and duplicate stable file identities.
1630
+ * @param {import("node:fs").Stats} status - Regular-file status.
1631
+ * @param {Set<string>} identities - Previously observed device/inode identities.
1632
+ * @param {string} projectId - Project identity.
1633
+ * @returns {void}
1634
+ */
1635
+ function assertRegularFileImmutability(status, identities, projectId) {
1636
+ if (status.nlink > 1) {
1637
+ publicationFailure("PUBLICATION_HARD_LINK", projectId,
1638
+ "Immutable generation files must not have external hard links.")
1639
+ }
1640
+ const identity = stableFileIdentity(status)
1641
+
1642
+ if (identity !== null) {
1643
+ if (identities.has(identity)) {
1644
+ publicationFailure("PUBLICATION_HARD_LINK", projectId,
1645
+ "Immutable generation files must have unique filesystem identities.")
1646
+ }
1647
+ identities.add(identity)
1648
+ }
1649
+ }
1650
+
1651
+ /**
1652
+ * Returns a stable device/inode identity only where the host reports one.
1653
+ * @param {import("node:fs").Stats} status - File status.
1654
+ * @returns {string | null} Identity or unavailable.
1655
+ */
1656
+ function stableFileIdentity(status) {
1657
+ if ((typeof status.dev != "number" && typeof status.dev != "bigint") ||
1658
+ (typeof status.ino != "number" && typeof status.ino != "bigint") || status.ino == 0) return null
1659
+
1660
+ return `${String(status.dev)}:${String(status.ino)}`
1661
+ }
1662
+
1663
+ /**
1664
+ * Compares two string sets exactly.
1665
+ * @param {Set<string>} left - First set.
1666
+ * @param {Set<string>} right - Second set.
1667
+ * @returns {boolean} Equality.
1668
+ */
1669
+ function sameStringSet(left, right) {
1670
+ return left.size == right.size && [...left].every(value => right.has(value))
1671
+ }
1672
+
1673
+ /**
1674
+ * Synchronizes every directory in a staged tree from leaves to root.
1675
+ * @param {string} root - Tree root.
1676
+ * @returns {Promise<void>} Completion.
1677
+ */
1678
+ async function syncTreeDirectories(root) {
1679
+ const entries = await readdir(root, {withFileTypes: true})
1680
+
1681
+ for (const entry of entries) {
1682
+ if (entry.isDirectory() && !entry.isSymbolicLink()) await syncTreeDirectories(path.join(root, entry.name))
1683
+ }
1684
+ await syncDirectory(root)
1685
+ }
1686
+
1687
+ /**
1688
+ * Synchronizes one directory entry set.
1689
+ * @param {string} directory - Directory path.
1690
+ * @returns {Promise<void>} Completion.
1691
+ */
1692
+ async function syncDirectory(directory) {
1693
+ const handle = await open(directory, constants.O_RDONLY)
1694
+
1695
+ try {
1696
+ await handle.sync()
1697
+ } finally {
1698
+ await handle.close()
1699
+ }
1700
+ }
1701
+
1702
+ /**
1703
+ * Ensures every existing path component is a real directory and optionally creates missing components.
1704
+ * @param {string} directory - Canonical absolute directory.
1705
+ * @param {boolean} create - Whether missing components may be created.
1706
+ * @param {string} projectId - Project identity.
1707
+ * @returns {Promise<void>} Completion.
1708
+ */
1709
+ async function ensureDirectoryWithoutSymlinks(directory, create, projectId) {
1710
+ const parsed = path.parse(directory)
1711
+ const relative = directory.slice(parsed.root.length)
1712
+ const parts = relative.split(path.sep).filter(Boolean)
1713
+ let current = parsed.root
1714
+
1715
+ for (const part of parts) {
1716
+ current = path.join(current, part)
1717
+ try {
1718
+ const status = await lstat(current)
1719
+
1720
+ if (status.isSymbolicLink()) {
1721
+ publicationFailure("PUBLICATION_SYMLINK_TRAVERSAL", projectId,
1722
+ "Owned path traversal encountered a symbolic link.")
1723
+ }
1724
+ if (!status.isDirectory()) {
1725
+ publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
1726
+ "Owned root path contains a non-directory component.")
1727
+ }
1728
+ } catch (error) {
1729
+ if (!isErrorCode(error, "ENOENT")) {
1730
+ if (error instanceof SemantifoldDiagnostic) throw error
1731
+ publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
1732
+ "Owned root path could not be validated.", error)
1733
+ }
1734
+ if (!create) {
1735
+ publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
1736
+ "Source root must exist as a real directory.", error)
1737
+ }
1738
+ try {
1739
+ await mkdir(current, {mode: 0o700})
1740
+ } catch (mkdirError) {
1741
+ publicationFailure("INVALID_PUBLICATION_ROOT", projectId,
1742
+ "Publication root could not be created safely.", mkdirError)
1743
+ }
1744
+ }
1745
+ }
1746
+ }
1747
+
1748
+ /**
1749
+ * Ensures one reserved publisher directory is neither a symlink nor an unowned file.
1750
+ * @param {string} directory - Reserved directory.
1751
+ * @param {string} projectId - Project identity.
1752
+ * @returns {Promise<void>} Completion.
1753
+ */
1754
+ async function ensureOwnedDirectory(directory, projectId) {
1755
+ try {
1756
+ await mkdir(directory, {mode: 0o700})
1757
+ } catch (error) {
1758
+ if (!isErrorCode(error, "EEXIST")) {
1759
+ publicationFailure("PUBLICATION_STAGE_WRITE_FAILED", projectId,
1760
+ "Publisher-owned directory could not be created.", error)
1761
+ }
1762
+ }
1763
+ let status
1764
+
1765
+ try {
1766
+ status = await lstat(directory)
1767
+ } catch (error) {
1768
+ publicationFailure("PUBLICATION_STAGE_WRITE_FAILED", projectId,
1769
+ "Reserved publisher path could not be inspected.", error)
1770
+ }
1771
+
1772
+ if (!status.isDirectory() || status.isSymbolicLink()) {
1773
+ publicationFailure(status.isSymbolicLink() ? "PUBLICATION_SYMLINK_TRAVERSAL" : "PUBLICATION_UNOWNED_CONFLICT",
1774
+ projectId, "Reserved publisher path is not an owned real directory.")
1775
+ }
1776
+ }
1777
+
1778
+ /**
1779
+ * Ensures an owned path has no pre-existing entry.
1780
+ * @param {string} filename - Exact path.
1781
+ * @param {string} code - Diagnostic code.
1782
+ * @param {string} projectId - Project identity.
1783
+ * @param {string} message - Stable failure detail.
1784
+ * @returns {Promise<void>} Completion.
1785
+ */
1786
+ async function assertAbsent(filename, code, projectId, message) {
1787
+ try {
1788
+ await lstat(filename)
1789
+ publicationFailure(code, projectId, message)
1790
+ } catch (error) {
1791
+ if (isErrorCode(error, "ENOENT")) return
1792
+ if (error instanceof SemantifoldDiagnostic) throw error
1793
+ publicationFailure(code, projectId, message, error)
1794
+ }
1795
+ }
1796
+
1797
+ /**
1798
+ * Resolves a validated relative path under an owned root and rejects escape.
1799
+ * @param {string} root - Owned root.
1800
+ * @param {string} relative - Validated relative POSIX path.
1801
+ * @param {string} projectId - Project identity.
1802
+ * @returns {string} Exact owned path.
1803
+ */
1804
+ function ownedPath(root, relative, projectId) {
1805
+ const resolved = path.resolve(root, ...relative.split("/"))
1806
+ const child = path.relative(root, resolved)
1807
+
1808
+ if (child.length == 0 || child == ".." || child.startsWith(`..${path.sep}`) || path.isAbsolute(child)) {
1809
+ publicationFailure("PUBLICATION_PATH_ESCAPE", projectId,
1810
+ "Resolved publication path escaped its owned root.")
1811
+ }
1812
+
1813
+ return resolved
1814
+ }
1815
+
1816
+ /**
1817
+ * Checks a canonical non-root absolute path without filesystem access.
1818
+ * @param {unknown} value - Path candidate.
1819
+ * @returns {value is string} Whether the path is canonical.
1820
+ */
1821
+ function isCanonicalAbsolutePath(value) {
1822
+ return isCanonicalAbsolutePathForPath(value, path)
1823
+ }
1824
+
1825
+ /**
1826
+ * Checks one absolute path using the supplied host path semantics.
1827
+ * This helper is intentionally not part of the package root API.
1828
+ * @param {unknown} value - Path candidate.
1829
+ * @param {typeof path.posix | typeof path.win32} hostPath - Host-native path implementation.
1830
+ * @returns {value is string} Whether the path is canonical for that host.
1831
+ */
1832
+ export function isCanonicalAbsolutePathForPath(value, hostPath) {
1833
+ const separatorAlias = hostPath.sep == "/" ? "\\" : "/"
1834
+
1835
+ return typeof value == "string" && value.length > 0 && !value.includes("\0") && !value.includes(separatorAlias) &&
1836
+ hostPath.isAbsolute(value) && value != hostPath.parse(value).root && hostPath.normalize(value) == value &&
1837
+ hostPath.resolve(value) == value
1838
+ }
1839
+
1840
+ /**
1841
+ * Checks conservative portable overlap between absolute paths.
1842
+ * @param {string} left - First path.
1843
+ * @param {string} right - Second path.
1844
+ * @returns {boolean} Whether either contains the other.
1845
+ */
1846
+ function pathsOverlap(left, right) {
1847
+ const leftKey = left.toLowerCase()
1848
+ const rightKey = right.toLowerCase()
1849
+
1850
+ return leftKey == rightKey || leftKey.startsWith(`${rightKey}${path.sep}`) || rightKey.startsWith(`${leftKey}${path.sep}`)
1851
+ }
1852
+
1853
+ /**
1854
+ * Checks whether a path is equal to or nested beneath one canonical root.
1855
+ * @param {string} root - Canonical root.
1856
+ * @param {string} candidate - Canonical candidate.
1857
+ * @returns {boolean} Whether the candidate is within the root.
1858
+ */
1859
+ function isPathWithin(root, candidate) {
1860
+ const relative = path.relative(root, candidate)
1861
+
1862
+ return relative.length == 0 || (relative != ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
1863
+ }
1864
+
1865
+ /**
1866
+ * Checks whether a path is strictly nested beneath one canonical root.
1867
+ * @param {string} root - Canonical root.
1868
+ * @param {string} candidate - Canonical candidate.
1869
+ * @returns {boolean} Whether the candidate is a strict descendant.
1870
+ */
1871
+ function isStrictPathDescendant(root, candidate) {
1872
+ return isPathWithin(root, candidate) && path.relative(root, candidate).length > 0
1873
+ }
1874
+
1875
+ /**
1876
+ * Serializes stable generated JSON with one trailing LF.
1877
+ * @param {unknown} value - JSON value.
1878
+ * @returns {Buffer} UTF-8 bytes.
1879
+ */
1880
+ function serializeJson(value) {
1881
+ return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8")
1882
+ }
1883
+
1884
+ /**
1885
+ * Hashes exact bytes with SHA-256.
1886
+ * @param {Uint8Array} bytes - Exact bytes.
1887
+ * @returns {string} Lowercase hexadecimal hash.
1888
+ */
1889
+ function sha256(bytes) {
1890
+ return createHash("sha256").update(bytes).digest("hex")
1891
+ }
1892
+
1893
+ /**
1894
+ * Deeply freezes a parsed JSON value.
1895
+ * @template T
1896
+ * @param {T} value - JSON value.
1897
+ * @returns {T} Frozen value.
1898
+ */
1899
+ function deepFreeze(value) {
1900
+ if (typeof value == "object" && value != null) {
1901
+ for (const child of Object.values(value)) deepFreeze(child)
1902
+ Object.freeze(value)
1903
+ }
1904
+
1905
+ return value
1906
+ }
1907
+
1908
+ /**
1909
+ * Compares detached JSON values without depending on object-key order.
1910
+ * @param {unknown} left - First JSON value.
1911
+ * @param {unknown} right - Second JSON value.
1912
+ * @returns {boolean} Whether the values are structurally equal.
1913
+ */
1914
+ function sameJsonValue(left, right) {
1915
+ if (left === right) return true
1916
+ if (Array.isArray(left) || Array.isArray(right)) {
1917
+ return Array.isArray(left) && Array.isArray(right) && left.length == right.length &&
1918
+ left.every((entry, index) => sameJsonValue(entry, right[index]))
1919
+ }
1920
+ if (!isPlainObject(left) || !isPlainObject(right)) return false
1921
+ const leftKeys = Object.keys(left)
1922
+ const rightKeys = Object.keys(right)
1923
+
1924
+ return leftKeys.length == rightKeys.length &&
1925
+ leftKeys.every(key => Object.hasOwn(right, key) && sameJsonValue(left[key], right[key]))
1926
+ }
1927
+
1928
+ /**
1929
+ * Checks an active-pointer value.
1930
+ * @param {unknown} value - Parsed value.
1931
+ * @returns {value is ActivePointer} Whether the value is strict and versioned.
1932
+ */
1933
+ function isActivePointer(value) {
1934
+ return isPlainObject(value) && hasExactKeys(value, ["generationId", "manifestHash", "projectId", "schema", "version"]) &&
1935
+ value.schema == "SemantifoldActiveGeneration" && value.version == 1 &&
1936
+ typeof value.projectId == "string" && identityPattern.test(value.projectId) &&
1937
+ typeof value.generationId == "string" && generationPattern.test(value.generationId) &&
1938
+ typeof value.manifestHash == "string" && /^[a-f0-9]{64}$/u.test(value.manifestHash)
1939
+ }
1940
+
1941
+ /**
1942
+ * Checks a recovery-journal value.
1943
+ * @param {unknown} value - Parsed value.
1944
+ * @returns {value is PublicationJournal} Whether the value is strict and versioned.
1945
+ */
1946
+ function isPublicationJournal(value) {
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"])
1955
+ }
1956
+
1957
+ /**
1958
+ * Checks an ordinary object with exactly the expected enumerable string keys.
1959
+ * @param {Record<string, unknown>} value - Object candidate.
1960
+ * @param {readonly string[]} keys - Sorted expected keys.
1961
+ * @returns {boolean} Whether keys match exactly.
1962
+ */
1963
+ function hasExactKeys(value, keys) {
1964
+ const actual = Object.keys(value).sort(compareStrings)
1965
+
1966
+ return actual.length == keys.length && actual.every((key, index) => key == keys[index])
1967
+ }
1968
+
1969
+ /**
1970
+ * Checks for an ordinary string-keyed object.
1971
+ * @param {unknown} value - Candidate value.
1972
+ * @returns {value is Record<string, unknown>} Whether the value is plain.
1973
+ */
1974
+ function isPlainObject(value) {
1975
+ return typeof value == "object" && value != null && !Array.isArray(value) && Object.getPrototypeOf(value) == Object.prototype
1976
+ }
1977
+
1978
+ /**
1979
+ * Checks for a dense ordinary array.
1980
+ * @param {unknown} value - Candidate value.
1981
+ * @returns {value is unknown[]} Whether the value is dense.
1982
+ */
1983
+ function isDenseArray(value) {
1984
+ if (!Array.isArray(value)) return false
1985
+ const indexedKeys = Object.keys(value).filter(key => /^(?:0|[1-9][0-9]*)$/u.test(key))
1986
+
1987
+ return indexedKeys.length == value.length
1988
+ }
1989
+
1990
+ /**
1991
+ * Compares strings by Unicode code unit.
1992
+ * @param {string} left - First string.
1993
+ * @param {string} right - Second string.
1994
+ * @returns {number} Ordering.
1995
+ */
1996
+ function compareStrings(left, right) {
1997
+ return left < right ? -1 : left > right ? 1 : 0
1998
+ }
1999
+
2000
+ /**
2001
+ * Checks a narrowed filesystem error code.
2002
+ * @param {unknown} error - Opaque error.
2003
+ * @param {string} code - Expected code.
2004
+ * @returns {boolean} Whether the code matches.
2005
+ */
2006
+ function isErrorCode(error, code) {
2007
+ return error instanceof Error && "code" in error && error.code == code
2008
+ }
2009
+
2010
+ /**
2011
+ * Throws one stable publication diagnostic without embedding host paths or raw filesystem messages.
2012
+ * @param {string} code - Stable diagnostic code.
2013
+ * @param {string} projectId - Project identity.
2014
+ * @param {string} message - Stable detail.
2015
+ * @param {unknown} [error] - Preserved cause.
2016
+ * @returns {never} Always throws.
2017
+ */
2018
+ function publicationFailure(code, projectId, message, error) {
2019
+ throw new SemantifoldDiagnostic({
2020
+ cause: error instanceof Error ? error : undefined,
2021
+ code,
2022
+ language: projectId,
2023
+ message
2024
+ })
2025
+ }
2026
+
2027
+ /**
2028
+ * @typedef ValidatedPublicationTarget
2029
+ * @property {string} id - Target identity.
2030
+ * @property {import("./semantic/types.js").GeneratedArtifactSet} artifactSet - Detached artifact set.
2031
+ * @property {string} sourceProjection - Source projection.
2032
+ * @property {string} buildProjection - Build projection.
2033
+ * @property {import("./semantic/types.js").PublicationTargetRole} role - Language-neutral target role.
2034
+ * @property {readonly import("./semantic/types.js").PublicationValidator[]} validators - Ordered validators.
2035
+ */
2036
+
2037
+ /**
2038
+ * @typedef ValidatedPublicationRequest
2039
+ * @property {string} generationId - Snapshotted generation identity.
2040
+ * @property {readonly ValidatedPublicationTarget[]} targets - Snapshotted ordered targets.
2041
+ */
2042
+
2043
+ /**
2044
+ * @typedef ActivePointer
2045
+ * @property {"SemantifoldActiveGeneration"} schema - Schema discriminator.
2046
+ * @property {1} version - Schema version.
2047
+ * @property {string} projectId - Project identity.
2048
+ * @property {string} generationId - Generation identity.
2049
+ * @property {string} manifestHash - SHA-256 generation-manifest hash.
2050
+ */
2051
+
2052
+ /**
2053
+ * @typedef CandidatePublicationJournal
2054
+ * @property {"SemantifoldPublicationJournal"} schema - Schema discriminator.
2055
+ * @property {1} version - Schema version.
2056
+ * @property {string} projectId - Project identity.
2057
+ * @property {string} generationId - Candidate generation identity.
2058
+ * @property {string} tempPointer - Owned sibling temporary-pointer filename.
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 */