ushman-ledger 0.3.0 → 1.2.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 +11 -7
- package/CHANGELOG.md +8 -12
- package/README.md +28 -57
- package/dist/archive-journal.d.ts +29 -18
- package/dist/archive-journal.d.ts.map +1 -1
- package/dist/archive-journal.js +17 -17
- package/dist/blobs.js +3 -3
- package/dist/builders.d.ts +79 -358
- package/dist/builders.d.ts.map +1 -1
- package/dist/builders.js +15 -60
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +227 -52
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +104 -4
- package/dist/handle.d.ts +4 -2
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +20 -15
- package/dist/helpers.d.ts +7 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +38 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/lab-min.d.ts +7 -7
- package/dist/lab-min.d.ts.map +1 -1
- package/dist/lab-min.js +7 -9
- package/dist/list.d.ts +104 -303
- package/dist/list.d.ts.map +1 -1
- package/dist/note.d.ts +20 -0
- package/dist/note.d.ts.map +1 -1
- package/dist/note.js +5 -0
- package/dist/patch-resolver.d.ts +27 -0
- package/dist/patch-resolver.d.ts.map +1 -0
- package/dist/patch-resolver.js +184 -0
- package/dist/read-index.d.ts +45 -57
- package/dist/read-index.d.ts.map +1 -1
- package/dist/read-index.js +16 -34
- package/dist/record.d.ts.map +1 -1
- package/dist/record.js +19 -130
- package/dist/recovery.d.ts +19 -8
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +13 -13
- package/dist/render/migration-log.d.ts +3 -0
- package/dist/render/migration-log.d.ts.map +1 -0
- package/dist/render/migration-log.js +72 -0
- package/dist/render/retro.d.ts.map +1 -1
- package/dist/render/retro.js +41 -25
- package/dist/render/workspace-narrative.d.ts +6 -0
- package/dist/render/workspace-narrative.d.ts.map +1 -0
- package/dist/render/workspace-narrative.js +69 -0
- package/dist/schema/entry-core.d.ts +110 -0
- package/dist/schema/entry-core.d.ts.map +1 -0
- package/dist/schema/entry-core.js +143 -0
- package/dist/schema/entry-migrations.d.ts +3 -0
- package/dist/schema/entry-migrations.d.ts.map +1 -0
- package/dist/schema/entry-migrations.js +48 -0
- package/dist/schema/entry-read.d.ts +694 -0
- package/dist/schema/entry-read.d.ts.map +1 -0
- package/dist/schema/entry-read.js +92 -0
- package/dist/schema/entry-write.d.ts +865 -0
- package/dist/schema/entry-write.d.ts.map +1 -0
- package/dist/schema/entry-write.js +105 -0
- package/dist/schema/entry.d.ts +6 -3295
- package/dist/schema/entry.d.ts.map +1 -1
- package/dist/schema/entry.js +10 -619
- package/dist/schema/manifest.d.ts +28 -41
- package/dist/schema/manifest.d.ts.map +1 -1
- package/dist/schema/manifest.js +20 -24
- package/dist/schema/note.d.ts +3 -9
- package/dist/schema/note.d.ts.map +1 -1
- package/dist/schema/note.js +13 -2
- package/dist/storage/filesystem.d.ts +2 -1
- package/dist/storage/filesystem.d.ts.map +1 -1
- package/dist/storage/filesystem.js +6 -4
- package/dist/storage/lock-reclaimer.d.ts +2 -0
- package/dist/storage/lock-reclaimer.d.ts.map +1 -0
- package/dist/storage/lock-reclaimer.js +45 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -4
package/dist/record.js
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import * as v from 'valibot';
|
|
4
4
|
import { sha256Hex, stableStringify } from "./json.js";
|
|
5
5
|
import { updateManifestForEntry } from "./manifest-update.js";
|
|
6
|
-
import {
|
|
6
|
+
import { LedgerSchemaVersion } from "./schema/entry.js";
|
|
7
|
+
import { resolvePatchRecord } from "./patch-resolver.js";
|
|
7
8
|
import { appendEntryToReadIndex, saveReadIndex } from "./read-index.js";
|
|
8
9
|
import { reconcileLedgerStateUnderLock, removePendingCommit, writePendingCommit } from "./recovery.js";
|
|
9
|
-
import { LedgerEntrySchema,
|
|
10
|
+
import { LedgerEntrySchema, parseLedgerEntry, parseLedgerRecord, } from "./schema/entry.js";
|
|
10
11
|
import { ensureLedgerDirectories, readManifest, resolveLedgerPaths, saveManifest, writeEntryFile, } from "./storage/filesystem.js";
|
|
11
12
|
import { acquireLock } from "./storage/lock.js";
|
|
12
|
-
import { createDeterministicUuidV7 } from "./uuid.js";
|
|
13
13
|
const appendRecordTestHooks = new Map();
|
|
14
14
|
const resolveWorkspaceKey = (workspaceRoot) => path.resolve(workspaceRoot);
|
|
15
15
|
const getAppendRecordTestHooks = (workspaceRoot) => appendRecordTestHooks.get(resolveWorkspaceKey(workspaceRoot));
|
|
16
|
-
const warnedDeprecations = new Set();
|
|
17
|
-
const warnOnce = (key, message) => {
|
|
18
|
-
if (warnedDeprecations.has(key)) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
warnedDeprecations.add(key);
|
|
22
|
-
process.emitWarning(message, {
|
|
23
|
-
code: key,
|
|
24
|
-
type: 'DeprecationWarning',
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
16
|
export const setAppendRecordTestHooks = (workspaceRoot, hooks) => {
|
|
28
17
|
const key = resolveWorkspaceKey(workspaceRoot);
|
|
29
18
|
if (!hooks) {
|
|
@@ -32,7 +21,6 @@ export const setAppendRecordTestHooks = (workspaceRoot, hooks) => {
|
|
|
32
21
|
}
|
|
33
22
|
appendRecordTestHooks.set(key, hooks);
|
|
34
23
|
};
|
|
35
|
-
const readBlobText = async (workspaceRoot, diff) => readFile(resolveBlobPath(workspaceRoot, diff.blobSha256), 'utf8');
|
|
36
24
|
const addIdempotencyMetadata = (record) => {
|
|
37
25
|
if (!record.idempotencyKey) {
|
|
38
26
|
return record;
|
|
@@ -64,118 +52,22 @@ const buildEntryId = (entryWithoutId, sequence) => {
|
|
|
64
52
|
const bodyHash = sha256Hex(stableStringify(entryWithoutId));
|
|
65
53
|
return `${toEntryIdTimestamp(entryWithoutId.ts)}-${sequence.toString().padStart(8, '0')}-${bodyHash.slice(0, 12)}`;
|
|
66
54
|
};
|
|
67
|
-
const
|
|
68
|
-
if (record.kind
|
|
69
|
-
return record;
|
|
70
|
-
}
|
|
71
|
-
const payloadId = record.payload?.id;
|
|
72
|
-
if (payloadId) {
|
|
73
|
-
return {
|
|
74
|
-
...record,
|
|
75
|
-
payload: ValidatorResultPayloadSchema.parse(record.payload),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
warnOnce('USHMAN_LEDGER_VALIDATOR_ID_COMPAT', 'validator-result records should provide payload.id. A compatibility UUIDv7 was synthesized for this append.');
|
|
79
|
-
return {
|
|
80
|
-
...record,
|
|
81
|
-
payload: {
|
|
82
|
-
id: createDeterministicUuidV7({
|
|
83
|
-
seed: stableStringify(record),
|
|
84
|
-
timestamp: 0,
|
|
85
|
-
}),
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
const resolvePatchTextAndBlob = async ({ record, workspaceRoot, }) => {
|
|
90
|
-
if (record.diffPath) {
|
|
91
|
-
const patchText = await readFile(path.resolve(record.diffPath), 'utf8');
|
|
92
|
-
return {
|
|
93
|
-
patchText,
|
|
94
|
-
storedDiff: await storePatchBlob(workspaceRoot, patchText),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
if (record.payload?.diff) {
|
|
98
|
-
const storedDiff = await storePatchBlob(workspaceRoot, record.payload.diff);
|
|
99
|
-
if (record.diff && record.diff.blobSha256 !== storedDiff.blobSha256) {
|
|
100
|
-
throw new Error(`Provided diff blob ${record.diff.blobSha256} does not match payload.diff hash ${storedDiff.blobSha256}.`);
|
|
101
|
-
}
|
|
102
|
-
return {
|
|
103
|
-
patchText: record.payload.diff,
|
|
104
|
-
storedDiff,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
if (!record.diff) {
|
|
108
|
-
throw new Error(`${record.kind} records require payload.diff, diffPath, or diff metadata.`);
|
|
109
|
-
}
|
|
110
|
-
try {
|
|
111
|
-
const patchText = await readBlobText(workspaceRoot, record.diff);
|
|
112
|
-
return {
|
|
113
|
-
patchText,
|
|
114
|
-
storedDiff: record.diff,
|
|
115
|
-
};
|
|
55
|
+
const normalizeRecord = async ({ record, workspaceRoot, }) => {
|
|
56
|
+
if (record.kind === 'agent-patch') {
|
|
57
|
+
return resolvePatchRecord({ record, workspaceRoot });
|
|
116
58
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
throw new Error(`Patch blob ${record.diff.blobSha256} was not found. Store it first or use diffPath.`);
|
|
120
|
-
}
|
|
121
|
-
throw error;
|
|
59
|
+
if (record.kind === 'operator-patch') {
|
|
60
|
+
return resolvePatchRecord({ record, workspaceRoot });
|
|
122
61
|
}
|
|
62
|
+
return record;
|
|
123
63
|
};
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
workspaceRoot,
|
|
128
|
-
});
|
|
129
|
-
const mergedPayload = PatchPayloadWriteSchema.parse({
|
|
130
|
-
...derivedPayload,
|
|
131
|
-
...record.payload,
|
|
132
|
-
diff: record.payload?.diff ?? patchText,
|
|
133
|
-
diffSha256: record.payload?.diffSha256 ?? derivedPayload.diffSha256,
|
|
134
|
-
fileSha256After: record.payload?.fileSha256After ?? derivedPayload.fileSha256After,
|
|
135
|
-
fileSha256Before: record.payload?.fileSha256Before ?? derivedPayload.fileSha256Before,
|
|
136
|
-
hunks: record.payload?.hunks ?? derivedPayload.hunks,
|
|
137
|
-
touchedPaths: record.payload?.touchedPaths ?? derivedPayload.touchedPaths,
|
|
138
|
-
});
|
|
139
|
-
return mergedPayload;
|
|
140
|
-
};
|
|
141
|
-
const resolvePatchRecord = async ({ record, workspaceRoot, }) => {
|
|
142
|
-
const { patchText, storedDiff } = await resolvePatchTextAndBlob({
|
|
143
|
-
record,
|
|
144
|
-
workspaceRoot,
|
|
145
|
-
});
|
|
146
|
-
const payload = await resolvePatchPayload({
|
|
147
|
-
patchText,
|
|
148
|
-
record,
|
|
149
|
-
workspaceRoot,
|
|
150
|
-
});
|
|
151
|
-
if (payload.diffSha256 !== storedDiff.blobSha256) {
|
|
152
|
-
throw new Error(`Patch payload hash ${payload.diffSha256} does not match stored blob hash ${storedDiff.blobSha256}.`);
|
|
64
|
+
const assertRollbackTargetExists = (record, manifest) => {
|
|
65
|
+
if (record.kind !== 'change-log' || record.subkind !== 'rollback' || !record.rollsBack) {
|
|
66
|
+
return;
|
|
153
67
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
diff: storedDiff,
|
|
157
|
-
links: {
|
|
158
|
-
...record.links,
|
|
159
|
-
affectedFiles: [...new Set([...(record.links?.affectedFiles ?? []), ...payload.touchedPaths])],
|
|
160
|
-
blobs: [...new Set([...(record.links?.blobs ?? []), storedDiff.blobSha256])],
|
|
161
|
-
},
|
|
162
|
-
payload,
|
|
163
|
-
};
|
|
164
|
-
};
|
|
165
|
-
const normalizeRecord = async ({ record, workspaceRoot, }) => {
|
|
166
|
-
const normalizedRecord = normalizeValidatorResultRecord(record);
|
|
167
|
-
if (normalizedRecord.kind === 'agent-patch' || normalizedRecord.kind === 'operator-patch') {
|
|
168
|
-
return resolvePatchRecord({
|
|
169
|
-
record: normalizedRecord,
|
|
170
|
-
workspaceRoot,
|
|
171
|
-
});
|
|
68
|
+
if (!manifest.entryLocations[record.rollsBack]) {
|
|
69
|
+
throw new Error(`change-log rollback target was not found in the ledger: ${record.rollsBack}`);
|
|
172
70
|
}
|
|
173
|
-
return normalizedRecord.kind === 'operator-decision'
|
|
174
|
-
? {
|
|
175
|
-
...normalizedRecord,
|
|
176
|
-
payload: OperatorDecisionPayloadSchema.parse(normalizedRecord.payload),
|
|
177
|
-
}
|
|
178
|
-
: normalizedRecord;
|
|
179
71
|
};
|
|
180
72
|
export const appendRecord = async (workspaceRoot, input) => {
|
|
181
73
|
const parsed = parseLedgerRecord(input);
|
|
@@ -188,6 +80,7 @@ export const appendRecord = async (workspaceRoot, input) => {
|
|
|
188
80
|
const lock = await acquireLock(paths.manifestLockFile);
|
|
189
81
|
try {
|
|
190
82
|
const { manifest, readIndex } = await reconcileLedgerStateUnderLock(workspaceRoot);
|
|
83
|
+
assertRollbackTargetExists(recordWithMetadata, manifest);
|
|
191
84
|
const manifestFingerprint = fingerprintManifest(manifest);
|
|
192
85
|
const keyedLogicalHash = parsed.idempotencyKey ? buildLogicalHash(recordWithMetadata) : null;
|
|
193
86
|
if (keyedLogicalHash) {
|
|
@@ -219,10 +112,10 @@ export const appendRecord = async (workspaceRoot, input) => {
|
|
|
219
112
|
...recordWithoutIdempotencyKey,
|
|
220
113
|
links: recordWithoutIdempotencyKey.links ?? {},
|
|
221
114
|
prevEntryId: manifest.perPhaseLatest[normalizedRecord.phase] ?? null,
|
|
222
|
-
schemaVersion:
|
|
115
|
+
schemaVersion: LedgerSchemaVersion,
|
|
223
116
|
ts: new Date().toISOString(),
|
|
224
117
|
};
|
|
225
|
-
const entry =
|
|
118
|
+
const entry = v.parse(LedgerEntrySchema, {
|
|
226
119
|
...entryWithoutId,
|
|
227
120
|
id: buildEntryId(entryWithoutId, nextSequence),
|
|
228
121
|
});
|
|
@@ -259,10 +152,6 @@ export const appendRecord = async (workspaceRoot, input) => {
|
|
|
259
152
|
}
|
|
260
153
|
};
|
|
261
154
|
export const readEntryById = async (workspaceRoot, manifest, entryId) => {
|
|
262
|
-
const
|
|
263
|
-
if (!location) {
|
|
264
|
-
throw new Error(`Ledger entry not found: ${entryId}`);
|
|
265
|
-
}
|
|
266
|
-
const text = await readFile(path.join(resolveLedgerPaths(workspaceRoot).phaseDir(location.phase), `${entryId}.json`), 'utf8');
|
|
155
|
+
const text = await readFile(path.join(resolveLedgerPaths(workspaceRoot).phaseDir(manifest.entryLocations[entryId].phase), `${entryId}.json`), 'utf8');
|
|
267
156
|
return parseLedgerEntry(JSON.parse(text));
|
|
268
157
|
};
|
package/dist/recovery.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ export declare const writePendingCommit: ({ entry, logicalHash, manifest, worksp
|
|
|
13
13
|
}) => Promise<string>;
|
|
14
14
|
export declare const removePendingCommit: (filePath: string) => Promise<void>;
|
|
15
15
|
export declare const reconcilePendingCommitsUnderLock: (workspaceRoot: string) => Promise<{
|
|
16
|
-
[x: string]: unknown;
|
|
17
16
|
archives: {
|
|
18
17
|
createdAt: string;
|
|
19
18
|
integrityHash: string;
|
|
@@ -21,17 +20,29 @@ export declare const reconcilePendingCommitsUnderLock: (workspaceRoot: string) =
|
|
|
21
20
|
}[];
|
|
22
21
|
createdAt: string;
|
|
23
22
|
entryCount: number;
|
|
24
|
-
entryLocations:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
entryLocations: {
|
|
24
|
+
[x: string]: {
|
|
25
|
+
phase: "capture" | "intake" | "seed" | "vendor-extract" | "cleanup" | "parity" | "characterize" | "equiv" | "analyze" | "recover" | "ship" | "migration";
|
|
26
|
+
sequence: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
idempotencyIndex: {
|
|
30
|
+
[x: string]: {
|
|
31
|
+
[x: string]: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
29
34
|
lastSequence: number;
|
|
30
|
-
perPhaseCounts:
|
|
31
|
-
|
|
35
|
+
perPhaseCounts: {
|
|
36
|
+
[x: string]: number;
|
|
37
|
+
};
|
|
38
|
+
perPhaseLatest: {
|
|
39
|
+
[x: string]: string;
|
|
40
|
+
};
|
|
32
41
|
schemaVersion: "ushman-ledger-manifest/v1";
|
|
33
42
|
updatedAt: string;
|
|
34
43
|
workspaceId: string;
|
|
44
|
+
} & {
|
|
45
|
+
[key: string]: unknown;
|
|
35
46
|
}>;
|
|
36
47
|
export declare const reconcileLedgerStateUnderLock: (workspaceRoot: string) => Promise<PreparedLedgerState>;
|
|
37
48
|
export declare const loadLedgerState: (workspaceRoot: string) => Promise<PreparedLedgerState>;
|
package/dist/recovery.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"AAKA,OAAO,EAA4B,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,mBAAmB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA0B3D,MAAM,MAAM,mBAAmB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;CACvC,CAAC;AA0JF,eAAO,MAAM,kBAAkB,GAAU,kDAKtC;IACC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,oBAeA,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAU,UAAU,MAAM,kBAEzD,CAAC;AAEF,eAAO,MAAM,gCAAgC,GAAU,eAAe,MAAM
|
|
1
|
+
{"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"AAKA,OAAO,EAA4B,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,mBAAmB,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA0B3D,MAAM,MAAM,mBAAmB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;CACvC,CAAC;AA0JF,eAAO,MAAM,kBAAkB,GAAU,kDAKtC;IACC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,oBAeA,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAU,UAAU,MAAM,kBAEzD,CAAC;AAEF,eAAO,MAAM,gCAAgC,GAAU,eAAe,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyB3E,CAAC;AAEF,eAAO,MAAM,6BAA6B,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,mBAAmB,CAQtG,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,mBAAmB,CAQxF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAU,eAAe,MAAM,kBAE7D,CAAC"}
|
package/dist/recovery.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import * as v from 'valibot';
|
|
4
4
|
import { reconcilePendingArchivesUnderLock } from "./archive-journal.js";
|
|
5
5
|
import { getNextManifestSequence, updateManifestForEntry } from "./manifest-update.js";
|
|
6
6
|
import { ensureReadIndexUnderLock } from "./read-index.js";
|
|
@@ -10,18 +10,18 @@ import { acquireLock } from "./storage/lock.js";
|
|
|
10
10
|
const PendingCommitSchemaVersion = 'ushman-ledger-pending-commit/v1';
|
|
11
11
|
// Pending commits capture the intended sequence and logical hash before the
|
|
12
12
|
// entry file and manifest update diverge, so recovery can replay safely.
|
|
13
|
-
const PendingCommitSchema =
|
|
14
|
-
baseLastSequence:
|
|
15
|
-
createdAt:
|
|
13
|
+
const PendingCommitSchema = v.object({
|
|
14
|
+
baseLastSequence: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
15
|
+
createdAt: v.pipe(v.string(), v.isoTimestamp()),
|
|
16
16
|
entry: LedgerEntrySchema,
|
|
17
|
-
logicalHash:
|
|
18
|
-
schemaVersion:
|
|
19
|
-
sequence:
|
|
17
|
+
logicalHash: v.pipe(v.string(), v.minLength(1)),
|
|
18
|
+
schemaVersion: v.literal(PendingCommitSchemaVersion),
|
|
19
|
+
sequence: v.pipe(v.number(), v.integer(), v.minValue(1)),
|
|
20
20
|
});
|
|
21
21
|
const formatSequence = (sequence) => sequence.toString().padStart(8, '0');
|
|
22
|
-
const PendingCommitFileNameSchema =
|
|
23
|
-
entryId:
|
|
24
|
-
sequence:
|
|
22
|
+
const PendingCommitFileNameSchema = v.object({
|
|
23
|
+
entryId: v.pipe(v.string(), v.minLength(1)),
|
|
24
|
+
sequence: v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(Number.MAX_SAFE_INTEGER)),
|
|
25
25
|
});
|
|
26
26
|
const buildPendingCommitFilePath = (workspaceRoot, sequence, entryId) => path.join(resolveLedgerPaths(workspaceRoot).pendingCommitsDir, `${formatSequence(sequence)}-${entryId}.json`);
|
|
27
27
|
const parsePendingCommitFileName = (name) => {
|
|
@@ -29,7 +29,7 @@ const parsePendingCommitFileName = (name) => {
|
|
|
29
29
|
if (!match) {
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
|
-
return
|
|
32
|
+
return v.parse(PendingCommitFileNameSchema, {
|
|
33
33
|
entryId: match[2],
|
|
34
34
|
sequence: Number.parseInt(match[1], 10),
|
|
35
35
|
});
|
|
@@ -37,7 +37,7 @@ const parsePendingCommitFileName = (name) => {
|
|
|
37
37
|
const readPendingCommit = async (filePath) => {
|
|
38
38
|
try {
|
|
39
39
|
const text = await readFile(filePath, 'utf8');
|
|
40
|
-
return
|
|
40
|
+
return v.parse(PendingCommitSchema, JSON.parse(text));
|
|
41
41
|
}
|
|
42
42
|
catch (error) {
|
|
43
43
|
throw new Error(`Invalid pending commit at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -128,7 +128,7 @@ const applyPendingCommit = async ({ manifest, pending, workspaceRoot, }) => {
|
|
|
128
128
|
export const writePendingCommit = async ({ entry, logicalHash, manifest, workspaceRoot, }) => {
|
|
129
129
|
const nextSequence = getNextManifestSequence(manifest.lastSequence);
|
|
130
130
|
const filePath = buildPendingCommitFilePath(workspaceRoot, nextSequence, entry.id);
|
|
131
|
-
await writeAtomicJsonFile(filePath,
|
|
131
|
+
await writeAtomicJsonFile(filePath, v.parse(PendingCommitSchema, {
|
|
132
132
|
baseLastSequence: manifest.lastSequence,
|
|
133
133
|
createdAt: new Date().toISOString(),
|
|
134
134
|
entry,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-log.d.ts","sourceRoot":"","sources":["../../src/render/migration-log.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAuC,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA2C3F,eAAO,MAAM,0BAA0B,GAAU,SAAS,aAAa,CAAC,WAAW,CAAC,KAAG,OAAO,CAAC,MAAM,CAwCpG,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const getFence = (lines) => {
|
|
2
|
+
const longestBacktickRun = lines.reduce((longest, line) => {
|
|
3
|
+
const matches = line.match(/`+/gu) ?? [];
|
|
4
|
+
return Math.max(longest, ...matches.map((match) => match.length), 3);
|
|
5
|
+
}, 3);
|
|
6
|
+
return '`'.repeat(longestBacktickRun + 1);
|
|
7
|
+
};
|
|
8
|
+
const toCodeBlock = (commandsRun) => {
|
|
9
|
+
const lines = commandsRun && commandsRun.length > 0 ? commandsRun : ['# None recorded'];
|
|
10
|
+
const fence = getFence(lines);
|
|
11
|
+
return [fence + 'bash', ...lines, fence].join('\n');
|
|
12
|
+
};
|
|
13
|
+
const renderFilesChanged = (filesChanged) => filesChanged.length === 0
|
|
14
|
+
? '- None recorded.'
|
|
15
|
+
: filesChanged
|
|
16
|
+
.map((fileChange) => {
|
|
17
|
+
const stats = [`+${fileChange.added ?? 0}`, `-${fileChange.removed ?? 0}`].join(' ');
|
|
18
|
+
return `- \`${fileChange.path}\` (${stats})`;
|
|
19
|
+
})
|
|
20
|
+
.join('\n');
|
|
21
|
+
const renderOptionalLine = (label, value) => `**${label}**: ${value ?? 'Not recorded.'}`;
|
|
22
|
+
const renderSmokeLine = (entry) => {
|
|
23
|
+
if (entry.smokeResult && entry.smokeNotes) {
|
|
24
|
+
return `**Smoke result**: ${entry.smokeResult} - ${entry.smokeNotes}`;
|
|
25
|
+
}
|
|
26
|
+
if (entry.smokeResult) {
|
|
27
|
+
return `**Smoke result**: ${entry.smokeResult}`;
|
|
28
|
+
}
|
|
29
|
+
if (entry.smokeNotes) {
|
|
30
|
+
return ['**Smoke result**: Not recorded.', '', `**Smoke notes**: ${entry.smokeNotes}`].join('\n');
|
|
31
|
+
}
|
|
32
|
+
return '**Smoke result**: Not recorded.';
|
|
33
|
+
};
|
|
34
|
+
const isChangeLogEntry = (entry) => entry.kind === 'change-log';
|
|
35
|
+
export const renderMigrationLogMarkdown = async (entries) => {
|
|
36
|
+
const changeLogEntries = [];
|
|
37
|
+
for await (const entry of entries) {
|
|
38
|
+
if (isChangeLogEntry(entry)) {
|
|
39
|
+
changeLogEntries.push(entry);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
changeLogEntries.reverse();
|
|
43
|
+
if (changeLogEntries.length === 0) {
|
|
44
|
+
return ['# Ushman Ledger Migration Log', '', 'No change-log entries recorded.'].join('\n');
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
'# Ushman Ledger Migration Log',
|
|
48
|
+
'',
|
|
49
|
+
...changeLogEntries.flatMap((entry) => [
|
|
50
|
+
`## ${entry.ts} - ${entry.summary}`,
|
|
51
|
+
'',
|
|
52
|
+
`**Type**: ${entry.subkind}`,
|
|
53
|
+
'',
|
|
54
|
+
'**Files changed**:',
|
|
55
|
+
renderFilesChanged(entry.filesChanged),
|
|
56
|
+
'',
|
|
57
|
+
renderOptionalLine('Hypothesis', entry.hypothesis),
|
|
58
|
+
'',
|
|
59
|
+
'**Commands run**:',
|
|
60
|
+
toCodeBlock(entry.commandsRun),
|
|
61
|
+
'',
|
|
62
|
+
renderSmokeLine(entry),
|
|
63
|
+
'',
|
|
64
|
+
renderOptionalLine('Parity status', entry.parityStatus),
|
|
65
|
+
'',
|
|
66
|
+
renderOptionalLine('Rollback plan', entry.rollbackPlan),
|
|
67
|
+
'',
|
|
68
|
+
'---',
|
|
69
|
+
'',
|
|
70
|
+
]),
|
|
71
|
+
].join('\n');
|
|
72
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retro.d.ts","sourceRoot":"","sources":["../../src/render/retro.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"retro.d.ts","sourceRoot":"","sources":["../../src/render/retro.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAgItD,eAAO,MAAM,mBAAmB,GAAU,wBAGvC;IACC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAC7C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC9B,KAAG,OAAO,CAAC,MAAM,CAgDjB,CAAC"}
|
package/dist/render/retro.js
CHANGED
|
@@ -4,6 +4,7 @@ const toSingleLine = (value) => value.replaceAll(/\s+/gu, ' ').trim();
|
|
|
4
4
|
const formatEntry = (entry) => `${entry.ts} [${entry.phase}] ${toSingleLine(entry.summary)}`;
|
|
5
5
|
const createRetroBuckets = () => ({
|
|
6
6
|
correctionEntries: [],
|
|
7
|
+
dedicatedNarrativeEntries: [],
|
|
7
8
|
operatorEntries: [],
|
|
8
9
|
problemEntries: [],
|
|
9
10
|
retroEntries: [],
|
|
@@ -13,14 +14,20 @@ const createRetroBuckets = () => ({
|
|
|
13
14
|
});
|
|
14
15
|
const isProblemEntry = (entry) => (entry.kind === 'note' && entry.subkind === 'regression') ||
|
|
15
16
|
(entry.kind === 'runtime-event' && entry.level === 'error');
|
|
16
|
-
const isToolEntry = (entry) => entry.kind === 'tool-invocation' ||
|
|
17
|
-
entry.kind === 'agent-patch' ||
|
|
18
|
-
entry.kind === 'operator-patch' ||
|
|
19
|
-
entry.kind === 'stage-transition';
|
|
17
|
+
const isToolEntry = (entry) => entry.kind === 'tool-invocation' || entry.kind === 'agent-patch' || entry.kind === 'operator-patch';
|
|
20
18
|
const isToolingEntry = (entry) => (entry.kind === 'note' && entry.subkind === 'automation') ||
|
|
21
19
|
(entry.kind === 'note' && entry.subkind === 'tooling-gap');
|
|
22
20
|
const isRetroNote = (entry) => entry.kind === 'note' && entry.subkind === 'retro';
|
|
23
21
|
const isOperatorNote = (entry) => entry.kind === 'note' && entry.subkind === 'operator';
|
|
22
|
+
const isDedicatedNarrativeEntry = (entry) => entry.kind === 'change-log' ||
|
|
23
|
+
(entry.kind === 'note' &&
|
|
24
|
+
[
|
|
25
|
+
'cleanup-wave',
|
|
26
|
+
'verified-flow',
|
|
27
|
+
'open-issue',
|
|
28
|
+
'decomposition-wave',
|
|
29
|
+
'semantic-cleanup-summary',
|
|
30
|
+
].includes(entry.subkind));
|
|
24
31
|
const resolveRenderedAt = (entryCount, lastEntryTimestamp, manifest) => {
|
|
25
32
|
if (entryCount > 0) {
|
|
26
33
|
return lastEntryTimestamp ?? 'n/a';
|
|
@@ -39,6 +46,32 @@ const normalizeManifestForHash = (manifest) => typeof manifest === 'object' && m
|
|
|
39
46
|
const formatValidatorEntry = (entry) => entry.kind === 'validator-result'
|
|
40
47
|
? `${entry.ts} [${entry.validator}/${entry.verdict}] ${entry.summary}`
|
|
41
48
|
: formatEntry(entry);
|
|
49
|
+
const bucketRetroEntry = (buckets, entry) => {
|
|
50
|
+
if (isToolEntry(entry)) {
|
|
51
|
+
buckets.toolEntries.push(formatEntry(entry));
|
|
52
|
+
}
|
|
53
|
+
if (isProblemEntry(entry)) {
|
|
54
|
+
buckets.problemEntries.push(formatEntry(entry));
|
|
55
|
+
}
|
|
56
|
+
if (isToolingEntry(entry)) {
|
|
57
|
+
buckets.toolingEntries.push(formatEntry(entry));
|
|
58
|
+
}
|
|
59
|
+
if (isRetroNote(entry)) {
|
|
60
|
+
buckets.retroEntries.push(formatEntry(entry));
|
|
61
|
+
}
|
|
62
|
+
if (entry.kind === 'validator-result') {
|
|
63
|
+
buckets.validatorEntries.push(formatValidatorEntry(entry));
|
|
64
|
+
}
|
|
65
|
+
if (entry.kind === 'correction') {
|
|
66
|
+
buckets.correctionEntries.push(formatEntry(entry));
|
|
67
|
+
}
|
|
68
|
+
if (isOperatorNote(entry)) {
|
|
69
|
+
buckets.operatorEntries.push(formatEntry(entry));
|
|
70
|
+
}
|
|
71
|
+
if (isDedicatedNarrativeEntry(entry)) {
|
|
72
|
+
buckets.dedicatedNarrativeEntries.push(formatEntry(entry));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
42
75
|
const collectRetroState = async (entries) => {
|
|
43
76
|
const buckets = createRetroBuckets();
|
|
44
77
|
const tools = new Set();
|
|
@@ -48,27 +81,7 @@ const collectRetroState = async (entries) => {
|
|
|
48
81
|
entryCount += 1;
|
|
49
82
|
lastEntryTimestamp = entry.ts;
|
|
50
83
|
tools.add(entry.emitter.tool);
|
|
51
|
-
|
|
52
|
-
buckets.toolEntries.push(formatEntry(entry));
|
|
53
|
-
}
|
|
54
|
-
if (isProblemEntry(entry)) {
|
|
55
|
-
buckets.problemEntries.push(formatEntry(entry));
|
|
56
|
-
}
|
|
57
|
-
if (isToolingEntry(entry)) {
|
|
58
|
-
buckets.toolingEntries.push(formatEntry(entry));
|
|
59
|
-
}
|
|
60
|
-
if (isRetroNote(entry)) {
|
|
61
|
-
buckets.retroEntries.push(formatEntry(entry));
|
|
62
|
-
}
|
|
63
|
-
if (entry.kind === 'validator-result') {
|
|
64
|
-
buckets.validatorEntries.push(formatValidatorEntry(entry));
|
|
65
|
-
}
|
|
66
|
-
if (entry.kind === 'correction') {
|
|
67
|
-
buckets.correctionEntries.push(formatEntry(entry));
|
|
68
|
-
}
|
|
69
|
-
if (isOperatorNote(entry)) {
|
|
70
|
-
buckets.operatorEntries.push(formatEntry(entry));
|
|
71
|
-
}
|
|
84
|
+
bucketRetroEntry(buckets, entry);
|
|
72
85
|
}
|
|
73
86
|
return {
|
|
74
87
|
buckets,
|
|
@@ -120,5 +133,8 @@ export const renderRetroMarkdown = async ({ entries, manifest, }) => {
|
|
|
120
133
|
'## 10. Triage signals',
|
|
121
134
|
renderList(buckets.validatorEntries),
|
|
122
135
|
'',
|
|
136
|
+
'## 11. Dedicated narrative/change-log entries',
|
|
137
|
+
renderList(buckets.dedicatedNarrativeEntries),
|
|
138
|
+
'',
|
|
123
139
|
].join('\n');
|
|
124
140
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { LedgerEntry } from '../schema/entry.ts';
|
|
2
|
+
export declare const renderWorkspaceNarrativeMarkdown: ({ entries, workspaceName, }: {
|
|
3
|
+
readonly entries: AsyncIterable<LedgerEntry>;
|
|
4
|
+
readonly workspaceName: string;
|
|
5
|
+
}) => Promise<string>;
|
|
6
|
+
//# sourceMappingURL=workspace-narrative.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-narrative.d.ts","sourceRoot":"","sources":["../../src/render/workspace-narrative.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,oBAAoB,CAAC;AAsCjE,eAAO,MAAM,gCAAgC,GAAU,6BAGpD;IACC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAC7C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,KAAG,OAAO,CAAC,MAAM,CA+CjB,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const SECTION_TITLES = {
|
|
2
|
+
cleanupWave: '## Cleanup waves (newest first)',
|
|
3
|
+
decompositionWave: '## Decomposition waves',
|
|
4
|
+
openIssue: '## Open issues',
|
|
5
|
+
semanticCleanupSummary: '## Semantic cleanup summary',
|
|
6
|
+
semanticCleanupSummaryArchive: '## Archive — prior semantic-cleanup summaries',
|
|
7
|
+
verifiedFlow: '## Verified flows',
|
|
8
|
+
};
|
|
9
|
+
const NARRATIVE_SUBKINDS = [
|
|
10
|
+
'cleanup-wave',
|
|
11
|
+
'verified-flow',
|
|
12
|
+
'open-issue',
|
|
13
|
+
'decomposition-wave',
|
|
14
|
+
'semantic-cleanup-summary',
|
|
15
|
+
];
|
|
16
|
+
const isNarrativeNote = (entry) => entry.kind === 'note' && NARRATIVE_SUBKINDS.some((subkind) => subkind === entry.subkind);
|
|
17
|
+
const byNewestFirst = (left, right) => right.ts.localeCompare(left.ts);
|
|
18
|
+
const renderBulletSection = (entries) => entries.length === 0
|
|
19
|
+
? '- None recorded.'
|
|
20
|
+
: entries.map((entry) => `- ${entry.ts} — ${entry.summary}`).join('\n');
|
|
21
|
+
const renderLatestSummary = (entries) => {
|
|
22
|
+
const [latest] = entries;
|
|
23
|
+
if (!latest) {
|
|
24
|
+
return 'No entries recorded.';
|
|
25
|
+
}
|
|
26
|
+
return [`### ${latest.ts} — ${latest.summary}`, '', latest.body || 'No narrative body recorded.'].join('\n');
|
|
27
|
+
};
|
|
28
|
+
export const renderWorkspaceNarrativeMarkdown = async ({ entries, workspaceName, }) => {
|
|
29
|
+
const entriesBySubkind = new Map(NARRATIVE_SUBKINDS.map((subkind) => [subkind, []]));
|
|
30
|
+
for await (const entry of entries) {
|
|
31
|
+
if (!isNarrativeNote(entry)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
entriesBySubkind.get(entry.subkind)?.push(entry);
|
|
35
|
+
}
|
|
36
|
+
for (const noteEntries of entriesBySubkind.values()) {
|
|
37
|
+
noteEntries.sort(byNewestFirst);
|
|
38
|
+
}
|
|
39
|
+
const semanticCleanupSummaries = entriesBySubkind.get('semantic-cleanup-summary') ?? [];
|
|
40
|
+
const [, ...archivedSummaries] = semanticCleanupSummaries;
|
|
41
|
+
return [
|
|
42
|
+
`# Workspace narrative — ${workspaceName}`,
|
|
43
|
+
'',
|
|
44
|
+
SECTION_TITLES.semanticCleanupSummary,
|
|
45
|
+
'',
|
|
46
|
+
renderLatestSummary(semanticCleanupSummaries),
|
|
47
|
+
'',
|
|
48
|
+
SECTION_TITLES.cleanupWave,
|
|
49
|
+
'',
|
|
50
|
+
renderBulletSection(entriesBySubkind.get('cleanup-wave') ?? []),
|
|
51
|
+
'',
|
|
52
|
+
SECTION_TITLES.verifiedFlow,
|
|
53
|
+
'',
|
|
54
|
+
renderBulletSection(entriesBySubkind.get('verified-flow') ?? []),
|
|
55
|
+
'',
|
|
56
|
+
SECTION_TITLES.openIssue,
|
|
57
|
+
'',
|
|
58
|
+
renderBulletSection(entriesBySubkind.get('open-issue') ?? []),
|
|
59
|
+
'',
|
|
60
|
+
SECTION_TITLES.decompositionWave,
|
|
61
|
+
'',
|
|
62
|
+
renderBulletSection(entriesBySubkind.get('decomposition-wave') ?? []),
|
|
63
|
+
'',
|
|
64
|
+
SECTION_TITLES.semanticCleanupSummaryArchive,
|
|
65
|
+
'',
|
|
66
|
+
renderBulletSection(archivedSummaries),
|
|
67
|
+
'',
|
|
68
|
+
].join('\n');
|
|
69
|
+
};
|