@aviaratech/ai-delivery 0.3.16 → 0.3.18
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 +65 -0
- package/dist/agent.d.ts +2 -1
- package/dist/agent.js +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/lifecycle.test.js +1383 -4
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/utils/lockfile.d.ts +3 -1
- package/dist/utils/lockfile.js +6 -2
- package/dist/utils/lockfile.js.map +1 -1
- package/dist/verification.d.ts +138 -0
- package/dist/verification.js +1008 -13
- package/dist/verification.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ai-delivery/.claude-plugin/plugin.json +1 -1
package/dist/verification.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
|
-
import { closeSync, existsSync, fsyncSync, openSync, unlinkSync, lstatSync, mkdirSync, rmdirSync, renameSync, readFileSync, readlinkSync, readdirSync, realpathSync, statfsSync, } from 'node:fs';
|
|
4
|
+
import { closeSync, existsSync, fsyncSync, fstatSync, openSync, unlinkSync, lstatSync, mkdirSync, rmdirSync, renameSync, readFileSync, readSync, readlinkSync, readdirSync, realpathSync, statfsSync, } from 'node:fs';
|
|
5
5
|
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { z } from 'zod';
|
|
@@ -9,8 +9,10 @@ import { loadDeliveryConfig } from './config/deliveryConfig.js';
|
|
|
9
9
|
import { assertPrivateFile } from './delivery/common.js';
|
|
10
10
|
import { classifyRepositoryExactRange, assertRepositoryClassificationCurrent, createRepositoryDeliveryEvidence, createRepositoryStageAggregate, createRepositoryStageInput, createRepositoryStageReceipt, digestBytes, digestValue, assertRepositoryStageProof, loadRepositoryStageCheckpoint, loadSelectedRepositoryPolicy, RepositoryClassificationReceiptSchema, RepositoryStageAggregateSchema, RepositoryStageReceiptSchema, writeRepositoryCommandOutput, writeRepositoryDeliveryEvidence, writeRepositoryStageAggregate, writeRepositoryStageCheckpoint, } from './delivery/index.js';
|
|
11
11
|
import { DeliveryError } from './errors.js';
|
|
12
|
+
import { createDeliveryGitHubClients } from './github/client.js';
|
|
12
13
|
import { assertClean, changedPaths, coordinate, defaultBaseRef, git, gitCommonDir, gitRoot } from './git.js';
|
|
13
|
-
import { getIssueWorktreeStrict } from './services/worktreeRegistry.js';
|
|
14
|
+
import { assertAiDeliveryWorktreeOwner, getIssueWorktreeStrict } from './services/worktreeRegistry.js';
|
|
15
|
+
import { assertDeliveryRuntimeAdmitted } from './services/deliveryAdmission.js';
|
|
14
16
|
import { withLock } from './utils/lockfile.js';
|
|
15
17
|
import { ensurePrivateDirectoryDurably, writePrivateJsonFileAtomically } from './utils/atomicJson.js';
|
|
16
18
|
const ResourceSummarySchema = z
|
|
@@ -512,6 +514,10 @@ function scanOutputRoots(roots, links, commandRunning = false, onMissingTarget)
|
|
|
512
514
|
current = lstatSync(path);
|
|
513
515
|
}
|
|
514
516
|
catch (error) {
|
|
517
|
+
// A previously validated alias removed during this visit has no physical payload.
|
|
518
|
+
// Continue the complete current scan, publishing no identity for the vanished alias.
|
|
519
|
+
if (commandRunning && error.code === 'ENOENT' && previous?.identity === identity)
|
|
520
|
+
return;
|
|
515
521
|
throw new OutputObservationError('Filesystem output alias identity readback failed.', path, error);
|
|
516
522
|
}
|
|
517
523
|
if (!current.isSymbolicLink() ||
|
|
@@ -786,9 +792,9 @@ function observeOwnedProcesses(state, snapshot) {
|
|
|
786
792
|
state.sampled = true;
|
|
787
793
|
return [...selected.values()];
|
|
788
794
|
}
|
|
789
|
-
function sampleOwnedTree(state, repoRoot, bounds, baseline, newOutputBytes) {
|
|
795
|
+
function sampleOwnedTree(state, repoRoot, bounds, baseline, newOutputBytes, controllerRssBytes = 0) {
|
|
790
796
|
const owned = observeOwnedProcesses(state, processSnapshot());
|
|
791
|
-
const rssBytes = owned.reduce((total, member) => total + member.rssBytes,
|
|
797
|
+
const rssBytes = owned.reduce((total, member) => total + member.rssBytes, controllerRssBytes);
|
|
792
798
|
if (!Number.isSafeInteger(rssBytes))
|
|
793
799
|
throw new DeliveryError('Owned aggregate RSS is outside the safe integer range.');
|
|
794
800
|
if (rssBytes > bounds.maxAggregateRssBytes) {
|
|
@@ -863,8 +869,28 @@ function terminateOwnedProcesses(state) {
|
|
|
863
869
|
process.kill(pid, 'SIGKILL');
|
|
864
870
|
}
|
|
865
871
|
catch (error) {
|
|
866
|
-
|
|
872
|
+
const code = error.code;
|
|
873
|
+
if (code === 'ESRCH')
|
|
874
|
+
return;
|
|
875
|
+
if (code !== 'EPERM')
|
|
867
876
|
throw error;
|
|
877
|
+
// macOS can return EPERM when the last group member exits after our snapshot.
|
|
878
|
+
// Accept only fresh absence/zombie proof; a live target remains a cleanup failure.
|
|
879
|
+
const readback = processSnapshot();
|
|
880
|
+
if (pid < 0) {
|
|
881
|
+
const currentRoot = readback.get(state.rootPid);
|
|
882
|
+
if (currentRoot !== undefined && currentRoot.identity !== state.rootIdentity)
|
|
883
|
+
throw new DeliveryError('Owned process group identity changed during cleanup signal readback.');
|
|
884
|
+
if ([...readback.values()].some((member) => member.pgid === state.rootPid && !member.status.startsWith('Z')))
|
|
885
|
+
throw error;
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
const current = readback.get(pid);
|
|
889
|
+
if (current !== undefined && current.identity !== state.tracked.get(pid)?.identity)
|
|
890
|
+
throw new DeliveryError('Tracked process identity changed during cleanup signal readback.');
|
|
891
|
+
if (current !== undefined && !current.status.startsWith('Z'))
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
868
894
|
}
|
|
869
895
|
};
|
|
870
896
|
if (group.length > 0)
|
|
@@ -916,14 +942,17 @@ export function assertWorktreeTransitionWriterQuiescent(worktreePath, bytes) {
|
|
|
916
942
|
throw new DeliveryError('Transition requires identity-bound command and process-group absence.');
|
|
917
943
|
}
|
|
918
944
|
}
|
|
919
|
-
async function withVerificationWriter(root, operation, transition, commonDirectory) {
|
|
945
|
+
async function withVerificationWriter(root, operation, transition, commonDirectory, refuseUnresolvedSourcePhase = false) {
|
|
920
946
|
const digest = worktreeDigest(root);
|
|
921
947
|
const path = join(commonDirectory ?? gitCommonDir(root), 'ai-delivery', 'writers@1', `${digest.slice(7)}.json`);
|
|
922
948
|
ensurePrivateDirectoryDurably(dirname(path));
|
|
923
949
|
return withLock(path, {
|
|
924
950
|
projectRoot: root,
|
|
925
951
|
timeout: 200,
|
|
952
|
+
...(transition?.onLockReleaseError === undefined ? {} : { onReleaseError: transition.onLockReleaseError }),
|
|
953
|
+
...(transition?.onLockCompromised === undefined ? {} : { onCompromised: transition.onLockCompromised }),
|
|
926
954
|
operation: async () => {
|
|
955
|
+
await assertNoUnsealedSourcePhase(commonDirectory ?? gitCommonDir(root), digest, refuseUnresolvedSourcePhase);
|
|
927
956
|
const snapshot = processSnapshot();
|
|
928
957
|
const owner = snapshot.get(process.pid);
|
|
929
958
|
if (owner === undefined || owner.status.startsWith('Z'))
|
|
@@ -1074,6 +1103,7 @@ export async function withWorktreeTransitionWriterAbsent(repoRoot, worktreePath,
|
|
|
1074
1103
|
projectRoot: repoRoot,
|
|
1075
1104
|
timeout: 200,
|
|
1076
1105
|
operation: async () => {
|
|
1106
|
+
await assertNoUnsealedSourcePhase(gitCommonDir(repoRoot), worktreeDigest(worktreePath));
|
|
1077
1107
|
try {
|
|
1078
1108
|
lstatSync(path);
|
|
1079
1109
|
}
|
|
@@ -1270,7 +1300,7 @@ function verificationEnvironmentDigest(root, resources, producer = producerDiges
|
|
|
1270
1300
|
function reportVerificationProgress(input) {
|
|
1271
1301
|
process.stderr.write(`ai-delivery.verify ${JSON.stringify(input)}\n`);
|
|
1272
1302
|
}
|
|
1273
|
-
function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds, baseline, onResourceSample, writer) {
|
|
1303
|
+
function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds, baseline, onResourceSample, writer, execution) {
|
|
1274
1304
|
const [executable, ...args] = argv;
|
|
1275
1305
|
if (!executable)
|
|
1276
1306
|
throw new DeliveryError('Policy selected an empty stage command.');
|
|
@@ -1289,9 +1319,14 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1289
1319
|
cwd: repoRoot,
|
|
1290
1320
|
detached: process.platform !== 'win32',
|
|
1291
1321
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1292
|
-
...(baseline?.gate === undefined
|
|
1322
|
+
...(baseline?.gate === undefined && execution === undefined
|
|
1293
1323
|
? {}
|
|
1294
|
-
: {
|
|
1324
|
+
: {
|
|
1325
|
+
env: {
|
|
1326
|
+
...(execution?.environment ?? process.env),
|
|
1327
|
+
...(baseline?.gate === undefined ? {} : { [OUTPUT_GATE_ENV]: JSON.stringify(baseline.gate) }),
|
|
1328
|
+
},
|
|
1329
|
+
}),
|
|
1295
1330
|
});
|
|
1296
1331
|
const stdout = [];
|
|
1297
1332
|
const stderr = [];
|
|
@@ -1299,6 +1334,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1299
1334
|
? undefined
|
|
1300
1335
|
: { rootPid: child.pid, sampled: false, tracked: new Map() };
|
|
1301
1336
|
let outputBytes = 0;
|
|
1337
|
+
let sampledOutputBytes = 0;
|
|
1302
1338
|
let failure;
|
|
1303
1339
|
let cleanupFailure;
|
|
1304
1340
|
let writerFailure;
|
|
@@ -1388,12 +1424,20 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1388
1424
|
child.stdout.destroy();
|
|
1389
1425
|
child.stderr.destroy();
|
|
1390
1426
|
child.unref();
|
|
1427
|
+
execution?.onCleanupFailure?.('Owned command pipes did not close; descendant cleanup could not be verified.');
|
|
1391
1428
|
reject(new DeliveryError(`Selected policy stage command failed (${failure}).${failedOutput()}${writerFailure === undefined ? '' : ` Writer state persistence failed (${writerFailure}).`} Owned process cleanup failed (owned command pipes did not close; descendant cleanup could not be verified).`));
|
|
1392
1429
|
}, 2_000);
|
|
1393
1430
|
}
|
|
1394
1431
|
};
|
|
1395
1432
|
const capture = (chunks, chunk) => {
|
|
1396
1433
|
outputBytes += chunk.length;
|
|
1434
|
+
execution?.onCaptured?.(outputBytes);
|
|
1435
|
+
if (execution !== undefined &&
|
|
1436
|
+
resourceBounds?.maxNewOutputBytes !== undefined &&
|
|
1437
|
+
outputBytes + sampledOutputBytes > resourceBounds.maxNewOutputBytes) {
|
|
1438
|
+
stop('source-phase captured output exceeded remaining cumulative allowance');
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1397
1441
|
if (outputBytes > 8 * 1024 * 1024) {
|
|
1398
1442
|
stop('captured output exceeded 8 MiB');
|
|
1399
1443
|
return;
|
|
@@ -1430,7 +1474,7 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1430
1474
|
observeOwnedProcesses(owned, processSnapshot());
|
|
1431
1475
|
writer?.observed(owned);
|
|
1432
1476
|
if (resourceBounds !== undefined) {
|
|
1433
|
-
const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline);
|
|
1477
|
+
const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, undefined, execution === undefined ? 0 : process.memoryUsage().rss);
|
|
1434
1478
|
onResourceSample?.(result);
|
|
1435
1479
|
onRunning?.(outputBytes, result);
|
|
1436
1480
|
}
|
|
@@ -1445,7 +1489,8 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1445
1489
|
});
|
|
1446
1490
|
if (failure !== undefined)
|
|
1447
1491
|
return;
|
|
1448
|
-
|
|
1492
|
+
sampledOutputBytes = bytes;
|
|
1493
|
+
const result = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, bytes + (execution === undefined ? 0 : outputBytes), execution === undefined ? 0 : process.memoryUsage().rss);
|
|
1449
1494
|
onResourceSample?.(result);
|
|
1450
1495
|
onRunning?.(outputBytes, result);
|
|
1451
1496
|
}
|
|
@@ -1485,7 +1530,10 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1485
1530
|
if (failure === undefined && owned !== undefined) {
|
|
1486
1531
|
try {
|
|
1487
1532
|
if (resourceBounds !== undefined) {
|
|
1488
|
-
const sample = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, baseline === undefined
|
|
1533
|
+
const sample = sampleOwnedTree(owned, repoRoot, resourceBounds, baseline, baseline === undefined
|
|
1534
|
+
? undefined
|
|
1535
|
+
: (await positiveNewOutputBytes(baseline, scanCancellation.signal)) +
|
|
1536
|
+
(execution === undefined ? 0 : outputBytes), execution === undefined ? 0 : process.memoryUsage().rss);
|
|
1489
1537
|
onResourceSample?.(sample);
|
|
1490
1538
|
onRunning?.(outputBytes, sample);
|
|
1491
1539
|
}
|
|
@@ -1520,6 +1568,8 @@ function runStageCommand(repoRoot, argv, abortSignal, onRunning, resourceBounds,
|
|
|
1520
1568
|
}
|
|
1521
1569
|
}
|
|
1522
1570
|
settled = true;
|
|
1571
|
+
if (cleanupFailure !== undefined)
|
|
1572
|
+
execution?.onCleanupFailure?.(cleanupFailure);
|
|
1523
1573
|
reject(new DeliveryError(`Selected policy stage command failed (${reason}). Command exit ${String(code)}${signal === null ? '' : ` signal ${signal}`}.${failedOutput()}${writerFailure === undefined ? '' : ` Writer state persistence failed (${writerFailure}).`}${cleanupFailure === undefined ? '' : ` Owned process cleanup failed (${cleanupFailure}).`}`));
|
|
1524
1574
|
return;
|
|
1525
1575
|
}
|
|
@@ -1890,6 +1940,951 @@ export function loadRemovedMergedRun(primaryRepoRoot, issueNumber) {
|
|
|
1890
1940
|
}
|
|
1891
1941
|
return run;
|
|
1892
1942
|
}
|
|
1943
|
+
const SourceDigest = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
|
|
1944
|
+
const SourceSha = z.string().regex(/^[a-f0-9]{40}$/u);
|
|
1945
|
+
const SourceArtifactSchema = z.strictObject({ path: z.string().min(1), digest: SourceDigest });
|
|
1946
|
+
const SourceFileSchema = SourceArtifactSchema.extend({
|
|
1947
|
+
device: z.number().int().nonnegative().safe(),
|
|
1948
|
+
inode: z.number().int().nonnegative().safe(),
|
|
1949
|
+
uid: z.number().int().nonnegative().safe(),
|
|
1950
|
+
mode: z.number().int().nonnegative().safe(),
|
|
1951
|
+
});
|
|
1952
|
+
const SourceCommandSchema = z.strictObject({
|
|
1953
|
+
argv: z.array(z.string()).min(1),
|
|
1954
|
+
cwd: z.string().min(1),
|
|
1955
|
+
executable: SourceFileSchema,
|
|
1956
|
+
});
|
|
1957
|
+
const SourceSnapshotSchema = z.strictObject({
|
|
1958
|
+
head: SourceSha,
|
|
1959
|
+
indexTree: SourceSha,
|
|
1960
|
+
branch: z.string().min(1),
|
|
1961
|
+
configDigest: SourceDigest,
|
|
1962
|
+
dirty: z.array(z.strictObject({ path: z.string().min(1), digest: SourceDigest.nullable() })),
|
|
1963
|
+
});
|
|
1964
|
+
const SourceBoundsSchema = z.strictObject({
|
|
1965
|
+
maxAggregateRssBytes: z.number().int().positive().safe(),
|
|
1966
|
+
maxNewOutputBytes: z.number().int().positive().safe(),
|
|
1967
|
+
minFreeDiskBytes: z.number().int().positive().safe(),
|
|
1968
|
+
outputRoots: z.array(z.string().min(1)).min(1),
|
|
1969
|
+
});
|
|
1970
|
+
const SourceReconciliationSchema = z.strictObject({
|
|
1971
|
+
phaseId: SourceDigest,
|
|
1972
|
+
recordId: SourceDigest,
|
|
1973
|
+
baselineId: SourceDigest.optional(),
|
|
1974
|
+
authorizationDigest: SourceDigest,
|
|
1975
|
+
});
|
|
1976
|
+
const IssueSourcePhaseInputSchema = z.strictObject({
|
|
1977
|
+
repoRoot: z.string().min(1),
|
|
1978
|
+
issueNumber: z.number().int().positive().safe(),
|
|
1979
|
+
identity: z.string().min(1),
|
|
1980
|
+
rowDigest: SourceDigest,
|
|
1981
|
+
controller: z.strictObject({
|
|
1982
|
+
head: SourceSha,
|
|
1983
|
+
configDigest: SourceDigest,
|
|
1984
|
+
admissionId: SourceDigest,
|
|
1985
|
+
runtimeEntryPath: z.string().min(1),
|
|
1986
|
+
packageVersion: z.string().min(1),
|
|
1987
|
+
archiveDigest: SourceDigest,
|
|
1988
|
+
}),
|
|
1989
|
+
source: SourceSnapshotSchema.extend({
|
|
1990
|
+
path: z.string().min(1),
|
|
1991
|
+
effect: z.discriminatedUnion('kind', [
|
|
1992
|
+
z.strictObject({ kind: z.literal('preserve') }),
|
|
1993
|
+
z.strictObject({ kind: z.literal('commitOnce'), parent: SourceSha, tree: SourceSha }),
|
|
1994
|
+
]),
|
|
1995
|
+
}),
|
|
1996
|
+
caller: SourceArtifactSchema,
|
|
1997
|
+
authorization: SourceArtifactSchema,
|
|
1998
|
+
inputs: z.array(SourceArtifactSchema),
|
|
1999
|
+
commands: z.array(SourceCommandSchema).min(1),
|
|
2000
|
+
commandGraphDigest: SourceDigest,
|
|
2001
|
+
environment: z.strictObject({ digest: SourceDigest, overrides: z.record(z.string(), z.string().nullable()) }),
|
|
2002
|
+
bounds: SourceBoundsSchema,
|
|
2003
|
+
completionArtifacts: z.array(z.string().min(1)),
|
|
2004
|
+
reconciliation: SourceReconciliationSchema.optional(),
|
|
2005
|
+
});
|
|
2006
|
+
const SourceBindingSchema = IssueSourcePhaseInputSchema.omit({ environment: true }).extend({
|
|
2007
|
+
environmentDigest: SourceDigest,
|
|
2008
|
+
});
|
|
2009
|
+
const SourcePhaseBaselineSchema = z.strictObject({
|
|
2010
|
+
roots: z.array(z.string().min(1)).min(1),
|
|
2011
|
+
files: z.array(z.tuple([z.string(), z.number().int().nonnegative().safe()])),
|
|
2012
|
+
links: z.array(z.tuple([z.string(), z.strictObject({ identity: z.string(), target: z.string(), missing: z.boolean() })])),
|
|
2013
|
+
baselineId: SourceDigest,
|
|
2014
|
+
});
|
|
2015
|
+
const SourcePhaseCommandResultSchema = z.strictObject({
|
|
2016
|
+
index: z.number().int().nonnegative().safe(),
|
|
2017
|
+
outputDigest: SourceDigest.nullable(),
|
|
2018
|
+
outputBytes: z.number().int().nonnegative().safe(),
|
|
2019
|
+
status: z.enum(['complete', 'failed']),
|
|
2020
|
+
});
|
|
2021
|
+
const SourcePhaseResultSchema = z.strictObject({
|
|
2022
|
+
source: SourceSnapshotSchema,
|
|
2023
|
+
artifacts: z.array(SourceFileSchema),
|
|
2024
|
+
newOutputBytes: z.number().int().nonnegative().safe(),
|
|
2025
|
+
writerReleased: z.literal(true),
|
|
2026
|
+
lockReleased: z.literal(true),
|
|
2027
|
+
});
|
|
2028
|
+
const SourcePhaseRecordSchema = z
|
|
2029
|
+
.strictObject({
|
|
2030
|
+
schemaVersion: z.literal('ai-delivery.source-phase@1'),
|
|
2031
|
+
phaseId: SourceDigest,
|
|
2032
|
+
recordId: SourceDigest,
|
|
2033
|
+
worktreeDigest: SourceDigest,
|
|
2034
|
+
bindingDigest: SourceDigest,
|
|
2035
|
+
authorizationDigest: SourceDigest,
|
|
2036
|
+
status: z.enum(['preparing', 'intent', 'complete', 'failed-quiescent', 'rejected-before-work', 'unresolved']),
|
|
2037
|
+
writerId: z.uuid(),
|
|
2038
|
+
binding: SourceBindingSchema.optional(),
|
|
2039
|
+
baseline: SourcePhaseBaselineSchema.optional(),
|
|
2040
|
+
allocation: SourceBoundsSchema.optional(),
|
|
2041
|
+
owner: z.strictObject({ pid: z.number().int().positive().safe(), identity: z.string().min(1) }),
|
|
2042
|
+
bootstrapFiles: z.array(z.tuple([z.string(), z.number().int().nonnegative().safe()])),
|
|
2043
|
+
bootstrapBytes: z.number().int().nonnegative().safe(),
|
|
2044
|
+
capturedBytes: z.number().int().nonnegative().safe(),
|
|
2045
|
+
nextCommandIndex: z.number().int().nonnegative().safe(),
|
|
2046
|
+
commands: z.array(SourcePhaseCommandResultSchema),
|
|
2047
|
+
claimDigest: SourceDigest.optional(),
|
|
2048
|
+
releaseDigest: SourceDigest.optional(),
|
|
2049
|
+
preparationArtifact: SourceFileSchema.optional(),
|
|
2050
|
+
writerArtifact: SourceFileSchema.optional(),
|
|
2051
|
+
producerDigest: SourceDigest.optional(),
|
|
2052
|
+
actor: z
|
|
2053
|
+
.strictObject({
|
|
2054
|
+
identity: z.string().min(1),
|
|
2055
|
+
actorLogin: z.string().min(1),
|
|
2056
|
+
credentialIdentity: z.string().min(1),
|
|
2057
|
+
})
|
|
2058
|
+
.optional(),
|
|
2059
|
+
retainedArtifacts: z.array(SourceFileSchema).optional(),
|
|
2060
|
+
retainedPredecessors: z.array(z.strictObject({ phaseId: SourceDigest, recordId: SourceDigest })),
|
|
2061
|
+
reconciliation: SourceReconciliationSchema.optional(),
|
|
2062
|
+
result: SourcePhaseResultSchema.optional(),
|
|
2063
|
+
failure: z
|
|
2064
|
+
.strictObject({
|
|
2065
|
+
operation: z.string().optional(),
|
|
2066
|
+
accounting: z.string().optional(),
|
|
2067
|
+
cleanup: z.string().optional(),
|
|
2068
|
+
writerRelease: z.string().optional(),
|
|
2069
|
+
lockRelease: z.string().optional(),
|
|
2070
|
+
})
|
|
2071
|
+
.optional(),
|
|
2072
|
+
})
|
|
2073
|
+
.superRefine((record, context) => {
|
|
2074
|
+
const { recordId, ...content } = record;
|
|
2075
|
+
if (recordId !== digestValue(content) || record.phaseId !== record.bindingDigest)
|
|
2076
|
+
context.addIssue({ code: 'custom', message: 'Source-phase record identity is invalid.' });
|
|
2077
|
+
if (record.binding !== undefined && digestValue(record.binding) !== record.bindingDigest)
|
|
2078
|
+
context.addIssue({ code: 'custom', message: 'Source-phase input binding is invalid.' });
|
|
2079
|
+
if (record.baseline !== undefined) {
|
|
2080
|
+
const { baselineId, ...baseline } = record.baseline;
|
|
2081
|
+
if (baselineId !== digestValue(baseline))
|
|
2082
|
+
context.addIssue({ code: 'custom', message: 'Source-phase baseline is invalid.' });
|
|
2083
|
+
}
|
|
2084
|
+
if (record.status === 'rejected-before-work' &&
|
|
2085
|
+
(record.commands.length !== 0 || record.nextCommandIndex !== 0 || record.capturedBytes !== 0))
|
|
2086
|
+
context.addIssue({ code: 'custom', message: 'Rejected source-phase input cannot contain executed work.' });
|
|
2087
|
+
if (record.status === 'complete' &&
|
|
2088
|
+
(record.result === undefined ||
|
|
2089
|
+
record.baseline === undefined ||
|
|
2090
|
+
record.binding === undefined ||
|
|
2091
|
+
record.failure !== undefined))
|
|
2092
|
+
context.addIssue({ code: 'custom', message: 'Source-phase completion is incomplete.' });
|
|
2093
|
+
if (record.status === 'complete' &&
|
|
2094
|
+
(record.commands.length !== record.binding?.commands.length ||
|
|
2095
|
+
record.nextCommandIndex !== record.commands.length ||
|
|
2096
|
+
record.commands.some((command, index) => command.index !== index || command.status !== 'complete')))
|
|
2097
|
+
context.addIssue({
|
|
2098
|
+
code: 'custom',
|
|
2099
|
+
message: 'Source-phase completion does not contain its exact successful command graph.',
|
|
2100
|
+
});
|
|
2101
|
+
});
|
|
2102
|
+
async function assertNoUnsealedSourcePhase(common, subject, refuseUnresolved = false) {
|
|
2103
|
+
const directory = join(common, 'ai-delivery', 'receipts', 'source-phase@1', subject.slice(7));
|
|
2104
|
+
let names;
|
|
2105
|
+
try {
|
|
2106
|
+
const stat = lstatSync(directory);
|
|
2107
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
2108
|
+
throw new DeliveryError('Source-phase admission metadata is not a canonical directory.');
|
|
2109
|
+
names = readdirSync(directory);
|
|
2110
|
+
}
|
|
2111
|
+
catch (error) {
|
|
2112
|
+
if (error.code === 'ENOENT')
|
|
2113
|
+
return;
|
|
2114
|
+
throw error;
|
|
2115
|
+
}
|
|
2116
|
+
if (names.length > 100_000)
|
|
2117
|
+
throw new DeliveryError('Source-phase admission exceeded its entry bound.');
|
|
2118
|
+
for (const name of names) {
|
|
2119
|
+
const record = SourcePhaseRecordSchema.parse(JSON.parse(assertPrivateFile(join(directory, name)).toString('utf8')));
|
|
2120
|
+
if (record.worktreeDigest !== subject || name !== `${record.phaseId.slice(7)}.json`)
|
|
2121
|
+
throw new DeliveryError('Source-phase admission record belongs to another input or worktree.');
|
|
2122
|
+
if (record.status === 'preparing' || record.status === 'intent')
|
|
2123
|
+
throw new DeliveryError('An unsealed source phase still excludes another worktree writer.');
|
|
2124
|
+
if (refuseUnresolved && record.status === 'unresolved')
|
|
2125
|
+
throw new DeliveryError('Unresolved source phase requires owning reconciliation before another attempt.');
|
|
2126
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
const IssueSourcePhaseReceiptSchema = z.strictObject({
|
|
2130
|
+
status: z.literal('complete'),
|
|
2131
|
+
phaseId: SourceDigest,
|
|
2132
|
+
recordId: SourceDigest,
|
|
2133
|
+
source: SourceSnapshotSchema,
|
|
2134
|
+
commands: z.array(SourcePhaseCommandResultSchema),
|
|
2135
|
+
});
|
|
2136
|
+
function sourceFile(path) {
|
|
2137
|
+
const stat = lstatSync(path);
|
|
2138
|
+
if (!stat.isFile() || realpathSync(path) !== path)
|
|
2139
|
+
throw new DeliveryError('Source-phase artifact must be a canonical regular file.');
|
|
2140
|
+
const descriptor = openSync(path, 'r');
|
|
2141
|
+
const hash = createHash('sha256');
|
|
2142
|
+
try {
|
|
2143
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
2144
|
+
let bytes;
|
|
2145
|
+
while ((bytes = readSync(descriptor, buffer, 0, buffer.length, null)) > 0)
|
|
2146
|
+
hash.update(buffer.subarray(0, bytes));
|
|
2147
|
+
const current = fstatSync(descriptor);
|
|
2148
|
+
const named = lstatSync(path);
|
|
2149
|
+
if (current.dev !== stat.dev ||
|
|
2150
|
+
current.ino !== stat.ino ||
|
|
2151
|
+
current.size !== stat.size ||
|
|
2152
|
+
current.mode !== stat.mode ||
|
|
2153
|
+
current.uid !== stat.uid ||
|
|
2154
|
+
current.mtimeMs !== stat.mtimeMs ||
|
|
2155
|
+
current.ctimeMs !== stat.ctimeMs ||
|
|
2156
|
+
named.dev !== current.dev ||
|
|
2157
|
+
named.ino !== current.ino ||
|
|
2158
|
+
realpathSync(path) !== path)
|
|
2159
|
+
throw new DeliveryError('Source-phase artifact changed during identity-bound hashing.');
|
|
2160
|
+
}
|
|
2161
|
+
finally {
|
|
2162
|
+
closeSync(descriptor);
|
|
2163
|
+
}
|
|
2164
|
+
return {
|
|
2165
|
+
path,
|
|
2166
|
+
digest: `sha256:${hash.digest('hex')}`,
|
|
2167
|
+
device: stat.dev,
|
|
2168
|
+
inode: stat.ino,
|
|
2169
|
+
uid: stat.uid,
|
|
2170
|
+
mode: stat.mode,
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
function sourceDirty(root) {
|
|
2174
|
+
const paths = [
|
|
2175
|
+
...new Set([
|
|
2176
|
+
...git(root, 'diff', '--cached', '--name-only', '-z').split('\0'),
|
|
2177
|
+
...git(root, 'ls-files', '-m', '-o', '--exclude-standard', '-z').split('\0'),
|
|
2178
|
+
].filter(Boolean)),
|
|
2179
|
+
].sort();
|
|
2180
|
+
return paths.map((path) => {
|
|
2181
|
+
const absolute = resolve(root, path);
|
|
2182
|
+
if (!absolute.startsWith(`${root}${sep}`))
|
|
2183
|
+
throw new DeliveryError('Source-phase dirty path escapes the worktree.');
|
|
2184
|
+
try {
|
|
2185
|
+
lstatSync(absolute);
|
|
2186
|
+
}
|
|
2187
|
+
catch (error) {
|
|
2188
|
+
if (error.code === 'ENOENT')
|
|
2189
|
+
return { path, digest: null };
|
|
2190
|
+
throw error;
|
|
2191
|
+
}
|
|
2192
|
+
return { path, digest: sourceFile(absolute).digest };
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
function sourceReceipt(record) {
|
|
2196
|
+
if (record.status !== 'complete' || record.result === undefined)
|
|
2197
|
+
throw new DeliveryError('Source phase is not complete.');
|
|
2198
|
+
return IssueSourcePhaseReceiptSchema.parse({
|
|
2199
|
+
status: 'complete',
|
|
2200
|
+
phaseId: record.phaseId,
|
|
2201
|
+
recordId: record.recordId,
|
|
2202
|
+
source: record.result.source,
|
|
2203
|
+
commands: record.commands,
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
/** Run an authorized, frozen source graph under the registered issue's existing writer fence. */
|
|
2207
|
+
export async function withIssueSourcePhase(supplied, operation) {
|
|
2208
|
+
const { signal, ...values } = supplied;
|
|
2209
|
+
const input = IssueSourcePhaseInputSchema.parse(values);
|
|
2210
|
+
assertResourceBounds(input.bounds);
|
|
2211
|
+
if (signal?.aborted)
|
|
2212
|
+
throw new DeliveryError('Source phase cancelled before preparation.');
|
|
2213
|
+
const root = gitRoot(input.repoRoot);
|
|
2214
|
+
if (root !== input.repoRoot ||
|
|
2215
|
+
root !== primaryRoot(root) ||
|
|
2216
|
+
realpathSync(input.source.path) !== input.source.path ||
|
|
2217
|
+
gitCommonDir(root) !== gitCommonDir(input.source.path))
|
|
2218
|
+
throw new DeliveryError('Source phase requires canonical primary controller and issue worktree roots.');
|
|
2219
|
+
const row = getIssueWorktreeStrict(input.issueNumber, root);
|
|
2220
|
+
assertAiDeliveryWorktreeOwner(row, root);
|
|
2221
|
+
if (!row.identity ||
|
|
2222
|
+
row.path !== input.source.path ||
|
|
2223
|
+
row.branch !== input.source.branch ||
|
|
2224
|
+
!['active', 'pr-published'].includes(row.status) ||
|
|
2225
|
+
digestValue(row) !== input.rowDigest)
|
|
2226
|
+
throw new DeliveryError('Source phase disagrees with the exact registered issue owner.');
|
|
2227
|
+
const environment = { ...process.env };
|
|
2228
|
+
delete environment[OUTPUT_GATE_ENV];
|
|
2229
|
+
for (const [name, value] of Object.entries(input.environment.overrides)) {
|
|
2230
|
+
if (name === OUTPUT_GATE_ENV)
|
|
2231
|
+
throw new DeliveryError('Source phase cannot supply an output observer capability.');
|
|
2232
|
+
if (value === null)
|
|
2233
|
+
delete environment[name];
|
|
2234
|
+
else
|
|
2235
|
+
environment[name] = value;
|
|
2236
|
+
}
|
|
2237
|
+
Object.freeze(environment);
|
|
2238
|
+
const { environment: _environment, ...bound } = input;
|
|
2239
|
+
const binding = SourceBindingSchema.parse({ ...bound, environmentDigest: input.environment.digest });
|
|
2240
|
+
const phaseId = digestValue(binding);
|
|
2241
|
+
const subject = worktreeDigest(row.path);
|
|
2242
|
+
const common = gitCommonDir(root);
|
|
2243
|
+
const directory = join(common, 'ai-delivery', 'receipts', 'source-phase@1', subject.slice(7));
|
|
2244
|
+
const path = join(directory, `${phaseId.slice(7)}.json`);
|
|
2245
|
+
const writerPath = join(common, 'ai-delivery', 'writers@1', `${subject.slice(7)}.json`);
|
|
2246
|
+
const roots = input.bounds.outputRoots.map((value) => resolve(root, value)).sort();
|
|
2247
|
+
if (roots.some((value, index) => index > 0 && (value === roots[index - 1] || value.startsWith(`${roots[index - 1]}${sep}`))))
|
|
2248
|
+
throw new DeliveryError('Source-phase output roots must not overlap.');
|
|
2249
|
+
for (const required of [
|
|
2250
|
+
directory,
|
|
2251
|
+
writerPath,
|
|
2252
|
+
join(common, 'ai-delivery', 'receipts'),
|
|
2253
|
+
join(common, 'ai-delivery', 'output-observation', subject.slice(7)),
|
|
2254
|
+
]) {
|
|
2255
|
+
if (!roots.some((value) => required === value || required.startsWith(`${value}${sep}`)))
|
|
2256
|
+
throw new DeliveryError('Source-phase roots must account for writer, observer and SDK receipt metadata.');
|
|
2257
|
+
}
|
|
2258
|
+
const cancelled = new AbortController();
|
|
2259
|
+
const combined = signal === undefined ? cancelled.signal : AbortSignal.any([signal, cancelled.signal]);
|
|
2260
|
+
let record;
|
|
2261
|
+
let baseline;
|
|
2262
|
+
let reused = false;
|
|
2263
|
+
let created = false;
|
|
2264
|
+
let entered = false;
|
|
2265
|
+
let startedCommand = false;
|
|
2266
|
+
let validated = false;
|
|
2267
|
+
let writerReleased = false;
|
|
2268
|
+
let lockFailure;
|
|
2269
|
+
let releaseFailure;
|
|
2270
|
+
let operationFailure;
|
|
2271
|
+
let accountingFailure;
|
|
2272
|
+
let cleanupFailure;
|
|
2273
|
+
let active;
|
|
2274
|
+
let expired = false;
|
|
2275
|
+
let captured = 0;
|
|
2276
|
+
let bootstrapCharge = 0;
|
|
2277
|
+
let previousBootstrap = 0;
|
|
2278
|
+
let writerBootstrap = 0;
|
|
2279
|
+
let predecessorChain = [];
|
|
2280
|
+
let accountingBounds = input.bounds;
|
|
2281
|
+
let protocolFailure;
|
|
2282
|
+
let commandFailure;
|
|
2283
|
+
const attempted = new Set();
|
|
2284
|
+
let finalSource;
|
|
2285
|
+
let artifacts = [];
|
|
2286
|
+
let newBytes = 0;
|
|
2287
|
+
const writerId = randomUUID();
|
|
2288
|
+
const started = Date.now();
|
|
2289
|
+
const message = (error) => (error instanceof Error ? error.message : String(error));
|
|
2290
|
+
const persist = () => {
|
|
2291
|
+
if (record === undefined)
|
|
2292
|
+
throw new DeliveryError('Source-phase intent is unavailable.');
|
|
2293
|
+
const { recordId: _recordId, ...content } = record;
|
|
2294
|
+
record = SourcePhaseRecordSchema.parse({ ...content, recordId: digestValue(content) });
|
|
2295
|
+
writePrivateJsonFileAtomically(path, record);
|
|
2296
|
+
};
|
|
2297
|
+
const readRecord = (file) => {
|
|
2298
|
+
const value = SourcePhaseRecordSchema.parse(JSON.parse(assertPrivateFile(file).toString('utf8')));
|
|
2299
|
+
if (value.worktreeDigest !== subject || basename(file) !== `${value.phaseId.slice(7)}.json`)
|
|
2300
|
+
throw new DeliveryError('Source-phase record belongs to another input or worktree.');
|
|
2301
|
+
return value;
|
|
2302
|
+
};
|
|
2303
|
+
const assertArtifact = (artifact) => {
|
|
2304
|
+
if (sourceFile(artifact.path).digest !== artifact.digest)
|
|
2305
|
+
throw new DeliveryError('Frozen source-phase input artifact changed.');
|
|
2306
|
+
};
|
|
2307
|
+
const sourceState = async (allowFinal = false) => {
|
|
2308
|
+
const head = git(row.path, 'rev-parse', 'HEAD');
|
|
2309
|
+
const branch = git(row.path, 'branch', '--show-current');
|
|
2310
|
+
const dirty = sourceDirty(row.path);
|
|
2311
|
+
const candidate = await loadDeliveryConfig(row.path);
|
|
2312
|
+
if (branch !== input.source.branch || candidate.configDigest !== input.source.configDigest)
|
|
2313
|
+
throw new DeliveryError('Source-phase candidate branch or configuration changed.');
|
|
2314
|
+
if (head === input.source.head) {
|
|
2315
|
+
if (git(row.path, 'diff', '--cached', '--name-only', input.source.indexTree) !== '' ||
|
|
2316
|
+
digestValue(dirty) !== digestValue(input.source.dirty))
|
|
2317
|
+
throw new DeliveryError('Source-phase frozen index or dirty source changed.');
|
|
2318
|
+
return { head, branch, indexTree: input.source.indexTree, configDigest: candidate.configDigest, dirty };
|
|
2319
|
+
}
|
|
2320
|
+
if (!allowFinal)
|
|
2321
|
+
throw new DeliveryError('Source phase requires its exact initial source before new work.');
|
|
2322
|
+
if (input.source.effect.kind !== 'commitOnce' ||
|
|
2323
|
+
input.source.effect.parent !== input.source.head ||
|
|
2324
|
+
git(row.path, 'rev-list', '--parents', '-n', '1', head) !== `${head} ${input.source.effect.parent}` ||
|
|
2325
|
+
git(row.path, 'rev-parse', 'HEAD^{tree}') !== input.source.effect.tree ||
|
|
2326
|
+
git(row.path, 'diff', '--cached', '--name-only', head) !== '' ||
|
|
2327
|
+
dirty.length !== 0)
|
|
2328
|
+
throw new DeliveryError('Source-phase commit does not match its exact parent, tree and clean postconditions.');
|
|
2329
|
+
return { head, branch, indexTree: input.source.effect.tree, configDigest: candidate.configDigest, dirty };
|
|
2330
|
+
};
|
|
2331
|
+
let actor;
|
|
2332
|
+
const validate = async (allowFinal = false) => {
|
|
2333
|
+
const currentRow = getIssueWorktreeStrict(input.issueNumber, root);
|
|
2334
|
+
assertAiDeliveryWorktreeOwner(currentRow, root);
|
|
2335
|
+
const configuration = await loadDeliveryConfig(root);
|
|
2336
|
+
const admission = await assertDeliveryRuntimeAdmitted({
|
|
2337
|
+
repoRoot: root,
|
|
2338
|
+
runtimeEntryPath: input.controller.runtimeEntryPath,
|
|
2339
|
+
configuration,
|
|
2340
|
+
});
|
|
2341
|
+
if (dirname(realpathSync(input.controller.runtimeEntryPath)) !== dirname(realpathSync(fileURLToPath(import.meta.url))))
|
|
2342
|
+
throw new DeliveryError('Source-phase executing SDK installation is not the selected admitted runtime.');
|
|
2343
|
+
if (digestValue(currentRow) !== input.rowDigest ||
|
|
2344
|
+
currentRow.identity !== row.identity ||
|
|
2345
|
+
configuration.config.roles.author.identity !== input.identity ||
|
|
2346
|
+
git(root, 'rev-parse', 'HEAD') !== input.controller.head ||
|
|
2347
|
+
configuration.configDigest !== input.controller.configDigest ||
|
|
2348
|
+
admission.admissionId !== input.controller.admissionId ||
|
|
2349
|
+
admission.sourceArchiveSha256 !== input.controller.archiveDigest ||
|
|
2350
|
+
admission.packageVersion !== input.controller.packageVersion ||
|
|
2351
|
+
digestValue(environment) !== input.environment.digest ||
|
|
2352
|
+
digestValue(input.commands) !== input.commandGraphDigest)
|
|
2353
|
+
throw new DeliveryError('Source-phase controller, selected runtime, actor or command environment binding changed.');
|
|
2354
|
+
const clients = await createDeliveryGitHubClients({
|
|
2355
|
+
config: configuration.config,
|
|
2356
|
+
identity: input.identity,
|
|
2357
|
+
role: 'author',
|
|
2358
|
+
env: environment,
|
|
2359
|
+
});
|
|
2360
|
+
if (clients.role !== 'author' || clients.authenticatedAuthor === undefined)
|
|
2361
|
+
throw new DeliveryError('Source-phase configured author authentication is unavailable.');
|
|
2362
|
+
const authenticated = await clients.authenticatedAuthor();
|
|
2363
|
+
if (!authenticated.actorLogin || !authenticated.credentialIdentity)
|
|
2364
|
+
throw new DeliveryError('Source-phase configured author identity readback is incomplete.');
|
|
2365
|
+
const currentActor = { identity: input.identity, ...authenticated };
|
|
2366
|
+
if (actor !== undefined && digestValue(actor) !== digestValue(currentActor))
|
|
2367
|
+
throw new DeliveryError('Source-phase authenticated author changed.');
|
|
2368
|
+
actor = currentActor;
|
|
2369
|
+
for (const artifact of [input.caller, input.authorization, ...input.inputs])
|
|
2370
|
+
assertArtifact(artifact);
|
|
2371
|
+
for (const command of input.commands) {
|
|
2372
|
+
if (command.cwd !== row.path ||
|
|
2373
|
+
command.argv[0] !== command.executable.path ||
|
|
2374
|
+
digestValue(sourceFile(command.executable.path)) !== digestValue(command.executable))
|
|
2375
|
+
throw new DeliveryError('Source-phase command or executable identity changed.');
|
|
2376
|
+
}
|
|
2377
|
+
await sourceState(allowFinal);
|
|
2378
|
+
};
|
|
2379
|
+
const observe = async (finishing = false) => {
|
|
2380
|
+
if (baseline === undefined)
|
|
2381
|
+
throw new DeliveryError('Source-phase output baseline is unavailable.');
|
|
2382
|
+
assertDiskHeadroom(row.path, accountingBounds, baseline);
|
|
2383
|
+
newBytes = bootstrapCharge + captured + (await positiveNewOutputBytes(baseline, finishing ? undefined : combined));
|
|
2384
|
+
if (newBytes > accountingBounds.maxNewOutputBytes)
|
|
2385
|
+
throw new DeliveryError('Source-phase cumulative output exceeded its original allowance.');
|
|
2386
|
+
if (process.memoryUsage().rss > accountingBounds.maxAggregateRssBytes)
|
|
2387
|
+
throw new DeliveryError('Source-phase controller RSS exceeded its bound.');
|
|
2388
|
+
};
|
|
2389
|
+
try {
|
|
2390
|
+
await withVerificationWriter(row.path, async (writer) => {
|
|
2391
|
+
try {
|
|
2392
|
+
ensurePrivateDirectoryDurably(directory);
|
|
2393
|
+
if (created && record !== undefined) {
|
|
2394
|
+
record.binding = binding;
|
|
2395
|
+
record.allocation = input.bounds;
|
|
2396
|
+
}
|
|
2397
|
+
const names = readdirSync(directory);
|
|
2398
|
+
if (names.length > 100_000)
|
|
2399
|
+
throw new DeliveryError('Source-phase record observation exceeded its entry bound.');
|
|
2400
|
+
const history = names
|
|
2401
|
+
.map((name) => readRecord(join(directory, name)))
|
|
2402
|
+
.filter((value) => value.phaseId !== phaseId);
|
|
2403
|
+
if (history.some((value) => ['preparing', 'intent', 'unresolved'].includes(value.status)))
|
|
2404
|
+
throw new DeliveryError('Unresolved source phase requires owning reconciliation before another attempt.');
|
|
2405
|
+
const previous = input.reconciliation === undefined
|
|
2406
|
+
? undefined
|
|
2407
|
+
: history.find((value) => value.phaseId === input.reconciliation?.phaseId && value.recordId === input.reconciliation.recordId);
|
|
2408
|
+
const failed = history.filter((value) => ['failed-quiescent', 'rejected-before-work'].includes(value.status) &&
|
|
2409
|
+
!history.some((later) => later.retainedPredecessors.some((retained) => retained.phaseId === value.phaseId && retained.recordId === value.recordId)));
|
|
2410
|
+
predecessorChain = [];
|
|
2411
|
+
const visiting = new Set();
|
|
2412
|
+
const retained = new Set();
|
|
2413
|
+
const retain = (value) => {
|
|
2414
|
+
if (visiting.has(value))
|
|
2415
|
+
throw new DeliveryError('Source-phase predecessor cycle is invalid.');
|
|
2416
|
+
if (retained.has(value))
|
|
2417
|
+
return;
|
|
2418
|
+
visiting.add(value);
|
|
2419
|
+
predecessorChain.push(value);
|
|
2420
|
+
for (const link of value.retainedPredecessors) {
|
|
2421
|
+
const predecessor = history.find((candidate) => candidate.phaseId === link.phaseId && candidate.recordId === link.recordId);
|
|
2422
|
+
if (predecessor === undefined)
|
|
2423
|
+
throw new DeliveryError('Source-phase retained predecessor is unavailable.');
|
|
2424
|
+
retain(predecessor);
|
|
2425
|
+
}
|
|
2426
|
+
visiting.delete(value);
|
|
2427
|
+
retained.add(value);
|
|
2428
|
+
};
|
|
2429
|
+
for (const value of failed)
|
|
2430
|
+
retain(value);
|
|
2431
|
+
if (record !== undefined && created)
|
|
2432
|
+
record.retainedPredecessors = failed.map(({ phaseId: retainedPhaseId, recordId }) => ({
|
|
2433
|
+
phaseId: retainedPhaseId,
|
|
2434
|
+
recordId,
|
|
2435
|
+
}));
|
|
2436
|
+
previousBootstrap = predecessorChain.reduce((sum, value) => sum + value.bootstrapBytes, 0);
|
|
2437
|
+
captured = predecessorChain.reduce((sum, value) => sum + value.capturedBytes, 0);
|
|
2438
|
+
const allocations = predecessorChain.map((value) => {
|
|
2439
|
+
if (value.allocation === undefined)
|
|
2440
|
+
throw new DeliveryError('Source-phase retained predecessor lacks its original allocation.');
|
|
2441
|
+
return value.allocation;
|
|
2442
|
+
});
|
|
2443
|
+
const allocation = { ...(allocations[0] ?? input.bounds) };
|
|
2444
|
+
for (const value of allocations) {
|
|
2445
|
+
allocation.maxAggregateRssBytes = Math.min(allocation.maxAggregateRssBytes, value.maxAggregateRssBytes);
|
|
2446
|
+
allocation.maxNewOutputBytes = Math.min(allocation.maxNewOutputBytes, value.maxNewOutputBytes);
|
|
2447
|
+
allocation.minFreeDiskBytes = Math.max(allocation.minFreeDiskBytes, value.minFreeDiskBytes);
|
|
2448
|
+
}
|
|
2449
|
+
accountingBounds = {
|
|
2450
|
+
...allocation,
|
|
2451
|
+
maxAggregateRssBytes: Math.min(allocation.maxAggregateRssBytes, input.bounds.maxAggregateRssBytes),
|
|
2452
|
+
maxNewOutputBytes: Math.min(allocation.maxNewOutputBytes, input.bounds.maxNewOutputBytes),
|
|
2453
|
+
minFreeDiskBytes: Math.max(allocation.minFreeDiskBytes, input.bounds.minFreeDiskBytes),
|
|
2454
|
+
};
|
|
2455
|
+
const priorBaselines = predecessorChain.flatMap((value) => value.baseline === undefined ? [] : [value.baseline]);
|
|
2456
|
+
const prior = priorBaselines[0];
|
|
2457
|
+
if (created && record !== undefined) {
|
|
2458
|
+
record.allocation = allocation;
|
|
2459
|
+
if (prior !== undefined) {
|
|
2460
|
+
record.baseline = prior;
|
|
2461
|
+
baseline = {
|
|
2462
|
+
roots: prior.roots,
|
|
2463
|
+
files: new Map(prior.files),
|
|
2464
|
+
links: new Map(prior.links),
|
|
2465
|
+
baselineId: prior.baselineId,
|
|
2466
|
+
};
|
|
2467
|
+
for (const retained of [...predecessorChain, record])
|
|
2468
|
+
for (const [file, size] of retained.bootstrapFiles)
|
|
2469
|
+
baseline.files.set(file, Math.max(baseline.files.get(file) ?? 0, size));
|
|
2470
|
+
}
|
|
2471
|
+
bootstrapCharge = previousBootstrap + record.bootstrapBytes;
|
|
2472
|
+
}
|
|
2473
|
+
if (allocations.some((value) => digestValue(value.outputRoots) !== digestValue(allocation.outputRoots))) {
|
|
2474
|
+
accountingFailure = 'Source-phase retained original output roots disagree.';
|
|
2475
|
+
throw new DeliveryError(accountingFailure);
|
|
2476
|
+
}
|
|
2477
|
+
if (input.bounds.maxNewOutputBytes > allocation.maxNewOutputBytes ||
|
|
2478
|
+
input.bounds.maxAggregateRssBytes > allocation.maxAggregateRssBytes ||
|
|
2479
|
+
input.bounds.minFreeDiskBytes < allocation.minFreeDiskBytes ||
|
|
2480
|
+
digestValue(input.bounds.outputRoots) !== digestValue(allocation.outputRoots))
|
|
2481
|
+
throw new DeliveryError('Source-phase reconciliation cannot increase its original resource allowance.');
|
|
2482
|
+
if (failed.length > 0 &&
|
|
2483
|
+
(previous === undefined ||
|
|
2484
|
+
!predecessorChain.includes(previous) ||
|
|
2485
|
+
input.reconciliation?.authorizationDigest !== input.authorization.digest))
|
|
2486
|
+
throw new DeliveryError('A failed source phase requires exact authorized retained-output reconciliation.');
|
|
2487
|
+
if (input.reconciliation !== undefined && (previous === undefined || !predecessorChain.includes(previous)))
|
|
2488
|
+
throw new DeliveryError('Source-phase reconciliation does not name its exact terminal predecessor.');
|
|
2489
|
+
if (prior !== undefined &&
|
|
2490
|
+
(priorBaselines.some((value) => value.baselineId !== prior.baselineId) ||
|
|
2491
|
+
input.reconciliation?.baselineId !== prior.baselineId ||
|
|
2492
|
+
digestValue(prior.roots) !== digestValue(roots)))
|
|
2493
|
+
throw new DeliveryError('Source-phase reconciliation cannot replace its original output baseline.');
|
|
2494
|
+
for (const retained of predecessorChain.filter((value) => value.status === 'failed-quiescent')) {
|
|
2495
|
+
const old = retained.binding;
|
|
2496
|
+
if (old === undefined ||
|
|
2497
|
+
digestValue(old.controller) !== digestValue(input.controller) ||
|
|
2498
|
+
digestValue(old.source) !== digestValue(input.source) ||
|
|
2499
|
+
old.repoRoot !== root ||
|
|
2500
|
+
old.issueNumber !== input.issueNumber ||
|
|
2501
|
+
old.rowDigest !== input.rowDigest ||
|
|
2502
|
+
old.identity !== input.identity ||
|
|
2503
|
+
old.inputs.some((artifact) => !input.inputs.some((current) => digestValue(current) === digestValue(artifact))))
|
|
2504
|
+
throw new DeliveryError('Source-phase reconciliation requires compatible source/runtime and retained frozen inputs.');
|
|
2505
|
+
for (const artifact of [old.caller, old.authorization, ...old.inputs])
|
|
2506
|
+
assertArtifact(artifact);
|
|
2507
|
+
}
|
|
2508
|
+
if (predecessorChain.some((retained) => retained.retainedArtifacts?.some((artifact) => digestValue(sourceFile(artifact.path)) !== digestValue(artifact))))
|
|
2509
|
+
throw new DeliveryError('Source-phase retained output artifact identity changed before reconciliation.');
|
|
2510
|
+
await validate(!created);
|
|
2511
|
+
const executionProducer = producerDigest();
|
|
2512
|
+
if (predecessorChain.some((value) => value.status === 'failed-quiescent' &&
|
|
2513
|
+
(value.producerDigest !== executionProducer || digestValue(value.actor) !== digestValue(actor))))
|
|
2514
|
+
throw new DeliveryError('Source-phase reconciliation requires its original authenticated actor and producer.');
|
|
2515
|
+
if (!created) {
|
|
2516
|
+
record = readRecord(path);
|
|
2517
|
+
if (record.status !== 'complete' ||
|
|
2518
|
+
record.bindingDigest !== phaseId ||
|
|
2519
|
+
record.producerDigest !== executionProducer ||
|
|
2520
|
+
record.baseline === undefined ||
|
|
2521
|
+
record.result === undefined)
|
|
2522
|
+
throw new DeliveryError('Existing source phase is incomplete or belongs to another producer.');
|
|
2523
|
+
if (digestValue(actor) !== digestValue(record.actor) ||
|
|
2524
|
+
digestValue(await sourceState(true)) !== digestValue(record.result.source) ||
|
|
2525
|
+
record.result.artifacts.some((artifact) => digestValue(sourceFile(artifact.path)) !== digestValue(artifact)))
|
|
2526
|
+
throw new DeliveryError('Completed source-phase source or artifacts changed.');
|
|
2527
|
+
reused = true;
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
validated = true;
|
|
2531
|
+
if (record === undefined)
|
|
2532
|
+
throw new DeliveryError('Source phase lacks preparation intent.');
|
|
2533
|
+
record.binding = binding;
|
|
2534
|
+
record.producerDigest = executionProducer;
|
|
2535
|
+
record.actor = actor;
|
|
2536
|
+
await withOutputObservationGate(row.path, true, async (gate) => {
|
|
2537
|
+
const gatePath = gate === undefined ? undefined : join(gate.directory, 'gate.json');
|
|
2538
|
+
if (gatePath !== undefined) {
|
|
2539
|
+
const gateBytes = lstatSync(gatePath).size;
|
|
2540
|
+
record.bootstrapBytes += gateBytes;
|
|
2541
|
+
record.bootstrapFiles.push([gatePath, gateBytes]);
|
|
2542
|
+
}
|
|
2543
|
+
bootstrapCharge = previousBootstrap + record.bootstrapBytes;
|
|
2544
|
+
const links = new Map();
|
|
2545
|
+
const files = prior === undefined
|
|
2546
|
+
? await withOutputGateLock(gate, 'scan', () => scanOutputRoots(roots, links), combined)
|
|
2547
|
+
: new Map(prior.files);
|
|
2548
|
+
if (prior !== undefined)
|
|
2549
|
+
for (const [name, alias] of prior.links)
|
|
2550
|
+
links.set(name, alias);
|
|
2551
|
+
const content = { roots, files: [...files], links: [...links] };
|
|
2552
|
+
const persistedBaseline = prior ?? { ...content, baselineId: digestValue(content) };
|
|
2553
|
+
baseline = {
|
|
2554
|
+
roots,
|
|
2555
|
+
files,
|
|
2556
|
+
links,
|
|
2557
|
+
baselineId: persistedBaseline.baselineId,
|
|
2558
|
+
...(gate === undefined ? {} : { gate }),
|
|
2559
|
+
};
|
|
2560
|
+
record.baseline = persistedBaseline;
|
|
2561
|
+
for (const retained of [...predecessorChain, record])
|
|
2562
|
+
for (const [file, size] of retained.bootstrapFiles)
|
|
2563
|
+
baseline.files.set(file, Math.max(baseline.files.get(file) ?? 0, size));
|
|
2564
|
+
record.status = 'intent';
|
|
2565
|
+
persist();
|
|
2566
|
+
await observe();
|
|
2567
|
+
let callbackFailure;
|
|
2568
|
+
let callbackRejected = false;
|
|
2569
|
+
const run = (index) => {
|
|
2570
|
+
if (expired)
|
|
2571
|
+
return Promise.reject(new DeliveryError('Source-phase command capability has expired.'));
|
|
2572
|
+
if (!Number.isSafeInteger(index) ||
|
|
2573
|
+
index !== record.nextCommandIndex ||
|
|
2574
|
+
attempted.has(index) ||
|
|
2575
|
+
active !== undefined ||
|
|
2576
|
+
index >= input.commands.length) {
|
|
2577
|
+
protocolFailure = 'Source-phase commands must be ordered, single-use and awaited.';
|
|
2578
|
+
cancelled.abort(new DeliveryError(protocolFailure));
|
|
2579
|
+
return Promise.reject(new DeliveryError(protocolFailure));
|
|
2580
|
+
}
|
|
2581
|
+
attempted.add(index);
|
|
2582
|
+
const command = input.commands[index];
|
|
2583
|
+
active = (async () => {
|
|
2584
|
+
let commandCaptured = 0;
|
|
2585
|
+
try {
|
|
2586
|
+
await validate(true);
|
|
2587
|
+
await observe();
|
|
2588
|
+
writer.assertQuiescent();
|
|
2589
|
+
startedCommand = true;
|
|
2590
|
+
const output = await runStageCommand(command.cwd, command.argv, combined, (bytes, sample, reason) => reportVerificationProgress({
|
|
2591
|
+
state: 'running',
|
|
2592
|
+
stageId: 'source-phase',
|
|
2593
|
+
completedStages: index,
|
|
2594
|
+
remainingStages: input.commands.length - index,
|
|
2595
|
+
reusedStages: 0,
|
|
2596
|
+
elapsedMs: Date.now() - started,
|
|
2597
|
+
capturedOutputBytes: bytes,
|
|
2598
|
+
...(sample === undefined ? {} : { sampledNewOutputBytes: sample.newOutputBytes }),
|
|
2599
|
+
...(reason === undefined ? {} : { reason }),
|
|
2600
|
+
}), { ...input.bounds, maxNewOutputBytes: input.bounds.maxNewOutputBytes - bootstrapCharge - captured }, baseline, undefined, writer, {
|
|
2601
|
+
environment,
|
|
2602
|
+
onCaptured: (bytes) => {
|
|
2603
|
+
commandCaptured = bytes;
|
|
2604
|
+
},
|
|
2605
|
+
onCleanupFailure: (reason) => {
|
|
2606
|
+
cleanupFailure = reason;
|
|
2607
|
+
},
|
|
2608
|
+
});
|
|
2609
|
+
record.commands.push({
|
|
2610
|
+
index,
|
|
2611
|
+
outputDigest: digestBytes(output),
|
|
2612
|
+
outputBytes: output.length,
|
|
2613
|
+
status: 'complete',
|
|
2614
|
+
});
|
|
2615
|
+
record.nextCommandIndex += 1;
|
|
2616
|
+
return output;
|
|
2617
|
+
}
|
|
2618
|
+
catch (error) {
|
|
2619
|
+
commandFailure = message(error);
|
|
2620
|
+
record.commands.push({ index, outputDigest: null, outputBytes: commandCaptured, status: 'failed' });
|
|
2621
|
+
throw error;
|
|
2622
|
+
}
|
|
2623
|
+
finally {
|
|
2624
|
+
captured += commandCaptured;
|
|
2625
|
+
record.capturedBytes += commandCaptured;
|
|
2626
|
+
active = undefined;
|
|
2627
|
+
persist();
|
|
2628
|
+
}
|
|
2629
|
+
})();
|
|
2630
|
+
return active;
|
|
2631
|
+
};
|
|
2632
|
+
entered = true;
|
|
2633
|
+
try {
|
|
2634
|
+
if (actor === undefined)
|
|
2635
|
+
throw new DeliveryError('Source-phase authenticated author is unavailable.');
|
|
2636
|
+
const source = await sourceState();
|
|
2637
|
+
await operation(Object.freeze({
|
|
2638
|
+
phaseId,
|
|
2639
|
+
signal: combined,
|
|
2640
|
+
source: Object.freeze({
|
|
2641
|
+
...source,
|
|
2642
|
+
dirty: Object.freeze(source.dirty.map((entry) => Object.freeze(entry))),
|
|
2643
|
+
}),
|
|
2644
|
+
controller: Object.freeze({ ...input.controller }),
|
|
2645
|
+
actor: Object.freeze({ ...actor }),
|
|
2646
|
+
inputs: Object.freeze([input.caller, input.authorization, ...input.inputs].map((artifact) => Object.freeze({ ...artifact }))),
|
|
2647
|
+
run,
|
|
2648
|
+
}));
|
|
2649
|
+
}
|
|
2650
|
+
catch (error) {
|
|
2651
|
+
callbackRejected = true;
|
|
2652
|
+
callbackFailure = error;
|
|
2653
|
+
}
|
|
2654
|
+
expired = true;
|
|
2655
|
+
if (active !== undefined) {
|
|
2656
|
+
protocolFailure = 'Source-phase callback settled with an unawaited command.';
|
|
2657
|
+
cancelled.abort(new DeliveryError(protocolFailure));
|
|
2658
|
+
await active.catch(() => undefined);
|
|
2659
|
+
}
|
|
2660
|
+
if (callbackRejected)
|
|
2661
|
+
throw callbackFailure;
|
|
2662
|
+
if (protocolFailure !== undefined)
|
|
2663
|
+
throw new DeliveryError(protocolFailure);
|
|
2664
|
+
if (commandFailure !== undefined)
|
|
2665
|
+
throw new DeliveryError(commandFailure);
|
|
2666
|
+
if (combined.aborted)
|
|
2667
|
+
throw new DeliveryError('Source phase cancelled.');
|
|
2668
|
+
if (record.nextCommandIndex !== input.commands.length)
|
|
2669
|
+
throw new DeliveryError('Source-phase callback did not complete its frozen graph.');
|
|
2670
|
+
await validate(true);
|
|
2671
|
+
finalSource = await sourceState(true);
|
|
2672
|
+
if (input.source.effect.kind === 'commitOnce' && finalSource.head === input.source.head)
|
|
2673
|
+
throw new DeliveryError('Source phase did not publish its required commit.');
|
|
2674
|
+
artifacts = input.completionArtifacts.map(sourceFile);
|
|
2675
|
+
writer.assertQuiescent();
|
|
2676
|
+
await observe();
|
|
2677
|
+
}).finally(() => {
|
|
2678
|
+
if (baseline !== undefined)
|
|
2679
|
+
delete baseline.gate;
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
catch (error) {
|
|
2683
|
+
operationFailure = message(error);
|
|
2684
|
+
}
|
|
2685
|
+
finally {
|
|
2686
|
+
expired = true;
|
|
2687
|
+
if (active !== undefined) {
|
|
2688
|
+
cancelled.abort(new DeliveryError('Source phase is releasing its command capability.'));
|
|
2689
|
+
await active.catch((error) => {
|
|
2690
|
+
operationFailure ??= message(error);
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
if (created && baseline !== undefined) {
|
|
2694
|
+
try {
|
|
2695
|
+
await observe(true);
|
|
2696
|
+
}
|
|
2697
|
+
catch (error) {
|
|
2698
|
+
accountingFailure = message(error);
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}, {
|
|
2703
|
+
repoRoot: root,
|
|
2704
|
+
worktreePath: row.path,
|
|
2705
|
+
writerId,
|
|
2706
|
+
beforeClaim: ({ previousBytes }) => {
|
|
2707
|
+
if (previousBytes !== undefined)
|
|
2708
|
+
throw new DeliveryError('Source phase requires an absent writer; it never recovers a prior command.');
|
|
2709
|
+
},
|
|
2710
|
+
recordClaim: (bytes) => {
|
|
2711
|
+
ensurePrivateDirectoryDurably(directory);
|
|
2712
|
+
if (existsSync(path))
|
|
2713
|
+
return;
|
|
2714
|
+
created = true;
|
|
2715
|
+
writerBootstrap = bytes.length;
|
|
2716
|
+
const content = {
|
|
2717
|
+
schemaVersion: 'ai-delivery.source-phase@1',
|
|
2718
|
+
phaseId,
|
|
2719
|
+
bindingDigest: phaseId,
|
|
2720
|
+
authorizationDigest: input.authorization.digest,
|
|
2721
|
+
worktreeDigest: subject,
|
|
2722
|
+
status: 'preparing',
|
|
2723
|
+
writerId,
|
|
2724
|
+
bootstrapBytes: bytes.length,
|
|
2725
|
+
allocation: input.bounds,
|
|
2726
|
+
capturedBytes: 0,
|
|
2727
|
+
nextCommandIndex: 0,
|
|
2728
|
+
owner: JSON.parse(bytes.toString('utf8')).owner,
|
|
2729
|
+
bootstrapFiles: [],
|
|
2730
|
+
commands: [],
|
|
2731
|
+
retainedPredecessors: [],
|
|
2732
|
+
claimDigest: digestBytes(bytes),
|
|
2733
|
+
...(input.reconciliation === undefined ? {} : { reconciliation: input.reconciliation }),
|
|
2734
|
+
};
|
|
2735
|
+
record = { ...content, recordId: digestValue(content) };
|
|
2736
|
+
persist();
|
|
2737
|
+
record.preparationArtifact = sourceFile(path);
|
|
2738
|
+
record.bootstrapFiles.push([path, lstatSync(path).size]);
|
|
2739
|
+
for (let index = 0; index < 4; index++) {
|
|
2740
|
+
const size = Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`);
|
|
2741
|
+
record.bootstrapFiles[0] = [path, size];
|
|
2742
|
+
record.bootstrapBytes = writerBootstrap + size;
|
|
2743
|
+
}
|
|
2744
|
+
persist();
|
|
2745
|
+
},
|
|
2746
|
+
afterClaim: () => {
|
|
2747
|
+
if (record !== undefined && created)
|
|
2748
|
+
record.writerArtifact = sourceFile(writerPath);
|
|
2749
|
+
},
|
|
2750
|
+
recordRelease: (bytes) => {
|
|
2751
|
+
if (record !== undefined && !reused)
|
|
2752
|
+
record.releaseDigest = digestBytes(bytes);
|
|
2753
|
+
},
|
|
2754
|
+
afterRelease: () => {
|
|
2755
|
+
writerReleased = true;
|
|
2756
|
+
},
|
|
2757
|
+
onLockReleaseError: (error) => {
|
|
2758
|
+
lockFailure = message(error);
|
|
2759
|
+
},
|
|
2760
|
+
onLockCompromised: (error) => {
|
|
2761
|
+
lockFailure = message(error);
|
|
2762
|
+
cancelled.abort(error);
|
|
2763
|
+
},
|
|
2764
|
+
}, common, true);
|
|
2765
|
+
}
|
|
2766
|
+
catch (error) {
|
|
2767
|
+
releaseFailure = message(error);
|
|
2768
|
+
}
|
|
2769
|
+
if (reused &&
|
|
2770
|
+
record !== undefined &&
|
|
2771
|
+
writerReleased &&
|
|
2772
|
+
lockFailure === undefined &&
|
|
2773
|
+
releaseFailure === undefined &&
|
|
2774
|
+
operationFailure === undefined)
|
|
2775
|
+
return sourceReceipt(record);
|
|
2776
|
+
if (record === undefined || !created)
|
|
2777
|
+
throw new DeliveryError(operationFailure ?? releaseFailure ?? 'Source-phase ownership could not be established.');
|
|
2778
|
+
const failures = {
|
|
2779
|
+
...(operationFailure === undefined ? {} : { operation: operationFailure }),
|
|
2780
|
+
...(accountingFailure === undefined ? {} : { accounting: accountingFailure }),
|
|
2781
|
+
...(cleanupFailure === undefined ? {} : { cleanup: cleanupFailure }),
|
|
2782
|
+
...(writerReleased && releaseFailure === undefined
|
|
2783
|
+
? {}
|
|
2784
|
+
: { writerRelease: releaseFailure ?? 'Writer absence was not confirmed.' }),
|
|
2785
|
+
...(lockFailure === undefined ? {} : { lockRelease: lockFailure }),
|
|
2786
|
+
};
|
|
2787
|
+
const quiescent = writerReleased && releaseFailure === undefined && lockFailure === undefined && cleanupFailure === undefined;
|
|
2788
|
+
let unchanged = false;
|
|
2789
|
+
if (validated) {
|
|
2790
|
+
try {
|
|
2791
|
+
await validate(finalSource !== undefined);
|
|
2792
|
+
unchanged = (await sourceState(finalSource !== undefined)).head === input.source.head;
|
|
2793
|
+
if (finalSource !== undefined &&
|
|
2794
|
+
(existsSync(writerPath) ||
|
|
2795
|
+
artifacts.some((artifact) => digestValue(sourceFile(artifact.path)) !== digestValue(artifact))))
|
|
2796
|
+
throw new DeliveryError('Source-phase writer absence or completion artifact changed before sealing.');
|
|
2797
|
+
}
|
|
2798
|
+
catch (error) {
|
|
2799
|
+
unchanged = false;
|
|
2800
|
+
if (finalSource !== undefined)
|
|
2801
|
+
failures.operation ??= message(error);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
const successful = Object.keys(failures).length === 0 && finalSource !== undefined;
|
|
2805
|
+
record.status = successful
|
|
2806
|
+
? 'complete'
|
|
2807
|
+
: !entered && !startedCommand && quiescent && accountingFailure === undefined
|
|
2808
|
+
? 'rejected-before-work'
|
|
2809
|
+
: quiescent && unchanged && accountingFailure === undefined && baseline !== undefined
|
|
2810
|
+
? 'failed-quiescent'
|
|
2811
|
+
: 'unresolved';
|
|
2812
|
+
if (successful && finalSource !== undefined)
|
|
2813
|
+
record.result = {
|
|
2814
|
+
source: finalSource,
|
|
2815
|
+
artifacts,
|
|
2816
|
+
newOutputBytes: newBytes,
|
|
2817
|
+
writerReleased: true,
|
|
2818
|
+
lockReleased: true,
|
|
2819
|
+
};
|
|
2820
|
+
else
|
|
2821
|
+
record.failure = failures;
|
|
2822
|
+
if (record.status === 'failed-quiescent' && baseline !== undefined) {
|
|
2823
|
+
try {
|
|
2824
|
+
const files = scanOutputRoots(baseline.roots, new Map());
|
|
2825
|
+
record.retainedArtifacts = [...files]
|
|
2826
|
+
.filter(([file, bytes]) => file !== path && bytes > (baseline.files.get(file) ?? 0))
|
|
2827
|
+
.map(([file]) => sourceFile(file));
|
|
2828
|
+
}
|
|
2829
|
+
catch (error) {
|
|
2830
|
+
record.status = 'unresolved';
|
|
2831
|
+
record.failure = { ...failures, accounting: message(error) };
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
if (baseline !== undefined) {
|
|
2835
|
+
try {
|
|
2836
|
+
await observe(true);
|
|
2837
|
+
const originalSize = baseline.files.get(path) ?? 0;
|
|
2838
|
+
const currentGrowth = Math.max(0, lstatSync(path).size - originalSize);
|
|
2839
|
+
const observed = newBytes;
|
|
2840
|
+
let projected = observed;
|
|
2841
|
+
for (let index = 0; index < 4; index++) {
|
|
2842
|
+
if (record.result !== undefined)
|
|
2843
|
+
record.result.newOutputBytes = projected;
|
|
2844
|
+
const { recordId: _recordId, ...content } = record;
|
|
2845
|
+
record.recordId = digestValue(content);
|
|
2846
|
+
projected =
|
|
2847
|
+
observed -
|
|
2848
|
+
currentGrowth +
|
|
2849
|
+
Math.max(0, Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`) - originalSize);
|
|
2850
|
+
}
|
|
2851
|
+
if (projected > accountingBounds.maxNewOutputBytes)
|
|
2852
|
+
throw new DeliveryError('Source-phase final receipt exceeds its original output allowance.');
|
|
2853
|
+
if (record.result !== undefined)
|
|
2854
|
+
record.result.newOutputBytes = projected;
|
|
2855
|
+
}
|
|
2856
|
+
catch (error) {
|
|
2857
|
+
record.status = 'unresolved';
|
|
2858
|
+
delete record.result;
|
|
2859
|
+
record.failure = { ...failures, accounting: message(error) };
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
else {
|
|
2863
|
+
for (let index = 0; index < 4; index++) {
|
|
2864
|
+
const size = Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`);
|
|
2865
|
+
record.bootstrapFiles[0] = [path, size];
|
|
2866
|
+
record.bootstrapBytes =
|
|
2867
|
+
writerBootstrap + size + record.bootstrapFiles.slice(1).reduce((sum, [, bytes]) => sum + bytes, 0);
|
|
2868
|
+
}
|
|
2869
|
+
if (previousBootstrap + record.bootstrapBytes + captured > accountingBounds.maxNewOutputBytes) {
|
|
2870
|
+
record.status = 'unresolved';
|
|
2871
|
+
record.failure = {
|
|
2872
|
+
...failures,
|
|
2873
|
+
accounting: 'Retained rejected bootstrap exceeded the original output allowance.',
|
|
2874
|
+
};
|
|
2875
|
+
for (let index = 0; index < 4; index++) {
|
|
2876
|
+
const size = Buffer.byteLength(`${JSON.stringify(record, null, 2)}\n`);
|
|
2877
|
+
record.bootstrapFiles[0] = [path, size];
|
|
2878
|
+
record.bootstrapBytes =
|
|
2879
|
+
writerBootstrap + size + record.bootstrapFiles.slice(1).reduce((sum, [, bytes]) => sum + bytes, 0);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
persist();
|
|
2884
|
+
if (record.status !== 'complete')
|
|
2885
|
+
throw new DeliveryError(`Source phase ${record.status}: ${operationFailure ?? accountingFailure ?? releaseFailure ?? lockFailure ?? failures.operation ?? 'postconditions failed'}.`, path);
|
|
2886
|
+
return sourceReceipt(record);
|
|
2887
|
+
}
|
|
1893
2888
|
/** @internal Runtime setup shares the existing writer, ownership handshake and bounded command runner. */
|
|
1894
2889
|
export async function withRuntimeSetupWriter(root, operation) {
|
|
1895
2890
|
return withVerificationWriter(root, async (writer) => operation({
|