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.
Files changed (80) hide show
  1. package/AGENTS.md +11 -7
  2. package/CHANGELOG.md +8 -12
  3. package/README.md +28 -57
  4. package/dist/archive-journal.d.ts +29 -18
  5. package/dist/archive-journal.d.ts.map +1 -1
  6. package/dist/archive-journal.js +17 -17
  7. package/dist/blobs.js +3 -3
  8. package/dist/builders.d.ts +79 -358
  9. package/dist/builders.d.ts.map +1 -1
  10. package/dist/builders.js +15 -60
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +227 -52
  13. package/dist/doctor.d.ts.map +1 -1
  14. package/dist/doctor.js +104 -4
  15. package/dist/handle.d.ts +4 -2
  16. package/dist/handle.d.ts.map +1 -1
  17. package/dist/handle.js +20 -15
  18. package/dist/helpers.d.ts +7 -0
  19. package/dist/helpers.d.ts.map +1 -0
  20. package/dist/helpers.js +38 -0
  21. package/dist/index.d.ts +4 -5
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -4
  24. package/dist/lab-min.d.ts +7 -7
  25. package/dist/lab-min.d.ts.map +1 -1
  26. package/dist/lab-min.js +7 -9
  27. package/dist/list.d.ts +104 -303
  28. package/dist/list.d.ts.map +1 -1
  29. package/dist/note.d.ts +20 -0
  30. package/dist/note.d.ts.map +1 -1
  31. package/dist/note.js +5 -0
  32. package/dist/patch-resolver.d.ts +27 -0
  33. package/dist/patch-resolver.d.ts.map +1 -0
  34. package/dist/patch-resolver.js +184 -0
  35. package/dist/read-index.d.ts +45 -57
  36. package/dist/read-index.d.ts.map +1 -1
  37. package/dist/read-index.js +16 -34
  38. package/dist/record.d.ts.map +1 -1
  39. package/dist/record.js +19 -130
  40. package/dist/recovery.d.ts +19 -8
  41. package/dist/recovery.d.ts.map +1 -1
  42. package/dist/recovery.js +13 -13
  43. package/dist/render/migration-log.d.ts +3 -0
  44. package/dist/render/migration-log.d.ts.map +1 -0
  45. package/dist/render/migration-log.js +72 -0
  46. package/dist/render/retro.d.ts.map +1 -1
  47. package/dist/render/retro.js +41 -25
  48. package/dist/render/workspace-narrative.d.ts +6 -0
  49. package/dist/render/workspace-narrative.d.ts.map +1 -0
  50. package/dist/render/workspace-narrative.js +69 -0
  51. package/dist/schema/entry-core.d.ts +110 -0
  52. package/dist/schema/entry-core.d.ts.map +1 -0
  53. package/dist/schema/entry-core.js +143 -0
  54. package/dist/schema/entry-migrations.d.ts +3 -0
  55. package/dist/schema/entry-migrations.d.ts.map +1 -0
  56. package/dist/schema/entry-migrations.js +48 -0
  57. package/dist/schema/entry-read.d.ts +694 -0
  58. package/dist/schema/entry-read.d.ts.map +1 -0
  59. package/dist/schema/entry-read.js +92 -0
  60. package/dist/schema/entry-write.d.ts +865 -0
  61. package/dist/schema/entry-write.d.ts.map +1 -0
  62. package/dist/schema/entry-write.js +105 -0
  63. package/dist/schema/entry.d.ts +6 -3295
  64. package/dist/schema/entry.d.ts.map +1 -1
  65. package/dist/schema/entry.js +10 -619
  66. package/dist/schema/manifest.d.ts +28 -41
  67. package/dist/schema/manifest.d.ts.map +1 -1
  68. package/dist/schema/manifest.js +20 -24
  69. package/dist/schema/note.d.ts +3 -9
  70. package/dist/schema/note.d.ts.map +1 -1
  71. package/dist/schema/note.js +13 -2
  72. package/dist/storage/filesystem.d.ts +2 -1
  73. package/dist/storage/filesystem.d.ts.map +1 -1
  74. package/dist/storage/filesystem.js +6 -4
  75. package/dist/storage/lock-reclaimer.d.ts +2 -0
  76. package/dist/storage/lock-reclaimer.d.ts.map +1 -0
  77. package/dist/storage/lock-reclaimer.js +45 -0
  78. package/dist/version.d.ts +1 -1
  79. package/dist/version.js +1 -1
  80. 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 { resolveBlobPath, storePatchBlob } from "./blobs.js";
3
+ import * as v from 'valibot';
4
4
  import { sha256Hex, stableStringify } from "./json.js";
5
5
  import { updateManifestForEntry } from "./manifest-update.js";
6
- import { derivePatchPayloadFromDiffText } from "./patch-metadata.js";
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, OperatorDecisionPayloadSchema, PatchPayloadWriteSchema, parseLedgerEntry, parseLedgerRecord, ValidatorResultPayloadSchema, } from "./schema/entry.js";
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 normalizeValidatorResultRecord = (record) => {
68
- if (record.kind !== 'validator-result') {
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
- catch (error) {
118
- if (error.code === 'ENOENT') {
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 resolvePatchPayload = async ({ patchText, record, workspaceRoot, }) => {
125
- const derivedPayload = await derivePatchPayloadFromDiffText({
126
- patchText,
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
- return {
155
- ...record,
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: 'ushman-ledger-entry/v1',
115
+ schemaVersion: LedgerSchemaVersion,
223
116
  ts: new Date().toISOString(),
224
117
  };
225
- const entry = LedgerEntrySchema.parse({
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 location = manifest.entryLocations[entryId];
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
  };
@@ -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: Record<string, {
25
- phase: "seed" | "capture" | "intake" | "vendor-extract" | "cleanup" | "parity" | "characterize" | "equiv" | "analyze" | "recover" | "ship" | "migration";
26
- sequence: number;
27
- }>;
28
- idempotencyIndex: Record<string, Record<string, string>>;
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: Record<string, number>;
31
- perPhaseLatest: Record<string, string>;
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>;
@@ -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;;;;;;;;;;;;;;;;;;;;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"}
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 { z } from 'zod';
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 = z.object({
14
- baseLastSequence: z.number().int().nonnegative(),
15
- createdAt: z.string().datetime({ offset: true }),
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: z.string().min(1),
18
- schemaVersion: z.literal(PendingCommitSchemaVersion),
19
- sequence: z.number().int().positive(),
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 = z.object({
23
- entryId: z.string().min(1),
24
- sequence: z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
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 PendingCommitFileNameSchema.parse({
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 PendingCommitSchema.parse(JSON.parse(text));
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, PendingCommitSchema.parse({
131
+ await writeAtomicJsonFile(filePath, v.parse(PendingCommitSchema, {
132
132
  baseLastSequence: manifest.lastSequence,
133
133
  createdAt: new Date().toISOString(),
134
134
  entry,
@@ -0,0 +1,3 @@
1
+ import type { LedgerEntry } from '../schema/entry.ts';
2
+ export declare const renderMigrationLogMarkdown: (entries: AsyncIterable<LedgerEntry>) => Promise<string>;
3
+ //# sourceMappingURL=migration-log.d.ts.map
@@ -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;AAgHtD,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,CA6CjB,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"}
@@ -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
- if (isToolEntry(entry)) {
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
+ };