ushman-ledger 0.3.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/AGENTS.md +41 -0
- package/CHANGELOG.md +23 -0
- package/LICENSE.md +21 -0
- package/README.md +233 -0
- package/dist/archive-journal.d.ts +63 -0
- package/dist/archive-journal.d.ts.map +1 -0
- package/dist/archive-journal.js +220 -0
- package/dist/archive.d.ts +30 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +117 -0
- package/dist/async.d.ts +2 -0
- package/dist/async.d.ts.map +1 -0
- package/dist/async.js +20 -0
- package/dist/blobs.d.ts +10 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +58 -0
- package/dist/builders.d.ts +465 -0
- package/dist/builders.d.ts.map +1 -0
- package/dist/builders.js +73 -0
- package/dist/candidate-paths.d.ts +3 -0
- package/dist/candidate-paths.d.ts.map +1 -0
- package/dist/candidate-paths.js +11 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +562 -0
- package/dist/coverage.d.ts +8 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/coverage.js +128 -0
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +172 -0
- package/dist/handle.d.ts +28 -0
- package/dist/handle.d.ts.map +1 -0
- package/dist/handle.js +90 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/json.d.ts +4 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +25 -0
- package/dist/lab-min.d.ts +9 -0
- package/dist/lab-min.d.ts.map +1 -0
- package/dist/lab-min.js +23 -0
- package/dist/list.d.ts +582 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +139 -0
- package/dist/manifest-update.d.ts +13 -0
- package/dist/manifest-update.d.ts.map +1 -0
- package/dist/manifest-update.js +43 -0
- package/dist/note.d.ts +13 -0
- package/dist/note.d.ts.map +1 -0
- package/dist/note.js +15 -0
- package/dist/patch-metadata.d.ts +37 -0
- package/dist/patch-metadata.d.ts.map +1 -0
- package/dist/patch-metadata.js +300 -0
- package/dist/read-index.d.ts +114 -0
- package/dist/read-index.d.ts.map +1 -0
- package/dist/read-index.js +210 -0
- package/dist/record.d.ts +25 -0
- package/dist/record.d.ts.map +1 -0
- package/dist/record.js +268 -0
- package/dist/recovery.d.ts +39 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +189 -0
- package/dist/render/analytics-summary.d.ts +58 -0
- package/dist/render/analytics-summary.d.ts.map +1 -0
- package/dist/render/analytics-summary.js +151 -0
- package/dist/render/dependency-graph.d.ts +3 -0
- package/dist/render/dependency-graph.d.ts.map +1 -0
- package/dist/render/dependency-graph.js +18 -0
- package/dist/render/jsonl.d.ts +3 -0
- package/dist/render/jsonl.d.ts.map +1 -0
- package/dist/render/jsonl.js +8 -0
- package/dist/render/retro.d.ts +6 -0
- package/dist/render/retro.d.ts.map +1 -0
- package/dist/render/retro.js +124 -0
- package/dist/render/timeline-html.d.ts +3 -0
- package/dist/render/timeline-html.d.ts.map +1 -0
- package/dist/render/timeline-html.js +37 -0
- package/dist/schema/entry.d.ts +3298 -0
- package/dist/schema/entry.d.ts.map +1 -0
- package/dist/schema/entry.js +619 -0
- package/dist/schema/manifest.d.ts +42 -0
- package/dist/schema/manifest.d.ts.map +1 -0
- package/dist/schema/manifest.js +27 -0
- package/dist/schema/note.d.ts +10 -0
- package/dist/schema/note.d.ts.map +1 -0
- package/dist/schema/note.js +2 -0
- package/dist/storage/filesystem.d.ts +35 -0
- package/dist/storage/filesystem.d.ts.map +1 -0
- package/dist/storage/filesystem.js +258 -0
- package/dist/storage/lock.d.ts +18 -0
- package/dist/storage/lock.d.ts.map +1 -0
- package/dist/storage/lock.js +224 -0
- package/dist/uuid.d.ts +7 -0
- package/dist/uuid.d.ts.map +1 -0
- package/dist/uuid.js +25 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1 -0
- package/package.json +73 -0
package/dist/list.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { mapWithConcurrencyLimit } from "./async.js";
|
|
2
|
+
import { matchesReadIndexFilter, } from "./read-index.js";
|
|
3
|
+
import { loadLedgerState } from "./recovery.js";
|
|
4
|
+
import { parseLedgerEntry } from "./schema/entry.js";
|
|
5
|
+
import { readPhaseEntryText } from "./storage/filesystem.js";
|
|
6
|
+
const matchesFilter = (entry, filter) => {
|
|
7
|
+
if (filter.kind && entry.kind !== filter.kind) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (filter.phase && entry.phase !== filter.phase) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
if (filter.since && entry.ts < filter.since) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
};
|
|
18
|
+
const ENTRY_READ_BATCH_SIZE = 32;
|
|
19
|
+
const ENTRY_READ_CONCURRENCY = 16;
|
|
20
|
+
const getFilteredReadIndexEntries = ({ direction, filter, manifest, readIndex, }) => {
|
|
21
|
+
const sourceEntries = direction === 'asc' ? readIndex.entries : [...readIndex.entries].reverse();
|
|
22
|
+
const filteredEntries = [];
|
|
23
|
+
for (const readIndexEntry of sourceEntries) {
|
|
24
|
+
if (!matchesReadIndexFilter({
|
|
25
|
+
filter,
|
|
26
|
+
manifest,
|
|
27
|
+
readIndexEntry,
|
|
28
|
+
})) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
filteredEntries.push(readIndexEntry);
|
|
32
|
+
if (direction === 'desc' && filter.limit && filteredEntries.length === filter.limit) {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return filteredEntries;
|
|
37
|
+
};
|
|
38
|
+
export const getOrderedEntryLocations = (manifest, readIndex, filter, direction = 'asc') => {
|
|
39
|
+
return getFilteredReadIndexEntries({
|
|
40
|
+
direction,
|
|
41
|
+
filter,
|
|
42
|
+
manifest,
|
|
43
|
+
readIndex,
|
|
44
|
+
})
|
|
45
|
+
.map((readIndexEntry) => {
|
|
46
|
+
const location = manifest.entryLocations[readIndexEntry.id];
|
|
47
|
+
if (!location) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return [readIndexEntry.id, location];
|
|
51
|
+
})
|
|
52
|
+
.filter((entryLocation) => entryLocation !== null);
|
|
53
|
+
};
|
|
54
|
+
const readManifestEntry = async ({ allowMissing, entryId, location, workspaceRoot, }) => {
|
|
55
|
+
try {
|
|
56
|
+
const text = await readPhaseEntryText(workspaceRoot, location.phase, `${entryId}.json`);
|
|
57
|
+
return {
|
|
58
|
+
entry: parseLedgerEntry(JSON.parse(text)),
|
|
59
|
+
entryId,
|
|
60
|
+
location,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (allowMissing && error.code === 'ENOENT') {
|
|
65
|
+
return {
|
|
66
|
+
entry: null,
|
|
67
|
+
entryId,
|
|
68
|
+
location,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
export const readManifestEntryBatch = async ({ allowMissing = false, entryLocations, workspaceRoot, }) => mapWithConcurrencyLimit(entryLocations, ENTRY_READ_CONCURRENCY, async ([entryId, location]) => readManifestEntry({
|
|
75
|
+
allowMissing,
|
|
76
|
+
entryId,
|
|
77
|
+
location,
|
|
78
|
+
workspaceRoot,
|
|
79
|
+
}));
|
|
80
|
+
const collectLimitedEntries = async ({ filter, manifest, readIndex, workspaceRoot, }) => {
|
|
81
|
+
const collectedEntries = [];
|
|
82
|
+
const orderedEntries = getOrderedEntryLocations(manifest, readIndex, filter, 'desc');
|
|
83
|
+
const limit = filter.limit ?? 0;
|
|
84
|
+
for (let index = 0; index < orderedEntries.length && collectedEntries.length < limit; index += ENTRY_READ_BATCH_SIZE) {
|
|
85
|
+
const batch = orderedEntries.slice(index, index + ENTRY_READ_BATCH_SIZE);
|
|
86
|
+
const resolvedEntries = await readManifestEntryBatch({
|
|
87
|
+
entryLocations: batch,
|
|
88
|
+
workspaceRoot,
|
|
89
|
+
});
|
|
90
|
+
for (const resolvedEntry of resolvedEntries) {
|
|
91
|
+
if (!resolvedEntry.entry) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (matchesFilter(resolvedEntry.entry, filter)) {
|
|
95
|
+
collectedEntries.push(resolvedEntry.entry);
|
|
96
|
+
}
|
|
97
|
+
if (collectedEntries.length === limit) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
collectedEntries.reverse();
|
|
103
|
+
return collectedEntries;
|
|
104
|
+
};
|
|
105
|
+
export const iterateEntriesFromManifest = async function* (workspaceRoot, manifest, readIndex, filter = {}) {
|
|
106
|
+
if (filter.limit) {
|
|
107
|
+
for (const entry of await collectLimitedEntries({ filter, manifest, readIndex, workspaceRoot })) {
|
|
108
|
+
yield entry;
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const orderedEntries = getOrderedEntryLocations(manifest, readIndex, filter);
|
|
113
|
+
for (let index = 0; index < orderedEntries.length; index += ENTRY_READ_BATCH_SIZE) {
|
|
114
|
+
const batch = orderedEntries.slice(index, index + ENTRY_READ_BATCH_SIZE);
|
|
115
|
+
const resolvedEntries = await readManifestEntryBatch({
|
|
116
|
+
entryLocations: batch,
|
|
117
|
+
workspaceRoot,
|
|
118
|
+
});
|
|
119
|
+
for (const resolvedEntry of resolvedEntries) {
|
|
120
|
+
if (!resolvedEntry.entry) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (matchesFilter(resolvedEntry.entry, filter)) {
|
|
124
|
+
yield resolvedEntry.entry;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
export const listEntries = async function* (workspaceRoot, filter = {}) {
|
|
130
|
+
const { manifest, readIndex } = await loadLedgerState(workspaceRoot);
|
|
131
|
+
yield* iterateEntriesFromManifest(workspaceRoot, manifest, readIndex, filter);
|
|
132
|
+
};
|
|
133
|
+
export const readAllEntries = async (workspaceRoot, filter = {}) => {
|
|
134
|
+
const entries = [];
|
|
135
|
+
for await (const entry of listEntries(workspaceRoot, filter)) {
|
|
136
|
+
entries.push(entry);
|
|
137
|
+
}
|
|
138
|
+
return entries;
|
|
139
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LedgerEntry } from './schema/entry.ts';
|
|
2
|
+
import type { LedgerArchiveMetadata, LedgerManifest } from './schema/manifest.ts';
|
|
3
|
+
export declare const getNextManifestSequence: (lastSequence: number) => number;
|
|
4
|
+
export declare const updateManifestForEntry: ({ entry, logicalHash, manifest, }: {
|
|
5
|
+
readonly entry: LedgerEntry;
|
|
6
|
+
readonly logicalHash: string;
|
|
7
|
+
readonly manifest: LedgerManifest;
|
|
8
|
+
}) => LedgerManifest;
|
|
9
|
+
export declare const updateManifestForArchive: ({ archive, manifest, }: {
|
|
10
|
+
readonly archive: LedgerArchiveMetadata;
|
|
11
|
+
readonly manifest: LedgerManifest;
|
|
12
|
+
}) => LedgerManifest;
|
|
13
|
+
//# sourceMappingURL=manifest-update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-update.d.ts","sourceRoot":"","sources":["../src/manifest-update.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAElF,eAAO,MAAM,uBAAuB,GAAI,cAAc,MAAM,WAQ3D,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,mCAIpC;IACC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;CACrC,KAAG,cA6BH,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,wBAGtC;IACC,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;CACrC,KAAG,cAGF,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const getNextManifestSequence = (lastSequence) => {
|
|
2
|
+
if (!Number.isSafeInteger(lastSequence) || lastSequence < 0) {
|
|
3
|
+
throw new Error(`Invalid ledger sequence counter: ${lastSequence}.`);
|
|
4
|
+
}
|
|
5
|
+
if (lastSequence >= Number.MAX_SAFE_INTEGER) {
|
|
6
|
+
throw new Error('Ledger sequence counter exceeded Number.MAX_SAFE_INTEGER.');
|
|
7
|
+
}
|
|
8
|
+
return lastSequence + 1;
|
|
9
|
+
};
|
|
10
|
+
export const updateManifestForEntry = ({ entry, logicalHash, manifest, }) => {
|
|
11
|
+
const nextSequence = getNextManifestSequence(manifest.lastSequence);
|
|
12
|
+
return {
|
|
13
|
+
...manifest,
|
|
14
|
+
entryCount: manifest.entryCount + 1,
|
|
15
|
+
entryLocations: {
|
|
16
|
+
...manifest.entryLocations,
|
|
17
|
+
[entry.id]: {
|
|
18
|
+
phase: entry.phase,
|
|
19
|
+
sequence: nextSequence,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
idempotencyIndex: {
|
|
23
|
+
...manifest.idempotencyIndex,
|
|
24
|
+
[entry.phase]: {
|
|
25
|
+
...(manifest.idempotencyIndex[entry.phase] ?? {}),
|
|
26
|
+
[logicalHash]: entry.id,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
lastSequence: nextSequence,
|
|
30
|
+
perPhaseCounts: {
|
|
31
|
+
...manifest.perPhaseCounts,
|
|
32
|
+
[entry.phase]: (manifest.perPhaseCounts[entry.phase] ?? 0) + 1,
|
|
33
|
+
},
|
|
34
|
+
perPhaseLatest: {
|
|
35
|
+
...manifest.perPhaseLatest,
|
|
36
|
+
[entry.phase]: entry.id,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export const updateManifestForArchive = ({ archive, manifest, }) => ({
|
|
41
|
+
...manifest,
|
|
42
|
+
archives: [...manifest.archives, archive],
|
|
43
|
+
});
|
package/dist/note.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { appendRecord } from './record.ts';
|
|
2
|
+
import type { LedgerPhase } from './schema/entry.ts';
|
|
3
|
+
import type { NoteSubkind } from './schema/note.ts';
|
|
4
|
+
export type NoteBody = {
|
|
5
|
+
readonly body: string;
|
|
6
|
+
readonly phase: LedgerPhase;
|
|
7
|
+
readonly summary: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const appendNote: (workspaceRoot: string, subkind: NoteSubkind, noteBody: NoteBody) => Promise<{
|
|
10
|
+
entry: Awaited<ReturnType<typeof appendRecord>>["entry"];
|
|
11
|
+
id: string;
|
|
12
|
+
}>;
|
|
13
|
+
//# sourceMappingURL=note.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"note.d.ts","sourceRoot":"","sources":["../src/note.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,MAAM,MAAM,QAAQ,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,UAAU,GACnB,eAAe,MAAM,EACrB,SAAS,WAAW,EACpB,UAAU,QAAQ,KACnB,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAYlF,CAAC"}
|
package/dist/note.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { appendRecord } from "./record.js";
|
|
2
|
+
import { LEDGER_LIBRARY_VERSION } from "./version.js";
|
|
3
|
+
export const appendNote = async (workspaceRoot, subkind, noteBody) => {
|
|
4
|
+
return appendRecord(workspaceRoot, {
|
|
5
|
+
body: noteBody.body,
|
|
6
|
+
emitter: {
|
|
7
|
+
tool: 'ushman-ledger',
|
|
8
|
+
version: LEDGER_LIBRARY_VERSION,
|
|
9
|
+
},
|
|
10
|
+
kind: 'note',
|
|
11
|
+
phase: noteBody.phase,
|
|
12
|
+
subkind,
|
|
13
|
+
summary: noteBody.summary,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type ParsedPatchLine = {
|
|
2
|
+
readonly kind: 'add' | 'context' | 'remove';
|
|
3
|
+
readonly value: string;
|
|
4
|
+
};
|
|
5
|
+
type ParsedPatchHunk = {
|
|
6
|
+
readonly endLine: number;
|
|
7
|
+
readonly lines: readonly ParsedPatchLine[];
|
|
8
|
+
readonly newCount: number;
|
|
9
|
+
readonly newStart: number;
|
|
10
|
+
readonly oldCount: number;
|
|
11
|
+
readonly oldStart: number;
|
|
12
|
+
readonly startLine: number;
|
|
13
|
+
};
|
|
14
|
+
type ParsedPatchFile = {
|
|
15
|
+
readonly hunks: readonly ParsedPatchHunk[];
|
|
16
|
+
readonly newPath: string | null;
|
|
17
|
+
readonly oldPath: string | null;
|
|
18
|
+
readonly path: string;
|
|
19
|
+
};
|
|
20
|
+
export declare const parseStructuredPatch: (patchText: string) => ParsedPatchFile[];
|
|
21
|
+
export declare const derivePatchPayloadFromDiffText: ({ patchText, workspaceRoot, }: {
|
|
22
|
+
readonly patchText: string;
|
|
23
|
+
readonly workspaceRoot: string;
|
|
24
|
+
}) => Promise<{
|
|
25
|
+
diff: string;
|
|
26
|
+
diffSha256: string;
|
|
27
|
+
fileSha256After: Record<string, string>;
|
|
28
|
+
fileSha256Before: Record<string, string>;
|
|
29
|
+
hunks: {
|
|
30
|
+
endLine: number;
|
|
31
|
+
path: string;
|
|
32
|
+
startLine: number;
|
|
33
|
+
}[];
|
|
34
|
+
touchedPaths: string[];
|
|
35
|
+
}>;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=patch-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patch-metadata.d.ts","sourceRoot":"","sources":["../src/patch-metadata.ts"],"names":[],"mappings":"AAMA,KAAK,eAAe,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,KAAK,eAAe,GAAG;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,KAAK,eAAe,GAAG;IACnB,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACzB,CAAC;AA0IF,eAAO,MAAM,oBAAoB,GAAI,WAAW,MAAM,sBA6DrD,CAAC;AAwLF,eAAO,MAAM,8BAA8B,GAAU,+BAGlD;IACC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC;;;;;;;;;;;EA2BA,CAAC"}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { sha256Hex } from "./json.js";
|
|
4
|
+
const EMPTY_FILE_SHA256 = sha256Hex('');
|
|
5
|
+
const toWorkspaceRelativePath = (value) => value.replaceAll('\\', '/').replace(/^[ab]\//u, '');
|
|
6
|
+
const resolvePatchPath = (value) => {
|
|
7
|
+
if (!value || value === '/dev/null') {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return toWorkspaceRelativePath(value);
|
|
11
|
+
};
|
|
12
|
+
const parseHunkHeader = (line) => {
|
|
13
|
+
const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/u.exec(line);
|
|
14
|
+
if (!match) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const oldStart = Number.parseInt(match[1], 10);
|
|
18
|
+
const oldCount = Number.parseInt(match[2] ?? '1', 10);
|
|
19
|
+
const newStart = Number.parseInt(match[3], 10);
|
|
20
|
+
const newCount = Number.parseInt(match[4] ?? '1', 10);
|
|
21
|
+
const startLine = Math.max(1, newCount > 0 ? newStart : oldStart);
|
|
22
|
+
const endLine = Math.max(startLine, startLine + Math.max(newCount, 1) - 1);
|
|
23
|
+
return {
|
|
24
|
+
endLine,
|
|
25
|
+
newCount,
|
|
26
|
+
newStart,
|
|
27
|
+
oldCount,
|
|
28
|
+
oldStart,
|
|
29
|
+
startLine,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const splitPatchLines = (patchText) => patchText.replace(/\r\n/gu, '\n').split('\n');
|
|
33
|
+
const finalizeParsedPatchFile = (value) => {
|
|
34
|
+
if (value.sawMalformedPatchLines) {
|
|
35
|
+
throw new Error('Patch payload derivation requires valid unified diff hunks for every changed text block.');
|
|
36
|
+
}
|
|
37
|
+
const resolvedPath = value.newPath ?? value.oldPath;
|
|
38
|
+
if (!resolvedPath) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
hunks: value.hunks,
|
|
43
|
+
newPath: value.newPath,
|
|
44
|
+
oldPath: value.oldPath,
|
|
45
|
+
path: resolvedPath,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const createMutablePatchFile = (diffMatch) => ({
|
|
49
|
+
hunks: [],
|
|
50
|
+
newPath: toWorkspaceRelativePath(diffMatch[2]),
|
|
51
|
+
oldPath: toWorkspaceRelativePath(diffMatch[1]),
|
|
52
|
+
sawMalformedPatchLines: false,
|
|
53
|
+
});
|
|
54
|
+
const applyPatchMetadataLine = ({ currentFile, line, }) => {
|
|
55
|
+
if (line.startsWith('--- ')) {
|
|
56
|
+
currentFile.oldPath = resolvePatchPath(line.slice(4).trim());
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (line.startsWith('+++ ')) {
|
|
60
|
+
currentFile.newPath = resolvePatchPath(line.slice(4).trim());
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
65
|
+
const createMutablePatchHunk = (line) => {
|
|
66
|
+
const parsedHeader = parseHunkHeader(line);
|
|
67
|
+
if (!parsedHeader) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...parsedHeader,
|
|
72
|
+
lines: [],
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
const appendHunkLine = ({ currentHunk, line, }) => {
|
|
76
|
+
if (!currentHunk || line.startsWith('\')) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (line.startsWith('+')) {
|
|
80
|
+
currentHunk.lines.push({ kind: 'add', value: line.slice(1) });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (line.startsWith('-')) {
|
|
84
|
+
currentHunk.lines.push({ kind: 'remove', value: line.slice(1) });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (line.startsWith(' ')) {
|
|
88
|
+
currentHunk.lines.push({ kind: 'context', value: line.slice(1) });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
export const parseStructuredPatch = (patchText) => {
|
|
92
|
+
const lines = splitPatchLines(patchText);
|
|
93
|
+
const files = [];
|
|
94
|
+
let currentFile = null;
|
|
95
|
+
let currentHunk = null;
|
|
96
|
+
const flushHunk = () => {
|
|
97
|
+
if (!currentFile || !currentHunk) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
currentFile.hunks.push({
|
|
101
|
+
...currentHunk,
|
|
102
|
+
lines: [...currentHunk.lines],
|
|
103
|
+
});
|
|
104
|
+
currentHunk = null;
|
|
105
|
+
};
|
|
106
|
+
const flushFile = () => {
|
|
107
|
+
flushHunk();
|
|
108
|
+
if (!currentFile) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const parsedFile = finalizeParsedPatchFile(currentFile);
|
|
112
|
+
if (parsedFile) {
|
|
113
|
+
files.push(parsedFile);
|
|
114
|
+
}
|
|
115
|
+
currentFile = null;
|
|
116
|
+
};
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const diffMatch = /^diff --git a\/(.+?) b\/(.+)$/u.exec(line);
|
|
119
|
+
if (diffMatch) {
|
|
120
|
+
flushFile();
|
|
121
|
+
currentFile = createMutablePatchFile(diffMatch);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!currentFile) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (applyPatchMetadataLine({ currentFile, line })) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const nextHunk = createMutablePatchHunk(line);
|
|
131
|
+
if (nextHunk) {
|
|
132
|
+
flushHunk();
|
|
133
|
+
currentHunk = nextHunk;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (line.startsWith('@@')) {
|
|
137
|
+
currentFile.sawMalformedPatchLines = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
appendHunkLine({ currentHunk, line });
|
|
141
|
+
}
|
|
142
|
+
flushFile();
|
|
143
|
+
return files;
|
|
144
|
+
};
|
|
145
|
+
const splitTextSnapshot = (text) => {
|
|
146
|
+
if (text.length === 0) {
|
|
147
|
+
return {
|
|
148
|
+
hasTrailingNewline: false,
|
|
149
|
+
lines: [],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const hasTrailingNewline = text.endsWith('\n');
|
|
153
|
+
const lines = text.replace(/\r\n/gu, '\n').split('\n');
|
|
154
|
+
if (hasTrailingNewline) {
|
|
155
|
+
lines.pop();
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
hasTrailingNewline,
|
|
159
|
+
lines,
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
const joinTextSnapshot = (snapshot) => {
|
|
163
|
+
if (snapshot.lines.length === 0) {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
return `${snapshot.lines.join('\n')}${snapshot.hasTrailingNewline ? '\n' : ''}`;
|
|
167
|
+
};
|
|
168
|
+
const buildExpectedAfterSegment = (lines) => lines.filter((line) => line.kind !== 'remove').map((line) => line.value);
|
|
169
|
+
const buildExpectedBeforeSegment = (lines) => lines.filter((line) => line.kind !== 'add').map((line) => line.value);
|
|
170
|
+
const segmentsEqual = (left, right) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
171
|
+
const reconstructBeforeText = ({ afterText, file }) => {
|
|
172
|
+
const snapshot = splitTextSnapshot(afterText);
|
|
173
|
+
const lines = [...snapshot.lines];
|
|
174
|
+
for (const hunk of [...file.hunks].reverse()) {
|
|
175
|
+
const afterSegment = buildExpectedAfterSegment(hunk.lines);
|
|
176
|
+
const beforeSegment = buildExpectedBeforeSegment(hunk.lines);
|
|
177
|
+
const startIndex = Math.max(0, hunk.newStart - 1);
|
|
178
|
+
const actualAfterSegment = lines.slice(startIndex, startIndex + afterSegment.length);
|
|
179
|
+
if (!segmentsEqual(actualAfterSegment, afterSegment)) {
|
|
180
|
+
throw new Error(`Patch hunk did not match the current workspace content for ${file.path} at line ${hunk.startLine}.`);
|
|
181
|
+
}
|
|
182
|
+
lines.splice(startIndex, afterSegment.length, ...beforeSegment);
|
|
183
|
+
}
|
|
184
|
+
return joinTextSnapshot({
|
|
185
|
+
hasTrailingNewline: snapshot.hasTrailingNewline,
|
|
186
|
+
lines,
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
const reconstructAfterText = ({ beforeText, file, }) => {
|
|
190
|
+
const snapshot = splitTextSnapshot(beforeText);
|
|
191
|
+
const lines = [...snapshot.lines];
|
|
192
|
+
for (const hunk of file.hunks) {
|
|
193
|
+
const beforeSegment = buildExpectedBeforeSegment(hunk.lines);
|
|
194
|
+
const afterSegment = buildExpectedAfterSegment(hunk.lines);
|
|
195
|
+
const startIndex = Math.max(0, hunk.oldStart - 1);
|
|
196
|
+
const actualBeforeSegment = lines.slice(startIndex, startIndex + beforeSegment.length);
|
|
197
|
+
if (!segmentsEqual(actualBeforeSegment, beforeSegment)) {
|
|
198
|
+
throw new Error(`Patch hunk did not match the expected preimage content for ${file.path} at line ${hunk.startLine}.`);
|
|
199
|
+
}
|
|
200
|
+
lines.splice(startIndex, beforeSegment.length, ...afterSegment);
|
|
201
|
+
}
|
|
202
|
+
return joinTextSnapshot({
|
|
203
|
+
hasTrailingNewline: snapshot.hasTrailingNewline,
|
|
204
|
+
lines,
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
const isMissingPathError = (error) => error.code === 'ENOENT';
|
|
208
|
+
const readWorkspacePatchTarget = async ({ workspaceFilePath }) => {
|
|
209
|
+
try {
|
|
210
|
+
return {
|
|
211
|
+
exists: true,
|
|
212
|
+
text: await readFile(workspaceFilePath, 'utf8'),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (!isMissingPathError(error)) {
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
exists: false,
|
|
221
|
+
text: '',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const resolvePatchTextPair = ({ file, workspaceAfterText, workspaceFileExists, }) => {
|
|
226
|
+
if (file.hunks.length === 0) {
|
|
227
|
+
return {
|
|
228
|
+
afterText: workspaceAfterText,
|
|
229
|
+
beforeText: workspaceAfterText,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
return {
|
|
234
|
+
afterText: workspaceAfterText,
|
|
235
|
+
beforeText: reconstructBeforeText({
|
|
236
|
+
afterText: workspaceAfterText,
|
|
237
|
+
file,
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
const synthesizedAfterText = reconstructAfterText({
|
|
243
|
+
beforeText: '',
|
|
244
|
+
file,
|
|
245
|
+
});
|
|
246
|
+
const isNewFilePatch = !workspaceFileExists || workspaceAfterText === synthesizedAfterText;
|
|
247
|
+
if (!isNewFilePatch) {
|
|
248
|
+
throw new Error(`Patch payload derivation expected ${file.path} to match the diff postimage or be absent: ${error instanceof Error ? error.message : String(error)}`);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
afterText: synthesizedAfterText,
|
|
252
|
+
beforeText: '',
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const buildHashMaps = async ({ files, workspaceRoot, }) => {
|
|
257
|
+
const fileSha256After = {};
|
|
258
|
+
const fileSha256Before = {};
|
|
259
|
+
for (const file of files) {
|
|
260
|
+
const workspaceFilePath = path.join(workspaceRoot, file.path);
|
|
261
|
+
const workspacePatchTarget = await readWorkspacePatchTarget({
|
|
262
|
+
workspaceFilePath,
|
|
263
|
+
});
|
|
264
|
+
const { afterText, beforeText } = resolvePatchTextPair({
|
|
265
|
+
file,
|
|
266
|
+
workspaceAfterText: workspacePatchTarget.text,
|
|
267
|
+
workspaceFileExists: workspacePatchTarget.exists,
|
|
268
|
+
});
|
|
269
|
+
fileSha256After[file.path] = afterText.length > 0 ? sha256Hex(afterText) : EMPTY_FILE_SHA256;
|
|
270
|
+
fileSha256Before[file.path] = beforeText.length > 0 ? sha256Hex(beforeText) : EMPTY_FILE_SHA256;
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
fileSha256After,
|
|
274
|
+
fileSha256Before,
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
export const derivePatchPayloadFromDiffText = async ({ patchText, workspaceRoot, }) => {
|
|
278
|
+
const files = parseStructuredPatch(patchText);
|
|
279
|
+
if (files.length === 0) {
|
|
280
|
+
throw new Error('Patch payload derivation requires at least one unified diff file block.');
|
|
281
|
+
}
|
|
282
|
+
const hunks = files.flatMap((file) => file.hunks.map((hunk) => ({
|
|
283
|
+
endLine: hunk.endLine,
|
|
284
|
+
path: file.path,
|
|
285
|
+
startLine: hunk.startLine,
|
|
286
|
+
})));
|
|
287
|
+
const touchedPaths = [...new Set(files.map((file) => file.path))];
|
|
288
|
+
const { fileSha256After, fileSha256Before } = await buildHashMaps({
|
|
289
|
+
files,
|
|
290
|
+
workspaceRoot,
|
|
291
|
+
});
|
|
292
|
+
return {
|
|
293
|
+
diff: patchText,
|
|
294
|
+
diffSha256: sha256Hex(patchText),
|
|
295
|
+
fileSha256After,
|
|
296
|
+
fileSha256Before,
|
|
297
|
+
hunks,
|
|
298
|
+
touchedPaths,
|
|
299
|
+
};
|
|
300
|
+
};
|