dotmd-cli 0.12.0 → 0.13.1
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/README.md +2 -1
- package/bin/dotmd.mjs +65 -48
- package/package.json +1 -1
- package/src/completions.mjs +2 -1
- package/src/git.mjs +18 -0
- package/src/lifecycle.mjs +3 -2
- package/src/query.mjs +3 -2
- package/src/render.mjs +38 -0
- package/src/validate.mjs +3 -2
package/README.md
CHANGED
|
@@ -113,7 +113,8 @@ dotmd coverage [--json] Metadata coverage report
|
|
|
113
113
|
dotmd stats [--json] Doc health dashboard
|
|
114
114
|
dotmd graph [--dot|--json] Visualize document relationships
|
|
115
115
|
dotmd deps [file] Dependency tree or overview
|
|
116
|
-
dotmd
|
|
116
|
+
dotmd briefing Compact summary for session start
|
|
117
|
+
dotmd context [--summarize] Full briefing (LLM-oriented)
|
|
117
118
|
dotmd focus [status] Detailed view for one status group
|
|
118
119
|
dotmd query [filters] Filtered search
|
|
119
120
|
dotmd plans List all plans
|
package/bin/dotmd.mjs
CHANGED
|
@@ -4,29 +4,6 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { resolveConfig } from '../src/config.mjs';
|
|
7
|
-
import { buildIndex } from '../src/index.mjs';
|
|
8
|
-
import { renderCompactList, renderVerboseList, renderContext, renderCheck, renderCoverage, buildCoverage } from '../src/render.mjs';
|
|
9
|
-
import { renderIndexFile, writeIndex } from '../src/index-file.mjs';
|
|
10
|
-
import { runFocus, runQuery } from '../src/query.mjs';
|
|
11
|
-
import { runStatus, runArchive, runTouch, runBulkArchive, runPickup, runFinish } from '../src/lifecycle.mjs';
|
|
12
|
-
import { runInit } from '../src/init.mjs';
|
|
13
|
-
import { runNew } from '../src/new.mjs';
|
|
14
|
-
import { runCompletions } from '../src/completions.mjs';
|
|
15
|
-
import { runWatch } from '../src/watch.mjs';
|
|
16
|
-
import { runDiff } from '../src/diff.mjs';
|
|
17
|
-
import { runLint } from '../src/lint.mjs';
|
|
18
|
-
import { runRename } from '../src/rename.mjs';
|
|
19
|
-
import { runMigrate } from '../src/migrate.mjs';
|
|
20
|
-
import { runFixRefs, fixBrokenRefs } from '../src/fix-refs.mjs';
|
|
21
|
-
import { buildGraph, renderGraphText, renderGraphDot, renderGraphJson } from '../src/graph.mjs';
|
|
22
|
-
import { runDoctor } from '../src/doctor.mjs';
|
|
23
|
-
import { buildStats, renderStats, renderStatsJson } from '../src/stats.mjs';
|
|
24
|
-
import { runSummary } from '../src/summary.mjs';
|
|
25
|
-
import { runDeps, runUnblocks } from '../src/deps.mjs';
|
|
26
|
-
import { runHealth } from '../src/health.mjs';
|
|
27
|
-
import { runGlossary } from '../src/glossary.mjs';
|
|
28
|
-
import { runExport } from '../src/export.mjs';
|
|
29
|
-
import { runNotion } from '../src/notion.mjs';
|
|
30
7
|
import { die, warn, levenshtein } from '../src/util.mjs';
|
|
31
8
|
|
|
32
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -39,7 +16,8 @@ const HELP = {
|
|
|
39
16
|
View & Query:
|
|
40
17
|
list [--verbose] [--json] List docs grouped by status (default command)
|
|
41
18
|
json Full index as JSON
|
|
42
|
-
|
|
19
|
+
briefing [--json] Compact summary for session start (5-10 lines)
|
|
20
|
+
context [--summarize] [--json] Full briefing (LLM-oriented)
|
|
43
21
|
focus [status] [--json] Detailed view for one status group
|
|
44
22
|
query [filters] [--json] Filtered search (--status, --keyword, --stale, etc.)
|
|
45
23
|
coverage [--json] Metadata coverage report
|
|
@@ -191,7 +169,15 @@ Shows detailed info for all docs matching the given status (default: active).
|
|
|
191
169
|
Options:
|
|
192
170
|
--json Output as JSON`,
|
|
193
171
|
|
|
194
|
-
|
|
172
|
+
briefing: `dotmd briefing — compact summary for session start
|
|
173
|
+
|
|
174
|
+
Shows plan statuses with next steps, doc/research counts, and health
|
|
175
|
+
in 5-10 lines. Designed for LLM context injection.
|
|
176
|
+
|
|
177
|
+
Options:
|
|
178
|
+
--json Output as JSON`,
|
|
179
|
+
|
|
180
|
+
context: `dotmd context — full briefing (LLM-oriented)
|
|
195
181
|
|
|
196
182
|
Generates a compact status briefing designed for AI/LLM consumption.
|
|
197
183
|
|
|
@@ -418,6 +404,7 @@ async function main() {
|
|
|
418
404
|
}
|
|
419
405
|
|
|
420
406
|
if (command === 'completions') {
|
|
407
|
+
const { runCompletions } = await import('../src/completions.mjs');
|
|
421
408
|
runCompletions(args.slice(1));
|
|
422
409
|
return;
|
|
423
410
|
}
|
|
@@ -438,12 +425,13 @@ async function main() {
|
|
|
438
425
|
|
|
439
426
|
// Init — now has access to config for Claude command generation
|
|
440
427
|
if (command === 'init') {
|
|
428
|
+
const { runInit } = await import('../src/init.mjs');
|
|
441
429
|
runInit(process.cwd(), config.configFound ? config : null);
|
|
442
430
|
return;
|
|
443
431
|
}
|
|
444
432
|
|
|
445
433
|
// Watch is a pure proxy — pass raw args so the child process gets all flags
|
|
446
|
-
if (command === 'watch') { runWatch(args.slice(1), config); return; }
|
|
434
|
+
if (command === 'watch') { const { runWatch } = await import('../src/watch.mjs'); runWatch(args.slice(1), config); return; }
|
|
447
435
|
|
|
448
436
|
// Strip global flags from restArgs so commands don't have to filter them
|
|
449
437
|
const restArgs = [];
|
|
@@ -476,35 +464,41 @@ async function main() {
|
|
|
476
464
|
|
|
477
465
|
// Preset aliases
|
|
478
466
|
if (config.presets[command]) {
|
|
467
|
+
const { buildIndex } = await import('../src/index.mjs');
|
|
468
|
+
const { runQuery } = await import('../src/query.mjs');
|
|
479
469
|
const index = buildIndex(config);
|
|
480
470
|
runQuery(index, [...config.presets[command], ...restArgs], config);
|
|
481
471
|
return;
|
|
482
472
|
}
|
|
483
473
|
|
|
484
474
|
// Commands that handle their own index building
|
|
485
|
-
if (command === 'diff') { runDiff(restArgs, config); return; }
|
|
486
|
-
if (command === 'summary') { runSummary(restArgs, config); return; }
|
|
487
|
-
if (command === 'deps') { runDeps(restArgs, config); return; }
|
|
488
|
-
if (command === 'unblocks') { runUnblocks(restArgs, config); return; }
|
|
489
|
-
if (command === 'health') { runHealth(restArgs, config); return; }
|
|
490
|
-
if (command === 'glossary') { runGlossary(restArgs, config); return; }
|
|
491
|
-
if (command === 'export') { runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
|
|
492
|
-
if (command === 'notion') { await runNotion(restArgs, config, { dryRun }); return; }
|
|
475
|
+
if (command === 'diff') { const { runDiff } = await import('../src/diff.mjs'); runDiff(restArgs, config); return; }
|
|
476
|
+
if (command === 'summary') { const { runSummary } = await import('../src/summary.mjs'); runSummary(restArgs, config); return; }
|
|
477
|
+
if (command === 'deps') { const { runDeps } = await import('../src/deps.mjs'); runDeps(restArgs, config); return; }
|
|
478
|
+
if (command === 'unblocks') { const { runUnblocks } = await import('../src/deps.mjs'); runUnblocks(restArgs, config); return; }
|
|
479
|
+
if (command === 'health') { const { runHealth } = await import('../src/health.mjs'); runHealth(restArgs, config); return; }
|
|
480
|
+
if (command === 'glossary') { const { runGlossary } = await import('../src/glossary.mjs'); runGlossary(restArgs, config); return; }
|
|
481
|
+
if (command === 'export') { const { runExport } = await import('../src/export.mjs'); runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
|
|
482
|
+
if (command === 'notion') { const { runNotion } = await import('../src/notion.mjs'); await runNotion(restArgs, config, { dryRun }); return; }
|
|
493
483
|
|
|
494
484
|
// Lifecycle commands
|
|
495
|
-
if (command === 'pickup') { await runPickup(restArgs, config, { dryRun }); return; }
|
|
496
|
-
if (command === 'finish') { await runFinish(restArgs, config, { dryRun }); return; }
|
|
497
|
-
if (command === 'status') { await runStatus(restArgs, config, { dryRun }); return; }
|
|
498
|
-
if (command === 'archive') { runArchive(restArgs, config, { dryRun }); return; }
|
|
499
|
-
if (command === 'bulk' && restArgs[0] === 'archive') { runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
|
|
500
|
-
if (command === 'touch') { runTouch(restArgs, config, { dryRun }); return; }
|
|
501
|
-
if (command === 'new') { await runNew(restArgs, config, { dryRun, root: rootArg }); return; }
|
|
502
|
-
if (command === 'lint') { runLint(restArgs, config, { dryRun }); return; }
|
|
503
|
-
if (command === 'rename') { await runRename(restArgs, config, { dryRun }); return; }
|
|
504
|
-
if (command === 'migrate') { runMigrate(restArgs, config, { dryRun }); return; }
|
|
505
|
-
if (command === 'fix-refs') { runFixRefs(restArgs, config, { dryRun }); return; }
|
|
506
|
-
if (command === 'doctor') { runDoctor(restArgs, config, { dryRun }); return; }
|
|
507
|
-
|
|
485
|
+
if (command === 'pickup') { const { runPickup } = await import('../src/lifecycle.mjs'); await runPickup(restArgs, config, { dryRun }); return; }
|
|
486
|
+
if (command === 'finish') { const { runFinish } = await import('../src/lifecycle.mjs'); await runFinish(restArgs, config, { dryRun }); return; }
|
|
487
|
+
if (command === 'status') { const { runStatus } = await import('../src/lifecycle.mjs'); await runStatus(restArgs, config, { dryRun }); return; }
|
|
488
|
+
if (command === 'archive') { const { runArchive } = await import('../src/lifecycle.mjs'); runArchive(restArgs, config, { dryRun }); return; }
|
|
489
|
+
if (command === 'bulk' && restArgs[0] === 'archive') { const { runBulkArchive } = await import('../src/lifecycle.mjs'); runBulkArchive(restArgs.slice(1), config, { dryRun }); return; }
|
|
490
|
+
if (command === 'touch') { const { runTouch } = await import('../src/lifecycle.mjs'); runTouch(restArgs, config, { dryRun }); return; }
|
|
491
|
+
if (command === 'new') { const { runNew } = await import('../src/new.mjs'); await runNew(restArgs, config, { dryRun, root: rootArg }); return; }
|
|
492
|
+
if (command === 'lint') { const { runLint } = await import('../src/lint.mjs'); runLint(restArgs, config, { dryRun }); return; }
|
|
493
|
+
if (command === 'rename') { const { runRename } = await import('../src/rename.mjs'); await runRename(restArgs, config, { dryRun }); return; }
|
|
494
|
+
if (command === 'migrate') { const { runMigrate } = await import('../src/migrate.mjs'); runMigrate(restArgs, config, { dryRun }); return; }
|
|
495
|
+
if (command === 'fix-refs') { const { runFixRefs } = await import('../src/fix-refs.mjs'); runFixRefs(restArgs, config, { dryRun }); return; }
|
|
496
|
+
if (command === 'doctor') { const { runDoctor } = await import('../src/doctor.mjs'); runDoctor(restArgs, config, { dryRun }); return; }
|
|
497
|
+
|
|
498
|
+
// All remaining commands need the index + render modules
|
|
499
|
+
const { buildIndex } = await import('../src/index.mjs');
|
|
500
|
+
const { renderCompactList, renderVerboseList, renderContext, renderBriefing, renderCheck, renderCoverage, buildCoverage } = await import('../src/render.mjs');
|
|
501
|
+
const { runFocus, runQuery } = await import('../src/query.mjs');
|
|
508
502
|
const index = buildIndex(config);
|
|
509
503
|
|
|
510
504
|
// Apply --root and --type filters
|
|
@@ -561,6 +555,8 @@ async function main() {
|
|
|
561
555
|
|
|
562
556
|
if (fix) {
|
|
563
557
|
// Auto-fix: broken refs, then lint, then rebuild index
|
|
558
|
+
const { fixBrokenRefs } = await import('../src/fix-refs.mjs');
|
|
559
|
+
const { runLint } = await import('../src/lint.mjs');
|
|
564
560
|
fixBrokenRefs(config, { dryRun, quiet: false });
|
|
565
561
|
runLint(['--fix'], config, { dryRun });
|
|
566
562
|
if (config.indexPath) {
|
|
@@ -620,6 +616,7 @@ async function main() {
|
|
|
620
616
|
}
|
|
621
617
|
|
|
622
618
|
if (command === 'stats') {
|
|
619
|
+
const { buildStats, renderStats, renderStatsJson } = await import('../src/stats.mjs');
|
|
623
620
|
const stats = buildStats(index, config);
|
|
624
621
|
if (args.includes('--json')) {
|
|
625
622
|
process.stdout.write(renderStatsJson(stats));
|
|
@@ -634,6 +631,7 @@ async function main() {
|
|
|
634
631
|
die('Index generation is not configured. Add an `index` section to your dotmd.config.mjs.');
|
|
635
632
|
}
|
|
636
633
|
const write = args.includes('--write');
|
|
634
|
+
const { renderIndexFile, writeIndex } = await import('../src/index-file.mjs');
|
|
637
635
|
const rendered = renderIndexFile(index, config);
|
|
638
636
|
if (write && !dryRun) {
|
|
639
637
|
writeIndex(rendered, config);
|
|
@@ -648,6 +646,24 @@ async function main() {
|
|
|
648
646
|
|
|
649
647
|
if (command === 'focus') { runFocus(index, restArgs, config); return; }
|
|
650
648
|
if (command === 'query') { runQuery(index, restArgs, config); return; }
|
|
649
|
+
if (command === 'briefing') {
|
|
650
|
+
if (args.includes('--json')) {
|
|
651
|
+
const plans = index.docs.filter(d => d.type === 'plan');
|
|
652
|
+
const docs = index.docs.filter(d => d.type === 'doc');
|
|
653
|
+
const research = index.docs.filter(d => d.type === 'research');
|
|
654
|
+
const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status)).length;
|
|
655
|
+
process.stdout.write(JSON.stringify({
|
|
656
|
+
plans: { total: plans.length, inSession: plans.filter(d => d.status === 'in-session').map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })), active: plans.filter(d => d.status === 'active').map(d => ({ path: d.path, title: d.title, nextStep: d.nextStep })) },
|
|
657
|
+
docs: { total: docs.length, active: docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length },
|
|
658
|
+
research: { total: research.length, active: research.filter(d => d.status === 'active').length },
|
|
659
|
+
stale, errorCount: index.errors.length, warningCount: index.warnings.length,
|
|
660
|
+
}, null, 2) + '\n');
|
|
661
|
+
} else {
|
|
662
|
+
process.stdout.write(renderBriefing(index, config));
|
|
663
|
+
}
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
651
667
|
if (command === 'context') {
|
|
652
668
|
const summarize = args.includes('--summarize');
|
|
653
669
|
const modelIdx = args.indexOf('--model');
|
|
@@ -703,6 +719,7 @@ async function main() {
|
|
|
703
719
|
}
|
|
704
720
|
|
|
705
721
|
if (command === 'graph') {
|
|
722
|
+
const { buildGraph, renderGraphText, renderGraphDot, renderGraphJson } = await import('../src/graph.mjs');
|
|
706
723
|
const statusFilter = (() => { const i = args.indexOf('--status'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
|
|
707
724
|
const moduleFilter = (() => { const i = args.indexOf('--module'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
|
|
708
725
|
const surfaceFilter = (() => { const i = args.indexOf('--surface'); return i !== -1 && args[i + 1] ? args[i + 1] : null; })();
|
|
@@ -723,7 +740,7 @@ async function main() {
|
|
|
723
740
|
|
|
724
741
|
// Unknown command — suggest closest match
|
|
725
742
|
const allCommands = [
|
|
726
|
-
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'context',
|
|
743
|
+
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'briefing', 'context',
|
|
727
744
|
'focus', 'query', 'plans', 'stale', 'actionable', 'index', 'pickup', 'finish', 'status', 'archive', 'touch', 'doctor',
|
|
728
745
|
'fix-refs', 'lint', 'rename', 'migrate', 'notion', 'export', 'summary',
|
|
729
746
|
'watch', 'diff', 'new', 'init', 'completions',
|
package/package.json
CHANGED
package/src/completions.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { die } from './util.mjs';
|
|
2
2
|
|
|
3
3
|
const COMMANDS = [
|
|
4
|
-
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'unblocks', 'health', 'glossary', 'context', 'focus', 'query',
|
|
4
|
+
'list', 'json', 'check', 'coverage', 'stats', 'graph', 'deps', 'unblocks', 'health', 'glossary', 'briefing', 'context', 'focus', 'query',
|
|
5
5
|
'plans', 'stale', 'actionable', 'index', 'pickup', 'finish', 'status', 'archive', 'bulk', 'touch', 'doctor', 'lint', 'rename', 'migrate',
|
|
6
6
|
'fix-refs', 'notion', 'export', 'summary', 'watch', 'diff', 'init', 'new', 'completions',
|
|
7
7
|
];
|
|
@@ -32,6 +32,7 @@ const COMMAND_FLAGS = {
|
|
|
32
32
|
plans: ['--status', '--json', '--sort', '--limit', '--all', '--stale', '--has-next-step'],
|
|
33
33
|
stale: ['--json', '--sort', '--limit', '--all'],
|
|
34
34
|
actionable: ['--json', '--sort', '--limit', '--all'],
|
|
35
|
+
briefing: ['--json'],
|
|
35
36
|
pickup: ['--json'],
|
|
36
37
|
finish: ['--json'],
|
|
37
38
|
status: [],
|
package/src/git.mjs
CHANGED
|
@@ -19,6 +19,24 @@ export function getGitLastModified(relPath, repoRoot) {
|
|
|
19
19
|
return result.stdout.trim();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export function getGitLastModifiedBatch(repoRoot) {
|
|
23
|
+
const result = spawnSync('git', [
|
|
24
|
+
'log', '--format=commit %aI', '--name-only', '--diff-filter=ACDMR', 'HEAD',
|
|
25
|
+
], { cwd: repoRoot, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
|
|
26
|
+
if (result.error || result.status !== 0) return new Map();
|
|
27
|
+
|
|
28
|
+
const map = new Map();
|
|
29
|
+
let currentDate = null;
|
|
30
|
+
for (const line of result.stdout.split('\n')) {
|
|
31
|
+
if (line.startsWith('commit ')) {
|
|
32
|
+
currentDate = line.slice(7).trim();
|
|
33
|
+
} else if (line && currentDate && !map.has(line)) {
|
|
34
|
+
map.set(line, currentDate);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return map;
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
export function gitMv(source, target, repoRoot) {
|
|
23
41
|
ensureGit();
|
|
24
42
|
const result = spawnSync('git', ['mv', source, target], {
|
package/src/lifecycle.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { extractFrontmatter, parseSimpleFrontmatter, replaceFrontmatter } from './frontmatter.mjs';
|
|
4
4
|
import { asString, toRepoPath, die, warn, resolveDocPath, escapeRegex } from './util.mjs';
|
|
5
|
-
import { gitMv, getGitLastModified } from './git.mjs';
|
|
5
|
+
import { gitMv, getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
|
|
6
6
|
import { buildIndex, collectDocFiles } from './index.mjs';
|
|
7
7
|
import { renderIndexFile, writeIndex } from './index-file.mjs';
|
|
8
8
|
import { green, dim, yellow } from './color.mjs';
|
|
@@ -356,6 +356,7 @@ export function runTouch(argv, config, opts = {}) {
|
|
|
356
356
|
|
|
357
357
|
const prefix = dryRun ? dim('[dry-run] ') : '';
|
|
358
358
|
let synced = 0;
|
|
359
|
+
const gitDates = getGitLastModifiedBatch(config.repoRoot);
|
|
359
360
|
|
|
360
361
|
for (const filePath of allFiles) {
|
|
361
362
|
const repoPath = toRepoPath(filePath, config.repoRoot);
|
|
@@ -368,7 +369,7 @@ export function runTouch(argv, config, opts = {}) {
|
|
|
368
369
|
if (config.lifecycle.skipStaleFor.has(status)) continue;
|
|
369
370
|
|
|
370
371
|
const fmUpdated = asString(parsed.updated);
|
|
371
|
-
const gitDate =
|
|
372
|
+
const gitDate = gitDates.get(repoPath) ?? null;
|
|
372
373
|
if (!gitDate) continue;
|
|
373
374
|
|
|
374
375
|
const gitDay = gitDate.slice(0, 10);
|
package/src/query.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { capitalize, toSlug, warn } from './util.mjs';
|
|
4
4
|
import { renderProgressBar } from './render.mjs';
|
|
5
5
|
import { computeDaysSinceUpdate, computeIsStale } from './validate.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { getGitLastModifiedBatch } from './git.mjs';
|
|
7
7
|
import { extractFrontmatter } from './frontmatter.mjs';
|
|
8
8
|
import { summarizeDocBody } from './ai.mjs';
|
|
9
9
|
import { dim } from './color.mjs';
|
|
@@ -128,8 +128,9 @@ export function filterDocs(docs, filters, config) {
|
|
|
128
128
|
if (filters.updatedSince) result = result.filter(d => d.updated && d.updated >= filters.updatedSince);
|
|
129
129
|
|
|
130
130
|
if (filters.git) {
|
|
131
|
+
const gitDates = getGitLastModifiedBatch(config.repoRoot);
|
|
131
132
|
for (const doc of result) {
|
|
132
|
-
const gitDate =
|
|
133
|
+
const gitDate = gitDates.get(doc.path) ?? null;
|
|
133
134
|
if (gitDate) {
|
|
134
135
|
doc.daysSinceUpdate = computeDaysSinceUpdate(gitDate);
|
|
135
136
|
doc.isStale = computeIsStale(doc.status, gitDate, config);
|
package/src/render.mjs
CHANGED
|
@@ -258,6 +258,44 @@ function _renderContext(index, config, opts = {}) {
|
|
|
258
258
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
export function renderBriefing(index, config) {
|
|
262
|
+
const lines = [];
|
|
263
|
+
const plans = index.docs.filter(d => d.type === 'plan');
|
|
264
|
+
const docs = index.docs.filter(d => d.type === 'doc');
|
|
265
|
+
const research = index.docs.filter(d => d.type === 'research');
|
|
266
|
+
const untyped = index.docs.filter(d => !d.type);
|
|
267
|
+
|
|
268
|
+
if (plans.length) {
|
|
269
|
+
const bySt = {};
|
|
270
|
+
for (const p of plans) { bySt[p.status] = (bySt[p.status] ?? 0) + 1; }
|
|
271
|
+
const counts = Object.entries(bySt).map(([s, n]) => `${n} ${s}`).join(', ');
|
|
272
|
+
lines.push(`${plans.length} plans: ${counts}`);
|
|
273
|
+
const show = plans.filter(p => p.status === 'in-session' || p.status === 'active');
|
|
274
|
+
for (const p of show) {
|
|
275
|
+
const next = p.nextStep ? `next: ${p.nextStep}` : '(no next step)';
|
|
276
|
+
lines.push(` > ${path.basename(p.path, '.md')} (${p.status}) ${next}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const parts = [];
|
|
281
|
+
if (docs.length) {
|
|
282
|
+
const active = docs.filter(d => !config.lifecycle.terminalStatuses.has(d.status)).length;
|
|
283
|
+
const rest = docs.length - active;
|
|
284
|
+
parts.push(`${active} docs active` + (rest ? `, ${rest} other` : ''));
|
|
285
|
+
}
|
|
286
|
+
if (research.length) {
|
|
287
|
+
const active = research.filter(d => d.status === 'active').length;
|
|
288
|
+
parts.push(`${active} research active`);
|
|
289
|
+
}
|
|
290
|
+
if (untyped.length) parts.push(`${untyped.length} untyped`);
|
|
291
|
+
if (parts.length) lines.push(parts.join(' | '));
|
|
292
|
+
|
|
293
|
+
const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status)).length;
|
|
294
|
+
lines.push(`Stale: ${stale} | Errors: ${index.errors.length} | Warnings: ${index.warnings.length}`);
|
|
295
|
+
|
|
296
|
+
return lines.join('\n') + '\n';
|
|
297
|
+
}
|
|
298
|
+
|
|
261
299
|
export function renderCheck(index, config, opts = {}) {
|
|
262
300
|
const defaultRenderer = (idx) => _renderCheck(idx, opts);
|
|
263
301
|
if (config.hooks.renderCheck) {
|
package/src/validate.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { asString } from './util.mjs';
|
|
4
|
-
import { getGitLastModified } from './git.mjs';
|
|
4
|
+
import { getGitLastModified, getGitLastModifiedBatch } from './git.mjs';
|
|
5
5
|
import { toRepoPath } from './util.mjs';
|
|
6
6
|
|
|
7
7
|
const NOW = new Date();
|
|
@@ -150,11 +150,12 @@ export function checkBidirectionalReferences(docs, config) {
|
|
|
150
150
|
|
|
151
151
|
export function checkGitStaleness(docs, config) {
|
|
152
152
|
const warnings = [];
|
|
153
|
+
const gitDates = getGitLastModifiedBatch(config.repoRoot);
|
|
153
154
|
for (const doc of docs) {
|
|
154
155
|
if (config.lifecycle.skipStaleFor.has(doc.status)) continue;
|
|
155
156
|
if (!doc.updated) continue;
|
|
156
157
|
|
|
157
|
-
const gitDate =
|
|
158
|
+
const gitDate = gitDates.get(doc.path) ?? null;
|
|
158
159
|
if (!gitDate) continue;
|
|
159
160
|
|
|
160
161
|
const gitDay = gitDate.slice(0, 10);
|