ushman-ledger 1.2.0 → 1.2.2

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 (63) hide show
  1. package/ARCHITECTURE.md +79 -0
  2. package/README.md +144 -5
  3. package/TROUBLESHOOTING.md +170 -0
  4. package/dist/blobs.d.ts +3 -0
  5. package/dist/blobs.d.ts.map +1 -1
  6. package/dist/blobs.js +41 -15
  7. package/dist/builders.d.ts +1 -2
  8. package/dist/builders.d.ts.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +231 -70
  11. package/dist/coverage.d.ts.map +1 -1
  12. package/dist/coverage.js +3 -2
  13. package/dist/doctor.d.ts +17 -4
  14. package/dist/doctor.d.ts.map +1 -1
  15. package/dist/doctor.js +225 -58
  16. package/dist/handle.d.ts +27 -7
  17. package/dist/handle.d.ts.map +1 -1
  18. package/dist/handle.js +96 -20
  19. package/dist/helpers.d.ts +1 -0
  20. package/dist/helpers.d.ts.map +1 -1
  21. package/dist/helpers.js +23 -0
  22. package/dist/index.d.ts +6 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +4 -1
  25. package/dist/list.d.ts +3 -2
  26. package/dist/list.d.ts.map +1 -1
  27. package/dist/list.js +24 -12
  28. package/dist/note.d.ts +7 -0
  29. package/dist/note.d.ts.map +1 -1
  30. package/dist/note.js +6 -0
  31. package/dist/patch-resolver.d.ts +12 -0
  32. package/dist/patch-resolver.d.ts.map +1 -1
  33. package/dist/patch-resolver.js +205 -53
  34. package/dist/read-index.d.ts.map +1 -1
  35. package/dist/read-index.js +6 -5
  36. package/dist/record.d.ts.map +1 -1
  37. package/dist/record.js +3 -3
  38. package/dist/render/migration-log.d.ts +8 -1
  39. package/dist/render/migration-log.d.ts.map +1 -1
  40. package/dist/render/migration-log.js +40 -33
  41. package/dist/render/retro.d.ts.map +1 -1
  42. package/dist/render/retro.js +1 -7
  43. package/dist/render/workspace-narrative.d.ts +7 -1
  44. package/dist/render/workspace-narrative.d.ts.map +1 -1
  45. package/dist/render/workspace-narrative.js +114 -46
  46. package/dist/runtime-config.d.ts +12 -0
  47. package/dist/runtime-config.d.ts.map +1 -0
  48. package/dist/runtime-config.js +83 -0
  49. package/dist/schema/entry-read.d.ts.map +1 -1
  50. package/dist/schema/entry-read.js +1 -1
  51. package/dist/schema/entry-write.d.ts.map +1 -1
  52. package/dist/schema/entry-write.js +1 -1
  53. package/dist/schema/entry.d.ts.map +1 -1
  54. package/dist/storage/filesystem.d.ts +8 -0
  55. package/dist/storage/filesystem.d.ts.map +1 -1
  56. package/dist/storage/filesystem.js +110 -5
  57. package/dist/text-lines.d.ts +8 -0
  58. package/dist/text-lines.d.ts.map +1 -0
  59. package/dist/text-lines.js +20 -0
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.d.ts.map +1 -1
  62. package/dist/version.js +2 -1
  63. package/package.json +5 -3
package/dist/helpers.js CHANGED
@@ -36,3 +36,26 @@ export const waitForFile = async (filePath, timeoutMs = 1_000) => {
36
36
  }
37
37
  throw new Error(`Timed out waiting for file: ${filePath}`);
38
38
  };
39
+ export const withEnvOverrides = async (overrides, action) => {
40
+ const previousValues = new Map();
41
+ for (const [key, value] of Object.entries(overrides)) {
42
+ previousValues.set(key, process.env[key]);
43
+ if (value === undefined) {
44
+ delete process.env[key];
45
+ continue;
46
+ }
47
+ process.env[key] = value;
48
+ }
49
+ try {
50
+ return await action();
51
+ }
52
+ finally {
53
+ for (const [key, value] of previousValues.entries()) {
54
+ if (value === undefined) {
55
+ delete process.env[key];
56
+ continue;
57
+ }
58
+ process.env[key] = value;
59
+ }
60
+ }
61
+ };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,13 @@
1
- export { buildChangeLogRecord, buildCorrectionRecord, buildOperatorDecisionRecord, buildStripDecisionRevertedRecord, buildValidatorResultRecord, } from './builders.ts';
1
+ export { type BuildRecordInput, buildChangeLogRecord, buildCorrectionRecord, buildOperatorDecisionRecord, buildStripDecisionRevertedRecord, buildValidatorResultRecord, } from './builders.ts';
2
2
  export { runLedgerCli } from './cli.ts';
3
3
  export { type CoverageReport, computeCoverage } from './coverage.ts';
4
- export { type LedgerHandle, type RenderTarget, openLedger } from './handle.ts';
4
+ export { DOCTOR_FINDING_CODES, type DoctorFinding, type DoctorFindingCode, type DoctorFindingMetadataValue, type DoctorReport, runLedgerDoctor, } from './doctor.ts';
5
+ export { type LedgerHandle, type LedgerRenderOptions, type LedgerRenderToOptions, openLedger, type RenderTarget, type RenderWriter, } from './handle.ts';
5
6
  export type { LedgerFilter } from './list.ts';
7
+ export { type LedgerRuntimeConfig, getLedgerRuntimeConfig, validateLedgerRuntimeConfig } from './runtime-config.ts';
6
8
  export { appendCleanupWaveNote, appendDecompositionWaveNote, appendNote, appendOpenIssueNote, appendSemanticCleanupSummaryNote, appendVerifiedFlowNote, type NoteBody, } from './note.ts';
7
- export { type AgentPatchDiff, AgentPatchDiffSchema, AgentPatchEntrySchema, AgentPatchRecordSchema, type ChangeLogEntry, ChangeLogEntrySchema, type ChangeLogFileChange, ChangeLogFileChangeSchema, type ChangeLogParityStatus, ChangeLogParityStatusSchema, type ChangeLogRecord, ChangeLogRecordSchema, type ChangeLogSmokeResult, ChangeLogSmokeResultSchema, type ChangeLogSubkind, ChangeLogSubkindSchema, CorrectionEntrySchema, CorrectionRecordSchema, EmitterSchema, type LedgerEntry, LedgerEntrySchema, type LedgerKind, LedgerLinksSchema, type LedgerLinks, type LedgerPhase, LedgerPhaseSchema, type LedgerRecord, LedgerRecordSchema, type NoteEntry, NoteEntrySchema, type OperatorDecisionAction, OperatorDecisionActionSchema, OperatorDecisionEntrySchema, type OperatorDecisionPayload, OperatorDecisionPayloadSchema, OperatorDecisionRecordSchema, OperatorPatchEntrySchema, OperatorPatchRecordSchema, parseLedgerEntry, parseLedgerRecord, RuntimeEventEntrySchema, StripDecisionRevertedEntrySchema, type StripDecisionRevertedPayload, StripDecisionRevertedPayloadSchema, StripDecisionRevertedRecordSchema, ToolInvocationEntrySchema, ToolInvocationRecordSchema, ValidatorResultEntrySchema, ValidatorResultRecordSchema, } from './schema/entry.ts';
9
+ export { deriveFilesChangedFromPatch } from './patch-resolver.ts';
10
+ export { type AgentPatchDiff, AgentPatchDiffSchema, AgentPatchEntrySchema, AgentPatchRecordSchema, type ChangeLogEntry, ChangeLogEntrySchema, type ChangeLogFileChange, ChangeLogFileChangeSchema, type ChangeLogParityStatus, ChangeLogParityStatusSchema, type ChangeLogRecord, ChangeLogRecordSchema, type ChangeLogSmokeResult, ChangeLogSmokeResultSchema, type ChangeLogSubkind, ChangeLogSubkindSchema, CorrectionEntrySchema, CorrectionRecordSchema, EmitterSchema, type LedgerEntry, LedgerEntrySchema, type LedgerKind, type LedgerLinks, LedgerLinksSchema, type LedgerPhase, LedgerPhaseSchema, type LedgerRecord, LedgerRecordSchema, type NoteEntry, NoteEntrySchema, type OperatorDecisionAction, OperatorDecisionActionSchema, OperatorDecisionEntrySchema, type OperatorDecisionPayload, OperatorDecisionPayloadSchema, OperatorDecisionRecordSchema, OperatorPatchEntrySchema, OperatorPatchRecordSchema, parseLedgerEntry, parseLedgerRecord, RuntimeEventEntrySchema, StripDecisionRevertedEntrySchema, type StripDecisionRevertedPayload, StripDecisionRevertedPayloadSchema, StripDecisionRevertedRecordSchema, ToolInvocationEntrySchema, ToolInvocationRecordSchema, ValidatorResultEntrySchema, ValidatorResultRecordSchema, } from './schema/entry.ts';
8
11
  export { type NoteSubkind, NoteSubkindSchema } from './schema/note.ts';
9
12
  export { resolveLedgerPaths } from './storage/filesystem.ts';
10
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,oBAAoB,EACpB,qBAAqB,EACrB,2BAA2B,EAC3B,gCAAgC,EAChC,0BAA0B,GAC7B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,KAAK,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC/E,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EACH,qBAAqB,EACrB,2BAA2B,EAC3B,UAAU,EACV,mBAAmB,EACnB,gCAAgC,EAChC,sBAAsB,EACtB,KAAK,QAAQ,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EACH,KAAK,cAAc,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,cAAc,EACnB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,KAAK,qBAAqB,EAC1B,2BAA2B,EAC3B,KAAK,eAAe,EACpB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,KAAK,WAAW,EAChB,iBAAiB,EACjB,KAAK,UAAU,EACf,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,iBAAiB,EACjB,KAAK,YAAY,EACjB,kBAAkB,EAClB,KAAK,SAAS,EACd,eAAe,EACf,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,6BAA6B,EAC7B,4BAA4B,EAC5B,wBAAwB,EACxB,yBAAyB,EACzB,gBAAgB,EAChB,iBAAiB,EACjB,uBAAuB,EACvB,gCAAgC,EAChC,KAAK,4BAA4B,EACjC,kCAAkC,EAClC,iCAAiC,EACjC,yBAAyB,EACzB,0BAA0B,EAC1B,0BAA0B,EAC1B,2BAA2B,GAC9B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,gBAAgB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,2BAA2B,EAC3B,gCAAgC,EAChC,0BAA0B,GAC7B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,KAAK,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EACH,oBAAoB,EACpB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,0BAA0B,EAC/B,KAAK,YAAY,EACjB,eAAe,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EACH,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,UAAU,EACV,KAAK,YAAY,EACjB,KAAK,YAAY,GACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EAAE,KAAK,mBAAmB,EAAE,sBAAsB,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AACpH,OAAO,EACH,qBAAqB,EACrB,2BAA2B,EAC3B,UAAU,EACV,mBAAmB,EACnB,gCAAgC,EAChC,sBAAsB,EACtB,KAAK,QAAQ,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EACH,KAAK,cAAc,EACnB,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,cAAc,EACnB,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,yBAAyB,EACzB,KAAK,qBAAqB,EAC1B,2BAA2B,EAC3B,KAAK,eAAe,EACpB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,0BAA0B,EAC1B,KAAK,gBAAgB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,KAAK,WAAW,EAChB,iBAAiB,EACjB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,iBAAiB,EACjB,KAAK,WAAW,EAChB,iBAAiB,EACjB,KAAK,YAAY,EACjB,kBAAkB,EAClB,KAAK,SAAS,EACd,eAAe,EACf,KAAK,sBAAsB,EAC3B,4BAA4B,EAC5B,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,6BAA6B,EAC7B,4BAA4B,EAC5B,wBAAwB,EACxB,yBAAyB,EACzB,gBAAgB,EAChB,iBAAiB,EACjB,uBAAuB,EACvB,gCAAgC,EAChC,KAAK,4BAA4B,EACjC,kCAAkC,EAClC,iCAAiC,EACjC,yBAAyB,EACzB,0BAA0B,EAC1B,0BAA0B,EAC1B,2BAA2B,GAC9B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
1
  export { buildChangeLogRecord, buildCorrectionRecord, buildOperatorDecisionRecord, buildStripDecisionRevertedRecord, buildValidatorResultRecord, } from "./builders.js";
2
2
  export { runLedgerCli } from "./cli.js";
3
3
  export { computeCoverage } from "./coverage.js";
4
- export { openLedger } from "./handle.js";
4
+ export { DOCTOR_FINDING_CODES, runLedgerDoctor, } from "./doctor.js";
5
+ export { openLedger, } from "./handle.js";
6
+ export { getLedgerRuntimeConfig, validateLedgerRuntimeConfig } from "./runtime-config.js";
5
7
  export { appendCleanupWaveNote, appendDecompositionWaveNote, appendNote, appendOpenIssueNote, appendSemanticCleanupSummaryNote, appendVerifiedFlowNote, } from "./note.js";
8
+ export { deriveFilesChangedFromPatch } from "./patch-resolver.js";
6
9
  export { AgentPatchDiffSchema, AgentPatchEntrySchema, AgentPatchRecordSchema, ChangeLogEntrySchema, ChangeLogFileChangeSchema, ChangeLogParityStatusSchema, ChangeLogRecordSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, CorrectionEntrySchema, CorrectionRecordSchema, EmitterSchema, LedgerEntrySchema, LedgerLinksSchema, LedgerPhaseSchema, LedgerRecordSchema, NoteEntrySchema, OperatorDecisionActionSchema, OperatorDecisionEntrySchema, OperatorDecisionPayloadSchema, OperatorDecisionRecordSchema, OperatorPatchEntrySchema, OperatorPatchRecordSchema, parseLedgerEntry, parseLedgerRecord, RuntimeEventEntrySchema, StripDecisionRevertedEntrySchema, StripDecisionRevertedPayloadSchema, StripDecisionRevertedRecordSchema, ToolInvocationEntrySchema, ToolInvocationRecordSchema, ValidatorResultEntrySchema, ValidatorResultRecordSchema, } from "./schema/entry.js";
7
10
  export { NoteSubkindSchema } from "./schema/note.js";
8
11
  export { resolveLedgerPaths } from "./storage/filesystem.js";
package/dist/list.d.ts CHANGED
@@ -8,9 +8,10 @@ export type LedgerFilter = {
8
8
  readonly since?: string;
9
9
  };
10
10
  export declare const getOrderedEntryLocations: (manifest: LedgerManifest, readIndex: LedgerReadIndex, filter: LedgerFilter, direction?: "asc" | "desc") => ManifestEntryLocation[];
11
- export declare const readManifestEntryBatch: ({ allowMissing, entryLocations, workspaceRoot, }: {
11
+ export declare const readManifestEntryBatch: ({ allowMissing, entryLocations, entryReadConcurrency, workspaceRoot, }: {
12
12
  readonly allowMissing?: boolean;
13
13
  readonly entryLocations: readonly ManifestEntryLocation[];
14
+ readonly entryReadConcurrency?: number;
14
15
  readonly workspaceRoot: string;
15
16
  }) => Promise<({
16
17
  entry: {
@@ -377,7 +378,7 @@ export declare const readManifestEntryBatch: ({ allowMissing, entryLocations, wo
377
378
  sequence: number;
378
379
  };
379
380
  })[]>;
380
- export declare const iterateEntriesFromManifest: (workspaceRoot: string, manifest: LedgerManifest, readIndex: LedgerReadIndex, filter?: LedgerFilter) => AsyncIterable<LedgerEntry>;
381
+ export declare const iterateEntriesFromManifest: (workspaceRoot: string, manifest: LedgerManifest, readIndex: LedgerReadIndex, filter?: LedgerFilter, direction?: "asc" | "desc") => AsyncIterable<LedgerEntry>;
381
382
  export declare const listEntries: (workspaceRoot: string, filter?: LedgerFilter) => AsyncIterable<LedgerEntry>;
382
383
  export declare const readAllEntries: (workspaceRoot: string, filter?: LedgerFilter) => Promise<LedgerEntry[]>;
383
384
  //# sourceMappingURL=list.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../src/list.ts"],"names":[],"mappings":"AACA,OAAO,EACH,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAG7B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAoB,MAAM,mBAAmB,CAAC;AAC1G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAmDF,eAAO,MAAM,wBAAwB,GACjC,UAAU,cAAc,EACxB,WAAW,eAAe,EAC1B,QAAQ,YAAY,EACpB,YAAW,KAAK,GAAG,MAAc,4BAgBpC,CAAC;AAgCF,eAAO,MAAM,sBAAsB,GAAU,kDAI1C;IACC,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAQI,CAAC;AA6CN,eAAO,MAAM,0BAA0B,GACnC,eAAe,MAAM,EACrB,UAAU,cAAc,EACxB,WAAW,eAAe,EAC1B,SAAQ,YAAiB,KAC1B,aAAa,CAAC,WAAW,CAwB3B,CAAC;AAEF,eAAO,MAAM,WAAW,GACpB,eAAe,MAAM,EACrB,SAAQ,YAAiB,KAC1B,aAAa,CAAC,WAAW,CAG3B,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,eAAe,MAAM,EAAE,SAAQ,YAAiB,KAAG,OAAO,CAAC,WAAW,EAAE,CAM5G,CAAC"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../src/list.ts"],"names":[],"mappings":"AACA,OAAO,EACH,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAG7B,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,KAAK,WAAW,EAAoB,MAAM,mBAAmB,CAAC;AAC1G,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAgDF,eAAO,MAAM,wBAAwB,GACjC,UAAU,cAAc,EACxB,WAAW,eAAe,EAC1B,QAAQ,YAAY,EACpB,YAAW,KAAK,GAAG,MAAc,4BAgBpC,CAAC;AAiCF,eAAO,MAAM,sBAAsB,GAAU,wEAK1C;IACC,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1D,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAQI,CAAC;AAsDN,eAAO,MAAM,0BAA0B,GACnC,eAAe,MAAM,EACrB,UAAU,cAAc,EACxB,WAAW,eAAe,EAC1B,SAAQ,YAAiB,EACzB,YAAW,KAAK,GAAG,MAAc,KAClC,aAAa,CAAC,WAAW,CAoC3B,CAAC;AAEF,eAAO,MAAM,WAAW,GACpB,eAAe,MAAM,EACrB,SAAQ,YAAiB,KAC1B,aAAa,CAAC,WAAW,CAG3B,CAAC;AAEF,eAAO,MAAM,cAAc,GAAU,eAAe,MAAM,EAAE,SAAQ,YAAiB,KAAG,OAAO,CAAC,WAAW,EAAE,CAM5G,CAAC"}
package/dist/list.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mapWithConcurrencyLimit } from "./async.js";
2
2
  import { matchesReadIndexFilter, } from "./read-index.js";
3
+ import { getLedgerRuntimeConfig } from "./runtime-config.js";
3
4
  import { loadLedgerState } from "./recovery.js";
4
5
  import { parseLedgerEntry } from "./schema/entry.js";
5
6
  import { readPhaseEntryText } from "./storage/filesystem.js";
@@ -15,8 +16,6 @@ const matchesFilter = (entry, filter) => {
15
16
  }
16
17
  return true;
17
18
  };
18
- const ENTRY_READ_BATCH_SIZE = 32;
19
- const ENTRY_READ_CONCURRENCY = 16;
20
19
  const getFilteredReadIndexEntries = ({ direction, filter, manifest, readIndex, }) => {
21
20
  const sourceEntries = direction === 'asc' ? readIndex.entries : [...readIndex.entries].reverse();
22
21
  const filteredEntries = [];
@@ -71,20 +70,21 @@ const readManifestEntry = async ({ allowMissing, entryId, location, workspaceRoo
71
70
  throw error;
72
71
  }
73
72
  };
74
- export const readManifestEntryBatch = async ({ allowMissing = false, entryLocations, workspaceRoot, }) => mapWithConcurrencyLimit(entryLocations, ENTRY_READ_CONCURRENCY, async ([entryId, location]) => readManifestEntry({
73
+ export const readManifestEntryBatch = async ({ allowMissing = false, entryLocations, entryReadConcurrency = getLedgerRuntimeConfig().scanConcurrency, workspaceRoot, }) => mapWithConcurrencyLimit(entryLocations, entryReadConcurrency, async ([entryId, location]) => readManifestEntry({
75
74
  allowMissing,
76
75
  entryId,
77
76
  location,
78
77
  workspaceRoot,
79
78
  }));
80
- const collectLimitedEntries = async ({ filter, manifest, readIndex, workspaceRoot, }) => {
79
+ const collectLimitedEntries = async ({ batchSize, direction, entryReadConcurrency, filter, manifest, readIndex, workspaceRoot, }) => {
81
80
  const collectedEntries = [];
82
81
  const orderedEntries = getOrderedEntryLocations(manifest, readIndex, filter, 'desc');
83
82
  const limit = filter.limit ?? 0;
84
- for (let index = 0; index < orderedEntries.length && collectedEntries.length < limit; index += ENTRY_READ_BATCH_SIZE) {
85
- const batch = orderedEntries.slice(index, index + ENTRY_READ_BATCH_SIZE);
83
+ for (let index = 0; index < orderedEntries.length && collectedEntries.length < limit; index += batchSize) {
84
+ const batch = orderedEntries.slice(index, index + batchSize);
86
85
  const resolvedEntries = await readManifestEntryBatch({
87
86
  entryLocations: batch,
87
+ entryReadConcurrency,
88
88
  workspaceRoot,
89
89
  });
90
90
  for (const resolvedEntry of resolvedEntries) {
@@ -99,21 +99,33 @@ const collectLimitedEntries = async ({ filter, manifest, readIndex, workspaceRoo
99
99
  }
100
100
  }
101
101
  }
102
- collectedEntries.reverse();
102
+ if (direction === 'asc') {
103
+ collectedEntries.reverse();
104
+ }
103
105
  return collectedEntries;
104
106
  };
105
- export const iterateEntriesFromManifest = async function* (workspaceRoot, manifest, readIndex, filter = {}) {
107
+ export const iterateEntriesFromManifest = async function* (workspaceRoot, manifest, readIndex, filter = {}, direction = 'asc') {
108
+ const { scanBatchSize, scanConcurrency } = getLedgerRuntimeConfig();
106
109
  if (filter.limit) {
107
- for (const entry of await collectLimitedEntries({ filter, manifest, readIndex, workspaceRoot })) {
110
+ for (const entry of await collectLimitedEntries({
111
+ batchSize: scanBatchSize,
112
+ direction,
113
+ entryReadConcurrency: scanConcurrency,
114
+ filter,
115
+ manifest,
116
+ readIndex,
117
+ workspaceRoot,
118
+ })) {
108
119
  yield entry;
109
120
  }
110
121
  return;
111
122
  }
112
- const orderedEntries = getOrderedEntryLocations(manifest, readIndex, filter);
113
- for (let index = 0; index < orderedEntries.length; index += ENTRY_READ_BATCH_SIZE) {
114
- const batch = orderedEntries.slice(index, index + ENTRY_READ_BATCH_SIZE);
123
+ const orderedEntries = getOrderedEntryLocations(manifest, readIndex, filter, direction);
124
+ for (let index = 0; index < orderedEntries.length; index += scanBatchSize) {
125
+ const batch = orderedEntries.slice(index, index + scanBatchSize);
115
126
  const resolvedEntries = await readManifestEntryBatch({
116
127
  entryLocations: batch,
128
+ entryReadConcurrency: scanConcurrency,
117
129
  workspaceRoot,
118
130
  });
119
131
  for (const resolvedEntry of resolvedEntries) {
package/dist/note.d.ts CHANGED
@@ -1,31 +1,38 @@
1
1
  import { appendRecord } from './record.ts';
2
2
  import type { LedgerPhase } from './schema/entry.ts';
3
3
  import type { NoteSubkind } from './schema/note.ts';
4
+ /** Input shared by the narrative and note helper wrappers. */
4
5
  export type NoteBody = {
5
6
  readonly body: string;
6
7
  readonly phase: LedgerPhase;
7
8
  readonly summary: string;
8
9
  };
10
+ /** Append a typed note entry using the library emitter metadata. */
9
11
  export declare const appendNote: (workspaceRoot: string, subkind: NoteSubkind, noteBody: NoteBody) => Promise<{
10
12
  entry: Awaited<ReturnType<typeof appendRecord>>["entry"];
11
13
  id: string;
12
14
  }>;
15
+ /** Append a `cleanup-wave` narrative note. */
13
16
  export declare const appendCleanupWaveNote: (workspaceRoot: string, noteBody: NoteBody) => Promise<{
14
17
  entry: Awaited<ReturnType<typeof appendRecord>>["entry"];
15
18
  id: string;
16
19
  }>;
20
+ /** Append a `verified-flow` narrative note. */
17
21
  export declare const appendVerifiedFlowNote: (workspaceRoot: string, noteBody: NoteBody) => Promise<{
18
22
  entry: Awaited<ReturnType<typeof appendRecord>>["entry"];
19
23
  id: string;
20
24
  }>;
25
+ /** Append an `open-issue` narrative note. */
21
26
  export declare const appendOpenIssueNote: (workspaceRoot: string, noteBody: NoteBody) => Promise<{
22
27
  entry: Awaited<ReturnType<typeof appendRecord>>["entry"];
23
28
  id: string;
24
29
  }>;
30
+ /** Append a `decomposition-wave` narrative note. */
25
31
  export declare const appendDecompositionWaveNote: (workspaceRoot: string, noteBody: NoteBody) => Promise<{
26
32
  entry: Awaited<ReturnType<typeof appendRecord>>["entry"];
27
33
  id: string;
28
34
  }>;
35
+ /** Append the current `semantic-cleanup-summary` note for workspace narrative rendering. */
29
36
  export declare const appendSemanticCleanupSummaryNote: (workspaceRoot: string, noteBody: NoteBody) => Promise<{
30
37
  entry: Awaited<ReturnType<typeof appendRecord>>["entry"];
31
38
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"note.d.ts","sourceRoot":"","sources":["../src/note.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,MAAM,MAAM,QAAQ,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,UAAU,GACnB,eAAe,MAAM,EACrB,SAAS,WAAW,EACpB,UAAU,QAAQ,KACnB,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAYlF,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WAd3D,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EAe1B,CAAC;AAExD,eAAO,MAAM,sBAAsB,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WAjB5D,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EAkBzB,CAAC;AAEzD,eAAO,MAAM,mBAAmB,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WApBzD,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EAqB5B,CAAC;AAEtD,eAAO,MAAM,2BAA2B,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WAvBjE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EAwBpB,CAAC;AAE9D,eAAO,MAAM,gCAAgC,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WA1BtE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EA2Bd,CAAC"}
1
+ {"version":3,"file":"note.d.ts","sourceRoot":"","sources":["../src/note.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpD,8DAA8D;AAC9D,MAAM,MAAM,QAAQ,GAAG;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,UAAU,GACnB,eAAe,MAAM,EACrB,SAAS,WAAW,EACpB,UAAU,QAAQ,KACnB,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAYlF,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,qBAAqB,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WAf3D,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EAgB1B,CAAC;AAExD,+CAA+C;AAC/C,eAAO,MAAM,sBAAsB,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WAnB5D,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EAoBzB,CAAC;AAEzD,6CAA6C;AAC7C,eAAO,MAAM,mBAAmB,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WAvBzD,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EAwB5B,CAAC;AAEtD,oDAAoD;AACpD,eAAO,MAAM,2BAA2B,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WA3BjE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EA4BpB,CAAC;AAE9D,4FAA4F;AAC5F,eAAO,MAAM,gCAAgC,GAAI,eAAe,MAAM,EAAE,UAAU,QAAQ;WA/BtE,OAAO,CAAC,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC;QAAM,MAAM;EAgCd,CAAC"}
package/dist/note.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { appendRecord } from "./record.js";
2
2
  import { LEDGER_LIBRARY_VERSION } from "./version.js";
3
+ /** Append a typed note entry using the library emitter metadata. */
3
4
  export const appendNote = async (workspaceRoot, subkind, noteBody) => {
4
5
  return appendRecord(workspaceRoot, {
5
6
  body: noteBody.body,
@@ -13,8 +14,13 @@ export const appendNote = async (workspaceRoot, subkind, noteBody) => {
13
14
  summary: noteBody.summary,
14
15
  });
15
16
  };
17
+ /** Append a `cleanup-wave` narrative note. */
16
18
  export const appendCleanupWaveNote = (workspaceRoot, noteBody) => appendNote(workspaceRoot, 'cleanup-wave', noteBody);
19
+ /** Append a `verified-flow` narrative note. */
17
20
  export const appendVerifiedFlowNote = (workspaceRoot, noteBody) => appendNote(workspaceRoot, 'verified-flow', noteBody);
21
+ /** Append an `open-issue` narrative note. */
18
22
  export const appendOpenIssueNote = (workspaceRoot, noteBody) => appendNote(workspaceRoot, 'open-issue', noteBody);
23
+ /** Append a `decomposition-wave` narrative note. */
19
24
  export const appendDecompositionWaveNote = (workspaceRoot, noteBody) => appendNote(workspaceRoot, 'decomposition-wave', noteBody);
25
+ /** Append the current `semantic-cleanup-summary` note for workspace narrative rendering. */
20
26
  export const appendSemanticCleanupSummaryNote = (workspaceRoot, noteBody) => appendNote(workspaceRoot, 'semantic-cleanup-summary', noteBody);
@@ -14,6 +14,18 @@ type ResolvedOperatorPatchRecord = Omit<OperatorPatchRecord, 'diffPath' | 'diffT
14
14
  readonly links: LedgerLinks;
15
15
  };
16
16
  export type ResolvedPatchRecord = ResolvedAgentPatchRecord | ResolvedOperatorPatchRecord;
17
+ /**
18
+ * Derive `change-log.filesChanged` metadata from a git-style unified diff.
19
+ *
20
+ * Example:
21
+ * `deriveFilesChangedFromPatch(await readFile('/tmp/change.patch', 'utf8'))`
22
+ *
23
+ * Known limits:
24
+ * - Requires `diff --git` headers to identify file boundaries.
25
+ * - Counts only textual hunk `+` and `-` lines, so binary or mode-only diffs can yield zero line counts.
26
+ * - Uses the post-image path when available and rejects diff blocks that cannot be mapped to a normalized
27
+ * workspace-relative path.
28
+ */
17
29
  export declare const deriveFilesChangedFromPatch: (patchText: string) => ChangeLogFileChange[];
18
30
  export declare function resolvePatchRecord(args: {
19
31
  readonly record: AgentPatchRecord;
@@ -1 +1 @@
1
- {"version":3,"file":"patch-resolver.d.ts","sourceRoot":"","sources":["../src/patch-resolver.ts"],"names":[],"mappings":"AAIA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,YAAY,EAEpB,MAAM,mBAAmB,CAAC;AAI3B,KAAK,gBAAgB,GAAG,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,CAAC;AACvE,KAAK,mBAAmB,GAAG,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAC,CAAC;AAQ7E,KAAK,wBAAwB,GAAG,IAAI,CAAC,gBAAgB,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC,GAAG;IACxF,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC/B,CAAC;AACF,KAAK,2BAA2B,GAAG,IAAI,CAAC,mBAAmB,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC,GAAG;IAC9F,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,wBAAwB,GAAG,2BAA2B,CAAC;AAqKzF,eAAO,MAAM,2BAA2B,GAAI,WAAW,MAAM,KAAG,mBAAmB,EAuClF,CAAC;AAmBF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;AACtC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC"}
1
+ {"version":3,"file":"patch-resolver.d.ts","sourceRoot":"","sources":["../src/patch-resolver.ts"],"names":[],"mappings":"AASA,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,YAAY,EAEpB,MAAM,mBAAmB,CAAC;AAK3B,KAAK,gBAAgB,GAAG,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,aAAa,CAAA;CAAE,CAAC,CAAC;AACvE,KAAK,mBAAmB,GAAG,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAC,CAAC;AAQ7E,KAAK,wBAAwB,GAAG,IAAI,CAAC,gBAAgB,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC,GAAG;IACxF,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC/B,CAAC;AACF,KAAK,2BAA2B,GAAG,IAAI,CAAC,mBAAmB,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC,GAAG;IAC9F,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,wBAAwB,GAAG,2BAA2B,CAAC;AAsTzF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B,GAAI,WAAW,MAAM,KAAG,mBAAmB,EAuClF,CAAC;AAmBF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAAC;AACtC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC,GAAG,OAAO,CAAC,2BAA2B,CAAC,CAAC"}
@@ -1,28 +1,94 @@
1
- import { readFile } from 'node:fs/promises';
1
+ import { readFile, stat } 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";
4
+ import { assertPatchTextWithinLimit, readPatchTextFromFile, resolveBlobPath, storePatchBlob, } from "./blobs.js";
5
5
  import { WorkspaceRelativePathSchema, } from "./schema/entry.js";
6
+ import { forEachLine } from "./text-lines.js";
6
7
  const readBlobText = async (workspaceRoot, blobSha256) => readFile(resolveBlobPath(workspaceRoot, blobSha256), 'utf8');
7
8
  const formatRecordContext = (record) => `${record.kind} "${record.summary}" [${record.phase}]`;
8
9
  const normalizeWorkspaceRelativePath = (value) => v.parse(WorkspaceRelativePathSchema, value.replaceAll('\\', '/'));
9
- const stripGitPathPrefix = (value) => {
10
+ const normalizeDiffPathToken = ({ headerPrefixes, side, value, }) => {
10
11
  const normalized = value.trim().replace(/^"([^"]*)"$/u, '$1');
11
12
  if (normalized === '/dev/null') {
12
13
  return null;
13
14
  }
14
- if (normalized.startsWith('a/') || normalized.startsWith('b/')) {
15
- return normalized.slice(2);
15
+ const expectedPrefix = side === 'old' ? headerPrefixes?.oldPrefix : headerPrefixes?.newPrefix;
16
+ if (expectedPrefix && normalized.startsWith(expectedPrefix)) {
17
+ return normalized.slice(expectedPrefix.length);
16
18
  }
17
19
  return normalized;
18
20
  };
19
- const inferHeaderPath = (line) => {
20
- const match = /^diff --git (?:"a\/([^"]+)"|a\/(\S+)) (?:"b\/([^"]+)"|b\/(\S+))$/u.exec(line.trim());
21
- if (!match) {
21
+ const splitPrefixCandidate = (value) => {
22
+ const slashIndex = value.indexOf('/');
23
+ if (slashIndex <= 0) {
22
24
  return null;
23
25
  }
24
- const rightPath = match[3] ?? match[4];
25
- return rightPath ? stripGitPathPrefix(`b/${rightPath}`) : null;
26
+ return {
27
+ prefix: value.slice(0, slashIndex + 1),
28
+ suffix: value.slice(slashIndex + 1),
29
+ };
30
+ };
31
+ const readDiffHeaderToken = (headerBody, startIndex) => {
32
+ let index = startIndex;
33
+ while (headerBody[index] === ' ') {
34
+ index += 1;
35
+ }
36
+ if (index >= headerBody.length) {
37
+ return null;
38
+ }
39
+ if (headerBody[index] === '"') {
40
+ const endIndex = headerBody.indexOf('"', index + 1);
41
+ if (endIndex === -1) {
42
+ return null;
43
+ }
44
+ return {
45
+ nextIndex: endIndex + 1,
46
+ token: headerBody.slice(index + 1, endIndex),
47
+ };
48
+ }
49
+ let endIndex = index;
50
+ while (endIndex < headerBody.length && headerBody[endIndex] !== ' ') {
51
+ endIndex += 1;
52
+ }
53
+ return {
54
+ nextIndex: endIndex + 1,
55
+ token: headerBody.slice(index, endIndex),
56
+ };
57
+ };
58
+ const resolveHeaderPrefixes = (oldPath, newPath) => {
59
+ if (oldPath.startsWith('a/') && newPath.startsWith('b/')) {
60
+ return {
61
+ newPrefix: 'b/',
62
+ oldPrefix: 'a/',
63
+ };
64
+ }
65
+ const oldCandidate = splitPrefixCandidate(oldPath);
66
+ const newCandidate = splitPrefixCandidate(newPath);
67
+ if (!oldCandidate ||
68
+ !newCandidate ||
69
+ oldCandidate.prefix === newCandidate.prefix ||
70
+ oldCandidate.suffix !== newCandidate.suffix) {
71
+ return null;
72
+ }
73
+ return {
74
+ newPrefix: newCandidate.prefix,
75
+ oldPrefix: oldCandidate.prefix,
76
+ };
77
+ };
78
+ const parseDiffHeaderPaths = (line) => {
79
+ const headerBody = line.trim().slice('diff --git '.length);
80
+ const firstToken = readDiffHeaderToken(headerBody, 0);
81
+ const secondToken = firstToken ? readDiffHeaderToken(headerBody, firstToken.nextIndex) : null;
82
+ if (!firstToken?.token || !secondToken?.token) {
83
+ return null;
84
+ }
85
+ const oldPath = firstToken.token;
86
+ const newPath = secondToken.token;
87
+ return {
88
+ headerPrefixes: resolveHeaderPrefixes(oldPath, newPath),
89
+ newPath,
90
+ oldPath,
91
+ };
26
92
  };
27
93
  const finalizeFileChange = (current, filesChanged) => {
28
94
  if (!current) {
@@ -38,19 +104,33 @@ const finalizeFileChange = (current, filesChanged) => {
38
104
  removed: current.removed > 0 ? current.removed : undefined,
39
105
  });
40
106
  };
41
- const createDiffFileAccumulator = (line) => ({
42
- added: 0,
43
- headerPath: inferHeaderPath(line),
44
- minusPath: null,
45
- plusPath: null,
46
- removed: 0,
47
- });
107
+ const createDiffFileAccumulator = (line) => {
108
+ const headerPaths = parseDiffHeaderPaths(line);
109
+ return {
110
+ added: 0,
111
+ headerPath: headerPaths
112
+ ? normalizeDiffPathToken({
113
+ headerPrefixes: headerPaths.headerPrefixes,
114
+ side: 'new',
115
+ value: headerPaths.newPath,
116
+ })
117
+ : null,
118
+ headerPrefixes: headerPaths?.headerPrefixes ?? null,
119
+ minusPath: null,
120
+ plusPath: null,
121
+ removed: 0,
122
+ };
123
+ };
48
124
  const applyPatchMetadataLine = (current, line) => {
49
125
  if (line.startsWith('+++ ')) {
50
126
  return {
51
127
  next: {
52
128
  ...current,
53
- plusPath: stripGitPathPrefix(line.slice(4)),
129
+ plusPath: normalizeDiffPathToken({
130
+ headerPrefixes: current.headerPrefixes,
131
+ side: 'new',
132
+ value: line.slice(4),
133
+ }),
54
134
  },
55
135
  };
56
136
  }
@@ -58,7 +138,35 @@ const applyPatchMetadataLine = (current, line) => {
58
138
  return {
59
139
  next: {
60
140
  ...current,
61
- minusPath: stripGitPathPrefix(line.slice(4)),
141
+ minusPath: normalizeDiffPathToken({
142
+ headerPrefixes: current.headerPrefixes,
143
+ side: 'old',
144
+ value: line.slice(4),
145
+ }),
146
+ },
147
+ };
148
+ }
149
+ if (line.startsWith('rename from ')) {
150
+ return {
151
+ next: {
152
+ ...current,
153
+ minusPath: normalizeDiffPathToken({
154
+ headerPrefixes: null,
155
+ side: 'old',
156
+ value: line.slice('rename from '.length),
157
+ }),
158
+ },
159
+ };
160
+ }
161
+ if (line.startsWith('rename to ')) {
162
+ return {
163
+ next: {
164
+ ...current,
165
+ plusPath: normalizeDiffPathToken({
166
+ headerPrefixes: null,
167
+ side: 'new',
168
+ value: line.slice('rename to '.length),
169
+ }),
62
170
  },
63
171
  };
64
172
  }
@@ -74,16 +182,12 @@ const applyPatchMetadataLine = (current, line) => {
74
182
  };
75
183
  const applyPatchHunkLine = (current, line) => {
76
184
  if (line.startsWith('+')) {
77
- return {
78
- ...current,
79
- added: current.added + 1,
80
- };
185
+ current.added += 1;
186
+ return current;
81
187
  }
82
188
  if (line.startsWith('-')) {
83
- return {
84
- ...current,
85
- removed: current.removed + 1,
86
- };
189
+ current.removed += 1;
190
+ return current;
87
191
  }
88
192
  return current;
89
193
  };
@@ -91,71 +195,103 @@ const mergeBlobLink = (links, blobSha256) => ({
91
195
  ...links,
92
196
  blobs: [...new Set([...(links?.blobs ?? []), blobSha256])],
93
197
  });
94
- const resolvePatchSource = async ({ record, workspaceRoot, }) => {
95
- if (record.diffPath) {
96
- const resolvedPath = path.resolve(workspaceRoot, record.diffPath);
198
+ const resolvePatchSourceFromFile = async (record, workspaceRoot) => {
199
+ const resolvedPath = path.resolve(workspaceRoot, record.diffPath);
200
+ try {
97
201
  return {
98
- patchText: await readFile(resolvedPath, 'utf8'),
202
+ patchText: await readPatchTextFromFile(resolvedPath),
99
203
  source: 'file',
100
204
  sourceLabel: resolvedPath,
101
205
  };
102
206
  }
103
- if (record.diffText) {
207
+ catch (error) {
208
+ throw new Error(`Failed to read diff file ${resolvedPath} for ${formatRecordContext(record)}: ${error instanceof Error ? error.message : String(error)}`);
209
+ }
210
+ };
211
+ const resolvePatchSourceFromInlineText = (record) => {
212
+ try {
213
+ assertPatchTextWithinLimit(record.diffText, 'inline diff text');
104
214
  return {
105
215
  patchText: record.diffText,
106
216
  source: 'inline',
107
217
  sourceLabel: 'inline diff text',
108
218
  };
109
219
  }
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;
220
+ catch (error) {
221
+ throw new Error(`Failed to validate inline diff text for ${formatRecordContext(record)}: ${error instanceof Error ? error.message : String(error)}`);
222
+ }
223
+ };
224
+ const resolvePatchSourceFromStoredBlob = async (record, workspaceRoot) => {
225
+ try {
226
+ return {
227
+ patchText: await readBlobText(workspaceRoot, record.diff?.blobSha256),
228
+ source: 'blob',
229
+ sourceLabel: `blob ${record.diff?.blobSha256}`,
230
+ };
231
+ }
232
+ catch (error) {
233
+ if (error.code === 'ENOENT') {
234
+ throw new Error(`Patch blob ${record.diff?.blobSha256} was not found for ${formatRecordContext(record)}. Store it first or use diffPath.`);
123
235
  }
236
+ throw error;
237
+ }
238
+ };
239
+ const resolvePatchSource = async ({ record, workspaceRoot, }) => {
240
+ if (record.diffPath) {
241
+ return resolvePatchSourceFromFile(record, workspaceRoot);
242
+ }
243
+ if (record.diffText) {
244
+ return resolvePatchSourceFromInlineText(record);
245
+ }
246
+ if (record.diff) {
247
+ return resolvePatchSourceFromStoredBlob(record, workspaceRoot);
124
248
  }
125
249
  throw new Error(`${record.kind} records require diff, diffPath, or diffText.`);
126
250
  };
251
+ /**
252
+ * Derive `change-log.filesChanged` metadata from a git-style unified diff.
253
+ *
254
+ * Example:
255
+ * `deriveFilesChangedFromPatch(await readFile('/tmp/change.patch', 'utf8'))`
256
+ *
257
+ * Known limits:
258
+ * - Requires `diff --git` headers to identify file boundaries.
259
+ * - Counts only textual hunk `+` and `-` lines, so binary or mode-only diffs can yield zero line counts.
260
+ * - Uses the post-image path when available and rejects diff blocks that cannot be mapped to a normalized
261
+ * workspace-relative path.
262
+ */
127
263
  export const deriveFilesChangedFromPatch = (patchText) => {
128
264
  const filesChanged = [];
129
265
  let current;
130
266
  let insideHunk = false;
131
- for (const line of patchText.split(/\r?\n/u)) {
267
+ forEachLine(patchText, (line) => {
132
268
  if (line.startsWith('diff --git ')) {
133
269
  finalizeFileChange(current, filesChanged);
134
270
  current = createDiffFileAccumulator(line);
135
271
  insideHunk = false;
136
- continue;
272
+ return;
137
273
  }
138
274
  if (!current) {
139
- continue;
275
+ return;
140
276
  }
141
277
  if (insideHunk) {
142
278
  if (line.startsWith('@@')) {
143
- continue;
279
+ return;
144
280
  }
145
281
  current = applyPatchHunkLine(current, line);
146
- continue;
282
+ return;
147
283
  }
148
284
  const metadataUpdate = applyPatchMetadataLine(current, line);
149
285
  current = metadataUpdate.next;
150
286
  if (metadataUpdate.startHunk) {
151
287
  insideHunk = true;
152
- continue;
288
+ return;
153
289
  }
154
290
  if (!insideHunk) {
155
- continue;
291
+ return;
156
292
  }
157
293
  current = applyPatchHunkLine(current, line);
158
- }
294
+ });
159
295
  finalizeFileChange(current, filesChanged);
160
296
  return filesChanged;
161
297
  };
@@ -168,6 +304,22 @@ const buildResolvedPatchRecord = ({ links, record, storedDiff, }) => {
168
304
  };
169
305
  };
170
306
  export async function resolvePatchRecord({ record, workspaceRoot, }) {
307
+ if (record.diff && !record.diffPath && !record.diffText) {
308
+ try {
309
+ await stat(resolveBlobPath(workspaceRoot, record.diff.blobSha256));
310
+ }
311
+ catch (error) {
312
+ if (error.code === 'ENOENT') {
313
+ throw new Error(`Patch blob ${record.diff.blobSha256} was not found for ${formatRecordContext(record)}. Store it first or use diffPath.`);
314
+ }
315
+ throw new Error(`Failed to read patch blob ${record.diff.blobSha256} for ${formatRecordContext(record)}: ${error instanceof Error ? error.message : String(error)}`);
316
+ }
317
+ return buildResolvedPatchRecord({
318
+ links: mergeBlobLink(record.links, record.diff.blobSha256),
319
+ record,
320
+ storedDiff: record.diff,
321
+ });
322
+ }
171
323
  const { patchText, sourceLabel } = await resolvePatchSource({
172
324
  record,
173
325
  workspaceRoot,