semantifold 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +6 -3
  2. package/bin/semantifold.js +6 -0
  3. package/build/bin/semantifold.d.ts +3 -0
  4. package/build/bin/semantifold.d.ts.map +1 -0
  5. package/build/bin/semantifold.js +5 -0
  6. package/build/index.d.ts +8 -0
  7. package/build/index.d.ts.map +1 -1
  8. package/build/index.js +9 -1
  9. package/build/src/artifact-path.d.ts +16 -0
  10. package/build/src/artifact-path.d.ts.map +1 -1
  11. package/build/src/artifact-path.js +29 -3
  12. package/build/src/backends/c-check.d.ts +7 -0
  13. package/build/src/backends/c-check.d.ts.map +1 -0
  14. package/build/src/backends/c-check.js +65 -0
  15. package/build/src/backends/check-plan.d.ts +96 -0
  16. package/build/src/backends/check-plan.d.ts.map +1 -0
  17. package/build/src/backends/check-plan.js +135 -0
  18. package/build/src/backends/cpp-check.d.ts +7 -0
  19. package/build/src/backends/cpp-check.d.ts.map +1 -0
  20. package/build/src/backends/cpp-check.js +59 -0
  21. package/build/src/backends/csharp-check.d.ts +7 -0
  22. package/build/src/backends/csharp-check.d.ts.map +1 -0
  23. package/build/src/backends/csharp-check.js +125 -0
  24. package/build/src/backends/csharp.d.ts +1 -0
  25. package/build/src/backends/csharp.d.ts.map +1 -1
  26. package/build/src/backends/csharp.js +5 -4
  27. package/build/src/backends/dart-check.d.ts +7 -0
  28. package/build/src/backends/dart-check.d.ts.map +1 -0
  29. package/build/src/backends/dart-check.js +103 -0
  30. package/build/src/backends/dart.d.ts +2 -0
  31. package/build/src/backends/dart.d.ts.map +1 -1
  32. package/build/src/backends/dart.js +5 -5
  33. package/build/src/backends/go-check.d.ts +7 -0
  34. package/build/src/backends/go-check.d.ts.map +1 -0
  35. package/build/src/backends/go-check.js +77 -0
  36. package/build/src/backends/go.d.ts +1 -0
  37. package/build/src/backends/go.d.ts.map +1 -1
  38. package/build/src/backends/go.js +3 -3
  39. package/build/src/backends/java-check.d.ts +21 -0
  40. package/build/src/backends/java-check.d.ts.map +1 -0
  41. package/build/src/backends/java-check.js +73 -0
  42. package/build/src/backends/javascript-check.d.ts +7 -0
  43. package/build/src/backends/javascript-check.d.ts.map +1 -0
  44. package/build/src/backends/javascript-check.js +29 -0
  45. package/build/src/backends/kotlin-check.d.ts +7 -0
  46. package/build/src/backends/kotlin-check.d.ts.map +1 -0
  47. package/build/src/backends/kotlin-check.js +31 -0
  48. package/build/src/backends/php-check.d.ts +7 -0
  49. package/build/src/backends/php-check.d.ts.map +1 -0
  50. package/build/src/backends/php-check.js +21 -0
  51. package/build/src/backends/program.d.ts +7 -0
  52. package/build/src/backends/program.d.ts.map +1 -1
  53. package/build/src/backends/program.js +16 -1
  54. package/build/src/backends/python-check.d.ts +7 -0
  55. package/build/src/backends/python-check.d.ts.map +1 -0
  56. package/build/src/backends/python-check.js +28 -0
  57. package/build/src/backends/ruby-check.d.ts +7 -0
  58. package/build/src/backends/ruby-check.d.ts.map +1 -0
  59. package/build/src/backends/ruby-check.js +21 -0
  60. package/build/src/backends/rust-check.d.ts +7 -0
  61. package/build/src/backends/rust-check.d.ts.map +1 -0
  62. package/build/src/backends/rust-check.js +65 -0
  63. package/build/src/backends/swift-check.d.ts +7 -0
  64. package/build/src/backends/swift-check.d.ts.map +1 -0
  65. package/build/src/backends/swift-check.js +77 -0
  66. package/build/src/backends/typescript-check.d.ts +7 -0
  67. package/build/src/backends/typescript-check.d.ts.map +1 -0
  68. package/build/src/backends/typescript-check.js +33 -0
  69. package/build/src/backends/zig-check.d.ts +7 -0
  70. package/build/src/backends/zig-check.d.ts.map +1 -0
  71. package/build/src/backends/zig-check.js +70 -0
  72. package/build/src/diagnostic.d.ts +13 -1
  73. package/build/src/diagnostic.d.ts.map +1 -1
  74. package/build/src/diagnostic.js +14 -2
  75. package/build/src/frontends/program.d.ts +6 -0
  76. package/build/src/frontends/program.d.ts.map +1 -1
  77. package/build/src/frontends/program.js +9 -1
  78. package/build/src/language-registry.d.ts +54 -0
  79. package/build/src/language-registry.d.ts.map +1 -1
  80. package/build/src/language-registry.js +107 -5
  81. package/build/src/project-build.d.ts +172 -0
  82. package/build/src/project-build.d.ts.map +1 -0
  83. package/build/src/project-build.js +320 -0
  84. package/build/src/project-cli.d.ts +40 -0
  85. package/build/src/project-cli.d.ts.map +1 -0
  86. package/build/src/project-cli.js +145 -0
  87. package/build/src/project-manifest.d.ts +148 -0
  88. package/build/src/project-manifest.d.ts.map +1 -0
  89. package/build/src/project-manifest.js +432 -0
  90. package/build/src/project-reporter.d.ts +81 -0
  91. package/build/src/project-reporter.d.ts.map +1 -0
  92. package/build/src/project-reporter.js +293 -0
  93. package/build/src/project-snapshot.d.ts +129 -0
  94. package/build/src/project-snapshot.d.ts.map +1 -0
  95. package/build/src/project-snapshot.js +362 -0
  96. package/build/src/project-watch-reporter.d.ts +125 -0
  97. package/build/src/project-watch-reporter.d.ts.map +1 -0
  98. package/build/src/project-watch-reporter.js +280 -0
  99. package/build/src/project-watch.d.ts +133 -0
  100. package/build/src/project-watch.d.ts.map +1 -0
  101. package/build/src/project-watch.js +839 -0
  102. package/build/src/publication.d.ts +69 -5
  103. package/build/src/publication.d.ts.map +1 -1
  104. package/build/src/publication.js +246 -57
  105. package/build/src/semantic/types.d.ts +303 -2
  106. package/build/src/semantic/types.d.ts.map +1 -1
  107. package/build/src/semantic/types.js +79 -2
  108. package/build/src/subprocess.d.ts +8 -0
  109. package/build/src/subprocess.d.ts.map +1 -1
  110. package/build/src/subprocess.js +50 -9
  111. package/build/src/target-check.d.ts +52 -0
  112. package/build/src/target-check.d.ts.map +1 -0
  113. package/build/src/target-check.js +796 -0
  114. package/build/src/toolchains.d.ts.map +1 -1
  115. package/build/src/toolchains.js +3 -3
  116. package/docs/architecture.md +3 -1
  117. package/docs/c.md +4 -0
  118. package/docs/cpp.md +4 -0
  119. package/docs/dart.md +4 -0
  120. package/docs/generated-artifact-publication.md +11 -8
  121. package/docs/kotlin.md +2 -2
  122. package/docs/language-support.md +29 -3
  123. package/docs/project-build-cli.md +147 -0
  124. package/docs/project-watch.md +60 -0
  125. package/docs/rust.md +4 -0
  126. package/docs/swift.md +2 -0
  127. package/docs/testing.md +70 -3
  128. package/docs/watch-build-pipeline.md +39 -23
  129. package/docs/zig.md +2 -0
  130. package/examples/jsdoc-java-watch/expected-output.txt +3 -0
  131. package/examples/jsdoc-java-watch/semantifold.json +23 -0
  132. package/examples/jsdoc-java-watch/src/main.js +21 -0
  133. package/index.js +8 -0
  134. package/package.json +6 -1
  135. package/src/artifact-path.js +31 -2
  136. package/src/backends/c-check.js +80 -0
  137. package/src/backends/check-plan.js +147 -0
  138. package/src/backends/cpp-check.js +73 -0
  139. package/src/backends/csharp-check.js +139 -0
  140. package/src/backends/csharp.js +4 -3
  141. package/src/backends/dart-check.js +117 -0
  142. package/src/backends/dart.js +4 -4
  143. package/src/backends/go-check.js +89 -0
  144. package/src/backends/go.js +2 -2
  145. package/src/backends/java-check.js +77 -0
  146. package/src/backends/javascript-check.js +33 -0
  147. package/src/backends/kotlin-check.js +41 -0
  148. package/src/backends/php-check.js +23 -0
  149. package/src/backends/program.js +17 -0
  150. package/src/backends/python-check.js +30 -0
  151. package/src/backends/ruby-check.js +23 -0
  152. package/src/backends/rust-check.js +78 -0
  153. package/src/backends/swift-check.js +89 -0
  154. package/src/backends/typescript-check.js +37 -0
  155. package/src/backends/zig-check.js +81 -0
  156. package/src/diagnostic.js +9 -1
  157. package/src/frontends/program.js +9 -0
  158. package/src/language-registry.js +113 -4
  159. package/src/project-build.js +353 -0
  160. package/src/project-cli.js +155 -0
  161. package/src/project-manifest.js +479 -0
  162. package/src/project-reporter.js +320 -0
  163. package/src/project-snapshot.js +403 -0
  164. package/src/project-watch-reporter.js +303 -0
  165. package/src/project-watch.js +870 -0
  166. package/src/publication.js +275 -58
  167. package/src/semantic/types.js +87 -1
  168. package/src/subprocess.js +49 -8
  169. package/src/target-check.js +844 -0
  170. package/src/toolchains.js +3 -2
  171. package/tsconfig.json +1 -1
@@ -0,0 +1,844 @@
1
+ // @ts-check
2
+
3
+ import {lstat, mkdir, readdir, rm, rmdir} from "node:fs/promises"
4
+ import path from "node:path"
5
+ import {isDenseArray} from "./array.js"
6
+ import {createGeneratedArtifactSet} from "./artifacts.js"
7
+ import {SemantifoldDiagnostic} from "./diagnostic.js"
8
+ import {languageRegistry} from "./language-registry.js"
9
+ import {cancelledOwnedProcess, exceededOwnedDeadline, executeFileWithDeadline} from "./subprocess.js"
10
+ import {isSupportedTimeoutMs, maximumTimeoutMs} from "./timeout.js"
11
+ import {deterministicEnvironment} from "./toolchains.js"
12
+
13
+ const defaultTimeoutMs = 20_000
14
+ const maximumOutputBytes = 1024 * 1024
15
+ const identityPattern = /^[a-z][a-z0-9-]*$/u
16
+ const mediaTypePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+(?:;[\u0020-\u007e]+)?$/u
17
+ const rolePattern = /^[a-z][a-z0-9-]*$/u
18
+ const launchErrorCodes = new Set([
19
+ "E2BIG", "EACCES", "EAGAIN", "ELOOP", "EMFILE", "ENAMETOOLONG", "ENFILE", "ENOENT", "ENOEXEC", "ENOMEM",
20
+ "ENOTDIR", "ENOTSUP", "EPERM", "ETXTBSY"
21
+ ])
22
+ /** @type {WeakMap<TargetCheckRunner, {execute: typeof executeFileWithDeadline, now: () => number}>} */
23
+ const targetCheckRunnerDependencies = new WeakMap()
24
+
25
+ /**
26
+ * Creates and validates one target-owned developer check plan.
27
+ * @param {object} input - Exact staged target context.
28
+ * @param {import("./semantic/types.js").GeneratedArtifactSet} input.artifacts - Validated staged artifacts.
29
+ * @param {string} input.buildPath - Exact generation-scoped target build subtree.
30
+ * @param {string} input.projectId - Exact project identity.
31
+ * @param {string} input.sourcePath - Exact generation-scoped target source subtree.
32
+ * @param {string} input.targetId - Exact project target identity.
33
+ * @param {readonly import("./semantic/types.js").DiscoveredToolchain[]} input.tools - Discovered immutable tool records.
34
+ * @returns {import("./semantic/types.js").TargetCheckPlan} Validated immutable plan.
35
+ */
36
+ export function createTargetCheckPlan(input) {
37
+ if (!isPlainObject(input) || !hasExactKeys(input, ["artifacts", "buildPath", "projectId", "sourcePath", "targetId", "tools"])) {
38
+ return invalidPlan("Target check planning requires only staged artifacts, candidate roots, identities, and tools.")
39
+ }
40
+ let artifacts
41
+
42
+ try {
43
+ artifacts = createGeneratedArtifactSet(input.artifacts)
44
+ } catch (error) {
45
+ return invalidPlan("Target check planning requires a valid generated artifact set.", "check", error)
46
+ }
47
+ const target = artifacts.target
48
+ const record = languageRegistry.record(target)
49
+ const capability = record.check
50
+
51
+ if (!capability.supported || typeof capability.factory != "function") {
52
+ throw new SemantifoldDiagnostic({
53
+ code: "UNSUPPORTED_TARGET_CHECK",
54
+ language: target,
55
+ message: "Registered target does not provide a developer check plan."
56
+ })
57
+ }
58
+ if (typeof input.projectId != "string" || !identityPattern.test(input.projectId) ||
59
+ typeof input.targetId != "string" || !identityPattern.test(input.targetId) ||
60
+ typeof input.sourcePath != "string" || !path.isAbsolute(input.sourcePath) ||
61
+ typeof input.buildPath != "string" || !path.isAbsolute(input.buildPath) ||
62
+ pathsOverlap(input.sourcePath, input.buildPath) || !isDenseArray(input.tools)) {
63
+ return invalidPlan("Target check planning received invalid identities, candidate roots, or tools.", target)
64
+ }
65
+ /** @type {import("./semantic/types.js").DiscoveredToolchain[]} */
66
+ const tools = []
67
+
68
+ for (let index = 0; index < input.tools.length; index += 1) {
69
+ const tool = input.tools[index]
70
+
71
+ if (!isDiscoveredTool(tool) || !Object.isFrozen(tool) || !Object.isFrozen(tool.versionArguments)) {
72
+ return invalidPlan("Target check planning requires immutable discovered tool records.", target)
73
+ }
74
+ tools.push(tool)
75
+ }
76
+ if (tools.map(({id}) => id).join("\0") != capability.toolchains.join("\0")) {
77
+ return invalidPlan("Target check planning tools must exactly match the target-declared developer-check toolchains.", target)
78
+ }
79
+ const context = Object.freeze({
80
+ artifacts,
81
+ buildPath: input.buildPath,
82
+ projectId: input.projectId,
83
+ sourcePath: input.sourcePath,
84
+ targetId: input.targetId,
85
+ tools: Object.freeze(tools)
86
+ })
87
+ let plan
88
+
89
+ try {
90
+ plan = capability.factory(context)
91
+ } catch (error) {
92
+ if (error instanceof SemantifoldDiagnostic) throw error
93
+ return invalidPlan("Target check plan factory failed.", target, error)
94
+ }
95
+ const validated = validateTargetCheckPlan(plan)
96
+ const expectedArtifacts = new Set(context.artifacts.artifacts.map(artifact => path.join(context.sourcePath, artifact.path)))
97
+
98
+ if (validated.projectId != context.projectId || validated.targetId != context.targetId ||
99
+ validated.target != target || validated.sourcePath != context.sourcePath || validated.buildPath != context.buildPath ||
100
+ validated.artifactPaths.length != expectedArtifacts.size || validated.artifactPaths.some(artifact => !expectedArtifacts.has(artifact))) {
101
+ return invalidPlan("Target check plan changed its validated candidate context.", target)
102
+ }
103
+
104
+ return validated
105
+ }
106
+
107
+ /**
108
+ * Owns ordered check subprocesses and settles only after each child closes.
109
+ */
110
+ export class TargetCheckRunner {
111
+ /**
112
+ * Runs one already-staged immutable plan without executing generated application code.
113
+ * @param {import("./semantic/types.js").TargetCheckPlan} plan - Target-owned plan.
114
+ * @param {{onStage?: (result: import("./semantic/types.js").TargetCheckStageResult) => void, signal?: AbortSignal, timeoutMs?: number}} [options] - Lifecycle controls.
115
+ * @returns {Promise<import("./semantic/types.js").TargetCheckResult>} Successful check evidence and output metadata.
116
+ */
117
+ async run(plan, options = {}) {
118
+ const validated = validateTargetCheckPlan(plan)
119
+ const dependencies = targetCheckRunnerDependencies.get(this) ?? {
120
+ execute: executeFileWithDeadline,
121
+ now: () => performance.now()
122
+ }
123
+
124
+ if (!isPlainObject(options) || options.onStage !== undefined && typeof options.onStage != "function" ||
125
+ options.signal !== undefined && !(options.signal instanceof AbortSignal)) {
126
+ return invalidPlan("Target check runner options are invalid.", validated.target)
127
+ }
128
+ const timeoutMs = options.timeoutMs === undefined ? defaultTimeoutMs : options.timeoutMs
129
+
130
+ if (!isSupportedTimeoutMs(timeoutMs)) {
131
+ return invalidPlan(`Target check timeout must be from 1 through ${maximumTimeoutMs}ms.`, validated.target)
132
+ }
133
+ /** @type {import("./semantic/types.js").TargetCheckStageResult[]} */
134
+ const results = []
135
+ /** @type {unknown} */
136
+ let stageFailure
137
+ let stageFailed = false
138
+
139
+ try {
140
+ for (const stage of validated.stages) {
141
+ const startedAt = dependencies.now()
142
+ /** @type {import("./semantic/types.js").TargetCheckStageResult} */
143
+ let stageResult
144
+
145
+ if (options.signal?.aborted) throw cancelledDiagnostic(stage, validated, 0)
146
+ try {
147
+ await prepareStage(stage, validated)
148
+ } catch (error) {
149
+ throw preparationDiagnostic(error, stage, validated, elapsed(dependencies.now(), startedAt))
150
+ }
151
+ try {
152
+ const output = await dependencies.execute({
153
+ arguments: stage.argv,
154
+ cwd: stage.cwd,
155
+ environment: stage.environment,
156
+ executable: stage.executable,
157
+ maxBuffer: maximumOutputBytes,
158
+ signal: options.signal,
159
+ timeoutMs
160
+ })
161
+ stageResult = Object.freeze({
162
+ argv: stage.argv,
163
+ durationMs: elapsed(dependencies.now(), startedAt),
164
+ exitCode: 0,
165
+ signal: null,
166
+ stage: stage.stage,
167
+ stderr: output.stderr,
168
+ stdout: output.stdout,
169
+ tool: Object.freeze({
170
+ command: stage.tool.command,
171
+ executable: stage.tool.executable,
172
+ id: stage.tool.id,
173
+ source: stage.tool.source,
174
+ version: stage.tool.version
175
+ })
176
+ })
177
+
178
+ } catch (error) {
179
+ throw processDiagnostic(error, stage, validated, timeoutMs, elapsed(dependencies.now(), startedAt))
180
+ }
181
+ results.push(stageResult)
182
+ options.onStage?.(stageResult)
183
+ }
184
+ } catch (error) {
185
+ stageFailed = true
186
+ stageFailure = error
187
+ }
188
+ try {
189
+ await cleanupStageEnvironment(validated)
190
+ } catch (error) {
191
+ const cleanup = cleanupDiagnostic(error, validated)
192
+
193
+ if (stageFailed && stageFailure instanceof Error) throw preservePrimaryFailure(stageFailure, cleanup)
194
+ if (stageFailed) throw new AggregateError([stageFailure, cleanup],
195
+ "Target check failed and its owned transient environment could not be cleaned.", {cause: error})
196
+ throw cleanup
197
+ }
198
+ if (stageFailed) throw stageFailure
199
+ const output = validated.stages.findLast(stage => stage.output !== null)?.output
200
+ const outputs = output == null ? [] : await collectBuildOutputs(validated.buildPath, output)
201
+
202
+ return Object.freeze({
203
+ outputs: Object.freeze(outputs),
204
+ projectId: validated.projectId,
205
+ stages: Object.freeze(results),
206
+ target: validated.target,
207
+ targetId: validated.targetId
208
+ })
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Creates a runner with focused instance-owned dependencies.
214
+ * @param {{execute: typeof executeFileWithDeadline, now: () => number}} dependencies - Testable lifecycle collaborators.
215
+ * @returns {TargetCheckRunner} Runner.
216
+ */
217
+ export function createTargetCheckRunner(dependencies) {
218
+ if (!isPlainObject(dependencies) || typeof dependencies.execute != "function" || typeof dependencies.now != "function") {
219
+ throw new TypeError("Invalid target check runner dependencies.")
220
+ }
221
+ const runner = new TargetCheckRunner()
222
+
223
+ targetCheckRunnerDependencies.set(runner, dependencies)
224
+
225
+ return runner
226
+ }
227
+
228
+ /**
229
+ * Validates a complete immutable plan before any process starts.
230
+ * @param {unknown} candidate - Candidate plan.
231
+ * @returns {import("./semantic/types.js").TargetCheckPlan} Exact validated plan.
232
+ */
233
+ export function validateTargetCheckPlan(candidate) {
234
+ if (!isPlainObject(candidate) || !Object.isFrozen(candidate) || !hasExactKeys(candidate, [
235
+ "artifactPaths", "buildPath", "mode", "projectId", "schema", "sourcePath", "stages", "target", "targetId", "version"
236
+ ])) return invalidPlan("Target check plan must be one immutable closed schema record.")
237
+ const target = typeof candidate.target == "string" ? candidate.target : "check"
238
+ const record = languageRegistry.record(target)
239
+ const capability = record.check
240
+
241
+ if (!capability.supported || candidate.schema != "SemantifoldTargetCheckPlan" || candidate.version != 1 ||
242
+ candidate.mode != "developer-check" || typeof candidate.projectId != "string" || !identityPattern.test(candidate.projectId) ||
243
+ typeof candidate.targetId != "string" || !identityPattern.test(candidate.targetId) ||
244
+ typeof candidate.sourcePath != "string" || !path.isAbsolute(candidate.sourcePath) ||
245
+ typeof candidate.buildPath != "string" || !path.isAbsolute(candidate.buildPath) ||
246
+ pathsOverlap(candidate.sourcePath, candidate.buildPath)) {
247
+ return invalidPlan("Target check plan has invalid schema, mode, identity, target, or candidate roots.", target)
248
+ }
249
+ if (!isDenseArray(candidate.artifactPaths) || !Object.isFrozen(candidate.artifactPaths) || candidate.artifactPaths.length == 0 ||
250
+ !isDenseArray(candidate.stages) || !Object.isFrozen(candidate.stages) || candidate.stages.length == 0) {
251
+ return invalidPlan("Target check plan requires dense immutable artifact and stage vectors.", target)
252
+ }
253
+ const artifactPaths = new Set()
254
+
255
+ for (let index = 0; index < candidate.artifactPaths.length; index += 1) {
256
+ const artifactPath = candidate.artifactPaths[index]
257
+
258
+ if (typeof artifactPath != "string" || !ownedBy(candidate.sourcePath, artifactPath) || artifactPaths.has(artifactPath)) {
259
+ return invalidPlan("Every planned artifact must be a distinct absolute path inside the candidate source subtree.", target)
260
+ }
261
+ artifactPaths.add(artifactPath)
262
+ }
263
+ const plannedStageKinds = candidate.stages.map(stage => isPlainObject(stage) && typeof stage.stage == "string" ? stage.stage : "")
264
+ .filter((stage, index, stages) => index == 0 || stage != stages[index - 1])
265
+
266
+ if (plannedStageKinds.join("\0") != capability.stages.join("\0")) {
267
+ return invalidPlan("Target check stages must exactly match the target-declared developer-check stages.", target)
268
+ }
269
+ const usedTools = new Set()
270
+ const referencedArtifactPaths = new Set()
271
+
272
+ for (let index = 0; index < candidate.stages.length; index += 1) {
273
+ const stage = candidate.stages[index]
274
+ if (!isPlainObject(stage) || !Object.isFrozen(stage) || !hasExactKeys(stage, [
275
+ "argv", "cwd", "environment", "environmentPaths", "executable", "inputHash", "inputs", "output", "pathArguments",
276
+ "stage", "tool", "transientPaths"
277
+ ]) || typeof stage.stage != "string" || stage.stage == "execute" ||
278
+ !record.acceptance.stages.includes(/** @type {import("./semantic/types.js").AcceptanceStage} */ (stage.stage))) {
279
+ return invalidPlan("Target check stages must be immutable, ordered, declared, and non-executing.", target)
280
+ }
281
+ const request = /** @type {import("./semantic/types.js").TargetCheckStageRequest} */ (stage)
282
+
283
+ if (!isDiscoveredTool(request.tool) || !Object.isFrozen(request.tool) || !Object.isFrozen(request.tool.versionArguments) ||
284
+ request.executable != request.tool.executable || !capability.toolchains.includes(request.tool.id)) {
285
+ return invalidPlan(`Target check stage '${request.stage}' uses an invalid or undeclared tool.`, target)
286
+ }
287
+ usedTools.add(request.tool.id)
288
+ if (!isDenseArray(request.argv) || !Object.isFrozen(request.argv) ||
289
+ !request.argv.every(argument => typeof argument == "string" && !argument.includes("\0")) ||
290
+ !isDenseArray(request.pathArguments) || !Object.isFrozen(request.pathArguments)) {
291
+ return invalidPlan(`Target check stage '${request.stage}' requires a dense immutable shell-free argv.`, target)
292
+ }
293
+ if (!validEnvironment(request.environment)) {
294
+ return invalidPlan(`Target check stage '${request.stage}' requires an immutable deterministic environment.`, target)
295
+ }
296
+ if (!validEnvironmentPaths(request.environmentPaths, request.environment, candidate.sourcePath, candidate.buildPath,
297
+ capability.toolchains, usedTools)) {
298
+ return invalidPlan(`Target check stage '${request.stage}' has invalid environment-path ownership.`, target)
299
+ }
300
+ if (typeof request.cwd != "string" || !ownedByEither(candidate.sourcePath, candidate.buildPath, request.cwd)) {
301
+ return invalidPlan(`Target check stage '${request.stage}' working directory escapes the candidate target subtrees.`, target)
302
+ }
303
+ if (request.output !== null && !validOutput(request.output, candidate.buildPath)) {
304
+ return invalidPlan(`Target check stage '${request.stage}' has invalid compiler-output ownership.`, target)
305
+ }
306
+ if (!validTransientPaths(request.transientPaths, candidate.sourcePath, candidate.buildPath, artifactPaths)) {
307
+ return invalidPlan(`Target check stage '${request.stage}' has invalid transient-path ownership.`, target)
308
+ }
309
+ if (request.stage == "restore" ? typeof request.inputHash != "string" || !/^[a-f0-9]{64}$/u.test(request.inputHash) : request.inputHash !== null) {
310
+ return invalidPlan(`Target check stage '${request.stage}' has invalid restore-input identity.`, target)
311
+ }
312
+ if (!isDenseArray(request.inputs) || !Object.isFrozen(request.inputs) || request.inputs.length == 0) {
313
+ return invalidPlan(`Target check stage '${request.stage}' requires immutable declared inputs.`, target)
314
+ }
315
+ const stageInputs = new Set()
316
+
317
+ for (let inputIndex = 0; inputIndex < request.inputs.length; inputIndex += 1) {
318
+ const inputPath = request.inputs[inputIndex]
319
+
320
+ if (typeof inputPath != "string" || stageInputs.has(inputPath) || !ownedByEither(candidate.sourcePath, candidate.buildPath, inputPath)) {
321
+ return invalidPlan(`Target check stage '${request.stage}' has invalid or duplicate declared inputs.`, target)
322
+ }
323
+ if (ownedBy(candidate.sourcePath, inputPath)) {
324
+ if (!artifactPaths.has(inputPath)) {
325
+ return invalidPlan(`Target check stage '${request.stage}' reads an undeclared staged artifact.`, target)
326
+ }
327
+ referencedArtifactPaths.add(inputPath)
328
+ }
329
+ stageInputs.add(inputPath)
330
+ }
331
+ const pathIndexes = new Set()
332
+ for (let pathIndex = 0; pathIndex < request.pathArguments.length; pathIndex += 1) {
333
+ const declaration = request.pathArguments[pathIndex]
334
+
335
+ if (!isPlainObject(declaration) || !Object.isFrozen(declaration) ||
336
+ !hasExactKeys(declaration, declaration.prefix === undefined ? ["index", "ownership"] : ["index", "ownership", "prefix"]) ||
337
+ typeof declaration.index != "number" || !Number.isInteger(declaration.index) || declaration.index < 0 ||
338
+ declaration.index >= request.argv.length ||
339
+ pathIndexes.has(declaration.index) || declaration.ownership != "source" && declaration.ownership != "build" ||
340
+ declaration.prefix !== undefined && (typeof declaration.prefix != "string" || declaration.prefix.length == 0 ||
341
+ declaration.prefix.includes("\0"))) {
342
+ return invalidPlan(`Target check stage '${request.stage}' has invalid path-argument ownership.`, target)
343
+ }
344
+ const rawArgument = request.argv[declaration.index]
345
+ const argument = declaration.prefix === undefined
346
+ ? rawArgument
347
+ : rawArgument.startsWith(declaration.prefix)
348
+ ? rawArgument.slice(declaration.prefix.length)
349
+ : ""
350
+ const root = declaration.ownership == "source" ? candidate.sourcePath : candidate.buildPath
351
+
352
+ if (argument != root && !ownedBy(root, argument)) {
353
+ return invalidPlan(`Target check stage '${request.stage}' path argument escapes its candidate subtree.`, target)
354
+ }
355
+ pathIndexes.add(declaration.index)
356
+ }
357
+ for (let argumentIndex = 0; argumentIndex < request.argv.length; argumentIndex += 1) {
358
+ if (path.isAbsolute(request.argv[argumentIndex]) && !pathIndexes.has(argumentIndex)) {
359
+ return invalidPlan(`Target check stage '${request.stage}' contains an unowned absolute path argument.`, target)
360
+ }
361
+ }
362
+ }
363
+ if (referencedArtifactPaths.size != artifactPaths.size) {
364
+ return invalidPlan("Target check plan must include every staged check artifact.", target)
365
+ }
366
+ if (capability.toolchains.some(toolchain => !usedTools.has(toolchain))) {
367
+ return invalidPlan("Target check plan omits a target-declared toolchain.", target)
368
+ }
369
+
370
+ return /** @type {import("./semantic/types.js").TargetCheckPlan} */ (candidate)
371
+ }
372
+
373
+ /**
374
+ * Collects every regular compiler output without following links.
375
+ * @param {string} buildPath - Exact owned build root.
376
+ * @param {import("./semantic/types.js").TargetCheckOutputOwnership} output - Output metadata.
377
+ * @returns {Promise<import("./semantic/types.js").PublicationBuildArtifactInput[]>} Ordered metadata.
378
+ */
379
+ async function collectBuildOutputs(buildPath, output) {
380
+ /** @type {string[]} */
381
+ const files = []
382
+ /** @type {{absolute: string, relative: string}[]} */
383
+ const pending = [{absolute: buildPath, relative: ""}]
384
+
385
+ while (pending.length > 0) {
386
+ const current = /** @type {{absolute: string, relative: string}} */ (pending.pop())
387
+ let entries
388
+
389
+ try {
390
+ entries = await readdir(current.absolute, {withFileTypes: true})
391
+ } catch (error) {
392
+ return outputFailure("Compiler output subtree could not be inspected.", error)
393
+ }
394
+ entries.sort((left, right) => left.name.localeCompare(right.name, "en"))
395
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
396
+ const entry = entries[index]
397
+ const relative = current.relative.length == 0 ? entry.name : `${current.relative}/${entry.name}`
398
+ const absolute = path.join(current.absolute, entry.name)
399
+
400
+ if (entry.isDirectory()) pending.push({absolute, relative})
401
+ else if (entry.isFile()) files.push(relative)
402
+ else return outputFailure(`Compiler output '${relative}' is not a regular file or directory.`)
403
+ }
404
+ }
405
+ files.sort((left, right) => left.localeCompare(right, "en"))
406
+ for (const filename of files) {
407
+ try {
408
+ if (!(await lstat(path.join(buildPath, filename))).isFile()) {
409
+ return outputFailure(`Compiler output '${filename}' is not a regular file.`)
410
+ }
411
+ } catch (error) {
412
+ return outputFailure(`Compiler output '${filename}' could not be inspected.`, error)
413
+ }
414
+ }
415
+
416
+ return files.map(filename => Object.freeze({mediaType: output.mediaType, path: filename, role: output.role}))
417
+ }
418
+
419
+ /**
420
+ * Normalizes one child failure while retaining complete bounded output.
421
+ * @param {unknown} error - Opaque process failure.
422
+ * @param {import("./semantic/types.js").TargetCheckStageRequest} stage - Failed stage.
423
+ * @param {import("./semantic/types.js").TargetCheckPlan} plan - Plan context.
424
+ * @param {number} timeoutMs - Exact timeout.
425
+ * @param {number} durationMs - Observed duration.
426
+ * @returns {SemantifoldDiagnostic} Structured check failure.
427
+ */
428
+ function processDiagnostic(error, stage, plan, timeoutMs, durationMs) {
429
+ const fields = processErrorFields(error)
430
+ const outputLimited = fields.nativeCode == "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
431
+ const cancelled = !outputLimited && cancelledOwnedProcess(error)
432
+ const timedOut = !outputLimited && !cancelled && exceededOwnedDeadline(error)
433
+ const launchFailure = fields.nativeCode != undefined && launchErrorCodes.has(fields.nativeCode)
434
+ const signaled = !outputLimited && !cancelled && !timedOut && fields.signal != undefined
435
+ const code = outputLimited ? "TARGET_CHECK_OUTPUT_LIMIT" : cancelled ? "TARGET_CHECK_CANCELLED" : timedOut
436
+ ? "TARGET_CHECK_TIMEOUT" : launchFailure ? "TARGET_CHECK_LAUNCH_FAILURE" : signaled
437
+ ? "TARGET_CHECK_SIGNAL" : "TARGET_CHECK_NONZERO_EXIT"
438
+ const reason = outputLimited ? `exceeded the ${maximumOutputBytes}-byte output limit` : cancelled
439
+ ? "was cancelled" : timedOut ? `timed out after ${timeoutMs}ms` : launchFailure
440
+ ? `could not launch (${fields.nativeCode})` : signaled ? `closed after signal ${fields.signal}`
441
+ : fields.exitCode == undefined ? "failed after launch" : `exited nonzero (${fields.exitCode})`
442
+
443
+ return new SemantifoldDiagnostic({
444
+ cause: outputLimited ? undefined : error instanceof Error ? error : undefined,
445
+ code,
446
+ durationMs,
447
+ executable: stage.executable,
448
+ exitCode: fields.exitCode,
449
+ language: plan.target,
450
+ message: `Target '${plan.targetId}' stage '${stage.stage}' ${reason} using '${stage.executable}' (${stage.tool.version}).`,
451
+ projectId: plan.projectId,
452
+ signal: fields.signal,
453
+ stage: stage.stage,
454
+ stderr: fields.stderr,
455
+ stdout: fields.stdout,
456
+ targetId: plan.targetId,
457
+ toolId: stage.tool.id,
458
+ version: stage.tool.version
459
+ })
460
+ }
461
+
462
+ /**
463
+ * Creates target-owned environment directories before launch without touching source staging.
464
+ * @param {import("./semantic/types.js").TargetCheckStageRequest} stage - Validated stage.
465
+ * @param {import("./semantic/types.js").TargetCheckPlan} plan - Validated plan context.
466
+ * @returns {Promise<void>} Completion.
467
+ */
468
+ async function prepareStage(stage, plan) {
469
+ for (const declaration of stage.environmentPaths) {
470
+ if (declaration.ownership == "build") {
471
+ await mkdir(stage.environment[declaration.name], {mode: 0o700, recursive: true})
472
+ }
473
+ }
474
+ for (const transientPath of stage.transientPaths) {
475
+ if (ownedBy(plan.buildPath, transientPath)) await mkdir(transientPath, {mode: 0o700, recursive: true})
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Removes only explicitly declared generation-owned transient cache/home directories after every child has closed.
481
+ * @param {import("./semantic/types.js").TargetCheckPlan} plan - Validated plan.
482
+ * @returns {Promise<void>} Completion.
483
+ */
484
+ async function cleanupStageEnvironment(plan) {
485
+ const owned = new Set()
486
+
487
+ for (const stage of plan.stages) {
488
+ for (const declaration of stage.environmentPaths) {
489
+ if (declaration.ownership == "build") owned.add(stage.environment[declaration.name])
490
+ }
491
+ for (const transientPath of stage.transientPaths) owned.add(transientPath)
492
+ }
493
+ const paths = [...owned].sort((left, right) => right.length - left.length || right.localeCompare(left, "en"))
494
+
495
+ for (const ownedPath of paths) {
496
+ await rm(ownedPath, {force: true, recursive: true})
497
+ await removeEmptyParents(path.dirname(ownedPath), ownedBy(plan.buildPath, ownedPath) ? plan.buildPath : plan.sourcePath)
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Removes empty transient ancestors without ever removing the candidate build root.
503
+ * @param {string} candidate - First possible empty ancestor.
504
+ * @param {string} root - Exact retained candidate root.
505
+ * @returns {Promise<void>} Completion.
506
+ */
507
+ async function removeEmptyParents(candidate, root) {
508
+ let current = candidate
509
+
510
+ while (ownedBy(root, current)) {
511
+ if ((await readdir(current)).length > 0) return
512
+ await rmdir(current)
513
+ current = path.dirname(current)
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Preserves cache/home preparation failures separately from tool launch or compilation.
519
+ * @param {unknown} error - Filesystem failure.
520
+ * @param {import("./semantic/types.js").TargetCheckStageRequest} stage - Failed stage.
521
+ * @param {import("./semantic/types.js").TargetCheckPlan} plan - Plan context.
522
+ * @param {number} durationMs - Observed duration.
523
+ * @returns {SemantifoldDiagnostic} Structured failure.
524
+ */
525
+ function preparationDiagnostic(error, stage, plan, durationMs) {
526
+ return new SemantifoldDiagnostic({
527
+ cause: error instanceof Error ? error : undefined,
528
+ code: "TARGET_CHECK_PREPARATION_FAILURE",
529
+ durationMs,
530
+ executable: stage.executable,
531
+ language: plan.target,
532
+ message: `Target '${plan.targetId}' stage '${stage.stage}' could not prepare its owned cache/home directories.`,
533
+ projectId: plan.projectId,
534
+ stage: stage.stage,
535
+ stderr: "",
536
+ stdout: "",
537
+ targetId: plan.targetId,
538
+ toolId: stage.tool.id,
539
+ version: stage.tool.version
540
+ })
541
+ }
542
+
543
+ /**
544
+ * Reports transient cache/home cleanup separately from native compiler diagnostics.
545
+ * @param {unknown} error - Filesystem failure.
546
+ * @param {import("./semantic/types.js").TargetCheckPlan} plan - Plan context.
547
+ * @returns {SemantifoldDiagnostic} Structured cleanup failure.
548
+ */
549
+ function cleanupDiagnostic(error, plan) {
550
+ return new SemantifoldDiagnostic({
551
+ cause: error instanceof Error ? error : undefined,
552
+ code: "TARGET_CHECK_CLEANUP_FAILURE",
553
+ language: plan.target,
554
+ message: `Target '${plan.targetId}' could not remove its owned transient cache/home directories.`,
555
+ projectId: plan.projectId,
556
+ targetId: plan.targetId
557
+ })
558
+ }
559
+
560
+ /**
561
+ * Retains the first plan/process diagnostic while attaching bounded cleanup evidence.
562
+ * @param {Error} primaryFailure - First check failure.
563
+ * @param {SemantifoldDiagnostic} cleanupFailure - Structured cleanup failure.
564
+ * @returns {Error} Original failure with cleanup evidence in its cause chain.
565
+ */
566
+ function preservePrimaryFailure(primaryFailure, cleanupFailure) {
567
+ const causes = primaryFailure.cause instanceof Error ? [primaryFailure.cause, cleanupFailure] : [cleanupFailure]
568
+
569
+ Object.defineProperty(primaryFailure, "cause", {
570
+ configurable: true,
571
+ value: new AggregateError(causes, "Target check failed and its owned transient environment cleanup also failed."),
572
+ writable: true
573
+ })
574
+
575
+ return primaryFailure
576
+ }
577
+
578
+ /**
579
+ * Creates a structured failure when cancellation precedes stage launch.
580
+ * @param {import("./semantic/types.js").TargetCheckStageRequest} stage - Cancelled stage.
581
+ * @param {import("./semantic/types.js").TargetCheckPlan} plan - Plan context.
582
+ * @param {number} durationMs - Observed duration.
583
+ * @returns {SemantifoldDiagnostic} Structured cancellation diagnostic.
584
+ */
585
+ function cancelledDiagnostic(stage, plan, durationMs) {
586
+ return new SemantifoldDiagnostic({
587
+ code: "TARGET_CHECK_CANCELLED",
588
+ durationMs,
589
+ executable: stage.executable,
590
+ language: plan.target,
591
+ message: `Target '${plan.targetId}' stage '${stage.stage}' was cancelled before launch.`,
592
+ projectId: plan.projectId,
593
+ stage: stage.stage,
594
+ stderr: "",
595
+ stdout: "",
596
+ targetId: plan.targetId,
597
+ toolId: stage.tool.id,
598
+ version: stage.tool.version
599
+ })
600
+ }
601
+
602
+ /**
603
+ * Extracts stable bounded child-process failure fields.
604
+ * @param {unknown} error - Opaque process rejection.
605
+ * @returns {{exitCode: number | undefined, nativeCode: string | undefined, signal: string | undefined, stderr: string, stdout: string}} Stable fields.
606
+ */
607
+ function processErrorFields(error) {
608
+ if (!error || typeof error != "object") return {exitCode: undefined, nativeCode: undefined, signal: undefined, stderr: "", stdout: ""}
609
+ const value = /** @type {Record<string, unknown>} */ (error)
610
+
611
+ return {
612
+ exitCode: typeof value.code == "number" ? value.code : undefined,
613
+ nativeCode: typeof value.code == "string" ? value.code : undefined,
614
+ signal: typeof value.signal == "string" ? value.signal : undefined,
615
+ stderr: typeof value.stderr == "string" ? value.stderr : "",
616
+ stdout: typeof value.stdout == "string" ? value.stdout : ""
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Checks the complete immutable discovered-tool shape consumed by a plan.
622
+ * @param {unknown} value - Candidate tool record.
623
+ * @returns {value is import("./semantic/types.js").DiscoveredToolchain} Whether the tool shape is valid.
624
+ */
625
+ function isDiscoveredTool(value) {
626
+ return isPlainObject(value) && typeof value.id == "string" && typeof value.command == "string" &&
627
+ typeof value.executable == "string" && path.isAbsolute(value.executable) && !value.executable.includes("\0") &&
628
+ (value.source == "canonical" || value.source == "override") && typeof value.version == "string" && value.version.length > 0 &&
629
+ typeof value.versionOutput == "string" && isDenseArray(value.versionArguments) &&
630
+ value.versionArguments.every(argument => typeof argument == "string" && !argument.includes("\0"))
631
+ }
632
+
633
+ /**
634
+ * Checks that an environment is immutable and already deterministic.
635
+ * @param {unknown} value - Candidate environment record.
636
+ * @returns {boolean} Whether the environment is normalized.
637
+ */
638
+ function validEnvironment(value) {
639
+ if (!isPlainRecord(value) || !Object.isFrozen(value)) return false
640
+ let normalized
641
+
642
+ try {
643
+ normalized = deterministicEnvironment(/** @type {Readonly<Record<string, string>>} */ (value))
644
+ } catch {
645
+ return false
646
+ }
647
+ const actual = Object.entries(value).sort(([left], [right]) => left.localeCompare(right, "en"))
648
+ const expected = Object.entries(normalized).sort(([left], [right]) => left.localeCompare(right, "en"))
649
+
650
+ return JSON.stringify(actual) == JSON.stringify(expected)
651
+ }
652
+
653
+ /**
654
+ * Checks every absolute non-PATH environment value against an explicit candidate-owned directory or discovered-tool declaration.
655
+ * @param {unknown} value - Candidate declarations.
656
+ * @param {Readonly<Record<string, string>>} environment - Validated deterministic environment.
657
+ * @param {string} sourcePath - Candidate source root.
658
+ * @param {string} buildPath - Candidate build root.
659
+ * @param {readonly string[]} toolchains - Target-declared toolchain IDs.
660
+ * @param {Set<string>} usedTools - Validated tools used by this plan.
661
+ * @returns {value is readonly import("./semantic/types.js").TargetCheckEnvironmentPath[]} Whether ownership is complete.
662
+ */
663
+ function validEnvironmentPaths(value, environment, sourcePath, buildPath, toolchains, usedTools) {
664
+ if (!isDenseArray(value) || !Object.isFrozen(value)) return false
665
+ const names = new Set()
666
+
667
+ for (let index = 0; index < value.length; index += 1) {
668
+ const declaration = value[index]
669
+
670
+ if (!isPlainObject(declaration) || !Object.isFrozen(declaration) ||
671
+ !hasExactKeys(declaration, declaration.ownership == "tool" ? ["name", "ownership", "tool"] : ["name", "ownership"]) ||
672
+ typeof declaration.name != "string" || names.has(declaration.name)) return false
673
+ const environmentPath = environment[declaration.name]
674
+
675
+ if (declaration.ownership == "tool") {
676
+ if (!isDiscoveredTool(declaration.tool) || !Object.isFrozen(declaration.tool) ||
677
+ !Object.isFrozen(declaration.tool.versionArguments) || !toolchains.includes(declaration.tool.id) ||
678
+ environmentPath != declaration.tool.executable) return false
679
+ usedTools.add(declaration.tool.id)
680
+ names.add(declaration.name)
681
+ continue
682
+ }
683
+ if (declaration.ownership != "source" && declaration.ownership != "build") return false
684
+ const root = declaration.ownership == "source" ? sourcePath : buildPath
685
+
686
+ if (typeof environmentPath != "string" || !ownedBy(root, environmentPath)) return false
687
+ names.add(declaration.name)
688
+ }
689
+ for (const [name, environmentValue] of Object.entries(environment)) {
690
+ if (name != "PATH" && path.isAbsolute(environmentValue) && !names.has(name)) return false
691
+ }
692
+
693
+ return true
694
+ }
695
+
696
+ /**
697
+ * Checks generation-owned transient cache/intermediate directories.
698
+ * @param {unknown} value - Candidate path vector.
699
+ * @param {string} sourcePath - Candidate source root.
700
+ * @param {string} buildPath - Candidate build root.
701
+ * @param {Set<string>} artifactPaths - Exact staged artifact paths.
702
+ * @returns {value is readonly string[]} Whether paths are immutable distinct candidate descendants.
703
+ */
704
+ function validTransientPaths(value, sourcePath, buildPath, artifactPaths) {
705
+ if (!isDenseArray(value) || !Object.isFrozen(value)) return false
706
+ const paths = new Set()
707
+
708
+ for (let index = 0; index < value.length; index += 1) {
709
+ const transientPath = value[index]
710
+
711
+ if (typeof transientPath != "string" || !ownedBy(sourcePath, transientPath) && !ownedBy(buildPath, transientPath) ||
712
+ paths.has(transientPath) || ownedBy(sourcePath, transientPath) && [...artifactPaths].some(artifactPath =>
713
+ transientPath == artifactPath || pathsOverlap(transientPath, artifactPath))) return false
714
+ paths.add(transientPath)
715
+ }
716
+
717
+ return true
718
+ }
719
+
720
+ /**
721
+ * Checks compiler-output metadata against the owned build root.
722
+ * @param {unknown} value - Candidate output record.
723
+ * @param {string} buildPath - Exact candidate build root.
724
+ * @returns {boolean} Whether output ownership is valid.
725
+ */
726
+ function validOutput(value, buildPath) {
727
+ return isPlainObject(value) && Object.isFrozen(value) && hasExactKeys(value, ["mediaType", "ownership", "path", "role"]) &&
728
+ value.ownership == "build" && value.path == buildPath && typeof value.mediaType == "string" &&
729
+ mediaTypePattern.test(value.mediaType) && typeof value.role == "string" && rolePattern.test(value.role)
730
+ }
731
+
732
+ /**
733
+ * Checks strict descendant ownership without accepting the root itself.
734
+ * @param {string} root - Absolute owned root.
735
+ * @param {string} candidate - Absolute candidate path.
736
+ * @returns {boolean} Whether the candidate is a strict descendant.
737
+ */
738
+ function ownedBy(root, candidate) {
739
+ if (!path.isAbsolute(candidate)) return false
740
+ const relative = path.relative(root, candidate)
741
+
742
+ return relative.length > 0 && !relative.startsWith(`..${path.sep}`) && relative != ".." && !path.isAbsolute(relative)
743
+ }
744
+
745
+ /**
746
+ * Checks candidate ownership by either exact target subtree.
747
+ * @param {string} sourcePath - Exact source root.
748
+ * @param {string} buildPath - Exact build root.
749
+ * @param {string} candidate - Candidate working directory.
750
+ * @returns {boolean} Whether either target subtree owns the candidate.
751
+ */
752
+ function ownedByEither(sourcePath, buildPath, candidate) {
753
+ return candidate == sourcePath || candidate == buildPath || ownedBy(sourcePath, candidate) || ownedBy(buildPath, candidate)
754
+ }
755
+
756
+ /**
757
+ * Checks whether two absolute roots overlap in either direction.
758
+ * @param {string} left - First root.
759
+ * @param {string} right - Second root.
760
+ * @returns {boolean} Whether the roots overlap.
761
+ */
762
+ function pathsOverlap(left, right) {
763
+ return left == right || ownedBy(left, right) || ownedBy(right, left)
764
+ }
765
+
766
+ /**
767
+ * Normalizes one monotonic duration observation.
768
+ * @param {number} endedAt - End observation.
769
+ * @param {number} startedAt - Start observation.
770
+ * @returns {number} Finite non-negative duration.
771
+ */
772
+ function elapsed(endedAt, startedAt) {
773
+ const duration = endedAt - startedAt
774
+
775
+ return Number.isFinite(duration) && duration >= 0 ? duration : 0
776
+ }
777
+
778
+ /**
779
+ * Throws one compiler-output inspection diagnostic.
780
+ * @param {string} message - Failure detail.
781
+ * @param {unknown} [cause] - Optional underlying failure.
782
+ * @returns {never} Always throws.
783
+ */
784
+ function outputFailure(message, cause) {
785
+ throw new SemantifoldDiagnostic({
786
+ cause: cause instanceof Error ? cause : undefined,
787
+ code: "TARGET_CHECK_OUTPUT_INVALID",
788
+ language: "check",
789
+ message
790
+ })
791
+ }
792
+
793
+ /**
794
+ * Throws one invalid-plan diagnostic.
795
+ * @param {string} message - Failure detail.
796
+ * @param {string} [target] - Target diagnostic identity.
797
+ * @param {unknown} [cause] - Optional underlying failure.
798
+ * @returns {never} Always throws.
799
+ */
800
+ function invalidPlan(message, target = "check", cause) {
801
+ throw new SemantifoldDiagnostic({
802
+ cause: cause instanceof Error ? cause : undefined,
803
+ code: "INVALID_TARGET_CHECK_PLAN",
804
+ language: target,
805
+ message
806
+ })
807
+ }
808
+
809
+ /**
810
+ * Checks for an ordinary object-prototype record.
811
+ * @param {unknown} value - Candidate value.
812
+ * @returns {value is Record<string, unknown>} Whether the value is a plain object.
813
+ */
814
+ function isPlainObject(value) {
815
+ return typeof value == "object" && value != null && !Array.isArray(value) && Object.getPrototypeOf(value) == Object.prototype
816
+ }
817
+
818
+ /**
819
+ * Checks for an own-property string-data record with an ordinary or null prototype.
820
+ * @param {unknown} value - Candidate value.
821
+ * @returns {value is Record<string, unknown>} Whether the value is a plain data record.
822
+ */
823
+ function isPlainRecord(value) {
824
+ if (typeof value != "object" || value == null || Array.isArray(value)) return false
825
+ const prototype = Object.getPrototypeOf(value)
826
+
827
+ if (prototype !== Object.prototype && prototype !== null) return false
828
+ return Reflect.ownKeys(value).every(key => {
829
+ if (typeof key != "string") return false
830
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)
831
+
832
+ return descriptor != undefined && descriptor.enumerable && "value" in descriptor && typeof descriptor.value == "string"
833
+ })
834
+ }
835
+
836
+ /**
837
+ * Checks one closed object-key set.
838
+ * @param {Record<string, unknown>} value - Candidate record.
839
+ * @param {string[]} keys - Exact allowed keys.
840
+ * @returns {boolean} Whether the key sets match.
841
+ */
842
+ function hasExactKeys(value, keys) {
843
+ return Object.keys(value).sort().join("\0") == [...keys].sort().join("\0")
844
+ }