ushman-ledger 1.1.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 (66) hide show
  1. package/AGENTS.md +11 -7
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +22 -3
  4. package/dist/blobs.js +3 -3
  5. package/dist/builders.d.ts +43 -0
  6. package/dist/builders.d.ts.map +1 -1
  7. package/dist/builders.js +7 -2
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +222 -41
  10. package/dist/doctor.d.ts.map +1 -1
  11. package/dist/doctor.js +104 -4
  12. package/dist/handle.d.ts +3 -1
  13. package/dist/handle.d.ts.map +1 -1
  14. package/dist/handle.js +19 -1
  15. package/dist/helpers.d.ts +7 -0
  16. package/dist/helpers.d.ts.map +1 -0
  17. package/dist/helpers.js +38 -0
  18. package/dist/index.d.ts +3 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -2
  21. package/dist/list.d.ts +43 -1
  22. package/dist/list.d.ts.map +1 -1
  23. package/dist/note.d.ts +20 -0
  24. package/dist/note.d.ts.map +1 -1
  25. package/dist/note.js +5 -0
  26. package/dist/patch-resolver.d.ts +27 -0
  27. package/dist/patch-resolver.d.ts.map +1 -0
  28. package/dist/patch-resolver.js +184 -0
  29. package/dist/read-index.d.ts +7 -7
  30. package/dist/record.d.ts.map +1 -1
  31. package/dist/record.js +15 -39
  32. package/dist/render/migration-log.d.ts +3 -0
  33. package/dist/render/migration-log.d.ts.map +1 -0
  34. package/dist/render/migration-log.js +72 -0
  35. package/dist/render/retro.d.ts.map +1 -1
  36. package/dist/render/retro.js +40 -21
  37. package/dist/render/workspace-narrative.d.ts +6 -0
  38. package/dist/render/workspace-narrative.d.ts.map +1 -0
  39. package/dist/render/workspace-narrative.js +69 -0
  40. package/dist/schema/entry-core.d.ts +110 -0
  41. package/dist/schema/entry-core.d.ts.map +1 -0
  42. package/dist/schema/entry-core.js +143 -0
  43. package/dist/schema/entry-migrations.d.ts +3 -0
  44. package/dist/schema/entry-migrations.d.ts.map +1 -0
  45. package/dist/schema/entry-migrations.js +48 -0
  46. package/dist/schema/entry-read.d.ts +694 -0
  47. package/dist/schema/entry-read.d.ts.map +1 -0
  48. package/dist/schema/entry-read.js +92 -0
  49. package/dist/schema/entry-write.d.ts +865 -0
  50. package/dist/schema/entry-write.d.ts.map +1 -0
  51. package/dist/schema/entry-write.js +105 -0
  52. package/dist/schema/entry.d.ts +6 -1369
  53. package/dist/schema/entry.d.ts.map +1 -1
  54. package/dist/schema/entry.js +9 -286
  55. package/dist/schema/note.d.ts +1 -1
  56. package/dist/schema/note.d.ts.map +1 -1
  57. package/dist/schema/note.js +12 -1
  58. package/dist/storage/filesystem.d.ts +2 -0
  59. package/dist/storage/filesystem.d.ts.map +1 -1
  60. package/dist/storage/filesystem.js +2 -0
  61. package/dist/storage/lock-reclaimer.d.ts +2 -0
  62. package/dist/storage/lock-reclaimer.d.ts.map +1 -0
  63. package/dist/storage/lock-reclaimer.js +45 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +2 -2
@@ -0,0 +1,184 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import * as v from 'valibot';
4
+ import { resolveBlobPath, storePatchBlob } from "./blobs.js";
5
+ import { WorkspaceRelativePathSchema, } from "./schema/entry.js";
6
+ const readBlobText = async (workspaceRoot, blobSha256) => readFile(resolveBlobPath(workspaceRoot, blobSha256), 'utf8');
7
+ const formatRecordContext = (record) => `${record.kind} "${record.summary}" [${record.phase}]`;
8
+ const normalizeWorkspaceRelativePath = (value) => v.parse(WorkspaceRelativePathSchema, value.replaceAll('\\', '/'));
9
+ const stripGitPathPrefix = (value) => {
10
+ const normalized = value.trim().replace(/^"([^"]*)"$/u, '$1');
11
+ if (normalized === '/dev/null') {
12
+ return null;
13
+ }
14
+ if (normalized.startsWith('a/') || normalized.startsWith('b/')) {
15
+ return normalized.slice(2);
16
+ }
17
+ return normalized;
18
+ };
19
+ const inferHeaderPath = (line) => {
20
+ const match = /^diff --git (?:"a\/([^"]+)"|a\/(\S+)) (?:"b\/([^"]+)"|b\/(\S+))$/u.exec(line.trim());
21
+ if (!match) {
22
+ return null;
23
+ }
24
+ const rightPath = match[3] ?? match[4];
25
+ return rightPath ? stripGitPathPrefix(`b/${rightPath}`) : null;
26
+ };
27
+ const finalizeFileChange = (current, filesChanged) => {
28
+ if (!current) {
29
+ return;
30
+ }
31
+ const resolvedPath = current.plusPath ?? current.minusPath ?? current.headerPath;
32
+ if (!resolvedPath) {
33
+ throw new Error('Unable to derive a workspace-relative path from a diff block.');
34
+ }
35
+ filesChanged.push({
36
+ added: current.added > 0 ? current.added : undefined,
37
+ path: normalizeWorkspaceRelativePath(resolvedPath),
38
+ removed: current.removed > 0 ? current.removed : undefined,
39
+ });
40
+ };
41
+ const createDiffFileAccumulator = (line) => ({
42
+ added: 0,
43
+ headerPath: inferHeaderPath(line),
44
+ minusPath: null,
45
+ plusPath: null,
46
+ removed: 0,
47
+ });
48
+ const applyPatchMetadataLine = (current, line) => {
49
+ if (line.startsWith('+++ ')) {
50
+ return {
51
+ next: {
52
+ ...current,
53
+ plusPath: stripGitPathPrefix(line.slice(4)),
54
+ },
55
+ };
56
+ }
57
+ if (line.startsWith('--- ')) {
58
+ return {
59
+ next: {
60
+ ...current,
61
+ minusPath: stripGitPathPrefix(line.slice(4)),
62
+ },
63
+ };
64
+ }
65
+ if (line.startsWith('@@')) {
66
+ return {
67
+ next: current,
68
+ startHunk: true,
69
+ };
70
+ }
71
+ return {
72
+ next: current,
73
+ };
74
+ };
75
+ const applyPatchHunkLine = (current, line) => {
76
+ if (line.startsWith('+')) {
77
+ return {
78
+ ...current,
79
+ added: current.added + 1,
80
+ };
81
+ }
82
+ if (line.startsWith('-')) {
83
+ return {
84
+ ...current,
85
+ removed: current.removed + 1,
86
+ };
87
+ }
88
+ return current;
89
+ };
90
+ const mergeBlobLink = (links, blobSha256) => ({
91
+ ...links,
92
+ blobs: [...new Set([...(links?.blobs ?? []), blobSha256])],
93
+ });
94
+ const resolvePatchSource = async ({ record, workspaceRoot, }) => {
95
+ if (record.diffPath) {
96
+ const resolvedPath = path.resolve(workspaceRoot, record.diffPath);
97
+ return {
98
+ patchText: await readFile(resolvedPath, 'utf8'),
99
+ source: 'file',
100
+ sourceLabel: resolvedPath,
101
+ };
102
+ }
103
+ if (record.diffText) {
104
+ return {
105
+ patchText: record.diffText,
106
+ source: 'inline',
107
+ sourceLabel: 'inline diff text',
108
+ };
109
+ }
110
+ if (record.diff) {
111
+ try {
112
+ return {
113
+ patchText: await readBlobText(workspaceRoot, record.diff.blobSha256),
114
+ source: 'blob',
115
+ sourceLabel: `blob ${record.diff.blobSha256}`,
116
+ };
117
+ }
118
+ catch (error) {
119
+ if (error.code === 'ENOENT') {
120
+ throw new Error(`Patch blob ${record.diff.blobSha256} was not found for ${formatRecordContext(record)}. Store it first or use diffPath.`);
121
+ }
122
+ throw error;
123
+ }
124
+ }
125
+ throw new Error(`${record.kind} records require diff, diffPath, or diffText.`);
126
+ };
127
+ export const deriveFilesChangedFromPatch = (patchText) => {
128
+ const filesChanged = [];
129
+ let current;
130
+ let insideHunk = false;
131
+ for (const line of patchText.split(/\r?\n/u)) {
132
+ if (line.startsWith('diff --git ')) {
133
+ finalizeFileChange(current, filesChanged);
134
+ current = createDiffFileAccumulator(line);
135
+ insideHunk = false;
136
+ continue;
137
+ }
138
+ if (!current) {
139
+ continue;
140
+ }
141
+ if (insideHunk) {
142
+ if (line.startsWith('@@')) {
143
+ continue;
144
+ }
145
+ current = applyPatchHunkLine(current, line);
146
+ continue;
147
+ }
148
+ const metadataUpdate = applyPatchMetadataLine(current, line);
149
+ current = metadataUpdate.next;
150
+ if (metadataUpdate.startHunk) {
151
+ insideHunk = true;
152
+ continue;
153
+ }
154
+ if (!insideHunk) {
155
+ continue;
156
+ }
157
+ current = applyPatchHunkLine(current, line);
158
+ }
159
+ finalizeFileChange(current, filesChanged);
160
+ return filesChanged;
161
+ };
162
+ const buildResolvedPatchRecord = ({ links, record, storedDiff, }) => {
163
+ const { diffPath: _diffPath, diffText: _diffText, ...rest } = record;
164
+ return {
165
+ ...rest,
166
+ diff: storedDiff,
167
+ links,
168
+ };
169
+ };
170
+ export async function resolvePatchRecord({ record, workspaceRoot, }) {
171
+ const { patchText, sourceLabel } = await resolvePatchSource({
172
+ record,
173
+ workspaceRoot,
174
+ });
175
+ const storedDiff = await storePatchBlob(workspaceRoot, patchText);
176
+ if (record.diff && record.diff.blobSha256 !== storedDiff.blobSha256) {
177
+ throw new Error(`Provided diff blob ${record.diff.blobSha256} from ${sourceLabel} does not match patch text hash ${storedDiff.blobSha256} for ${formatRecordContext(record)}.`);
178
+ }
179
+ return buildResolvedPatchRecord({
180
+ links: mergeBlobLink(record.links, storedDiff.blobSha256),
181
+ record,
182
+ storedDiff,
183
+ });
184
+ }
@@ -3,14 +3,14 @@ import { type LedgerEntry, type LedgerKind, type LedgerPhase } from './schema/en
3
3
  import type { LedgerManifest } from './schema/manifest.ts';
4
4
  declare const ReadIndexEntrySchema: v.ObjectSchema<{
5
5
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
6
- readonly kind: v.PicklistSchema<readonly ["tool-invocation", "agent-patch", "operator-patch", "operator-decision", "validator-result", "runtime-event", "note", "correction", "strip-decision-reverted"], undefined>;
6
+ readonly kind: v.PicklistSchema<readonly ["tool-invocation", "agent-patch", "operator-patch", "operator-decision", "validator-result", "runtime-event", "note", "correction", "strip-decision-reverted", "change-log"], undefined>;
7
7
  readonly ts: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, undefined>]>;
8
8
  }, undefined>;
9
9
  declare const LedgerReadIndexSchema: v.SchemaWithPipe<readonly [v.ObjectSchema<{
10
10
  readonly coveredFiles: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, readonly []>;
11
11
  readonly entries: v.OptionalSchema<v.ArraySchema<v.ObjectSchema<{
12
12
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
13
- readonly kind: v.PicklistSchema<readonly ["tool-invocation", "agent-patch", "operator-patch", "operator-decision", "validator-result", "runtime-event", "note", "correction", "strip-decision-reverted"], undefined>;
13
+ readonly kind: v.PicklistSchema<readonly ["tool-invocation", "agent-patch", "operator-patch", "operator-decision", "validator-result", "runtime-event", "note", "correction", "strip-decision-reverted", "change-log"], undefined>;
14
14
  readonly ts: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, undefined>]>;
15
15
  }, undefined>, undefined>, readonly []>;
16
16
  readonly entryCount: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
@@ -21,7 +21,7 @@ declare const LedgerReadIndexSchema: v.SchemaWithPipe<readonly [v.ObjectSchema<{
21
21
  coveredFiles: string[];
22
22
  entries: {
23
23
  id: string;
24
- kind: "tool-invocation" | "agent-patch" | "operator-patch" | "operator-decision" | "validator-result" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
24
+ kind: "operator-decision" | "validator-result" | "change-log" | "tool-invocation" | "agent-patch" | "operator-patch" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
25
25
  ts: string;
26
26
  }[];
27
27
  entryCount: number;
@@ -32,7 +32,7 @@ declare const LedgerReadIndexSchema: v.SchemaWithPipe<readonly [v.ObjectSchema<{
32
32
  coveredFiles: string[];
33
33
  entries: {
34
34
  id: string;
35
- kind: "tool-invocation" | "agent-patch" | "operator-patch" | "operator-decision" | "validator-result" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
35
+ kind: "operator-decision" | "validator-result" | "change-log" | "tool-invocation" | "agent-patch" | "operator-patch" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
36
36
  ts: string;
37
37
  }[];
38
38
  entryCount: number;
@@ -50,7 +50,7 @@ export declare const buildReadIndexFromManifest: (workspaceRoot: string, manifes
50
50
  coveredFiles: string[];
51
51
  entries: {
52
52
  id: string;
53
- kind: "tool-invocation" | "agent-patch" | "operator-patch" | "operator-decision" | "validator-result" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
53
+ kind: "operator-decision" | "validator-result" | "change-log" | "tool-invocation" | "agent-patch" | "operator-patch" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
54
54
  ts: string;
55
55
  }[];
56
56
  entryCount: number;
@@ -65,7 +65,7 @@ export declare const ensureReadIndexUnderLock: (workspaceRoot: string, manifest:
65
65
  coveredFiles: string[];
66
66
  entries: {
67
67
  id: string;
68
- kind: "tool-invocation" | "agent-patch" | "operator-patch" | "operator-decision" | "validator-result" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
68
+ kind: "operator-decision" | "validator-result" | "change-log" | "tool-invocation" | "agent-patch" | "operator-patch" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
69
69
  ts: string;
70
70
  }[];
71
71
  entryCount: number;
@@ -81,7 +81,7 @@ export declare const appendEntryToReadIndex: ({ entry, readIndex, sequence, }: {
81
81
  coveredFiles: string[];
82
82
  entries: {
83
83
  id: string;
84
- kind: "tool-invocation" | "agent-patch" | "operator-patch" | "operator-decision" | "validator-result" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
84
+ kind: "operator-decision" | "validator-result" | "change-log" | "tool-invocation" | "agent-patch" | "operator-patch" | "runtime-event" | "note" | "correction" | "strip-decision-reverted";
85
85
  ts: string;
86
86
  }[];
87
87
  entryCount: number;
@@ -1 +1 @@
1
- {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAQA,OAAO,EAEH,KAAK,WAAW,EAKnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAU3D,KAAK,qBAAqB,GAAG;IACzB,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChF,QAAQ,CAAC,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpF,CAAC;AAUF,eAAO,MAAM,wBAAwB,GAAI,eAAe,MAAM,EAAE,OAAO,qBAAqB,GAAG,IAAI,SAOlG,CAAC;AAoHF,eAAO,MAAM,YAAY,GACrB,eAAe,MAAM,EACrB,OAAO,OAAO,KACf,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAqF5C,CAAC;AAEF,eAAO,MAAM,aAAa,GACtB,eAAe,MAAM,EACrB,UAAU,cAAc,EACxB,SAAS,MAAM,KAChB,OAAO,CAAC,WAAW,CAMrB,CAAC"}
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AASA,OAAO,EACH,KAAK,WAAW,EAKnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAU3D,KAAK,qBAAqB,GAAG;IACzB,QAAQ,CAAC,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChF,QAAQ,CAAC,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpF,CAAC;AAOF,eAAO,MAAM,wBAAwB,GAAI,eAAe,MAAM,EAAE,OAAO,qBAAqB,GAAG,IAAI,SAOlG,CAAC;AA0EF,eAAO,MAAM,YAAY,GACrB,eAAe,MAAM,EACrB,OAAO,OAAO,KACf,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAsF5C,CAAC;AAEF,eAAO,MAAM,aAAa,GACtB,eAAe,MAAM,EACrB,UAAU,cAAc,EACxB,SAAS,MAAM,KAChB,OAAO,CAAC,WAAW,CAMrB,CAAC"}
package/dist/record.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import * as v from 'valibot';
4
- import { resolveBlobPath, storePatchBlob } from "./blobs.js";
5
4
  import { sha256Hex, stableStringify } from "./json.js";
6
5
  import { updateManifestForEntry } from "./manifest-update.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
10
  import { LedgerEntrySchema, parseLedgerEntry, parseLedgerRecord, } from "./schema/entry.js";
@@ -20,7 +21,6 @@ export const setAppendRecordTestHooks = (workspaceRoot, hooks) => {
20
21
  }
21
22
  appendRecordTestHooks.set(key, hooks);
22
23
  };
23
- const readBlobText = async (workspaceRoot, diff) => readFile(resolveBlobPath(workspaceRoot, diff.blobSha256), 'utf8');
24
24
  const addIdempotencyMetadata = (record) => {
25
25
  if (!record.idempotencyKey) {
26
26
  return record;
@@ -52,47 +52,22 @@ const buildEntryId = (entryWithoutId, sequence) => {
52
52
  const bodyHash = sha256Hex(stableStringify(entryWithoutId));
53
53
  return `${toEntryIdTimestamp(entryWithoutId.ts)}-${sequence.toString().padStart(8, '0')}-${bodyHash.slice(0, 12)}`;
54
54
  };
55
- const loadPatchText = async ({ record, workspaceRoot, }) => {
56
- if (record.diffPath) {
57
- return readFile(path.resolve(record.diffPath), 'utf8');
58
- }
59
- if (record.diffText) {
60
- return record.diffText;
55
+ const normalizeRecord = async ({ record, workspaceRoot, }) => {
56
+ if (record.kind === 'agent-patch') {
57
+ return resolvePatchRecord({ record, workspaceRoot });
61
58
  }
62
- if (record.diff) {
63
- try {
64
- return await readBlobText(workspaceRoot, record.diff);
65
- }
66
- catch (error) {
67
- if (error.code === 'ENOENT') {
68
- throw new Error(`Patch blob ${record.diff.blobSha256} was not found. Store it first or use diffPath.`);
69
- }
70
- throw error;
71
- }
59
+ if (record.kind === 'operator-patch') {
60
+ return resolvePatchRecord({ record, workspaceRoot });
72
61
  }
73
- throw new Error(`${record.kind} records require diff, diffPath, or diffText.`);
62
+ return record;
74
63
  };
75
- const resolvePatchRecord = async ({ record, workspaceRoot, }) => {
76
- const patchText = await loadPatchText({ record, workspaceRoot });
77
- const storedDiff = await storePatchBlob(workspaceRoot, patchText);
78
- if (record.diff && record.diff.blobSha256 !== storedDiff.blobSha256) {
79
- throw new Error(`Provided diff blob ${record.diff.blobSha256} does not match patch text hash ${storedDiff.blobSha256}.`);
64
+ const assertRollbackTargetExists = (record, manifest) => {
65
+ if (record.kind !== 'change-log' || record.subkind !== 'rollback' || !record.rollsBack) {
66
+ return;
80
67
  }
81
- const { diffPath: _diffPath, diffText: _diffText, ...rest } = record;
82
- return {
83
- ...rest,
84
- diff: storedDiff,
85
- links: {
86
- ...record.links,
87
- blobs: [...new Set([...(record.links?.blobs ?? []), storedDiff.blobSha256])],
88
- },
89
- };
90
- };
91
- const normalizeRecord = async ({ record, workspaceRoot, }) => {
92
- if (record.kind === 'agent-patch' || record.kind === 'operator-patch') {
93
- return resolvePatchRecord({ record, workspaceRoot });
68
+ if (!manifest.entryLocations[record.rollsBack]) {
69
+ throw new Error(`change-log rollback target was not found in the ledger: ${record.rollsBack}`);
94
70
  }
95
- return record;
96
71
  };
97
72
  export const appendRecord = async (workspaceRoot, input) => {
98
73
  const parsed = parseLedgerRecord(input);
@@ -105,6 +80,7 @@ export const appendRecord = async (workspaceRoot, input) => {
105
80
  const lock = await acquireLock(paths.manifestLockFile);
106
81
  try {
107
82
  const { manifest, readIndex } = await reconcileLedgerStateUnderLock(workspaceRoot);
83
+ assertRollbackTargetExists(recordWithMetadata, manifest);
108
84
  const manifestFingerprint = fingerprintManifest(manifest);
109
85
  const keyedLogicalHash = parsed.idempotencyKey ? buildLogicalHash(recordWithMetadata) : null;
110
86
  if (keyedLogicalHash) {
@@ -136,7 +112,7 @@ export const appendRecord = async (workspaceRoot, input) => {
136
112
  ...recordWithoutIdempotencyKey,
137
113
  links: recordWithoutIdempotencyKey.links ?? {},
138
114
  prevEntryId: manifest.perPhaseLatest[normalizedRecord.phase] ?? null,
139
- schemaVersion: 'ushman-ledger-entry/v1',
115
+ schemaVersion: LedgerSchemaVersion,
140
116
  ts: new Date().toISOString(),
141
117
  };
142
118
  const entry = v.parse(LedgerEntrySchema, {
@@ -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;AA6GtD,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: [],
@@ -18,6 +19,15 @@ const isToolingEntry = (entry) => (entry.kind === 'note' && entry.subkind === 'a
18
19
  (entry.kind === 'note' && entry.subkind === 'tooling-gap');
19
20
  const isRetroNote = (entry) => entry.kind === 'note' && entry.subkind === 'retro';
20
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));
21
31
  const resolveRenderedAt = (entryCount, lastEntryTimestamp, manifest) => {
22
32
  if (entryCount > 0) {
23
33
  return lastEntryTimestamp ?? 'n/a';
@@ -36,6 +46,32 @@ const normalizeManifestForHash = (manifest) => typeof manifest === 'object' && m
36
46
  const formatValidatorEntry = (entry) => entry.kind === 'validator-result'
37
47
  ? `${entry.ts} [${entry.validator}/${entry.verdict}] ${entry.summary}`
38
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
+ };
39
75
  const collectRetroState = async (entries) => {
40
76
  const buckets = createRetroBuckets();
41
77
  const tools = new Set();
@@ -45,27 +81,7 @@ const collectRetroState = async (entries) => {
45
81
  entryCount += 1;
46
82
  lastEntryTimestamp = entry.ts;
47
83
  tools.add(entry.emitter.tool);
48
- if (isToolEntry(entry)) {
49
- buckets.toolEntries.push(formatEntry(entry));
50
- }
51
- if (isProblemEntry(entry)) {
52
- buckets.problemEntries.push(formatEntry(entry));
53
- }
54
- if (isToolingEntry(entry)) {
55
- buckets.toolingEntries.push(formatEntry(entry));
56
- }
57
- if (isRetroNote(entry)) {
58
- buckets.retroEntries.push(formatEntry(entry));
59
- }
60
- if (entry.kind === 'validator-result') {
61
- buckets.validatorEntries.push(formatValidatorEntry(entry));
62
- }
63
- if (entry.kind === 'correction') {
64
- buckets.correctionEntries.push(formatEntry(entry));
65
- }
66
- if (isOperatorNote(entry)) {
67
- buckets.operatorEntries.push(formatEntry(entry));
68
- }
84
+ bucketRetroEntry(buckets, entry);
69
85
  }
70
86
  return {
71
87
  buckets,
@@ -117,5 +133,8 @@ export const renderRetroMarkdown = async ({ entries, manifest, }) => {
117
133
  '## 10. Triage signals',
118
134
  renderList(buckets.validatorEntries),
119
135
  '',
136
+ '## 11. Dedicated narrative/change-log entries',
137
+ renderList(buckets.dedicatedNarrativeEntries),
138
+ '',
120
139
  ].join('\n');
121
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
+ };