wowbagger 0.1.0-alpha.9 → 0.5.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 +509 -0
- package/README.md +272 -136
- package/docs/adapter-contract.md +1 -1
- package/docs/host-contract.md +7 -1
- package/docs/mutation-contract.md +354 -82
- package/docs/work-claim-contract.md +466 -88
- package/package.json +2 -2
- package/schemas/core-capabilities-response.json +1 -1
- package/schemas/core-envelope.json +4 -3
- package/schemas/index.json +18 -0
- package/schemas/ledger-repair-proposal.json +170 -0
- package/schemas/ledger-repair-request.json +61 -0
- package/schemas/ledger-repair-response.json +90 -0
- package/schemas/report-config-v1.json +4 -0
- package/schemas/report-config-v2.json +5 -0
- package/skills/wowbagger/SKILL.md +242 -59
- package/src/adapter/core-probe.js +3 -4
- package/src/adapter/process-outcome.js +8 -1
- package/src/claim-capabilities.js +3 -3
- package/src/claim-coordinator.js +61 -12
- package/src/claim-journal.js +212 -7
- package/src/claim-prospective.js +1 -28
- package/src/claim-publication.js +412 -78
- package/src/claim-request.js +9 -0
- package/src/claim-store.js +9 -4
- package/src/cli.js +302 -56
- package/src/extensions.js +1 -0
- package/src/git-autocommit.js +106 -43
- package/src/git-reconciliation.js +74 -19
- package/src/git-worktrees.js +73 -0
- package/src/instrumentation.js +1 -0
- package/src/launch.js +2 -2
- package/src/ledger-repair.js +1170 -0
- package/src/mutation.js +73 -15
- package/src/reconciliation-classifier.js +117 -0
- package/src/report-evidence.js +158 -41
- package/src/report-graph.js +201 -73
- package/src/report-html.js +358 -155
- package/src/report-impact.js +106 -0
- package/src/report-selection.js +97 -0
- package/src/report-sequencing.js +4 -4
- package/src/report-svg.js +74 -20
- package/src/report-view.js +14 -1
- package/src/report.js +109 -23
- package/src/version-drift.js +98 -0
- package/src/worktree-identity.js +165 -0
package/src/claim-publication.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { access, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
7
|
appendClaimEntry,
|
|
8
|
+
assertClaimJournalCapacity,
|
|
7
9
|
claimJournalPath,
|
|
8
10
|
claimReconcileLogPath,
|
|
11
|
+
isClaimJournalCapacityError,
|
|
12
|
+
isProjectedJournalEntry,
|
|
13
|
+
parseReconcileLog,
|
|
14
|
+
replayClaimEntries,
|
|
9
15
|
replayClaimJournal,
|
|
10
16
|
writeReconcileLog,
|
|
11
17
|
} from './claim-journal.js';
|
|
@@ -13,9 +19,21 @@ import { advanceClockFloor, readBack } from './claim-operations.js';
|
|
|
13
19
|
import { claimStorePath, withClaimLock, writeClaimState } from './claim-store.js';
|
|
14
20
|
import { loadLedger, parseLedgerItemSource } from './ledger.js';
|
|
15
21
|
import { MAX_ITEM_SOURCE_BYTES } from './limits.js';
|
|
16
|
-
import { findRevisionOwner, readGitHeadLedger } from './git-reconciliation.js';
|
|
22
|
+
import { findRevisionOwner, readGitHeadLedger, readGitTreeFile } from './git-reconciliation.js';
|
|
17
23
|
import { publishClaimedCandidate, revisionFor } from './mutation.js';
|
|
24
|
+
import {
|
|
25
|
+
blocksTarget,
|
|
26
|
+
classifyReconciliation,
|
|
27
|
+
normalizeRevision,
|
|
28
|
+
requiresOwnerEvidence,
|
|
29
|
+
} from './reconciliation-classifier.js';
|
|
18
30
|
import { validateLedger } from './validate.js';
|
|
31
|
+
import {
|
|
32
|
+
assertUniqueWorktreeIdentity,
|
|
33
|
+
ensureWorktreeIdentity,
|
|
34
|
+
identityDiagnosticDetails,
|
|
35
|
+
readWorktreeIdentity,
|
|
36
|
+
} from './worktree-identity.js';
|
|
19
37
|
|
|
20
38
|
const ITEM_ID = /^wb_[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
|
|
21
39
|
const NAMESPACE_ID = /^wbns_[a-f0-9]{32}$/;
|
|
@@ -138,7 +156,11 @@ export async function readPublicationOutcome({ gitCommonDir, namespace, request
|
|
|
138
156
|
} catch (error) {
|
|
139
157
|
return publicationReadError(request, 'claim-store-unavailable',
|
|
140
158
|
'The durable claim store is unavailable.', {
|
|
141
|
-
reason: error?.code === 'CLAIM_LOCK_HELD'
|
|
159
|
+
reason: error?.code === 'CLAIM_LOCK_HELD'
|
|
160
|
+
? 'claim-store-locked'
|
|
161
|
+
: isClaimJournalCapacityError(error)
|
|
162
|
+
? 'journal-capacity-exceeded'
|
|
163
|
+
: 'claim-store-unreadable',
|
|
142
164
|
}, 6);
|
|
143
165
|
}
|
|
144
166
|
}
|
|
@@ -152,6 +174,7 @@ export async function publishClaimed({ ledgerDirectory, gitCommonDir, namespace,
|
|
|
152
174
|
}
|
|
153
175
|
|
|
154
176
|
const storePath = claimStorePath(gitCommonDir, namespace);
|
|
177
|
+
let intentAppended = false;
|
|
155
178
|
const journalPath = claimJournalPath(gitCommonDir, namespace);
|
|
156
179
|
try {
|
|
157
180
|
return await withClaimLock(storePath, async (namespaceLock) => {
|
|
@@ -176,6 +199,15 @@ export async function publishClaimed({ ledgerDirectory, gitCommonDir, namespace,
|
|
|
176
199
|
// file, so the first of them is the snapshot the rest share. The read
|
|
177
200
|
// under lock, which the mutation engine still performs on its own, is
|
|
178
201
|
// what makes the revision compare-and-swap meaningful.
|
|
202
|
+
//
|
|
203
|
+
// A claimed publication authorizes an item revision, so an ambiguous
|
|
204
|
+
// domain is refused before reconciliation classifies one. The writer
|
|
205
|
+
// names itself first, under this same hold, so a domain that cannot
|
|
206
|
+
// resolve its own identity refuses as its own invalid identity rather
|
|
207
|
+
// than as an unreadable roster — and so every entry this hold appends
|
|
208
|
+
// carries one ID resolved exactly once.
|
|
209
|
+
const currentWorktreeId = await ensureWorktreeIdentity({ ledgerDirectory, gitCommonDir });
|
|
210
|
+
await assertUniqueWorktreeIdentity({ ledgerDirectory });
|
|
179
211
|
let reconciled;
|
|
180
212
|
try {
|
|
181
213
|
reconciled = await reconcileClaimJournal({
|
|
@@ -185,8 +217,18 @@ export async function publishClaimed({ ledgerDirectory, gitCommonDir, namespace,
|
|
|
185
217
|
replayed,
|
|
186
218
|
physicalNow: new Date().toISOString(),
|
|
187
219
|
targetItemId: request.item_id,
|
|
220
|
+
currentWorktreeId,
|
|
221
|
+
writeLogOnUnsafe: false,
|
|
188
222
|
});
|
|
189
223
|
} catch (error) {
|
|
224
|
+
if (isClaimJournalCapacityError(error)) {
|
|
225
|
+
return publicationError(request, 'claim-store-unavailable',
|
|
226
|
+
'The durable claim store is unavailable.', {
|
|
227
|
+
reason: 'journal-capacity-exceeded',
|
|
228
|
+
ledger_namespace: request.ledger_namespace,
|
|
229
|
+
item_id: request.item_id,
|
|
230
|
+
}, 6);
|
|
231
|
+
}
|
|
190
232
|
if (error?.code !== 'CLOCK_FLOOR_PERSISTENCE_FAILED') throw error;
|
|
191
233
|
return publicationError(request, 'clock-floor-persistence-failed',
|
|
192
234
|
'The authoritative clock floor could not be persisted.', {
|
|
@@ -233,10 +275,9 @@ export async function publishClaimed({ ledgerDirectory, gitCommonDir, namespace,
|
|
|
233
275
|
active_owner_id: record.active?.owner_id ?? null,
|
|
234
276
|
active_epoch: record.active?.epoch ?? null,
|
|
235
277
|
}, 4);
|
|
236
|
-
return persistTerminal(entries, journalPath, ledgerDirectory, namespace, request, outcome, replayed.state, storePath);
|
|
278
|
+
return persistTerminal(entries, journalPath, ledgerDirectory, namespace, request, outcome, replayed.state, storePath, currentWorktreeId);
|
|
237
279
|
}
|
|
238
|
-
|
|
239
|
-
const intent = await appendClaimEntry(journalPath, {
|
|
280
|
+
const intentEntry = {
|
|
240
281
|
type: 'publish-intent',
|
|
241
282
|
operation_id: request.operation_id,
|
|
242
283
|
operation_digest: operationDigest(request),
|
|
@@ -245,20 +286,55 @@ export async function publishClaimed({ ledgerDirectory, gitCommonDir, namespace,
|
|
|
245
286
|
candidate_sha256: request.candidate_sha256,
|
|
246
287
|
fence: request.claim_fence,
|
|
247
288
|
floor: observedAt,
|
|
289
|
+
writer_worktree_id: currentWorktreeId,
|
|
248
290
|
state: 'pending',
|
|
291
|
+
};
|
|
292
|
+
const terminalEntry = (outcome) => ({
|
|
293
|
+
type: 'publish-final',
|
|
294
|
+
operation_id: request.operation_id,
|
|
295
|
+
operation_digest: operationDigest(request),
|
|
296
|
+
ledger_namespace: request.ledger_namespace,
|
|
297
|
+
item_id: request.item_id,
|
|
298
|
+
writer_worktree_id: currentWorktreeId,
|
|
299
|
+
outcome,
|
|
249
300
|
});
|
|
301
|
+
const itemPath = ledgerSnapshot.items.find((item) => item.data.id === request.item_id)?.path;
|
|
302
|
+
const possibleTerminals = [
|
|
303
|
+
terminalEntry(publicationSuccess(request, record, observedAt, itemPath, namespace)),
|
|
304
|
+
terminalEntry(publicationUnknown(request)),
|
|
305
|
+
terminalEntry(publicationError(request, 'ledger-revision-conflict',
|
|
306
|
+
'The durable ledger revision no longer matches this publication.', {
|
|
307
|
+
ledger_namespace: request.ledger_namespace,
|
|
308
|
+
item_id: request.item_id,
|
|
309
|
+
expected_revision: request.expected_revision,
|
|
310
|
+
actual_revision: request.expected_revision,
|
|
311
|
+
}, 4)),
|
|
312
|
+
];
|
|
313
|
+
const largestTerminal = possibleTerminals.reduce((largest, candidate) => (
|
|
314
|
+
Buffer.byteLength(JSON.stringify(candidate)) > Buffer.byteLength(JSON.stringify(largest))
|
|
315
|
+
? candidate
|
|
316
|
+
: largest
|
|
317
|
+
));
|
|
318
|
+
await assertClaimJournalCapacity(journalPath, [
|
|
319
|
+
intentEntry,
|
|
320
|
+
{ type: 'clock', now: observedAt, floor: observedAt },
|
|
321
|
+
largestTerminal,
|
|
322
|
+
]);
|
|
323
|
+
await publicationTestCheckpoint(scenario, 'before-publish-intent', ledgerDirectory);
|
|
324
|
+
const intent = await appendClaimEntry(journalPath, intentEntry);
|
|
250
325
|
entries.push(intent);
|
|
326
|
+
intentAppended = true;
|
|
251
327
|
await publicationTestCheckpoint(scenario, 'after-publish-intent', ledgerDirectory);
|
|
252
328
|
const mutation = await publishClaimedCandidate({
|
|
253
329
|
ledgerDirectory, request, scenario, ledgerSnapshot, namespaceLock, storePath,
|
|
254
330
|
});
|
|
255
331
|
if (!mutation.ok) {
|
|
256
332
|
const outcome = mutationFailure(request, mutation);
|
|
257
|
-
return persistTerminal(entries, journalPath, ledgerDirectory, namespace, request, outcome, replayed.state, storePath);
|
|
333
|
+
return persistTerminal(entries, journalPath, ledgerDirectory, namespace, request, outcome, replayed.state, storePath, currentWorktreeId);
|
|
258
334
|
}
|
|
259
335
|
await publicationTestCheckpoint(scenario, 'after-ledger-commit', ledgerDirectory);
|
|
260
336
|
const outcome = publicationSuccess(request, record, observedAt, mutation.item?.path, namespace);
|
|
261
|
-
const terminal = await persistTerminal(entries, journalPath, ledgerDirectory, namespace, request, outcome, replayed.state, storePath);
|
|
337
|
+
const terminal = await persistTerminal(entries, journalPath, ledgerDirectory, namespace, request, outcome, replayed.state, storePath, currentWorktreeId);
|
|
262
338
|
await publicationTestCheckpoint(scenario, 'after-terminal-record', ledgerDirectory);
|
|
263
339
|
return terminal;
|
|
264
340
|
});
|
|
@@ -268,20 +344,132 @@ export async function publishClaimed({ ledgerDirectory, gitCommonDir, namespace,
|
|
|
268
344
|
reason: 'claim-store-locked',
|
|
269
345
|
}, 6);
|
|
270
346
|
}
|
|
347
|
+
if (!intentAppended && isClaimJournalCapacityError(error)) {
|
|
348
|
+
return publicationError(request, 'claim-store-unavailable', 'The durable claim store is unavailable.', {
|
|
349
|
+
reason: 'journal-capacity-exceeded',
|
|
350
|
+
}, 6);
|
|
351
|
+
}
|
|
352
|
+
// An identity the coordination domain cannot resolve is a store this
|
|
353
|
+
// command could not read, not a mutation whose outcome is unknown: it
|
|
354
|
+
// refused before it appended an intent or touched an item byte.
|
|
355
|
+
if (error?.code === 'CLAIM_WORKTREE_IDENTITY_INVALID') {
|
|
356
|
+
return publicationError(request, 'claim-store-unavailable', 'The durable claim store is unavailable.', {
|
|
357
|
+
reason: 'claim-store-unreadable',
|
|
358
|
+
...identityDiagnosticDetails(error),
|
|
359
|
+
}, 6);
|
|
360
|
+
}
|
|
361
|
+
if (!intentAppended) {
|
|
362
|
+
return publicationError(request, 'claim-store-unavailable', 'The durable claim store is unavailable.', {
|
|
363
|
+
reason: 'claim-store-unreadable',
|
|
364
|
+
}, 6);
|
|
365
|
+
}
|
|
271
366
|
return publicationUnknown(request);
|
|
272
367
|
}
|
|
273
368
|
}
|
|
274
369
|
|
|
370
|
+
export async function hydrateClaimJournalFromHead({
|
|
371
|
+
ledgerDirectory,
|
|
372
|
+
gitCommonDir,
|
|
373
|
+
namespace,
|
|
374
|
+
replayed,
|
|
375
|
+
persist = true,
|
|
376
|
+
}) {
|
|
377
|
+
let log;
|
|
378
|
+
try {
|
|
379
|
+
log = await readGitTreeFile(
|
|
380
|
+
ledgerDirectory,
|
|
381
|
+
'HEAD',
|
|
382
|
+
`.wowbagger/reconcile-${namespace}.md`,
|
|
383
|
+
);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (error?.code === 128) return replayed;
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
const committed = parseReconcileLog(log, namespace);
|
|
389
|
+
if (committed.error) throw hydrationError(committed.error.reason);
|
|
390
|
+
if (committed.length === 0) return replayed;
|
|
391
|
+
const bySequence = new Map();
|
|
392
|
+
for (const entry of committed) {
|
|
393
|
+
if (!Number.isSafeInteger(entry.seq) || entry.seq < 1 || bySequence.has(entry.seq)) {
|
|
394
|
+
throw hydrationError('non-contiguous-sequence');
|
|
395
|
+
}
|
|
396
|
+
bySequence.set(entry.seq, entry);
|
|
397
|
+
}
|
|
398
|
+
for (const entry of committed) {
|
|
399
|
+
if (entry.seq > replayed.entries.length) continue;
|
|
400
|
+
if (!isDeepStrictEqual(replayed.entries[entry.seq - 1], entry)) {
|
|
401
|
+
throw hydrationError('local-journal-diverges-from-committed-log');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const maxSequence = Math.max(...bySequence.keys());
|
|
405
|
+
if (replayed.entries.length >= maxSequence) return replayed;
|
|
406
|
+
const fallbackTime = committed
|
|
407
|
+
.map((entry) => entry.observed_at ?? entry.physical_now)
|
|
408
|
+
.find((value) => typeof value === 'string')
|
|
409
|
+
?? '1970-01-01T00:00:00.000Z';
|
|
410
|
+
const missingEntries = [];
|
|
411
|
+
for (let sequence = replayed.entries.length + 1; sequence <= maxSequence; sequence += 1) {
|
|
412
|
+
const entry = bySequence.get(sequence);
|
|
413
|
+
if (entry) {
|
|
414
|
+
const { seq, ...withoutSequence } = entry;
|
|
415
|
+
missingEntries.push(withoutSequence);
|
|
416
|
+
} else {
|
|
417
|
+
missingEntries.push({
|
|
418
|
+
type: 'clock',
|
|
419
|
+
now: fallbackTime,
|
|
420
|
+
floor: fallbackTime,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!persist) {
|
|
425
|
+
const entries = [
|
|
426
|
+
...replayed.entries,
|
|
427
|
+
...missingEntries.map((entry, index) => ({
|
|
428
|
+
seq: replayed.entries.length + index + 1,
|
|
429
|
+
...entry,
|
|
430
|
+
})),
|
|
431
|
+
];
|
|
432
|
+
return {
|
|
433
|
+
state: replayClaimEntries(entries, namespace),
|
|
434
|
+
entries,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const journalPath = claimJournalPath(gitCommonDir, namespace);
|
|
438
|
+
for (const entry of missingEntries) {
|
|
439
|
+
await appendClaimEntry(journalPath, entry);
|
|
440
|
+
}
|
|
441
|
+
return replayClaimJournal(journalPath, namespace);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function hydrationError(reason) {
|
|
445
|
+
const error = new Error('The committed reconciliation log is invalid.');
|
|
446
|
+
error.code = 'CLAIM_JOURNAL_INVALID';
|
|
447
|
+
error.reason = reason;
|
|
448
|
+
return error;
|
|
449
|
+
}
|
|
450
|
+
|
|
275
451
|
export async function reconcileClaimJournal({
|
|
276
452
|
ledgerDirectory,
|
|
277
453
|
gitCommonDir,
|
|
278
454
|
namespace,
|
|
279
455
|
replayed,
|
|
280
456
|
physicalNow,
|
|
457
|
+
// The worktree this invocation speaks for, when it has established an
|
|
458
|
+
// identity. A caller that cannot name itself passes null, and every
|
|
459
|
+
// recorded writer then reads as unknown.
|
|
460
|
+
currentWorktreeId = null,
|
|
281
461
|
targetItemId = null,
|
|
462
|
+
writeLogOnUnsafe = true,
|
|
463
|
+
writeLogWhenEmpty = true,
|
|
282
464
|
}) {
|
|
283
465
|
const storePath = claimStorePath(gitCommonDir, namespace);
|
|
284
466
|
const journalPath = claimJournalPath(gitCommonDir, namespace);
|
|
467
|
+
replayed = await hydrateClaimJournalFromHead({
|
|
468
|
+
ledgerDirectory,
|
|
469
|
+
gitCommonDir,
|
|
470
|
+
namespace,
|
|
471
|
+
replayed,
|
|
472
|
+
});
|
|
285
473
|
const ledger = await loadLedger(path.resolve(ledgerDirectory));
|
|
286
474
|
const items = new Map(ledger.items.map((item) => [item.data.id, item]));
|
|
287
475
|
const terminalKeys = new Set(replayed.entries
|
|
@@ -293,6 +481,16 @@ export async function reconcileClaimJournal({
|
|
|
293
481
|
));
|
|
294
482
|
const entries = [...replayed.entries];
|
|
295
483
|
const findings = [];
|
|
484
|
+
const findingScopes = [];
|
|
485
|
+
// What each finding refuses is the coordinator's own judgement, never a
|
|
486
|
+
// published member, so the scope rules on the write as the finding is
|
|
487
|
+
// recorded and never becomes a member something has to strip back out.
|
|
488
|
+
let unsafe = false;
|
|
489
|
+
const addFinding = (scope, finding) => {
|
|
490
|
+
unsafe ||= blocksTarget(scope, finding.item_id, targetItemId);
|
|
491
|
+
findings.push(finding);
|
|
492
|
+
findingScopes.push(scope);
|
|
493
|
+
};
|
|
296
494
|
const observedAt = advanceClockFloor(replayed.state, physicalNow);
|
|
297
495
|
try {
|
|
298
496
|
entries.push(await appendClaimEntry(journalPath, {
|
|
@@ -327,6 +525,12 @@ export async function reconcileClaimJournal({
|
|
|
327
525
|
command: intent.command,
|
|
328
526
|
committed_revision: actualRevision,
|
|
329
527
|
...(intent.item_path ? { item_path: intent.item_path } : {}),
|
|
528
|
+
// The writer that authorized the attempt owns the revision this
|
|
529
|
+
// resolution ratifies, so its identity travels with the terminal
|
|
530
|
+
// entry. Absent on alpha.12 intents, which stay valid without it.
|
|
531
|
+
...(intent.writer_worktree_id
|
|
532
|
+
? { writer_worktree_id: intent.writer_worktree_id }
|
|
533
|
+
: {}),
|
|
330
534
|
observed_at: observedAt,
|
|
331
535
|
}));
|
|
332
536
|
} else if (actualRevision === intent.expected_revision) {
|
|
@@ -335,6 +539,10 @@ export async function reconcileClaimJournal({
|
|
|
335
539
|
attempt_id: intent.attempt_id,
|
|
336
540
|
ledger_namespace: namespace,
|
|
337
541
|
item_id: intent.item_id,
|
|
542
|
+
// A create abort names no predecessor revision, so its terminal must
|
|
543
|
+
// say which command aborted; patch and transition aborts keep their
|
|
544
|
+
// exact legacy shape and never carry a command.
|
|
545
|
+
...(intent.command === 'create-v1' ? { command: intent.command } : {}),
|
|
338
546
|
observed_revision: actualRevision,
|
|
339
547
|
observed_at: observedAt,
|
|
340
548
|
}));
|
|
@@ -343,7 +551,7 @@ export async function reconcileClaimJournal({
|
|
|
343
551
|
?? intent.item_path
|
|
344
552
|
?? null;
|
|
345
553
|
const pathLabel = expectedPath ?? `item ${intent.item_id}`;
|
|
346
|
-
|
|
554
|
+
addFinding('global', {
|
|
347
555
|
code: 'legacy-mutation-outcome-unknown',
|
|
348
556
|
item_id: intent.item_id,
|
|
349
557
|
attempt_id: intent.attempt_id,
|
|
@@ -416,10 +624,17 @@ export async function reconcileClaimJournal({
|
|
|
416
624
|
operation_digest: intent.operation_digest,
|
|
417
625
|
ledger_namespace: namespace,
|
|
418
626
|
item_id: intent.item_id,
|
|
627
|
+
// The writer that authorized the publication owns the revision this
|
|
628
|
+
// resolution ratifies, so its identity travels with the terminal entry.
|
|
629
|
+
// Absent on alpha.12 intents, which stay valid without it.
|
|
630
|
+
...(intent.writer_worktree_id
|
|
631
|
+
? { writer_worktree_id: intent.writer_worktree_id }
|
|
632
|
+
: {}),
|
|
419
633
|
outcome,
|
|
420
634
|
}));
|
|
421
635
|
const unknownPath = itemPathRelativeToLedger(ledgerDirectory, item?.file);
|
|
422
|
-
|
|
636
|
+
// A resolved intent is news, not a barrier: it refuses nothing.
|
|
637
|
+
addFinding(outcome.stdout.state === 'unknown' ? 'global' : 'none', {
|
|
423
638
|
code: outcome.stdout.state === 'unknown'
|
|
424
639
|
? 'publication-outcome-unknown'
|
|
425
640
|
: 'pending-intent-resolved',
|
|
@@ -484,7 +699,12 @@ export async function reconcileClaimJournal({
|
|
|
484
699
|
const expectedRevision = authorizedRevisionOf(latestAuthorized);
|
|
485
700
|
const authorizedRevisions = new Set(authorized.map(authorizedRevisionOf));
|
|
486
701
|
for (const intent of entries) {
|
|
487
|
-
|
|
702
|
+
// A create names no predecessor revision, so its `null` is the absence of
|
|
703
|
+
// one, never a ruling that absent bytes are authorized. Reading it as a
|
|
704
|
+
// revision would make an uncommitted create look finalized in Git.
|
|
705
|
+
if (intent.type === 'legacy-mutation-intent'
|
|
706
|
+
&& intent.item_id === itemId
|
|
707
|
+
&& intent.expected_revision !== null) {
|
|
488
708
|
authorizedRevisions.add(intent.expected_revision);
|
|
489
709
|
}
|
|
490
710
|
}
|
|
@@ -509,7 +729,7 @@ export async function reconcileClaimJournal({
|
|
|
509
729
|
);
|
|
510
730
|
if (activeMismatch?.earlier) {
|
|
511
731
|
const pathLabel = expectedPath ?? `item ${itemId}`;
|
|
512
|
-
|
|
732
|
+
addFinding('global', {
|
|
513
733
|
code: 'stale-write-detected',
|
|
514
734
|
item_id: itemId,
|
|
515
735
|
actual_revision: actualRevision,
|
|
@@ -528,21 +748,26 @@ export async function reconcileClaimJournal({
|
|
|
528
748
|
});
|
|
529
749
|
continue;
|
|
530
750
|
}
|
|
751
|
+
const expectedWriter = expectedWriterOf(
|
|
752
|
+
latestAuthorized.writer_worktree_id ?? null,
|
|
753
|
+
currentWorktreeId,
|
|
754
|
+
);
|
|
531
755
|
const diagnosis = await reconciliationDiagnosis({
|
|
532
756
|
ledgerDirectory,
|
|
533
757
|
actualRevision,
|
|
758
|
+
authorizedRevisions,
|
|
534
759
|
expectedPath,
|
|
535
760
|
expectedRevision,
|
|
536
761
|
headRevision,
|
|
537
|
-
|
|
762
|
+
expectedWriter,
|
|
538
763
|
});
|
|
539
|
-
|
|
764
|
+
addFinding(diagnosis.scope, {
|
|
540
765
|
code: 'stale-write-detected',
|
|
541
766
|
item_id: itemId,
|
|
542
767
|
actual_revision: workingTreeChanged ? actualRevision : headRevision,
|
|
543
768
|
expected_revision: expectedRevision,
|
|
544
769
|
observed_surface: workingTreeChanged ? 'working-tree' : 'git-head',
|
|
545
|
-
...diagnosis,
|
|
770
|
+
...diagnosis.finding,
|
|
546
771
|
...(record?.active ? {
|
|
547
772
|
active_fence: {
|
|
548
773
|
ledger_namespace: namespace,
|
|
@@ -575,7 +800,7 @@ export async function reconcileClaimJournal({
|
|
|
575
800
|
?? expected.item_path
|
|
576
801
|
?? null;
|
|
577
802
|
const pathLabel = expectedPath ?? `item ${record.item_id}`;
|
|
578
|
-
|
|
803
|
+
addFinding('global', {
|
|
579
804
|
code: earlier ? 'stale-write-detected' : 'revision-regression',
|
|
580
805
|
item_id: record.item_id,
|
|
581
806
|
actual_revision: actualRevision,
|
|
@@ -599,11 +824,20 @@ export async function reconcileClaimJournal({
|
|
|
599
824
|
});
|
|
600
825
|
}
|
|
601
826
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
)
|
|
827
|
+
const logPath = claimReconcileLogPath(path.resolve(ledgerDirectory), namespace);
|
|
828
|
+
let logExists = true;
|
|
829
|
+
try {
|
|
830
|
+
await access(logPath);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
833
|
+
logExists = false;
|
|
834
|
+
}
|
|
835
|
+
// An unsafe reconciliation and a log with nothing to project both leave the
|
|
836
|
+
// working tree alone unless the caller asks for the write, so a refusal never
|
|
837
|
+
// has to answer for a tracked artifact it did not intend to change.
|
|
838
|
+
const writesLog = (writeLogWhenEmpty || logExists || entries.some(isProjectedJournalEntry))
|
|
839
|
+
&& (writeLogOnUnsafe || !unsafe);
|
|
840
|
+
if (writesLog) await writeReconcileLog(logPath, namespace, entries);
|
|
607
841
|
try {
|
|
608
842
|
await writeClaimState(storePath, replayed.state);
|
|
609
843
|
} catch {
|
|
@@ -612,8 +846,14 @@ export async function reconcileClaimJournal({
|
|
|
612
846
|
return {
|
|
613
847
|
entries,
|
|
614
848
|
findings,
|
|
849
|
+
findingScopes,
|
|
615
850
|
gitHead,
|
|
616
851
|
headItems,
|
|
852
|
+
// Every coordinated item the journal knows and this working ledger does not
|
|
853
|
+
// hold. An item's number is immutable, so a caller that allocates the next
|
|
854
|
+
// one needs to know when an allocation exists that it cannot read; a stale
|
|
855
|
+
// revision of an item that is present hides no number.
|
|
856
|
+
missingCoordinatedItems: [...coordinatedItems].filter((itemId) => !items.has(itemId)),
|
|
617
857
|
// The snapshot reconciliation judged. Reconciliation writes only the
|
|
618
858
|
// journal, the claim state, and the ledger's `.wowbagger` reconcile log,
|
|
619
859
|
// none of which a complete ledger load reads, so these are still the bytes
|
|
@@ -623,15 +863,12 @@ export async function reconcileClaimJournal({
|
|
|
623
863
|
ledger,
|
|
624
864
|
observedAt,
|
|
625
865
|
state: replayed.state,
|
|
626
|
-
unsafe
|
|
627
|
-
finding.code !== 'pending-intent-resolved' && blocksTarget(finding, targetItemId)
|
|
628
|
-
)),
|
|
866
|
+
unsafe,
|
|
629
867
|
};
|
|
630
868
|
}
|
|
631
869
|
|
|
632
870
|
// An item's authorized revision is whatever the journal last ruled legitimate:
|
|
633
871
|
// a committed claimed publication, a legacy mutation, or an operator adoption.
|
|
634
|
-
// Adoption is the only one of the three that changes no item byte.
|
|
635
872
|
function authorizingEntries(entries, itemId) {
|
|
636
873
|
return entries.filter((entry) => (
|
|
637
874
|
entry.item_id === itemId
|
|
@@ -653,60 +890,110 @@ function itemPathRelativeToLedger(ledgerDirectory, file) {
|
|
|
653
890
|
return path.relative(path.resolve(ledgerDirectory), file).split(path.sep).join('/');
|
|
654
891
|
}
|
|
655
892
|
|
|
656
|
-
// A mutation names the item it targets. Another item's unresolved publication
|
|
657
|
-
// waits on a synchronization this mutation does not touch, so it reports as a
|
|
658
|
-
// finding without refusing the write. A caller that names no target, such as
|
|
659
|
-
// the `claim-verify` command, keeps every finding blocking.
|
|
660
|
-
function blocksTarget(finding, targetItemId) {
|
|
661
|
-
if (targetItemId === null || finding.item_id === targetItemId) return true;
|
|
662
|
-
return finding.reason !== 'worktree-synchronization-required';
|
|
663
|
-
}
|
|
664
|
-
|
|
665
893
|
// Ownership is evidence, never a guess: an unreadable history or a missing
|
|
666
894
|
// expected path leaves the revision unattributed instead of naming a ref.
|
|
667
895
|
async function revisionOwnerEvidence(ledgerDirectory, expectedPath, expectedRevision) {
|
|
668
|
-
if (!expectedPath) return {
|
|
896
|
+
if (!expectedPath) return { kind: 'unreachable' };
|
|
669
897
|
try {
|
|
670
898
|
return await findRevisionOwner(ledgerDirectory, expectedPath, expectedRevision);
|
|
671
899
|
} catch {
|
|
672
|
-
return {
|
|
900
|
+
return { kind: 'unreachable' };
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// The classifier decides which topology this is; the sentences stay here.
|
|
905
|
+
// Member order is part of the published finding, so each remedy builds its own
|
|
906
|
+
// shape rather than sharing a base object.
|
|
907
|
+
function topologyFinding(decision, expectedPath, expectedRevision) {
|
|
908
|
+
const pathLabel = expectedPath ?? 'the item path';
|
|
909
|
+
const at = expectedPath ? { expected_path: expectedPath } : {};
|
|
910
|
+
switch (decision.remediation) {
|
|
911
|
+
case 'commit-in-git':
|
|
912
|
+
return {
|
|
913
|
+
reason: decision.reason,
|
|
914
|
+
...at,
|
|
915
|
+
remediation: `Commit ${pathLabel} in Git, then run claim-verify.`,
|
|
916
|
+
};
|
|
917
|
+
case 'wait-for-named-owner':
|
|
918
|
+
return {
|
|
919
|
+
reason: decision.reason,
|
|
920
|
+
...at,
|
|
921
|
+
owner_ref: decision.owner.ref,
|
|
922
|
+
owner_commit: decision.owner.commit,
|
|
923
|
+
remediation: `WAIT for owner ${decision.owner.ref} to publish ${decision.owner.commit}, then synchronize this worktree and run claim-verify.`,
|
|
924
|
+
};
|
|
925
|
+
case 'establish-ownership':
|
|
926
|
+
return {
|
|
927
|
+
reason: decision.reason,
|
|
928
|
+
...at,
|
|
929
|
+
owner_unavailable: true,
|
|
930
|
+
remediation: `Ownership of ${pathLabel} revision ${expectedRevision} cannot be established from reachable refs; inspect reachable or dangling commits, restore or explicitly adopt reviewed bytes, then run claim-verify.`,
|
|
931
|
+
};
|
|
932
|
+
// Reachable but unowned: the revision is in Git already, so the only honest
|
|
933
|
+
// instruction is to go read it. Naming an owner to wait for would be a wait
|
|
934
|
+
// with no end, which is what item #178 found in the field.
|
|
935
|
+
case 'inspect-reachable-history':
|
|
936
|
+
return {
|
|
937
|
+
reason: decision.reason,
|
|
938
|
+
...at,
|
|
939
|
+
owner_unavailable: true,
|
|
940
|
+
remediation: `Revision ${expectedRevision} of ${pathLabel} is reachable in Git, but no active named worktree owner is established; inspect the reachable history, restore or explicitly adopt reviewed bytes, then run claim-verify.`,
|
|
941
|
+
};
|
|
942
|
+
case 'await-owner-commit':
|
|
943
|
+
return {
|
|
944
|
+
reason: decision.reason,
|
|
945
|
+
...at,
|
|
946
|
+
owner_unavailable: true,
|
|
947
|
+
remediation: `Ownership of ${pathLabel} revision ${expectedRevision} is not yet reachable; wait for the owning worktree to commit, then synchronize this worktree and run claim-verify.`,
|
|
948
|
+
};
|
|
949
|
+
// Two remedies, both named, in the order that makes the cost obvious. The
|
|
950
|
+
// field report behind item #113 read the single restore sentence as the
|
|
951
|
+
// only way out and discarded reviewed, merged work to obey it.
|
|
952
|
+
case 'restore-or-adopt':
|
|
953
|
+
return {
|
|
954
|
+
reason: decision.reason,
|
|
955
|
+
...at,
|
|
956
|
+
remediation: `Restore the authorized revision at ${pathLabel}, then run claim-verify; that discards the edit. Or adopt the committed revision of ${pathLabel} with claim-adopt, then run claim-verify; that keeps the edit.`,
|
|
957
|
+
};
|
|
958
|
+
default:
|
|
959
|
+
throw new Error(`The reconciliation topology named no remedy: ${decision.remediation}.`);
|
|
673
960
|
}
|
|
674
961
|
}
|
|
675
962
|
|
|
963
|
+
// Who the journal says wrote the authorized revision, judged against who is
|
|
964
|
+
// asking. An entry from before writer identity existed, or a caller that
|
|
965
|
+
// cannot name itself, leaves the writer unknown.
|
|
966
|
+
function expectedWriterOf(recordedWriter, currentWorktreeId) {
|
|
967
|
+
if (recordedWriter === null || currentWorktreeId === null) return 'unknown';
|
|
968
|
+
return recordedWriter === currentWorktreeId ? 'current' : 'other';
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Evidence in, scope and public finding out: the classifier rules on the
|
|
972
|
+
// topology, and this gathers exactly the evidence it rules on, then renders
|
|
973
|
+
// the sentence it prescribes.
|
|
676
974
|
async function reconciliationDiagnosis({
|
|
677
975
|
ledgerDirectory,
|
|
678
976
|
actualRevision,
|
|
977
|
+
authorizedRevisions,
|
|
679
978
|
expectedPath,
|
|
680
979
|
expectedRevision,
|
|
681
980
|
headRevision,
|
|
682
|
-
|
|
981
|
+
expectedWriter,
|
|
683
982
|
}) {
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
reason: 'worktree-synchronization-required',
|
|
696
|
-
...(expectedPath ? { expected_path: expectedPath } : {}),
|
|
697
|
-
...owner,
|
|
698
|
-
remediation: owner.owner_ref
|
|
699
|
-
? `WAIT for owner ${owner.owner_ref} to publish ${owner.owner_commit}, then synchronize this worktree and run claim-verify.`
|
|
700
|
-
: `Ownership of ${pathLabel} revision ${expectedRevision} cannot be established from reachable refs; inspect reachable or dangling commits, restore or explicitly adopt reviewed bytes, then run claim-verify.`,
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
// Two remedies, both named, in the order that makes the cost obvious. The
|
|
704
|
-
// field report behind item #113 read the single restore sentence as the only
|
|
705
|
-
// way out and discarded reviewed, merged work to obey it.
|
|
983
|
+
const revisions = {
|
|
984
|
+
workingTree: normalizeRevision(actualRevision, expectedRevision, authorizedRevisions),
|
|
985
|
+
head: normalizeRevision(headRevision, expectedRevision, authorizedRevisions),
|
|
986
|
+
};
|
|
987
|
+
const decision = classifyReconciliation({
|
|
988
|
+
...revisions,
|
|
989
|
+
expectedOwner: requiresOwnerEvidence(revisions)
|
|
990
|
+
? await revisionOwnerEvidence(ledgerDirectory, expectedPath, expectedRevision)
|
|
991
|
+
: null,
|
|
992
|
+
expectedWriter,
|
|
993
|
+
});
|
|
706
994
|
return {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
remediation: `Restore the authorized revision at ${pathLabel}, then run claim-verify; that discards the edit. Or adopt the committed revision of ${pathLabel} with claim-adopt, then run claim-verify; that keeps the edit.`,
|
|
995
|
+
scope: decision.scope,
|
|
996
|
+
finding: topologyFinding(decision, expectedPath, expectedRevision),
|
|
710
997
|
};
|
|
711
998
|
}
|
|
712
999
|
|
|
@@ -771,18 +1058,32 @@ export async function verifyClaimJournal({
|
|
|
771
1058
|
gitCommonDir,
|
|
772
1059
|
namespace,
|
|
773
1060
|
targetItemId = null,
|
|
1061
|
+
writeLogOnUnsafe = true,
|
|
1062
|
+
writeLogWhenEmpty = true,
|
|
774
1063
|
}) {
|
|
775
1064
|
const storePath = claimStorePath(gitCommonDir, namespace);
|
|
776
1065
|
const journalPath = claimJournalPath(gitCommonDir, namespace);
|
|
777
1066
|
try {
|
|
778
1067
|
return await withClaimLock(storePath, async () => {
|
|
1068
|
+
// Verification writes no item byte and creates no identity: it reports on
|
|
1069
|
+
// the worktree it runs in, and an unidentified worktree simply cannot
|
|
1070
|
+
// recognize itself in a recorded writer. Its own file is judged first, so
|
|
1071
|
+
// bytes this worktree owns keep reporting as its own invalid identity
|
|
1072
|
+
// rather than as an unreadable roster.
|
|
1073
|
+
const currentWorktreeId = await readWorktreeIdentity({ ledgerDirectory, gitCommonDir });
|
|
1074
|
+
// Verification reasons from recorded writers, so it refuses an ambiguous
|
|
1075
|
+
// domain before it classifies anything, exactly as a mutation does.
|
|
1076
|
+
await assertUniqueWorktreeIdentity({ ledgerDirectory });
|
|
779
1077
|
const reconciled = await reconcileClaimJournal({
|
|
780
1078
|
ledgerDirectory,
|
|
781
1079
|
gitCommonDir,
|
|
782
1080
|
namespace,
|
|
783
1081
|
replayed: await replayClaimJournal(journalPath, namespace),
|
|
784
1082
|
physicalNow: new Date().toISOString(),
|
|
1083
|
+
currentWorktreeId,
|
|
785
1084
|
targetItemId,
|
|
1085
|
+
writeLogWhenEmpty,
|
|
1086
|
+
writeLogOnUnsafe,
|
|
786
1087
|
});
|
|
787
1088
|
return {
|
|
788
1089
|
exit: reconciled.unsafe ? 6 : 0,
|
|
@@ -795,7 +1096,17 @@ export async function verifyClaimJournal({
|
|
|
795
1096
|
result: {
|
|
796
1097
|
ledger_namespace: namespace,
|
|
797
1098
|
observed_at: reconciled.observedAt,
|
|
798
|
-
|
|
1099
|
+
verification_scope: targetItemId === null
|
|
1100
|
+
? { mode: 'repository' }
|
|
1101
|
+
: { mode: 'target-item', item_id: targetItemId },
|
|
1102
|
+
findings: reconciled.findings.map((finding, index) => ({
|
|
1103
|
+
...finding,
|
|
1104
|
+
blocks_verification_scope: blocksTarget(
|
|
1105
|
+
reconciled.findingScopes[index],
|
|
1106
|
+
finding.item_id,
|
|
1107
|
+
targetItemId,
|
|
1108
|
+
),
|
|
1109
|
+
})),
|
|
799
1110
|
ledger_validation: ledgerValidationReport(reconciled.ledger),
|
|
800
1111
|
publications: publicationStatuses(reconciled.entries),
|
|
801
1112
|
},
|
|
@@ -817,7 +1128,10 @@ export async function verifyClaimJournal({
|
|
|
817
1128
|
details: {
|
|
818
1129
|
reason: error?.code === 'CLAIM_LOCK_HELD'
|
|
819
1130
|
? 'claim-store-locked'
|
|
820
|
-
:
|
|
1131
|
+
: isClaimJournalCapacityError(error)
|
|
1132
|
+
? 'journal-capacity-exceeded'
|
|
1133
|
+
: 'claim-store-unreadable',
|
|
1134
|
+
...identityDiagnosticDetails(error),
|
|
821
1135
|
},
|
|
822
1136
|
},
|
|
823
1137
|
},
|
|
@@ -829,20 +1143,30 @@ export async function verifyClaimJournal({
|
|
|
829
1143
|
// re-baselines the coordinator's authorized revision onto bytes that are
|
|
830
1144
|
// already committed, and writes no item byte. It runs while reconciliation is
|
|
831
1145
|
// unsafe on purpose — clearing that state is the point — so it never refuses on
|
|
832
|
-
// `publication-reconciliation-required` the way
|
|
833
|
-
//
|
|
834
|
-
// reconciliation just observed.
|
|
1146
|
+
// `publication-reconciliation-required` the way `claim acquire` or
|
|
1147
|
+
// `claim renew` do. Every precondition is checked under the claim lock against
|
|
1148
|
+
// the state reconciliation just observed.
|
|
835
1149
|
export async function adoptItemRevision({ ledgerDirectory, gitCommonDir, namespace, request }) {
|
|
836
1150
|
const storePath = claimStorePath(gitCommonDir, namespace);
|
|
837
1151
|
const journalPath = claimJournalPath(gitCommonDir, namespace);
|
|
838
1152
|
try {
|
|
839
1153
|
return await withClaimLock(storePath, async () => {
|
|
1154
|
+
// Adoption writes no item byte and creates no identity, so it names
|
|
1155
|
+
// itself by reading the ID it already answers to. Its own file is judged
|
|
1156
|
+
// first, so bytes this worktree owns keep reporting as its own invalid
|
|
1157
|
+
// identity rather than as an unreadable roster, exactly as claim-verify
|
|
1158
|
+
// reports them.
|
|
1159
|
+
const currentWorktreeId = await readWorktreeIdentity({ ledgerDirectory, gitCommonDir });
|
|
1160
|
+
// Adoption re-baselines an authorized revision, so it too refuses an
|
|
1161
|
+
// ambiguous domain before reconciliation classifies one.
|
|
1162
|
+
await assertUniqueWorktreeIdentity({ ledgerDirectory });
|
|
840
1163
|
const reconciled = await reconcileClaimJournal({
|
|
841
1164
|
ledgerDirectory,
|
|
842
1165
|
gitCommonDir,
|
|
843
1166
|
namespace,
|
|
844
1167
|
replayed: await replayClaimJournal(journalPath, namespace),
|
|
845
1168
|
physicalNow: new Date().toISOString(),
|
|
1169
|
+
currentWorktreeId,
|
|
846
1170
|
});
|
|
847
1171
|
const refusal = adoptionRefusal(reconciled, ledgerDirectory, request);
|
|
848
1172
|
if (refusal) return refusal;
|
|
@@ -901,17 +1225,26 @@ export async function adoptItemRevision({ ledgerDirectory, gitCommonDir, namespa
|
|
|
901
1225
|
contract_version: 1,
|
|
902
1226
|
state: 'unchanged',
|
|
903
1227
|
error: {
|
|
904
|
-
code: error
|
|
905
|
-
? '
|
|
906
|
-
: '
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
? '
|
|
913
|
-
: 'claim
|
|
914
|
-
|
|
1228
|
+
code: isClaimJournalCapacityError(error)
|
|
1229
|
+
? 'claim-store-unavailable'
|
|
1230
|
+
: error?.code === 'CLOCK_FLOOR_PERSISTENCE_FAILED'
|
|
1231
|
+
? 'clock-floor-persistence-failed'
|
|
1232
|
+
: 'claim-store-unavailable',
|
|
1233
|
+
message: isClaimJournalCapacityError(error)
|
|
1234
|
+
? 'The durable claim store is unavailable.'
|
|
1235
|
+
: error?.code === 'CLOCK_FLOOR_PERSISTENCE_FAILED'
|
|
1236
|
+
? 'The authoritative clock floor could not be persisted.'
|
|
1237
|
+
: 'The durable claim store is unavailable.',
|
|
1238
|
+
details: isClaimJournalCapacityError(error)
|
|
1239
|
+
? { reason: 'journal-capacity-exceeded' }
|
|
1240
|
+
: error?.code === 'CLOCK_FLOOR_PERSISTENCE_FAILED'
|
|
1241
|
+
? {}
|
|
1242
|
+
: {
|
|
1243
|
+
reason: error?.code === 'CLAIM_LOCK_HELD'
|
|
1244
|
+
? 'claim-store-locked'
|
|
1245
|
+
: 'claim-store-unreadable',
|
|
1246
|
+
...identityDiagnosticDetails(error),
|
|
1247
|
+
},
|
|
915
1248
|
},
|
|
916
1249
|
},
|
|
917
1250
|
};
|
|
@@ -1017,13 +1350,14 @@ export function operationDigest(request) {
|
|
|
1017
1350
|
return `sha256:${createHash('sha256').update(canonicalJson(request)).digest('hex')}`;
|
|
1018
1351
|
}
|
|
1019
1352
|
|
|
1020
|
-
async function persistTerminal(entries, journalPath, ledgerDirectory, namespace, request, outcome, state, storePath) {
|
|
1353
|
+
async function persistTerminal(entries, journalPath, ledgerDirectory, namespace, request, outcome, state, storePath, writerWorktreeId) {
|
|
1021
1354
|
const terminal = await appendClaimEntry(journalPath, {
|
|
1022
1355
|
type: 'publish-final',
|
|
1023
1356
|
operation_id: request.operation_id,
|
|
1024
1357
|
operation_digest: operationDigest(request),
|
|
1025
1358
|
ledger_namespace: request.ledger_namespace,
|
|
1026
1359
|
item_id: request.item_id,
|
|
1360
|
+
writer_worktree_id: writerWorktreeId,
|
|
1027
1361
|
outcome,
|
|
1028
1362
|
});
|
|
1029
1363
|
entries.push(terminal);
|