semantifold 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/bin/semantifold.js +6 -0
- package/build/bin/semantifold.d.ts +3 -0
- package/build/bin/semantifold.d.ts.map +1 -0
- package/build/bin/semantifold.js +5 -0
- package/build/index.d.ts +8 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +9 -1
- package/build/src/artifact-path.d.ts +16 -0
- package/build/src/artifact-path.d.ts.map +1 -1
- package/build/src/artifact-path.js +29 -3
- package/build/src/backends/c-check.d.ts +7 -0
- package/build/src/backends/c-check.d.ts.map +1 -0
- package/build/src/backends/c-check.js +65 -0
- package/build/src/backends/check-plan.d.ts +96 -0
- package/build/src/backends/check-plan.d.ts.map +1 -0
- package/build/src/backends/check-plan.js +135 -0
- package/build/src/backends/cpp-check.d.ts +7 -0
- package/build/src/backends/cpp-check.d.ts.map +1 -0
- package/build/src/backends/cpp-check.js +59 -0
- package/build/src/backends/csharp-check.d.ts +7 -0
- package/build/src/backends/csharp-check.d.ts.map +1 -0
- package/build/src/backends/csharp-check.js +125 -0
- package/build/src/backends/csharp.d.ts +1 -0
- package/build/src/backends/csharp.d.ts.map +1 -1
- package/build/src/backends/csharp.js +5 -4
- package/build/src/backends/dart-check.d.ts +7 -0
- package/build/src/backends/dart-check.d.ts.map +1 -0
- package/build/src/backends/dart-check.js +103 -0
- package/build/src/backends/dart.d.ts +2 -0
- package/build/src/backends/dart.d.ts.map +1 -1
- package/build/src/backends/dart.js +5 -5
- package/build/src/backends/go-check.d.ts +7 -0
- package/build/src/backends/go-check.d.ts.map +1 -0
- package/build/src/backends/go-check.js +77 -0
- package/build/src/backends/go.d.ts +1 -0
- package/build/src/backends/go.d.ts.map +1 -1
- package/build/src/backends/go.js +3 -3
- package/build/src/backends/java-check.d.ts +21 -0
- package/build/src/backends/java-check.d.ts.map +1 -0
- package/build/src/backends/java-check.js +73 -0
- package/build/src/backends/javascript-check.d.ts +7 -0
- package/build/src/backends/javascript-check.d.ts.map +1 -0
- package/build/src/backends/javascript-check.js +29 -0
- package/build/src/backends/kotlin-check.d.ts +7 -0
- package/build/src/backends/kotlin-check.d.ts.map +1 -0
- package/build/src/backends/kotlin-check.js +31 -0
- package/build/src/backends/php-check.d.ts +7 -0
- package/build/src/backends/php-check.d.ts.map +1 -0
- package/build/src/backends/php-check.js +21 -0
- package/build/src/backends/program.d.ts +7 -0
- package/build/src/backends/program.d.ts.map +1 -1
- package/build/src/backends/program.js +16 -1
- package/build/src/backends/python-check.d.ts +7 -0
- package/build/src/backends/python-check.d.ts.map +1 -0
- package/build/src/backends/python-check.js +28 -0
- package/build/src/backends/ruby-check.d.ts +7 -0
- package/build/src/backends/ruby-check.d.ts.map +1 -0
- package/build/src/backends/ruby-check.js +21 -0
- package/build/src/backends/rust-check.d.ts +7 -0
- package/build/src/backends/rust-check.d.ts.map +1 -0
- package/build/src/backends/rust-check.js +65 -0
- package/build/src/backends/swift-check.d.ts +7 -0
- package/build/src/backends/swift-check.d.ts.map +1 -0
- package/build/src/backends/swift-check.js +77 -0
- package/build/src/backends/typescript-check.d.ts +7 -0
- package/build/src/backends/typescript-check.d.ts.map +1 -0
- package/build/src/backends/typescript-check.js +33 -0
- package/build/src/backends/zig-check.d.ts +7 -0
- package/build/src/backends/zig-check.d.ts.map +1 -0
- package/build/src/backends/zig-check.js +70 -0
- package/build/src/diagnostic.d.ts +13 -1
- package/build/src/diagnostic.d.ts.map +1 -1
- package/build/src/diagnostic.js +14 -2
- package/build/src/frontends/program.d.ts +6 -0
- package/build/src/frontends/program.d.ts.map +1 -1
- package/build/src/frontends/program.js +9 -1
- package/build/src/language-registry.d.ts +54 -0
- package/build/src/language-registry.d.ts.map +1 -1
- package/build/src/language-registry.js +107 -5
- package/build/src/project-build.d.ts +172 -0
- package/build/src/project-build.d.ts.map +1 -0
- package/build/src/project-build.js +320 -0
- package/build/src/project-cli.d.ts +40 -0
- package/build/src/project-cli.d.ts.map +1 -0
- package/build/src/project-cli.js +145 -0
- package/build/src/project-manifest.d.ts +148 -0
- package/build/src/project-manifest.d.ts.map +1 -0
- package/build/src/project-manifest.js +432 -0
- package/build/src/project-reporter.d.ts +81 -0
- package/build/src/project-reporter.d.ts.map +1 -0
- package/build/src/project-reporter.js +293 -0
- package/build/src/project-snapshot.d.ts +129 -0
- package/build/src/project-snapshot.d.ts.map +1 -0
- package/build/src/project-snapshot.js +362 -0
- package/build/src/project-watch-reporter.d.ts +125 -0
- package/build/src/project-watch-reporter.d.ts.map +1 -0
- package/build/src/project-watch-reporter.js +280 -0
- package/build/src/project-watch.d.ts +133 -0
- package/build/src/project-watch.d.ts.map +1 -0
- package/build/src/project-watch.js +839 -0
- package/build/src/publication.d.ts +69 -5
- package/build/src/publication.d.ts.map +1 -1
- package/build/src/publication.js +246 -57
- package/build/src/semantic/types.d.ts +303 -2
- package/build/src/semantic/types.d.ts.map +1 -1
- package/build/src/semantic/types.js +79 -2
- package/build/src/subprocess.d.ts +8 -0
- package/build/src/subprocess.d.ts.map +1 -1
- package/build/src/subprocess.js +50 -9
- package/build/src/target-check.d.ts +52 -0
- package/build/src/target-check.d.ts.map +1 -0
- package/build/src/target-check.js +796 -0
- package/build/src/toolchains.d.ts.map +1 -1
- package/build/src/toolchains.js +3 -3
- package/docs/architecture.md +3 -1
- package/docs/c.md +4 -0
- package/docs/cpp.md +4 -0
- package/docs/dart.md +4 -0
- package/docs/generated-artifact-publication.md +11 -8
- package/docs/kotlin.md +2 -2
- package/docs/language-support.md +29 -3
- package/docs/project-build-cli.md +147 -0
- package/docs/project-watch.md +60 -0
- package/docs/rust.md +4 -0
- package/docs/swift.md +2 -0
- package/docs/testing.md +70 -3
- package/docs/watch-build-pipeline.md +39 -23
- package/docs/zig.md +2 -0
- package/examples/jsdoc-java-watch/expected-output.txt +3 -0
- package/examples/jsdoc-java-watch/semantifold.json +23 -0
- package/examples/jsdoc-java-watch/src/main.js +21 -0
- package/index.js +8 -0
- package/package.json +6 -1
- package/src/artifact-path.js +31 -2
- package/src/backends/c-check.js +80 -0
- package/src/backends/check-plan.js +147 -0
- package/src/backends/cpp-check.js +73 -0
- package/src/backends/csharp-check.js +139 -0
- package/src/backends/csharp.js +4 -3
- package/src/backends/dart-check.js +117 -0
- package/src/backends/dart.js +4 -4
- package/src/backends/go-check.js +89 -0
- package/src/backends/go.js +2 -2
- package/src/backends/java-check.js +77 -0
- package/src/backends/javascript-check.js +33 -0
- package/src/backends/kotlin-check.js +41 -0
- package/src/backends/php-check.js +23 -0
- package/src/backends/program.js +17 -0
- package/src/backends/python-check.js +30 -0
- package/src/backends/ruby-check.js +23 -0
- package/src/backends/rust-check.js +78 -0
- package/src/backends/swift-check.js +89 -0
- package/src/backends/typescript-check.js +37 -0
- package/src/backends/zig-check.js +81 -0
- package/src/diagnostic.js +9 -1
- package/src/frontends/program.js +9 -0
- package/src/language-registry.js +113 -4
- package/src/project-build.js +353 -0
- package/src/project-cli.js +155 -0
- package/src/project-manifest.js +479 -0
- package/src/project-reporter.js +320 -0
- package/src/project-snapshot.js +403 -0
- package/src/project-watch-reporter.js +303 -0
- package/src/project-watch.js +870 -0
- package/src/publication.js +275 -58
- package/src/semantic/types.js +87 -1
- package/src/subprocess.js +49 -8
- package/src/target-check.js +844 -0
- package/src/toolchains.js +3 -2
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,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
|
+
}
|