driftseal 2.1.0 → 3.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/README.md +42 -18
- package/README.zh-CN.md +34 -14
- package/benchmark/recent-log.js +181 -0
- package/bin/driftseal.js +561 -532
- package/lib/outcome-fold.js +377 -0
- package/lib/outcome-index-sqlite.js +802 -0
- package/lib/sqlite-runtime.js +71 -0
- package/package.json +7 -2
- package/skills/use-driftseal/SKILL.md +6 -5
- package/test/package-smoke.js +150 -0
package/bin/driftseal.js
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Seal root: $DRIFTSEAL_HOME, or .seal in cwd.
|
|
22
22
|
* Outcome log: <seal-root>/outcomes/events.jsonl.
|
|
23
|
-
* In a Git
|
|
23
|
+
* In a default Git-repository seal, an open outcome is parked beside the WAL until end.
|
|
24
|
+
* Custom $DRIFTSEAL_HOME seals write open outcomes directly to the WAL.
|
|
24
25
|
* MADR records: <seal-root>/madr/.
|
|
25
26
|
*/
|
|
26
27
|
|
|
@@ -32,6 +33,15 @@ const { isDeepStrictEqual } = require('util');
|
|
|
32
33
|
const { StringDecoder } = require('string_decoder');
|
|
33
34
|
const { execFileSync, spawnSync } = require('child_process');
|
|
34
35
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
36
|
+
const { createOutcomeFold } = require('../lib/outcome-fold.js');
|
|
37
|
+
const {
|
|
38
|
+
OutcomeIndexError,
|
|
39
|
+
SqliteUnavailableError,
|
|
40
|
+
openOutcomeIndex,
|
|
41
|
+
removeIndexFiles,
|
|
42
|
+
temporaryIndexPath,
|
|
43
|
+
} = require('../lib/outcome-index-sqlite.js');
|
|
44
|
+
const { assertSupportedNode } = require('../lib/sqlite-runtime.js');
|
|
35
45
|
|
|
36
46
|
const END_STATUSES = ['completed', 'partial', 'failed', 'abandoned'];
|
|
37
47
|
const DECISION_STATUSES = [
|
|
@@ -50,12 +60,22 @@ const PROTOCOL_VERSION = '2.1';
|
|
|
50
60
|
const DEFAULT_LOG_LANGUAGE = 'en';
|
|
51
61
|
const DEFAULT_LANE = 'main';
|
|
52
62
|
const LANE_NAME_RE = /^[a-z][a-z0-9-]{0,62}$/;
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
const IN_PROGRESS_SIDECAR = '.in-progress.jsonl';
|
|
64
|
+
const WORKSPACE_SIDECAR_IGNORE_NAMES = Object.freeze([
|
|
65
|
+
'.current-lane',
|
|
66
|
+
'.lane-index.json',
|
|
67
|
+
'.in-progress.jsonl',
|
|
68
|
+
'.driftseal-local-outcome.json',
|
|
69
|
+
'.outcome-index.sqlite',
|
|
70
|
+
'.outcome-index.sqlite-journal',
|
|
71
|
+
'.outcome-index.sqlite-wal',
|
|
72
|
+
'.outcome-index.sqlite-shm',
|
|
73
|
+
'..outcome-index.sqlite.*.tmp',
|
|
74
|
+
'..outcome-index.sqlite.*.tmp-*',
|
|
75
|
+
'..current-lane.*.tmp',
|
|
76
|
+
'..in-progress.jsonl.*.tmp',
|
|
77
|
+
'..driftseal-local-outcome.json.*.tmp',
|
|
78
|
+
]);
|
|
59
79
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
60
80
|
const LOCK_INIT_STALE_MS = 5 * 1000;
|
|
61
81
|
const READ_ONLY_NOTICE = '(read-only: another mutation holds the lock; tail repair skipped)';
|
|
@@ -65,6 +85,12 @@ const VERIFICATION_OUTPUT_CHUNK_BYTES = 64 * 1024;
|
|
|
65
85
|
const CAPTURE_OUTPUT_EDGE_CHARACTERS = 32 * 1024;
|
|
66
86
|
const CAPTURE_OUTPUT_OMISSION = '\n... [driftseal captured output truncated] ...\n';
|
|
67
87
|
const LOCAL_OUTCOME_PROVENANCE_FILE = '.driftseal-local-outcome.json';
|
|
88
|
+
const outcomeFoldEngine = createOutcomeFold({
|
|
89
|
+
fail,
|
|
90
|
+
contentHash,
|
|
91
|
+
logVersion: LOG_VERSION,
|
|
92
|
+
defaultLane: DEFAULT_LANE,
|
|
93
|
+
});
|
|
68
94
|
|
|
69
95
|
class DriftSealError extends Error {
|
|
70
96
|
constructor(message) {
|
|
@@ -561,13 +587,6 @@ function gitWorktreeRoot(cwd = process.cwd()) {
|
|
|
561
587
|
return gitCapture(['rev-parse', '--show-toplevel'], cwd);
|
|
562
588
|
}
|
|
563
589
|
|
|
564
|
-
function worktreeInProgressFile(cwd = process.cwd()) {
|
|
565
|
-
if (!isGitWorkTree(cwd)) return null;
|
|
566
|
-
const gitPath = gitCapture(['rev-parse', '--git-path', IN_PROGRESS_GIT_PATH], cwd);
|
|
567
|
-
if (!gitPath) return null;
|
|
568
|
-
return path.resolve(cwd, gitPath);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
590
|
function isParkableOutcomeLog() {
|
|
572
591
|
if (process.env.DRIFTSEAL_HOME) return false;
|
|
573
592
|
const root = gitWorktreeRoot();
|
|
@@ -575,40 +594,63 @@ function isParkableOutcomeLog() {
|
|
|
575
594
|
return path.resolve(logFile()) === path.resolve(root, '.seal', 'outcomes', 'events.jsonl');
|
|
576
595
|
}
|
|
577
596
|
|
|
597
|
+
function inProgressFileForLog(file) {
|
|
598
|
+
return path.join(path.dirname(file), IN_PROGRESS_SIDECAR);
|
|
599
|
+
}
|
|
600
|
+
|
|
578
601
|
function inProgressFile() {
|
|
579
602
|
if (!isParkableOutcomeLog()) return null;
|
|
580
|
-
return
|
|
603
|
+
return inProgressFileForLog(logFile());
|
|
581
604
|
}
|
|
582
605
|
|
|
583
|
-
function
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
606
|
+
function existingInProgressFileForLog(file) {
|
|
607
|
+
return inProgressFileForLog(file);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function existingInProgressFile() {
|
|
611
|
+
if (!isParkableOutcomeLog()) return null;
|
|
612
|
+
return existingInProgressFileForLog(logFile());
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function adoptInProgressFile() {
|
|
616
|
+
const current = inProgressFile();
|
|
617
|
+
if (!current) return null;
|
|
618
|
+
ensureDerivedLaneSidecarIgnore();
|
|
619
|
+
ensureDirectoryDurable(path.dirname(current));
|
|
620
|
+
return current;
|
|
588
621
|
}
|
|
589
622
|
|
|
590
623
|
function currentLaneFile() {
|
|
591
|
-
|
|
624
|
+
// Keep the worktree-local pointer beside the WAL so agent sandboxes that
|
|
625
|
+
// deny .git writes can still switch lanes.
|
|
592
626
|
return path.join(logDir(), '.current-lane');
|
|
593
627
|
}
|
|
594
628
|
|
|
595
629
|
function laneIndexFile() {
|
|
596
|
-
|
|
630
|
+
// Keep the disposable index beside the WAL so agent sandboxes that deny
|
|
631
|
+
// .git writes can still rebuild it inside the workspace.
|
|
632
|
+
return path.join(logDir(), '.outcome-index.sqlite');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function legacyLaneIndexFile() {
|
|
597
636
|
return path.join(logDir(), '.lane-index.json');
|
|
598
637
|
}
|
|
599
638
|
|
|
600
639
|
function emptyLaneCatalog() {
|
|
601
|
-
return
|
|
640
|
+
return outcomeFoldEngine.emptyLaneCatalog();
|
|
602
641
|
}
|
|
603
642
|
|
|
604
|
-
function
|
|
605
|
-
|
|
606
|
-
if (!file || !fs.existsSync(file)) return DEFAULT_LANE;
|
|
643
|
+
function readLaneNameFromFile(file) {
|
|
644
|
+
if (!file || !fs.existsSync(file)) return null;
|
|
607
645
|
const name = fs.readFileSync(file, 'utf8').trim();
|
|
608
|
-
if (!name) return
|
|
646
|
+
if (!name) return null;
|
|
609
647
|
return normalizeLaneName(name);
|
|
610
648
|
}
|
|
611
649
|
|
|
650
|
+
function readCurrentLaneName() {
|
|
651
|
+
return readLaneNameFromFile(currentLaneFile()) || DEFAULT_LANE;
|
|
652
|
+
}
|
|
653
|
+
|
|
612
654
|
function writeCurrentLaneName(name, { readOnly = false } = {}) {
|
|
613
655
|
if (readOnly) return;
|
|
614
656
|
const file = currentLaneFile();
|
|
@@ -618,371 +660,138 @@ function writeCurrentLaneName(name, { readOnly = false } = {}) {
|
|
|
618
660
|
atomicWriteFile(file, `${name}\n`, 0o600);
|
|
619
661
|
}
|
|
620
662
|
|
|
663
|
+
function existingAncestor(dir) {
|
|
664
|
+
let current = path.resolve(dir);
|
|
665
|
+
while (!fs.existsSync(current)) {
|
|
666
|
+
const parent = path.dirname(current);
|
|
667
|
+
if (parent === current) return current;
|
|
668
|
+
current = parent;
|
|
669
|
+
}
|
|
670
|
+
return current;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function listedIgnoreNames(content) {
|
|
674
|
+
return new Set(
|
|
675
|
+
content
|
|
676
|
+
.split(/\r?\n/)
|
|
677
|
+
.map((line) => line.trim())
|
|
678
|
+
.filter((line) => line.length > 0)
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function workspaceSidecarIgnoreFile() {
|
|
683
|
+
return path.join(logDir(), '.gitignore');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function gitContainedOutcomeLog() {
|
|
687
|
+
return isGitWorkTree(existingAncestor(logDir()));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function workspaceSidecarIgnoreComplete() {
|
|
691
|
+
const ignoreFile = workspaceSidecarIgnoreFile();
|
|
692
|
+
if (!fs.existsSync(ignoreFile)) return false;
|
|
693
|
+
const listed = listedIgnoreNames(fs.readFileSync(ignoreFile, 'utf8'));
|
|
694
|
+
return WORKSPACE_SIDECAR_IGNORE_NAMES.every((name) => listed.has(name));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function canPersistDerivedOutcomeIndex() {
|
|
698
|
+
return !gitContainedOutcomeLog() || workspaceSidecarIgnoreComplete();
|
|
699
|
+
}
|
|
700
|
+
|
|
621
701
|
function ensureDerivedLaneSidecarIgnore() {
|
|
622
|
-
if (
|
|
623
|
-
|
|
624
|
-
const ignoreFile = path.join(logDir(), '.gitignore');
|
|
702
|
+
if (!gitContainedOutcomeLog()) return { changed: false, target: null };
|
|
703
|
+
const ignoreFile = workspaceSidecarIgnoreFile();
|
|
625
704
|
let current = fs.existsSync(ignoreFile) ? fs.readFileSync(ignoreFile, 'utf8') : '';
|
|
705
|
+
const listed = listedIgnoreNames(current);
|
|
626
706
|
let next = current;
|
|
627
|
-
for (const name of
|
|
628
|
-
|
|
629
|
-
if (present) continue;
|
|
707
|
+
for (const name of WORKSPACE_SIDECAR_IGNORE_NAMES) {
|
|
708
|
+
if (listed.has(name)) continue;
|
|
630
709
|
if (next && !next.endsWith('\n')) next += '\n';
|
|
631
710
|
next += `${name}\n`;
|
|
632
711
|
}
|
|
633
|
-
if (next === current) return;
|
|
712
|
+
if (next === current) return { changed: false, target: ignoreFile };
|
|
634
713
|
ensureDirectoryDurable(logDir());
|
|
635
714
|
atomicWriteFile(ignoreFile, next, 0o644);
|
|
715
|
+
return { changed: true, target: ignoreFile };
|
|
636
716
|
}
|
|
637
717
|
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
function emptyLaneIndexState() {
|
|
643
|
-
return {
|
|
644
|
-
indexVersion: LANE_INDEX_VERSION,
|
|
645
|
-
source: {
|
|
646
|
-
indexedThrough: 0,
|
|
647
|
-
indexedLines: 0,
|
|
648
|
-
prefixHash: contentHash(''),
|
|
649
|
-
tailHash: contentHash(''),
|
|
650
|
-
},
|
|
651
|
-
lastBuild: 'full',
|
|
652
|
-
lanes: emptyLaneCatalog(),
|
|
653
|
-
order: [],
|
|
654
|
-
records: new Map(),
|
|
655
|
-
ranges: new Map(),
|
|
656
|
-
reconciliations: new Map(),
|
|
657
|
-
};
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function hashFileRange(file, start, length) {
|
|
661
|
-
if (length <= 0) return contentHash('');
|
|
718
|
+
function hashFilePrefix(file, length) {
|
|
719
|
+
const hash = crypto.createHash('sha256');
|
|
720
|
+
if (length <= 0) return hash.digest('hex');
|
|
662
721
|
const fd = fs.openSync(file, 'r');
|
|
663
722
|
try {
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
723
|
+
const buffer = Buffer.alloc(Math.min(1024 * 1024, length));
|
|
724
|
+
let position = 0;
|
|
725
|
+
while (position < length) {
|
|
726
|
+
const requested = Math.min(buffer.length, length - position);
|
|
727
|
+
const read = fs.readSync(fd, buffer, 0, requested, position);
|
|
728
|
+
if (read === 0) break;
|
|
729
|
+
hash.update(buffer.subarray(0, read));
|
|
730
|
+
position += read;
|
|
731
|
+
}
|
|
732
|
+
if (position !== length) return null;
|
|
733
|
+
return hash.digest('hex');
|
|
667
734
|
} finally {
|
|
668
735
|
fs.closeSync(fd);
|
|
669
736
|
}
|
|
670
737
|
}
|
|
671
738
|
|
|
672
739
|
function laneIndexSourceIdentity(file, indexedThrough, indexedLines = 0) {
|
|
673
|
-
if (!fs.existsSync(file)
|
|
740
|
+
if (!fs.existsSync(file)) {
|
|
674
741
|
return {
|
|
675
742
|
indexedThrough: 0,
|
|
676
743
|
indexedLines: 0,
|
|
677
|
-
|
|
678
|
-
|
|
744
|
+
walHash: contentHash(''),
|
|
745
|
+
device: null,
|
|
746
|
+
inode: null,
|
|
747
|
+
mtimeMs: null,
|
|
748
|
+
ctimeMs: null,
|
|
679
749
|
};
|
|
680
750
|
}
|
|
681
|
-
const
|
|
682
|
-
const tail = Math.min(LANE_INDEX_TAIL_BYTES, indexedThrough);
|
|
751
|
+
const stat = fs.statSync(file);
|
|
683
752
|
return {
|
|
684
753
|
indexedThrough,
|
|
685
754
|
indexedLines,
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
function laneIndexMatchesFile(index, file) {
|
|
692
|
-
if (!index || index.indexVersion !== LANE_INDEX_VERSION) return false;
|
|
693
|
-
if (!Number.isSafeInteger(index.source?.indexedLines) || index.source.indexedLines < 0) return false;
|
|
694
|
-
if (!fs.existsSync(file)) return index.source.indexedThrough === 0;
|
|
695
|
-
const size = fs.statSync(file).size;
|
|
696
|
-
if (size < index.source.indexedThrough) return false;
|
|
697
|
-
const identity = laneIndexSourceIdentity(file, index.source.indexedThrough);
|
|
698
|
-
return (
|
|
699
|
-
identity.prefixHash === index.source.prefixHash &&
|
|
700
|
-
identity.tailHash === index.source.tailHash
|
|
701
|
-
);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function serializeLaneIndex(state) {
|
|
705
|
-
return {
|
|
706
|
-
indexVersion: state.indexVersion,
|
|
707
|
-
source: state.source,
|
|
708
|
-
lastBuild: state.lastBuild,
|
|
709
|
-
lanes: Object.fromEntries(
|
|
710
|
-
[...state.lanes.entries()].map(([name, lane]) => [
|
|
711
|
-
name,
|
|
712
|
-
{ name, description: lane.description || null, addedAt: lane.addedAt || null, head: lane.head || null, inferred: lane.inferred === true },
|
|
713
|
-
])
|
|
714
|
-
),
|
|
715
|
-
order: [...state.order],
|
|
716
|
-
records: Object.fromEntries(state.records),
|
|
717
|
-
ranges: Object.fromEntries(state.ranges),
|
|
718
|
-
reconciliations: Object.fromEntries(state.reconciliations),
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function deserializeLaneIndex(raw) {
|
|
723
|
-
if (!raw || raw.indexVersion !== LANE_INDEX_VERSION || !raw.source || !raw.records) return null;
|
|
724
|
-
const lanes = new Map();
|
|
725
|
-
for (const [name, lane] of Object.entries(raw.lanes || defaultLaneCatalogObject())) {
|
|
726
|
-
lanes.set(name, {
|
|
727
|
-
name,
|
|
728
|
-
description: lane.description || null,
|
|
729
|
-
addedAt: lane.addedAt || null,
|
|
730
|
-
head: lane.head || null,
|
|
731
|
-
inferred: lane.inferred === true,
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
if (!lanes.has(DEFAULT_LANE)) {
|
|
735
|
-
lanes.set(DEFAULT_LANE, { name: DEFAULT_LANE, description: null, addedAt: null, head: null });
|
|
736
|
-
}
|
|
737
|
-
return {
|
|
738
|
-
indexVersion: LANE_INDEX_VERSION,
|
|
739
|
-
source: raw.source,
|
|
740
|
-
lastBuild: raw.lastBuild || 'full',
|
|
741
|
-
lanes,
|
|
742
|
-
order: Array.isArray(raw.order) ? [...raw.order] : [],
|
|
743
|
-
records: new Map(Object.entries(raw.records)),
|
|
744
|
-
ranges: new Map(Object.entries(raw.ranges || {})),
|
|
745
|
-
reconciliations: new Map(Object.entries(raw.reconciliations || {})),
|
|
755
|
+
walHash: hashFilePrefix(file, indexedThrough),
|
|
756
|
+
device: Number(stat.dev),
|
|
757
|
+
inode: Number(stat.ino),
|
|
758
|
+
mtimeMs: stat.mtimeMs,
|
|
759
|
+
ctimeMs: stat.ctimeMs,
|
|
746
760
|
};
|
|
747
761
|
}
|
|
748
762
|
|
|
749
|
-
function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
rec.previous = heads.get(rec.lane) || null;
|
|
758
|
-
heads.set(rec.lane, id);
|
|
763
|
+
function laneIndexMatchesFile(source, file, { exact = false } = {}) {
|
|
764
|
+
if (
|
|
765
|
+
!source ||
|
|
766
|
+
typeof source.walHash !== 'string' ||
|
|
767
|
+
!Number.isSafeInteger(source.indexedLines) ||
|
|
768
|
+
source.indexedLines < 0
|
|
769
|
+
) {
|
|
770
|
+
return false;
|
|
759
771
|
}
|
|
760
|
-
|
|
761
|
-
|
|
772
|
+
if (!fs.existsSync(file)) return source.indexedThrough === 0;
|
|
773
|
+
const stat = fs.statSync(file);
|
|
774
|
+
const size = stat.size;
|
|
775
|
+
if (size < source.indexedThrough || (exact && size !== source.indexedThrough)) return false;
|
|
776
|
+
if (
|
|
777
|
+
source.device !== null &&
|
|
778
|
+
source.inode !== null &&
|
|
779
|
+
(Number(stat.dev) !== source.device || Number(stat.ino) !== source.inode)
|
|
780
|
+
) {
|
|
781
|
+
return false;
|
|
762
782
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const range = state.ranges.get(ev.id) || { firstByte: startByte, lastByte: endByte };
|
|
770
|
-
range.lastByte = endByte;
|
|
771
|
-
state.ranges.set(ev.id, range);
|
|
783
|
+
if (
|
|
784
|
+
size === source.indexedThrough &&
|
|
785
|
+
stat.mtimeMs === source.mtimeMs &&
|
|
786
|
+
stat.ctimeMs === source.ctimeMs
|
|
787
|
+
) {
|
|
788
|
+
return true;
|
|
772
789
|
}
|
|
773
|
-
|
|
790
|
+
return hashFilePrefix(file, source.indexedThrough) === source.walHash;
|
|
774
791
|
}
|
|
775
792
|
|
|
776
793
|
function applyFoldEvent(state, ev) {
|
|
777
|
-
|
|
778
|
-
const ensureLane = (name) => {
|
|
779
|
-
if (lanes.has(name)) return;
|
|
780
|
-
lanes.set(name, { name, description: null, addedAt: null, head: null, inferred: true });
|
|
781
|
-
};
|
|
782
|
-
if (ev.type === 'begin') {
|
|
783
|
-
if (records.has(ev.id)) fail(`duplicate begin event for outcome id: ${ev.id}`);
|
|
784
|
-
const record = newOutcomeRecord(ev);
|
|
785
|
-
ensureLane(record.lane);
|
|
786
|
-
records.set(ev.id, record);
|
|
787
|
-
order.push(ev.id);
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
if (ev.type === 'import') {
|
|
791
|
-
if (records.has(ev.id)) fail(`duplicate imported outcome id: ${ev.id}`);
|
|
792
|
-
const record = newOutcomeRecord({
|
|
793
|
-
...ev,
|
|
794
|
-
ts: ev.beganAt,
|
|
795
|
-
acceptance: [],
|
|
796
|
-
verify: null,
|
|
797
|
-
});
|
|
798
|
-
ensureLane(record.lane);
|
|
799
|
-
record.status = ev.status;
|
|
800
|
-
record.tsEnd = ev.endedAt;
|
|
801
|
-
record.note = ev.summary || null;
|
|
802
|
-
record.reclaimed = ev.reclaimed === true;
|
|
803
|
-
record.reclaimReason = ev.reclaimReason || null;
|
|
804
|
-
record.reclaimedAt = ev.reclaimedAt || null;
|
|
805
|
-
record.imported = {
|
|
806
|
-
sourceIds: ev.sources.map((source) => source.id),
|
|
807
|
-
sourceFingerprint: ev.sourceFingerprint,
|
|
808
|
-
sources: ev.sources,
|
|
809
|
-
};
|
|
810
|
-
records.set(ev.id, record);
|
|
811
|
-
order.push(ev.id);
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
if (ev.type === 'migration') return;
|
|
815
|
-
if (ev.type === 'lane_add') {
|
|
816
|
-
const existing = lanes.get(ev.lane);
|
|
817
|
-
if (existing && !existing.inferred) {
|
|
818
|
-
if (ev.description) existing.description = ev.description;
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
lanes.set(ev.lane, {
|
|
822
|
-
name: ev.lane,
|
|
823
|
-
description: ev.description || null,
|
|
824
|
-
addedAt: ev.ts,
|
|
825
|
-
head: existing ? existing.head : null,
|
|
826
|
-
inferred: false,
|
|
827
|
-
});
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
if (ev.type === 'lane_assign') {
|
|
831
|
-
const rec = records.get(ev.id);
|
|
832
|
-
if (!rec) fail(`lane assign references unknown outcome id: ${ev.id}`);
|
|
833
|
-
if (rec.status === 'in_progress') fail(`cannot assign lane of in_progress outcome ${ev.id}`);
|
|
834
|
-
ensureLane(ev.lane);
|
|
835
|
-
rec.lane = ev.lane;
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
if (ev.type === 'extend') {
|
|
839
|
-
const rec = records.get(ev.id);
|
|
840
|
-
if (!rec) fail(`extension references unknown outcome id: ${ev.id}`);
|
|
841
|
-
if (rec.status !== 'in_progress') fail(`extension occurred after outcome ${ev.id} was closed`);
|
|
842
|
-
rec.extensions.push({
|
|
843
|
-
extension: ev.extension,
|
|
844
|
-
acceptance: ev.acceptance,
|
|
845
|
-
verify: ev.verify,
|
|
846
|
-
decisions: ev.decisions,
|
|
847
|
-
extendedAt: ev.ts,
|
|
848
|
-
head: ev.head || null,
|
|
849
|
-
});
|
|
850
|
-
rec.acceptance = [...new Set([...rec.acceptance, ...ev.acceptance])];
|
|
851
|
-
if (ev.verify) rec.verify = ev.verify;
|
|
852
|
-
rec.decisions = [...new Set([...rec.decisions, ...ev.decisions])];
|
|
853
|
-
rec.contractHash = outcomeContractHash(rec);
|
|
854
|
-
rec.verification = null;
|
|
855
|
-
rec.decisionUpdates = [];
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
if (ev.type === 'verify') {
|
|
859
|
-
const rec = records.get(ev.id);
|
|
860
|
-
if (!rec) fail(`verification event references unknown outcome id: ${ev.id}`);
|
|
861
|
-
if (rec.status !== 'in_progress') fail(`verification occurred after outcome ${ev.id} was closed`);
|
|
862
|
-
if (rec.acceptance.length === 0 || !rec.verify) {
|
|
863
|
-
fail(`verification event references outcome ${ev.id} without acceptance criteria`);
|
|
864
|
-
}
|
|
865
|
-
if (ev.command !== rec.verify) fail(`verification command does not match outcome ${ev.id}`);
|
|
866
|
-
if (rec.logVersion === LOG_VERSION && ev.contractHash !== rec.contractHash) {
|
|
867
|
-
fail(`verification contract does not match outcome ${ev.id}`);
|
|
868
|
-
}
|
|
869
|
-
rec.verificationAttempts.push(ev);
|
|
870
|
-
rec.verification = ev;
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
if (ev.type === 'reclaim' || ev.type === 'unreclaim') {
|
|
874
|
-
const rec = records.get(ev.id);
|
|
875
|
-
if (!rec) fail(`${ev.type} event references unknown outcome id: ${ev.id}`);
|
|
876
|
-
if (ev.type === 'reclaim') {
|
|
877
|
-
if (rec.status === 'in_progress') fail(`cannot reclaim outcome ${ev.id} while it is in_progress`);
|
|
878
|
-
if (rec.reclaimed) fail(`duplicate reclaim event for outcome id: ${ev.id}`);
|
|
879
|
-
rec.reclaimed = true;
|
|
880
|
-
rec.reclaimReason = ev.reason;
|
|
881
|
-
rec.reclaimedAt = ev.ts;
|
|
882
|
-
} else {
|
|
883
|
-
if (!rec.reclaimed) fail(`unreclaim event for outcome id that is not reclaimed: ${ev.id}`);
|
|
884
|
-
rec.reclaimed = false;
|
|
885
|
-
rec.reclaimReason = null;
|
|
886
|
-
rec.reclaimedAt = null;
|
|
887
|
-
}
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
if (ev.type === 'end') {
|
|
891
|
-
const rec = records.get(ev.id);
|
|
892
|
-
if (!rec) fail(`end event references unknown outcome id: ${ev.id}`);
|
|
893
|
-
if (rec.status !== 'in_progress') fail(`duplicate end event for outcome id: ${ev.id}`);
|
|
894
|
-
const conflictingCancellation = rec.decisionTerminals.find(
|
|
895
|
-
(terminal) => terminal.type === 'decision_reconcile_cancel' && terminal.outcomeStatus !== ev.status
|
|
896
|
-
);
|
|
897
|
-
if (conflictingCancellation) {
|
|
898
|
-
fail(`outcome ${ev.id} was closed as ${ev.status} after reconciliation recovery was cancelled for ${conflictingCancellation.outcomeStatus}`);
|
|
899
|
-
}
|
|
900
|
-
if (
|
|
901
|
-
['completed', 'partial'].includes(ev.status) &&
|
|
902
|
-
rec.decisions.length > 0 &&
|
|
903
|
-
((rec.logVersion === 1 && rec.schemaVersion >= 2 && (ev.schemaVersion || 1) < 2) ||
|
|
904
|
-
rec.decisions.some((decisionId) => qualifyingDecisionUpdates(rec, decisionId).length === 0))
|
|
905
|
-
) {
|
|
906
|
-
fail(`linked outcome ${ev.id} was closed without reconciling every declared decision`);
|
|
907
|
-
}
|
|
908
|
-
if (ev.status === 'completed' && rec.acceptance.length > 0) {
|
|
909
|
-
if (!rec.verification || !rec.verification.passed) {
|
|
910
|
-
fail(`acceptance-bound outcome ${ev.id} was completed without successful machine verification`);
|
|
911
|
-
}
|
|
912
|
-
if (
|
|
913
|
-
(rec.logVersion === 1 && (ev.schemaVersion || 1) < 4) ||
|
|
914
|
-
ev.verificationId !== rec.verification.verificationId ||
|
|
915
|
-
(ev.workspace ?? null) !== rec.verification.workspace ||
|
|
916
|
-
(rec.logVersion === LOG_VERSION &&
|
|
917
|
-
(ev.contractHash !== rec.contractHash || rec.verification.contractHash !== rec.contractHash))
|
|
918
|
-
) {
|
|
919
|
-
fail(`acceptance-bound outcome ${ev.id} was completed with stale machine verification`);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
rec.status = ev.status;
|
|
923
|
-
rec.tsEnd = ev.ts;
|
|
924
|
-
rec.note = ev.note || null;
|
|
925
|
-
rec.verifyResult = ev.verifyResult || null;
|
|
926
|
-
rec.endHead = ev.head || null;
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
if (ev.type === 'decision_reconcile_prepare') {
|
|
930
|
-
const rec = records.get(ev.id);
|
|
931
|
-
if (!rec) fail(`decision reconciliation references unknown outcome id: ${ev.id}`);
|
|
932
|
-
if (rec.status !== 'in_progress') fail(`decision reconciliation occurred after outcome ${ev.id} was closed`);
|
|
933
|
-
if (!rec.decisions.includes(ev.decisionId)) fail(`decision reconciliation references unlinked decision ${ev.decisionId}`);
|
|
934
|
-
if (reconciliations.has(ev.reconciliationId)) fail(`duplicate reconciliation id: ${ev.reconciliationId}`);
|
|
935
|
-
rec.decisionPrepares.push(ev);
|
|
936
|
-
reconciliations.set(ev.reconciliationId, {
|
|
937
|
-
prepare: ev,
|
|
938
|
-
terminal: null,
|
|
939
|
-
contractHash: rec.contractHash,
|
|
940
|
-
});
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
if (ev.type === 'decision_reconcile') {
|
|
944
|
-
const rec = records.get(ev.id);
|
|
945
|
-
if (!rec) fail(`decision reconciliation references unknown outcome id: ${ev.id}`);
|
|
946
|
-
if (rec.status !== 'in_progress') fail(`decision reconciliation occurred after outcome ${ev.id} was closed`);
|
|
947
|
-
if (rec.logVersion === 1 && rec.schemaVersion >= 2) {
|
|
948
|
-
fail(`linked legacy schema-v2 outcome ${rec.id} contains a legacy decision reconciliation`);
|
|
949
|
-
}
|
|
950
|
-
rec.decisionUpdates.push(ev);
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
if (
|
|
954
|
-
ev.type === 'decision_reconcile_commit' ||
|
|
955
|
-
ev.type === 'decision_reconcile_abort' ||
|
|
956
|
-
ev.type === 'decision_reconcile_cancel'
|
|
957
|
-
) {
|
|
958
|
-
const rec = records.get(ev.id);
|
|
959
|
-
const reconciliation = reconciliations.get(ev.reconciliationId);
|
|
960
|
-
if (rec && rec.status !== 'in_progress') fail(`decision reconciliation occurred after outcome ${ev.id} was closed`);
|
|
961
|
-
if (!rec || !reconciliation || reconciliation.prepare.id !== ev.id || reconciliation.prepare.decisionId !== ev.decisionId) {
|
|
962
|
-
fail(`decision reconciliation terminal has no matching prepare: ${ev.reconciliationId}`);
|
|
963
|
-
}
|
|
964
|
-
if (reconciliation.terminal) fail(`decision reconciliation already has a terminal event: ${ev.reconciliationId}`);
|
|
965
|
-
const priorCancellation = rec.decisionTerminals.find((terminal) => terminal.type === 'decision_reconcile_cancel');
|
|
966
|
-
if (ev.type === 'decision_reconcile_cancel' && priorCancellation && priorCancellation.outcomeStatus !== ev.outcomeStatus) {
|
|
967
|
-
fail(`outcome ${ev.id} has conflicting reconciliation cancellation statuses`);
|
|
968
|
-
}
|
|
969
|
-
if (
|
|
970
|
-
ev.type === 'decision_reconcile_commit' &&
|
|
971
|
-
(reconciliation.prepare.newHash !== ev.fileHash ||
|
|
972
|
-
reconciliation.prepare.fromStatus !== ev.fromStatus ||
|
|
973
|
-
reconciliation.prepare.toStatus !== ev.toStatus)
|
|
974
|
-
) {
|
|
975
|
-
fail(`decision reconciliation commit does not match prepare: ${ev.reconciliationId}`);
|
|
976
|
-
}
|
|
977
|
-
reconciliation.terminal = ev;
|
|
978
|
-
rec.decisionTerminals.push(ev);
|
|
979
|
-
if (
|
|
980
|
-
ev.type === 'decision_reconcile_commit' &&
|
|
981
|
-
reconciliation.contractHash === rec.contractHash
|
|
982
|
-
) {
|
|
983
|
-
rec.decisionUpdates.push(ev);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
794
|
+
return outcomeFoldEngine.applyFoldEvent(state, ev);
|
|
986
795
|
}
|
|
987
796
|
|
|
988
797
|
function consumeLogSlice(file, startByte, onEvent, { repairTail = false, readOnly = false, startLine = 0 } = {}) {
|
|
@@ -1039,111 +848,225 @@ function consumeLogSlice(file, startByte, onEvent, { repairTail = false, readOnl
|
|
|
1039
848
|
}
|
|
1040
849
|
onEvent(event, start, pos);
|
|
1041
850
|
} catch (err) {
|
|
1042
|
-
if (err instanceof DriftSealError) throw err;
|
|
851
|
+
if (err instanceof DriftSealError || err instanceof OutcomeIndexError) throw err;
|
|
1043
852
|
fail(`corrupt log line ${lineNumber} in ${file}`);
|
|
1044
853
|
}
|
|
1045
854
|
}
|
|
1046
855
|
return { endByte: pos, endLine: lineNumber };
|
|
1047
856
|
}
|
|
1048
857
|
|
|
1049
|
-
function
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
858
|
+
function replaceOutcomeIndexFile(temporary, target) {
|
|
859
|
+
try {
|
|
860
|
+
fs.renameSync(temporary, target);
|
|
861
|
+
} catch (error) {
|
|
862
|
+
if (!['EEXIST', 'EPERM'].includes(error && error.code)) throw error;
|
|
863
|
+
removeIndexFiles(target);
|
|
864
|
+
fs.renameSync(temporary, target);
|
|
865
|
+
}
|
|
866
|
+
fs.chmodSync(target, 0o600);
|
|
867
|
+
for (const suffix of ['-journal', '-wal', '-shm']) {
|
|
868
|
+
fs.rmSync(`${target}${suffix}`, { force: true });
|
|
869
|
+
}
|
|
870
|
+
fsyncDirectory(path.dirname(target));
|
|
1056
871
|
}
|
|
1057
872
|
|
|
1058
|
-
function
|
|
1059
|
-
|
|
1060
|
-
|
|
873
|
+
function removeLegacyLaneIndex() {
|
|
874
|
+
fs.rmSync(legacyLaneIndexFile(), { force: true });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function rebuildCommittedOutcomeIndex({ repairTail = false } = {}) {
|
|
878
|
+
const wal = logFile();
|
|
879
|
+
const target = laneIndexFile();
|
|
880
|
+
if (!target) return null;
|
|
881
|
+
if (!canPersistDerivedOutcomeIndex()) return null;
|
|
882
|
+
ensureDirectoryDurable(path.dirname(target));
|
|
883
|
+
const temporary = temporaryIndexPath(target);
|
|
884
|
+
removeIndexFiles(temporary);
|
|
885
|
+
let index;
|
|
1061
886
|
try {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
887
|
+
index = openOutcomeIndex(temporary);
|
|
888
|
+
let slice;
|
|
889
|
+
index.transaction(() => {
|
|
890
|
+
const indexedEvents = [];
|
|
891
|
+
slice = consumeLogSlice(
|
|
892
|
+
wal,
|
|
893
|
+
0,
|
|
894
|
+
(event, startByte, endByte) =>
|
|
895
|
+
indexedEvents.push({ event, startByte, endByte }),
|
|
896
|
+
{ repairTail }
|
|
897
|
+
);
|
|
898
|
+
index.replaceFromFoldState(
|
|
899
|
+
outcomeFoldEngine.foldState(indexedEvents.map((item) => item.event)),
|
|
900
|
+
indexedEvents
|
|
901
|
+
);
|
|
902
|
+
index.setSource(
|
|
903
|
+
laneIndexSourceIdentity(wal, slice.endByte, slice.endLine),
|
|
904
|
+
'full'
|
|
905
|
+
);
|
|
906
|
+
index.acceptProjection();
|
|
907
|
+
});
|
|
908
|
+
if (!index.integrityCheck()) fail('rebuilt SQLite outcome index failed integrity check');
|
|
909
|
+
index.close();
|
|
910
|
+
index = null;
|
|
911
|
+
replaceOutcomeIndexFile(temporary, target);
|
|
912
|
+
removeLegacyLaneIndex();
|
|
913
|
+
const reopened = openOutcomeIndex(target);
|
|
914
|
+
reopened.build = 'full';
|
|
915
|
+
return reopened;
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (index) index.close();
|
|
918
|
+
removeIndexFiles(temporary);
|
|
919
|
+
throw error;
|
|
1065
920
|
}
|
|
1066
921
|
}
|
|
1067
922
|
|
|
1068
|
-
function syncCommittedLaneIndex({ repairTail = false, readOnly = false } = {}) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
if (
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
923
|
+
function syncCommittedLaneIndex({ repairTail = false, readOnly = false, forceFull = false } = {}) {
|
|
924
|
+
if (process.env._DRIFTSEAL_TEST_DISABLE_OUTCOME_INDEX === '1') return null;
|
|
925
|
+
const wal = logFile();
|
|
926
|
+
const target = laneIndexFile();
|
|
927
|
+
if (!target) return null;
|
|
928
|
+
if (!readOnly && !canPersistDerivedOutcomeIndex()) return null;
|
|
929
|
+
if (readOnly) {
|
|
930
|
+
if (!fs.existsSync(target)) return null;
|
|
931
|
+
try {
|
|
932
|
+
const index = openOutcomeIndex(target, { readOnly: true });
|
|
933
|
+
if (
|
|
934
|
+
!index.projectionTrusted() ||
|
|
935
|
+
!laneIndexMatchesFile(index.source(), wal, { exact: true })
|
|
936
|
+
) {
|
|
937
|
+
index.close();
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
index.build = 'hot';
|
|
941
|
+
return index;
|
|
942
|
+
} catch {
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (forceFull || !fs.existsSync(target)) {
|
|
947
|
+
try {
|
|
948
|
+
return rebuildCommittedOutcomeIndex({ repairTail });
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (error instanceof SqliteUnavailableError) return null;
|
|
951
|
+
throw error;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
let index;
|
|
955
|
+
try {
|
|
956
|
+
index = openOutcomeIndex(target);
|
|
957
|
+
const source = index.source();
|
|
958
|
+
if (!index.projectionTrusted() || !laneIndexMatchesFile(source, wal)) {
|
|
959
|
+
index.close();
|
|
960
|
+
return rebuildCommittedOutcomeIndex({ repairTail });
|
|
961
|
+
}
|
|
962
|
+
const size = fs.existsSync(wal) ? fs.statSync(wal).size : 0;
|
|
963
|
+
if (size === source.indexedThrough) {
|
|
964
|
+
index.build = 'hot';
|
|
965
|
+
removeLegacyLaneIndex();
|
|
966
|
+
return index;
|
|
967
|
+
}
|
|
968
|
+
let slice;
|
|
969
|
+
index.transaction(() => {
|
|
970
|
+
slice = consumeLogSlice(
|
|
971
|
+
wal,
|
|
972
|
+
source.indexedThrough,
|
|
973
|
+
(event, start, end) =>
|
|
974
|
+
index.applyEvent(event, start, end, { applyFoldEvent, defaultLane: DEFAULT_LANE }),
|
|
975
|
+
{
|
|
976
|
+
repairTail,
|
|
977
|
+
startLine: source.indexedLines || 0,
|
|
978
|
+
}
|
|
1091
979
|
);
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
980
|
+
index.setSource(
|
|
981
|
+
laneIndexSourceIdentity(wal, slice.endByte, slice.endLine),
|
|
982
|
+
'incremental'
|
|
983
|
+
);
|
|
984
|
+
index.acceptProjection();
|
|
985
|
+
});
|
|
986
|
+
index.build = 'incremental';
|
|
987
|
+
removeLegacyLaneIndex();
|
|
988
|
+
return index;
|
|
989
|
+
} catch (error) {
|
|
990
|
+
if (index) index.close();
|
|
991
|
+
if (error instanceof DriftSealError) throw error;
|
|
992
|
+
if (error instanceof SqliteUnavailableError) return null;
|
|
993
|
+
return rebuildCommittedOutcomeIndex({ repairTail });
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function attachIndexedOverlay(index, committed, plan, { readOnly = false } = {}) {
|
|
998
|
+
const lanes = index.laneCatalog();
|
|
999
|
+
if (!plan || plan.alreadyCommitted || plan.records.length === 0) {
|
|
1000
|
+
committed.lanes = lanes;
|
|
1001
|
+
return committed;
|
|
1002
|
+
}
|
|
1003
|
+
if (!readOnly && plan.mappings.length > 0) writeJsonl(plan.park, plan.records);
|
|
1004
|
+
const overlay = fold(plan.records.map((record) => record.event));
|
|
1005
|
+
for (const record of overlay) {
|
|
1006
|
+
let lane = lanes.get(record.lane);
|
|
1007
|
+
if (!lane) {
|
|
1008
|
+
const foldedLane = overlay.lanes.get(record.lane);
|
|
1009
|
+
lane = {
|
|
1010
|
+
name: record.lane,
|
|
1011
|
+
description: foldedLane ? foldedLane.description : null,
|
|
1012
|
+
addedAt: foldedLane ? foldedLane.addedAt : null,
|
|
1013
|
+
inferred: foldedLane ? foldedLane.inferred === true : true,
|
|
1014
|
+
head: null,
|
|
1015
|
+
count: 0,
|
|
1016
|
+
visible: 0,
|
|
1017
|
+
};
|
|
1018
|
+
lanes.set(record.lane, lane);
|
|
1099
1019
|
}
|
|
1020
|
+
lane.count = (lane.count || 0) + 1;
|
|
1021
|
+
if (!record.reclaimed) lane.visible = (lane.visible || 0) + 1;
|
|
1100
1022
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
return state;
|
|
1023
|
+
const records = [...committed, ...overlay];
|
|
1024
|
+
records.lanes = lanes;
|
|
1025
|
+
return records;
|
|
1105
1026
|
}
|
|
1106
1027
|
|
|
1107
|
-
function
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1028
|
+
function queryOutcomeView(index, park, { repairTail = false, readOnly = false } = {}) {
|
|
1029
|
+
const committed = index.queryAll();
|
|
1030
|
+
if (!park || !fs.existsSync(park)) {
|
|
1031
|
+
committed.lanes = index.laneCatalog();
|
|
1032
|
+
return { index: null, records: committed };
|
|
1033
|
+
}
|
|
1034
|
+
const plan = planIndexedInProgressOverlay(index, park, { repairTail, readOnly });
|
|
1035
|
+
if (plan && plan.alreadyCommitted && !readOnly) discardInProgressLog(park);
|
|
1036
|
+
return {
|
|
1037
|
+
index: null,
|
|
1038
|
+
records: attachIndexedOverlay(index, committed, plan, { readOnly }),
|
|
1039
|
+
};
|
|
1113
1040
|
}
|
|
1114
1041
|
|
|
1115
|
-
function
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1042
|
+
function recoverableOutcomeIndexError(error) {
|
|
1043
|
+
return (
|
|
1044
|
+
error instanceof OutcomeIndexError ||
|
|
1045
|
+
/^SQLITE_|^ERR_SQLITE_/.test(String(error && error.code))
|
|
1046
|
+
);
|
|
1119
1047
|
}
|
|
1120
1048
|
|
|
1121
1049
|
function loadOutcomeView({ repairTail = false, readOnly = false } = {}) {
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
if (!
|
|
1125
|
-
|
|
1050
|
+
const park = existingInProgressFile();
|
|
1051
|
+
let index = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
1052
|
+
if (!index) {
|
|
1053
|
+
const records = fold(readEvents({ repairTail, readOnly }));
|
|
1054
|
+
return { index: null, records };
|
|
1126
1055
|
}
|
|
1127
1056
|
try {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
);
|
|
1131
|
-
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
);
|
|
1141
|
-
return { state, records: foldedRecordsFromLaneIndex(state) };
|
|
1142
|
-
} catch (err) {
|
|
1143
|
-
if (readOnly && err && err.code === 'ENOENT') {
|
|
1144
|
-
return { state: committed, records: foldedRecordsFromLaneIndex(committed) };
|
|
1145
|
-
}
|
|
1146
|
-
throw err;
|
|
1057
|
+
return queryOutcomeView(index, park, { repairTail, readOnly });
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
index.close();
|
|
1060
|
+
index = null;
|
|
1061
|
+
if (readOnly && (error.code === 'ENOENT' || recoverableOutcomeIndexError(error))) {
|
|
1062
|
+
return { index: null, records: fold(readEvents({ repairTail, readOnly })) };
|
|
1063
|
+
}
|
|
1064
|
+
if (!recoverableOutcomeIndexError(error)) throw error;
|
|
1065
|
+
index = syncCommittedLaneIndex({ repairTail, forceFull: true });
|
|
1066
|
+
if (!index) return { index: null, records: fold(readEvents({ repairTail })) };
|
|
1067
|
+
return queryOutcomeView(index, park, { repairTail });
|
|
1068
|
+
} finally {
|
|
1069
|
+
if (index) index.close();
|
|
1147
1070
|
}
|
|
1148
1071
|
}
|
|
1149
1072
|
|
|
@@ -1213,6 +1136,53 @@ function selectLastLogRecords(records, current, n) {
|
|
|
1213
1136
|
return [...clipped, ...extras].sort((left, right) => order.get(left.id) - order.get(right.id));
|
|
1214
1137
|
}
|
|
1215
1138
|
|
|
1139
|
+
function indexedLaneSummary(state, name) {
|
|
1140
|
+
const lane = state.lanes.get(name);
|
|
1141
|
+
return {
|
|
1142
|
+
name,
|
|
1143
|
+
description: lane ? lane.description : null,
|
|
1144
|
+
addedAt: lane ? lane.addedAt : null,
|
|
1145
|
+
inferred: Boolean(lane && lane.inferred),
|
|
1146
|
+
visible: lane ? lane.visible : 0,
|
|
1147
|
+
count: lane ? lane.count : 0,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function tryLoadRecentOutcomeView(n, { includeReclaimed = false, repairTail = false, readOnly = false } = {}) {
|
|
1152
|
+
const park = existingInProgressFile();
|
|
1153
|
+
let index = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
1154
|
+
if (!index) return null;
|
|
1155
|
+
const query = () => {
|
|
1156
|
+
const plan =
|
|
1157
|
+
park && fs.existsSync(park)
|
|
1158
|
+
? planIndexedInProgressOverlay(index, park, { repairTail, readOnly })
|
|
1159
|
+
: null;
|
|
1160
|
+
if (plan && plan.alreadyCommitted && !readOnly) discardInProgressLog(park);
|
|
1161
|
+
const overlayView = attachIndexedOverlay(index, [], plan, { readOnly });
|
|
1162
|
+
const state = { lanes: overlayView.lanes };
|
|
1163
|
+
const { current, missing } = resolveCurrentLane(state);
|
|
1164
|
+
const committed = index.queryRecent(current, n, { includeReclaimed });
|
|
1165
|
+
const records = selectLastLogRecords(
|
|
1166
|
+
[...committed, ...overlayView],
|
|
1167
|
+
current,
|
|
1168
|
+
n
|
|
1169
|
+
);
|
|
1170
|
+
return { state, records, current, missing };
|
|
1171
|
+
};
|
|
1172
|
+
try {
|
|
1173
|
+
return query();
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
if (readOnly && error && error.code === 'ENOENT') return null;
|
|
1176
|
+
index.close();
|
|
1177
|
+
index = null;
|
|
1178
|
+
if (readOnly) return null;
|
|
1179
|
+
index = syncCommittedLaneIndex({ repairTail, forceFull: true });
|
|
1180
|
+
return query();
|
|
1181
|
+
} finally {
|
|
1182
|
+
if (index) index.close();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1216
1186
|
function liveWorktreeOutcomeLog() {
|
|
1217
1187
|
const root = gitWorktreeRoot();
|
|
1218
1188
|
if (!root) return null;
|
|
@@ -1234,7 +1204,7 @@ function readJsonlRecordsFromFile(file, { repairTail = false, readOnly = false }
|
|
|
1234
1204
|
if (
|
|
1235
1205
|
readOnly &&
|
|
1236
1206
|
process.env._DRIFTSEAL_TEST_UNLINK_PARK_BEFORE_READ === '1' &&
|
|
1237
|
-
path.basename(file) ===
|
|
1207
|
+
path.basename(file) === IN_PROGRESS_SIDECAR
|
|
1238
1208
|
) {
|
|
1239
1209
|
// Simulate a writer flushing and unlinking the park between the existence
|
|
1240
1210
|
// check and the read; the next attempt sees the park as absent.
|
|
@@ -1294,14 +1264,35 @@ function planInProgressOverlay(committedEvents, park, { repairTail = false, read
|
|
|
1294
1264
|
return { park, records: remapped.records, mappings: remapped.mappings, alreadyCommitted: false };
|
|
1295
1265
|
}
|
|
1296
1266
|
|
|
1267
|
+
function planIndexedInProgressOverlay(index, park, { repairTail = false, readOnly = false } = {}) {
|
|
1268
|
+
if (!park || !fs.existsSync(park)) return null;
|
|
1269
|
+
const overlayRecords = readJsonlRecordsFromFile(park, { repairTail, readOnly });
|
|
1270
|
+
const overlayEvents = overlayRecords.map((record) => record.event);
|
|
1271
|
+
if (overlayEvents.length === 0 || index.containsEventSequence(overlayEvents)) {
|
|
1272
|
+
return { park, records: [], mappings: [], alreadyCommitted: true };
|
|
1273
|
+
}
|
|
1274
|
+
const remapped = remapTheirsRecords(
|
|
1275
|
+
overlayRecords,
|
|
1276
|
+
index.outcomeStartEvents(),
|
|
1277
|
+
new Map(),
|
|
1278
|
+
new Map()
|
|
1279
|
+
);
|
|
1280
|
+
return {
|
|
1281
|
+
park,
|
|
1282
|
+
records: remapped.records,
|
|
1283
|
+
mappings: remapped.mappings,
|
|
1284
|
+
alreadyCommitted: false,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1297
1288
|
function discardInProgressLog(park) {
|
|
1298
|
-
fs.
|
|
1289
|
+
fs.rmSync(park, { force: true });
|
|
1299
1290
|
fsyncDirectory(path.dirname(park));
|
|
1300
1291
|
}
|
|
1301
1292
|
|
|
1302
1293
|
function reconcileInProgressRecords(
|
|
1303
1294
|
committedEvents,
|
|
1304
|
-
{ repairTail = false, readOnly = false, park =
|
|
1295
|
+
{ repairTail = false, readOnly = false, park = existingInProgressFile() } = {}
|
|
1305
1296
|
) {
|
|
1306
1297
|
const plan = planInProgressOverlay(committedEvents, park, { repairTail, readOnly });
|
|
1307
1298
|
if (!plan) return [];
|
|
@@ -1323,7 +1314,7 @@ function readEventsSnapshot(file, { repairTail = false, readOnly = false } = {})
|
|
|
1323
1314
|
reconcileInProgressRecords(events, {
|
|
1324
1315
|
repairTail,
|
|
1325
1316
|
readOnly,
|
|
1326
|
-
park:
|
|
1317
|
+
park: existingInProgressFileForLog(file),
|
|
1327
1318
|
}).map((record) => record.event)
|
|
1328
1319
|
);
|
|
1329
1320
|
}
|
|
@@ -1452,7 +1443,7 @@ function appendEventTo(file, event) {
|
|
|
1452
1443
|
* Returns the outcome ids it had to remap.
|
|
1453
1444
|
*/
|
|
1454
1445
|
function flushInProgressLog() {
|
|
1455
|
-
const park =
|
|
1446
|
+
const park = adoptInProgressFile();
|
|
1456
1447
|
if (!park || !fs.existsSync(park)) return new Map();
|
|
1457
1448
|
const committedRecords = readJsonlRecordsFromFile(logFile());
|
|
1458
1449
|
const plan = planInProgressOverlay(
|
|
@@ -1482,7 +1473,8 @@ function parkedOpenOutcome(park) {
|
|
|
1482
1473
|
}
|
|
1483
1474
|
|
|
1484
1475
|
function appendEvent(event) {
|
|
1485
|
-
|
|
1476
|
+
ensureDerivedLaneSidecarIgnore();
|
|
1477
|
+
const park = adoptInProgressFile();
|
|
1486
1478
|
if (!park) {
|
|
1487
1479
|
const stored = appendEventTo(logFile(), event);
|
|
1488
1480
|
if (event.type === 'begin') writeLocalOutcomeProvenance(stored);
|
|
@@ -1497,7 +1489,7 @@ function appendEvent(event) {
|
|
|
1497
1489
|
if (event.type === 'begin') return appendEventTo(park, event);
|
|
1498
1490
|
if (!open || open.id !== event.id) return appendEventTo(logFile(), event);
|
|
1499
1491
|
if (event.type !== 'end') return appendEventTo(park, event);
|
|
1500
|
-
// Close in the tracked log, never in
|
|
1492
|
+
// Close in the tracked log, never only in the park: the parked records move first, so the
|
|
1501
1493
|
// closing record cannot end up somewhere a clone or a removed worktree would drop it.
|
|
1502
1494
|
const remapped = flushInProgressLog();
|
|
1503
1495
|
if (process.env._DRIFTSEAL_TEST_CRASH_AFTER_IN_PROGRESS_FLUSH === '1') {
|
|
@@ -1511,11 +1503,7 @@ function contentHash(content) {
|
|
|
1511
1503
|
}
|
|
1512
1504
|
|
|
1513
1505
|
function localOutcomeProvenanceFile() {
|
|
1514
|
-
|
|
1515
|
-
const key = contentHash(path.resolve(logFile())).slice(0, 16);
|
|
1516
|
-
if (!root) return path.join(logDir(), LOCAL_OUTCOME_PROVENANCE_FILE);
|
|
1517
|
-
const gitPath = gitCapture(['rev-parse', '--git-path', `driftseal-local-outcome-${key}.json`]);
|
|
1518
|
-
return gitPath ? path.resolve(process.cwd(), gitPath) : null;
|
|
1506
|
+
return path.join(logDir(), LOCAL_OUTCOME_PROVENANCE_FILE);
|
|
1519
1507
|
}
|
|
1520
1508
|
|
|
1521
1509
|
function localOutcomeLogIdentity() {
|
|
@@ -1534,6 +1522,7 @@ function localOutcomeProvenanceFingerprint({ id, ts, verify }) {
|
|
|
1534
1522
|
function writeLocalOutcomeProvenance(event) {
|
|
1535
1523
|
const file = localOutcomeProvenanceFile();
|
|
1536
1524
|
if (!file) return;
|
|
1525
|
+
ensureDerivedLaneSidecarIgnore();
|
|
1537
1526
|
ensureDirectoryDurable(path.dirname(file));
|
|
1538
1527
|
atomicWriteFile(
|
|
1539
1528
|
file,
|
|
@@ -1547,8 +1536,7 @@ function writeLocalOutcomeProvenance(event) {
|
|
|
1547
1536
|
);
|
|
1548
1537
|
}
|
|
1549
1538
|
|
|
1550
|
-
function
|
|
1551
|
-
const file = localOutcomeProvenanceFile();
|
|
1539
|
+
function parseLocalOutcomeProvenance(file) {
|
|
1552
1540
|
if (!file || !fs.existsSync(file)) return null;
|
|
1553
1541
|
try {
|
|
1554
1542
|
const provenance = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -1566,6 +1554,10 @@ function readLocalOutcomeProvenance() {
|
|
|
1566
1554
|
}
|
|
1567
1555
|
}
|
|
1568
1556
|
|
|
1557
|
+
function readLocalOutcomeProvenance() {
|
|
1558
|
+
return parseLocalOutcomeProvenance(localOutcomeProvenanceFile());
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1569
1561
|
function hasMatchingLocalOutcomeProvenance(outcome) {
|
|
1570
1562
|
const provenance = readLocalOutcomeProvenance();
|
|
1571
1563
|
return (
|
|
@@ -1582,11 +1574,13 @@ function hasMatchingLocalOutcomeProvenance(outcome) {
|
|
|
1582
1574
|
}
|
|
1583
1575
|
|
|
1584
1576
|
function clearLocalOutcomeProvenance(id) {
|
|
1585
|
-
const file = localOutcomeProvenanceFile();
|
|
1586
1577
|
const provenance = readLocalOutcomeProvenance();
|
|
1587
|
-
if (!
|
|
1588
|
-
|
|
1589
|
-
|
|
1578
|
+
if (!provenance || provenance.id !== id) return;
|
|
1579
|
+
const file = localOutcomeProvenanceFile();
|
|
1580
|
+
if (file && fs.existsSync(file)) {
|
|
1581
|
+
fs.unlinkSync(file);
|
|
1582
|
+
fsyncDirectory(path.dirname(file));
|
|
1583
|
+
}
|
|
1590
1584
|
}
|
|
1591
1585
|
|
|
1592
1586
|
function atomicWriteFile(target, content, createMode = 0o644) {
|
|
@@ -1867,77 +1861,20 @@ function withMutationLocks(resources, action, { tryWaitMs } = {}) {
|
|
|
1867
1861
|
}
|
|
1868
1862
|
|
|
1869
1863
|
function outcomeContractHash(record) {
|
|
1870
|
-
return
|
|
1871
|
-
outcome: record.outcome,
|
|
1872
|
-
extensions: record.extensions.map(({ extension, acceptance, verify, decisions }) => ({
|
|
1873
|
-
extension,
|
|
1874
|
-
acceptance,
|
|
1875
|
-
verify,
|
|
1876
|
-
decisions,
|
|
1877
|
-
})),
|
|
1878
|
-
acceptance: record.acceptance,
|
|
1879
|
-
verify: record.verify,
|
|
1880
|
-
decisions: record.decisions,
|
|
1881
|
-
}));
|
|
1864
|
+
return outcomeFoldEngine.outcomeContractHash(record);
|
|
1882
1865
|
}
|
|
1883
1866
|
|
|
1884
1867
|
function newOutcomeRecord(ev) {
|
|
1885
|
-
|
|
1886
|
-
id: ev.id,
|
|
1887
|
-
tsBegin: ev.ts,
|
|
1888
|
-
outcome: ev.outcome,
|
|
1889
|
-
extensions: [],
|
|
1890
|
-
acceptance: Array.isArray(ev.acceptance) ? ev.acceptance : [],
|
|
1891
|
-
verify: ev.verify || null,
|
|
1892
|
-
beginHead: ev.head || null,
|
|
1893
|
-
decisions: Array.isArray(ev.decisions) ? ev.decisions : [],
|
|
1894
|
-
logVersion: ev.logVersion || 1,
|
|
1895
|
-
schemaVersion: ev.schemaVersion || 1,
|
|
1896
|
-
lane: ev.lane || DEFAULT_LANE,
|
|
1897
|
-
decisionPrepares: [],
|
|
1898
|
-
decisionTerminals: [],
|
|
1899
|
-
decisionUpdates: [],
|
|
1900
|
-
verificationAttempts: [],
|
|
1901
|
-
verification: null,
|
|
1902
|
-
status: 'in_progress',
|
|
1903
|
-
tsEnd: null,
|
|
1904
|
-
note: null,
|
|
1905
|
-
verifyResult: null,
|
|
1906
|
-
endHead: null,
|
|
1907
|
-
reclaimed: false,
|
|
1908
|
-
reclaimReason: null,
|
|
1909
|
-
reclaimedAt: null,
|
|
1910
|
-
imported: null,
|
|
1911
|
-
contractHash: null,
|
|
1912
|
-
};
|
|
1913
|
-
record.contractHash = outcomeContractHash(record);
|
|
1914
|
-
return record;
|
|
1868
|
+
return outcomeFoldEngine.newOutcomeRecord(ev);
|
|
1915
1869
|
}
|
|
1916
1870
|
|
|
1917
1871
|
/** Fold the event stream into one record per outcome. Legacy v1 events are accepted for migration. */
|
|
1918
1872
|
function fold(events) {
|
|
1919
|
-
|
|
1920
|
-
records: new Map(),
|
|
1921
|
-
reconciliations: new Map(),
|
|
1922
|
-
order: [],
|
|
1923
|
-
lanes: emptyLaneCatalog(),
|
|
1924
|
-
};
|
|
1925
|
-
for (const ev of events) applyFoldEvent(state, ev);
|
|
1926
|
-
const folded = state.order.map((id) => state.records.get(id));
|
|
1927
|
-
folded.lanes = state.lanes;
|
|
1928
|
-
return folded;
|
|
1873
|
+
return outcomeFoldEngine.fold(events);
|
|
1929
1874
|
}
|
|
1930
1875
|
|
|
1931
1876
|
function qualifyingDecisionUpdates(record, decisionId) {
|
|
1932
|
-
return
|
|
1933
|
-
if (update.decisionId !== decisionId) return false;
|
|
1934
|
-
if (record.logVersion === 1 && record.schemaVersion < 2) return true;
|
|
1935
|
-
return (
|
|
1936
|
-
update.type === 'decision_reconcile_commit' &&
|
|
1937
|
-
(update.logVersion === LOG_VERSION || (update.schemaVersion || 1) >= 2) &&
|
|
1938
|
-
typeof update.fileHash === 'string'
|
|
1939
|
-
);
|
|
1940
|
-
});
|
|
1877
|
+
return outcomeFoldEngine.qualifyingDecisionUpdates(record, decisionId);
|
|
1941
1878
|
}
|
|
1942
1879
|
|
|
1943
1880
|
function openOutcome(records) {
|
|
@@ -3482,6 +3419,8 @@ const SKILL_RELEASE_DIGESTS = new Set([
|
|
|
3482
3419
|
'42a0549dff21483c0508ea4a79658e7bf05cd98f8af4238c95dbd23cdcde7ee6', // 2.0.0 outcome workflow
|
|
3483
3420
|
'38e89060ff37ecdd663eae73a3e0c646d6bb220fc8232a5762356375d84ba69b', // 2.1.0 outcome lanes
|
|
3484
3421
|
'b4b2cc27ea71c5777b5eb9b67d861fbe1da43f31ab5d422d6a877e0cad592493', // 2.1.0 lane re-anchor recovery
|
|
3422
|
+
'6c49523c2f4a36ea1cd9d0b4e793ea3cdbd9d299174cf1f9a0a54f3d9a7cd331', // workspace park sidecar wording
|
|
3423
|
+
'678cbaa8380f75f7a45d634010b500e51a59b06ea759eaf23cff2980142662f0', // default-seal parking help wording
|
|
3485
3424
|
]);
|
|
3486
3425
|
|
|
3487
3426
|
function skillInstallUsage() {
|
|
@@ -3903,11 +3842,9 @@ function hookLogFile() {
|
|
|
3903
3842
|
const root = gitWorktreeRoot(current);
|
|
3904
3843
|
while (true) {
|
|
3905
3844
|
const candidate = path.join(current, '.seal', 'outcomes', 'events.jsonl');
|
|
3906
|
-
|
|
3907
|
-
if (
|
|
3908
|
-
|
|
3909
|
-
if (park && fs.existsSync(park)) return candidate;
|
|
3910
|
-
}
|
|
3845
|
+
const workspacePark = path.join(current, '.seal', 'outcomes', IN_PROGRESS_SIDECAR);
|
|
3846
|
+
if (fs.existsSync(candidate) || fs.existsSync(workspacePark)) return candidate;
|
|
3847
|
+
if (root && path.resolve(root) === current) return null;
|
|
3911
3848
|
const parent = path.dirname(current);
|
|
3912
3849
|
if (parent === current) return null;
|
|
3913
3850
|
current = parent;
|
|
@@ -4740,8 +4677,12 @@ function finishAbsorb({
|
|
|
4740
4677
|
allowConflict = false,
|
|
4741
4678
|
followupMessage = null,
|
|
4742
4679
|
}) {
|
|
4743
|
-
//
|
|
4744
|
-
const park = shouldAttachInProgress(outputFile)
|
|
4680
|
+
// A parked outcome is local even though the tracked log never saw it.
|
|
4681
|
+
const park = shouldAttachInProgress(outputFile)
|
|
4682
|
+
? dryRun
|
|
4683
|
+
? existingInProgressFileForLog(outputFile)
|
|
4684
|
+
: adoptInProgressFile() || existingInProgressFileForLog(outputFile)
|
|
4685
|
+
: null;
|
|
4745
4686
|
const plan = planInProgressOverlay(result.map((record) => record.event), park, {
|
|
4746
4687
|
repairTail: true,
|
|
4747
4688
|
});
|
|
@@ -5112,7 +5053,7 @@ function runMachineVerification({ allowTrackedCommand = false } = {}) {
|
|
|
5112
5053
|
fail(`outcome ${outcome.id} has no acceptance criteria; declare them with driftseal begin --accept`);
|
|
5113
5054
|
}
|
|
5114
5055
|
if (!outcome.verify) fail(`outcome ${outcome.id} has no verification command`);
|
|
5115
|
-
const park =
|
|
5056
|
+
const park = existingInProgressFile();
|
|
5116
5057
|
const parked = park ? parkedOpenOutcome(park) : null;
|
|
5117
5058
|
const locallyProvenanced = hasMatchingLocalOutcomeProvenance(outcome);
|
|
5118
5059
|
return {
|
|
@@ -6294,16 +6235,47 @@ const commands = {
|
|
|
6294
6235
|
'all-lanes': 'boolean',
|
|
6295
6236
|
}, 'log');
|
|
6296
6237
|
if (positionals.length > 0) fail(usageFor('log'));
|
|
6238
|
+
const n = flags.last === undefined ? null : positiveInteger(flags.last, '--last');
|
|
6239
|
+
const allLanes = flags['all-lanes'] === true;
|
|
6240
|
+
const parkedV1 = legacyParkedIntent();
|
|
6241
|
+
if (
|
|
6242
|
+
n !== null &&
|
|
6243
|
+
!allLanes &&
|
|
6244
|
+
!parkedV1 &&
|
|
6245
|
+
process.env._DRIFTSEAL_TEST_DISABLE_RECENT_INDEX !== '1'
|
|
6246
|
+
) {
|
|
6247
|
+
const recent = tryLoadRecentOutcomeView(n, {
|
|
6248
|
+
includeReclaimed: flags.all === true,
|
|
6249
|
+
repairTail: true,
|
|
6250
|
+
readOnly,
|
|
6251
|
+
});
|
|
6252
|
+
if (recent) {
|
|
6253
|
+
const { state, records, current, missing } = recent;
|
|
6254
|
+
warnMissingCurrentLane(missing);
|
|
6255
|
+
const customLanes = state.lanes.size > 1;
|
|
6256
|
+
if (customLanes || current !== DEFAULT_LANE || missing) {
|
|
6257
|
+
const summary = indexedLaneSummary(state, current);
|
|
6258
|
+
printLine(`lane: ${current} (${summary.visible} visible / ${summary.count} in lane)`);
|
|
6259
|
+
}
|
|
6260
|
+
if (records.length === 0) {
|
|
6261
|
+
printLine('log is empty');
|
|
6262
|
+
return [];
|
|
6263
|
+
}
|
|
6264
|
+
printLine(records.map((record) => render(record, { currentLane: current })).join('\n\n'));
|
|
6265
|
+
return records.map(publicOutcome);
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
if (process.env._DRIFTSEAL_TEST_REQUIRE_RECENT_INDEX === '1' && n !== null && !allLanes) {
|
|
6269
|
+
fail('recent lane index fast path was not used');
|
|
6270
|
+
}
|
|
6297
6271
|
const view = loadOutcomeView({ repairTail: true, readOnly });
|
|
6298
6272
|
let records = view.records;
|
|
6299
|
-
const parkedV1 = legacyParkedIntent();
|
|
6300
6273
|
if (parkedV1 && !records.some((record) => record.id === parkedV1.id && record.status === 'in_progress')) {
|
|
6301
6274
|
records = Object.assign([...records, parkedV1], { lanes: records.lanes });
|
|
6302
6275
|
}
|
|
6303
6276
|
const catalog = records.lanes || emptyLaneCatalog();
|
|
6304
6277
|
const { current, missing } = resolveCurrentLane(view.records);
|
|
6305
6278
|
warnMissingCurrentLane(missing);
|
|
6306
|
-
const allLanes = flags['all-lanes'] === true;
|
|
6307
6279
|
if (!allLanes) {
|
|
6308
6280
|
records = Object.assign(selectLaneLogRecords(records, current), { lanes: catalog });
|
|
6309
6281
|
}
|
|
@@ -6313,8 +6285,7 @@ const commands = {
|
|
|
6313
6285
|
printLine(renderLaneLine(view.records, current));
|
|
6314
6286
|
}
|
|
6315
6287
|
let shown = visible;
|
|
6316
|
-
if (
|
|
6317
|
-
const n = positiveInteger(flags.last, '--last');
|
|
6288
|
+
if (n !== null) {
|
|
6318
6289
|
shown = selectLastLogRecords(visible, current, n);
|
|
6319
6290
|
}
|
|
6320
6291
|
if (shown.length === 0) {
|
|
@@ -6781,10 +6752,11 @@ const commands = {
|
|
|
6781
6752
|
} catch (err) {
|
|
6782
6753
|
printLine(`warning: could not configure git merge driver: ${err && err.message ? err.message : err}`);
|
|
6783
6754
|
}
|
|
6755
|
+
const indexIgnore = ensureDerivedLaneSidecarIgnore();
|
|
6784
6756
|
|
|
6785
6757
|
if (localLog) warnIfDefaultLogsTracked();
|
|
6786
6758
|
|
|
6787
|
-
if (updated === current && !attributes.changed && !driver.changed) {
|
|
6759
|
+
if (updated === current && !attributes.changed && !driver.changed && !indexIgnore.changed) {
|
|
6788
6760
|
printLine('AGENTS.md already contains the DriftSeal protocols; nothing to do');
|
|
6789
6761
|
return { changed: false, target };
|
|
6790
6762
|
}
|
|
@@ -6798,6 +6770,9 @@ const commands = {
|
|
|
6798
6770
|
if (driver.changed) {
|
|
6799
6771
|
printLine('Configured local git merge driver for DriftSeal outcome logs');
|
|
6800
6772
|
}
|
|
6773
|
+
if (indexIgnore.changed) {
|
|
6774
|
+
printLine(`Configured derived outcome-index ignore: ${indexIgnore.target}`);
|
|
6775
|
+
}
|
|
6801
6776
|
return { changed: true, target };
|
|
6802
6777
|
},
|
|
6803
6778
|
|
|
@@ -6875,7 +6850,7 @@ seal root: $DRIFTSEAL_HOME, or .seal in the current directory
|
|
|
6875
6850
|
outcome log: <seal-root>/outcomes/events.jsonl
|
|
6876
6851
|
MADR records: <seal-root>/madr/
|
|
6877
6852
|
$DRIFTSEAL_DECISION_HOME is a v1-only default for migration source detection; v2 runtime ignores it.
|
|
6878
|
-
In a Git
|
|
6853
|
+
In a default Git-repository seal, begin parks an open outcome beside the WAL until end, so merge does not need a log-only commit. Custom $DRIFTSEAL_HOME seals write that open outcome directly to events.jsonl.`);
|
|
6879
6854
|
return null;
|
|
6880
6855
|
},
|
|
6881
6856
|
|
|
@@ -7035,9 +7010,52 @@ function repositoryOutcomeLogFiles() {
|
|
|
7035
7010
|
return files;
|
|
7036
7011
|
}
|
|
7037
7012
|
|
|
7013
|
+
function indexedMigrationEvent(file) {
|
|
7014
|
+
if (
|
|
7015
|
+
canonicalPath(file) !== canonicalPath(logFile()) ||
|
|
7016
|
+
!laneIndexFile() ||
|
|
7017
|
+
!fs.existsSync(laneIndexFile())
|
|
7018
|
+
) {
|
|
7019
|
+
return { usable: false, migration: null };
|
|
7020
|
+
}
|
|
7021
|
+
let index;
|
|
7022
|
+
try {
|
|
7023
|
+
index = openOutcomeIndex(laneIndexFile(), { readOnly: true });
|
|
7024
|
+
const source = index.source();
|
|
7025
|
+
if (!laneIndexMatchesFile(source, file)) {
|
|
7026
|
+
return { usable: false, migration: null };
|
|
7027
|
+
}
|
|
7028
|
+
let migration = index.migrationEvent();
|
|
7029
|
+
const size = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
7030
|
+
if (size > source.indexedThrough) {
|
|
7031
|
+
consumeLogSlice(
|
|
7032
|
+
file,
|
|
7033
|
+
source.indexedThrough,
|
|
7034
|
+
(event) => {
|
|
7035
|
+
if (event.type === 'migration' && event.id === 'v1-to-v2') migration = event;
|
|
7036
|
+
},
|
|
7037
|
+
{
|
|
7038
|
+
readOnly: true,
|
|
7039
|
+
startLine: source.indexedLines || 0,
|
|
7040
|
+
}
|
|
7041
|
+
);
|
|
7042
|
+
}
|
|
7043
|
+
return { usable: true, migration };
|
|
7044
|
+
} catch {
|
|
7045
|
+
return { usable: false, migration: null };
|
|
7046
|
+
} finally {
|
|
7047
|
+
if (index) index.close();
|
|
7048
|
+
}
|
|
7049
|
+
}
|
|
7050
|
+
|
|
7038
7051
|
function repositoryMigrationEvent() {
|
|
7039
7052
|
for (const file of repositoryOutcomeLogFiles()) {
|
|
7040
7053
|
try {
|
|
7054
|
+
const indexed = indexedMigrationEvent(file);
|
|
7055
|
+
if (indexed.usable) {
|
|
7056
|
+
if (indexed.migration) return indexed.migration;
|
|
7057
|
+
continue;
|
|
7058
|
+
}
|
|
7041
7059
|
const migration = findMigrationEvent(file);
|
|
7042
7060
|
if (migration) return migration;
|
|
7043
7061
|
} catch {
|
|
@@ -7244,7 +7262,7 @@ function dispatch(argv) {
|
|
|
7244
7262
|
if (cmd === 'hook') {
|
|
7245
7263
|
// Hooks read the nearest ancestor log, not the cwd-relative one: lock
|
|
7246
7264
|
// the directory of the file the hook will actually read (the park a
|
|
7247
|
-
// writer flushes under that same lock lives
|
|
7265
|
+
// writer flushes under that same lock lives beside the WAL).
|
|
7248
7266
|
// With no ancestor log the hook prints nothing, so skip locking — this
|
|
7249
7267
|
// also avoids creating a spurious <cwd>/.intent-log.
|
|
7250
7268
|
const hookFile = hookLogFile();
|
|
@@ -7252,6 +7270,14 @@ function dispatch(argv) {
|
|
|
7252
7270
|
resources = [path.dirname(hookFile)];
|
|
7253
7271
|
} else if (legacyParkedIntent()) {
|
|
7254
7272
|
resources = [path.dirname(legacyIntentLogFile())];
|
|
7273
|
+
} else if (!fs.existsSync(logDir())) {
|
|
7274
|
+
// Do not mkdir a cwd-relative seal just to lock a read: status/log
|
|
7275
|
+
// from a subdirectory would otherwise plant packages/**/.seal.
|
|
7276
|
+
const data = fn(rest, { readOnly: true });
|
|
7277
|
+
return {
|
|
7278
|
+
data,
|
|
7279
|
+
exitCode: data && Number.isInteger(data.exitCode) ? data.exitCode : 0,
|
|
7280
|
+
};
|
|
7255
7281
|
} else {
|
|
7256
7282
|
resources = [logDir()];
|
|
7257
7283
|
}
|
|
@@ -7305,6 +7331,7 @@ function repositoryRoot(root) {
|
|
|
7305
7331
|
}
|
|
7306
7332
|
|
|
7307
7333
|
function runCommand(argv, { root = process.cwd(), isolateStorage = false, capture = true } = {}) {
|
|
7334
|
+
assertSupportedNode();
|
|
7308
7335
|
if (!Array.isArray(argv) || argv.some((arg) => typeof arg !== 'string')) {
|
|
7309
7336
|
fail('command arguments must be an array of strings');
|
|
7310
7337
|
}
|
|
@@ -7365,6 +7392,7 @@ function appendFlag(argv, flag, value) {
|
|
|
7365
7392
|
}
|
|
7366
7393
|
|
|
7367
7394
|
function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
7395
|
+
assertSupportedNode();
|
|
7368
7396
|
const fixedRoot = repositoryRoot(root);
|
|
7369
7397
|
let lastReadOnly = false;
|
|
7370
7398
|
const call = (argv) => {
|
|
@@ -7501,6 +7529,7 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
|
7501
7529
|
|
|
7502
7530
|
function main() {
|
|
7503
7531
|
try {
|
|
7532
|
+
assertSupportedNode();
|
|
7504
7533
|
const result = dispatch(process.argv.slice(2));
|
|
7505
7534
|
process.exitCode = result.exitCode;
|
|
7506
7535
|
} catch (err) {
|