driftseal 2.1.0 → 3.0.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 +21 -9
- package/README.zh-CN.md +17 -8
- package/benchmark/recent-log.js +181 -0
- package/bin/driftseal.js +430 -474
- 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/test/package-smoke.js +150 -0
package/bin/driftseal.js
CHANGED
|
@@ -32,6 +32,15 @@ const { isDeepStrictEqual } = require('util');
|
|
|
32
32
|
const { StringDecoder } = require('string_decoder');
|
|
33
33
|
const { execFileSync, spawnSync } = require('child_process');
|
|
34
34
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
35
|
+
const { createOutcomeFold } = require('../lib/outcome-fold.js');
|
|
36
|
+
const {
|
|
37
|
+
OutcomeIndexError,
|
|
38
|
+
SqliteUnavailableError,
|
|
39
|
+
openOutcomeIndex,
|
|
40
|
+
removeIndexFiles,
|
|
41
|
+
temporaryIndexPath,
|
|
42
|
+
} = require('../lib/outcome-index-sqlite.js');
|
|
43
|
+
const { assertSupportedNode } = require('../lib/sqlite-runtime.js');
|
|
35
44
|
|
|
36
45
|
const END_STATUSES = ['completed', 'partial', 'failed', 'abandoned'];
|
|
37
46
|
const DECISION_STATUSES = [
|
|
@@ -52,10 +61,8 @@ const DEFAULT_LANE = 'main';
|
|
|
52
61
|
const LANE_NAME_RE = /^[a-z][a-z0-9-]{0,62}$/;
|
|
53
62
|
const IN_PROGRESS_GIT_PATH = 'driftseal-v2-in-progress.jsonl';
|
|
54
63
|
const CURRENT_LANE_GIT_PATH = 'driftseal-v2-current-lane';
|
|
55
|
-
const LANE_INDEX_GIT_PATH = 'driftseal-
|
|
56
|
-
const
|
|
57
|
-
const LANE_INDEX_PREFIX_BYTES = 8192;
|
|
58
|
-
const LANE_INDEX_TAIL_BYTES = 64 * 1024;
|
|
64
|
+
const LANE_INDEX_GIT_PATH = 'driftseal-v3-outcome-index.sqlite';
|
|
65
|
+
const LEGACY_LANE_INDEX_GIT_PATH = 'driftseal-v2-lane-index.json';
|
|
59
66
|
const LOCK_STALE_MS = 30 * 60 * 1000;
|
|
60
67
|
const LOCK_INIT_STALE_MS = 5 * 1000;
|
|
61
68
|
const READ_ONLY_NOTICE = '(read-only: another mutation holds the lock; tail repair skipped)';
|
|
@@ -65,6 +72,12 @@ const VERIFICATION_OUTPUT_CHUNK_BYTES = 64 * 1024;
|
|
|
65
72
|
const CAPTURE_OUTPUT_EDGE_CHARACTERS = 32 * 1024;
|
|
66
73
|
const CAPTURE_OUTPUT_OMISSION = '\n... [driftseal captured output truncated] ...\n';
|
|
67
74
|
const LOCAL_OUTCOME_PROVENANCE_FILE = '.driftseal-local-outcome.json';
|
|
75
|
+
const outcomeFoldEngine = createOutcomeFold({
|
|
76
|
+
fail,
|
|
77
|
+
contentHash,
|
|
78
|
+
logVersion: LOG_VERSION,
|
|
79
|
+
defaultLane: DEFAULT_LANE,
|
|
80
|
+
});
|
|
68
81
|
|
|
69
82
|
class DriftSealError extends Error {
|
|
70
83
|
constructor(message) {
|
|
@@ -594,11 +607,16 @@ function currentLaneFile() {
|
|
|
594
607
|
|
|
595
608
|
function laneIndexFile() {
|
|
596
609
|
if (isParkableOutcomeLog()) return worktreeMetadataFile(LANE_INDEX_GIT_PATH);
|
|
610
|
+
return path.join(logDir(), '.outcome-index.sqlite');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function legacyLaneIndexFile() {
|
|
614
|
+
if (isParkableOutcomeLog()) return worktreeMetadataFile(LEGACY_LANE_INDEX_GIT_PATH);
|
|
597
615
|
return path.join(logDir(), '.lane-index.json');
|
|
598
616
|
}
|
|
599
617
|
|
|
600
618
|
function emptyLaneCatalog() {
|
|
601
|
-
return
|
|
619
|
+
return outcomeFoldEngine.emptyLaneCatalog();
|
|
602
620
|
}
|
|
603
621
|
|
|
604
622
|
function readCurrentLaneName() {
|
|
@@ -624,7 +642,15 @@ function ensureDerivedLaneSidecarIgnore() {
|
|
|
624
642
|
const ignoreFile = path.join(logDir(), '.gitignore');
|
|
625
643
|
let current = fs.existsSync(ignoreFile) ? fs.readFileSync(ignoreFile, 'utf8') : '';
|
|
626
644
|
let next = current;
|
|
627
|
-
for (const name of [
|
|
645
|
+
for (const name of [
|
|
646
|
+
'.current-lane',
|
|
647
|
+
'.lane-index.json',
|
|
648
|
+
'.outcome-index.sqlite',
|
|
649
|
+
'.outcome-index.sqlite-journal',
|
|
650
|
+
'.outcome-index.sqlite-wal',
|
|
651
|
+
'.outcome-index.sqlite-shm',
|
|
652
|
+
'..outcome-index.sqlite.*.tmp',
|
|
653
|
+
]) {
|
|
628
654
|
const present = next.split(/\r?\n/).some((line) => line.trim() === name);
|
|
629
655
|
if (present) continue;
|
|
630
656
|
if (next && !next.endsWith('\n')) next += '\n';
|
|
@@ -635,354 +661,83 @@ function ensureDerivedLaneSidecarIgnore() {
|
|
|
635
661
|
atomicWriteFile(ignoreFile, next, 0o644);
|
|
636
662
|
}
|
|
637
663
|
|
|
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('');
|
|
664
|
+
function hashFilePrefix(file, length) {
|
|
665
|
+
const hash = crypto.createHash('sha256');
|
|
666
|
+
if (length <= 0) return hash.digest('hex');
|
|
662
667
|
const fd = fs.openSync(file, 'r');
|
|
663
668
|
try {
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
669
|
+
const buffer = Buffer.alloc(Math.min(1024 * 1024, length));
|
|
670
|
+
let position = 0;
|
|
671
|
+
while (position < length) {
|
|
672
|
+
const requested = Math.min(buffer.length, length - position);
|
|
673
|
+
const read = fs.readSync(fd, buffer, 0, requested, position);
|
|
674
|
+
if (read === 0) break;
|
|
675
|
+
hash.update(buffer.subarray(0, read));
|
|
676
|
+
position += read;
|
|
677
|
+
}
|
|
678
|
+
if (position !== length) return null;
|
|
679
|
+
return hash.digest('hex');
|
|
667
680
|
} finally {
|
|
668
681
|
fs.closeSync(fd);
|
|
669
682
|
}
|
|
670
683
|
}
|
|
671
684
|
|
|
672
685
|
function laneIndexSourceIdentity(file, indexedThrough, indexedLines = 0) {
|
|
673
|
-
if (!fs.existsSync(file)
|
|
686
|
+
if (!fs.existsSync(file)) {
|
|
674
687
|
return {
|
|
675
688
|
indexedThrough: 0,
|
|
676
689
|
indexedLines: 0,
|
|
677
|
-
|
|
678
|
-
|
|
690
|
+
walHash: contentHash(''),
|
|
691
|
+
device: null,
|
|
692
|
+
inode: null,
|
|
693
|
+
mtimeMs: null,
|
|
694
|
+
ctimeMs: null,
|
|
679
695
|
};
|
|
680
696
|
}
|
|
681
|
-
const
|
|
682
|
-
const tail = Math.min(LANE_INDEX_TAIL_BYTES, indexedThrough);
|
|
697
|
+
const stat = fs.statSync(file);
|
|
683
698
|
return {
|
|
684
699
|
indexedThrough,
|
|
685
700
|
indexedLines,
|
|
686
|
-
|
|
687
|
-
|
|
701
|
+
walHash: hashFilePrefix(file, indexedThrough),
|
|
702
|
+
device: Number(stat.dev),
|
|
703
|
+
inode: Number(stat.ino),
|
|
704
|
+
mtimeMs: stat.mtimeMs,
|
|
705
|
+
ctimeMs: stat.ctimeMs,
|
|
688
706
|
};
|
|
689
707
|
}
|
|
690
708
|
|
|
691
|
-
function laneIndexMatchesFile(
|
|
692
|
-
if (
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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 || {})),
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function cloneLaneIndexState(state) {
|
|
750
|
-
return deserializeLaneIndex(serializeLaneIndex(state));
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function linkLaneIndex(state) {
|
|
754
|
-
const heads = new Map();
|
|
755
|
-
for (const id of state.order) {
|
|
756
|
-
const rec = state.records.get(id);
|
|
757
|
-
rec.previous = heads.get(rec.lane) || null;
|
|
758
|
-
heads.set(rec.lane, id);
|
|
709
|
+
function laneIndexMatchesFile(source, file, { exact = false } = {}) {
|
|
710
|
+
if (
|
|
711
|
+
!source ||
|
|
712
|
+
typeof source.walHash !== 'string' ||
|
|
713
|
+
!Number.isSafeInteger(source.indexedLines) ||
|
|
714
|
+
source.indexedLines < 0
|
|
715
|
+
) {
|
|
716
|
+
return false;
|
|
759
717
|
}
|
|
760
|
-
|
|
761
|
-
|
|
718
|
+
if (!fs.existsSync(file)) return source.indexedThrough === 0;
|
|
719
|
+
const stat = fs.statSync(file);
|
|
720
|
+
const size = stat.size;
|
|
721
|
+
if (size < source.indexedThrough || (exact && size !== source.indexedThrough)) return false;
|
|
722
|
+
if (
|
|
723
|
+
source.device !== null &&
|
|
724
|
+
source.inode !== null &&
|
|
725
|
+
(Number(stat.dev) !== source.device || Number(stat.ino) !== source.inode)
|
|
726
|
+
) {
|
|
727
|
+
return false;
|
|
762
728
|
}
|
|
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);
|
|
729
|
+
if (
|
|
730
|
+
size === source.indexedThrough &&
|
|
731
|
+
stat.mtimeMs === source.mtimeMs &&
|
|
732
|
+
stat.ctimeMs === source.ctimeMs
|
|
733
|
+
) {
|
|
734
|
+
return true;
|
|
772
735
|
}
|
|
773
|
-
|
|
736
|
+
return hashFilePrefix(file, source.indexedThrough) === source.walHash;
|
|
774
737
|
}
|
|
775
738
|
|
|
776
739
|
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
|
-
}
|
|
740
|
+
return outcomeFoldEngine.applyFoldEvent(state, ev);
|
|
986
741
|
}
|
|
987
742
|
|
|
988
743
|
function consumeLogSlice(file, startByte, onEvent, { repairTail = false, readOnly = false, startLine = 0 } = {}) {
|
|
@@ -1039,111 +794,225 @@ function consumeLogSlice(file, startByte, onEvent, { repairTail = false, readOnl
|
|
|
1039
794
|
}
|
|
1040
795
|
onEvent(event, start, pos);
|
|
1041
796
|
} catch (err) {
|
|
1042
|
-
if (err instanceof DriftSealError) throw err;
|
|
797
|
+
if (err instanceof DriftSealError || err instanceof OutcomeIndexError) throw err;
|
|
1043
798
|
fail(`corrupt log line ${lineNumber} in ${file}`);
|
|
1044
799
|
}
|
|
1045
800
|
}
|
|
1046
801
|
return { endByte: pos, endLine: lineNumber };
|
|
1047
802
|
}
|
|
1048
803
|
|
|
1049
|
-
function
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
804
|
+
function replaceOutcomeIndexFile(temporary, target) {
|
|
805
|
+
try {
|
|
806
|
+
fs.renameSync(temporary, target);
|
|
807
|
+
} catch (error) {
|
|
808
|
+
if (!['EEXIST', 'EPERM'].includes(error && error.code)) throw error;
|
|
809
|
+
removeIndexFiles(target);
|
|
810
|
+
fs.renameSync(temporary, target);
|
|
811
|
+
}
|
|
812
|
+
fs.chmodSync(target, 0o600);
|
|
813
|
+
for (const suffix of ['-journal', '-wal', '-shm']) {
|
|
814
|
+
fs.rmSync(`${target}${suffix}`, { force: true });
|
|
815
|
+
}
|
|
816
|
+
fsyncDirectory(path.dirname(target));
|
|
1056
817
|
}
|
|
1057
818
|
|
|
1058
|
-
function
|
|
1059
|
-
const
|
|
1060
|
-
if (
|
|
819
|
+
function removeLegacyLaneIndex() {
|
|
820
|
+
const legacy = legacyLaneIndexFile();
|
|
821
|
+
if (legacy) fs.rmSync(legacy, { force: true });
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function rebuildCommittedOutcomeIndex({ repairTail = false } = {}) {
|
|
825
|
+
const wal = logFile();
|
|
826
|
+
const target = laneIndexFile();
|
|
827
|
+
if (!target) return null;
|
|
828
|
+
ensureDirectoryDurable(path.dirname(target));
|
|
829
|
+
ensureDerivedLaneSidecarIgnore();
|
|
830
|
+
const temporary = temporaryIndexPath(target);
|
|
831
|
+
removeIndexFiles(temporary);
|
|
832
|
+
let index;
|
|
1061
833
|
try {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
834
|
+
index = openOutcomeIndex(temporary);
|
|
835
|
+
let slice;
|
|
836
|
+
index.transaction(() => {
|
|
837
|
+
const indexedEvents = [];
|
|
838
|
+
slice = consumeLogSlice(
|
|
839
|
+
wal,
|
|
840
|
+
0,
|
|
841
|
+
(event, startByte, endByte) =>
|
|
842
|
+
indexedEvents.push({ event, startByte, endByte }),
|
|
843
|
+
{ repairTail }
|
|
844
|
+
);
|
|
845
|
+
index.replaceFromFoldState(
|
|
846
|
+
outcomeFoldEngine.foldState(indexedEvents.map((item) => item.event)),
|
|
847
|
+
indexedEvents
|
|
848
|
+
);
|
|
849
|
+
index.setSource(
|
|
850
|
+
laneIndexSourceIdentity(wal, slice.endByte, slice.endLine),
|
|
851
|
+
'full'
|
|
852
|
+
);
|
|
853
|
+
index.acceptProjection();
|
|
854
|
+
});
|
|
855
|
+
if (!index.integrityCheck()) fail('rebuilt SQLite outcome index failed integrity check');
|
|
856
|
+
index.close();
|
|
857
|
+
index = null;
|
|
858
|
+
replaceOutcomeIndexFile(temporary, target);
|
|
859
|
+
removeLegacyLaneIndex();
|
|
860
|
+
const reopened = openOutcomeIndex(target);
|
|
861
|
+
reopened.build = 'full';
|
|
862
|
+
return reopened;
|
|
863
|
+
} catch (error) {
|
|
864
|
+
if (index) index.close();
|
|
865
|
+
removeIndexFiles(temporary);
|
|
866
|
+
throw error;
|
|
1065
867
|
}
|
|
1066
868
|
}
|
|
1067
869
|
|
|
1068
|
-
function syncCommittedLaneIndex({ repairTail = false, readOnly = false } = {}) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
870
|
+
function syncCommittedLaneIndex({ repairTail = false, readOnly = false, forceFull = false } = {}) {
|
|
871
|
+
if (process.env._DRIFTSEAL_TEST_DISABLE_OUTCOME_INDEX === '1') return null;
|
|
872
|
+
const wal = logFile();
|
|
873
|
+
const target = laneIndexFile();
|
|
874
|
+
if (!target) return null;
|
|
875
|
+
if (readOnly) {
|
|
876
|
+
if (!fs.existsSync(target)) return null;
|
|
877
|
+
try {
|
|
878
|
+
const index = openOutcomeIndex(target, { readOnly: true });
|
|
879
|
+
if (
|
|
880
|
+
!index.projectionTrusted() ||
|
|
881
|
+
!laneIndexMatchesFile(index.source(), wal, { exact: true })
|
|
882
|
+
) {
|
|
883
|
+
index.close();
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
index.build = 'hot';
|
|
887
|
+
return index;
|
|
888
|
+
} catch {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (forceFull || !fs.existsSync(target)) {
|
|
893
|
+
try {
|
|
894
|
+
return rebuildCommittedOutcomeIndex({ repairTail });
|
|
895
|
+
} catch (error) {
|
|
896
|
+
if (error instanceof SqliteUnavailableError) return null;
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
let index;
|
|
901
|
+
try {
|
|
902
|
+
index = openOutcomeIndex(target);
|
|
903
|
+
const source = index.source();
|
|
904
|
+
if (!index.projectionTrusted() || !laneIndexMatchesFile(source, wal)) {
|
|
905
|
+
index.close();
|
|
906
|
+
return rebuildCommittedOutcomeIndex({ repairTail });
|
|
907
|
+
}
|
|
908
|
+
const size = fs.existsSync(wal) ? fs.statSync(wal).size : 0;
|
|
909
|
+
if (size === source.indexedThrough) {
|
|
910
|
+
index.build = 'hot';
|
|
911
|
+
removeLegacyLaneIndex();
|
|
912
|
+
return index;
|
|
913
|
+
}
|
|
914
|
+
let slice;
|
|
915
|
+
index.transaction(() => {
|
|
916
|
+
slice = consumeLogSlice(
|
|
917
|
+
wal,
|
|
918
|
+
source.indexedThrough,
|
|
919
|
+
(event, start, end) =>
|
|
920
|
+
index.applyEvent(event, start, end, { applyFoldEvent, defaultLane: DEFAULT_LANE }),
|
|
921
|
+
{
|
|
922
|
+
repairTail,
|
|
923
|
+
startLine: source.indexedLines || 0,
|
|
924
|
+
}
|
|
1091
925
|
);
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
926
|
+
index.setSource(
|
|
927
|
+
laneIndexSourceIdentity(wal, slice.endByte, slice.endLine),
|
|
928
|
+
'incremental'
|
|
929
|
+
);
|
|
930
|
+
index.acceptProjection();
|
|
931
|
+
});
|
|
932
|
+
index.build = 'incremental';
|
|
933
|
+
removeLegacyLaneIndex();
|
|
934
|
+
return index;
|
|
935
|
+
} catch (error) {
|
|
936
|
+
if (index) index.close();
|
|
937
|
+
if (error instanceof DriftSealError) throw error;
|
|
938
|
+
if (error instanceof SqliteUnavailableError) return null;
|
|
939
|
+
return rebuildCommittedOutcomeIndex({ repairTail });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function attachIndexedOverlay(index, committed, plan, { readOnly = false } = {}) {
|
|
944
|
+
const lanes = index.laneCatalog();
|
|
945
|
+
if (!plan || plan.alreadyCommitted || plan.records.length === 0) {
|
|
946
|
+
committed.lanes = lanes;
|
|
947
|
+
return committed;
|
|
948
|
+
}
|
|
949
|
+
if (!readOnly && plan.mappings.length > 0) writeJsonl(plan.park, plan.records);
|
|
950
|
+
const overlay = fold(plan.records.map((record) => record.event));
|
|
951
|
+
for (const record of overlay) {
|
|
952
|
+
let lane = lanes.get(record.lane);
|
|
953
|
+
if (!lane) {
|
|
954
|
+
const foldedLane = overlay.lanes.get(record.lane);
|
|
955
|
+
lane = {
|
|
956
|
+
name: record.lane,
|
|
957
|
+
description: foldedLane ? foldedLane.description : null,
|
|
958
|
+
addedAt: foldedLane ? foldedLane.addedAt : null,
|
|
959
|
+
inferred: foldedLane ? foldedLane.inferred === true : true,
|
|
960
|
+
head: null,
|
|
961
|
+
count: 0,
|
|
962
|
+
visible: 0,
|
|
963
|
+
};
|
|
964
|
+
lanes.set(record.lane, lane);
|
|
1099
965
|
}
|
|
966
|
+
lane.count = (lane.count || 0) + 1;
|
|
967
|
+
if (!record.reclaimed) lane.visible = (lane.visible || 0) + 1;
|
|
1100
968
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
return state;
|
|
969
|
+
const records = [...committed, ...overlay];
|
|
970
|
+
records.lanes = lanes;
|
|
971
|
+
return records;
|
|
1105
972
|
}
|
|
1106
973
|
|
|
1107
|
-
function
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
974
|
+
function queryOutcomeView(index, park, { repairTail = false, readOnly = false } = {}) {
|
|
975
|
+
const committed = index.queryAll();
|
|
976
|
+
if (!park || !fs.existsSync(park)) {
|
|
977
|
+
committed.lanes = index.laneCatalog();
|
|
978
|
+
return { index: null, records: committed };
|
|
979
|
+
}
|
|
980
|
+
const plan = planIndexedInProgressOverlay(index, park, { repairTail, readOnly });
|
|
981
|
+
if (plan && plan.alreadyCommitted && !readOnly) discardInProgressLog(park);
|
|
982
|
+
return {
|
|
983
|
+
index: null,
|
|
984
|
+
records: attachIndexedOverlay(index, committed, plan, { readOnly }),
|
|
985
|
+
};
|
|
1113
986
|
}
|
|
1114
987
|
|
|
1115
|
-
function
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
988
|
+
function recoverableOutcomeIndexError(error) {
|
|
989
|
+
return (
|
|
990
|
+
error instanceof OutcomeIndexError ||
|
|
991
|
+
/^SQLITE_|^ERR_SQLITE_/.test(String(error && error.code))
|
|
992
|
+
);
|
|
1119
993
|
}
|
|
1120
994
|
|
|
1121
995
|
function loadOutcomeView({ repairTail = false, readOnly = false } = {}) {
|
|
1122
|
-
const committed = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
1123
996
|
const park = inProgressFile();
|
|
1124
|
-
|
|
1125
|
-
|
|
997
|
+
let index = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
998
|
+
if (!index) {
|
|
999
|
+
const records = fold(readEvents({ repairTail, readOnly }));
|
|
1000
|
+
return { index: null, records };
|
|
1126
1001
|
}
|
|
1127
1002
|
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;
|
|
1003
|
+
return queryOutcomeView(index, park, { repairTail, readOnly });
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
index.close();
|
|
1006
|
+
index = null;
|
|
1007
|
+
if (readOnly && (error.code === 'ENOENT' || recoverableOutcomeIndexError(error))) {
|
|
1008
|
+
return { index: null, records: fold(readEvents({ repairTail, readOnly })) };
|
|
1009
|
+
}
|
|
1010
|
+
if (!recoverableOutcomeIndexError(error)) throw error;
|
|
1011
|
+
index = syncCommittedLaneIndex({ repairTail, forceFull: true });
|
|
1012
|
+
if (!index) return { index: null, records: fold(readEvents({ repairTail })) };
|
|
1013
|
+
return queryOutcomeView(index, park, { repairTail });
|
|
1014
|
+
} finally {
|
|
1015
|
+
if (index) index.close();
|
|
1147
1016
|
}
|
|
1148
1017
|
}
|
|
1149
1018
|
|
|
@@ -1213,6 +1082,53 @@ function selectLastLogRecords(records, current, n) {
|
|
|
1213
1082
|
return [...clipped, ...extras].sort((left, right) => order.get(left.id) - order.get(right.id));
|
|
1214
1083
|
}
|
|
1215
1084
|
|
|
1085
|
+
function indexedLaneSummary(state, name) {
|
|
1086
|
+
const lane = state.lanes.get(name);
|
|
1087
|
+
return {
|
|
1088
|
+
name,
|
|
1089
|
+
description: lane ? lane.description : null,
|
|
1090
|
+
addedAt: lane ? lane.addedAt : null,
|
|
1091
|
+
inferred: Boolean(lane && lane.inferred),
|
|
1092
|
+
visible: lane ? lane.visible : 0,
|
|
1093
|
+
count: lane ? lane.count : 0,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function tryLoadRecentOutcomeView(n, { includeReclaimed = false, repairTail = false, readOnly = false } = {}) {
|
|
1098
|
+
const park = inProgressFile();
|
|
1099
|
+
let index = syncCommittedLaneIndex({ repairTail, readOnly });
|
|
1100
|
+
if (!index) return null;
|
|
1101
|
+
const query = () => {
|
|
1102
|
+
const plan =
|
|
1103
|
+
park && fs.existsSync(park)
|
|
1104
|
+
? planIndexedInProgressOverlay(index, park, { repairTail, readOnly })
|
|
1105
|
+
: null;
|
|
1106
|
+
if (plan && plan.alreadyCommitted && !readOnly) discardInProgressLog(park);
|
|
1107
|
+
const overlayView = attachIndexedOverlay(index, [], plan, { readOnly });
|
|
1108
|
+
const state = { lanes: overlayView.lanes };
|
|
1109
|
+
const { current, missing } = resolveCurrentLane(state);
|
|
1110
|
+
const committed = index.queryRecent(current, n, { includeReclaimed });
|
|
1111
|
+
const records = selectLastLogRecords(
|
|
1112
|
+
[...committed, ...overlayView],
|
|
1113
|
+
current,
|
|
1114
|
+
n
|
|
1115
|
+
);
|
|
1116
|
+
return { state, records, current, missing };
|
|
1117
|
+
};
|
|
1118
|
+
try {
|
|
1119
|
+
return query();
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
if (readOnly && error && error.code === 'ENOENT') return null;
|
|
1122
|
+
index.close();
|
|
1123
|
+
index = null;
|
|
1124
|
+
if (readOnly) return null;
|
|
1125
|
+
index = syncCommittedLaneIndex({ repairTail, forceFull: true });
|
|
1126
|
+
return query();
|
|
1127
|
+
} finally {
|
|
1128
|
+
if (index) index.close();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1216
1132
|
function liveWorktreeOutcomeLog() {
|
|
1217
1133
|
const root = gitWorktreeRoot();
|
|
1218
1134
|
if (!root) return null;
|
|
@@ -1294,6 +1210,27 @@ function planInProgressOverlay(committedEvents, park, { repairTail = false, read
|
|
|
1294
1210
|
return { park, records: remapped.records, mappings: remapped.mappings, alreadyCommitted: false };
|
|
1295
1211
|
}
|
|
1296
1212
|
|
|
1213
|
+
function planIndexedInProgressOverlay(index, park, { repairTail = false, readOnly = false } = {}) {
|
|
1214
|
+
if (!park || !fs.existsSync(park)) return null;
|
|
1215
|
+
const overlayRecords = readJsonlRecordsFromFile(park, { repairTail, readOnly });
|
|
1216
|
+
const overlayEvents = overlayRecords.map((record) => record.event);
|
|
1217
|
+
if (overlayEvents.length === 0 || index.containsEventSequence(overlayEvents)) {
|
|
1218
|
+
return { park, records: [], mappings: [], alreadyCommitted: true };
|
|
1219
|
+
}
|
|
1220
|
+
const remapped = remapTheirsRecords(
|
|
1221
|
+
overlayRecords,
|
|
1222
|
+
index.outcomeStartEvents(),
|
|
1223
|
+
new Map(),
|
|
1224
|
+
new Map()
|
|
1225
|
+
);
|
|
1226
|
+
return {
|
|
1227
|
+
park,
|
|
1228
|
+
records: remapped.records,
|
|
1229
|
+
mappings: remapped.mappings,
|
|
1230
|
+
alreadyCommitted: false,
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1297
1234
|
function discardInProgressLog(park) {
|
|
1298
1235
|
fs.unlinkSync(park);
|
|
1299
1236
|
fsyncDirectory(path.dirname(park));
|
|
@@ -1867,77 +1804,20 @@ function withMutationLocks(resources, action, { tryWaitMs } = {}) {
|
|
|
1867
1804
|
}
|
|
1868
1805
|
|
|
1869
1806
|
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
|
-
}));
|
|
1807
|
+
return outcomeFoldEngine.outcomeContractHash(record);
|
|
1882
1808
|
}
|
|
1883
1809
|
|
|
1884
1810
|
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;
|
|
1811
|
+
return outcomeFoldEngine.newOutcomeRecord(ev);
|
|
1915
1812
|
}
|
|
1916
1813
|
|
|
1917
1814
|
/** Fold the event stream into one record per outcome. Legacy v1 events are accepted for migration. */
|
|
1918
1815
|
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;
|
|
1816
|
+
return outcomeFoldEngine.fold(events);
|
|
1929
1817
|
}
|
|
1930
1818
|
|
|
1931
1819
|
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
|
-
});
|
|
1820
|
+
return outcomeFoldEngine.qualifyingDecisionUpdates(record, decisionId);
|
|
1941
1821
|
}
|
|
1942
1822
|
|
|
1943
1823
|
function openOutcome(records) {
|
|
@@ -6294,16 +6174,47 @@ const commands = {
|
|
|
6294
6174
|
'all-lanes': 'boolean',
|
|
6295
6175
|
}, 'log');
|
|
6296
6176
|
if (positionals.length > 0) fail(usageFor('log'));
|
|
6177
|
+
const n = flags.last === undefined ? null : positiveInteger(flags.last, '--last');
|
|
6178
|
+
const allLanes = flags['all-lanes'] === true;
|
|
6179
|
+
const parkedV1 = legacyParkedIntent();
|
|
6180
|
+
if (
|
|
6181
|
+
n !== null &&
|
|
6182
|
+
!allLanes &&
|
|
6183
|
+
!parkedV1 &&
|
|
6184
|
+
process.env._DRIFTSEAL_TEST_DISABLE_RECENT_INDEX !== '1'
|
|
6185
|
+
) {
|
|
6186
|
+
const recent = tryLoadRecentOutcomeView(n, {
|
|
6187
|
+
includeReclaimed: flags.all === true,
|
|
6188
|
+
repairTail: true,
|
|
6189
|
+
readOnly,
|
|
6190
|
+
});
|
|
6191
|
+
if (recent) {
|
|
6192
|
+
const { state, records, current, missing } = recent;
|
|
6193
|
+
warnMissingCurrentLane(missing);
|
|
6194
|
+
const customLanes = state.lanes.size > 1;
|
|
6195
|
+
if (customLanes || current !== DEFAULT_LANE || missing) {
|
|
6196
|
+
const summary = indexedLaneSummary(state, current);
|
|
6197
|
+
printLine(`lane: ${current} (${summary.visible} visible / ${summary.count} in lane)`);
|
|
6198
|
+
}
|
|
6199
|
+
if (records.length === 0) {
|
|
6200
|
+
printLine('log is empty');
|
|
6201
|
+
return [];
|
|
6202
|
+
}
|
|
6203
|
+
printLine(records.map((record) => render(record, { currentLane: current })).join('\n\n'));
|
|
6204
|
+
return records.map(publicOutcome);
|
|
6205
|
+
}
|
|
6206
|
+
}
|
|
6207
|
+
if (process.env._DRIFTSEAL_TEST_REQUIRE_RECENT_INDEX === '1' && n !== null && !allLanes) {
|
|
6208
|
+
fail('recent lane index fast path was not used');
|
|
6209
|
+
}
|
|
6297
6210
|
const view = loadOutcomeView({ repairTail: true, readOnly });
|
|
6298
6211
|
let records = view.records;
|
|
6299
|
-
const parkedV1 = legacyParkedIntent();
|
|
6300
6212
|
if (parkedV1 && !records.some((record) => record.id === parkedV1.id && record.status === 'in_progress')) {
|
|
6301
6213
|
records = Object.assign([...records, parkedV1], { lanes: records.lanes });
|
|
6302
6214
|
}
|
|
6303
6215
|
const catalog = records.lanes || emptyLaneCatalog();
|
|
6304
6216
|
const { current, missing } = resolveCurrentLane(view.records);
|
|
6305
6217
|
warnMissingCurrentLane(missing);
|
|
6306
|
-
const allLanes = flags['all-lanes'] === true;
|
|
6307
6218
|
if (!allLanes) {
|
|
6308
6219
|
records = Object.assign(selectLaneLogRecords(records, current), { lanes: catalog });
|
|
6309
6220
|
}
|
|
@@ -6313,8 +6224,7 @@ const commands = {
|
|
|
6313
6224
|
printLine(renderLaneLine(view.records, current));
|
|
6314
6225
|
}
|
|
6315
6226
|
let shown = visible;
|
|
6316
|
-
if (
|
|
6317
|
-
const n = positiveInteger(flags.last, '--last');
|
|
6227
|
+
if (n !== null) {
|
|
6318
6228
|
shown = selectLastLogRecords(visible, current, n);
|
|
6319
6229
|
}
|
|
6320
6230
|
if (shown.length === 0) {
|
|
@@ -7035,9 +6945,52 @@ function repositoryOutcomeLogFiles() {
|
|
|
7035
6945
|
return files;
|
|
7036
6946
|
}
|
|
7037
6947
|
|
|
6948
|
+
function indexedMigrationEvent(file) {
|
|
6949
|
+
if (
|
|
6950
|
+
canonicalPath(file) !== canonicalPath(logFile()) ||
|
|
6951
|
+
!laneIndexFile() ||
|
|
6952
|
+
!fs.existsSync(laneIndexFile())
|
|
6953
|
+
) {
|
|
6954
|
+
return { usable: false, migration: null };
|
|
6955
|
+
}
|
|
6956
|
+
let index;
|
|
6957
|
+
try {
|
|
6958
|
+
index = openOutcomeIndex(laneIndexFile(), { readOnly: true });
|
|
6959
|
+
const source = index.source();
|
|
6960
|
+
if (!laneIndexMatchesFile(source, file)) {
|
|
6961
|
+
return { usable: false, migration: null };
|
|
6962
|
+
}
|
|
6963
|
+
let migration = index.migrationEvent();
|
|
6964
|
+
const size = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
6965
|
+
if (size > source.indexedThrough) {
|
|
6966
|
+
consumeLogSlice(
|
|
6967
|
+
file,
|
|
6968
|
+
source.indexedThrough,
|
|
6969
|
+
(event) => {
|
|
6970
|
+
if (event.type === 'migration' && event.id === 'v1-to-v2') migration = event;
|
|
6971
|
+
},
|
|
6972
|
+
{
|
|
6973
|
+
readOnly: true,
|
|
6974
|
+
startLine: source.indexedLines || 0,
|
|
6975
|
+
}
|
|
6976
|
+
);
|
|
6977
|
+
}
|
|
6978
|
+
return { usable: true, migration };
|
|
6979
|
+
} catch {
|
|
6980
|
+
return { usable: false, migration: null };
|
|
6981
|
+
} finally {
|
|
6982
|
+
if (index) index.close();
|
|
6983
|
+
}
|
|
6984
|
+
}
|
|
6985
|
+
|
|
7038
6986
|
function repositoryMigrationEvent() {
|
|
7039
6987
|
for (const file of repositoryOutcomeLogFiles()) {
|
|
7040
6988
|
try {
|
|
6989
|
+
const indexed = indexedMigrationEvent(file);
|
|
6990
|
+
if (indexed.usable) {
|
|
6991
|
+
if (indexed.migration) return indexed.migration;
|
|
6992
|
+
continue;
|
|
6993
|
+
}
|
|
7041
6994
|
const migration = findMigrationEvent(file);
|
|
7042
6995
|
if (migration) return migration;
|
|
7043
6996
|
} catch {
|
|
@@ -7305,6 +7258,7 @@ function repositoryRoot(root) {
|
|
|
7305
7258
|
}
|
|
7306
7259
|
|
|
7307
7260
|
function runCommand(argv, { root = process.cwd(), isolateStorage = false, capture = true } = {}) {
|
|
7261
|
+
assertSupportedNode();
|
|
7308
7262
|
if (!Array.isArray(argv) || argv.some((arg) => typeof arg !== 'string')) {
|
|
7309
7263
|
fail('command arguments must be an array of strings');
|
|
7310
7264
|
}
|
|
@@ -7365,6 +7319,7 @@ function appendFlag(argv, flag, value) {
|
|
|
7365
7319
|
}
|
|
7366
7320
|
|
|
7367
7321
|
function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
7322
|
+
assertSupportedNode();
|
|
7368
7323
|
const fixedRoot = repositoryRoot(root);
|
|
7369
7324
|
let lastReadOnly = false;
|
|
7370
7325
|
const call = (argv) => {
|
|
@@ -7501,6 +7456,7 @@ function createApi({ root = process.cwd(), isolateStorage = false } = {}) {
|
|
|
7501
7456
|
|
|
7502
7457
|
function main() {
|
|
7503
7458
|
try {
|
|
7459
|
+
assertSupportedNode();
|
|
7504
7460
|
const result = dispatch(process.argv.slice(2));
|
|
7505
7461
|
process.exitCode = result.exitCode;
|
|
7506
7462
|
} catch (err) {
|