@sabaiway/agent-workflow-kit 12.4.0 → 13.1.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/CHANGELOG.md +161 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/dispatch.md +9 -9
- package/references/modes/review-state.md +1 -1
- package/tools/claim-relation.mjs +33 -0
- package/tools/dispatch-baseline.mjs +172 -0
- package/tools/dispatch-record.mjs +28 -1
- package/tools/dispatch-store-read.mjs +6 -1
- package/tools/dispatch-store.mjs +4 -3
- package/tools/dispatch.mjs +110 -71
- package/tools/epic-shape-cli.mjs +4 -13
- package/tools/epic-shape-ledger.mjs +11 -31
- package/tools/epic-store.mjs +33 -0
- package/tools/exec-producer.mjs +57 -37
- package/tools/held-session.mjs +22 -9
- package/tools/plan-shape-facts.mjs +22 -4
- package/tools/plan-shape-ownership.mjs +83 -0
- package/tools/plan-shape.mjs +36 -8
- package/tools/review-state.mjs +9 -5
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// dispatch-baseline.mjs — snapshotScope, computeBasePayload, computeBaseFingerprint,
|
|
2
|
+
// isCleanAgainstBase, runBaseDiff and resolveTreeObject; spec:dispatch-baseline.
|
|
3
|
+
// Writes only its own temporary indexes and the blob objects stored by forced adds.
|
|
4
|
+
// Head measurements delegate unchanged to core-evidence without a temporary index.
|
|
5
|
+
// runBaseDiff returns null for head; callers must use their existing head diff path.
|
|
6
|
+
|
|
7
|
+
import { mkdtempSync, rmSync, lstatSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join, dirname, basename, relative } from 'node:path';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { computeFingerprintPayload, isTreeClean, isNeverCommittableStat } from './core-evidence.mjs';
|
|
13
|
+
import { KIT_OWN_PATHS, KNOWN_FOOTPRINT, patternToProbe, isDirPattern, expandGlob, isGlobPattern } from './known-footprint.mjs';
|
|
14
|
+
import { GIT_MAX_BUFFER, resolveGitLocation, withGitPath } from './git-env.mjs';
|
|
15
|
+
|
|
16
|
+
const TEMP_PREFIX = 'aw-dispatch-baseline-';
|
|
17
|
+
const INDEX_NAME = 'index';
|
|
18
|
+
const HASH_ALGORITHM = 'sha256';
|
|
19
|
+
const PAYLOAD_DIFF_ARGS = ['--no-ext-diff', '--no-textconv', '--ignore-submodules=none'];
|
|
20
|
+
const SCOPE_EXCLUSIONS = ['/docs/plans/', '/.claude/settings.json', '/.claude/settings.local.json', '/.mcp.json'];
|
|
21
|
+
const MISSING_PATH_CODES = ['ENOENT', 'ENOTDIR'];
|
|
22
|
+
const ABSENT_HEAD_STATUS = 1;
|
|
23
|
+
const splitPaths = (bytes) => {
|
|
24
|
+
const text = bytes.toString('utf8');
|
|
25
|
+
if (!Buffer.from(text).equals(bytes)) throw new Error('git path names are not lossless UTF-8');
|
|
26
|
+
return text.split('\0').filter(Boolean).map((path) => path.replace(/\/$/, ''));
|
|
27
|
+
};
|
|
28
|
+
const defaultRunGit = (args, cwd, input, env) => spawnSync('git', args, {
|
|
29
|
+
cwd, input, env: withGitPath(env), maxBuffer: GIT_MAX_BUFFER, windowsHide: true,
|
|
30
|
+
});
|
|
31
|
+
const makeRunner = ({ runGit = defaultRunGit, env = process.env } = {}) => (args, cwd, input, override = env) => {
|
|
32
|
+
try { return runGit(args, cwd, input, override); }
|
|
33
|
+
catch (error) { return { error, status: null, stdout: Buffer.alloc(0) }; }
|
|
34
|
+
};
|
|
35
|
+
const readGit = (run, args, top, env) => {
|
|
36
|
+
const result = run(args, top, undefined, env);
|
|
37
|
+
if (result == null || result.error || result.signal || result.status !== 0 || result.stdout == null) {
|
|
38
|
+
throw new Error(`git ${args.join(' ')} failed: ${result?.error?.message ?? String(result?.stderr ?? '').trim()}`);
|
|
39
|
+
}
|
|
40
|
+
return Buffer.from(result.stdout);
|
|
41
|
+
};
|
|
42
|
+
const locateTop = (cwd, run, env) => {
|
|
43
|
+
const location = resolveGitLocation(cwd, { env, spawn: (command, args, options) => run(args, options.cwd, undefined, options.env) });
|
|
44
|
+
if (location.state !== 'work-tree') throw new Error(location.cause ?? 'not inside a git work tree');
|
|
45
|
+
return location.top;
|
|
46
|
+
};
|
|
47
|
+
const withTemporaryIndex = (env, action) => {
|
|
48
|
+
const directory = mkdtempSync(join(tmpdir(), TEMP_PREFIX));
|
|
49
|
+
try {
|
|
50
|
+
return action({ ...env, GIT_INDEX_FILE: join(directory, INDEX_NAME), GIT_LITERAL_PATHSPECS: '1' });
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(directory, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const readStat = (path, lstat) => {
|
|
56
|
+
try {
|
|
57
|
+
const stat = lstat(path);
|
|
58
|
+
if (stat == null) throw new Error(`cannot stat ${path}`);
|
|
59
|
+
return stat;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (MISSING_PATH_CODES.includes(error.code)) return null;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const readTreePaths = (run, top, treeOid, env) => splitPaths(readGit(run, ['ls-tree', '-r', '--name-only', '-z', treeOid], top, env));
|
|
66
|
+
const readHeadPaths = (run, top, env) => {
|
|
67
|
+
const head = run(['rev-parse', '--verify', '--quiet', 'HEAD'], top, undefined, env);
|
|
68
|
+
if (head && !head.error && !head.signal && head.status === ABSENT_HEAD_STATUS) return [];
|
|
69
|
+
if (!head || head.error || head.signal || head.status !== 0) throw new Error('cannot resolve HEAD');
|
|
70
|
+
return readTreePaths(run, top, 'HEAD', env);
|
|
71
|
+
};
|
|
72
|
+
const excludePatterns = (top, paths) => [
|
|
73
|
+
...SCOPE_EXCLUSIONS,
|
|
74
|
+
...KNOWN_FOOTPRINT.flatMap(({ pattern }) => isGlobPattern(pattern)
|
|
75
|
+
? expandGlob(pattern, {
|
|
76
|
+
dir: top,
|
|
77
|
+
readdir: (parent) => paths.filter((path) => dirname(path) === relative(top, parent).split('\\').join('/')).map((path) => basename(path)),
|
|
78
|
+
stat: () => ({ isFile: () => true }),
|
|
79
|
+
})
|
|
80
|
+
: [pattern]),
|
|
81
|
+
];
|
|
82
|
+
const collectScope = (top, baseTreeOid, run, lstat, env) => withTemporaryIndex(env, (indexEnv) => {
|
|
83
|
+
readGit(run, ['read-tree', '--empty'], top, indexEnv);
|
|
84
|
+
const paths = new Set(splitPaths(readGit(run, ['ls-files', '--others', '--exclude-standard', '-z'], top, indexEnv)));
|
|
85
|
+
for (const pattern of KIT_OWN_PATHS) {
|
|
86
|
+
const probe = patternToProbe(pattern);
|
|
87
|
+
const stat = readStat(join(top, probe), lstat);
|
|
88
|
+
if (stat === null) continue;
|
|
89
|
+
if (isDirPattern(pattern) && stat.isDirectory()) {
|
|
90
|
+
for (const path of splitPaths(readGit(run, ['ls-files', '--others', '-z', '--', probe], top, indexEnv))) paths.add(path);
|
|
91
|
+
} else paths.add(probe.replace(/\/$/, ''));
|
|
92
|
+
}
|
|
93
|
+
for (const path of readHeadPaths(run, top, indexEnv)) paths.add(path);
|
|
94
|
+
if (baseTreeOid !== null) for (const path of readTreePaths(run, top, baseTreeOid, indexEnv)) paths.add(path);
|
|
95
|
+
const exclusions = excludePatterns(top, [...paths]).map((pattern) => ({ probe: patternToProbe(pattern), directory: isDirPattern(pattern) }));
|
|
96
|
+
return [...paths].filter((path) => !path.split('/').includes('node_modules')
|
|
97
|
+
&& !exclusions.some(({ probe, directory }) => directory ? path === probe.slice(0, -1) || path.startsWith(probe) : path === probe)).sort();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const snapshotScope = (cwd, { baseTreeOid = null } = {}, io = {}) => {
|
|
101
|
+
try {
|
|
102
|
+
const env = io.env ?? process.env;
|
|
103
|
+
const run = makeRunner(io);
|
|
104
|
+
const top = locateTop(cwd, run, env);
|
|
105
|
+
return { ok: true, paths: collectScope(top, baseTreeOid, run, io.lstat ?? lstatSync, env) };
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return { ok: false, reason: `snapshot scope: ${error.message}` };
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const resolveTreeObject = (cwd, oid, io = {}) => {
|
|
112
|
+
try {
|
|
113
|
+
const env = io.env ?? process.env;
|
|
114
|
+
const run = makeRunner(io);
|
|
115
|
+
const top = locateTop(cwd, run, env);
|
|
116
|
+
const type = readGit(run, ['cat-file', '-t', oid], top, env).toString('utf8').trim();
|
|
117
|
+
return type === 'tree' ? { ok: true } : { ok: false, reason: `${oid} names a ${type}, not a tree` };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return { ok: false, reason: `${oid} is an unresolvable tree object: ${error.message}` };
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const runBaseDiff = (cwd, base, args, io = {}) => {
|
|
124
|
+
if (base.kind !== 'checkpoint') return null;
|
|
125
|
+
try {
|
|
126
|
+
const env = io.env ?? process.env;
|
|
127
|
+
const run = makeRunner(io);
|
|
128
|
+
const top = locateTop(cwd, run, env);
|
|
129
|
+
if (!resolveTreeObject(top, base.treeOid, io).ok) return null;
|
|
130
|
+
const scope = snapshotScope(top, { baseTreeOid: base.treeOid }, io);
|
|
131
|
+
if (!scope.ok) return null;
|
|
132
|
+
const basePaths = readTreePaths(run, top, base.treeOid, env);
|
|
133
|
+
const present = [];
|
|
134
|
+
const removed = [];
|
|
135
|
+
for (const path of scope.paths) {
|
|
136
|
+
const stat = readStat(join(top, path), io.lstat ?? lstatSync);
|
|
137
|
+
const baseHoldsPath = basePaths.includes(path);
|
|
138
|
+
const neverCommittable = stat !== null && isNeverCommittableStat(stat);
|
|
139
|
+
const materialized = stat !== null && !neverCommittable && (stat.isFile() || stat.isSymbolicLink());
|
|
140
|
+
if (neverCommittable && baseHoldsPath) throw new Error(`base path ${path} is no longer committable`);
|
|
141
|
+
if (materialized) present.push(path);
|
|
142
|
+
if (!materialized && !neverCommittable && (stat === null || baseHoldsPath)) removed.push(path);
|
|
143
|
+
}
|
|
144
|
+
const excludedBasePaths = basePaths.filter((path) => !scope.paths.includes(path));
|
|
145
|
+
const conflict = present.find((path) => excludedBasePaths.some((excluded) =>
|
|
146
|
+
path.startsWith(`${excluded}/`) || excluded.startsWith(`${path}/`)));
|
|
147
|
+
if (conflict !== undefined) throw new Error(`live path ${conflict} conflicts with an excluded base path`);
|
|
148
|
+
return withTemporaryIndex(env, (indexEnv) => {
|
|
149
|
+
readGit(run, ['read-tree', base.treeOid], top, indexEnv);
|
|
150
|
+
if (removed.length > 0) readGit(run, ['update-index', '--remove', '--', ...removed], top, indexEnv);
|
|
151
|
+
if (present.length > 0) readGit(run, ['add', '-f', '--', ...present], top, indexEnv);
|
|
152
|
+
return readGit(run, ['diff', '--cached', base.treeOid, ...args], top, indexEnv);
|
|
153
|
+
});
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const computeBasePayload = (cwd, base, io = {}) => base.kind === 'head'
|
|
160
|
+
? computeFingerprintPayload(cwd, io)
|
|
161
|
+
: runBaseDiff(cwd, base, PAYLOAD_DIFF_ARGS, io);
|
|
162
|
+
|
|
163
|
+
export const computeBaseFingerprint = (cwd, base, io) => {
|
|
164
|
+
const payload = computeBasePayload(cwd, base, io);
|
|
165
|
+
return payload === null ? null : createHash(HASH_ALGORITHM).update(payload).digest('hex');
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const isCleanAgainstBase = (cwd, base, io) => {
|
|
169
|
+
if (base.kind === 'head') return isTreeClean(cwd, io);
|
|
170
|
+
const payload = computeBasePayload(cwd, base, io);
|
|
171
|
+
return payload === null ? null : payload.length === 0;
|
|
172
|
+
};
|
|
@@ -131,13 +131,21 @@ const short = (v) => {
|
|
|
131
131
|
// ── the closed vocabulary ─────────────────────────────────────────────────────────────────────────
|
|
132
132
|
|
|
133
133
|
export const DELEGATION_SCHEMA_VERSION = 1;
|
|
134
|
+
export const BASELINE_KINDS = deepFreeze(['head', 'checkpoint']);
|
|
135
|
+
export const HEAD_BASELINE = deepFreeze({ kind: 'head', treeOid: null });
|
|
136
|
+
export const OPTIONAL_FIELDS = deepFreeze({ dispatch: ['baseline'] });
|
|
137
|
+
const TREE_OID_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
138
|
+
export const isTreeOid = (v) => typeof v === 'string' && TREE_OID_RE.test(v);
|
|
139
|
+
export const readsBaseline = (dispatch) => Object.hasOwn(dispatch, 'baseline')
|
|
140
|
+
? { kind: dispatch.baseline.kind, treeOid: dispatch.baseline.treeOid }
|
|
141
|
+
: { ...HEAD_BASELINE };
|
|
134
142
|
|
|
135
143
|
export const DELEGATION_KINDS = deepFreeze(['pre-registration', 'dispatch', 'return', 'fold', 'observation', 'degrade']);
|
|
136
144
|
|
|
137
145
|
// The closed key set per kind (schema + kind are implicit on every record).
|
|
138
146
|
export const DELEGATION_KEY_SETS = deepFreeze({
|
|
139
147
|
'pre-registration': ['waveId', 'stepClasses', 'pairingKey', 'minPerClass', 'meanLThreshold', 'firstPassNum', 'firstPassDen', 'timestamp'],
|
|
140
|
-
dispatch: ['waveId', 'nonce', 'stepClass', 'vehicle', 'backend', 'contractDigest', 'preTreeDigest', 'baselineClean', 'deadlineS', 'retryOf', 'retryIndex', 'retryCap', 'rationale', 'timestamp'],
|
|
148
|
+
dispatch: ['waveId', 'nonce', 'stepClass', 'vehicle', 'backend', 'contractDigest', 'preTreeDigest', 'baselineClean', 'deadlineS', 'retryOf', 'retryIndex', 'retryCap', 'rationale', 'timestamp', 'baseline'],
|
|
141
149
|
return: ['role', 'backend', 'nonce', 'contractDigest', 'preTreeDigest', 'postTreeDigest', 'diffDigest', 'diffLength', 'reportDigest', 'reportLength', 'bundleDigest', 'bundleLength', 'metric', 'outcome', 'exitStatus', 'sessionId', 'wrapperVersion', 'posture', 'timestamp'],
|
|
142
150
|
fold: ['nonce', 'returnDigest', 'treeDigestAtFold', 'verdict', 'timestamp'],
|
|
143
151
|
observation: ['waveId', 'stepClass', 'scope', 'metric', 'planId', 'phase', 'timestamp'],
|
|
@@ -217,6 +225,7 @@ const namesAnObject = (kind) => kind !== 'gate-output';
|
|
|
217
225
|
// ── nested closed forms ───────────────────────────────────────────────────────────────────────────
|
|
218
226
|
|
|
219
227
|
const VEHICLE_KEYS = ['requested', 'selected'];
|
|
228
|
+
const BASELINE_KEYS = ['kind', 'treeOid'];
|
|
220
229
|
const POSTURE_KEYS = ['model', 'effort', 'tier'];
|
|
221
230
|
const METRIC_KEYS = ['numeratorBytes', 'denominatorBytes', 'components', 'provenance', 'eligible', 'ineligibleReason'];
|
|
222
231
|
const COMPONENT_KEYS = ['kind', 'path', 'objectId', 'bytes'];
|
|
@@ -235,6 +244,18 @@ const checkClosedKeys = (at, value, keys) => {
|
|
|
235
244
|
return { ok: true };
|
|
236
245
|
};
|
|
237
246
|
|
|
247
|
+
const validateBaseline = (at, baseline) => {
|
|
248
|
+
const closed = checkClosedKeys(at, baseline, BASELINE_KEYS);
|
|
249
|
+
if (!closed.ok) return closed;
|
|
250
|
+
if (!BASELINE_KINDS.includes(baseline.kind)) return refuse(`${at}: unknown kind ${short(baseline.kind)} — expected ${BASELINE_KINDS.join(' | ')}`);
|
|
251
|
+
if (baseline.treeOid !== null && (typeof baseline.treeOid !== 'string' || !TREE_OID_RE.test(baseline.treeOid))) {
|
|
252
|
+
return refuse(`${at}: treeOid must be null or exactly 40 or 64 lowercase hex characters (got ${short(baseline.treeOid)})`);
|
|
253
|
+
}
|
|
254
|
+
if (baseline.kind === 'head' && baseline.treeOid !== null) return refuse(`${at}: head requires treeOid null`);
|
|
255
|
+
if (baseline.kind === 'checkpoint' && baseline.treeOid === null) return refuse(`${at}: checkpoint requires treeOid to be non-null`);
|
|
256
|
+
return { ok: true };
|
|
257
|
+
};
|
|
258
|
+
|
|
238
259
|
const validateVehicle = (at, vehicle) => {
|
|
239
260
|
const closed = checkClosedKeys(at, vehicle, VEHICLE_KEYS);
|
|
240
261
|
if (!closed.ok) return closed;
|
|
@@ -324,6 +345,7 @@ const FIELD_CHECKS = {
|
|
|
324
345
|
firstPassNum: { ok: isByteCount, want: 'a non-negative integer first-pass numerator' },
|
|
325
346
|
firstPassDen: { ok: (v) => Number.isSafeInteger(v) && v >= 1, want: 'a positive integer first-pass denominator' },
|
|
326
347
|
vehicle: { ok: isPlainObject, want: 'the closed vehicle pair {requested, selected}' },
|
|
348
|
+
baseline: { ok: isPlainObject, want: 'the closed baseline pair {kind, treeOid}' },
|
|
327
349
|
// The safe token grammar, not merely non-empty: the backend NAMES an artifact beside the store
|
|
328
350
|
// (the exec receipt and its report), so a name outside this grammar records a dispatch whose own
|
|
329
351
|
// receipt could never be written. One grammar on both sides, or the ledger accepts what the
|
|
@@ -375,6 +397,7 @@ const checkFields = (kind, record) => {
|
|
|
375
397
|
return refuse(`${kind}: unknown field "${stray}" — the key set is closed (the canonical digest is identity; a stray key would fork it)`);
|
|
376
398
|
}
|
|
377
399
|
for (const field of fields) {
|
|
400
|
+
if (!Object.hasOwn(record, field) && (OPTIONAL_FIELDS[kind] ?? []).includes(field)) continue;
|
|
378
401
|
if (!own.includes(field)) {
|
|
379
402
|
return refuse(`${kind}: missing field "${field}" — every field of the closed set is pinned as an OWN ENUMERABLE key (the digest domain)`);
|
|
380
403
|
}
|
|
@@ -492,6 +515,10 @@ export const validateDelegationRecord = (record) => {
|
|
|
492
515
|
}
|
|
493
516
|
if (record.kind === 'dispatch') {
|
|
494
517
|
const vehicle = validateVehicle('dispatch: vehicle', record.vehicle);
|
|
518
|
+
if (Object.hasOwn(record, 'baseline')) {
|
|
519
|
+
const baseline = validateBaseline('dispatch: baseline', record.baseline);
|
|
520
|
+
if (!baseline.ok) return baseline;
|
|
521
|
+
}
|
|
495
522
|
return vehicle.ok ? validateDispatchCrossFields(record) : vehicle;
|
|
496
523
|
}
|
|
497
524
|
if (record.kind === 'return') return validateReturnCrossFields(record);
|
|
@@ -3,7 +3,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
validateDelegationRecord, canonicalDelegationDigest, allowedSuccessorKinds,
|
|
6
|
-
isThreadTerminalRecord,
|
|
6
|
+
isThreadTerminalRecord, readsBaseline,
|
|
7
7
|
} from './dispatch-record.mjs';
|
|
8
8
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
9
9
|
import { GIT_MAX_BUFFER } from './git-env.mjs';
|
|
@@ -175,6 +175,11 @@ export const delegationSemanticPreflight = ({ records, snapshot, storePath }) =>
|
|
|
175
175
|
if (snapshot.stepClass !== prior.dispatch.stepClass) {
|
|
176
176
|
throw stop(`refusing a retry: it declares step class "${snapshot.stepClass}" but its retry origin "${snapshot.retryOf}" was dispatched as "${prior.dispatch.stepClass}" — the pairing key is the step class, so a chain that changes it mid-way would split one attempt's accounting across two classes; nothing was written`);
|
|
177
177
|
}
|
|
178
|
+
const base = readsBaseline(snapshot);
|
|
179
|
+
const priorBase = readsBaseline(prior.dispatch);
|
|
180
|
+
if (base.kind !== priorBase.kind || base.treeOid !== priorBase.treeOid) {
|
|
181
|
+
throw stop(`refusing a retry: its base {kind "${base.kind}", treeOid ${base.treeOid}} differs from its retry origin "${snapshot.retryOf}"'s {kind "${priorBase.kind}", treeOid ${priorBase.treeOid}} — a retry chain is measured against ONE base; nothing was written`);
|
|
182
|
+
}
|
|
178
183
|
const origin = findRetryChainOrigin(records, prior.dispatch);
|
|
179
184
|
if (snapshot.retryIndex > origin.retryCap) {
|
|
180
185
|
throw stop(`refusing a retry: retryIndex ${snapshot.retryIndex} exceeds the retryCap ${origin.retryCap} recorded on the thread's ORIGIN dispatch ("${origin.nonce}") — a fresh contract never manufactures a fresh retry budget; nothing was written`);
|
package/tools/dispatch-store.mjs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// The read half lives in dispatch-store-read.mjs so read-only consumers never reach this writer.
|
|
3
3
|
|
|
4
4
|
import { computeTreeFingerprint } from './core-evidence.mjs';
|
|
5
|
-
import { validateDelegationRecord } from './dispatch-record.mjs';
|
|
5
|
+
import { validateDelegationRecord, HEAD_BASELINE } from './dispatch-record.mjs';
|
|
6
|
+
import { computeBaseFingerprint } from './dispatch-baseline.mjs';
|
|
6
7
|
import {
|
|
7
8
|
delegationStoreStop,
|
|
8
9
|
resolveDelegationStorePath,
|
|
@@ -30,8 +31,8 @@ export const resolveDelegationLockPath = (storePath) => `${storePath}${DELEGATIO
|
|
|
30
31
|
|
|
31
32
|
export const UNCOMMITTED_STATE_FINGERPRINT = 'the uncommitted-state fingerprint';
|
|
32
33
|
|
|
33
|
-
export const uncommittedStateFingerprint = (cwd = process.cwd(), fsx) => {
|
|
34
|
-
const fingerprint = computeTreeFingerprint(cwd, fsx);
|
|
34
|
+
export const uncommittedStateFingerprint = (cwd = process.cwd(), fsx, base = HEAD_BASELINE) => {
|
|
35
|
+
const fingerprint = base.kind === 'head' ? computeTreeFingerprint(cwd, fsx) : computeBaseFingerprint(cwd, base, fsx);
|
|
35
36
|
if (fingerprint == null) {
|
|
36
37
|
throw stop(`cannot compute ${UNCOMMITTED_STATE_FINGERPRINT} — not inside a git work tree (or a git probe failed); a record never carries a null tree digest (fail closed)`);
|
|
37
38
|
}
|