ushman-ledger 1.2.1 → 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.
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { promisify } from 'node:util';
8
8
  import * as v from 'valibot';
9
+ import { readPatchTextFromFile } from "./blobs.js";
9
10
  import { openLedger } from "./handle.js";
10
11
  import { deriveFilesChangedFromPatch } from "./patch-resolver.js";
11
12
  import { ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, LEDGER_KINDS, LEDGER_PHASES, parseLedgerRecord, WorkspaceRelativePathSchema, } from "./schema/entry.js";
@@ -14,6 +15,8 @@ import { LEDGER_LIBRARY_VERSION } from "./version.js";
14
15
  const execFileAsync = promisify(execFile);
15
16
  const DEFAULT_GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
16
17
  const DEFAULT_GIT_DIFF_TIMEOUT_MS = 30_000;
18
+ const BOOLEAN_FLAG_NAMES = new Set(['from-stdin', 'help', 'json']);
19
+ const GIT_DIFF_FORMAT_ARGS = ['--no-color', '--no-ext-diff', '--src-prefix=a/', '--dst-prefix=b/'];
17
20
  const RENDER_TARGETS = [
18
21
  'retro',
19
22
  'jsonl',
@@ -54,6 +57,15 @@ const renderValidValues = () => `Valid values:
54
57
  note subkinds: ${NoteSubkindSchema.options.join(', ')}
55
58
  render targets: ${RENDER_TARGETS.join(', ')}
56
59
  `;
60
+ const renderRuntimeTuningHelp = () => `Runtime tuning env vars:
61
+ USHMAN_LEDGER_SCAN_BATCH_SIZE (default: 32)
62
+ USHMAN_LEDGER_SCAN_CONCURRENCY (default: 16)
63
+ USHMAN_LEDGER_READ_INDEX_REBUILD_BATCH_SIZE (default: USHMAN_LEDGER_SCAN_BATCH_SIZE)
64
+ USHMAN_LEDGER_READ_INDEX_REBUILD_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
65
+ USHMAN_LEDGER_COVERAGE_FILE_STAT_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
66
+ USHMAN_LEDGER_BLOB_HASH_CONCURRENCY (default: USHMAN_LEDGER_SCAN_CONCURRENCY)
67
+ USHMAN_LEDGER_MAX_PATCH_BYTES (default: 10485760)
68
+ `;
57
69
  const renderHelp = (commandName) => `${commandName}
58
70
 
59
71
  Commands:
@@ -64,28 +76,37 @@ Commands:
64
76
  ${commandName} tail [--workspace=<ws>] [--phase=<phase>] [--limit=<n>]
65
77
  ${commandName} render [--workspace=<ws>] [--to=retro|jsonl|timeline-html|dependency-graph|migration-log-md|workspace-narrative-md] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>]
66
78
  ${commandName} archive [--workspace=<ws>] --out=<file.tgz>
67
- ${commandName} doctor [--workspace=<ws>]
79
+ ${commandName} doctor [--workspace=<ws>] [--json]
68
80
  ${commandName} --version
69
81
 
70
- ${renderValidValues()}`;
82
+ ${renderValidValues()}
83
+ ${renderRuntimeTuningHelp()}`;
71
84
  const renderCommandHelp = (commandName, command) => {
72
85
  switch (command) {
73
86
  case 'record':
74
87
  return `${renderRecordUsage(commandName)}
75
88
 
76
- ${renderValidValues()}`;
89
+ ${renderValidValues()}
90
+ ${renderRuntimeTuningHelp()}`;
77
91
  case 'note':
78
92
  return `${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
79
93
 
80
- ${renderValidValues()}`;
94
+ ${renderValidValues()}
95
+ ${renderRuntimeTuningHelp()}`;
81
96
  case 'list':
82
97
  return `${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
83
98
 
84
- ${renderValidValues()}`;
99
+ ${renderValidValues()}
100
+ ${renderRuntimeTuningHelp()}`;
85
101
  case 'render':
86
102
  return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>]
87
103
 
88
- ${renderValidValues()}`;
104
+ ${renderValidValues()}
105
+ ${renderRuntimeTuningHelp()}`;
106
+ case 'doctor':
107
+ return `${commandName} doctor [--workspace=<ws>] [--json]
108
+
109
+ ${renderRuntimeTuningHelp()}`;
89
110
  default:
90
111
  return renderHelp(commandName);
91
112
  }
@@ -111,7 +132,7 @@ const parseArgv = (argv) => {
111
132
  continue;
112
133
  }
113
134
  const next = argv[index + 1];
114
- if (!next || next.startsWith('--')) {
135
+ if (BOOLEAN_FLAG_NAMES.has(name) || !next || next.startsWith('--')) {
115
136
  flags[name] = true;
116
137
  continue;
117
138
  }
@@ -211,11 +232,31 @@ const isGitDiffTimeoutError = (error) => {
211
232
  error.killed === true &&
212
233
  error.signal === 'SIGTERM');
213
234
  };
235
+ const buildGitDiffArgs = (gitOptions, gitRef) => gitOptions.scopedPaths.length === 0
236
+ ? ['diff', ...GIT_DIFF_FORMAT_ARGS, gitRef]
237
+ : ['diff', ...GIT_DIFF_FORMAT_ARGS, gitRef, '--', ...gitOptions.scopedPaths];
238
+ const throwGitDiffFailure = ({ error, gitOptions, gitRef, }) => {
239
+ if (getErrorCode(error) === 'ENOENT') {
240
+ throw new CliUsageError('git is required for --diff-from-git and was not found in PATH. Install git or use --diff with a patch file.');
241
+ }
242
+ if (isGitDiffTimeoutError(error)) {
243
+ throw new CliUsageError(`git diff ${gitRef} timed out after ${gitOptions.timeoutMs}ms. Narrow the diff or increase --git-diff-timeout-ms.`);
244
+ }
245
+ if (isGitDiffMaxBufferError(error)) {
246
+ throw new CliUsageError(`git diff ${gitRef} exceeded the configured stdout buffer (${gitOptions.maxBufferBytes} bytes). Narrow the diff or increase --git-diff-max-buffer-bytes.`);
247
+ }
248
+ const stderr = typeof error === 'object' && error !== null && 'stderr' in error
249
+ ? String(error.stderr ?? '').trim()
250
+ : '';
251
+ if (stderr.length > 0) {
252
+ throw new CliUsageError(`git diff ${gitRef} failed: ${stderr}`);
253
+ }
254
+ throw new CliUsageError(`git diff ${gitRef} failed. Ensure the ref exists and the workspace is a git repository.`);
255
+ };
214
256
  const materializeGitDiff = async ({ gitOptions, gitRef, workspaceRoot, }) => {
215
257
  let tempDir;
216
258
  try {
217
- const gitArgs = gitOptions.scopedPaths.length === 0 ? ['diff', gitRef] : ['diff', gitRef, '--', ...gitOptions.scopedPaths];
218
- const { stdout } = await execFileAsync('git', gitArgs, {
259
+ const { stdout } = await execFileAsync('git', buildGitDiffArgs(gitOptions, gitRef), {
219
260
  cwd: workspaceRoot,
220
261
  maxBuffer: gitOptions.maxBufferBytes,
221
262
  timeout: gitOptions.timeoutMs,
@@ -229,16 +270,11 @@ const materializeGitDiff = async ({ gitOptions, gitRef, workspaceRoot, }) => {
229
270
  if (tempDir) {
230
271
  await rm(tempDir, { force: true, recursive: true });
231
272
  }
232
- if (getErrorCode(error) === 'ENOENT') {
233
- throw new CliUsageError('git is required for --diff-from-git and was not found in PATH.');
234
- }
235
- if (isGitDiffTimeoutError(error)) {
236
- throw new CliUsageError(`git diff ${gitRef} timed out after ${gitOptions.timeoutMs}ms. Narrow the diff or increase --git-diff-timeout-ms.`);
237
- }
238
- if (isGitDiffMaxBufferError(error)) {
239
- throw new CliUsageError(`git diff ${gitRef} exceeded the configured stdout buffer (${gitOptions.maxBufferBytes} bytes). Narrow the diff or increase --git-diff-max-buffer-bytes.`);
240
- }
241
- throw error;
273
+ return throwGitDiffFailure({
274
+ error,
275
+ gitOptions,
276
+ gitRef,
277
+ });
242
278
  }
243
279
  };
244
280
  const parseJsonInput = (text, sourceLabel) => {
@@ -252,6 +288,9 @@ const parseJsonInput = (text, sourceLabel) => {
252
288
  const print = (context, text) => {
253
289
  context.stdout.write(text.endsWith('\n') ? text : `${text}\n`);
254
290
  };
291
+ const printJson = (context, value) => {
292
+ print(context, JSON.stringify(value, null, 2));
293
+ };
255
294
  const parseLimit = (flags) => getFlag(flags, 'limit')
256
295
  ? parsePositiveIntegerFlag({
257
296
  defaultValue: 0,
@@ -397,7 +436,7 @@ const assertDiffRecordKindSupported = (kind) => {
397
436
  }
398
437
  throw new CliUsageError('--diff and --diff-from-git are only supported for patch and change-log records.');
399
438
  };
400
- const readPatchText = async (diffPath) => readFile(diffPath, 'utf8');
439
+ const readPatchText = async (diffPath) => readPatchTextFromFile(diffPath);
401
440
  const readPatchTextForRecordKind = async (kind, diffPath) => {
402
441
  if (kind === 'change-log' || kind === 'agent-patch' || kind === 'operator-patch') {
403
442
  return readPatchText(diffPath);
@@ -695,16 +734,22 @@ const runNoteCli = async (parsed, context) => {
695
734
  const runListCli = async (parsed, context) => {
696
735
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
697
736
  if (hasFlag(parsed.flags, 'json')) {
698
- const entries = [];
737
+ let wroteEntry = false;
738
+ context.stdout.write('[\n');
699
739
  for await (const entry of ledger.list({
700
740
  kind: parseOptionalKind(parsed.flags),
701
741
  limit: parseLimit(parsed.flags),
702
742
  phase: parseOptionalPhase(parsed.flags),
703
743
  since: parseSince(parsed.flags),
704
744
  })) {
705
- entries.push(entry);
745
+ if (wroteEntry) {
746
+ context.stdout.write(',\n');
747
+ }
748
+ const renderedEntry = JSON.stringify(entry, null, 2).replaceAll('\n', '\n ');
749
+ context.stdout.write(` ${renderedEntry}`);
750
+ wroteEntry = true;
706
751
  }
707
- print(context, JSON.stringify(entries, null, 2));
752
+ context.stdout.write(wroteEntry ? '\n]\n' : ']\n');
708
753
  return 0;
709
754
  }
710
755
  for await (const entry of ledger.list({
@@ -762,16 +807,29 @@ const runArchiveCli = async (parsed, context) => {
762
807
  print(context, result.integrityHash);
763
808
  return 0;
764
809
  };
810
+ const formatDoctorFinding = (finding) => [`[${finding.code}] ${finding.message}`, `Next step: ${finding.remediation}`].join('\n');
811
+ const printDoctorFindings = (context, report) => {
812
+ context.stderr.write(`doctor found ${report.issueCount} issue${report.issueCount === 1 ? '' : 's'}.\n`);
813
+ for (const [index, finding] of report.findings.entries()) {
814
+ context.stderr.write(`\n${formatDoctorFinding(finding)}`);
815
+ if (index < report.findings.length - 1) {
816
+ context.stderr.write('\n');
817
+ }
818
+ }
819
+ context.stderr.write('\n');
820
+ };
765
821
  const runDoctorCli = async (parsed, context) => {
766
822
  const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
767
823
  const result = await ledger.doctor();
824
+ if (hasFlag(parsed.flags, 'json')) {
825
+ printJson(context, result);
826
+ return result.ok ? 0 : 1;
827
+ }
768
828
  if (result.ok) {
769
829
  print(context, 'ok');
770
830
  return 0;
771
831
  }
772
- for (const issue of result.issues) {
773
- context.stderr.write(`${issue}\n`);
774
- }
832
+ printDoctorFindings(context, result);
775
833
  return 1;
776
834
  };
777
835
  const formatCliError = (error) => {
@@ -787,7 +845,7 @@ const formatCliError = (error) => {
787
845
  })
788
846
  .join('\n');
789
847
  }
790
- return error instanceof Error ? (error.stack ?? error.message) : String(error);
848
+ return error instanceof Error ? error.message : String(error);
791
849
  };
792
850
  export const runLedgerCli = async (argv, context = {}) => {
793
851
  const mergedContext = { ...DEFAULT_CONTEXT, ...context };
@@ -1 +1 @@
1
- {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAgGA,MAAM,MAAM,cAAc,GAAG;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC;CACrC,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,cAAc,CAgDnF,CAAC"}
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AA+FA,MAAM,MAAM,cAAc,GAAG;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC;CACrC,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,cAAc,CAiDnF,CAAC"}
package/dist/coverage.js CHANGED
@@ -4,10 +4,10 @@ import { mapWithConcurrencyLimit } from "./async.js";
4
4
  import { CANDIDATE_EXCLUDE_GLOBS, CANDIDATE_FILE_GLOBS } from "./candidate-paths.js";
5
5
  import { readLabManifestMin } from "./lab-min.js";
6
6
  import { loadLedgerState } from "./recovery.js";
7
+ import { getLedgerRuntimeConfig } from "./runtime-config.js";
7
8
  const EXCLUDED_ROOTS = new Set(CANDIDATE_EXCLUDE_GLOBS.map((glob) => glob.replace(/\/\*\*$/u, '')));
8
9
  const CANDIDATE_DIRECTORIES = CANDIDATE_FILE_GLOBS.filter((glob) => glob.endsWith('/**/*')).map((glob) => glob.slice(0, -5));
9
10
  const CANDIDATE_FILES = CANDIDATE_FILE_GLOBS.filter((glob) => !glob.endsWith('/**/*'));
10
- const FILE_STAT_CONCURRENCY = 16;
11
11
  const toPosix = (value) => value.replaceAll(path.sep, '/');
12
12
  const isMissingPathError = (error) => {
13
13
  const code = error.code;
@@ -85,13 +85,14 @@ const getWorkspaceInitMs = async (workspaceRoot) => {
85
85
  return labManifestStat.birthtimeMs || labManifestStat.mtimeMs;
86
86
  };
87
87
  export const computeCoverage = async (workspaceRoot) => {
88
+ const { coverageFileStatConcurrency } = getLedgerRuntimeConfig();
88
89
  const [{ readIndex }, candidateFiles, workspaceInitMs] = await Promise.all([
89
90
  loadLedgerState(workspaceRoot),
90
91
  collectCandidateFiles(workspaceRoot),
91
92
  getWorkspaceInitMs(workspaceRoot),
92
93
  ]);
93
94
  const coverageIndex = new Set(readIndex.coveredFiles.map((filePath) => toPosix(filePath)));
94
- const candidateStats = await mapWithConcurrencyLimit(candidateFiles, FILE_STAT_CONCURRENCY, async (relativePath) => {
95
+ const candidateStats = await mapWithConcurrencyLimit(candidateFiles, coverageFileStatConcurrency, async (relativePath) => {
95
96
  try {
96
97
  return {
97
98
  mtimeMs: (await stat(path.join(workspaceRoot, relativePath))).mtimeMs,
package/dist/doctor.d.ts CHANGED
@@ -1,9 +1,22 @@
1
1
  import { type PreparedLedgerState } from './recovery.ts';
2
+ export declare const DOCTOR_FINDING_CODES: readonly ["blob-corrupt", "blob-missing", "blob-unreadable", "change-log-rollback-missing-target", "change-log-smoke-failure-missing-rollback-plan", "manifest-entry-count-mismatch", "manifest-entry-location-missing", "manifest-entry-missing-on-disk", "manifest-last-sequence-mismatch", "manifest-per-phase-latest-mismatch", "manifest-phase-mismatch", "manifest-sequence-mismatch", "open-issue-stale", "phase-prev-entry-mismatch", "pre-change-checkpoint-stale", "read-failure"];
3
+ export type DoctorFindingCode = (typeof DOCTOR_FINDING_CODES)[number];
4
+ export type DoctorFindingMetadataValue = boolean | null | number | string;
5
+ export type DoctorFinding = {
6
+ readonly code: DoctorFindingCode;
7
+ readonly message: string;
8
+ readonly metadata?: Record<string, DoctorFindingMetadataValue>;
9
+ readonly remediation: string;
10
+ };
11
+ export type DoctorReport = {
12
+ readonly checkedAt: string;
13
+ readonly findings: DoctorFinding[];
14
+ readonly issueCount: number;
15
+ readonly issues: string[];
16
+ readonly ok: boolean;
17
+ };
2
18
  export declare const runLedgerDoctor: (workspaceRoot: string, options?: {
3
19
  readonly skipPrepare?: boolean;
4
20
  readonly state?: PreparedLedgerState;
5
- }) => Promise<{
6
- issues: string[];
7
- ok: boolean;
8
- }>;
21
+ }) => Promise<DoctorReport>;
9
22
  //# sourceMappingURL=doctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AA+V1E,eAAO,MAAM,eAAe,GACxB,eAAe,MAAM,EACrB,UAAS;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAA;CAAO;;;EAyCzF,CAAC"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAS1E,eAAO,MAAM,oBAAoB,8dAiBvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AACtE,MAAM,MAAM,0BAA0B,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1E,MAAM,MAAM,aAAa,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,0BAA0B,CAAC,CAAC;IAC/D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,aAAa,EAAE,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;CACxB,CAAC;AAqjBF,eAAO,MAAM,eAAe,GACxB,eAAe,MAAM,EACrB,UAAS;IAAE,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,mBAAmB,CAAA;CAAO,KACvF,OAAO,CAAC,YAAY,CAqCtB,CAAC"}