ushman-ledger 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/AGENTS.md +41 -0
  2. package/CHANGELOG.md +23 -0
  3. package/LICENSE.md +21 -0
  4. package/README.md +233 -0
  5. package/dist/archive-journal.d.ts +63 -0
  6. package/dist/archive-journal.d.ts.map +1 -0
  7. package/dist/archive-journal.js +220 -0
  8. package/dist/archive.d.ts +30 -0
  9. package/dist/archive.d.ts.map +1 -0
  10. package/dist/archive.js +117 -0
  11. package/dist/async.d.ts +2 -0
  12. package/dist/async.d.ts.map +1 -0
  13. package/dist/async.js +20 -0
  14. package/dist/blobs.d.ts +10 -0
  15. package/dist/blobs.d.ts.map +1 -0
  16. package/dist/blobs.js +58 -0
  17. package/dist/builders.d.ts +465 -0
  18. package/dist/builders.d.ts.map +1 -0
  19. package/dist/builders.js +73 -0
  20. package/dist/candidate-paths.d.ts +3 -0
  21. package/dist/candidate-paths.d.ts.map +1 -0
  22. package/dist/candidate-paths.js +11 -0
  23. package/dist/cli.d.ts +15 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +562 -0
  26. package/dist/coverage.d.ts +8 -0
  27. package/dist/coverage.d.ts.map +1 -0
  28. package/dist/coverage.js +128 -0
  29. package/dist/doctor.d.ts +9 -0
  30. package/dist/doctor.d.ts.map +1 -0
  31. package/dist/doctor.js +172 -0
  32. package/dist/handle.d.ts +28 -0
  33. package/dist/handle.d.ts.map +1 -0
  34. package/dist/handle.js +90 -0
  35. package/dist/index.d.ts +11 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +9 -0
  38. package/dist/json.d.ts +4 -0
  39. package/dist/json.d.ts.map +1 -0
  40. package/dist/json.js +25 -0
  41. package/dist/lab-min.d.ts +9 -0
  42. package/dist/lab-min.d.ts.map +1 -0
  43. package/dist/lab-min.js +23 -0
  44. package/dist/list.d.ts +582 -0
  45. package/dist/list.d.ts.map +1 -0
  46. package/dist/list.js +139 -0
  47. package/dist/manifest-update.d.ts +13 -0
  48. package/dist/manifest-update.d.ts.map +1 -0
  49. package/dist/manifest-update.js +43 -0
  50. package/dist/note.d.ts +13 -0
  51. package/dist/note.d.ts.map +1 -0
  52. package/dist/note.js +15 -0
  53. package/dist/patch-metadata.d.ts +37 -0
  54. package/dist/patch-metadata.d.ts.map +1 -0
  55. package/dist/patch-metadata.js +300 -0
  56. package/dist/read-index.d.ts +114 -0
  57. package/dist/read-index.d.ts.map +1 -0
  58. package/dist/read-index.js +210 -0
  59. package/dist/record.d.ts +25 -0
  60. package/dist/record.d.ts.map +1 -0
  61. package/dist/record.js +268 -0
  62. package/dist/recovery.d.ts +39 -0
  63. package/dist/recovery.d.ts.map +1 -0
  64. package/dist/recovery.js +189 -0
  65. package/dist/render/analytics-summary.d.ts +58 -0
  66. package/dist/render/analytics-summary.d.ts.map +1 -0
  67. package/dist/render/analytics-summary.js +151 -0
  68. package/dist/render/dependency-graph.d.ts +3 -0
  69. package/dist/render/dependency-graph.d.ts.map +1 -0
  70. package/dist/render/dependency-graph.js +18 -0
  71. package/dist/render/jsonl.d.ts +3 -0
  72. package/dist/render/jsonl.d.ts.map +1 -0
  73. package/dist/render/jsonl.js +8 -0
  74. package/dist/render/retro.d.ts +6 -0
  75. package/dist/render/retro.d.ts.map +1 -0
  76. package/dist/render/retro.js +124 -0
  77. package/dist/render/timeline-html.d.ts +3 -0
  78. package/dist/render/timeline-html.d.ts.map +1 -0
  79. package/dist/render/timeline-html.js +37 -0
  80. package/dist/schema/entry.d.ts +3298 -0
  81. package/dist/schema/entry.d.ts.map +1 -0
  82. package/dist/schema/entry.js +619 -0
  83. package/dist/schema/manifest.d.ts +42 -0
  84. package/dist/schema/manifest.d.ts.map +1 -0
  85. package/dist/schema/manifest.js +27 -0
  86. package/dist/schema/note.d.ts +10 -0
  87. package/dist/schema/note.d.ts.map +1 -0
  88. package/dist/schema/note.js +2 -0
  89. package/dist/storage/filesystem.d.ts +35 -0
  90. package/dist/storage/filesystem.d.ts.map +1 -0
  91. package/dist/storage/filesystem.js +258 -0
  92. package/dist/storage/lock.d.ts +18 -0
  93. package/dist/storage/lock.d.ts.map +1 -0
  94. package/dist/storage/lock.js +224 -0
  95. package/dist/uuid.d.ts +7 -0
  96. package/dist/uuid.d.ts.map +1 -0
  97. package/dist/uuid.js +25 -0
  98. package/dist/version.d.ts +2 -0
  99. package/dist/version.d.ts.map +1 -0
  100. package/dist/version.js +1 -0
  101. package/package.json +73 -0
@@ -0,0 +1,189 @@
1
+ import { readdir, readFile, rm, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { reconcilePendingArchivesUnderLock } from "./archive-journal.js";
5
+ import { getNextManifestSequence, updateManifestForEntry } from "./manifest-update.js";
6
+ import { ensureReadIndexUnderLock } from "./read-index.js";
7
+ import { LedgerEntrySchema } from "./schema/entry.js";
8
+ import { cleanupStaleTempFiles, ensureLedgerDirectories, readManifest, resolveLedgerPaths, saveManifest, writeAtomicJsonFile, writeEntryFile, } from "./storage/filesystem.js";
9
+ import { acquireLock } from "./storage/lock.js";
10
+ const PendingCommitSchemaVersion = 'ushman-ledger-pending-commit/v1';
11
+ // Pending commits capture the intended sequence and logical hash before the
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 }),
16
+ entry: LedgerEntrySchema,
17
+ logicalHash: z.string().min(1),
18
+ schemaVersion: z.literal(PendingCommitSchemaVersion),
19
+ sequence: z.number().int().positive(),
20
+ });
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),
25
+ });
26
+ const buildPendingCommitFilePath = (workspaceRoot, sequence, entryId) => path.join(resolveLedgerPaths(workspaceRoot).pendingCommitsDir, `${formatSequence(sequence)}-${entryId}.json`);
27
+ const parsePendingCommitFileName = (name) => {
28
+ const match = /^(\d+)-(.+)\.json$/u.exec(name);
29
+ if (!match) {
30
+ return null;
31
+ }
32
+ return PendingCommitFileNameSchema.parse({
33
+ entryId: match[2],
34
+ sequence: Number.parseInt(match[1], 10),
35
+ });
36
+ };
37
+ const readPendingCommit = async (filePath) => {
38
+ try {
39
+ const text = await readFile(filePath, 'utf8');
40
+ return PendingCommitSchema.parse(JSON.parse(text));
41
+ }
42
+ catch (error) {
43
+ throw new Error(`Invalid pending commit at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
44
+ }
45
+ };
46
+ const readPendingCommits = async (workspaceRoot) => {
47
+ const paths = await ensureLedgerDirectories(workspaceRoot);
48
+ const entries = await readdir(paths.pendingCommitsDir, { withFileTypes: true });
49
+ const pendingFiles = entries
50
+ .filter((entry) => entry.isFile())
51
+ .map((entry) => {
52
+ if (!entry.name.endsWith('.json')) {
53
+ return null;
54
+ }
55
+ const parsed = parsePendingCommitFileName(entry.name);
56
+ if (!parsed) {
57
+ return null;
58
+ }
59
+ return {
60
+ ...parsed,
61
+ filePath: path.join(paths.pendingCommitsDir, entry.name),
62
+ };
63
+ })
64
+ .filter((pendingFile) => pendingFile !== null)
65
+ .sort((left, right) => left.sequence === right.sequence
66
+ ? left.entryId.localeCompare(right.entryId)
67
+ : left.sequence - right.sequence);
68
+ return Promise.all(pendingFiles.map(async (pendingFile) => ({
69
+ filePath: pendingFile.filePath,
70
+ pending: await readPendingCommit(pendingFile.filePath),
71
+ })));
72
+ };
73
+ const ensurePendingCommitEntryFile = async (workspaceRoot, pending) => {
74
+ const entryPath = path.join(resolveLedgerPaths(workspaceRoot).phaseDir(pending.entry.phase), `${pending.entry.id}.json`);
75
+ try {
76
+ await stat(entryPath);
77
+ return;
78
+ }
79
+ catch (error) {
80
+ if (error.code !== 'ENOENT') {
81
+ throw error;
82
+ }
83
+ }
84
+ await writeEntryFile(workspaceRoot, pending.entry.phase, `${pending.entry.id}.json`, pending.entry);
85
+ };
86
+ const checkPendingCommitConflict = (manifest, pending) => {
87
+ const location = manifest.entryLocations[pending.entry.id];
88
+ if (!location) {
89
+ return;
90
+ }
91
+ if (location.phase !== pending.entry.phase || location.sequence !== pending.sequence) {
92
+ throw new Error(`Pending commit ${pending.entry.id} conflicts with manifest location ${location.phase}/${location.sequence}.`);
93
+ }
94
+ };
95
+ const applyPendingCommit = async ({ manifest, pending, workspaceRoot, }) => {
96
+ checkPendingCommitConflict(manifest, pending);
97
+ if (manifest.entryLocations[pending.entry.id]) {
98
+ await ensurePendingCommitEntryFile(workspaceRoot, pending);
99
+ return {
100
+ didChangeManifest: false,
101
+ manifest,
102
+ };
103
+ }
104
+ if (manifest.lastSequence < pending.baseLastSequence) {
105
+ throw new Error(`Pending commit ${pending.entry.id} is waiting on earlier sequence ${pending.baseLastSequence}.`);
106
+ }
107
+ if (manifest.lastSequence > pending.baseLastSequence) {
108
+ throw new Error(`Pending commit ${pending.entry.id} cannot be replayed after manifest advanced to ${manifest.lastSequence}.`);
109
+ }
110
+ const expectedSequence = getNextManifestSequence(manifest.lastSequence);
111
+ if (expectedSequence !== pending.sequence) {
112
+ throw new Error(`Pending commit ${pending.entry.id} expected next sequence ${pending.sequence} but manifest would advance to ${expectedSequence}.`);
113
+ }
114
+ await writeEntryFile(workspaceRoot, pending.entry.phase, `${pending.entry.id}.json`, pending.entry);
115
+ const nextManifest = updateManifestForEntry({
116
+ entry: pending.entry,
117
+ logicalHash: pending.logicalHash,
118
+ manifest,
119
+ });
120
+ if (nextManifest.lastSequence !== pending.sequence) {
121
+ throw new Error(`Pending commit ${pending.entry.id} expected sequence ${pending.sequence} but resolved to ${nextManifest.lastSequence}.`);
122
+ }
123
+ return {
124
+ didChangeManifest: true,
125
+ manifest: nextManifest,
126
+ };
127
+ };
128
+ export const writePendingCommit = async ({ entry, logicalHash, manifest, workspaceRoot, }) => {
129
+ const nextSequence = getNextManifestSequence(manifest.lastSequence);
130
+ const filePath = buildPendingCommitFilePath(workspaceRoot, nextSequence, entry.id);
131
+ await writeAtomicJsonFile(filePath, PendingCommitSchema.parse({
132
+ baseLastSequence: manifest.lastSequence,
133
+ createdAt: new Date().toISOString(),
134
+ entry,
135
+ logicalHash,
136
+ schemaVersion: PendingCommitSchemaVersion,
137
+ sequence: nextSequence,
138
+ }));
139
+ return filePath;
140
+ };
141
+ export const removePendingCommit = async (filePath) => {
142
+ await rm(filePath, { force: true });
143
+ };
144
+ export const reconcilePendingCommitsUnderLock = async (workspaceRoot) => {
145
+ await cleanupStaleTempFiles(workspaceRoot);
146
+ let manifest = await readManifest(workspaceRoot);
147
+ const pendingCommits = await readPendingCommits(workspaceRoot);
148
+ const processedPendingFiles = [];
149
+ let manifestChanged = false;
150
+ for (const pendingCommit of pendingCommits) {
151
+ const replayed = await applyPendingCommit({
152
+ manifest,
153
+ pending: pendingCommit.pending,
154
+ workspaceRoot,
155
+ });
156
+ manifest = replayed.manifest;
157
+ manifestChanged = manifestChanged || replayed.didChangeManifest;
158
+ processedPendingFiles.push(pendingCommit.filePath);
159
+ }
160
+ if (manifestChanged) {
161
+ await saveManifest(workspaceRoot, manifest);
162
+ }
163
+ for (const filePath of processedPendingFiles) {
164
+ await removePendingCommit(filePath);
165
+ }
166
+ return manifest;
167
+ };
168
+ export const reconcileLedgerStateUnderLock = async (workspaceRoot) => {
169
+ const manifestAfterPendingCommits = await reconcilePendingCommitsUnderLock(workspaceRoot);
170
+ const readIndex = await ensureReadIndexUnderLock(workspaceRoot, manifestAfterPendingCommits);
171
+ const manifest = await reconcilePendingArchivesUnderLock({
172
+ manifest: manifestAfterPendingCommits,
173
+ workspaceRoot,
174
+ });
175
+ return { manifest, readIndex };
176
+ };
177
+ export const loadLedgerState = async (workspaceRoot) => {
178
+ const paths = await ensureLedgerDirectories(workspaceRoot);
179
+ const lock = await acquireLock(paths.manifestLockFile);
180
+ try {
181
+ return await reconcileLedgerStateUnderLock(workspaceRoot);
182
+ }
183
+ finally {
184
+ await lock.release();
185
+ }
186
+ };
187
+ export const prepareLedgerState = async (workspaceRoot) => {
188
+ await loadLedgerState(workspaceRoot);
189
+ };
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ import type { LedgerReadIndex } from '../read-index.ts';
3
+ import { type LedgerEntry } from '../schema/entry.ts';
4
+ import type { LedgerManifest } from '../schema/manifest.ts';
5
+ export declare const AnalyticsSummarySchema: z.ZodObject<{
6
+ agentPatchCount: z.ZodNumber;
7
+ entriesByKind: z.ZodRecord<z.ZodEnum<{
8
+ "tool-invocation": "tool-invocation";
9
+ "agent-patch": "agent-patch";
10
+ "operator-patch": "operator-patch";
11
+ "stage-transition": "stage-transition";
12
+ "operator-decision": "operator-decision";
13
+ "validator-result": "validator-result";
14
+ "runtime-event": "runtime-event";
15
+ note: "note";
16
+ correction: "correction";
17
+ "strip-decision-reverted": "strip-decision-reverted";
18
+ "descope-brief": "descope-brief";
19
+ "merge-return": "merge-return";
20
+ "merge-return-rejected": "merge-return-rejected";
21
+ revert: "revert";
22
+ rollback: "rollback";
23
+ "rework.test_retired": "rework.test_retired";
24
+ }>, z.ZodNumber>;
25
+ entryCount: z.ZodNumber;
26
+ filesTouchedCount: z.ZodNumber;
27
+ filterHash: z.ZodString;
28
+ legacyEntryCount: z.ZodNumber;
29
+ operatorDecisionActionCounts: z.ZodRecord<z.ZodEnum<{
30
+ "bypass-doctor": "bypass-doctor";
31
+ "skip-check": "skip-check";
32
+ "override-strip-decision": "override-strip-decision";
33
+ "override-ship-state": "override-ship-state";
34
+ "manual-parity-assertion": "manual-parity-assertion";
35
+ "ledger-hand-edit": "ledger-hand-edit";
36
+ escalation: "escalation";
37
+ }>, z.ZodNumber>;
38
+ operatorPatchCount: z.ZodNumber;
39
+ reworkTestRetiredCount: z.ZodNumber;
40
+ schemaVersion: z.ZodLiteral<"ushman-ledger-analytics-summary/v1">;
41
+ stageTransitionCount: z.ZodNumber;
42
+ tipHash: z.ZodString;
43
+ validatorPassFailCounts: z.ZodObject<{
44
+ green: z.ZodNumber;
45
+ red: z.ZodNumber;
46
+ yellow: z.ZodNumber;
47
+ }, z.core.$strip>;
48
+ }, z.core.$strip>;
49
+ export type AnalyticsSummary = z.infer<typeof AnalyticsSummarySchema>;
50
+ export declare const renderAnalyticsSummary: ({ cachePath, entries, filter, manifest, readIndex, useCache, }: {
51
+ readonly cachePath: string;
52
+ readonly entries: AsyncIterable<LedgerEntry>;
53
+ readonly filter: unknown;
54
+ readonly manifest: LedgerManifest;
55
+ readonly readIndex: LedgerReadIndex;
56
+ readonly useCache: boolean;
57
+ }) => Promise<string>;
58
+ //# sourceMappingURL=analytics-summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-summary.d.ts","sourceRoot":"","sources":["../../src/render/analytics-summary.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAEH,KAAK,WAAW,EAInB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAY5D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAcjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAgKtE,eAAO,MAAM,sBAAsB,GAAU,gEAO1C;IACC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC9B,oBAqBA,CAAC"}
@@ -0,0 +1,151 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { z } from 'zod';
3
+ import { sha256Hex, stableStringify } from "../json.js";
4
+ import { LEDGER_KINDS, OPERATOR_DECISION_ACTIONS, } from "../schema/entry.js";
5
+ import { writeAtomicJsonFile } from "../storage/filesystem.js";
6
+ const ANALYTICS_SUMMARY_SCHEMA_VERSION = 'ushman-ledger-analytics-summary/v1';
7
+ const EntriesByKindSchema = z.record(z.enum(LEDGER_KINDS), z.number().int().nonnegative());
8
+ const OperatorDecisionActionCountsSchema = z.record(z.enum(OPERATOR_DECISION_ACTIONS), z.number().int().nonnegative());
9
+ const ValidatorPassFailCountsSchema = z.object({
10
+ green: z.number().int().nonnegative(),
11
+ red: z.number().int().nonnegative(),
12
+ yellow: z.number().int().nonnegative(),
13
+ });
14
+ export const AnalyticsSummarySchema = z.object({
15
+ agentPatchCount: z.number().int().nonnegative(),
16
+ entriesByKind: EntriesByKindSchema,
17
+ entryCount: z.number().int().nonnegative(),
18
+ filesTouchedCount: z.number().int().nonnegative(),
19
+ filterHash: z.string().min(1),
20
+ legacyEntryCount: z.number().int().nonnegative(),
21
+ operatorDecisionActionCounts: OperatorDecisionActionCountsSchema,
22
+ operatorPatchCount: z.number().int().nonnegative(),
23
+ reworkTestRetiredCount: z.number().int().nonnegative(),
24
+ schemaVersion: z.literal(ANALYTICS_SUMMARY_SCHEMA_VERSION),
25
+ stageTransitionCount: z.number().int().nonnegative(),
26
+ tipHash: z.string().min(1),
27
+ validatorPassFailCounts: ValidatorPassFailCountsSchema,
28
+ });
29
+ const buildEntriesByKind = () => Object.fromEntries(LEDGER_KINDS.map((kind) => [kind, 0]));
30
+ const buildOperatorDecisionActionCounts = () => Object.fromEntries(OPERATOR_DECISION_ACTIONS.map((action) => [action, 0]));
31
+ const computeLedgerTipHash = ({ manifest, readIndex, }) => sha256Hex(stableStringify({
32
+ lastEntryId: readIndex.lastEntryId,
33
+ lastSequence: manifest.lastSequence,
34
+ }));
35
+ const computeFilterHash = (filter) => sha256Hex(stableStringify(filter));
36
+ const increment = (record, key) => {
37
+ record[key] += 1;
38
+ };
39
+ const createSummaryAccumulator = () => ({
40
+ agentPatchCount: 0,
41
+ entriesByKind: buildEntriesByKind(),
42
+ entryCount: 0,
43
+ legacyEntryCount: 0,
44
+ operatorDecisionActionCounts: buildOperatorDecisionActionCounts(),
45
+ operatorPatchCount: 0,
46
+ reworkTestRetiredCount: 0,
47
+ stageTransitionCount: 0,
48
+ touchedPaths: new Set(),
49
+ validatorPassFailCounts: {
50
+ green: 0,
51
+ red: 0,
52
+ yellow: 0,
53
+ },
54
+ });
55
+ const addTouchedPaths = (state, touchedPaths) => {
56
+ for (const touchedPath of touchedPaths) {
57
+ state.touchedPaths.add(touchedPath);
58
+ }
59
+ };
60
+ const accumulateEntry = (state, entry) => {
61
+ state.entryCount += 1;
62
+ increment(state.entriesByKind, entry.kind);
63
+ if (entry._legacy) {
64
+ state.legacyEntryCount += 1;
65
+ }
66
+ switch (entry.kind) {
67
+ case 'agent-patch':
68
+ state.agentPatchCount += 1;
69
+ addTouchedPaths(state, entry.payload.touchedPaths);
70
+ return;
71
+ case 'operator-patch':
72
+ state.operatorPatchCount += 1;
73
+ addTouchedPaths(state, entry.payload.touchedPaths);
74
+ return;
75
+ case 'operator-decision':
76
+ increment(state.operatorDecisionActionCounts, entry.payload.action);
77
+ return;
78
+ case 'validator-result':
79
+ increment(state.validatorPassFailCounts, entry.verdict);
80
+ return;
81
+ case 'stage-transition':
82
+ state.stageTransitionCount += 1;
83
+ return;
84
+ case 'rework.test_retired':
85
+ state.reworkTestRetiredCount += 1;
86
+ return;
87
+ default:
88
+ return;
89
+ }
90
+ };
91
+ const summarizeEntries = async ({ entries, filterHash, tipHash, }) => {
92
+ const state = createSummaryAccumulator();
93
+ for await (const entry of entries) {
94
+ accumulateEntry(state, entry);
95
+ }
96
+ return AnalyticsSummarySchema.parse({
97
+ agentPatchCount: state.agentPatchCount,
98
+ entriesByKind: state.entriesByKind,
99
+ entryCount: state.entryCount,
100
+ filesTouchedCount: state.touchedPaths.size,
101
+ filterHash,
102
+ legacyEntryCount: state.legacyEntryCount,
103
+ operatorDecisionActionCounts: state.operatorDecisionActionCounts,
104
+ operatorPatchCount: state.operatorPatchCount,
105
+ reworkTestRetiredCount: state.reworkTestRetiredCount,
106
+ schemaVersion: ANALYTICS_SUMMARY_SCHEMA_VERSION,
107
+ stageTransitionCount: state.stageTransitionCount,
108
+ tipHash,
109
+ validatorPassFailCounts: state.validatorPassFailCounts,
110
+ });
111
+ };
112
+ const formatAnalyticsSummary = (summary) => `${stableStringify(summary, true)}\n`;
113
+ const debugLog = (message, error) => {
114
+ if (process.env.NODE_ENV === 'test') {
115
+ return;
116
+ }
117
+ const details = error instanceof Error ? error.message : String(error);
118
+ console.debug(`[ushman-ledger] ${message}: ${details}`);
119
+ };
120
+ const readCachedSummary = async (cachePath) => {
121
+ try {
122
+ const text = await readFile(cachePath, 'utf8');
123
+ return AnalyticsSummarySchema.parse(JSON.parse(text));
124
+ }
125
+ catch (error) {
126
+ if (error.code === 'ENOENT') {
127
+ return null;
128
+ }
129
+ debugLog(`Ignoring analytics summary cache at ${cachePath}`, error);
130
+ return null;
131
+ }
132
+ };
133
+ export const renderAnalyticsSummary = async ({ cachePath, entries, filter, manifest, readIndex, useCache, }) => {
134
+ const filterHash = computeFilterHash(filter);
135
+ const tipHash = computeLedgerTipHash({ manifest, readIndex });
136
+ if (useCache) {
137
+ const cached = await readCachedSummary(cachePath);
138
+ if (cached?.tipHash === tipHash && cached.filterHash === filterHash) {
139
+ return formatAnalyticsSummary(cached);
140
+ }
141
+ }
142
+ const summary = await summarizeEntries({
143
+ entries,
144
+ filterHash,
145
+ tipHash,
146
+ });
147
+ if (useCache) {
148
+ await writeAtomicJsonFile(cachePath, summary);
149
+ }
150
+ return formatAnalyticsSummary(summary);
151
+ };
@@ -0,0 +1,3 @@
1
+ import type { LedgerEntry } from '../schema/entry.ts';
2
+ export declare const renderDependencyGraph: (entries: AsyncIterable<LedgerEntry>) => Promise<string>;
3
+ //# sourceMappingURL=dependency-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependency-graph.d.ts","sourceRoot":"","sources":["../../src/render/dependency-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKtD,eAAO,MAAM,qBAAqB,GAAU,SAAS,aAAa,CAAC,WAAW,CAAC,KAAG,OAAO,CAAC,MAAM,CAoB/F,CAAC"}
@@ -0,0 +1,18 @@
1
+ const escapeMermaid = (value) => value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll(/\s+/gu, ' ').trim();
2
+ export const renderDependencyGraph = async (entries) => {
3
+ const lines = ['graph TD'];
4
+ for await (const entry of entries) {
5
+ const nodeLabel = escapeMermaid(`${entry.phase}:${entry.kind}`);
6
+ lines.push(` "${escapeMermaid(entry.id)}"["${nodeLabel}\\n${escapeMermaid(entry.summary)}"]`);
7
+ if (entry.prevEntryId) {
8
+ lines.push(` "${escapeMermaid(entry.prevEntryId)}" --> "${escapeMermaid(entry.id)}"`);
9
+ }
10
+ if (entry.links.correctsLedgerId) {
11
+ lines.push(` "${escapeMermaid(entry.links.correctsLedgerId)}" -. corrects .-> "${escapeMermaid(entry.id)}"`);
12
+ }
13
+ if (entry.links.supersedesLedgerId) {
14
+ lines.push(` "${escapeMermaid(entry.links.supersedesLedgerId)}" -. supersedes .-> "${escapeMermaid(entry.id)}"`);
15
+ }
16
+ }
17
+ return lines.join('\n');
18
+ };
@@ -0,0 +1,3 @@
1
+ import type { LedgerEntry } from '../schema/entry.ts';
2
+ export declare const renderJsonl: (entries: AsyncIterable<LedgerEntry>) => Promise<string>;
3
+ //# sourceMappingURL=jsonl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl.d.ts","sourceRoot":"","sources":["../../src/render/jsonl.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,eAAO,MAAM,WAAW,GAAU,SAAS,aAAa,CAAC,WAAW,CAAC,KAAG,OAAO,CAAC,MAAM,CAMrF,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { stableStringify } from "../json.js";
2
+ export const renderJsonl = async (entries) => {
3
+ const lines = [];
4
+ for await (const entry of entries) {
5
+ lines.push(stableStringify(entry));
6
+ }
7
+ return `${lines.join('\n')}${lines.length > 0 ? '\n' : ''}`;
8
+ };
@@ -0,0 +1,6 @@
1
+ import type { LedgerEntry } from '../schema/entry.ts';
2
+ export declare const renderRetroMarkdown: ({ entries, manifest, }: {
3
+ readonly entries: AsyncIterable<LedgerEntry>;
4
+ readonly manifest: unknown;
5
+ }) => Promise<string>;
6
+ //# sourceMappingURL=retro.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,124 @@
1
+ import { sha256Hex, stableStringify } from "../json.js";
2
+ const renderList = (items) => items.length === 0 ? '- None recorded.' : items.map((item) => `- ${item}`).join('\n');
3
+ const toSingleLine = (value) => value.replaceAll(/\s+/gu, ' ').trim();
4
+ const formatEntry = (entry) => `${entry.ts} [${entry.phase}] ${toSingleLine(entry.summary)}`;
5
+ const createRetroBuckets = () => ({
6
+ correctionEntries: [],
7
+ operatorEntries: [],
8
+ problemEntries: [],
9
+ retroEntries: [],
10
+ toolEntries: [],
11
+ toolingEntries: [],
12
+ validatorEntries: [],
13
+ });
14
+ const isProblemEntry = (entry) => (entry.kind === 'note' && entry.subkind === 'regression') ||
15
+ (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';
20
+ const isToolingEntry = (entry) => (entry.kind === 'note' && entry.subkind === 'automation') ||
21
+ (entry.kind === 'note' && entry.subkind === 'tooling-gap');
22
+ const isRetroNote = (entry) => entry.kind === 'note' && entry.subkind === 'retro';
23
+ const isOperatorNote = (entry) => entry.kind === 'note' && entry.subkind === 'operator';
24
+ const resolveRenderedAt = (entryCount, lastEntryTimestamp, manifest) => {
25
+ if (entryCount > 0) {
26
+ return lastEntryTimestamp ?? 'n/a';
27
+ }
28
+ if (typeof manifest === 'object' && manifest && 'createdAt' in manifest) {
29
+ const createdAt = manifest.createdAt;
30
+ if (typeof createdAt === 'string') {
31
+ return createdAt;
32
+ }
33
+ }
34
+ return 'n/a';
35
+ };
36
+ const normalizeManifestForHash = (manifest) => typeof manifest === 'object' && manifest
37
+ ? { ...manifest, updatedAt: undefined }
38
+ : manifest;
39
+ const formatValidatorEntry = (entry) => entry.kind === 'validator-result'
40
+ ? `${entry.ts} [${entry.validator}/${entry.verdict}] ${entry.summary}`
41
+ : formatEntry(entry);
42
+ const collectRetroState = async (entries) => {
43
+ const buckets = createRetroBuckets();
44
+ const tools = new Set();
45
+ let entryCount = 0;
46
+ let lastEntryTimestamp = null;
47
+ for await (const entry of entries) {
48
+ entryCount += 1;
49
+ lastEntryTimestamp = entry.ts;
50
+ 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
+ }
72
+ }
73
+ return {
74
+ buckets,
75
+ entryCount,
76
+ lastEntryTimestamp,
77
+ tools: [...tools].sort((left, right) => left.localeCompare(right)),
78
+ };
79
+ };
80
+ export const renderRetroMarkdown = async ({ entries, manifest, }) => {
81
+ const { buckets, entryCount, lastEntryTimestamp, tools } = await collectRetroState(entries);
82
+ const renderedAt = resolveRenderedAt(entryCount, lastEntryTimestamp, manifest);
83
+ const manifestSha = sha256Hex(stableStringify(normalizeManifestForHash(manifest)));
84
+ return [
85
+ '<!-- LEDGER_SCHEMA_VERSION: ushman-ledger-entry/v1 -->',
86
+ '<!-- DO_NOT_EDIT: Auto-generated. Use `ushman-ledger record` / `ushman-ledger note`. -->',
87
+ `<!-- LAST_RENDERED: ${renderedAt} -->`,
88
+ `<!-- LAST_RENDERED_AT_ENTRY_COUNT: ${entryCount} -->`,
89
+ `<!-- LAST_RENDERED_MANIFEST_SHA: ${manifestSha} -->`,
90
+ '',
91
+ '# Ushman Ledger Retro',
92
+ '',
93
+ '## 1. Donor classification',
94
+ renderList(['Recorded through the workspace ledger.']),
95
+ '',
96
+ '## 2. Migration scope',
97
+ renderList(['See operator decisions and note entries for phase-specific scope.']),
98
+ '',
99
+ '## 3. Step-by-step migration log',
100
+ renderList(buckets.toolEntries),
101
+ '',
102
+ '## 4. Problems hit',
103
+ renderList(buckets.problemEntries),
104
+ '',
105
+ '## 5. Tools used',
106
+ renderList(tools),
107
+ '',
108
+ '## 6. Where dedicated tooling would have helped',
109
+ renderList(buckets.toolingEntries),
110
+ '',
111
+ '## 7. Where tooling was overkill',
112
+ renderList(buckets.retroEntries),
113
+ '',
114
+ '## 8. Corrections and revisions',
115
+ renderList(buckets.correctionEntries),
116
+ '',
117
+ '## 9. Operator notes',
118
+ renderList(buckets.operatorEntries),
119
+ '',
120
+ '## 10. Triage signals',
121
+ renderList(buckets.validatorEntries),
122
+ '',
123
+ ].join('\n');
124
+ };
@@ -0,0 +1,3 @@
1
+ import type { LedgerEntry } from '../schema/entry.ts';
2
+ export declare const renderTimelineHtml: (entries: AsyncIterable<LedgerEntry>) => Promise<string>;
3
+ //# sourceMappingURL=timeline-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline-html.d.ts","sourceRoot":"","sources":["../../src/render/timeline-html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAUtD,eAAO,MAAM,kBAAkB,GAAU,SAAS,aAAa,CAAC,WAAW,CAAC,KAAG,OAAO,CAAC,MAAM,CAmC5F,CAAC"}
@@ -0,0 +1,37 @@
1
+ const escapeHtml = (value) => value
2
+ .replaceAll('&', '&amp;')
3
+ .replaceAll('<', '&lt;')
4
+ .replaceAll('>', '&gt;')
5
+ .replaceAll('"', '&quot;')
6
+ .replaceAll("'", '&#39;');
7
+ export const renderTimelineHtml = async (entries) => {
8
+ const rows = [];
9
+ for await (const entry of entries) {
10
+ rows.push(`<tr><td>${escapeHtml(entry.ts)}</td><td>${escapeHtml(entry.phase)}</td><td>${escapeHtml(entry.kind)}</td><td>${escapeHtml(entry.summary)}</td></tr>`);
11
+ }
12
+ return `<!doctype html>
13
+ <html lang="en">
14
+ <head>
15
+ <meta charset="utf-8" />
16
+ <title>Ushman Ledger Timeline</title>
17
+ <style>
18
+ body { font-family: sans-serif; margin: 2rem; }
19
+ table { border-collapse: collapse; width: 100%; }
20
+ th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
21
+ th { background: #f5f5f5; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <h1>Ushman Ledger Timeline</h1>
26
+ <table>
27
+ <thead>
28
+ <tr><th>Timestamp</th><th>Phase</th><th>Kind</th><th>Summary</th></tr>
29
+ </thead>
30
+ <tbody>
31
+ ${rows.join('\n')}
32
+ </tbody>
33
+ </table>
34
+ </body>
35
+ </html>
36
+ `;
37
+ };