@sublang/slc 0.3.0 → 0.4.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 +52 -3
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +5 -4
- package/dist/app.js.map +1 -1
- package/dist/build-history.d.ts +74 -0
- package/dist/build-history.d.ts.map +1 -0
- package/dist/build-history.js +309 -0
- package/dist/build-history.js.map +1 -0
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/compiled-executor.d.ts +1 -1
- package/dist/compiled-executor.d.ts.map +1 -1
- package/dist/compiled-executor.js +10 -3
- package/dist/compiled-executor.js.map +1 -1
- package/dist/config-file.d.ts.map +1 -1
- package/dist/config-file.js +1 -3
- package/dist/config-file.js.map +1 -1
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/execution.d.ts +42 -0
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +224 -29
- package/dist/execution.js.map +1 -1
- package/dist/hash.d.ts +22 -0
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +47 -0
- package/dist/hash.js.map +1 -1
- package/dist/interpreter.d.ts +1 -1
- package/dist/interpreter.d.ts.map +1 -1
- package/dist/interpreter.js +9 -0
- package/dist/interpreter.js.map +1 -1
- package/dist/invocation.d.ts +5 -1
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +14 -3
- package/dist/invocation.js.map +1 -1
- package/dist/line-diff.d.ts +7 -0
- package/dist/line-diff.d.ts.map +1 -0
- package/dist/line-diff.js +66 -0
- package/dist/line-diff.js.map +1 -0
- package/dist/phase.d.ts +2 -0
- package/dist/phase.d.ts.map +1 -1
- package/dist/phase.js +19 -3
- package/dist/phase.js.map +1 -1
- package/dist/pin-closure.d.ts +2 -2
- package/dist/pin-closure.d.ts.map +1 -1
- package/dist/pin-closure.js +4 -3
- package/dist/pin-closure.js.map +1 -1
- package/dist/pin-currency.d.ts +6 -2
- package/dist/pin-currency.d.ts.map +1 -1
- package/dist/pin-currency.js +24 -43
- package/dist/pin-currency.js.map +1 -1
- package/dist/pin-generate.d.ts.map +1 -1
- package/dist/pin-generate.js +1 -3
- package/dist/pin-generate.js.map +1 -1
- package/dist/pin-paths.d.ts.map +1 -1
- package/dist/pin-paths.js +2 -13
- package/dist/pin-paths.js.map +1 -1
- package/dist/pins.d.ts.map +1 -1
- package/dist/pins.js +5 -15
- package/dist/pins.js.map +1 -1
- package/dist/playbook-ports.d.ts +9 -0
- package/dist/playbook-ports.d.ts.map +1 -1
- package/dist/playbook-ports.js +7 -4
- package/dist/playbook-ports.js.map +1 -1
- package/dist/runner.d.ts +2 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +628 -88
- package/dist/runner.js.map +1 -1
- package/dist/runtime-package.d.ts.map +1 -1
- package/dist/runtime-package.js +3 -11
- package/dist/runtime-package.js.map +1 -1
- package/dist/verify-coverage.d.ts.map +1 -1
- package/dist/verify-coverage.js +2 -0
- package/dist/verify-coverage.js.map +1 -1
- package/dist/verify-support.d.ts +7 -0
- package/dist/verify-support.d.ts.map +1 -1
- package/dist/verify-support.js +13 -6
- package/dist/verify-support.js.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +2 -0
- package/dist/verify.js.map +1 -1
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -9,21 +9,26 @@
|
|
|
9
9
|
* injected so a host wires the real pipeline resolution and Cligent agent while
|
|
10
10
|
* tests supply fakes. See specs/dev/pipeline.md and specs/dev/phase-execution.md.
|
|
11
11
|
*/
|
|
12
|
-
import { mkdir } from 'node:fs/promises';
|
|
13
|
-
import { dirname, join } from 'node:path';
|
|
12
|
+
import { mkdir, readFile, stat } from 'node:fs/promises';
|
|
13
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { artifactDir, planArtifacts, parseSource } from './artifacts.js';
|
|
16
|
+
import { encodeLocator, invalidateBuildHistory, loadBuildHistory, recordBuild, verifiedInput, } from './build-history.js';
|
|
16
17
|
import { emitEntryModule } from './entry-module.js';
|
|
17
18
|
import { unresolvableRelativeImports } from './emitted-imports.js';
|
|
18
|
-
import {
|
|
19
|
+
import { messageOf } from './errors.js';
|
|
20
|
+
import { assertSafeTarget, formatFailureReport, pathsAlias, runPhase, } from './execution.js';
|
|
21
|
+
import { hashBytes, isHash } from './hash.js';
|
|
19
22
|
import { parseInvocation } from './invocation.js';
|
|
23
|
+
import { unifiedLineDiff } from './line-diff.js';
|
|
20
24
|
import { linkedArtifactPath, loadLinkFile } from './link.js';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
25
|
+
import { deriveClosure } from './pin-closure.js';
|
|
26
|
+
import { evaluatePin, evaluatePinFile, hashTree } from './pin-currency.js';
|
|
27
|
+
import { PINS_FILE, PinError, loadPinFile, } from './pins.js';
|
|
23
28
|
import { loadPipeline, resolvePipeline, } from './pipeline.js';
|
|
24
29
|
import { defaultPlaybookLinkTarget, isReservedPipeline } from './resolver.js';
|
|
25
30
|
import { emitFsmCoverageTest, emitFsmIntrospectionTest, emitGearsFsmConformanceTest, emitPromptContractTest, } from './verify.js';
|
|
26
|
-
import { VERIFIER_SUPPORT_MODULE, emitVerifierSupport, } from './verify-support.js';
|
|
31
|
+
import { VERIFIER_SUPPORT_MODULE, emitVerifierSupport, verifierSupportFiles, } from './verify-support.js';
|
|
27
32
|
/**
|
|
28
33
|
* Parses argv and runs the requested pipeline, phase, or link end-to-end.
|
|
29
34
|
* Never rejects: malformed invocations, refusals, and phase failures all return
|
|
@@ -65,47 +70,67 @@ export async function runSlc(argv, deps) {
|
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
async function runFull(invocation, deps) {
|
|
73
|
+
const cwd = runCwd(deps);
|
|
68
74
|
const pipeline = await loadPipeline(await resolvePipeline(invocation.pipeline, deps.resolver));
|
|
69
75
|
const entry = pipeline.phases[0];
|
|
76
|
+
const source = resolve(cwd, invocation.source);
|
|
70
77
|
const { basename, raw } = parseSource({
|
|
71
|
-
path:
|
|
78
|
+
path: source,
|
|
72
79
|
sourceFormat: entry.source.format,
|
|
73
80
|
ext: entry.source.ext,
|
|
74
81
|
entry: true,
|
|
75
82
|
});
|
|
76
|
-
const artDir = artifactDir(
|
|
83
|
+
const artDir = artifactDir(cwd, basename, invocation.pipeline);
|
|
77
84
|
await mkdir(artDir, { recursive: true });
|
|
78
85
|
const plan = planArtifacts({
|
|
79
86
|
phases: pipeline.phases,
|
|
80
87
|
basename,
|
|
81
88
|
artDir,
|
|
82
|
-
output: invocation.output
|
|
89
|
+
output: invocation.output === null ? undefined : resolve(cwd, invocation.output),
|
|
83
90
|
});
|
|
84
91
|
const steps = buildCompileSteps({
|
|
85
92
|
pipeline,
|
|
86
93
|
plan,
|
|
87
|
-
source
|
|
94
|
+
source,
|
|
88
95
|
artDir,
|
|
89
96
|
basename,
|
|
90
97
|
optimize: !invocation.noOptimize,
|
|
91
98
|
normalize: invocation.normalize || raw,
|
|
92
99
|
});
|
|
93
|
-
const
|
|
94
|
-
return emitVerification(result, {
|
|
100
|
+
const verification = {
|
|
95
101
|
pipeline: invocation.pipeline,
|
|
96
102
|
plan,
|
|
97
103
|
artDir,
|
|
98
104
|
basename,
|
|
105
|
+
};
|
|
106
|
+
return executeSteps(steps, pipeline, deps, {
|
|
107
|
+
incremental: incrementalRun(invocation, artDir, source),
|
|
108
|
+
hostTargets: verificationHostTargets(verification),
|
|
109
|
+
complete: (result, guardTarget) => emitVerification(result, verification, guardTarget),
|
|
99
110
|
});
|
|
100
111
|
}
|
|
112
|
+
/** History covers only canonical full/full-link runs outside the `slc` meta-pipeline. */
|
|
113
|
+
function incrementalRun(invocation, artDir, source) {
|
|
114
|
+
if (invocation.output !== null || invocation.pipeline === 'slc') {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
artDir,
|
|
119
|
+
source,
|
|
120
|
+
pipelineName: invocation.pipeline,
|
|
121
|
+
rebuild: invocation.rebuild === true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
101
124
|
async function runSinglePhase(invocation, deps) {
|
|
125
|
+
const cwd = runCwd(deps);
|
|
102
126
|
const pipeline = await loadPipeline(await resolvePipeline(invocation.pipeline, deps.resolver));
|
|
103
127
|
const phase = [...pipeline.phases, ...pipeline.passes].find((candidate) => candidate.name === invocation.phase);
|
|
104
128
|
if (phase === undefined) {
|
|
105
129
|
return failure(`phase "${invocation.phase}" is not part of pipeline "${invocation.pipeline}"`);
|
|
106
130
|
}
|
|
131
|
+
const source = resolve(cwd, invocation.source);
|
|
107
132
|
const { basename, raw } = parseSource({
|
|
108
|
-
path:
|
|
133
|
+
path: source,
|
|
109
134
|
sourceFormat: phase.source.format,
|
|
110
135
|
ext: phase.source.ext,
|
|
111
136
|
entry: pipeline.phases[0] === phase,
|
|
@@ -115,14 +140,14 @@ async function runSinglePhase(invocation, deps) {
|
|
|
115
140
|
// path into the phase's declared source format here.
|
|
116
141
|
return failure(`source "${invocation.source}" is a raw input; run the full pipeline to normalize it`);
|
|
117
142
|
}
|
|
118
|
-
const artDir = artifactDir(
|
|
143
|
+
const artDir = artifactDir(cwd, basename, invocation.pipeline);
|
|
119
144
|
await mkdir(artDir, { recursive: true });
|
|
120
145
|
if (phase.pass) {
|
|
121
146
|
// A standalone pass run cannot overwrite its own source: it writes the
|
|
122
147
|
// `.opt` sibling unless `-o` relocates it (DR-013).
|
|
123
|
-
const target = invocation.output ??
|
|
148
|
+
const target = (invocation.output === null ? null : resolve(cwd, invocation.output)) ??
|
|
124
149
|
join(artDir, `${basename}.${phase.target.format}.opt${phase.target.ext}`);
|
|
125
|
-
const step = compileStep(pipeline, phase,
|
|
150
|
+
const step = compileStep(pipeline, phase, source, target);
|
|
126
151
|
return executeSteps([step], pipeline, deps);
|
|
127
152
|
}
|
|
128
153
|
// Plan over the whole chain so the named phase keeps its pipeline role: a
|
|
@@ -132,58 +157,62 @@ async function runSinglePhase(invocation, deps) {
|
|
|
132
157
|
phases: pipeline.phases,
|
|
133
158
|
basename,
|
|
134
159
|
artDir,
|
|
135
|
-
output: invocation.output
|
|
160
|
+
output: invocation.output === null ? undefined : resolve(cwd, invocation.output),
|
|
136
161
|
});
|
|
137
162
|
const artifact = plan[pipeline.phases.indexOf(phase)];
|
|
138
|
-
const step = compileStep(pipeline, phase,
|
|
163
|
+
const step = compileStep(pipeline, phase, source, artifact.path);
|
|
139
164
|
return executeSteps([step], pipeline, deps);
|
|
140
165
|
}
|
|
141
166
|
async function runDirectLink(invocation, deps) {
|
|
167
|
+
const cwd = runCwd(deps);
|
|
142
168
|
const pipeline = await loadPipeline(await resolvePipeline(invocation.pipeline, deps.resolver));
|
|
143
169
|
const link = await requireLink(pipeline, invocation.pipeline);
|
|
144
170
|
const linked = linkedArtifactPath({
|
|
145
171
|
kind: 'link',
|
|
146
172
|
pipeline: invocation.pipeline,
|
|
147
|
-
objects: invocation.objects,
|
|
173
|
+
objects: invocation.objects.map((path) => resolve(cwd, path)),
|
|
148
174
|
source: link.source,
|
|
149
175
|
linked: link.target,
|
|
150
|
-
output: invocation.output,
|
|
151
|
-
cwd
|
|
176
|
+
output: invocation.output === null ? null : resolve(cwd, invocation.output),
|
|
177
|
+
cwd,
|
|
152
178
|
});
|
|
153
179
|
await mkdir(dirname(linked), { recursive: true });
|
|
154
180
|
const step = {
|
|
155
181
|
request: {
|
|
156
182
|
kind: 'link',
|
|
157
183
|
definitionPath: pipeline.linkFile,
|
|
158
|
-
objects: invocation.objects,
|
|
159
|
-
linkTarget: invocation.linkTarget,
|
|
184
|
+
objects: invocation.objects.map((path) => resolve(cwd, path)),
|
|
185
|
+
linkTarget: resolve(cwd, invocation.linkTarget),
|
|
160
186
|
options: invocation.options,
|
|
161
187
|
linked,
|
|
162
188
|
},
|
|
163
189
|
phase: 'link',
|
|
190
|
+
pinKey: 'link',
|
|
164
191
|
targetExt: link.target.ext,
|
|
165
192
|
};
|
|
166
193
|
return executeSteps([step], pipeline, deps);
|
|
167
194
|
}
|
|
168
195
|
async function runFullLink(invocation, deps) {
|
|
196
|
+
const cwd = runCwd(deps);
|
|
169
197
|
const pipeline = await loadPipeline(await resolvePipeline(invocation.pipeline, deps.resolver));
|
|
170
198
|
const link = await requireLink(pipeline, invocation.pipeline);
|
|
171
199
|
const entry = pipeline.phases[0];
|
|
200
|
+
const source = resolve(cwd, invocation.source);
|
|
172
201
|
const { basename, raw } = parseSource({
|
|
173
|
-
path:
|
|
202
|
+
path: source,
|
|
174
203
|
sourceFormat: entry.source.format,
|
|
175
204
|
ext: entry.source.ext,
|
|
176
205
|
entry: true,
|
|
177
206
|
});
|
|
178
207
|
const normalize = invocation.normalize || raw;
|
|
179
|
-
const artDir = artifactDir(
|
|
208
|
+
const artDir = artifactDir(cwd, basename, invocation.pipeline);
|
|
180
209
|
await mkdir(artDir, { recursive: true });
|
|
181
210
|
// Compile chain: the exit artifact becomes the object artifact (PIPE-15).
|
|
182
211
|
const plan = planArtifacts({ phases: pipeline.phases, basename, artDir });
|
|
183
212
|
const compileSteps = buildCompileSteps({
|
|
184
213
|
pipeline,
|
|
185
214
|
plan,
|
|
186
|
-
source
|
|
215
|
+
source,
|
|
187
216
|
artDir,
|
|
188
217
|
basename,
|
|
189
218
|
optimize: !invocation.noOptimize,
|
|
@@ -194,64 +223,88 @@ async function runFullLink(invocation, deps) {
|
|
|
194
223
|
artDir,
|
|
195
224
|
basename,
|
|
196
225
|
linked: link.target,
|
|
197
|
-
output: invocation.output,
|
|
226
|
+
output: invocation.output === null ? null : resolve(cwd, invocation.output),
|
|
198
227
|
});
|
|
199
228
|
const linkStep = {
|
|
200
229
|
request: {
|
|
201
230
|
kind: 'link',
|
|
202
231
|
definitionPath: pipeline.linkFile,
|
|
203
232
|
objects: [plan[plan.length - 1].path],
|
|
204
|
-
linkTarget: invocation.linkTarget,
|
|
233
|
+
linkTarget: resolve(cwd, invocation.linkTarget),
|
|
205
234
|
options: invocation.options,
|
|
206
235
|
linked,
|
|
207
236
|
},
|
|
208
237
|
phase: 'link',
|
|
238
|
+
pinKey: 'link',
|
|
209
239
|
targetExt: link.target.ext,
|
|
210
240
|
};
|
|
211
|
-
const
|
|
212
|
-
const
|
|
241
|
+
const steps = [...compileSteps, linkStep];
|
|
242
|
+
const gearsPlan = plan.find((artifact) => artifact.phase.target.format === 'gears');
|
|
243
|
+
const entryCandidate = invocation.pipeline === 'playbook' &&
|
|
244
|
+
invocation.output === null &&
|
|
245
|
+
gearsPlan !== undefined
|
|
246
|
+
? resolve(cwd, `${basename}.ts`)
|
|
247
|
+
: null;
|
|
248
|
+
const entryAliasesSource = entryCandidate !== null && (await pathsAlias(entryCandidate, source));
|
|
249
|
+
const entryPath = entryAliasesSource ? null : entryCandidate;
|
|
250
|
+
const verification = {
|
|
213
251
|
pipeline: invocation.pipeline,
|
|
214
252
|
plan,
|
|
215
253
|
artDir,
|
|
216
254
|
basename,
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
: invocation.source;
|
|
228
|
-
const entryPath = await emitEntryModule({
|
|
229
|
-
cwd: runCwd(deps),
|
|
230
|
-
basename,
|
|
231
|
-
pipeline: invocation.pipeline,
|
|
232
|
-
gearsPath: gearsPlan.path,
|
|
233
|
-
textPath,
|
|
234
|
-
});
|
|
235
|
-
const missing = await unresolvableRelativeImports(entryPath);
|
|
236
|
-
if (missing.length > 0) {
|
|
255
|
+
};
|
|
256
|
+
return executeSteps(steps, pipeline, deps, {
|
|
257
|
+
incremental: incrementalRun(invocation, artDir, source),
|
|
258
|
+
hostTargets: [
|
|
259
|
+
...verificationHostTargets(verification),
|
|
260
|
+
...(entryPath === null ? [] : [{ path: entryPath }]),
|
|
261
|
+
],
|
|
262
|
+
complete: async (result, guardTarget) => {
|
|
263
|
+
const verified = await emitVerification(result, verification, guardTarget);
|
|
264
|
+
if (verified.ok && entryCandidate !== null && entryAliasesSource) {
|
|
237
265
|
return {
|
|
238
|
-
|
|
239
|
-
outputs: verified.outputs,
|
|
266
|
+
...verified,
|
|
240
267
|
diagnostics: [
|
|
241
268
|
...verified.diagnostics,
|
|
242
|
-
`entry module ${
|
|
243
|
-
`${missing.join(', ')} (VERIFY-18)`,
|
|
269
|
+
`entry module ${entryCandidate} not emitted because it aliases the invocation source`,
|
|
244
270
|
],
|
|
245
271
|
};
|
|
246
272
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
273
|
+
// Entry-module emission (DR-014, SELFHOST-15): only the playbook
|
|
274
|
+
// pipeline, only with the linked artifact at its canonical path.
|
|
275
|
+
if (verified.ok && entryPath !== null && gearsPlan !== undefined) {
|
|
276
|
+
await guardTarget(entryPath);
|
|
277
|
+
const textPath = normalize
|
|
278
|
+
? join(artDir, `${basename}.${entry.source.format}${entry.source.ext}`)
|
|
279
|
+
: source;
|
|
280
|
+
await emitEntryModule({
|
|
281
|
+
cwd,
|
|
282
|
+
basename,
|
|
283
|
+
pipeline: invocation.pipeline,
|
|
284
|
+
gearsPath: gearsPlan.path,
|
|
285
|
+
textPath,
|
|
286
|
+
});
|
|
287
|
+
const missing = await unresolvableRelativeImports(entryPath);
|
|
288
|
+
if (missing.length > 0) {
|
|
289
|
+
return {
|
|
290
|
+
ok: false,
|
|
291
|
+
outputs: verified.outputs,
|
|
292
|
+
diagnostics: [
|
|
293
|
+
...verified.diagnostics,
|
|
294
|
+
`entry module ${entryPath} has unresolvable relative imports: ` +
|
|
295
|
+
`${missing.join(', ')} (VERIFY-18)`,
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return { ...verified, outputs: [...verified.outputs, entryPath] };
|
|
300
|
+
}
|
|
301
|
+
return verified;
|
|
302
|
+
},
|
|
303
|
+
});
|
|
251
304
|
}
|
|
252
305
|
/** The invocation working directory anchoring artifact placement (DR-014). */
|
|
253
306
|
function runCwd(deps) {
|
|
254
|
-
return deps.cwd ?? process.cwd();
|
|
307
|
+
return resolve(deps.cwd ?? process.cwd());
|
|
255
308
|
}
|
|
256
309
|
/**
|
|
257
310
|
* After a reserved-pipeline full run produces a `gears` intermediate and an `fsm`
|
|
@@ -267,28 +320,35 @@ function runCwd(deps) {
|
|
|
267
320
|
* to a diagnostic when the artifact cannot be loaded, leaving the run outcome
|
|
268
321
|
* unchanged — the conformance test still fails at test time on a broken module.
|
|
269
322
|
*/
|
|
270
|
-
async function emitVerification(result, ctx) {
|
|
271
|
-
if (!result.ok
|
|
272
|
-
return result;
|
|
273
|
-
const fsm = ctx.plan.find((artifact) => artifact.phase.target.format === 'fsm');
|
|
274
|
-
const hasGears = ctx.plan.some((artifact) => artifact.phase.target.format === 'gears');
|
|
275
|
-
if (fsm === undefined || !hasGears)
|
|
323
|
+
async function emitVerification(result, ctx, guardTarget) {
|
|
324
|
+
if (!result.ok)
|
|
276
325
|
return result;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// elsewhere (PIPE-8), so that edge never targets a missing sibling artifact.
|
|
280
|
-
const canonicalFsm = join(ctx.artDir, `${ctx.basename}.fsm${fsm.phase.target.ext}`);
|
|
281
|
-
if (fsm.path !== canonicalFsm)
|
|
326
|
+
const hostTargets = verificationHostTargets(ctx);
|
|
327
|
+
if (hostTargets.length === 0)
|
|
282
328
|
return result;
|
|
329
|
+
const fsm = ctx.plan.find((artifact) => artifact.phase.target.format === 'fsm');
|
|
330
|
+
if (fsm === undefined)
|
|
331
|
+
throw new Error('verification plan lost its FSM');
|
|
283
332
|
const outputs = [...result.outputs];
|
|
284
333
|
const diagnostics = [...result.diagnostics];
|
|
334
|
+
if (guardTarget !== undefined) {
|
|
335
|
+
for (const target of verifierSupportFiles(ctx.artDir)) {
|
|
336
|
+
await guardTarget(target.target);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
285
339
|
outputs.push(...(await emitVerifierSupport(ctx.artDir)));
|
|
340
|
+
if (guardTarget !== undefined) {
|
|
341
|
+
await guardTarget(join(ctx.artDir, `${ctx.basename}.gears-fsm.test.ts`));
|
|
342
|
+
}
|
|
286
343
|
outputs.push(await emitGearsFsmConformanceTest({
|
|
287
344
|
artifactDir: ctx.artDir,
|
|
288
345
|
basename: ctx.basename,
|
|
289
346
|
verifyModule: VERIFIER_SUPPORT_MODULE,
|
|
290
347
|
}));
|
|
291
348
|
try {
|
|
349
|
+
if (guardTarget !== undefined) {
|
|
350
|
+
await guardTarget(join(ctx.artDir, `${ctx.basename}.fsm.introspect.test.ts`));
|
|
351
|
+
}
|
|
292
352
|
outputs.push(await emitFsmIntrospectionTest({
|
|
293
353
|
artifactDir: ctx.artDir,
|
|
294
354
|
basename: ctx.basename,
|
|
@@ -299,6 +359,9 @@ async function emitVerification(result, ctx) {
|
|
|
299
359
|
diagnostics.push(`verification: introspection test not emitted: ${messageOf(error)}`);
|
|
300
360
|
}
|
|
301
361
|
try {
|
|
362
|
+
if (guardTarget !== undefined) {
|
|
363
|
+
await guardTarget(join(ctx.artDir, `${ctx.basename}.prompt-contract.test.ts`));
|
|
364
|
+
}
|
|
302
365
|
const promptContract = await emitPromptContractTest({
|
|
303
366
|
artifactDir: ctx.artDir,
|
|
304
367
|
basename: ctx.basename,
|
|
@@ -311,6 +374,9 @@ async function emitVerification(result, ctx) {
|
|
|
311
374
|
diagnostics.push(`verification: prompt-contract test not emitted: ${messageOf(error)}`);
|
|
312
375
|
}
|
|
313
376
|
try {
|
|
377
|
+
if (guardTarget !== undefined) {
|
|
378
|
+
await guardTarget(join(ctx.artDir, `${ctx.basename}.fsm.coverage.test.ts`));
|
|
379
|
+
}
|
|
314
380
|
const coverage = await emitFsmCoverageTest({
|
|
315
381
|
artifactDir: ctx.artDir,
|
|
316
382
|
basename: ctx.basename,
|
|
@@ -324,6 +390,38 @@ async function emitVerification(result, ctx) {
|
|
|
324
390
|
}
|
|
325
391
|
return { ...result, outputs, diagnostics };
|
|
326
392
|
}
|
|
393
|
+
/** Exact deterministic writes attempted by {@link emitVerification}. */
|
|
394
|
+
function verificationHostTargets(ctx) {
|
|
395
|
+
if (!isReservedPipeline(ctx.pipeline))
|
|
396
|
+
return [];
|
|
397
|
+
const fsm = ctx.plan.find((artifact) => artifact.phase.target.format === 'fsm');
|
|
398
|
+
const hasGears = ctx.plan.some((artifact) => artifact.phase.target.format === 'gears');
|
|
399
|
+
if (fsm === undefined || !hasGears)
|
|
400
|
+
return [];
|
|
401
|
+
const canonicalFsm = join(ctx.artDir, `${ctx.basename}.fsm${fsm.phase.target.ext}`);
|
|
402
|
+
if (fsm.path !== canonicalFsm)
|
|
403
|
+
return [];
|
|
404
|
+
return [
|
|
405
|
+
...verifierSupportFiles(ctx.artDir).map(({ source, target }) => ({
|
|
406
|
+
path: target,
|
|
407
|
+
protectedInputs: [source],
|
|
408
|
+
allowVerifierOutput: true,
|
|
409
|
+
})),
|
|
410
|
+
{ path: join(ctx.artDir, `${ctx.basename}.gears-fsm.test.ts`) },
|
|
411
|
+
{
|
|
412
|
+
path: join(ctx.artDir, `${ctx.basename}.fsm.introspect.test.ts`),
|
|
413
|
+
required: false,
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
path: join(ctx.artDir, `${ctx.basename}.prompt-contract.test.ts`),
|
|
417
|
+
required: false,
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
path: join(ctx.artDir, `${ctx.basename}.fsm.coverage.test.ts`),
|
|
421
|
+
required: false,
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
}
|
|
327
425
|
/** Resolves the built-in pipeline-agnostic normalize definition (DR-013). */
|
|
328
426
|
export function normalizeDefinitionPath() {
|
|
329
427
|
return fileURLToPath(new URL('./normalize.md', import.meta.url));
|
|
@@ -389,6 +487,7 @@ function compileStep(pipeline, phase, source, target) {
|
|
|
389
487
|
target,
|
|
390
488
|
},
|
|
391
489
|
phase: phase.name,
|
|
490
|
+
pinKey: phase.name,
|
|
392
491
|
targetExt: phase.target.ext,
|
|
393
492
|
};
|
|
394
493
|
}
|
|
@@ -397,7 +496,9 @@ function compileStep(pipeline, phase, source, target) {
|
|
|
397
496
|
* the pin index and stopping at the first failure with its report (PHEXEC-9,
|
|
398
497
|
* PHEXEC-27). An unparseable pin file fails the run closed before any phase.
|
|
399
498
|
*/
|
|
400
|
-
async function executeSteps(steps, pipeline, deps) {
|
|
499
|
+
async function executeSteps(steps, pipeline, deps, opts = {}) {
|
|
500
|
+
const complete = opts.complete ?? (async (result) => result);
|
|
501
|
+
const pinInputs = new Set([join(pipeline.dir, PINS_FILE)]);
|
|
401
502
|
let pinFile;
|
|
402
503
|
try {
|
|
403
504
|
pinFile = (await loadPinFile(pipeline.dir)).file;
|
|
@@ -409,7 +510,9 @@ async function executeSteps(steps, pipeline, deps) {
|
|
|
409
510
|
throw error;
|
|
410
511
|
}
|
|
411
512
|
if (pinFile !== undefined) {
|
|
412
|
-
const verdicts = await evaluatePinFile(pipeline.dir, pinFile
|
|
513
|
+
const verdicts = await evaluatePinFile(pipeline.dir, pinFile, {
|
|
514
|
+
observePath: (path) => pinInputs.add(resolve(path)),
|
|
515
|
+
});
|
|
413
516
|
const malformed = Object.entries(verdicts).find(([, verdict]) => verdict.status === 'malformed');
|
|
414
517
|
if (malformed !== undefined) {
|
|
415
518
|
return {
|
|
@@ -423,13 +526,107 @@ async function executeSteps(steps, pipeline, deps) {
|
|
|
423
526
|
};
|
|
424
527
|
}
|
|
425
528
|
}
|
|
426
|
-
const definitions =
|
|
529
|
+
const definitions = [
|
|
530
|
+
...new Set([
|
|
531
|
+
...chainDefinitions(pipeline),
|
|
532
|
+
...steps.map((step) => step.request.definitionPath),
|
|
533
|
+
]),
|
|
534
|
+
];
|
|
427
535
|
const outputs = [];
|
|
428
536
|
const diagnostics = [];
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
537
|
+
const state = opts.incremental === undefined
|
|
538
|
+
? undefined
|
|
539
|
+
: await loadIncremental(opts.incremental);
|
|
540
|
+
const invalidatedTargetDirs = new Set();
|
|
541
|
+
const targets = steps.map(stepTarget);
|
|
542
|
+
const declaredByStep = await Promise.all(steps.map((step) => stepDeclaredInputs(step, pipeline, pinFile)));
|
|
543
|
+
const declaredInputs = [
|
|
544
|
+
...new Set(declaredByStep.flatMap((declared) => declared.paths)),
|
|
545
|
+
];
|
|
546
|
+
const immutableInputs = externalInputPaths(steps);
|
|
547
|
+
const hostInputs = (opts.hostTargets ?? []).flatMap((target) => target.protectedInputs ?? []);
|
|
548
|
+
const protectedInputs = [
|
|
549
|
+
...new Set([
|
|
550
|
+
...immutableInputs,
|
|
551
|
+
...definitions,
|
|
552
|
+
...declaredInputs,
|
|
553
|
+
...hostInputs,
|
|
554
|
+
...pinInputs,
|
|
555
|
+
]),
|
|
556
|
+
];
|
|
557
|
+
const plannedTargets = opts.hostTargets ?? [];
|
|
558
|
+
for (let index = 0; index < plannedTargets.length; index++) {
|
|
559
|
+
const planned = plannedTargets[index];
|
|
560
|
+
try {
|
|
561
|
+
await assertSafeTarget(planned.path, [
|
|
562
|
+
...protectedInputs,
|
|
563
|
+
...targets,
|
|
564
|
+
...(planned.protectedInputs ?? []),
|
|
565
|
+
...plannedTargets
|
|
566
|
+
.filter((_, candidate) => candidate !== index)
|
|
567
|
+
.map((candidate) => candidate.path),
|
|
568
|
+
], { allowVerifierOutput: planned.allowVerifierOutput });
|
|
569
|
+
}
|
|
570
|
+
catch (error) {
|
|
571
|
+
if (planned.required !== false)
|
|
572
|
+
return failure(messageOf(error));
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const hostTargets = new Map((opts.hostTargets ?? []).map((target) => [resolve(target.path), target]));
|
|
576
|
+
const guardCompletionTarget = async (path) => {
|
|
577
|
+
const planned = hostTargets.get(resolve(path));
|
|
578
|
+
if (planned === undefined) {
|
|
579
|
+
throw new Error(`unplanned deterministic output: ${path}`);
|
|
580
|
+
}
|
|
581
|
+
await assertSafeTarget(planned.path, [...protectedInputs, ...targets, ...(planned.protectedInputs ?? [])], { allowVerifierOutput: planned.allowVerifierOutput });
|
|
582
|
+
};
|
|
583
|
+
for (let index = 0; index < steps.length; index++) {
|
|
584
|
+
const step = steps[index];
|
|
585
|
+
const target = targets[index];
|
|
586
|
+
const stepDeclared = declaredByStep[index];
|
|
587
|
+
const identity = state === undefined
|
|
588
|
+
? null
|
|
589
|
+
: await stepIdentity(step, state, index, stepDeclared.complete ? stepDeclared.paths : null);
|
|
590
|
+
const mode = state === undefined
|
|
591
|
+
? { mode: 'ordinary' }
|
|
592
|
+
: await selectStepMode(step, index, target, identity, state);
|
|
593
|
+
// Reuse still honors pin currency, but it constructs no executor because
|
|
594
|
+
// no phase runs. A stale/malformed pin can never be bypassed by history.
|
|
595
|
+
if (mode.mode === 'reuse') {
|
|
596
|
+
if (state === undefined) {
|
|
597
|
+
throw new Error('internal error: reuse selected without history state');
|
|
598
|
+
}
|
|
599
|
+
let reasons;
|
|
600
|
+
try {
|
|
601
|
+
reasons = await reusePinFailure(step.pinKey, pipeline.dir, pinFile);
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
reasons = [messageOf(error)];
|
|
605
|
+
}
|
|
606
|
+
if (reasons !== null) {
|
|
607
|
+
const startedAt = Date.now();
|
|
608
|
+
deps.progress?.({ kind: 'phase-start', phase: step.phase, target });
|
|
609
|
+
deps.progress?.({
|
|
610
|
+
kind: 'phase-fail',
|
|
611
|
+
phase: step.phase,
|
|
612
|
+
target,
|
|
613
|
+
elapsedMs: Date.now() - startedAt,
|
|
614
|
+
});
|
|
615
|
+
diagnostics.push(formatFailureReport({ phase: step.phase, target, reasons }));
|
|
616
|
+
return { ok: false, outputs, diagnostics };
|
|
617
|
+
}
|
|
618
|
+
deps.progress?.({
|
|
619
|
+
kind: 'status',
|
|
620
|
+
text: `reusing ${step.phase} → ${target}`,
|
|
621
|
+
});
|
|
622
|
+
state.accepted.push({
|
|
623
|
+
step,
|
|
624
|
+
target,
|
|
625
|
+
inputs: identity?.inputs ?? null,
|
|
626
|
+
output: hashBytes(mode.bytes),
|
|
627
|
+
});
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
433
630
|
// In-run progress (DR-019, CLI-32): announce the phase, then report its
|
|
434
631
|
// outcome with the elapsed time.
|
|
435
632
|
const startedAt = Date.now();
|
|
@@ -448,7 +645,7 @@ async function executeSteps(steps, pipeline, deps) {
|
|
|
448
645
|
// phase and target from the report (CLI-4, CLI-32, PHEXEC-27).
|
|
449
646
|
let selection;
|
|
450
647
|
try {
|
|
451
|
-
selection = await selectExecutor(step.phase, pipeline.dir, pinFile, deps);
|
|
648
|
+
selection = await selectExecutor(step.phase, step.pinKey, pipeline.dir, pinFile, deps);
|
|
452
649
|
}
|
|
453
650
|
catch (error) {
|
|
454
651
|
selection = { kind: 'fail', reasons: [messageOf(error)] };
|
|
@@ -462,12 +659,33 @@ async function executeSteps(steps, pipeline, deps) {
|
|
|
462
659
|
}));
|
|
463
660
|
return { ok: false, outputs, diagnostics };
|
|
464
661
|
}
|
|
662
|
+
if (mode.mode === 'update') {
|
|
663
|
+
deps.progress?.({
|
|
664
|
+
kind: 'status',
|
|
665
|
+
text: `updating ${step.phase} → ${target}`,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
const request = mode.mode === 'update' && step.request.kind === 'compile'
|
|
669
|
+
? {
|
|
670
|
+
...step.request,
|
|
671
|
+
update: { priorInput: mode.priorInput, diff: mode.diff },
|
|
672
|
+
}
|
|
673
|
+
: step.request;
|
|
465
674
|
const result = await runPhase({
|
|
466
|
-
request
|
|
675
|
+
request,
|
|
467
676
|
phase: step.phase,
|
|
468
677
|
targetExt: step.targetExt,
|
|
469
678
|
executor: selection.executor,
|
|
470
679
|
definitions,
|
|
680
|
+
protectedInputs: stepDeclared.paths,
|
|
681
|
+
aliasInputs: [
|
|
682
|
+
...immutableInputs,
|
|
683
|
+
...declaredInputs,
|
|
684
|
+
...hostInputs,
|
|
685
|
+
...pinInputs,
|
|
686
|
+
...targets.filter((_, candidate) => candidate !== index),
|
|
687
|
+
],
|
|
688
|
+
beforeExecute: () => invalidateForExecution(state, target, invalidatedTargetDirs),
|
|
471
689
|
revalidate: () => revalidateChain(pipeline.dir),
|
|
472
690
|
signal: deps.signal,
|
|
473
691
|
});
|
|
@@ -511,8 +729,326 @@ async function executeSteps(steps, pipeline, deps) {
|
|
|
511
729
|
elapsedMs: Date.now() - startedAt,
|
|
512
730
|
});
|
|
513
731
|
outputs.push(target);
|
|
732
|
+
if (state !== undefined) {
|
|
733
|
+
let output = null;
|
|
734
|
+
try {
|
|
735
|
+
output = hashBytes(await readFile(target));
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// A successful compile remains successful when only history capture is
|
|
739
|
+
// unavailable; final publication reports the advisory diagnostic.
|
|
740
|
+
}
|
|
741
|
+
state.executed++;
|
|
742
|
+
state.accepted.push({
|
|
743
|
+
step,
|
|
744
|
+
target,
|
|
745
|
+
inputs: identity?.inputs ?? null,
|
|
746
|
+
output,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const selected = {
|
|
751
|
+
ok: true,
|
|
752
|
+
outputs,
|
|
753
|
+
diagnostics,
|
|
754
|
+
...(state !== undefined &&
|
|
755
|
+
state.executed === 0 &&
|
|
756
|
+
steps.length > 0 &&
|
|
757
|
+
state.accepted.length === steps.length
|
|
758
|
+
? { outcome: 'up-to-date' }
|
|
759
|
+
: {}),
|
|
760
|
+
};
|
|
761
|
+
const completed = await complete(selected, guardCompletionTarget);
|
|
762
|
+
return state === undefined
|
|
763
|
+
? completed
|
|
764
|
+
: publishIncremental(completed, state, steps);
|
|
765
|
+
}
|
|
766
|
+
function stepTarget(step) {
|
|
767
|
+
return step.request.kind === 'compile'
|
|
768
|
+
? step.request.target
|
|
769
|
+
: step.request.linked;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Inputs supplied from outside this schedule. A normal predecessor target may
|
|
773
|
+
* feed its successor, but source/reference/link operands supplied by the user
|
|
774
|
+
* remain protected even when they collide with some scheduled output.
|
|
775
|
+
*/
|
|
776
|
+
function externalInputPaths(steps) {
|
|
777
|
+
const produced = new Set();
|
|
778
|
+
const external = new Set();
|
|
779
|
+
for (const step of steps) {
|
|
780
|
+
if (step.request.kind === 'compile') {
|
|
781
|
+
const source = resolve(step.request.source);
|
|
782
|
+
if (!produced.has(source))
|
|
783
|
+
external.add(source);
|
|
784
|
+
for (const reference of step.request.references ?? []) {
|
|
785
|
+
external.add(resolve(reference));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
for (const object of step.request.objects) {
|
|
790
|
+
const path = resolve(object);
|
|
791
|
+
if (!produced.has(path))
|
|
792
|
+
external.add(path);
|
|
793
|
+
}
|
|
794
|
+
external.add(resolve(step.request.linkTarget));
|
|
795
|
+
}
|
|
796
|
+
produced.add(resolve(stepTarget(step)));
|
|
797
|
+
}
|
|
798
|
+
return [...external];
|
|
799
|
+
}
|
|
800
|
+
async function loadIncremental(run) {
|
|
801
|
+
const sourceBytes = await readFile(run.source);
|
|
802
|
+
const active = await loadBuildHistory(run.artDir);
|
|
803
|
+
const sourceLocator = encodeLocator(run.artDir, run.source);
|
|
804
|
+
const history = !run.rebuild &&
|
|
805
|
+
active?.manifest.pipeline === run.pipelineName &&
|
|
806
|
+
active.manifest.source.path === sourceLocator
|
|
807
|
+
? active
|
|
808
|
+
: null;
|
|
809
|
+
return {
|
|
810
|
+
...run,
|
|
811
|
+
active: active !== null,
|
|
812
|
+
history,
|
|
813
|
+
sourceBytes,
|
|
814
|
+
invalidated: false,
|
|
815
|
+
executed: 0,
|
|
816
|
+
accepted: [],
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
async function stepIdentity(step, state, index, declared) {
|
|
820
|
+
if (declared === null)
|
|
821
|
+
return null;
|
|
822
|
+
try {
|
|
823
|
+
if (step.request.kind === 'compile') {
|
|
824
|
+
const chainedInput = index === 0 ? state.sourceBytes : await readFile(step.request.source);
|
|
825
|
+
const roots = [
|
|
826
|
+
step.request.definitionPath,
|
|
827
|
+
...(step.request.references ?? []),
|
|
828
|
+
];
|
|
829
|
+
const rootHashes = await Promise.all(roots.map(async (path) => hashBytes(await readFile(path))));
|
|
830
|
+
return {
|
|
831
|
+
chainedInput,
|
|
832
|
+
inputs: [
|
|
833
|
+
hashBytes(chainedInput),
|
|
834
|
+
...rootHashes,
|
|
835
|
+
...(await Promise.all(declared.map(async (path) => hashBytes(await readFile(path))))),
|
|
836
|
+
],
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const inputs = [];
|
|
840
|
+
for (const object of step.request.objects) {
|
|
841
|
+
const path = resolve(object);
|
|
842
|
+
inputs.push(framedHash('object', encodeLocator(state.artDir, path), hashBytes(await readFile(path))));
|
|
843
|
+
}
|
|
844
|
+
inputs.push(hashBytes(await readFile(step.request.definitionPath)));
|
|
845
|
+
inputs.push(...(await Promise.all(declared.map(async (path) => hashBytes(await readFile(path))))));
|
|
846
|
+
const linkTarget = resolve(step.request.linkTarget);
|
|
847
|
+
const targetInfo = await stat(linkTarget);
|
|
848
|
+
let targetKind;
|
|
849
|
+
let targetIdentity;
|
|
850
|
+
if (targetInfo.isFile()) {
|
|
851
|
+
targetKind = 'file';
|
|
852
|
+
targetIdentity = hashBytes(await readFile(linkTarget));
|
|
853
|
+
}
|
|
854
|
+
else if (targetInfo.isDirectory()) {
|
|
855
|
+
targetKind = 'directory';
|
|
856
|
+
const identity = await hashTree(linkTarget);
|
|
857
|
+
if (!isHash(identity))
|
|
858
|
+
throw new Error('invalid link-target tree hash');
|
|
859
|
+
targetIdentity = identity;
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
throw new Error('link target is neither a file nor directory');
|
|
863
|
+
}
|
|
864
|
+
inputs.push(framedHash('link-target', encodeLocator(state.artDir, linkTarget), targetKind, targetIdentity));
|
|
865
|
+
for (const option of step.request.options) {
|
|
866
|
+
inputs.push(framedHash('option', option.name, option.value));
|
|
867
|
+
}
|
|
868
|
+
return { inputs };
|
|
514
869
|
}
|
|
515
|
-
|
|
870
|
+
catch {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async function stepDeclaredInputs(step, pipeline, pinFile) {
|
|
875
|
+
const roots = step.request.kind === 'compile'
|
|
876
|
+
? [step.request.definitionPath, ...(step.request.references ?? [])]
|
|
877
|
+
: [step.request.definitionPath];
|
|
878
|
+
return declaredInputPaths(pipeline, pinFile, roots);
|
|
879
|
+
}
|
|
880
|
+
/** Current local semantic-input closure for definitions and references. */
|
|
881
|
+
async function declaredInputPaths(pipeline, pinFile, roots) {
|
|
882
|
+
const boundary = pinFile?.pathBoundary.path ?? '.';
|
|
883
|
+
const base = new Set(roots.map((path) => resolve(path)));
|
|
884
|
+
const seen = new Set(base);
|
|
885
|
+
const inputs = [];
|
|
886
|
+
let complete = true;
|
|
887
|
+
const normalize = resolve(normalizeDefinitionPath());
|
|
888
|
+
for (const root of roots) {
|
|
889
|
+
const absolute = resolve(root);
|
|
890
|
+
if (!absolute.toLowerCase().endsWith('.md') || absolute === normalize) {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
const locator = relative(pipeline.dir, absolute).split(sep).join('/');
|
|
894
|
+
const closure = new Set();
|
|
895
|
+
try {
|
|
896
|
+
await deriveClosure(pipeline.dir, boundary, locator, (path) => closure.add(path));
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
// This derivation exists only for incremental identity and best-effort
|
|
900
|
+
// target protection. Pin generation/currency remain fail-closed, while an
|
|
901
|
+
// otherwise interpretable phase degrades to Ordinary execution.
|
|
902
|
+
complete = false;
|
|
903
|
+
}
|
|
904
|
+
for (const path of closure) {
|
|
905
|
+
const absolutePath = resolve(path);
|
|
906
|
+
if (seen.has(absolutePath))
|
|
907
|
+
continue;
|
|
908
|
+
seen.add(absolutePath);
|
|
909
|
+
inputs.push(absolutePath);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return { paths: inputs, complete };
|
|
913
|
+
}
|
|
914
|
+
async function selectStepMode(step, index, target, identity, state) {
|
|
915
|
+
if (state.rebuild || state.history === null || identity === null) {
|
|
916
|
+
return { mode: 'ordinary' };
|
|
917
|
+
}
|
|
918
|
+
const record = state.history.manifest.steps[index];
|
|
919
|
+
const kind = step.request.kind;
|
|
920
|
+
if (record === undefined ||
|
|
921
|
+
record.kind !== kind ||
|
|
922
|
+
record.name !== step.phase ||
|
|
923
|
+
record.target !== encodeLocator(state.artDir, resolve(target))) {
|
|
924
|
+
return { mode: 'ordinary' };
|
|
925
|
+
}
|
|
926
|
+
let targetBytes;
|
|
927
|
+
try {
|
|
928
|
+
targetBytes = await readFile(target);
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
return { mode: 'ordinary' };
|
|
932
|
+
}
|
|
933
|
+
if (sameHashes(record.inputs, identity.inputs)) {
|
|
934
|
+
return { mode: 'reuse', bytes: targetBytes };
|
|
935
|
+
}
|
|
936
|
+
if (step.request.kind !== 'compile' || identity.chainedInput === undefined) {
|
|
937
|
+
return { mode: 'ordinary' };
|
|
938
|
+
}
|
|
939
|
+
const prior = await verifiedInput(state.history, index, record.inputs[0]);
|
|
940
|
+
if (prior === null)
|
|
941
|
+
return { mode: 'ordinary' };
|
|
942
|
+
const diff = record.inputs[0] === identity.inputs[0]
|
|
943
|
+
? ''
|
|
944
|
+
: renderInputDiff(prior.bytes, identity.chainedInput);
|
|
945
|
+
return { mode: 'update', priorInput: prior.path, diff };
|
|
946
|
+
}
|
|
947
|
+
function renderInputDiff(prior, current) {
|
|
948
|
+
try {
|
|
949
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
950
|
+
return unifiedLineDiff(decoder.decode(prior), decoder.decode(current));
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function framedHash(tag, ...fields) {
|
|
957
|
+
return hashBytes(Buffer.from(JSON.stringify([tag, ...fields]), 'utf8'));
|
|
958
|
+
}
|
|
959
|
+
function sameHashes(left, right) {
|
|
960
|
+
return (left.length === right.length &&
|
|
961
|
+
left.every((hash, index) => hash === right[index]));
|
|
962
|
+
}
|
|
963
|
+
async function reusePinFailure(pinKey, pipelineDir, pinFile) {
|
|
964
|
+
const record = ownPin(pinFile, pinKey);
|
|
965
|
+
if (pinFile === undefined || pinKey === undefined || record === undefined) {
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
const verdict = await evaluatePin(pipelineDir, pinFile, pinKey, record);
|
|
969
|
+
return verdict.status === 'current'
|
|
970
|
+
? null
|
|
971
|
+
: [`pin is ${verdict.status}: ${verdict.reason}`];
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Removes any complete snapshot that could otherwise bless this target after
|
|
975
|
+
* executor work. Eligible runs already know their artifact directory; excluded
|
|
976
|
+
* forms perform only this safety invalidation and never select from or publish
|
|
977
|
+
* history.
|
|
978
|
+
*/
|
|
979
|
+
async function invalidateForExecution(state, target, invalidatedTargetDirs) {
|
|
980
|
+
if (state !== undefined) {
|
|
981
|
+
if (state.invalidated)
|
|
982
|
+
return;
|
|
983
|
+
try {
|
|
984
|
+
await invalidateBuildHistory(state.artDir, state.active);
|
|
985
|
+
state.invalidated = true;
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
catch (error) {
|
|
989
|
+
throw new Error(`active build history could not be invalidated: ${messageOf(error)}`, { cause: error });
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const targetDir = dirname(resolve(target));
|
|
993
|
+
if (invalidatedTargetDirs.has(targetDir))
|
|
994
|
+
return;
|
|
995
|
+
const active = await loadBuildHistory(targetDir);
|
|
996
|
+
const targetLocator = encodeLocator(targetDir, resolve(target));
|
|
997
|
+
if (active !== null &&
|
|
998
|
+
active.manifest.steps.some((step) => step.target === targetLocator)) {
|
|
999
|
+
try {
|
|
1000
|
+
await invalidateBuildHistory(targetDir, true);
|
|
1001
|
+
}
|
|
1002
|
+
catch (error) {
|
|
1003
|
+
throw new Error(`active build history could not be invalidated: ${messageOf(error)}`, { cause: error });
|
|
1004
|
+
}
|
|
1005
|
+
invalidatedTargetDirs.add(targetDir);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async function publishIncremental(result, state, steps) {
|
|
1009
|
+
if (!result.ok || state.executed === 0)
|
|
1010
|
+
return result;
|
|
1011
|
+
if (state.accepted.length !== steps.length ||
|
|
1012
|
+
state.accepted.some((entry) => entry.inputs === null || entry.output === null)) {
|
|
1013
|
+
return historyDiagnostic(result, 'one or more phase identities or output bytes were unavailable');
|
|
1014
|
+
}
|
|
1015
|
+
const records = [];
|
|
1016
|
+
try {
|
|
1017
|
+
for (const accepted of state.accepted) {
|
|
1018
|
+
const bytes = await readFile(accepted.target);
|
|
1019
|
+
if (hashBytes(bytes) !== accepted.output) {
|
|
1020
|
+
return historyDiagnostic(result, `accepted output changed before publication: ${accepted.target}`);
|
|
1021
|
+
}
|
|
1022
|
+
records.push({
|
|
1023
|
+
kind: accepted.step.request.kind,
|
|
1024
|
+
name: accepted.step.phase,
|
|
1025
|
+
target: accepted.target,
|
|
1026
|
+
inputs: accepted.inputs,
|
|
1027
|
+
output: accepted.output,
|
|
1028
|
+
bytes,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
await recordBuild({
|
|
1032
|
+
artDir: state.artDir,
|
|
1033
|
+
pipeline: state.pipelineName,
|
|
1034
|
+
sourcePath: state.source,
|
|
1035
|
+
sourceBytes: state.sourceBytes,
|
|
1036
|
+
steps: records,
|
|
1037
|
+
});
|
|
1038
|
+
return result;
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
return historyDiagnostic(result, messageOf(error));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function historyDiagnostic(result, reason) {
|
|
1045
|
+
return {
|
|
1046
|
+
...result,
|
|
1047
|
+
diagnostics: [
|
|
1048
|
+
...result.diagnostics,
|
|
1049
|
+
`slc: build history not recorded: ${reason}`,
|
|
1050
|
+
],
|
|
1051
|
+
};
|
|
516
1052
|
}
|
|
517
1053
|
/**
|
|
518
1054
|
* Selects a phase's execution strategy from the pin index (PHEXEC-27; DR-005,
|
|
@@ -520,12 +1056,12 @@ async function executeSteps(steps, pipeline, deps) {
|
|
|
520
1056
|
* artifact, and a stale or malformed pin fails closed and is never silently
|
|
521
1057
|
* interpreted.
|
|
522
1058
|
*/
|
|
523
|
-
async function selectExecutor(phase, pipelineDir, pinFile, deps) {
|
|
524
|
-
const record = pinFile
|
|
525
|
-
if (pinFile === undefined || record === undefined) {
|
|
1059
|
+
async function selectExecutor(phase, pinKey, pipelineDir, pinFile, deps) {
|
|
1060
|
+
const record = ownPin(pinFile, pinKey);
|
|
1061
|
+
if (pinFile === undefined || pinKey === undefined || record === undefined) {
|
|
526
1062
|
return { kind: 'run', executor: deps.executor };
|
|
527
1063
|
}
|
|
528
|
-
const verdict = await evaluatePin(pipelineDir, pinFile,
|
|
1064
|
+
const verdict = await evaluatePin(pipelineDir, pinFile, pinKey, record);
|
|
529
1065
|
if (verdict.status === 'current') {
|
|
530
1066
|
if (deps.compiled === undefined) {
|
|
531
1067
|
return {
|
|
@@ -537,7 +1073,7 @@ async function selectExecutor(phase, pipelineDir, pinFile, deps) {
|
|
|
537
1073
|
}
|
|
538
1074
|
return {
|
|
539
1075
|
kind: 'run',
|
|
540
|
-
executor: deps.compiled({ phase, pipelineDir, record }),
|
|
1076
|
+
executor: deps.compiled({ phase: pinKey, pipelineDir, record }),
|
|
541
1077
|
};
|
|
542
1078
|
}
|
|
543
1079
|
return {
|
|
@@ -545,6 +1081,13 @@ async function selectExecutor(phase, pipelineDir, pinFile, deps) {
|
|
|
545
1081
|
reasons: [`pin is ${verdict.status}: ${verdict.reason}`],
|
|
546
1082
|
};
|
|
547
1083
|
}
|
|
1084
|
+
function ownPin(pinFile, pinKey) {
|
|
1085
|
+
return pinFile !== undefined &&
|
|
1086
|
+
pinKey !== undefined &&
|
|
1087
|
+
Object.hasOwn(pinFile.pins, pinKey)
|
|
1088
|
+
? pinFile.pins[pinKey]
|
|
1089
|
+
: undefined;
|
|
1090
|
+
}
|
|
548
1091
|
function phaseDefinition(pipeline, name) {
|
|
549
1092
|
return join(pipeline.dir, `${name}.md`);
|
|
550
1093
|
}
|
|
@@ -570,7 +1113,4 @@ async function revalidateChain(dir) {
|
|
|
570
1113
|
function failure(message) {
|
|
571
1114
|
return { ok: false, outputs: [], diagnostics: [message] };
|
|
572
1115
|
}
|
|
573
|
-
function messageOf(error) {
|
|
574
|
-
return error instanceof Error ? error.message : String(error);
|
|
575
|
-
}
|
|
576
1116
|
//# sourceMappingURL=runner.js.map
|