-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdotmd.config.example.mjs
More file actions
294 lines (272 loc) · 16.2 KB
/
Copy pathdotmd.config.example.mjs
File metadata and controls
294 lines (272 loc) · 16.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
// dotmd.config.mjs — document management configuration
// All exports are optional. Omitted values use built-in defaults.
// Place this file at the root of your project.
// ─── Static Config ───────────────────────────────────────────────────────────
// Directory containing your markdown docs (relative to this config file)
export const root = 'docs';
// Subdirectory for archived docs (used by `dotmd archive` and `dotmd status`)
export const archiveDir = 'archived';
// Directories to skip when scanning
export const excludeDirs = ['evidence'];
// Document types — each type has its own status vocabulary and context layout.
// Defaults: plan, doc, prompt. Override to customize statuses per type, or add new types.
//
// Statuses can be defined as an array (names only) or as an object (rich form).
// The object form co-locates all behavioral properties with each status,
// eliminating the need for separate lifecycle, staleDays, context, and taxonomy sections.
// ─── Rich status definitions (recommended) ──────────────────────────────────
// Each status is an object with optional properties:
// context: 'expanded' | 'listed' | 'counted' (default: 'counted')
// staleDays: number | null — stale threshold (default: null = never stale)
// requiresModule: boolean — require `module` frontmatter (default: false)
// archive: boolean — auto-move to archiveDir on transition (default: false)
// terminal: boolean — closed state; excluded from active-work stats/coverage scope (default: false)
// skipStale: boolean — exempt from stale checks (default: false)
// skipWarnings: boolean — exempt from validation warnings (default: false)
// quiet: boolean — sugar for `skipStale: true, skipWarnings: true`. Use for visible-but-quiet
// statuses (e.g. `partial`) where you want no nagging but DO want them in scope.
// Setting `skipStale: false` or `skipWarnings: false` explicitly overrides the sugar.
//
// `terminal` and `quiet` are orthogonal. Mark a status `terminal` only when it represents closure
// (excluded from active-work scope). Use `quiet` for noise suppression without closure semantics.
//
// Contradiction check (since 0.36.2): dotmd `warn()`s at config-load when a status combines
// `skipStale: true` with a `staleDays` value (the number is silently ignored) or
// `skipWarnings: true` with `requiresModule: true` (the module requirement can never fire).
// The same check applies via the `quiet: true` sugar. Drop one of the conflicting fields to silence.
//
// Each plan stop-status maps to a distinct unstuck-action — the test for whether
// the vocabulary earns its weight. blocked = monitor (external arrival on its own
// schedule), awaiting = ask (chase the human/decision), queued-after = check the
// predecessor, paused = re-evaluate, partial = spawn successor plans for the
// deferred tail.
//
// export const types = {
// plan: {
// statuses: {
// 'in-session': { context: 'expanded', staleDays: 1, requiresModule: true },
// 'active': { context: 'expanded', staleDays: 14, requiresModule: true },
// 'planned': { context: 'listed', staleDays: 30, requiresModule: true },
// 'blocked': { context: 'listed', staleDays: 30, requiresModule: true },
// 'partial': { context: 'expanded', requiresModule: true, quiet: true }, // shipped + deferred tail; visible, no nagging
// 'paused': { context: 'listed', staleDays: 3, requiresModule: true }, // stopped mid-work, near-term review — loud (short stale threshold)
// 'awaiting': { context: 'listed', staleDays: 14, requiresModule: true }, // human input/decision wait — NOT quiet (pings get forgotten)
// 'queued-after': { context: 'counted', requiresModule: true, quiet: true }, // sequenced behind another plan
// 'archived': { context: 'counted', archive: true, terminal: true, quiet: true },
// },
// },
// doc: {
// statuses: {
// 'draft': { context: 'listed', staleDays: 30 },
// 'active': { context: 'expanded', staleDays: 14 },
// 'review': { context: 'listed', staleDays: 14 },
// 'reference': { context: 'counted', skipStale: true },
// 'deprecated': { context: 'counted', terminal: true, skipStale: true },
// 'archived': { context: 'counted', archive: true, terminal: true, quiet: true },
// },
// },
// prompt: {
// // Saved prompts that seed future Claude sessions. `dotmd hud` surfaces
// // pending prompts on session start; `dotmd prompts next` claims the oldest.
// statuses: {
// 'pending': { context: 'expanded', staleDays: 30 },
// 'held': { context: 'counted', quiet: true }, // saved but not next: hidden from hud/briefing, skipped by no-arg `use`
// 'shelved': { context: 'counted', quiet: true }, // legacy alias for held
// 'claimed': { context: 'counted', quiet: true },
// 'archived': { context: 'counted', archive: true, terminal: true, quiet: true },
// },
// },
// };
// ─── Array form (also supported) ────────────────────────────────────────────
// When using array form, define behavior in separate statuses/lifecycle/taxonomy sections.
// export const types = {
// plan: {
// statuses: ['in-session', 'active', 'planned', 'blocked', 'partial', 'paused', 'awaiting', 'queued-after', 'archived'],
// context: {
// expanded: ['in-session', 'active', 'partial'],
// listed: ['planned', 'blocked', 'paused', 'awaiting'],
// counted: ['queued-after', 'archived'],
// },
// staleDays: { 'in-session': 1, active: 14, planned: 30, blocked: 30, awaiting: 14 },
// },
// };
// Status workflow — fallback for docs without a type field. Order determines display grouping.
// When using rich status definitions, statuses.order and staleDays are derived automatically.
export const statuses = {
order: ['active', 'ready', 'planned', 'scoping', 'blocked', 'reference', 'archived'],
// Additional statuses valid only in specific roots (merged with order)
// Useful when different doc areas track different things (e.g. plans vs module docs)
// rootStatuses: {
// 'docs/modules': ['implemented', 'partial', 'draft', 'deprecated'],
// 'docs/core': ['implemented', 'partial'],
// },
// Days after which a doc is considered stale (null = never stale)
staleDays: {
active: 14,
ready: 14,
planned: 30,
blocked: 30,
scoping: 30,
},
};
// Lifecycle behavior — which statuses trigger special handling.
//
// IMPORTANT: only enable this block if you're using ARRAY-form types (above) or
// no `types` definition. With rich-form types, the runtime derives these from
// per-status `terminal` / `archive` / `skipStale` / `skipWarnings` / `quiet`
// flags. An explicit `lifecycle` export sitting alongside rich-form types will
// SILENTLY OVERRIDE the per-status flags — `dotmd statuses` will warn you
// before writing into a config in that state.
//
// export const lifecycle = {
// archiveStatuses: ['archived'], // auto-move to archiveDir on transition
// skipStaleFor: ['archived'], // skip staleness checks
// skipWarningsFor: ['archived'], // skip validation warnings (summary, etc.)
// terminalStatuses: ['archived', 'deprecated', 'reference'], // skip current_state/next_step warnings, exclude from stats scope
// archiveNestedTypes: ['prompt'], // types that archive under <typeDir>/<archiveDir>
// // (e.g. docs/prompts/archived/) instead of the
// // shared <root>/<archiveDir>; set [] to disable
// };
// Taxonomy validation — set fields to null to skip validation.
// moduleRequiredFor is derived from requiresModule when using rich status definitions.
export const taxonomy = {
surfaces: ['web', 'ios', 'android', 'mobile', 'full-stack', 'frontend', 'backend', 'api', 'docs', 'ops', 'platform', 'infra', 'design'],
moduleRequiredFor: ['active', 'ready', 'planned', 'blocked'],
};
// Index file generation — remove this section to disable
export const index = {
path: 'docs/docs.md',
startMarker: '<!-- GENERATED:dotmd:start -->',
endMarker: '<!-- GENERATED:dotmd:end -->',
snapshot: 'status', // default; use 'state' to include live current_state text
archivedLimit: 8,
};
// Context briefing layout (`dotmd context`)
export const context = {
expanded: ['active'],
listed: ['ready', 'planned'],
counted: ['blocked', 'scoping', 'reference', 'archived'],
recentDays: 3,
recentStatuses: ['active', 'ready', 'planned'],
recentLimit: 10,
truncateNextStep: 80,
// Cap on slugs shown in the "Stale: …" tail before "…and N more (run `dotmd stale`)"
// takes over (since 0.36.2). Raise on wide terminals; lower for tighter briefings.
staleTailLimit: 8,
};
// Display settings
export const display = {
lineWidth: 0, // 0 = auto-detect terminal width
truncateTitle: 30,
truncateNextStep: 80,
};
// Reference fields for bidirectional link checking
export const referenceFields = {
bidirectional: ['related_docs'], // warn if A→B but B↛A
unidirectional: ['supports'], // one-way, no symmetry check
};
// Query presets — expand to filter args when used as commands
// Built-in: plans, stale, actionable. Add your own here:
export const presets = {
// plans: ['--type', 'plan', '--sort', 'status', '--all'], // built-in
// stale: ['--status', '...', '--stale', '--sort', 'updated', '--all'], // built-in
// actionable: ['--status', 'active,ready', '--has-next-step', ...], // built-in
mine: ['--owner', 'robert', '--status', 'active', '--all'],
};
// ─── Templates ───────────────────────────────────────────────────────────────
// Define new types or override builtins. `dotmd new <type> <name>` looks here first.
//
// Properties:
// description: string — shown in `dotmd new --list-types`
// defaultStatus: string — initial status if `--status` not passed
// acceptsBody: boolean — allow body input (inline / --body / @file / piped stdin).
// REQUIRED if you want `cat draft.md | dotmd new <type> <slug>` (or @path,
// --body, heredoc) to work. Your `body` fn must also interpolate the input,
// e.g. `${ctx?.bodyInput?.trim() ?? ''}`. See the body-acceptance guard below.
// requiresBody: boolean — error if no body input (implies acceptsBody; see `prompt` builtin)
// targetRoot: string — name (basename or suffix) of the root this type lives in.
// In flat-array `root` configs (e.g. ['docs/plans', 'docs/prompts']),
// the new doc lands in the matching root. Falls back to `config.docsRoot`
// if no root matches. The `--root` CLI flag still overrides.
// dir: string — subdirectory under `docsRoot` to use when `targetRoot` doesn't
// match anything. Builtin `plan` and `prompt` set both for portability:
// under `docsRoot='docs'`, `dir` puts files in `docs/plans/` and `docs/prompts/`;
// under flat-array roots, `targetRoot` routes directly to the type-specific root.
// frontmatter: (status, isoTime, ctx) => string
// body: (title, ctx) => string — slot user-supplied body via `ctx.bodyInput`
//
// Body-acceptance guard (the #1 custom-template gotcha):
// When you override a builtin and supply your OWN `body` fn that NEVER references
// `bodyInput`, dotmd assumes the fn would silently discard piped input — so it strips
// the inherited `acceptsBody`/`requiresBody` and rejects body input with a fail-fast
// error. Two ways to keep piped/@path/heredoc bodies working in a custom template:
// 1. interpolate `${ctx?.bodyInput?.trim() ?? ''}` somewhere in your `body` fn, OR
// 2. set `acceptsBody: true` explicitly (do BOTH if you want the input to actually land).
// A `body: (t) =>` that ignores `ctx` is the classic trap — it scaffolds fine but
// `dotmd new <type> <slug> < draft.md` errors until you wire in `bodyInput`.
//
// Custom type example — adds a `spike` type that lives in the `spikes` root (or
// under `docs/spikes/` in single-root layouts):
// export const templates = {
// spike: {
// description: 'Timeboxed investigation',
// defaultStatus: 'active',
// targetRoot: 'spikes', // for flat-array root configs
// dir: 'spikes', // for single-root configs
// frontmatter: (s, d) => `type: spike\nstatus: ${s}\ncreated: ${d}\ntimebox: 2d`,
// body: (t) => `\n# ${t}\n\n## Hypothesis\n\n\n\n## Findings\n\n`,
// },
//
// // Override a builtin (e.g. project-specific prompt frontmatter shape).
// // Overrides shallow-merge OVER the builtin: any property you omit is inherited
// // (`targetRoot`, `dir`, `defaultStatus`, `requiresBody`, …), and anything you declare
// // wins. EXCEPTION: if you supply your own `body` fn that doesn't reference `bodyInput`,
// // the inherited `acceptsBody`/`requiresBody` are dropped (see the guard above) — so
// // re-declare `acceptsBody: true` and wire in `ctx.bodyInput` if you want piped bodies.
// // prompt: {
// // description: 'Project resume prompt',
// // defaultStatus: 'pending',
// // requiresBody: true,
// // targetRoot: 'prompts',
// // dir: 'prompts',
// // frontmatter: (s, d, ctx) => `type: prompt\nstatus: ${s}\nproject_field: yes`,
// // body: (t, ctx) => `\n${ctx?.bodyInput ?? ''}\n`,
// // },
// };
// ─── Function Hooks ──────────────────────────────────────────────────────────
// Hooks are optional. Each receives a default implementation it can wrap or replace.
// Custom validation — called after built-in validation for each doc.
// Return { errors: [], warnings: [] } to add issues.
// export function validate(doc, ctx) {
// const warnings = [];
// if (doc.status === 'active' && !doc.owner) {
// warnings.push({ path: doc.path, level: 'warning', message: 'Active docs should have an owner.' });
// }
// return { errors: [], warnings };
// }
// Render hooks — override any renderer by wrapping the default.
// export function renderContext(index, defaultRenderer) { return defaultRenderer(index); }
// export function renderCompactList(index, defaultRenderer) { return defaultRenderer(index); }
// export function renderCheck(index, defaultRenderer) { return defaultRenderer(index); }
// export function renderStats(stats, defaultRenderer) { return defaultRenderer(stats); }
// export function renderGraph(graph, defaultRenderer) { return defaultRenderer(graph); }
// export function formatSnapshot(doc, defaultFormatter) { return defaultFormatter(doc); }
// Post-parse doc transformation — add computed fields.
// export function transformDoc(doc) {
// return doc;
// }
// Lifecycle callbacks — fire after the operation completes.
// export function onArchive(doc, { oldPath, newPath }) {}
// export function onStatusChange(doc, { oldStatus, newStatus, path }) {}
// export function onTouch(doc, { path, date }) {}
// export function onNew({ path, status, title, template }) {}
// export function onRename({ oldPath, newPath, referencesUpdated }) {}
// export function onLint({ path, fixes }) {}
// Pickup hooks are delivered at least once. A retry reuses operationId, so any
// external side effect must deduplicate by that stable ID. Delivery uses a
// persisted process-identity lease; live/unverifiable owners are not reclaimed
// by age, and an expired lease retries only when its owner is demonstrably dead.
// export function onPickup({ path, oldStatus, newStatus, operationId }) {}
// AI hooks — override summarization (replaces local MLX model).
// export function summarizeDoc(body, meta) { return 'Custom summary'; }
// export function summarizeDiff(diffOutput, filePath) { return 'Custom diff summary'; }