dotmd-cli 0.14.12 → 0.15.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/README.md +23 -3
- package/bin/dotmd.mjs +27 -6
- package/dotmd.config.example.mjs +33 -14
- package/package.json +1 -1
- package/src/config.mjs +21 -13
- package/src/health.mjs +1 -1
- package/src/init.mjs +1 -1
- package/src/render.mjs +3 -3
- package/src/stats.mjs +2 -2
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ Every document can have a `type` field in its frontmatter. Types determine which
|
|
|
88
88
|
|
|
89
89
|
| Type | Purpose | Valid Statuses |
|
|
90
90
|
|------|---------|----------------|
|
|
91
|
-
| `plan` | Execution plans | `in-session`, `active`, `planned`, `blocked`, `
|
|
91
|
+
| `plan` | Execution plans | `in-session`, `active`, `planned`, `blocked`, `partial`, `paused`, `awaiting`, `queued-after`, `archived` |
|
|
92
92
|
| `doc` | Design docs, specs, ADRs, RFCs | `draft`, `active`, `review`, `reference`, `deprecated`, `archived` |
|
|
93
93
|
| `research` | Investigations, audits, analysis | `active`, `reference`, `archived` |
|
|
94
94
|
|
|
@@ -106,6 +106,26 @@ dotmd export --type research # export research only
|
|
|
106
106
|
|
|
107
107
|
Customize types and their statuses in config with the `types` key. See [`dotmd.config.example.mjs`](dotmd.config.example.mjs).
|
|
108
108
|
|
|
109
|
+
### What each plan status means
|
|
110
|
+
|
|
111
|
+
The default plan vocabulary is shaped around the **unstuck-action test**: every stop-status should map to a distinct next move. If two statuses have the same unstuck-action, one is dead weight; if a single status covers several different actions, it's overloaded.
|
|
112
|
+
|
|
113
|
+
| Status | Unstuck-action | When to use |
|
|
114
|
+
|--------|----------------|-------------|
|
|
115
|
+
| `in-session` | — | A Claude session is working on it right now. Don't pick up. |
|
|
116
|
+
| `active` | Pick up | Ready to be worked on. |
|
|
117
|
+
| `planned` | Wait for trigger | Queued; not yet ready to execute. |
|
|
118
|
+
| `blocked` | **Monitor** | External arrival on its own schedule (hardware, vendor, third-party rollout). You can't speed it up. |
|
|
119
|
+
| `partial` | **Spawn successors** | Shipped most of the plan; tail deferred. Body should reference successor plans tracking the tail. Visible but quiet (no nagging). |
|
|
120
|
+
| `paused` | **Re-evaluate** | Intentionally set aside, no external dependency. Resume by deciding the work is still worth doing. Quiet. |
|
|
121
|
+
| `awaiting` | **Ask** | Needs a human decision or input. NOT quiet — pings get forgotten, so this status generates stale pressure to chase the answer. |
|
|
122
|
+
| `queued-after` | **Check predecessor** | Sequenced behind another plan; can start once that one ships. Quiet. |
|
|
123
|
+
| `archived` | — | No longer relevant; auto-moved to the archive directory on transition. |
|
|
124
|
+
|
|
125
|
+
Each *quiet* status (`partial`, `paused`, `queued-after`, `archived`) is exempt from stale-warning pressure but still appears in active scope and metrics — quietness is a presentation flag, not a closure flag. `awaiting` deliberately stays loud so unanswered questions don't decay into invisible backlog.
|
|
126
|
+
|
|
127
|
+
> **Heads-up:** versions before 0.15 included a `done` plan status in the defaults. It saw effectively zero real-world use (plans went `in-session`/`active` → `archived` directly), so it was dropped from the built-in vocabulary. To finish a plan, run `dotmd archive <plan-file>` — or, if you preferred the previous behavior, add `done` back via the `types.plan.statuses` key in your config.
|
|
128
|
+
|
|
109
129
|
## Commands
|
|
110
130
|
|
|
111
131
|
```
|
|
@@ -390,8 +410,8 @@ dotmd rename old-name.md new-name # renames + updates refs
|
|
|
390
410
|
### Migrate
|
|
391
411
|
|
|
392
412
|
```bash
|
|
393
|
-
dotmd migrate status research
|
|
394
|
-
dotmd migrate module auth identity
|
|
413
|
+
dotmd migrate status research scoping # rename a status (e.g. for the 0.15 default-vocab change)
|
|
414
|
+
dotmd migrate module auth identity # rename a module
|
|
395
415
|
```
|
|
396
416
|
|
|
397
417
|
### Preset Aliases
|
package/bin/dotmd.mjs
CHANGED
|
@@ -140,6 +140,17 @@ Moves the document to the new status. If transitioning to an archive
|
|
|
140
140
|
status, automatically moves the file to the archive directory and
|
|
141
141
|
regenerates the index (if configured).
|
|
142
142
|
|
|
143
|
+
Default plan statuses (each maps to a distinct unstuck-action):
|
|
144
|
+
in-session A Claude session is working on it now
|
|
145
|
+
active Ready to be picked up
|
|
146
|
+
planned Queued for future work
|
|
147
|
+
blocked External arrival wait — monitor (hardware, vendor, rollout)
|
|
148
|
+
partial Shipped + deferred tail — spawn successor plans
|
|
149
|
+
paused Intentionally set aside — re-evaluate to resume
|
|
150
|
+
awaiting Needs human input/decision — chase the answer
|
|
151
|
+
queued-after Sequenced behind another plan — check predecessor
|
|
152
|
+
archived No longer relevant; auto-moved to archive directory
|
|
153
|
+
|
|
143
154
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
144
155
|
|
|
145
156
|
check: `dotmd check — validate frontmatter and references
|
|
@@ -262,6 +273,10 @@ Options:
|
|
|
262
273
|
--root <name> Create in a specific docs root
|
|
263
274
|
--list-templates Show available templates
|
|
264
275
|
|
|
276
|
+
For plans, the default status vocabulary is: in-session, active, planned,
|
|
277
|
+
blocked, partial, paused, awaiting, queued-after, archived. Run
|
|
278
|
+
\`dotmd status --help\` for what each one means.
|
|
279
|
+
|
|
265
280
|
The filename is derived from <name> by slugifying it.
|
|
266
281
|
Use --dry-run (-n) to preview without creating the file.`,
|
|
267
282
|
|
|
@@ -350,7 +365,7 @@ Finds all docs where the given field equals old-value and updates it
|
|
|
350
365
|
to new-value.
|
|
351
366
|
|
|
352
367
|
Examples:
|
|
353
|
-
dotmd migrate status research
|
|
368
|
+
dotmd migrate status research scoping
|
|
354
369
|
dotmd migrate module auth identity
|
|
355
370
|
|
|
356
371
|
Use --dry-run (-n) to preview changes without writing anything.`,
|
|
@@ -368,12 +383,18 @@ modules, and reference fields to pre-populate the config.`,
|
|
|
368
383
|
Shows all documents with type: plan, sorted by status.
|
|
369
384
|
Supports all query flags (--status, --module, --json, --sort, --group, etc.)
|
|
370
385
|
|
|
386
|
+
Default plan statuses: in-session, active, planned, blocked, partial,
|
|
387
|
+
paused, awaiting, queued-after, archived. Run \`dotmd status --help\` for
|
|
388
|
+
the unstuck-action behind each one.
|
|
389
|
+
|
|
371
390
|
Examples:
|
|
372
|
-
dotmd plans
|
|
373
|
-
dotmd plans --status active
|
|
374
|
-
dotmd plans --
|
|
375
|
-
dotmd plans --
|
|
376
|
-
dotmd plans --
|
|
391
|
+
dotmd plans # all plans
|
|
392
|
+
dotmd plans --status active # active plans only
|
|
393
|
+
dotmd plans --status awaiting # plans waiting on a human decision
|
|
394
|
+
dotmd plans --status partial,paused # shipped-tail and parked plans
|
|
395
|
+
dotmd plans --module auth # plans for the auth module
|
|
396
|
+
dotmd plans --group module # all plans grouped by module
|
|
397
|
+
dotmd plans --json # JSON output`,
|
|
377
398
|
|
|
378
399
|
stale: `dotmd stale — list stale documents
|
|
379
400
|
|
package/dotmd.config.example.mjs
CHANGED
|
@@ -25,20 +25,35 @@ export const excludeDirs = ['evidence'];
|
|
|
25
25
|
// context: 'expanded' | 'listed' | 'counted' (default: 'counted')
|
|
26
26
|
// staleDays: number | null — stale threshold (default: null = never stale)
|
|
27
27
|
// requiresModule: boolean — require `module` frontmatter (default: false)
|
|
28
|
-
// terminal: boolean — skip current_state/next_step warnings (default: false)
|
|
29
28
|
// archive: boolean — auto-move to archiveDir on transition (default: false)
|
|
29
|
+
// terminal: boolean — closed state; excluded from active-work stats/coverage scope (default: false)
|
|
30
30
|
// skipStale: boolean — exempt from stale checks (default: false)
|
|
31
31
|
// skipWarnings: boolean — exempt from validation warnings (default: false)
|
|
32
|
+
// quiet: boolean — sugar for `skipStale: true, skipWarnings: true`. Use for visible-but-quiet
|
|
33
|
+
// statuses (e.g. `partial`) where you want no nagging but DO want them in scope.
|
|
34
|
+
// Setting `skipStale: false` or `skipWarnings: false` explicitly overrides the sugar.
|
|
35
|
+
//
|
|
36
|
+
// `terminal` and `quiet` are orthogonal. Mark a status `terminal` only when it represents closure
|
|
37
|
+
// (excluded from active-work scope). Use `quiet` for noise suppression without closure semantics.
|
|
38
|
+
//
|
|
39
|
+
// Each plan stop-status maps to a distinct unstuck-action — the test for whether
|
|
40
|
+
// the vocabulary earns its weight. blocked = monitor (external arrival on its own
|
|
41
|
+
// schedule), awaiting = ask (chase the human/decision), queued-after = check the
|
|
42
|
+
// predecessor, paused = re-evaluate, partial = spawn successor plans for the
|
|
43
|
+
// deferred tail.
|
|
32
44
|
//
|
|
33
45
|
// export const types = {
|
|
34
46
|
// plan: {
|
|
35
47
|
// statuses: {
|
|
36
|
-
// 'in-session':
|
|
37
|
-
// 'active':
|
|
38
|
-
// 'planned':
|
|
39
|
-
// 'blocked':
|
|
40
|
-
// '
|
|
41
|
-
// '
|
|
48
|
+
// 'in-session': { context: 'expanded', staleDays: 1, requiresModule: true },
|
|
49
|
+
// 'active': { context: 'expanded', staleDays: 14, requiresModule: true },
|
|
50
|
+
// 'planned': { context: 'listed', staleDays: 30, requiresModule: true },
|
|
51
|
+
// 'blocked': { context: 'listed', staleDays: 30, requiresModule: true },
|
|
52
|
+
// 'partial': { context: 'expanded', requiresModule: true, quiet: true }, // shipped + deferred tail; visible, no nagging
|
|
53
|
+
// 'paused': { context: 'listed', requiresModule: true, quiet: true }, // intentionally set aside, no external dep
|
|
54
|
+
// 'awaiting': { context: 'listed', staleDays: 14, requiresModule: true }, // human input/decision wait — NOT quiet (pings get forgotten)
|
|
55
|
+
// 'queued-after': { context: 'counted', requiresModule: true, quiet: true }, // sequenced behind another plan
|
|
56
|
+
// 'archived': { context: 'counted', archive: true, terminal: true, quiet: true },
|
|
42
57
|
// },
|
|
43
58
|
// },
|
|
44
59
|
// doc: {
|
|
@@ -48,7 +63,7 @@ export const excludeDirs = ['evidence'];
|
|
|
48
63
|
// 'review': { context: 'listed', staleDays: 14 },
|
|
49
64
|
// 'reference': { context: 'counted', skipStale: true },
|
|
50
65
|
// 'deprecated': { context: 'counted', terminal: true, skipStale: true },
|
|
51
|
-
// 'archived': { context: 'counted', archive: true, terminal: true,
|
|
66
|
+
// 'archived': { context: 'counted', archive: true, terminal: true, quiet: true },
|
|
52
67
|
// },
|
|
53
68
|
// },
|
|
54
69
|
// };
|
|
@@ -57,16 +72,20 @@ export const excludeDirs = ['evidence'];
|
|
|
57
72
|
// When using array form, define behavior in separate statuses/lifecycle/taxonomy sections.
|
|
58
73
|
// export const types = {
|
|
59
74
|
// plan: {
|
|
60
|
-
// statuses: ['in-session', 'active', 'planned', 'blocked', '
|
|
61
|
-
// context: {
|
|
62
|
-
//
|
|
75
|
+
// statuses: ['in-session', 'active', 'planned', 'blocked', 'partial', 'paused', 'awaiting', 'queued-after', 'archived'],
|
|
76
|
+
// context: {
|
|
77
|
+
// expanded: ['in-session', 'active', 'partial'],
|
|
78
|
+
// listed: ['planned', 'blocked', 'paused', 'awaiting'],
|
|
79
|
+
// counted: ['queued-after', 'archived'],
|
|
80
|
+
// },
|
|
81
|
+
// staleDays: { 'in-session': 1, active: 14, planned: 30, blocked: 30, awaiting: 14 },
|
|
63
82
|
// },
|
|
64
83
|
// };
|
|
65
84
|
|
|
66
85
|
// Status workflow — fallback for docs without a type field. Order determines display grouping.
|
|
67
86
|
// When using rich status definitions, statuses.order and staleDays are derived automatically.
|
|
68
87
|
export const statuses = {
|
|
69
|
-
order: ['active', 'ready', 'planned', '
|
|
88
|
+
order: ['active', 'ready', 'planned', 'scoping', 'blocked', 'reference', 'archived'],
|
|
70
89
|
// Additional statuses valid only in specific roots (merged with order)
|
|
71
90
|
// Useful when different doc areas track different things (e.g. plans vs module docs)
|
|
72
91
|
// rootStatuses: {
|
|
@@ -79,7 +98,7 @@ export const statuses = {
|
|
|
79
98
|
ready: 14,
|
|
80
99
|
planned: 30,
|
|
81
100
|
blocked: 30,
|
|
82
|
-
|
|
101
|
+
scoping: 30,
|
|
83
102
|
},
|
|
84
103
|
};
|
|
85
104
|
|
|
@@ -111,7 +130,7 @@ export const index = {
|
|
|
111
130
|
export const context = {
|
|
112
131
|
expanded: ['active'],
|
|
113
132
|
listed: ['ready', 'planned'],
|
|
114
|
-
counted: ['blocked', '
|
|
133
|
+
counted: ['blocked', 'scoping', 'reference', 'archived'],
|
|
115
134
|
recentDays: 3,
|
|
116
135
|
recentStatuses: ['active', 'ready', 'planned'],
|
|
117
136
|
recentLimit: 10,
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -22,9 +22,13 @@ const DEFAULTS = {
|
|
|
22
22
|
|
|
23
23
|
types: {
|
|
24
24
|
plan: {
|
|
25
|
-
statuses: ['in-session', 'active', 'planned', 'blocked', '
|
|
26
|
-
context: {
|
|
27
|
-
|
|
25
|
+
statuses: ['in-session', 'active', 'planned', 'blocked', 'partial', 'paused', 'awaiting', 'queued-after', 'archived'],
|
|
26
|
+
context: {
|
|
27
|
+
expanded: ['in-session', 'active', 'partial'],
|
|
28
|
+
listed: ['planned', 'blocked', 'paused', 'awaiting'],
|
|
29
|
+
counted: ['queued-after', 'archived'],
|
|
30
|
+
},
|
|
31
|
+
staleDays: { 'in-session': 1, active: 14, planned: 30, blocked: 30, awaiting: 14 },
|
|
28
32
|
},
|
|
29
33
|
doc: {
|
|
30
34
|
statuses: ['draft', 'active', 'review', 'reference', 'deprecated', 'archived'],
|
|
@@ -39,26 +43,26 @@ const DEFAULTS = {
|
|
|
39
43
|
},
|
|
40
44
|
|
|
41
45
|
statuses: {
|
|
42
|
-
order: ['active', 'ready', 'planned', '
|
|
46
|
+
order: ['active', 'ready', 'planned', 'scoping', 'blocked', 'reference', 'archived'],
|
|
43
47
|
staleDays: {
|
|
44
48
|
active: 14,
|
|
45
49
|
ready: 14,
|
|
46
50
|
planned: 30,
|
|
47
51
|
blocked: 30,
|
|
48
|
-
|
|
52
|
+
scoping: 30,
|
|
49
53
|
},
|
|
50
54
|
},
|
|
51
55
|
|
|
52
56
|
lifecycle: {
|
|
53
57
|
archiveStatuses: ['archived'],
|
|
54
|
-
skipStaleFor: ['archived', 'reference'],
|
|
55
|
-
skipWarningsFor: ['archived'],
|
|
56
|
-
terminalStatuses: ['archived', 'deprecated', 'reference'
|
|
58
|
+
skipStaleFor: ['archived', 'reference', 'partial', 'paused', 'queued-after'],
|
|
59
|
+
skipWarningsFor: ['archived', 'partial', 'paused', 'queued-after'],
|
|
60
|
+
terminalStatuses: ['archived', 'deprecated', 'reference'],
|
|
57
61
|
},
|
|
58
62
|
|
|
59
63
|
taxonomy: {
|
|
60
64
|
surfaces: null,
|
|
61
|
-
moduleRequiredFor: [],
|
|
65
|
+
moduleRequiredFor: ['partial', 'paused', 'awaiting', 'queued-after'],
|
|
62
66
|
},
|
|
63
67
|
|
|
64
68
|
index: null,
|
|
@@ -66,7 +70,7 @@ const DEFAULTS = {
|
|
|
66
70
|
context: {
|
|
67
71
|
expanded: ['active'],
|
|
68
72
|
listed: ['ready', 'planned'],
|
|
69
|
-
counted: ['blocked', '
|
|
73
|
+
counted: ['blocked', 'scoping', 'reference', 'archived'],
|
|
70
74
|
recentDays: 3,
|
|
71
75
|
recentStatuses: ['active', 'ready', 'planned'],
|
|
72
76
|
recentLimit: 10,
|
|
@@ -92,7 +96,7 @@ const DEFAULTS = {
|
|
|
92
96
|
|
|
93
97
|
presets: {
|
|
94
98
|
plans: ['--type', 'plan', '--sort', 'status', '--all'],
|
|
95
|
-
stale: ['--status', 'active,ready,planned,blocked,
|
|
99
|
+
stale: ['--status', 'active,ready,planned,blocked,scoping', '--stale', '--sort', 'updated', '--all'],
|
|
96
100
|
actionable: ['--status', 'active,ready', '--has-next-step', '--sort', 'updated', '--all'],
|
|
97
101
|
},
|
|
98
102
|
};
|
|
@@ -147,9 +151,13 @@ function normalizeRichStatuses(config, userConfig) {
|
|
|
147
151
|
derived.staleDays[name] = p.staleDays;
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
// `quiet: true` is sugar for skipStale + skipWarnings unless those are explicitly false.
|
|
155
|
+
const quietImpliesSkipStale = p.quiet && p.skipStale !== false;
|
|
156
|
+
const quietImpliesSkipWarnings = p.quiet && p.skipWarnings !== false;
|
|
157
|
+
|
|
150
158
|
if (p.archive && !derived.archiveStatuses.includes(name)) derived.archiveStatuses.push(name);
|
|
151
|
-
if (p.skipStale && !derived.skipStaleFor.includes(name)) derived.skipStaleFor.push(name);
|
|
152
|
-
if (p.skipWarnings && !derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
|
|
159
|
+
if ((p.skipStale || quietImpliesSkipStale) && !derived.skipStaleFor.includes(name)) derived.skipStaleFor.push(name);
|
|
160
|
+
if ((p.skipWarnings || quietImpliesSkipWarnings) && !derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
|
|
153
161
|
if (p.terminal && !derived.terminalStatuses.includes(name)) derived.terminalStatuses.push(name);
|
|
154
162
|
if (p.requiresModule && !derived.moduleRequiredFor.includes(name)) derived.moduleRequiredFor.push(name);
|
|
155
163
|
|
package/src/health.mjs
CHANGED
|
@@ -76,7 +76,7 @@ export function runHealth(argv, config) {
|
|
|
76
76
|
|
|
77
77
|
// Pipeline
|
|
78
78
|
process.stdout.write(bold('Pipeline:') + '\n');
|
|
79
|
-
const pipeline = ['active', 'paused', 'ready', 'planned', 'blocked', '
|
|
79
|
+
const pipeline = ['active', 'paused', 'ready', 'planned', 'blocked', 'scoping', 'archived'];
|
|
80
80
|
for (const s of pipeline) {
|
|
81
81
|
const count = byStatus[s] || 0;
|
|
82
82
|
if (count > 0) {
|
package/src/init.mjs
CHANGED
|
@@ -68,7 +68,7 @@ function generateDetectedConfig(scan, rootPath) {
|
|
|
68
68
|
lines.push(`export const root = '${rootPath}';`);
|
|
69
69
|
lines.push('');
|
|
70
70
|
|
|
71
|
-
const defaultOrder = ['active', 'ready', 'planned', '
|
|
71
|
+
const defaultOrder = ['active', 'ready', 'planned', 'scoping', 'blocked', 'reference', 'archived'];
|
|
72
72
|
const ordered = defaultOrder.filter(s => scan.statuses.has(s));
|
|
73
73
|
const extra = [...scan.statuses].filter(s => !defaultOrder.includes(s)).sort();
|
|
74
74
|
const allStatuses = [...ordered, ...extra];
|
package/src/render.mjs
CHANGED
|
@@ -368,8 +368,8 @@ export function renderCoverage(index, config) {
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
export function buildCoverage(index, config) {
|
|
371
|
-
const scope = [...new Set(index.docs.map(d => d.status).filter(s => s && !config.lifecycle.terminalStatuses.has(s)
|
|
372
|
-
const scoped = index.docs.filter(doc => doc.status && !config.lifecycle.terminalStatuses.has(doc.status)
|
|
371
|
+
const scope = [...new Set(index.docs.map(d => d.status).filter(s => s && !config.lifecycle.terminalStatuses.has(s)))];
|
|
372
|
+
const scoped = index.docs.filter(doc => doc.status && !config.lifecycle.terminalStatuses.has(doc.status));
|
|
373
373
|
const missingSurface = scoped.filter(doc => !doc.surface);
|
|
374
374
|
const missingModule = scoped.filter(doc => !doc.module);
|
|
375
375
|
const modulePlatform = scoped.filter(doc => doc.module === 'platform');
|
|
@@ -403,7 +403,7 @@ export function formatSnapshot(doc, config) {
|
|
|
403
403
|
|
|
404
404
|
function _formatSnapshot(doc) {
|
|
405
405
|
const state = doc.currentState ?? 'No current_state set';
|
|
406
|
-
if (/^active:|^ready:|^planned:|^
|
|
406
|
+
if (/^active:|^ready:|^planned:|^scoping:|^blocked:|^archived:/i.test(state)) {
|
|
407
407
|
return state;
|
|
408
408
|
}
|
|
409
409
|
return `${capitalize(doc.status ?? 'unknown')}: ${state}`;
|
package/src/stats.mjs
CHANGED
|
@@ -8,10 +8,10 @@ function pct(n, total) {
|
|
|
8
8
|
|
|
9
9
|
export function buildStats(index, config) {
|
|
10
10
|
const docs = index.docs;
|
|
11
|
-
const scope = config.statusOrder.filter(s => !config.lifecycle.terminalStatuses.has(s)
|
|
11
|
+
const scope = config.statusOrder.filter(s => !config.lifecycle.terminalStatuses.has(s));
|
|
12
12
|
for (const typeSet of (config.typeStatuses?.values() ?? [])) {
|
|
13
13
|
for (const s of typeSet) {
|
|
14
|
-
if (!config.lifecycle.terminalStatuses.has(s) && !
|
|
14
|
+
if (!config.lifecycle.terminalStatuses.has(s) && !scope.includes(s)) scope.push(s);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
const scoped = docs.filter(d => scope.includes(d.status));
|