ushman-ledger 0.3.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -7
- package/CHANGELOG.md +8 -12
- package/README.md +28 -57
- package/dist/archive-journal.d.ts +29 -18
- package/dist/archive-journal.d.ts.map +1 -1
- package/dist/archive-journal.js +17 -17
- package/dist/blobs.js +3 -3
- package/dist/builders.d.ts +79 -358
- package/dist/builders.d.ts.map +1 -1
- package/dist/builders.js +15 -60
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +227 -52
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +104 -4
- package/dist/handle.d.ts +4 -2
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +20 -15
- package/dist/helpers.d.ts +7 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +38 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/lab-min.d.ts +7 -7
- package/dist/lab-min.d.ts.map +1 -1
- package/dist/lab-min.js +7 -9
- package/dist/list.d.ts +104 -303
- package/dist/list.d.ts.map +1 -1
- package/dist/note.d.ts +20 -0
- package/dist/note.d.ts.map +1 -1
- package/dist/note.js +5 -0
- package/dist/patch-resolver.d.ts +27 -0
- package/dist/patch-resolver.d.ts.map +1 -0
- package/dist/patch-resolver.js +184 -0
- package/dist/read-index.d.ts +45 -57
- package/dist/read-index.d.ts.map +1 -1
- package/dist/read-index.js +16 -34
- package/dist/record.d.ts.map +1 -1
- package/dist/record.js +19 -130
- package/dist/recovery.d.ts +19 -8
- package/dist/recovery.d.ts.map +1 -1
- package/dist/recovery.js +13 -13
- package/dist/render/migration-log.d.ts +3 -0
- package/dist/render/migration-log.d.ts.map +1 -0
- package/dist/render/migration-log.js +72 -0
- package/dist/render/retro.d.ts.map +1 -1
- package/dist/render/retro.js +41 -25
- package/dist/render/workspace-narrative.d.ts +6 -0
- package/dist/render/workspace-narrative.d.ts.map +1 -0
- package/dist/render/workspace-narrative.js +69 -0
- package/dist/schema/entry-core.d.ts +110 -0
- package/dist/schema/entry-core.d.ts.map +1 -0
- package/dist/schema/entry-core.js +143 -0
- package/dist/schema/entry-migrations.d.ts +3 -0
- package/dist/schema/entry-migrations.d.ts.map +1 -0
- package/dist/schema/entry-migrations.js +48 -0
- package/dist/schema/entry-read.d.ts +694 -0
- package/dist/schema/entry-read.d.ts.map +1 -0
- package/dist/schema/entry-read.js +92 -0
- package/dist/schema/entry-write.d.ts +865 -0
- package/dist/schema/entry-write.d.ts.map +1 -0
- package/dist/schema/entry-write.js +105 -0
- package/dist/schema/entry.d.ts +6 -3295
- package/dist/schema/entry.d.ts.map +1 -1
- package/dist/schema/entry.js +10 -619
- package/dist/schema/manifest.d.ts +28 -41
- package/dist/schema/manifest.d.ts.map +1 -1
- package/dist/schema/manifest.js +20 -24
- package/dist/schema/note.d.ts +3 -9
- package/dist/schema/note.d.ts.map +1 -1
- package/dist/schema/note.js +13 -2
- package/dist/storage/filesystem.d.ts +2 -1
- package/dist/storage/filesystem.d.ts.map +1 -1
- package/dist/storage/filesystem.js +6 -4
- package/dist/storage/lock-reclaimer.d.ts +2 -0
- package/dist/storage/lock-reclaimer.d.ts.map +1 -0
- package/dist/storage/lock-reclaimer.js +45 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -4
package/dist/cli.js
CHANGED
|
@@ -5,14 +5,35 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
|
-
import
|
|
8
|
+
import * as v from 'valibot';
|
|
9
9
|
import { openLedger } from "./handle.js";
|
|
10
|
-
import {
|
|
10
|
+
import { deriveFilesChangedFromPatch } from "./patch-resolver.js";
|
|
11
|
+
import { ChangeLogParityStatusSchema, ChangeLogSmokeResultSchema, ChangeLogSubkindSchema, LEDGER_KINDS, LEDGER_PHASES, parseLedgerRecord, WorkspaceRelativePathSchema, } from "./schema/entry.js";
|
|
11
12
|
import { NoteSubkindSchema } from "./schema/note.js";
|
|
12
13
|
import { LEDGER_LIBRARY_VERSION } from "./version.js";
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
15
|
+
const GIT_DIFF_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
14
16
|
const GIT_DIFF_TIMEOUT_MS = 30_000;
|
|
15
|
-
const RENDER_TARGETS = [
|
|
17
|
+
const RENDER_TARGETS = [
|
|
18
|
+
'retro',
|
|
19
|
+
'jsonl',
|
|
20
|
+
'timeline-html',
|
|
21
|
+
'dependency-graph',
|
|
22
|
+
'migration-log-md',
|
|
23
|
+
'workspace-narrative-md',
|
|
24
|
+
];
|
|
25
|
+
const CHANGE_LOG_RECORD_ONLY_FLAGS = [
|
|
26
|
+
'commands',
|
|
27
|
+
'commands-from',
|
|
28
|
+
'files-changed',
|
|
29
|
+
'hypothesis',
|
|
30
|
+
'parity-status',
|
|
31
|
+
'rollback-plan',
|
|
32
|
+
'rolls-back',
|
|
33
|
+
'smoke-notes',
|
|
34
|
+
'smoke-result',
|
|
35
|
+
'subkind',
|
|
36
|
+
];
|
|
16
37
|
class CliUsageError extends Error {
|
|
17
38
|
}
|
|
18
39
|
const DEFAULT_CONTEXT = {
|
|
@@ -34,12 +55,12 @@ const renderValidValues = () => `Valid values:
|
|
|
34
55
|
const renderHelp = (commandName) => `${commandName}
|
|
35
56
|
|
|
36
57
|
Commands:
|
|
37
|
-
${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--idempotency-key=<key>] [--from-stdin]
|
|
58
|
+
${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--idempotency-key=<key>] [--subkind=<change-log-subkind>] [--files-changed=<csv>] [--hypothesis="..."] [--commands="cmd1\ncmd2" | --commands-from=<file>] [--smoke-result=<result>] [--smoke-notes="..."] [--parity-status=<status>] [--rollback-plan="..."] [--rolls-back=<entry-id>] [--from-stdin]
|
|
38
59
|
${commandName} note <subkind> [--workspace=<ws>] --phase=<phase> --summary="..." [--body=<markdown-file>] [--from-stdin]
|
|
39
60
|
${commandName} list [--workspace=<ws>] [--phase=<phase>] [--kind=<kind>] [--since=<iso>] [--limit=<n>] [--json]
|
|
40
61
|
${commandName} show [--workspace=<ws>] <entry-id>
|
|
41
62
|
${commandName} tail [--workspace=<ws>] [--phase=<phase>] [--limit=<n>]
|
|
42
|
-
${commandName} render [--workspace=<ws>] [--to=retro|jsonl|timeline-html|dependency-graph|
|
|
63
|
+
${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>]
|
|
43
64
|
${commandName} archive [--workspace=<ws>] --out=<file.tgz>
|
|
44
65
|
${commandName} doctor [--workspace=<ws>]
|
|
45
66
|
${commandName} --version
|
|
@@ -48,7 +69,7 @@ ${renderValidValues()}`;
|
|
|
48
69
|
const renderCommandHelp = (commandName, command) => {
|
|
49
70
|
switch (command) {
|
|
50
71
|
case 'record':
|
|
51
|
-
return `${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--idempotency-key=<key>] [--from-stdin]
|
|
72
|
+
return `${commandName} record [--workspace=<ws>] --kind=<kind> --phase=<phase> --summary="..." [--rationale="..."] [--action=<operator-action>] [--check-id=<check-id>] [--diff=<patch-file>] [--diff-from-git=<ref>] [--idempotency-key=<key>] [--subkind=<change-log-subkind>] [--files-changed=<csv>] [--hypothesis="..."] [--commands="cmd1\ncmd2" | --commands-from=<file>] [--smoke-result=<result>] [--smoke-notes="..."] [--parity-status=<status>] [--rollback-plan="..."] [--rolls-back=<entry-id>] [--from-stdin]
|
|
52
73
|
|
|
53
74
|
${renderValidValues()}`;
|
|
54
75
|
case 'note':
|
|
@@ -60,9 +81,7 @@ ${renderValidValues()}`;
|
|
|
60
81
|
|
|
61
82
|
${renderValidValues()}`;
|
|
62
83
|
case 'render':
|
|
63
|
-
return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--
|
|
64
|
-
|
|
65
|
-
Use \`--fresh\` to bypass the cached analytics summary. \`--json\` remains a compatibility alias for the same behavior.
|
|
84
|
+
return `${commandName} render [--workspace=<ws>] [--to=<target>] [--phase=<phase>] [--since=<iso>] [--limit=<n>] [--out=<file>]
|
|
66
85
|
|
|
67
86
|
${renderValidValues()}`;
|
|
68
87
|
default:
|
|
@@ -101,6 +120,9 @@ const parseArgv = (argv) => {
|
|
|
101
120
|
};
|
|
102
121
|
const getFlag = (flags, name) => {
|
|
103
122
|
const value = flags[name];
|
|
123
|
+
if (value === true) {
|
|
124
|
+
throw new CliUsageError(`Missing value for --${name}.`);
|
|
125
|
+
}
|
|
104
126
|
return typeof value === 'string' ? value : undefined;
|
|
105
127
|
};
|
|
106
128
|
const hasFlag = (flags, name) => flags[name] === true || typeof flags[name] === 'string';
|
|
@@ -142,7 +164,7 @@ const materializeGitDiff = async (workspaceRoot, gitRef) => {
|
|
|
142
164
|
try {
|
|
143
165
|
const { stdout } = await execFileAsync('git', ['diff', gitRef], {
|
|
144
166
|
cwd: workspaceRoot,
|
|
145
|
-
maxBuffer:
|
|
167
|
+
maxBuffer: GIT_DIFF_MAX_BUFFER_BYTES,
|
|
146
168
|
timeout: GIT_DIFF_TIMEOUT_MS,
|
|
147
169
|
});
|
|
148
170
|
tempDir = await mkdtemp(path.join(os.tmpdir(), 'ushman-ledger-git-diff-'));
|
|
@@ -236,18 +258,121 @@ const parseRenderTarget = (flags) => {
|
|
|
236
258
|
}
|
|
237
259
|
return target;
|
|
238
260
|
};
|
|
261
|
+
const splitCommandLines = (value) => value
|
|
262
|
+
.split(/\r?\n/u)
|
|
263
|
+
.map((command) => command.trim())
|
|
264
|
+
.filter((command) => command.length > 0);
|
|
265
|
+
const getUsedFlags = (flags, names) => names.filter((name) => hasFlag(flags, name)).map((name) => `--${name}`);
|
|
266
|
+
const rejectUnsupportedFlags = (supportedKindLabel, flags, names) => {
|
|
267
|
+
const usedFlags = getUsedFlags(flags, names);
|
|
268
|
+
if (usedFlags.length === 0) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
throw new CliUsageError(`${usedFlags.join(', ')} ${usedFlags.length === 1 ? 'is' : 'are'} only supported for ${supportedKindLabel} records.`);
|
|
272
|
+
};
|
|
273
|
+
const parseRequiredChangeLogSubkind = (flags) => {
|
|
274
|
+
const subkind = getRequiredString(flags, 'subkind');
|
|
275
|
+
if (!ChangeLogSubkindSchema.options.includes(subkind)) {
|
|
276
|
+
throw new CliUsageError(`Invalid --subkind value: ${subkind}. Expected one of: ${ChangeLogSubkindSchema.options.join(', ')}.`);
|
|
277
|
+
}
|
|
278
|
+
return subkind;
|
|
279
|
+
};
|
|
280
|
+
const parseOptionalChangeLogPicklist = ({ flagName, flags, options, }) => {
|
|
281
|
+
const value = getFlag(flags, flagName);
|
|
282
|
+
if (!value) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
if (!options.includes(value)) {
|
|
286
|
+
throw new CliUsageError(`Invalid --${flagName} value: ${value}. Expected one of: ${options.join(', ')}.`);
|
|
287
|
+
}
|
|
288
|
+
return value;
|
|
289
|
+
};
|
|
290
|
+
const parseFilesChangedCsv = (raw) => {
|
|
291
|
+
const uniquePaths = [
|
|
292
|
+
...new Set(raw
|
|
293
|
+
.split(',')
|
|
294
|
+
.map((value) => value.trim())
|
|
295
|
+
.filter((value) => value.length > 0)
|
|
296
|
+
.map((filePath) => {
|
|
297
|
+
try {
|
|
298
|
+
return v.parse(WorkspaceRelativePathSchema, filePath);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
throw new CliUsageError(`--files-changed path is not a normalized workspace-relative path: ${filePath}`);
|
|
302
|
+
}
|
|
303
|
+
})),
|
|
304
|
+
];
|
|
305
|
+
if (uniquePaths.length === 0) {
|
|
306
|
+
throw new CliUsageError('--files-changed must include at least one workspace-relative path.');
|
|
307
|
+
}
|
|
308
|
+
return uniquePaths.map((filePath) => ({ path: filePath }));
|
|
309
|
+
};
|
|
310
|
+
const readCommandLines = async (flags) => {
|
|
311
|
+
const inlineCommands = getFlag(flags, 'commands');
|
|
312
|
+
const commandsFrom = getFlag(flags, 'commands-from');
|
|
313
|
+
if (inlineCommands && commandsFrom) {
|
|
314
|
+
throw new CliUsageError('Use either --commands or --commands-from, not both.');
|
|
315
|
+
}
|
|
316
|
+
if (commandsFrom) {
|
|
317
|
+
const commandsPath = path.resolve(commandsFrom);
|
|
318
|
+
await ensureFileExists(commandsPath, '--commands-from');
|
|
319
|
+
return splitCommandLines(await readFile(commandsPath, 'utf8'));
|
|
320
|
+
}
|
|
321
|
+
if (inlineCommands) {
|
|
322
|
+
return splitCommandLines(inlineCommands);
|
|
323
|
+
}
|
|
324
|
+
return undefined;
|
|
325
|
+
};
|
|
326
|
+
const resolveDiffInput = async ({ flags, kind, workspaceRoot, }) => {
|
|
327
|
+
const diffPath = getFlag(flags, 'diff');
|
|
328
|
+
const diffFromGit = getFlag(flags, 'diff-from-git');
|
|
329
|
+
if (diffPath && diffFromGit) {
|
|
330
|
+
throw new CliUsageError('Use either --diff or --diff-from-git, not both.');
|
|
331
|
+
}
|
|
332
|
+
if (!diffPath && !diffFromGit) {
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
if (kind !== 'agent-patch' && kind !== 'operator-patch' && kind !== 'change-log') {
|
|
336
|
+
throw new CliUsageError('--diff and --diff-from-git are only supported for patch and change-log records.');
|
|
337
|
+
}
|
|
338
|
+
if (diffPath) {
|
|
339
|
+
const resolvedDiffPath = path.resolve(diffPath);
|
|
340
|
+
await ensureFileExists(resolvedDiffPath, '--diff');
|
|
341
|
+
return {
|
|
342
|
+
diffPath: resolvedDiffPath,
|
|
343
|
+
patchText: kind === 'change-log' ? await readFile(resolvedDiffPath, 'utf8') : undefined,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const materialized = await materializeGitDiff(workspaceRoot, diffFromGit ?? '');
|
|
347
|
+
return {
|
|
348
|
+
cleanupTempDir: materialized.tempDir,
|
|
349
|
+
diffPath: materialized.patchPath,
|
|
350
|
+
gitRef: diffFromGit,
|
|
351
|
+
patchText: kind === 'change-log' ? await readFile(materialized.patchPath, 'utf8') : undefined,
|
|
352
|
+
};
|
|
353
|
+
};
|
|
239
354
|
const validateRecordStdinFlags = (flags) => {
|
|
240
355
|
const conflictingFlags = [
|
|
241
356
|
'agent',
|
|
242
357
|
'action',
|
|
243
358
|
'check-id',
|
|
359
|
+
'commands',
|
|
360
|
+
'commands-from',
|
|
244
361
|
'diff',
|
|
245
362
|
'diff-from-git',
|
|
363
|
+
'files-changed',
|
|
364
|
+
'hypothesis',
|
|
246
365
|
'idempotency-key',
|
|
247
366
|
'kind',
|
|
248
367
|
'operator',
|
|
368
|
+
'parity-status',
|
|
249
369
|
'phase',
|
|
250
370
|
'rationale',
|
|
371
|
+
'rollback-plan',
|
|
372
|
+
'rolls-back',
|
|
373
|
+
'smoke-notes',
|
|
374
|
+
'smoke-result',
|
|
375
|
+
'subkind',
|
|
251
376
|
'summary',
|
|
252
377
|
].filter((flagName) => hasFlag(flags, flagName));
|
|
253
378
|
if (conflictingFlags.length === 0) {
|
|
@@ -259,6 +384,9 @@ const validateRecordStdinFlags = (flags) => {
|
|
|
259
384
|
};
|
|
260
385
|
const buildBaseRecordFromFlags = (parsed, context) => {
|
|
261
386
|
const kind = getRequiredKind(parsed.flags);
|
|
387
|
+
if (kind !== 'change-log') {
|
|
388
|
+
rejectUnsupportedFlags('change-log', parsed.flags, CHANGE_LOG_RECORD_ONLY_FLAGS);
|
|
389
|
+
}
|
|
262
390
|
const record = {
|
|
263
391
|
emitter: {
|
|
264
392
|
tool: context.defaultEmitter.tool,
|
|
@@ -298,43 +426,68 @@ const buildBaseRecordFromFlags = (parsed, context) => {
|
|
|
298
426
|
rationale: rationale ?? '',
|
|
299
427
|
};
|
|
300
428
|
}
|
|
429
|
+
if (kind === 'change-log') {
|
|
430
|
+
record.subkind = parseRequiredChangeLogSubkind(parsed.flags);
|
|
431
|
+
}
|
|
301
432
|
const idempotencyKey = getFlag(parsed.flags, 'idempotency-key');
|
|
302
433
|
if (idempotencyKey) {
|
|
303
434
|
record.idempotencyKey = idempotencyKey;
|
|
304
435
|
}
|
|
305
436
|
return { kind, record };
|
|
306
437
|
};
|
|
307
|
-
const applyPatchInputToRecord = async ({ kind,
|
|
308
|
-
const diffPath = getFlag(parsed.flags, 'diff');
|
|
309
|
-
if (diffPath) {
|
|
310
|
-
if (kind !== 'agent-patch' && kind !== 'operator-patch') {
|
|
311
|
-
throw new CliUsageError('--diff is only supported for patch records.');
|
|
312
|
-
}
|
|
313
|
-
await ensureFileExists(path.resolve(diffPath), '--diff');
|
|
314
|
-
record.diffPath = diffPath;
|
|
315
|
-
}
|
|
316
|
-
const diffFromGit = getFlag(parsed.flags, 'diff-from-git');
|
|
317
|
-
if (!diffFromGit) {
|
|
318
|
-
return {};
|
|
319
|
-
}
|
|
438
|
+
const applyPatchInputToRecord = async ({ kind, patchInput, record, }) => {
|
|
320
439
|
if (kind !== 'agent-patch' && kind !== 'operator-patch') {
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
if (diffPath) {
|
|
324
|
-
throw new CliUsageError('Use either --diff or --diff-from-git, not both.');
|
|
440
|
+
return {};
|
|
325
441
|
}
|
|
326
|
-
const materialized = await materializeGitDiff(workspaceRoot, diffFromGit);
|
|
327
442
|
return {
|
|
328
|
-
cleanupTempDir:
|
|
443
|
+
cleanupTempDir: patchInput.cleanupTempDir,
|
|
329
444
|
record: {
|
|
330
445
|
...record,
|
|
331
|
-
diffPath:
|
|
332
|
-
links:
|
|
333
|
-
|
|
334
|
-
|
|
446
|
+
diffPath: patchInput.diffPath,
|
|
447
|
+
links: patchInput.gitRef
|
|
448
|
+
? {
|
|
449
|
+
...record.links,
|
|
450
|
+
gitRef: patchInput.gitRef,
|
|
451
|
+
}
|
|
452
|
+
: record.links,
|
|
335
453
|
},
|
|
336
454
|
};
|
|
337
455
|
};
|
|
456
|
+
const applyChangeLogFlagsToRecord = async ({ flags, patchInput, record, }) => {
|
|
457
|
+
const filesChangedFlag = getFlag(flags, 'files-changed');
|
|
458
|
+
if (filesChangedFlag && patchInput.patchText) {
|
|
459
|
+
throw new CliUsageError('Use either --files-changed or --diff/--diff-from-git to populate change-log files.');
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
...record,
|
|
463
|
+
commandsRun: await readCommandLines(flags),
|
|
464
|
+
filesChanged: filesChangedFlag
|
|
465
|
+
? parseFilesChangedCsv(filesChangedFlag)
|
|
466
|
+
: patchInput.patchText
|
|
467
|
+
? deriveFilesChangedFromPatch(patchInput.patchText)
|
|
468
|
+
: undefined,
|
|
469
|
+
hypothesis: getFlag(flags, 'hypothesis'),
|
|
470
|
+
links: patchInput.gitRef
|
|
471
|
+
? {
|
|
472
|
+
...record.links,
|
|
473
|
+
gitRef: patchInput.gitRef,
|
|
474
|
+
}
|
|
475
|
+
: record.links,
|
|
476
|
+
parityStatus: parseOptionalChangeLogPicklist({
|
|
477
|
+
flagName: 'parity-status',
|
|
478
|
+
flags,
|
|
479
|
+
options: ChangeLogParityStatusSchema.options,
|
|
480
|
+
}),
|
|
481
|
+
rollbackPlan: getFlag(flags, 'rollback-plan'),
|
|
482
|
+
rollsBack: getFlag(flags, 'rolls-back'),
|
|
483
|
+
smokeNotes: getFlag(flags, 'smoke-notes'),
|
|
484
|
+
smokeResult: parseOptionalChangeLogPicklist({
|
|
485
|
+
flagName: 'smoke-result',
|
|
486
|
+
flags,
|
|
487
|
+
options: ChangeLogSmokeResultSchema.options,
|
|
488
|
+
}),
|
|
489
|
+
};
|
|
490
|
+
};
|
|
338
491
|
const buildRecordFromFlags = async (parsed, context) => {
|
|
339
492
|
const workspaceRoot = getWorkspaceRoot(parsed.flags);
|
|
340
493
|
if (hasFlag(parsed.flags, 'from-stdin')) {
|
|
@@ -346,17 +499,41 @@ const buildRecordFromFlags = async (parsed, context) => {
|
|
|
346
499
|
};
|
|
347
500
|
}
|
|
348
501
|
const { kind, record } = buildBaseRecordFromFlags(parsed, context);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
502
|
+
let diffInput;
|
|
503
|
+
try {
|
|
504
|
+
diffInput = await resolveDiffInput({
|
|
505
|
+
flags: parsed.flags,
|
|
506
|
+
kind,
|
|
507
|
+
workspaceRoot,
|
|
508
|
+
});
|
|
509
|
+
if (kind === 'change-log') {
|
|
510
|
+
return {
|
|
511
|
+
cleanupTempDir: diffInput.cleanupTempDir,
|
|
512
|
+
record: await applyChangeLogFlagsToRecord({
|
|
513
|
+
flags: parsed.flags,
|
|
514
|
+
patchInput: diffInput,
|
|
515
|
+
record,
|
|
516
|
+
}),
|
|
517
|
+
workspaceRoot,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const patchInput = await applyPatchInputToRecord({
|
|
521
|
+
kind,
|
|
522
|
+
patchInput: diffInput,
|
|
523
|
+
record,
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
cleanupTempDir: patchInput.cleanupTempDir,
|
|
527
|
+
record: patchInput.record ?? record,
|
|
528
|
+
workspaceRoot,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
if (diffInput?.cleanupTempDir) {
|
|
533
|
+
await rm(diffInput.cleanupTempDir, { force: true, recursive: true });
|
|
534
|
+
}
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
360
537
|
};
|
|
361
538
|
const buildNoteRecord = async (parsed, context) => {
|
|
362
539
|
const [subkind] = parsed.positionals;
|
|
@@ -462,15 +639,12 @@ const runTailCli = async (parsed, context) => runListCli({
|
|
|
462
639
|
}, context);
|
|
463
640
|
const runRenderCli = async (parsed, context) => {
|
|
464
641
|
const target = parseRenderTarget(parsed.flags);
|
|
465
|
-
const wantsFresh = hasFlag(parsed.flags, 'fresh') || hasFlag(parsed.flags, 'json');
|
|
466
|
-
if (wantsFresh && target !== 'analytics-summary') {
|
|
467
|
-
throw new CliUsageError('--fresh and --json are only supported for render --to=analytics-summary.');
|
|
468
|
-
}
|
|
469
642
|
const ledger = await openLedger(getWorkspaceRoot(parsed.flags));
|
|
470
643
|
const content = await ledger.render({
|
|
471
|
-
|
|
644
|
+
limit: parseLimit(parsed.flags),
|
|
472
645
|
out: getFlag(parsed.flags, 'out'),
|
|
473
646
|
phase: parseOptionalPhase(parsed.flags),
|
|
647
|
+
since: parseSince(parsed.flags),
|
|
474
648
|
to: target,
|
|
475
649
|
});
|
|
476
650
|
if (!getFlag(parsed.flags, 'out')) {
|
|
@@ -501,10 +675,11 @@ const formatCliError = (error) => {
|
|
|
501
675
|
if (error instanceof CliUsageError) {
|
|
502
676
|
return error.message;
|
|
503
677
|
}
|
|
504
|
-
if (error
|
|
678
|
+
if (v.isValiError(error)) {
|
|
505
679
|
return error.issues
|
|
506
680
|
.map((issue) => {
|
|
507
|
-
const
|
|
681
|
+
const pathSegments = issue.path?.map((segment) => String(segment.key)) ?? [];
|
|
682
|
+
const pathLabel = pathSegments.length > 0 ? pathSegments.join('.') : 'input';
|
|
508
683
|
return `${pathLabel}: ${issue.message}`;
|
|
509
684
|
})
|
|
510
685
|
.join('\n');
|
|
@@ -557,6 +732,6 @@ if (isDirectExecution()) {
|
|
|
557
732
|
process.exit(code);
|
|
558
733
|
}, (error) => {
|
|
559
734
|
process.stderr.write(`${formatCliError(error)}\n`);
|
|
560
|
-
process.exit(error instanceof CliUsageError || error
|
|
735
|
+
process.exit(error instanceof CliUsageError || v.isValiError(error) ? 1 : 2);
|
|
561
736
|
});
|
|
562
737
|
}
|
package/dist/doctor.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AA8V1E,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"}
|
package/dist/doctor.js
CHANGED
|
@@ -7,7 +7,9 @@ import { isReadIndexCurrent, readReadIndex } from "./read-index.js";
|
|
|
7
7
|
import { loadLedgerState } from "./recovery.js";
|
|
8
8
|
import { readManifest } from "./storage/filesystem.js";
|
|
9
9
|
const BLOB_HASH_CONCURRENCY = 16;
|
|
10
|
+
const CHECKPOINT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
10
11
|
const ENTRY_READ_BATCH_SIZE = 32;
|
|
12
|
+
const OPEN_ISSUE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
11
13
|
const checkPrevChain = (entry, previousByPhase, issues) => {
|
|
12
14
|
const expectedPrev = previousByPhase.get(entry.phase) ?? null;
|
|
13
15
|
if (entry.prevEntryId !== expectedPrev) {
|
|
@@ -66,13 +68,51 @@ const buildReadFailure = (error) => ({
|
|
|
66
68
|
issues: [`Failed to read ledger state: ${error instanceof Error ? (error.message ?? error.name) : String(error)}.`],
|
|
67
69
|
ok: false,
|
|
68
70
|
});
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
const isChangeLogEntry = (entry) => entry.kind === 'change-log';
|
|
72
|
+
const isOpenIssueNote = (entry) => entry.kind === 'note' && entry.subkind === 'open-issue';
|
|
73
|
+
const trackResolutionLinks = (entry, resolvedLedgerIds) => {
|
|
74
|
+
if (entry.links.correctsLedgerId) {
|
|
75
|
+
resolvedLedgerIds.add(entry.links.correctsLedgerId);
|
|
76
|
+
}
|
|
77
|
+
if (entry.links.supersedesLedgerId) {
|
|
78
|
+
resolvedLedgerIds.add(entry.links.supersedesLedgerId);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const trackIdempotencyEntry = (entry, entriesByIdempotencyKey) => {
|
|
82
|
+
const idempotencyKey = entry.links.idempotencyKey;
|
|
83
|
+
if (!idempotencyKey) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const existingEntries = entriesByIdempotencyKey.get(idempotencyKey) ?? [];
|
|
87
|
+
existingEntries.push({ id: entry.id, ts: entry.ts });
|
|
88
|
+
entriesByIdempotencyKey.set(idempotencyKey, existingEntries);
|
|
89
|
+
};
|
|
90
|
+
const checkChangeLogWarnings = ({ checkpointEntries, entriesByIdempotencyKey, issues, nowMs, openIssueEntries, resolvedLedgerIds, }) => {
|
|
91
|
+
for (const checkpointEntry of checkpointEntries) {
|
|
92
|
+
const ageMs = nowMs - Date.parse(checkpointEntry.ts);
|
|
93
|
+
if (ageMs <= CHECKPOINT_MAX_AGE_MS) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const idempotencyKey = checkpointEntry.links.idempotencyKey;
|
|
97
|
+
const hasFollowUp = typeof idempotencyKey === 'string' &&
|
|
98
|
+
(entriesByIdempotencyKey.get(idempotencyKey) ?? []).some((candidate) => candidate.id !== checkpointEntry.id && candidate.ts >= checkpointEntry.ts);
|
|
99
|
+
if (!hasFollowUp) {
|
|
100
|
+
issues.push(`Pre-change checkpoint ${checkpointEntry.id} is older than 24h and has no follow-up entry with matching idempotencyKey.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const openIssueEntry of openIssueEntries) {
|
|
104
|
+
const ageMs = nowMs - Date.parse(openIssueEntry.ts);
|
|
105
|
+
if (ageMs <= OPEN_ISSUE_MAX_AGE_MS || resolvedLedgerIds.has(openIssueEntry.id)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
issues.push(`Open issue note ${openIssueEntry.id} is older than 30 days and has no resolution link.`);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const inspectManifestLocation = ({ entry, issues, latestByPhase, manifest, }) => {
|
|
72
112
|
const manifestLocation = manifest.entryLocations[entry.id];
|
|
73
113
|
if (!manifestLocation) {
|
|
74
114
|
issues.push(`Manifest is missing entry location for ${entry.id}.`);
|
|
75
|
-
return;
|
|
115
|
+
return null;
|
|
76
116
|
}
|
|
77
117
|
if (manifestLocation.phase !== entry.phase) {
|
|
78
118
|
issues.push(`Manifest phase mismatch for ${entry.id}: expected ${entry.phase}, found ${manifestLocation.phase}.`);
|
|
@@ -81,6 +121,25 @@ const inspectDoctorEntry = ({ blobChecks, entry, issues, latestByPhase, manifest
|
|
|
81
121
|
if (!currentLatest || manifestLocation.sequence > currentLatest.sequence) {
|
|
82
122
|
latestByPhase.set(entry.phase, { entryId: entry.id, sequence: manifestLocation.sequence });
|
|
83
123
|
}
|
|
124
|
+
return manifestLocation;
|
|
125
|
+
};
|
|
126
|
+
const inspectNarrativeEntry = ({ checkpointEntries, entry, issues, openIssueEntries, }) => {
|
|
127
|
+
if (isChangeLogEntry(entry)) {
|
|
128
|
+
if (entry.subkind === 'pre-change-checkpoint') {
|
|
129
|
+
checkpointEntries.push(entry);
|
|
130
|
+
}
|
|
131
|
+
if (entry.smokeResult === 'fail' && !entry.rollbackPlan) {
|
|
132
|
+
issues.push(`Change-log entry ${entry.id} has smokeResult=fail but no rollbackPlan.`);
|
|
133
|
+
}
|
|
134
|
+
if (entry.subkind === 'rollback' && !entry.rollsBack) {
|
|
135
|
+
issues.push(`Change-log rollback entry ${entry.id} is missing rollsBack.`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (isOpenIssueNote(entry)) {
|
|
139
|
+
openIssueEntries.push(entry);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const inspectPatchEntry = ({ blobChecks, entry, }) => {
|
|
84
143
|
if (entry.kind !== 'agent-patch' && entry.kind !== 'operator-patch') {
|
|
85
144
|
return;
|
|
86
145
|
}
|
|
@@ -88,13 +147,42 @@ const inspectDoctorEntry = ({ blobChecks, entry, issues, latestByPhase, manifest
|
|
|
88
147
|
blobChecks.push({ blobHash, entryId: entry.id });
|
|
89
148
|
}
|
|
90
149
|
};
|
|
150
|
+
const inspectDoctorEntry = ({ blobChecks, checkpointEntries, entry, entriesByIdempotencyKey, issues, latestByPhase, manifest, openIssueEntries, previousByPhase, resolvedLedgerIds, unseenManifestEntryIds, }) => {
|
|
151
|
+
unseenManifestEntryIds.delete(entry.id);
|
|
152
|
+
checkPrevChain(entry, previousByPhase, issues);
|
|
153
|
+
trackIdempotencyEntry(entry, entriesByIdempotencyKey);
|
|
154
|
+
trackResolutionLinks(entry, resolvedLedgerIds);
|
|
155
|
+
if (!inspectManifestLocation({
|
|
156
|
+
entry,
|
|
157
|
+
issues,
|
|
158
|
+
latestByPhase,
|
|
159
|
+
manifest,
|
|
160
|
+
})) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
inspectNarrativeEntry({
|
|
164
|
+
checkpointEntries,
|
|
165
|
+
entry,
|
|
166
|
+
issues,
|
|
167
|
+
openIssueEntries,
|
|
168
|
+
});
|
|
169
|
+
inspectPatchEntry({
|
|
170
|
+
blobChecks,
|
|
171
|
+
entry,
|
|
172
|
+
});
|
|
173
|
+
};
|
|
91
174
|
const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
92
175
|
const issues = [];
|
|
93
176
|
const previousByPhase = new Map();
|
|
94
177
|
const latestByPhase = new Map();
|
|
95
178
|
const unseenManifestEntryIds = new Set(Object.keys(manifest.entryLocations));
|
|
96
179
|
const blobChecks = [];
|
|
180
|
+
const checkpointEntries = [];
|
|
181
|
+
const entriesByIdempotencyKey = new Map();
|
|
182
|
+
const openIssueEntries = [];
|
|
183
|
+
const resolvedLedgerIds = new Set();
|
|
97
184
|
let entryCount = 0;
|
|
185
|
+
const nowMs = Date.now();
|
|
98
186
|
const orderedEntries = getOrderedEntryLocations(manifest, readIndex, {});
|
|
99
187
|
checkManifestSequenceOrder(orderedEntries, issues);
|
|
100
188
|
for (let index = 0; index < orderedEntries.length; index += ENTRY_READ_BATCH_SIZE) {
|
|
@@ -111,15 +199,27 @@ const collectDoctorState = async (workspaceRoot, manifest, readIndex) => {
|
|
|
111
199
|
entryCount += 1;
|
|
112
200
|
inspectDoctorEntry({
|
|
113
201
|
blobChecks,
|
|
202
|
+
checkpointEntries,
|
|
114
203
|
entry: resolvedEntry.entry,
|
|
204
|
+
entriesByIdempotencyKey,
|
|
115
205
|
issues,
|
|
116
206
|
latestByPhase,
|
|
117
207
|
manifest,
|
|
208
|
+
openIssueEntries,
|
|
118
209
|
previousByPhase,
|
|
210
|
+
resolvedLedgerIds,
|
|
119
211
|
unseenManifestEntryIds,
|
|
120
212
|
});
|
|
121
213
|
}
|
|
122
214
|
}
|
|
215
|
+
checkChangeLogWarnings({
|
|
216
|
+
checkpointEntries,
|
|
217
|
+
entriesByIdempotencyKey,
|
|
218
|
+
issues,
|
|
219
|
+
nowMs,
|
|
220
|
+
openIssueEntries,
|
|
221
|
+
resolvedLedgerIds,
|
|
222
|
+
});
|
|
123
223
|
return {
|
|
124
224
|
blobChecks,
|
|
125
225
|
entryCount,
|
package/dist/handle.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { runLedgerDoctor } from './doctor.ts';
|
|
|
3
3
|
import { type LedgerFilter } from './list.ts';
|
|
4
4
|
import { appendNote, type NoteBody } from './note.ts';
|
|
5
5
|
import type { LedgerEntry, LedgerPhase } from './schema/entry.ts';
|
|
6
|
+
export type RenderTarget = 'dependency-graph' | 'jsonl' | 'migration-log-md' | 'retro' | 'timeline-html' | 'workspace-narrative-md';
|
|
6
7
|
export type LedgerHandle = {
|
|
7
8
|
readonly archive: (outPath: string) => Promise<{
|
|
8
9
|
integrityHash: string;
|
|
@@ -17,10 +18,11 @@ export type LedgerHandle = {
|
|
|
17
18
|
id: string;
|
|
18
19
|
}>;
|
|
19
20
|
readonly render: (options: {
|
|
20
|
-
fresh?: boolean;
|
|
21
21
|
out?: string;
|
|
22
|
+
limit?: number;
|
|
22
23
|
phase?: LedgerPhase;
|
|
23
|
-
|
|
24
|
+
since?: string;
|
|
25
|
+
to: RenderTarget;
|
|
24
26
|
}) => Promise<string>;
|
|
25
27
|
readonly show: (entryId: string) => Promise<LedgerEntry | null>;
|
|
26
28
|
};
|
package/dist/handle.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"handle.d.ts","sourceRoot":"","sources":["../src/handle.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAA8B,KAAK,YAAY,EAAe,MAAM,WAAW,CAAC;AACvF,OAAO,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAStD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGlE,MAAM,MAAM,YAAY,GAClB,kBAAkB,GAClB,OAAO,GACP,kBAAkB,GAClB,OAAO,GACP,eAAe,GACf,wBAAwB,CAAC;AAkD/B,MAAM,MAAM,YAAY,GAAG;IACvB,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;IACrF,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;IAC5E,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,YAAY,KAAK,aAAa,CAAC,WAAW,CAAC,CAAC;IACrE,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtG,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7D,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;QACvB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,WAAW,CAAC;QACpB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,EAAE,EAAE,YAAY,CAAC;KACpB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;CACnE,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,eAAe,MAAM,KAAG,OAAO,CAAC,YAAY,CAyC5E,CAAC"}
|