semantifold 0.1.1 → 0.2.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 +13 -6
- package/build/index.d.ts +32 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +99 -4
- package/build/src/acceptance.d.ts +22 -0
- package/build/src/acceptance.d.ts.map +1 -0
- package/build/src/acceptance.js +339 -0
- package/build/src/array.d.ts +7 -0
- package/build/src/array.d.ts.map +1 -0
- package/build/src/array.js +18 -0
- package/build/src/artifact-path.d.ts +13 -0
- package/build/src/artifact-path.d.ts.map +1 -0
- package/build/src/artifact-path.js +30 -0
- package/build/src/artifacts.d.ts +7 -0
- package/build/src/artifacts.d.ts.map +1 -0
- package/build/src/artifacts.js +389 -0
- package/build/src/backends/identifiers.d.ts.map +1 -1
- package/build/src/backends/identifiers.js +10 -2
- package/build/src/backends/index.d.ts.map +1 -1
- package/build/src/backends/index.js +19 -37
- package/build/src/backends/python.d.ts +8 -0
- package/build/src/backends/python.d.ts.map +1 -0
- package/build/src/backends/python.js +162 -0
- package/build/src/backends/scalars.d.ts.map +1 -1
- package/build/src/backends/scalars.js +2 -1
- package/build/src/backends/shared.d.ts.map +1 -1
- package/build/src/backends/shared.js +9 -6
- package/build/src/binary-mapping.d.ts +32 -0
- package/build/src/binary-mapping.d.ts.map +1 -0
- package/build/src/binary-mapping.js +285 -0
- package/build/src/diagnostic.d.ts +36 -4
- package/build/src/diagnostic.d.ts.map +1 -1
- package/build/src/diagnostic.js +42 -3
- package/build/src/frontends/index.d.ts.map +1 -1
- package/build/src/frontends/index.js +5 -23
- package/build/src/frontends/python.d.ts +14 -0
- package/build/src/frontends/python.d.ts.map +1 -0
- package/build/src/frontends/python.js +615 -0
- package/build/src/frontends/scalars.d.ts.map +1 -1
- package/build/src/frontends/scalars.js +6 -1
- package/build/src/language-registry.d.ts +113 -0
- package/build/src/language-registry.d.ts.map +1 -0
- package/build/src/language-registry.js +372 -0
- package/build/src/mapping.js +3 -2
- package/build/src/semantic/location.d.ts +7 -0
- package/build/src/semantic/location.d.ts.map +1 -1
- package/build/src/semantic/location.js +10 -1
- package/build/src/semantic/provenance.js +3 -3
- package/build/src/semantic/types.d.ts +399 -2
- package/build/src/semantic/types.d.ts.map +1 -1
- package/build/src/semantic/types.js +110 -2
- package/build/src/subprocess.d.ts +29 -0
- package/build/src/subprocess.d.ts.map +1 -0
- package/build/src/subprocess.js +207 -0
- package/build/src/timeout.d.ts +8 -0
- package/build/src/timeout.d.ts.map +1 -0
- package/build/src/timeout.js +11 -0
- package/build/src/toolchains.d.ts +83 -0
- package/build/src/toolchains.d.ts.map +1 -0
- package/build/src/toolchains.js +413 -0
- package/docs/architecture.md +7 -3
- package/docs/language-support.md +25 -4
- package/docs/parser-qualification.md +70 -0
- package/docs/source-maps.md +31 -4
- package/docs/testing.md +21 -2
- package/index.js +109 -4
- package/package.json +4 -2
- package/src/acceptance.js +359 -0
- package/src/array.js +19 -0
- package/src/artifact-path.js +34 -0
- package/src/artifacts.js +420 -0
- package/src/backends/identifiers.js +9 -1
- package/src/backends/index.js +16 -32
- package/src/backends/python.js +175 -0
- package/src/backends/scalars.js +1 -0
- package/src/backends/shared.js +8 -5
- package/src/binary-mapping.js +305 -0
- package/src/diagnostic.js +34 -2
- package/src/frontends/index.js +5 -20
- package/src/frontends/python.js +689 -0
- package/src/frontends/scalars.js +5 -0
- package/src/language-registry.js +403 -0
- package/src/mapping.js +2 -1
- package/src/semantic/location.js +10 -0
- package/src/semantic/provenance.js +2 -2
- package/src/semantic/types.js +124 -1
- package/src/subprocess.js +203 -0
- package/src/timeout.js +12 -0
- package/src/toolchains.js +444 -0
- package/tsconfig.json +1 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {mkdir, mkdtemp, rm, writeFile} from "node:fs/promises"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import {isDenseArray} from "./array.js"
|
|
7
|
+
import {createGeneratedArtifactSet} from "./artifacts.js"
|
|
8
|
+
import {SemantifoldDiagnostic} from "./diagnostic.js"
|
|
9
|
+
import {exceededOwnedDeadline, executeFileWithDeadline} from "./subprocess.js"
|
|
10
|
+
import {isSupportedTimeoutMs, maximumTimeoutMs} from "./timeout.js"
|
|
11
|
+
import {deterministicEnvironment} from "./toolchains.js"
|
|
12
|
+
|
|
13
|
+
const acceptanceMaxBuffer = 16 * 1024 * 1024
|
|
14
|
+
const launchErrorCodes = new Set([
|
|
15
|
+
"E2BIG", "EACCES", "EAGAIN", "ELOOP", "EMFILE", "ENAMETOOLONG", "ENFILE", "ENOENT", "ENOEXEC", "ENOMEM", "ENOTDIR", "ENOTSUP", "EPERM", "ETXTBSY"
|
|
16
|
+
])
|
|
17
|
+
const orderedStages = Object.freeze(["parse", "generate", "compile", "link", "validate", "instantiate", "execute"])
|
|
18
|
+
const stageIndexes = new Map(orderedStages.map((stage, index) => [stage, index]))
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Materializes one artifact set and runs ordered acceptance stages without a shell.
|
|
22
|
+
* @param {object} input - Acceptance request.
|
|
23
|
+
* @param {import("./semantic/types.js").GeneratedArtifactSet} input.artifacts - Complete input artifacts.
|
|
24
|
+
* @param {Readonly<Record<string, string | undefined>>} [input.environment] - Explicit process environment.
|
|
25
|
+
* @param {{arguments: string[], stage: import("./semantic/types.js").AcceptanceStage, tool: import("./semantic/types.js").DiscoveredToolchain}[]} input.stages - Ordered stages.
|
|
26
|
+
* @param {string} input.target - Target identity.
|
|
27
|
+
* @param {number} [input.timeoutMs] - Per-stage timeout.
|
|
28
|
+
* @returns {Promise<import("./semantic/types.js").AcceptanceResult>} Captured results after cleanup.
|
|
29
|
+
*/
|
|
30
|
+
export async function runAcceptanceStages(input) {
|
|
31
|
+
if (!isPlainObject(input)) invalidRunner("Acceptance requires a request object.", "acceptance")
|
|
32
|
+
const artifacts = /** @type {import("./semantic/types.js").GeneratedArtifactSet} */ (input.artifacts)
|
|
33
|
+
const environment = /** @type {Readonly<Record<string, string | undefined>>} */ (
|
|
34
|
+
input.environment === undefined ? {PATH: process.env.PATH} : input.environment)
|
|
35
|
+
const stages = /** @type {{arguments: string[], stage: import("./semantic/types.js").AcceptanceStage, tool: import("./semantic/types.js").DiscoveredToolchain}[]} */ (input.stages)
|
|
36
|
+
const target = /** @type {string} */ (input.target)
|
|
37
|
+
const timeoutMs = /** @type {number} */ (input.timeoutMs === undefined ? 10_000 : input.timeoutMs)
|
|
38
|
+
const request = validateRequest({artifacts, environment, stages, target, timeoutMs})
|
|
39
|
+
const directory = await createAcceptanceDirectory(request.target)
|
|
40
|
+
/** @type {unknown} */
|
|
41
|
+
let primaryFailure
|
|
42
|
+
/** @type {import("./semantic/types.js").AcceptanceResult | undefined} */
|
|
43
|
+
let result
|
|
44
|
+
let primaryFailed = false
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await materializeArtifacts(request.artifacts, directory, request.target)
|
|
48
|
+
|
|
49
|
+
/** @type {import("./semantic/types.js").AcceptanceStageResult[]} */
|
|
50
|
+
const results = []
|
|
51
|
+
|
|
52
|
+
for (const stage of request.stages) {
|
|
53
|
+
try {
|
|
54
|
+
const result = await executeFileWithDeadline({
|
|
55
|
+
arguments: stage.arguments,
|
|
56
|
+
cwd: directory,
|
|
57
|
+
environment: request.environment,
|
|
58
|
+
executable: stage.tool.executable,
|
|
59
|
+
maxBuffer: acceptanceMaxBuffer,
|
|
60
|
+
timeoutMs: request.timeoutMs
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
results.push(Object.freeze({
|
|
64
|
+
arguments: Object.freeze([...stage.arguments]),
|
|
65
|
+
executable: stage.tool.executable,
|
|
66
|
+
stage: stage.stage,
|
|
67
|
+
stderr: result.stderr,
|
|
68
|
+
stdout: result.stdout,
|
|
69
|
+
version: stage.tool.version
|
|
70
|
+
}))
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw processDiagnostic(error, stage, request.target, request.timeoutMs)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
result = Object.freeze({directory, stages: Object.freeze(results), target: request.target})
|
|
77
|
+
} catch (error) {
|
|
78
|
+
primaryFailed = true
|
|
79
|
+
primaryFailure = error
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @type {unknown} */
|
|
83
|
+
let cleanupFailure
|
|
84
|
+
let cleanupFailed = false
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await removeAcceptanceDirectory(directory, request.target)
|
|
88
|
+
} catch (error) {
|
|
89
|
+
cleanupFailed = true
|
|
90
|
+
cleanupFailure = error
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (primaryFailed && cleanupFailed && primaryFailure instanceof Error) {
|
|
94
|
+
throw preservePrimaryFailure(primaryFailure, cleanupFailure, request.target)
|
|
95
|
+
}
|
|
96
|
+
if (primaryFailed) throw primaryFailure
|
|
97
|
+
if (cleanupFailed) throw cleanupFailure
|
|
98
|
+
|
|
99
|
+
return /** @type {import("./semantic/types.js").AcceptanceResult} */ (result)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validates and detaches a staged acceptance request before filesystem mutation.
|
|
104
|
+
* @param {{
|
|
105
|
+
* artifacts: import("./semantic/types.js").GeneratedArtifactSet,
|
|
106
|
+
* environment: Readonly<Record<string, string | undefined>>,
|
|
107
|
+
* stages: {arguments: string[], stage: import("./semantic/types.js").AcceptanceStage, tool: import("./semantic/types.js").DiscoveredToolchain}[],
|
|
108
|
+
* target: string,
|
|
109
|
+
* timeoutMs: number
|
|
110
|
+
* }} input - Candidate request.
|
|
111
|
+
* @returns {{
|
|
112
|
+
* artifacts: import("./semantic/types.js").GeneratedArtifactSet,
|
|
113
|
+
* environment: Readonly<Record<string, string>>,
|
|
114
|
+
* stages: {arguments: string[], stage: import("./semantic/types.js").AcceptanceStage, tool: Readonly<{executable: string, version: string}>}[],
|
|
115
|
+
* target: string,
|
|
116
|
+
* timeoutMs: number
|
|
117
|
+
* }} Validated request.
|
|
118
|
+
*/
|
|
119
|
+
function validateRequest(input) {
|
|
120
|
+
try {
|
|
121
|
+
if (typeof input.target != "string" || input.target.length == 0 || !isSupportedTimeoutMs(input.timeoutMs) ||
|
|
122
|
+
!isDenseArray(input.stages) || input.stages.length == 0) {
|
|
123
|
+
invalidRunner(`Acceptance requires a target, timeout from 1 through ${maximumTimeoutMs}ms, and non-empty ordered stages.`, input.target)
|
|
124
|
+
}
|
|
125
|
+
const artifacts = createGeneratedArtifactSet(input.artifacts)
|
|
126
|
+
|
|
127
|
+
if (artifacts.target != input.target) invalidRunner("Acceptance target must match the generated artifact-set target.", input.target)
|
|
128
|
+
const environment = deterministicEnvironment(input.environment)
|
|
129
|
+
let previousIndex = -1
|
|
130
|
+
const seen = new Set()
|
|
131
|
+
/** @type {{arguments: string[], stage: import("./semantic/types.js").AcceptanceStage, tool: Readonly<{executable: string, version: string}>}[]} */
|
|
132
|
+
const stages = []
|
|
133
|
+
|
|
134
|
+
for (let stagePosition = 0; stagePosition < input.stages.length; stagePosition += 1) {
|
|
135
|
+
const stage = input.stages[stagePosition]
|
|
136
|
+
|
|
137
|
+
if (!isPlainObject(stage)) invalidRunner("Acceptance stages must be distinct known stage records.", input.target)
|
|
138
|
+
const stageName = stage.stage
|
|
139
|
+
|
|
140
|
+
if (typeof stageName != "string" || !stageIndexes.has(stageName) || seen.has(stageName)) {
|
|
141
|
+
invalidRunner("Acceptance stages must be distinct known stage records.", input.target)
|
|
142
|
+
}
|
|
143
|
+
const index = /** @type {number} */ (stageIndexes.get(stageName))
|
|
144
|
+
|
|
145
|
+
if (index < previousIndex) invalidRunner("Acceptance stages must follow parse-to-execute order.", input.target)
|
|
146
|
+
previousIndex = index
|
|
147
|
+
seen.add(stageName)
|
|
148
|
+
const argumentCandidate = stage.arguments
|
|
149
|
+
const stageArguments = isDenseArray(argumentCandidate) ? [...argumentCandidate] : undefined
|
|
150
|
+
const toolCandidate = stage.tool
|
|
151
|
+
const tool = isPlainObject(toolCandidate) ? Object.freeze({
|
|
152
|
+
executable: toolCandidate.executable,
|
|
153
|
+
id: toolCandidate.id,
|
|
154
|
+
source: toolCandidate.source,
|
|
155
|
+
version: toolCandidate.version
|
|
156
|
+
}) : undefined
|
|
157
|
+
|
|
158
|
+
if (stageArguments == undefined || !stageArguments.every((argument) => typeof argument == "string" && !argument.includes("\0")) ||
|
|
159
|
+
!isTool(tool)) invalidRunner(`Acceptance stage '${stageName}' has an invalid tool or argument array.`, input.target)
|
|
160
|
+
|
|
161
|
+
stages.push(Object.freeze({arguments: stageArguments, stage: stageName, tool}))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {artifacts, environment, stages, target: input.target, timeoutMs: input.timeoutMs}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (error instanceof SemantifoldDiagnostic && error.code == "INVALID_ACCEPTANCE_RUNNER") throw error
|
|
167
|
+
if (error instanceof SemantifoldDiagnostic) {
|
|
168
|
+
throw new SemantifoldDiagnostic({
|
|
169
|
+
cause: error,
|
|
170
|
+
code: "INVALID_ACCEPTANCE_RUNNER",
|
|
171
|
+
language: typeof input.target == "string" ? input.target : "acceptance",
|
|
172
|
+
message: `Acceptance runner input is invalid: ${error.detail}`
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return invalidRunner("Acceptance runner input is invalid.", typeof input.target == "string" ? input.target : "acceptance", error)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Normalizes a subprocess failure with exact stage and toolchain context.
|
|
182
|
+
* @param {unknown} error - Subprocess rejection.
|
|
183
|
+
* @param {{arguments: string[], stage: string, tool: Readonly<{executable: string, version: string}>}} stage - Failed stage.
|
|
184
|
+
* @param {string} target - Acceptance target.
|
|
185
|
+
* @param {number} timeoutMs - Configured stage timeout.
|
|
186
|
+
* @returns {SemantifoldDiagnostic} Normalized failure.
|
|
187
|
+
*/
|
|
188
|
+
function processDiagnostic(error, stage, target, timeoutMs) {
|
|
189
|
+
const fields = processErrorFields(error)
|
|
190
|
+
const outputLimited = fields.nativeCode == "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
191
|
+
const timedOut = !outputLimited && exceededOwnedDeadline(error)
|
|
192
|
+
const launchFailure = fields.nativeCode != undefined && launchErrorCodes.has(fields.nativeCode)
|
|
193
|
+
const signaled = !outputLimited && !timedOut && fields.signal != undefined
|
|
194
|
+
const code = outputLimited ? "ACCEPTANCE_OUTPUT_LIMIT" : timedOut ? "ACCEPTANCE_TIMEOUT" : launchFailure ? "ACCEPTANCE_LAUNCH_FAILURE" : signaled
|
|
195
|
+
? "ACCEPTANCE_SIGNAL"
|
|
196
|
+
: "ACCEPTANCE_NONZERO_EXIT"
|
|
197
|
+
const reason = outputLimited ? `exceeded the ${acceptanceMaxBuffer}-byte output capture limit (${fields.nativeCode})` : timedOut
|
|
198
|
+
? `timed out after ${timeoutMs}ms` : launchFailure
|
|
199
|
+
? `could not launch (${fields.nativeCode})`
|
|
200
|
+
: signaled ? `terminated by signal ${fields.signal}`
|
|
201
|
+
: fields.exitCode != undefined ? `exited nonzero (${fields.exitCode})`
|
|
202
|
+
: `failed after launch (${fields.nativeCode ?? "unknown"})`
|
|
203
|
+
|
|
204
|
+
return new SemantifoldDiagnostic({
|
|
205
|
+
cause: outputLimited ? undefined : error instanceof Error ? error : undefined,
|
|
206
|
+
code,
|
|
207
|
+
executable: stage.tool.executable,
|
|
208
|
+
exitCode: fields.exitCode,
|
|
209
|
+
language: target,
|
|
210
|
+
message: `Acceptance stage '${stage.stage}' ${reason} using '${stage.tool.executable}' (${stage.tool.version}).`,
|
|
211
|
+
signal: fields.signal,
|
|
212
|
+
stage: stage.stage,
|
|
213
|
+
stderr: fields.stderr,
|
|
214
|
+
stdout: fields.stdout,
|
|
215
|
+
version: stage.tool.version
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Narrows the stable fields of an opaque child-process failure.
|
|
221
|
+
* @param {unknown} error - Process rejection.
|
|
222
|
+
* @returns {{exitCode: number | undefined, nativeCode: string | undefined, signal: string | undefined, stderr: string, stdout: string}} Stable fields.
|
|
223
|
+
*/
|
|
224
|
+
function processErrorFields(error) {
|
|
225
|
+
if (!error || typeof error != "object") return {exitCode: undefined, nativeCode: undefined, signal: undefined, stderr: "", stdout: ""}
|
|
226
|
+
const value = /** @type {Record<string, unknown>} */ (error)
|
|
227
|
+
const nativeCode = typeof value.code == "string" ? value.code : undefined
|
|
228
|
+
const outputLimited = nativeCode == "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
exitCode: typeof value.code == "number" ? value.code : undefined,
|
|
232
|
+
nativeCode,
|
|
233
|
+
signal: typeof value.signal == "string" ? value.signal : undefined,
|
|
234
|
+
stderr: !outputLimited && typeof value.stderr == "string" ? value.stderr : "",
|
|
235
|
+
stdout: !outputLimited && typeof value.stdout == "string" ? value.stdout : ""
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Checks the discovered-tool fields consumed by the runner.
|
|
241
|
+
* @param {unknown} value - Candidate tool.
|
|
242
|
+
* @returns {value is import("./semantic/types.js").DiscoveredToolchain} Whether the tool contract is present.
|
|
243
|
+
*/
|
|
244
|
+
function isTool(value) {
|
|
245
|
+
return isPlainObject(value) && typeof value.id == "string" && typeof value.executable == "string" && path.isAbsolute(value.executable) &&
|
|
246
|
+
!value.executable.includes("\0") && typeof value.version == "string" && value.version.length > 0 &&
|
|
247
|
+
(value.source == "canonical" || value.source == "override")
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Keeps the primary acceptance error while recording bounded cleanup evidence in its cause chain.
|
|
252
|
+
* @param {Error} primaryFailure - Original materialization or stage failure.
|
|
253
|
+
* @param {unknown} cleanupFailure - Cleanup rejection.
|
|
254
|
+
* @param {string} target - Acceptance target.
|
|
255
|
+
* @returns {Error} The original primary error with structured cleanup evidence.
|
|
256
|
+
*/
|
|
257
|
+
function preservePrimaryFailure(primaryFailure, cleanupFailure, target) {
|
|
258
|
+
const cleanupEvidence = new SemantifoldDiagnostic({
|
|
259
|
+
code: "ACCEPTANCE_CLEANUP_FAILURE",
|
|
260
|
+
language: cleanupFailure instanceof SemantifoldDiagnostic ? cleanupFailure.language : target,
|
|
261
|
+
message: "Isolated acceptance-directory cleanup failed after the primary acceptance failure."
|
|
262
|
+
})
|
|
263
|
+
const causes = primaryFailure.cause instanceof Error ? [primaryFailure.cause, cleanupEvidence] : [cleanupEvidence]
|
|
264
|
+
|
|
265
|
+
Object.defineProperty(primaryFailure, "cause", {
|
|
266
|
+
configurable: true,
|
|
267
|
+
value: new AggregateError(causes, "Acceptance failed and isolated-directory cleanup also failed."),
|
|
268
|
+
writable: true
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
return primaryFailure
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Throws a normalized runner-configuration diagnostic.
|
|
276
|
+
* @param {string} message - Failure detail.
|
|
277
|
+
* @param {unknown} target - Candidate target identity.
|
|
278
|
+
* @param {unknown} [error] - Preserved cause.
|
|
279
|
+
* @returns {never} Always throws.
|
|
280
|
+
*/
|
|
281
|
+
function invalidRunner(message, target, error) {
|
|
282
|
+
throw new SemantifoldDiagnostic({
|
|
283
|
+
cause: error instanceof Error ? error : undefined,
|
|
284
|
+
code: "INVALID_ACCEPTANCE_RUNNER",
|
|
285
|
+
language: typeof target == "string" && target.length > 0 ? target : "acceptance",
|
|
286
|
+
message
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Checks for an ordinary string-keyed object.
|
|
292
|
+
* @param {unknown} value - Candidate value.
|
|
293
|
+
* @returns {value is Record<string, unknown>} Whether the value is a plain object.
|
|
294
|
+
*/
|
|
295
|
+
function isPlainObject(value) {
|
|
296
|
+
return typeof value == "object" && value != null && !Array.isArray(value) && Object.getPrototypeOf(value) == Object.prototype
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Removes the isolated acceptance directory with a normalized failure.
|
|
301
|
+
* @param {string} directory - Exact temporary directory.
|
|
302
|
+
* @param {string} target - Acceptance target.
|
|
303
|
+
* @returns {Promise<void>} Cleanup completion.
|
|
304
|
+
*/
|
|
305
|
+
async function removeAcceptanceDirectory(directory, target) {
|
|
306
|
+
try {
|
|
307
|
+
await rm(directory, {force: true, recursive: true})
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw new SemantifoldDiagnostic({
|
|
310
|
+
cause: error instanceof Error ? error : undefined,
|
|
311
|
+
code: "ACCEPTANCE_CLEANUP_FAILURE",
|
|
312
|
+
language: target,
|
|
313
|
+
message: `Failed to remove isolated acceptance directory '${directory}'.`
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Creates one isolated acceptance directory with a normalized setup failure.
|
|
320
|
+
* @param {string} target - Acceptance target.
|
|
321
|
+
* @returns {Promise<string>} Exact temporary directory.
|
|
322
|
+
*/
|
|
323
|
+
async function createAcceptanceDirectory(target) {
|
|
324
|
+
try {
|
|
325
|
+
return await mkdtemp(path.join(os.tmpdir(), "semantifold-acceptance-"))
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw new SemantifoldDiagnostic({
|
|
328
|
+
cause: error instanceof Error ? error : undefined,
|
|
329
|
+
code: "ACCEPTANCE_SETUP_FAILURE",
|
|
330
|
+
language: target,
|
|
331
|
+
message: "Failed to create an isolated acceptance directory."
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Writes only the already-validated generated artifacts into the isolated directory.
|
|
338
|
+
* @param {import("./semantic/types.js").GeneratedArtifactSet} artifacts - Complete validated set.
|
|
339
|
+
* @param {string} directory - Exact temporary directory.
|
|
340
|
+
* @param {string} target - Acceptance target.
|
|
341
|
+
* @returns {Promise<void>} Materialization completion.
|
|
342
|
+
*/
|
|
343
|
+
async function materializeArtifacts(artifacts, directory, target) {
|
|
344
|
+
for (const artifact of artifacts.artifacts) {
|
|
345
|
+
const filename = path.join(directory, ...artifact.path.split("/"))
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
await mkdir(path.dirname(filename), {recursive: true})
|
|
349
|
+
await writeFile(filename, artifact.content)
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw new SemantifoldDiagnostic({
|
|
352
|
+
cause: error instanceof Error ? error : undefined,
|
|
353
|
+
code: "ACCEPTANCE_MATERIALIZATION_FAILURE",
|
|
354
|
+
language: target,
|
|
355
|
+
message: `Failed to materialize generated artifact '${artifact.path}'.`
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
package/src/array.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks that every index below an array's length is an own property.
|
|
5
|
+
* @param {unknown} value - Candidate ordered array.
|
|
6
|
+
* @returns {value is unknown[]} Whether the value is a dense array.
|
|
7
|
+
*/
|
|
8
|
+
export function isDenseArray(value) {
|
|
9
|
+
if (!Array.isArray(value)) return false
|
|
10
|
+
let indexedProperties = 0
|
|
11
|
+
|
|
12
|
+
for (const property of Object.getOwnPropertyNames(value)) {
|
|
13
|
+
const index = Number(property)
|
|
14
|
+
|
|
15
|
+
if (Number.isInteger(index) && index >= 0 && index < value.length && String(index) == property) indexedProperties += 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return indexedProperties == value.length
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {hasOnlyUnicodeScalars} from "./semantic/scalars.js"
|
|
4
|
+
|
|
5
|
+
const lineTerminatorPattern = /[\n\r\u2028\u2029]/u
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validates filename metadata without imposing artifact-set path semantics.
|
|
9
|
+
* @param {unknown} value - Candidate filename.
|
|
10
|
+
* @returns {value is string} Whether the value is a non-empty single-line string.
|
|
11
|
+
*/
|
|
12
|
+
export function isValidFilenameMetadata(value) {
|
|
13
|
+
return typeof value == "string" && value.length > 0 && !lineTerminatorPattern.test(value)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates a safe relative POSIX artifact path.
|
|
18
|
+
* @param {unknown} value - Candidate path.
|
|
19
|
+
* @returns {value is string} Whether the value is safe.
|
|
20
|
+
*/
|
|
21
|
+
export function isSafeArtifactPath(value) {
|
|
22
|
+
if (typeof value != "string" || value.length == 0 || value.includes("\\") || value.includes("\0") ||
|
|
23
|
+
!hasOnlyUnicodeScalars(value)) return false
|
|
24
|
+
if (value.startsWith("/") || /^[A-Za-z]:/u.test(value)) return false
|
|
25
|
+
|
|
26
|
+
const parts = value.split("/")
|
|
27
|
+
|
|
28
|
+
return parts.every((part) => part.length > 0 && part != "." && part != ".." &&
|
|
29
|
+
[...part].every((character) => {
|
|
30
|
+
const codePoint = character.codePointAt(0)
|
|
31
|
+
|
|
32
|
+
return codePoint != undefined && codePoint > 31 && codePoint != 127
|
|
33
|
+
}))
|
|
34
|
+
}
|