dotmd-cli 0.10.2 → 0.10.4
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 +41 -3
- package/bin/dotmd.mjs +70 -25
- package/dotmd.config.example.mjs +16 -15
- package/package.json +1 -1
- package/src/completions.mjs +4 -4
- package/src/config.mjs +3 -1
- package/src/export.mjs +7 -1
- package/src/index.mjs +2 -0
- package/src/init.mjs +1 -2
- package/src/new.mjs +2 -2
- package/src/query.mjs +10 -1
- package/src/render.mjs +2 -3
- package/src/stats.mjs +6 -1
- package/src/validate.mjs +6 -2
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ Any `.md` file with YAML frontmatter:
|
|
|
57
57
|
|
|
58
58
|
```markdown
|
|
59
59
|
---
|
|
60
|
+
type: doc
|
|
60
61
|
status: active
|
|
61
62
|
updated: 2026-03-14
|
|
62
63
|
module: auth
|
|
@@ -76,7 +77,31 @@ Design doc content here...
|
|
|
76
77
|
- [ ] Add tests
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
The only required field is `status`. Everything else is optional but unlocks more features.
|
|
80
|
+
The only required field is `status`. Everything else is optional but unlocks more features. The `type` field (`plan`, `doc`, or `research`) enables type-specific statuses and smarter context briefings.
|
|
81
|
+
|
|
82
|
+
## Document Types
|
|
83
|
+
|
|
84
|
+
Every document can have a `type` field in its frontmatter. Types determine which statuses are valid and how the document appears in context briefings.
|
|
85
|
+
|
|
86
|
+
| Type | Purpose | Valid Statuses |
|
|
87
|
+
|------|---------|----------------|
|
|
88
|
+
| `plan` | Execution plans | `in-session`, `active`, `planned`, `blocked`, `done`, `archived` |
|
|
89
|
+
| `doc` | Design docs, specs, ADRs, RFCs | `draft`, `active`, `review`, `reference`, `deprecated`, `archived` |
|
|
90
|
+
| `research` | Investigations, audits, analysis | `active`, `reference`, `archived` |
|
|
91
|
+
|
|
92
|
+
Documents without a `type` field use the global `statuses.order` from config.
|
|
93
|
+
|
|
94
|
+
Templates auto-set the type: `--template plan` sets `type: plan`, `--template adr` sets `type: doc`, `--template audit` sets `type: research`.
|
|
95
|
+
|
|
96
|
+
Filter by type with `--type`:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
dotmd query --type plan --status active # active plans
|
|
100
|
+
dotmd list --type doc # all docs
|
|
101
|
+
dotmd export --type research # export research only
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Customize types and their statuses in config with the `types` key. See [`dotmd.config.example.mjs`](dotmd.config.example.mjs).
|
|
80
105
|
|
|
81
106
|
## Commands
|
|
82
107
|
|
|
@@ -117,6 +142,7 @@ dotmd completions <shell> Output shell completion script (bash, zsh)
|
|
|
117
142
|
--config <path> Explicit config file path
|
|
118
143
|
--dry-run, -n Preview changes without writing anything
|
|
119
144
|
--root <name> Filter to a specific docs root
|
|
145
|
+
--type <t1,t2> Filter by document type (plan, doc, research)
|
|
120
146
|
--verbose Show resolved config details
|
|
121
147
|
--help, -h Show help (per-command with: dotmd <cmd> --help)
|
|
122
148
|
--version, -v Show version
|
|
@@ -133,7 +159,7 @@ dotmd query --status active --summarize # AI summaries
|
|
|
133
159
|
dotmd query --status active --summarize --summarize-limit 3
|
|
134
160
|
```
|
|
135
161
|
|
|
136
|
-
Flags: `--status`, `--keyword`, `--module`, `--surface`, `--domain`, `--owner`, `--updated-since`, `--stale`, `--has-next-step`, `--has-blockers`, `--checklist-open`, `--sort`, `--limit`, `--all`, `--git`, `--json`, `--summarize`, `--summarize-limit`, `--model`.
|
|
162
|
+
Flags: `--type`, `--status`, `--keyword`, `--module`, `--surface`, `--domain`, `--owner`, `--updated-since`, `--stale`, `--has-next-step`, `--has-blockers`, `--checklist-open`, `--sort`, `--limit`, `--all`, `--git`, `--json`, `--summarize`, `--summarize-limit`, `--model`.
|
|
137
163
|
|
|
138
164
|
### Scaffold with Templates
|
|
139
165
|
|
|
@@ -225,6 +251,7 @@ dotmd export --format html --output site # static HTML site
|
|
|
225
251
|
dotmd export --format json > bundle.json # JSON bundle with bodies
|
|
226
252
|
dotmd export docs/plan-a.md # single doc + dependencies
|
|
227
253
|
dotmd export --status active # filtered export
|
|
254
|
+
dotmd export --type plan # export only plans
|
|
228
255
|
```
|
|
229
256
|
|
|
230
257
|
### Notion Integration
|
|
@@ -359,6 +386,7 @@ export const lifecycle = {
|
|
|
359
386
|
archiveStatuses: ['archived'], // auto-move to archiveDir
|
|
360
387
|
skipStaleFor: ['archived'],
|
|
361
388
|
skipWarningsFor: ['archived'],
|
|
389
|
+
terminalStatuses: ['archived', 'deprecated', 'reference', 'done'],
|
|
362
390
|
};
|
|
363
391
|
|
|
364
392
|
export const taxonomy = {
|
|
@@ -378,7 +406,7 @@ export const index = {
|
|
|
378
406
|
};
|
|
379
407
|
```
|
|
380
408
|
|
|
381
|
-
All exports are optional. See [`dotmd.config.example.mjs`](dotmd.config.example.mjs) for the full reference.
|
|
409
|
+
All exports are optional. Additional options: `types`, `context`, `display`, `presets`, `templates`, `excludeDirs`, `notion`. See [`dotmd.config.example.mjs`](dotmd.config.example.mjs) for the full reference.
|
|
382
410
|
|
|
383
411
|
Config discovery walks up from cwd looking for `dotmd.config.mjs` or `.dotmd.config.mjs`.
|
|
384
412
|
|
|
@@ -424,6 +452,16 @@ export function onArchive(doc, { oldPath, newPath }) {
|
|
|
424
452
|
|
|
425
453
|
Available: `onArchive`, `onStatusChange`, `onTouch`, `onNew`, `onRename`, `onLint`.
|
|
426
454
|
|
|
455
|
+
### Transform Hooks
|
|
456
|
+
|
|
457
|
+
```js
|
|
458
|
+
// Add computed fields to every doc after parsing
|
|
459
|
+
export function transformDoc(doc) {
|
|
460
|
+
doc.priority = doc.blockers?.length ? 'high' : 'normal';
|
|
461
|
+
return doc;
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
427
465
|
### AI Hooks
|
|
428
466
|
|
|
429
467
|
```js
|
package/bin/dotmd.mjs
CHANGED
|
@@ -238,6 +238,7 @@ Options:
|
|
|
238
238
|
--template <name> Use a template (default, plan, adr, rfc, audit, design)
|
|
239
239
|
--status <s> Set initial status (default: active)
|
|
240
240
|
--title <t> Override the document title
|
|
241
|
+
--root <name> Create in a specific docs root
|
|
241
242
|
--list-templates Show available templates
|
|
242
243
|
|
|
243
244
|
The filename is derived from <name> by slugifying it.
|
|
@@ -275,6 +276,7 @@ Options:
|
|
|
275
276
|
--format <md|html|json> Output format (default: md)
|
|
276
277
|
--output <path> Write to file/directory (default: stdout for md/json)
|
|
277
278
|
--status <s1,s2> Filter by status
|
|
279
|
+
--type <t1,t2> Filter by type (plan, doc, research)
|
|
278
280
|
--module <name> Filter by module
|
|
279
281
|
--root <name> Filter by root
|
|
280
282
|
--dry-run, -n Preview without writing`,
|
|
@@ -335,7 +337,10 @@ Use --dry-run (-n) to preview changes without writing anything.`,
|
|
|
335
337
|
init: `dotmd init — create starter config and docs directory
|
|
336
338
|
|
|
337
339
|
Creates dotmd.config.mjs, docs/, and docs/docs.md in the current
|
|
338
|
-
directory. Skips any files that already exist
|
|
340
|
+
directory. Skips any files that already exist.
|
|
341
|
+
|
|
342
|
+
If docs/ already contains .md files, auto-detects statuses, surfaces,
|
|
343
|
+
modules, and reference fields to pre-populate the config.`,
|
|
339
344
|
};
|
|
340
345
|
|
|
341
346
|
async function main() {
|
|
@@ -384,10 +389,17 @@ async function main() {
|
|
|
384
389
|
|
|
385
390
|
const config = await resolveConfig(process.cwd(), explicitConfig);
|
|
386
391
|
|
|
392
|
+
// Watch is a pure proxy — pass raw args so the child process gets all flags
|
|
393
|
+
if (command === 'watch') { runWatch(args.slice(1), config); return; }
|
|
394
|
+
|
|
387
395
|
// Strip global flags from restArgs so commands don't have to filter them
|
|
388
396
|
const restArgs = [];
|
|
397
|
+
let rootArg = null;
|
|
398
|
+
let typeArg = null;
|
|
389
399
|
for (let i = 1; i < args.length; i++) {
|
|
390
400
|
if (args[i] === '--config') { i++; continue; }
|
|
401
|
+
if (args[i] === '--type' && args[i + 1]) { typeArg = args[++i]; continue; }
|
|
402
|
+
if (args[i] === '--root' && args[i + 1]) { rootArg = args[++i]; continue; }
|
|
391
403
|
if (args[i] === '--dry-run' || args[i] === '-n' || args[i] === '--verbose') continue;
|
|
392
404
|
restArgs.push(args[i]);
|
|
393
405
|
}
|
|
@@ -416,19 +428,18 @@ async function main() {
|
|
|
416
428
|
return;
|
|
417
429
|
}
|
|
418
430
|
|
|
419
|
-
//
|
|
420
|
-
if (command === 'watch') { runWatch(restArgs, config); return; }
|
|
431
|
+
// Commands that handle their own index building
|
|
421
432
|
if (command === 'diff') { runDiff(restArgs, config); return; }
|
|
422
433
|
if (command === 'summary') { runSummary(restArgs, config); return; }
|
|
423
434
|
if (command === 'deps') { runDeps(restArgs, config); return; }
|
|
424
|
-
if (command === 'export') { runExport(restArgs, config, { dryRun }); return; }
|
|
435
|
+
if (command === 'export') { runExport(restArgs, config, { dryRun, root: rootArg, type: typeArg }); return; }
|
|
425
436
|
if (command === 'notion') { await runNotion(restArgs, config, { dryRun }); return; }
|
|
426
437
|
|
|
427
438
|
// Lifecycle commands
|
|
428
439
|
if (command === 'status') { await runStatus(restArgs, config, { dryRun }); return; }
|
|
429
440
|
if (command === 'archive') { runArchive(restArgs, config, { dryRun }); return; }
|
|
430
441
|
if (command === 'touch') { runTouch(restArgs, config, { dryRun }); return; }
|
|
431
|
-
if (command === 'new') { await runNew(restArgs, config, { dryRun }); return; }
|
|
442
|
+
if (command === 'new') { await runNew(restArgs, config, { dryRun, root: rootArg }); return; }
|
|
432
443
|
if (command === 'lint') { runLint(restArgs, config, { dryRun }); return; }
|
|
433
444
|
if (command === 'rename') { await runRename(restArgs, config, { dryRun }); return; }
|
|
434
445
|
if (command === 'migrate') { runMigrate(restArgs, config, { dryRun }); return; }
|
|
@@ -437,27 +448,32 @@ async function main() {
|
|
|
437
448
|
|
|
438
449
|
const index = buildIndex(config);
|
|
439
450
|
|
|
440
|
-
// Apply --root
|
|
441
|
-
const rootFilter =
|
|
442
|
-
|
|
443
|
-
index.docs = index.docs.filter(d => d.root === rootFilter || d.root.endsWith('/' + rootFilter) || d.root.split('/').pop() === rootFilter);
|
|
444
|
-
}
|
|
451
|
+
// Apply --root and --type filters
|
|
452
|
+
const rootFilter = rootArg;
|
|
453
|
+
const typeFilter = typeArg;
|
|
445
454
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
455
|
+
function applyIndexFilters(idx) {
|
|
456
|
+
if (rootFilter) {
|
|
457
|
+
idx.docs = idx.docs.filter(d => d.root === rootFilter || d.root.endsWith('/' + rootFilter) || d.root.split('/').pop() === rootFilter);
|
|
458
|
+
}
|
|
459
|
+
if (typeFilter) {
|
|
460
|
+
const types = typeFilter.split(',').map(t => t.trim()).filter(Boolean);
|
|
461
|
+
idx.docs = idx.docs.filter(d => types.includes(d.type));
|
|
462
|
+
}
|
|
463
|
+
if (rootFilter || typeFilter) {
|
|
464
|
+
idx.errors = idx.errors.filter(e => idx.docs.some(d => d.path === e.path));
|
|
465
|
+
idx.warnings = idx.warnings.filter(w => idx.docs.some(d => d.path === w.path));
|
|
466
|
+
idx.countsByStatus = {};
|
|
467
|
+
for (const doc of idx.docs) {
|
|
468
|
+
const s = doc.status ?? 'unknown';
|
|
469
|
+
idx.countsByStatus[s] = (idx.countsByStatus[s] ?? 0) + 1;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
451
472
|
}
|
|
452
473
|
|
|
474
|
+
applyIndexFilters(index);
|
|
475
|
+
|
|
453
476
|
if (rootFilter || typeFilter) {
|
|
454
|
-
index.errors = index.errors.filter(e => index.docs.some(d => d.path === e.path));
|
|
455
|
-
index.warnings = index.warnings.filter(w => index.docs.some(d => d.path === w.path));
|
|
456
|
-
index.countsByStatus = {};
|
|
457
|
-
for (const doc of index.docs) {
|
|
458
|
-
const s = doc.status ?? 'unknown';
|
|
459
|
-
index.countsByStatus[s] = (index.countsByStatus[s] ?? 0) + 1;
|
|
460
|
-
}
|
|
461
477
|
}
|
|
462
478
|
|
|
463
479
|
if (verbose) {
|
|
@@ -500,6 +516,7 @@ async function main() {
|
|
|
500
516
|
}
|
|
501
517
|
// Show remaining issues
|
|
502
518
|
const freshIndex = buildIndex(config);
|
|
519
|
+
applyIndexFilters(freshIndex);
|
|
503
520
|
if (args.includes('--json')) {
|
|
504
521
|
process.stdout.write(JSON.stringify({
|
|
505
522
|
docsScanned: freshIndex.docs.length,
|
|
@@ -573,6 +590,10 @@ async function main() {
|
|
|
573
590
|
if (command === 'focus') { runFocus(index, restArgs, config); return; }
|
|
574
591
|
if (command === 'query') { runQuery(index, restArgs, config); return; }
|
|
575
592
|
if (command === 'context') {
|
|
593
|
+
const summarize = args.includes('--summarize');
|
|
594
|
+
const modelIdx = args.indexOf('--model');
|
|
595
|
+
const model = modelIdx !== -1 && args[modelIdx + 1] ? args[modelIdx + 1] : undefined;
|
|
596
|
+
|
|
576
597
|
if (args.includes('--json')) {
|
|
577
598
|
const byStatus = {};
|
|
578
599
|
for (const doc of index.docs) {
|
|
@@ -580,9 +601,36 @@ async function main() {
|
|
|
580
601
|
if (!byStatus[s]) byStatus[s] = [];
|
|
581
602
|
byStatus[s].push(doc);
|
|
582
603
|
}
|
|
604
|
+
const byType = {};
|
|
605
|
+
for (const doc of index.docs) {
|
|
606
|
+
if (doc.type) {
|
|
607
|
+
if (!byType[doc.type]) byType[doc.type] = [];
|
|
608
|
+
byType[doc.type].push(doc);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (summarize) {
|
|
612
|
+
const { summarizeDocBody } = await import('../src/ai.mjs');
|
|
613
|
+
const { extractFrontmatter } = await import('../src/frontmatter.mjs');
|
|
614
|
+
const { readFileSync } = await import('node:fs');
|
|
615
|
+
const limit = 5;
|
|
616
|
+
for (let i = 0; i < index.docs.length && i < limit; i++) {
|
|
617
|
+
try {
|
|
618
|
+
const absPath = path.resolve(config.repoRoot, index.docs[i].path);
|
|
619
|
+
const raw = readFileSync(absPath, 'utf8');
|
|
620
|
+
const { body } = extractFrontmatter(raw);
|
|
621
|
+
if (body?.trim()) {
|
|
622
|
+
const meta = { title: index.docs[i].title, status: index.docs[i].status, path: index.docs[i].path };
|
|
623
|
+
index.docs[i].aiSummary = config.hooks.summarizeDoc
|
|
624
|
+
? config.hooks.summarizeDoc(body, meta)
|
|
625
|
+
: summarizeDocBody(body, meta, { model });
|
|
626
|
+
}
|
|
627
|
+
} catch { /* skip */ }
|
|
628
|
+
}
|
|
629
|
+
}
|
|
583
630
|
const stale = index.docs.filter(d => d.isStale && !config.lifecycle.skipStaleFor.has(d.status));
|
|
584
631
|
process.stdout.write(JSON.stringify({
|
|
585
632
|
generatedAt: new Date().toISOString(),
|
|
633
|
+
docsByType: Object.keys(byType).length > 0 ? byType : undefined,
|
|
586
634
|
docsByStatus: byStatus,
|
|
587
635
|
countsByStatus: index.countsByStatus,
|
|
588
636
|
stale: stale.map(d => ({ path: d.path, title: d.title, daysSinceUpdate: d.daysSinceUpdate })),
|
|
@@ -591,9 +639,6 @@ async function main() {
|
|
|
591
639
|
}, null, 2) + '\n');
|
|
592
640
|
return;
|
|
593
641
|
}
|
|
594
|
-
const summarize = args.includes('--summarize');
|
|
595
|
-
const modelIdx = args.indexOf('--model');
|
|
596
|
-
const model = modelIdx !== -1 && args[modelIdx + 1] ? args[modelIdx + 1] : undefined;
|
|
597
642
|
process.stdout.write(renderContext(index, config, { summarize, model }));
|
|
598
643
|
return;
|
|
599
644
|
}
|
package/dotmd.config.example.mjs
CHANGED
|
@@ -57,6 +57,7 @@ export const lifecycle = {
|
|
|
57
57
|
archiveStatuses: ['archived'], // auto-move to archiveDir on transition
|
|
58
58
|
skipStaleFor: ['archived'], // skip staleness checks
|
|
59
59
|
skipWarningsFor: ['archived'], // skip validation warnings (summary, etc.)
|
|
60
|
+
terminalStatuses: ['archived', 'deprecated', 'reference', 'done'], // skip current_state/next_step warnings, exclude from stats scope
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
// Taxonomy validation — set fields to null to skip validation
|
|
@@ -107,7 +108,7 @@ export const presets = {
|
|
|
107
108
|
// IMPORTANT: Use environment variables for tokens — never hardcode secrets in config files.
|
|
108
109
|
// export const notion = {
|
|
109
110
|
// token: process.env.NOTION_TOKEN,
|
|
110
|
-
//
|
|
111
|
+
// database: process.env.NOTION_DATABASE_ID,
|
|
111
112
|
// };
|
|
112
113
|
|
|
113
114
|
// ─── Function Hooks ──────────────────────────────────────────────────────────
|
|
@@ -123,20 +124,13 @@ export const presets = {
|
|
|
123
124
|
// return { errors: [], warnings };
|
|
124
125
|
// }
|
|
125
126
|
|
|
126
|
-
//
|
|
127
|
-
// export function renderContext(index, defaultRenderer) {
|
|
128
|
-
//
|
|
129
|
-
// }
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
// export function
|
|
133
|
-
// return defaultRenderer(index);
|
|
134
|
-
// }
|
|
135
|
-
|
|
136
|
-
// Override the status snapshot display format.
|
|
137
|
-
// export function formatSnapshot(doc, defaultFormatter) {
|
|
138
|
-
// return defaultFormatter(doc);
|
|
139
|
-
// }
|
|
127
|
+
// Render hooks — override any renderer by wrapping the default.
|
|
128
|
+
// export function renderContext(index, defaultRenderer) { return defaultRenderer(index); }
|
|
129
|
+
// export function renderCompactList(index, defaultRenderer) { return defaultRenderer(index); }
|
|
130
|
+
// export function renderCheck(index, defaultRenderer) { return defaultRenderer(index); }
|
|
131
|
+
// export function renderStats(stats, defaultRenderer) { return defaultRenderer(stats); }
|
|
132
|
+
// export function renderGraph(graph, defaultRenderer) { return defaultRenderer(graph); }
|
|
133
|
+
// export function formatSnapshot(doc, defaultFormatter) { return defaultFormatter(doc); }
|
|
140
134
|
|
|
141
135
|
// Post-parse doc transformation — add computed fields.
|
|
142
136
|
// export function transformDoc(doc) {
|
|
@@ -147,3 +141,10 @@ export const presets = {
|
|
|
147
141
|
// export function onArchive(doc, { oldPath, newPath }) {}
|
|
148
142
|
// export function onStatusChange(doc, { oldStatus, newStatus, path }) {}
|
|
149
143
|
// export function onTouch(doc, { path, date }) {}
|
|
144
|
+
// export function onNew({ path, status, title, template }) {}
|
|
145
|
+
// export function onRename({ oldPath, newPath, referencesUpdated }) {}
|
|
146
|
+
// export function onLint({ path, fixes }) {}
|
|
147
|
+
|
|
148
|
+
// AI hooks — override summarization (replaces local MLX model).
|
|
149
|
+
// export function summarizeDoc(body, meta) { return 'Custom summary'; }
|
|
150
|
+
// export function summarizeDiff(diffOutput, filePath) { return 'Custom diff summary'; }
|
package/package.json
CHANGED
package/src/completions.mjs
CHANGED
|
@@ -6,10 +6,10 @@ const COMMANDS = [
|
|
|
6
6
|
'fix-refs', 'notion', 'export', 'summary', 'watch', 'diff', 'init', 'new', 'completions',
|
|
7
7
|
];
|
|
8
8
|
|
|
9
|
-
const GLOBAL_FLAGS = ['--config', '--dry-run', '--verbose', '--root', '--help', '--version'];
|
|
9
|
+
const GLOBAL_FLAGS = ['--config', '--dry-run', '--verbose', '--root', '--type', '--help', '--version'];
|
|
10
10
|
|
|
11
11
|
const COMMAND_FLAGS = {
|
|
12
|
-
query: ['--status', '--keyword', '--module', '--surface', '--domain', '--owner',
|
|
12
|
+
query: ['--type', '--status', '--keyword', '--module', '--surface', '--domain', '--owner',
|
|
13
13
|
'--updated-since', '--stale', '--has-next-step', '--has-blockers',
|
|
14
14
|
'--checklist-open', '--sort', '--limit', '--all', '--git', '--json',
|
|
15
15
|
'--summarize', '--summarize-limit', '--model'],
|
|
@@ -17,13 +17,13 @@ const COMMAND_FLAGS = {
|
|
|
17
17
|
list: ['--verbose', '--json'],
|
|
18
18
|
coverage: ['--json'],
|
|
19
19
|
new: ['--status', '--title', '--template', '--list-templates', '--root'],
|
|
20
|
-
diff: ['--stat', '--since', '--summarize', '--model'
|
|
20
|
+
diff: ['--stat', '--since', '--summarize', '--model'],
|
|
21
21
|
check: ['--errors-only', '--fix', '--json'],
|
|
22
22
|
stats: ['--json'],
|
|
23
23
|
graph: ['--dot', '--json', '--status', '--module', '--surface'],
|
|
24
24
|
deps: ['--json', '--depth'],
|
|
25
25
|
notion: ['import', 'export', 'sync', '--force', '--dry-run'],
|
|
26
|
-
export: ['--format', '--output', '--status', '--module', '--root'],
|
|
26
|
+
export: ['--format', '--output', '--status', '--module', '--root', '--type'],
|
|
27
27
|
focus: ['--json'],
|
|
28
28
|
status: [],
|
|
29
29
|
archive: [],
|
package/src/config.mjs
CHANGED
|
@@ -43,6 +43,7 @@ const DEFAULTS = {
|
|
|
43
43
|
archiveStatuses: ['archived'],
|
|
44
44
|
skipStaleFor: ['archived', 'reference'],
|
|
45
45
|
skipWarningsFor: ['archived'],
|
|
46
|
+
terminalStatuses: ['archived', 'deprecated', 'reference', 'done'],
|
|
46
47
|
},
|
|
47
48
|
|
|
48
49
|
taxonomy: {
|
|
@@ -284,6 +285,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
284
285
|
const archiveStatuses = new Set(lifecycle.archiveStatuses);
|
|
285
286
|
const skipStaleFor = new Set(lifecycle.skipStaleFor);
|
|
286
287
|
const skipWarningsFor = new Set(lifecycle.skipWarningsFor);
|
|
288
|
+
const terminalStatuses = new Set(lifecycle.terminalStatuses);
|
|
287
289
|
|
|
288
290
|
// Warn if rootStatuses keys don't match any configured root
|
|
289
291
|
for (const rootKey of Object.keys(rootStatusesRaw)) {
|
|
@@ -315,7 +317,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
315
317
|
rootValidStatuses,
|
|
316
318
|
staleDaysByStatus,
|
|
317
319
|
|
|
318
|
-
lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor },
|
|
320
|
+
lifecycle: { archiveStatuses, skipStaleFor, skipWarningsFor, terminalStatuses },
|
|
319
321
|
|
|
320
322
|
validSurfaces,
|
|
321
323
|
moduleRequiredStatuses,
|
package/src/export.mjs
CHANGED
|
@@ -11,7 +11,8 @@ export function runExport(argv, config, opts = {}) {
|
|
|
11
11
|
let output = null;
|
|
12
12
|
let statusFilter = null;
|
|
13
13
|
let moduleFilter = null;
|
|
14
|
-
let rootFilter = null;
|
|
14
|
+
let rootFilter = opts.root ?? null;
|
|
15
|
+
let typeFilter = opts.type ?? null;
|
|
15
16
|
const dryRun = opts.dryRun;
|
|
16
17
|
|
|
17
18
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -20,6 +21,7 @@ export function runExport(argv, config, opts = {}) {
|
|
|
20
21
|
if (argv[i] === '--status' && argv[i + 1]) { statusFilter = argv[++i]; continue; }
|
|
21
22
|
if (argv[i] === '--module' && argv[i + 1]) { moduleFilter = argv[++i]; continue; }
|
|
22
23
|
if (argv[i] === '--root' && argv[i + 1]) { rootFilter = argv[++i]; continue; }
|
|
24
|
+
if (argv[i] === '--type' && argv[i + 1]) { typeFilter = argv[++i]; continue; }
|
|
23
25
|
if (argv[i] === '--config') { i++; continue; }
|
|
24
26
|
if (argv[i].startsWith('-')) continue;
|
|
25
27
|
positional.push(argv[i]);
|
|
@@ -54,6 +56,10 @@ export function runExport(argv, config, opts = {}) {
|
|
|
54
56
|
if (rootFilter) {
|
|
55
57
|
docs = docs.filter(d => d.root === rootFilter || d.root?.endsWith('/' + rootFilter) || d.root?.split('/').pop() === rootFilter);
|
|
56
58
|
}
|
|
59
|
+
if (typeFilter) {
|
|
60
|
+
const types = typeFilter.split(',').map(t => t.trim()).filter(Boolean);
|
|
61
|
+
docs = docs.filter(d => types.includes(d.type));
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
if (docs.length === 0) {
|
package/src/index.mjs
CHANGED
|
@@ -131,6 +131,7 @@ export function parseDocFile(filePath, config) {
|
|
|
131
131
|
const executionMode = asString(parsedFrontmatter.execution_mode) ?? null;
|
|
132
132
|
const checklist = extractChecklistCounts(body);
|
|
133
133
|
const bodyLinks = extractBodyLinks(body);
|
|
134
|
+
const hasCloseout = /^##\s+Closeout/m.test(body);
|
|
134
135
|
|
|
135
136
|
// Dynamic reference field extraction
|
|
136
137
|
const refFields = {};
|
|
@@ -172,6 +173,7 @@ export function parseDocFile(filePath, config) {
|
|
|
172
173
|
bodyLinks,
|
|
173
174
|
refFields,
|
|
174
175
|
checklistCompletionRate: computeChecklistCompletionRate(checklist),
|
|
176
|
+
hasCloseout,
|
|
175
177
|
hasNextStep: Boolean(nextStep),
|
|
176
178
|
hasBlockers: blockers.length > 0,
|
|
177
179
|
daysSinceUpdate: computeDaysSinceUpdate(asString(parsedFrontmatter.updated) ?? null),
|
package/src/init.mjs
CHANGED
|
@@ -137,8 +137,7 @@ export function runInit(cwd) {
|
|
|
137
137
|
process.stdout.write(` ${green('create')} docs/docs.md\n`);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
141
140
|
process.stdout.write(`\nReady. Create your first doc:\n`);
|
|
142
|
-
process.stdout.write(`
|
|
141
|
+
process.stdout.write(` dotmd new my-doc\n`);
|
|
143
142
|
process.stdout.write(` dotmd list\n\n`);
|
|
144
143
|
}
|
package/src/new.mjs
CHANGED
|
@@ -27,7 +27,7 @@ const BUILTIN_TEMPLATES = {
|
|
|
27
27
|
},
|
|
28
28
|
audit: {
|
|
29
29
|
description: 'Codebase audit or research investigation',
|
|
30
|
-
frontmatter: (s, d) => `type: research\nstatus:
|
|
30
|
+
frontmatter: (s, d) => `type: research\nstatus: ${s}\nupdated: ${d}\naudited: ${d}\naudit_level: pass1\nmodule:\nsource_of_truth: code\nsupports_plans:`,
|
|
31
31
|
body: (t) => `\n# ${t}\n\n## Scope\n\n\n\n## Findings\n\n\n\n## Recommendations\n\n\n`,
|
|
32
32
|
},
|
|
33
33
|
design: {
|
|
@@ -45,7 +45,7 @@ export async function runNew(argv, config, opts = {}) {
|
|
|
45
45
|
let status = 'active';
|
|
46
46
|
let title = null;
|
|
47
47
|
let templateName = null;
|
|
48
|
-
let rootName = null;
|
|
48
|
+
let rootName = opts.root ?? null;
|
|
49
49
|
for (let i = 0; i < argv.length; i++) {
|
|
50
50
|
if (argv[i] === '--status' && argv[i + 1]) { status = argv[++i]; continue; }
|
|
51
51
|
if (argv[i] === '--title' && argv[i + 1]) { title = argv[++i]; continue; }
|
package/src/query.mjs
CHANGED
|
@@ -9,7 +9,16 @@ import { summarizeDocBody } from './ai.mjs';
|
|
|
9
9
|
import { dim } from './color.mjs';
|
|
10
10
|
|
|
11
11
|
export function runFocus(index, argv, config) {
|
|
12
|
-
|
|
12
|
+
// Find first positional arg, skipping flag-value pairs like --root <name>
|
|
13
|
+
const FLAGS_WITH_VALUES = new Set(['--root']);
|
|
14
|
+
let statusFilter = 'active';
|
|
15
|
+
for (let i = 0; i < argv.length; i++) {
|
|
16
|
+
if (FLAGS_WITH_VALUES.has(argv[i])) { i++; continue; }
|
|
17
|
+
if (argv[i].startsWith('-')) continue;
|
|
18
|
+
statusFilter = argv[i];
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
const docs = index.docs.filter(doc => doc.status === statusFilter);
|
|
14
23
|
|
|
15
24
|
if (argv.includes('--json')) {
|
package/src/render.mjs
CHANGED
|
@@ -324,9 +324,8 @@ export function renderCoverage(index, config) {
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
export function buildCoverage(index, config) {
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
const scoped = index.docs.filter(doc => doc.status && !terminalStatuses.has(doc.status) && !config.lifecycle.skipWarningsFor.has(doc.status));
|
|
327
|
+
const scope = [...new Set(index.docs.map(d => d.status).filter(s => s && !config.lifecycle.terminalStatuses.has(s) && !config.lifecycle.skipWarningsFor.has(s)))];
|
|
328
|
+
const scoped = index.docs.filter(doc => doc.status && !config.lifecycle.terminalStatuses.has(doc.status) && !config.lifecycle.skipWarningsFor.has(doc.status));
|
|
330
329
|
const missingSurface = scoped.filter(doc => !doc.surface);
|
|
331
330
|
const missingModule = scoped.filter(doc => !doc.module);
|
|
332
331
|
const modulePlatform = scoped.filter(doc => doc.module === 'platform');
|
package/src/stats.mjs
CHANGED
|
@@ -8,7 +8,12 @@ function pct(n, total) {
|
|
|
8
8
|
|
|
9
9
|
export function buildStats(index, config) {
|
|
10
10
|
const docs = index.docs;
|
|
11
|
-
const scope =
|
|
11
|
+
const scope = config.statusOrder.filter(s => !config.lifecycle.terminalStatuses.has(s) && !config.lifecycle.skipWarningsFor.has(s));
|
|
12
|
+
for (const typeSet of (config.typeStatuses?.values() ?? [])) {
|
|
13
|
+
for (const s of typeSet) {
|
|
14
|
+
if (!config.lifecycle.terminalStatuses.has(s) && !config.lifecycle.skipWarningsFor.has(s) && !scope.includes(s)) scope.push(s);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
12
17
|
const scoped = docs.filter(d => scope.includes(d.status));
|
|
13
18
|
const nonArchived = docs.filter(d => !config.lifecycle.skipWarningsFor.has(d.status));
|
|
14
19
|
|
package/src/validate.mjs
CHANGED
|
@@ -80,8 +80,7 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// Determine which statuses should have current_state and next_step
|
|
83
|
-
const
|
|
84
|
-
const isWorkStatus = knownStatus && doc.status && !terminalStatuses.has(doc.status) && !config.lifecycle.skipWarningsFor.has(doc.status);
|
|
83
|
+
const isWorkStatus = knownStatus && doc.status && !config.lifecycle.terminalStatuses.has(doc.status) && !config.lifecycle.skipWarningsFor.has(doc.status);
|
|
85
84
|
|
|
86
85
|
if (isWorkStatus && !asString(frontmatter.current_state)) {
|
|
87
86
|
doc.warnings.push({ path: doc.path, level: 'warning', message: 'Missing `current_state`; index output is using a fallback or placeholder.' });
|
|
@@ -91,6 +90,11 @@ export function validateDoc(doc, frontmatter, headingTitle, config) {
|
|
|
91
90
|
doc.warnings.push({ path: doc.path, level: 'warning', message: 'Missing `next_step`; command output will omit a clear immediate action.' });
|
|
92
91
|
}
|
|
93
92
|
|
|
93
|
+
// Archived plans must have a ## Closeout section
|
|
94
|
+
if (config.lifecycle.archiveStatuses.has(doc.status) && doc.type === 'plan' && !doc.hasCloseout) {
|
|
95
|
+
doc.warnings.push({ path: doc.path, level: 'warning', message: 'Archived plan missing `## Closeout` section.' });
|
|
96
|
+
}
|
|
97
|
+
|
|
94
98
|
// Validate reference fields resolve to existing files
|
|
95
99
|
const docDir = path.dirname(path.join(config.repoRoot, doc.path));
|
|
96
100
|
const allRefFields = [...(config.referenceFields.bidirectional || []), ...(config.referenceFields.unidirectional || [])];
|