runlist 0.0.0 → 0.94.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/LICENSE +21 -0
- package/README.md +1114 -6
- package/assets/app/app.css +283 -0
- package/assets/app/app.mjs +381 -0
- package/assets/app/block-editor.mjs +182 -0
- package/assets/app/block-model.mjs +52 -0
- package/assets/app/document-create.mjs +86 -0
- package/assets/app/document-lifecycle.mjs +36 -0
- package/assets/app/document-stages.mjs +22 -0
- package/assets/app/document-views.mjs +33 -0
- package/assets/app/document-yardstick.mjs +258 -0
- package/assets/app/editor-navigation.mjs +51 -0
- package/assets/app/filing-navigation.mjs +138 -0
- package/assets/app/git-changes.mjs +80 -0
- package/assets/app/git-commit.mjs +46 -0
- package/assets/app/index.html +70 -0
- package/assets/app/library-navigation.mjs +93 -0
- package/assets/app/outline.mjs +18 -0
- package/assets/app/quick-navigation.mjs +52 -0
- package/assets/app/record-navigation.mjs +78 -0
- package/assets/app/recovery-center.mjs +53 -0
- package/assets/app/semantic-search.mjs +14 -0
- package/assets/app/settings.mjs +15 -0
- package/assets/app/shared.mjs +104 -0
- package/assets/app/stage-source.mjs +75 -0
- package/assets/app/template-editor.mjs +47 -0
- package/assets/app/theme.mjs +25 -0
- package/assets/app/transport.mjs +43 -0
- package/assets/contracts/source-editing-v1.md +232 -0
- package/assets/opencode/plugin.js +162 -0
- package/assets/schemas/runlist-record-v1.schema.json +912 -0
- package/bin/dotmd.mjs +5 -0
- package/bin/runlist.mjs +2714 -0
- package/package.json +53 -6
- package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
- package/plugins/runlist-codex/bin/runlist-hook +86 -0
- package/plugins/runlist-codex/hooks/hooks.json +16 -0
- package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
- package/runlist.config.example.mjs +370 -0
- package/scripts/postinstall.mjs +56 -0
- package/src/agent-context.mjs +201 -0
- package/src/ai.mjs +30 -0
- package/src/app-create.mjs +169 -0
- package/src/app-filing.mjs +139 -0
- package/src/app-git-commits.mjs +48 -0
- package/src/app-git.mjs +180 -0
- package/src/app-library.mjs +152 -0
- package/src/app-lifecycle-worker.mjs +12 -0
- package/src/app-lifecycle.mjs +79 -0
- package/src/app-records.mjs +134 -0
- package/src/app-search.mjs +80 -0
- package/src/app-semantic-search.mjs +105 -0
- package/src/app-service.mjs +208 -0
- package/src/app-yardstick.mjs +269 -0
- package/src/app.mjs +102 -0
- package/src/atomic-mutation.mjs +1736 -0
- package/src/baton.mjs +435 -0
- package/src/body-link.mjs +48 -0
- package/src/bulk-tag.mjs +174 -0
- package/src/check-collapse.mjs +75 -0
- package/src/claude-commands.mjs +75 -0
- package/src/cli-editor.mjs +8 -0
- package/src/code-refs.mjs +545 -0
- package/src/codex-integration.mjs +81 -0
- package/src/color.mjs +14 -0
- package/src/commands.mjs +374 -0
- package/src/completions.mjs +93 -0
- package/src/config-edit.mjs +606 -0
- package/src/config.mjs +705 -0
- package/src/decision-action.mjs +58 -0
- package/src/decision.mjs +220 -0
- package/src/decisions.mjs +787 -0
- package/src/deps.mjs +317 -0
- package/src/desktop-install.mjs +158 -0
- package/src/desktop-runtime.mjs +4 -0
- package/src/desktop.mjs +25 -0
- package/src/diff.mjs +101 -0
- package/src/doctor.mjs +591 -0
- package/src/durable-rename.mjs +55 -0
- package/src/errors-read.mjs +44 -0
- package/src/export.mjs +496 -0
- package/src/extractors.mjs +101 -0
- package/src/filing.mjs +255 -0
- package/src/fix-membership.mjs +134 -0
- package/src/fix-refs.mjs +174 -0
- package/src/flags.mjs +318 -0
- package/src/frontmatter-fix.mjs +195 -0
- package/src/frontmatter.mjs +252 -0
- package/src/git-commit-filters.mjs +32 -0
- package/src/git-commit-protocol.mjs +198 -0
- package/src/git-commit-signing.mjs +75 -0
- package/src/git-commit-store.mjs +26 -0
- package/src/git-commit-support.mjs +16 -0
- package/src/git-commit-worker.mjs +32 -0
- package/src/git.mjs +909 -0
- package/src/glossary-check.mjs +35 -0
- package/src/glossary.mjs +243 -0
- package/src/graph.mjs +298 -0
- package/src/guard.mjs +488 -0
- package/src/health.mjs +212 -0
- package/src/hints.mjs +129 -0
- package/src/host-integration.mjs +339 -0
- package/src/hub-membership.mjs +284 -0
- package/src/hub.mjs +292 -0
- package/src/hud.mjs +436 -0
- package/src/index-file.mjs +163 -0
- package/src/index.mjs +478 -0
- package/src/init.mjs +512 -0
- package/src/install.mjs +222 -0
- package/src/journal-read.mjs +116 -0
- package/src/journal.mjs +472 -0
- package/src/lifecycle.mjs +1444 -0
- package/src/lint.mjs +386 -0
- package/src/managed-path.mjs +192 -0
- package/src/markdown-code-spans.mjs +58 -0
- package/src/migrate-prompts.mjs +171 -0
- package/src/migrate-template.mjs +170 -0
- package/src/migrate.mjs +90 -0
- package/src/misuse-read.mjs +61 -0
- package/src/model-request.mjs +33 -0
- package/src/model.mjs +523 -0
- package/src/modules.mjs +355 -0
- package/src/naming.mjs +55 -0
- package/src/native-action.mjs +59 -0
- package/src/native-record.mjs +244 -0
- package/src/new.mjs +1321 -0
- package/src/output-identity.mjs +111 -0
- package/src/parent-plan.mjs +58 -0
- package/src/parse-cache.mjs +99 -0
- package/src/path-containment.mjs +15 -0
- package/src/pickup-card.mjs +293 -0
- package/src/pickup.mjs +671 -0
- package/src/prompt.mjs +25 -0
- package/src/prompts.mjs +585 -0
- package/src/query.mjs +977 -0
- package/src/record-action.mjs +11 -0
- package/src/record-create.mjs +82 -0
- package/src/reference-planner.mjs +388 -0
- package/src/reference-values.mjs +11 -0
- package/src/rename.mjs +107 -0
- package/src/render.mjs +678 -0
- package/src/roadmap.mjs +184 -0
- package/src/runlist.mjs +944 -0
- package/src/section.mjs +225 -0
- package/src/semantic-search-client.mjs +58 -0
- package/src/ship.mjs +166 -0
- package/src/show.mjs +101 -0
- package/src/skill-drift.mjs +98 -0
- package/src/source-editor.mjs +491 -0
- package/src/stages.mjs +73 -0
- package/src/state-migration.mjs +123 -0
- package/src/stats.mjs +200 -0
- package/src/status-metadata.mjs +87 -0
- package/src/statuses.mjs +721 -0
- package/src/summary.mjs +74 -0
- package/src/surfaces.mjs +28 -0
- package/src/sync-status.mjs +316 -0
- package/src/template-store.mjs +62 -0
- package/src/update.mjs +338 -0
- package/src/use.mjs +58 -0
- package/src/util.mjs +337 -0
- package/src/validate.mjs +794 -0
- package/src/watch.mjs +56 -0
- package/src/xref.mjs +406 -0
- package/src/yardstick-command.mjs +178 -0
- package/src/yardstick.mjs +112 -0
package/src/config.mjs
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { die, warn } from './util.mjs';
|
|
5
|
+
import { CONFIG_FILENAMES } from './naming.mjs';
|
|
6
|
+
import { validateStageDefinitions } from './stages.mjs';
|
|
7
|
+
import { validateFilingConfig } from './filing.mjs';
|
|
8
|
+
import { validateYardstickConfig } from './yardstick.mjs';
|
|
9
|
+
|
|
10
|
+
// Keys where user config replaces defaults entirely (not deep-merged).
|
|
11
|
+
// These are flat maps or config sections where the user's version is authoritative —
|
|
12
|
+
// default keys that the user omitted should NOT survive the merge.
|
|
13
|
+
const REPLACE_KEYS = new Set([
|
|
14
|
+
'statuses.staleDays',
|
|
15
|
+
'statuses.rootStatuses',
|
|
16
|
+
'lifecycle.filedStatuses',
|
|
17
|
+
'presets',
|
|
18
|
+
'context',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const DEFAULTS = {
|
|
22
|
+
root: '.',
|
|
23
|
+
archiveDir: 'archived',
|
|
24
|
+
excludeDirs: [],
|
|
25
|
+
externalBodyLinkRoots: [],
|
|
26
|
+
records: null,
|
|
27
|
+
filing: null,
|
|
28
|
+
yardstick: null,
|
|
29
|
+
// Floor under the scan surface; null = off. See `applyScanFloor` in validate.mjs.
|
|
30
|
+
minDocs: null,
|
|
31
|
+
|
|
32
|
+
// Reference repair outside the doc roots (`runlist refs`). An empty
|
|
33
|
+
// `codeRoots` is off, which is the behaviour before this key existed: the
|
|
34
|
+
// doc-root repair archive and rename already run is unchanged either way.
|
|
35
|
+
// Paths are repo-relative. `null` on the two lists means "the built-in
|
|
36
|
+
// list", which lives in code-refs.mjs so config.mjs stays a leaf.
|
|
37
|
+
codeRoots: [],
|
|
38
|
+
codeExtensions: null,
|
|
39
|
+
codeExcludes: null,
|
|
40
|
+
// Files whose content is data keyed on document paths — a guard baseline
|
|
41
|
+
// whose keys are plan paths, for instance. Reported, never written.
|
|
42
|
+
codeRefsUntouched: [],
|
|
43
|
+
|
|
44
|
+
types: {
|
|
45
|
+
plan: {
|
|
46
|
+
statuses: ['in-session', 'active', 'planned', 'blocked', 'partial', 'paused', 'awaiting', 'queued-after', 'archived'],
|
|
47
|
+
context: {
|
|
48
|
+
expanded: ['in-session', 'active', 'partial'],
|
|
49
|
+
listed: ['planned', 'blocked', 'paused', 'awaiting'],
|
|
50
|
+
counted: ['queued-after', 'archived'],
|
|
51
|
+
},
|
|
52
|
+
staleDays: { 'in-session': 1, active: 14, planned: 30, blocked: 30, paused: 3, awaiting: 14 },
|
|
53
|
+
},
|
|
54
|
+
doc: {
|
|
55
|
+
statuses: ['draft', 'active', 'review', 'reference', 'deprecated', 'archived'],
|
|
56
|
+
context: { expanded: ['active'], listed: ['draft', 'review'], counted: ['reference', 'deprecated', 'archived'] },
|
|
57
|
+
staleDays: { draft: 30, active: 14, review: 14 },
|
|
58
|
+
},
|
|
59
|
+
// A prompt has two states and no third. It is pending — the next session
|
|
60
|
+
// should consume it — or it is archived, consumed, body kept under the
|
|
61
|
+
// archive directory. `held`, `shelved` and `claimed` were removed 2026-08-30:
|
|
62
|
+
// a prompt directs work, so parking one instead of archiving it leaves work
|
|
63
|
+
// state sitting outside the plan that owns it, where it fights with the
|
|
64
|
+
// plan's own status and neither one is the truth. If a prompt should not be
|
|
65
|
+
// consumed, its content belongs in the plan and the prompt should be
|
|
66
|
+
// archived. See lifecycle.filedStatuses — prompts file nowhere.
|
|
67
|
+
prompt: {
|
|
68
|
+
statuses: ['pending', 'archived'],
|
|
69
|
+
context: { expanded: ['pending'], counted: ['archived'] },
|
|
70
|
+
staleDays: { pending: 30 },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
statuses: {
|
|
75
|
+
order: ['active', 'ready', 'planned', 'scoping', 'blocked', 'reference', 'archived'],
|
|
76
|
+
staleDays: {
|
|
77
|
+
active: 14,
|
|
78
|
+
ready: 14,
|
|
79
|
+
planned: 30,
|
|
80
|
+
blocked: 30,
|
|
81
|
+
scoping: 30,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
lifecycle: {
|
|
86
|
+
startableStatuses: ['active', 'planned'],
|
|
87
|
+
archiveStatuses: ['archived'],
|
|
88
|
+
skipStaleFor: ['archived', 'reference', 'partial', 'queued-after'],
|
|
89
|
+
skipWarningsFor: ['archived', 'partial', 'queued-after'],
|
|
90
|
+
terminalStatuses: ['archived', 'deprecated', 'reference'],
|
|
91
|
+
// F15: per-status filing buckets (status → dirName). Built-in held/paused
|
|
92
|
+
// statuses file under the owning type folder; archive remains a separate
|
|
93
|
+
// primitive untouched.
|
|
94
|
+
// Plans only. A paused plan files under <plansDir>/held/ so the live plan
|
|
95
|
+
// list stays readable. Prompts were removed from this map 2026-08-30 along
|
|
96
|
+
// with their held/shelved statuses — `archived` is the only directory a
|
|
97
|
+
// prompt ever moves into.
|
|
98
|
+
filedStatuses: { paused: 'held' },
|
|
99
|
+
// Types whose archive nests under their own type dir (<typeDir>/<archiveDir>,
|
|
100
|
+
// e.g. docs/prompts/archived/) instead of the shared <root>/<archiveDir>.
|
|
101
|
+
// Prompts are session-local churn — keeping their archive out of the shared
|
|
102
|
+
// docs/archived/ stops them from burying plans/docs there. Set to [] to send
|
|
103
|
+
// every type to the shared archive.
|
|
104
|
+
archiveNestedTypes: ['prompt'],
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
taxonomy: {
|
|
108
|
+
surfaces: null,
|
|
109
|
+
// Ordered delivery stages; null leaves the repository vocabulary unrestricted.
|
|
110
|
+
milestones: null,
|
|
111
|
+
// modules: when null (default), the project doesn't enumerate product
|
|
112
|
+
// modules — the modules-required validator silently skips. When set to
|
|
113
|
+
// an array, modules are taxonomy-enforced AND moduleRequiredFor activates.
|
|
114
|
+
// Mirrors the long-standing semantics of `taxonomy.surfaces`.
|
|
115
|
+
modules: null,
|
|
116
|
+
moduleRequiredFor: ['partial', 'paused', 'awaiting', 'queued-after'],
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
index: null,
|
|
120
|
+
|
|
121
|
+
context: {
|
|
122
|
+
expanded: ['active'],
|
|
123
|
+
listed: ['ready', 'planned'],
|
|
124
|
+
counted: ['blocked', 'scoping', 'reference', 'archived'],
|
|
125
|
+
recentDays: 3,
|
|
126
|
+
recentStatuses: ['active', 'ready', 'planned'],
|
|
127
|
+
recentLimit: 10,
|
|
128
|
+
truncateNextStep: 80,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
display: {
|
|
132
|
+
lineWidth: 0,
|
|
133
|
+
truncateTitle: 30,
|
|
134
|
+
truncateNextStep: 80,
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
referenceFields: {
|
|
138
|
+
// Defaults match the fields the built-in `plan`, `doc`, and `prompt`
|
|
139
|
+
// templates scaffold, so graph / deps / unblocks / pickup's Related:
|
|
140
|
+
// resolver work without config. Override by setting `referenceFields`
|
|
141
|
+
// in your config — your value fully replaces (no per-field merge).
|
|
142
|
+
bidirectional: ['related_plans', 'related_docs'],
|
|
143
|
+
unidirectional: ['parent_plan', 'runlist'],
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
templates: {},
|
|
147
|
+
|
|
148
|
+
glossary: null,
|
|
149
|
+
// `runlist new decision`: { section, prefix, register: { file, statusLine } }.
|
|
150
|
+
decisions: null,
|
|
151
|
+
// `runlist flag` / `runlist flags`: { file } moves the log from .runlist/flags.jsonl.
|
|
152
|
+
flags: null,
|
|
153
|
+
|
|
154
|
+
// Opt-in JSONL command journal at .runlist/journal.jsonl. Default off — agents
|
|
155
|
+
// and users who want usage observability flip this on (or set RUNLIST_JOURNAL=1).
|
|
156
|
+
journal: false,
|
|
157
|
+
|
|
158
|
+
// PreToolUse guard behavior. `deny: false` drops status edits to warnings;
|
|
159
|
+
// `promptReads: 'deny'` blocks direct reads of existing pending prompts.
|
|
160
|
+
guard: { deny: true, promptReads: 'warn' },
|
|
161
|
+
|
|
162
|
+
presets: {
|
|
163
|
+
stale: ['--status', 'active,ready,planned,blocked,scoping', '--stale', '--sort', 'updated', '--all'],
|
|
164
|
+
actionable: ['--status', 'active,ready', '--has-next-step', '--sort', 'updated', '--all'],
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const VALID_CONTEXT_VALUES = new Set(['expanded', 'listed', 'counted']);
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Normalize rich status definitions (object form) into array form + derived config.
|
|
172
|
+
* When types.<type>.statuses is an object like:
|
|
173
|
+
* { 'active': { context: 'expanded', staleDays: 14, requiresModule: true }, ... }
|
|
174
|
+
* this extracts all behavioral properties, converts statuses to an array for
|
|
175
|
+
* downstream processing, and returns derived values for lifecycle/taxonomy/context.
|
|
176
|
+
*
|
|
177
|
+
* Returns null if no types use object-form statuses.
|
|
178
|
+
*/
|
|
179
|
+
function normalizeRichStatuses(config, userConfig) {
|
|
180
|
+
const derived = {
|
|
181
|
+
startableStatuses: [],
|
|
182
|
+
archiveStatuses: [],
|
|
183
|
+
skipStaleFor: [],
|
|
184
|
+
skipWarningsFor: [],
|
|
185
|
+
// Same membership as `skipWarningsFor`, but kept per declaring type. The flat
|
|
186
|
+
// list is a union of status NAMES, so a name any type marks quiet suppresses
|
|
187
|
+
// warnings for every other type that happens to share it — `journey.active`
|
|
188
|
+
// silencing every `active` plan in the repo. Statuses are type-scoped
|
|
189
|
+
// everywhere else (`typeStatuses`, `isValidStatus`), so the suppression that
|
|
190
|
+
// reads them has to be too.
|
|
191
|
+
skipWarningsForByType: {},
|
|
192
|
+
terminalStatuses: [],
|
|
193
|
+
terminalStatusesByType: {},
|
|
194
|
+
moduleRequiredFor: [],
|
|
195
|
+
// F15: status-name → directory-name (defaults to the status name verbatim).
|
|
196
|
+
// Filed statuses move docs into <root>/<dirName>/ on transition INTO the
|
|
197
|
+
// status, and back to flat <root>/ on transition OUT. Separate from
|
|
198
|
+
// archive: true to avoid touching that long-standing primitive's
|
|
199
|
+
// semantics.
|
|
200
|
+
filedStatuses: {},
|
|
201
|
+
staleDays: {},
|
|
202
|
+
statusOrder: [],
|
|
203
|
+
context: { expanded: [], listed: [], counted: [] },
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
let hasRich = false;
|
|
207
|
+
|
|
208
|
+
for (const [typeName, typeDef] of Object.entries(config.types ?? {})) {
|
|
209
|
+
if (!typeDef.statuses || Array.isArray(typeDef.statuses)) continue;
|
|
210
|
+
if (typeof typeDef.statuses !== 'object') continue;
|
|
211
|
+
|
|
212
|
+
hasRich = true;
|
|
213
|
+
// Claim the key even when no status is quiet: an empty list means "this type
|
|
214
|
+
// declared its statuses and suppresses nothing", which must not fall back to
|
|
215
|
+
// the global union. Absent entirely means "type never declared rich statuses".
|
|
216
|
+
derived.skipWarningsForByType[typeName] ??= [];
|
|
217
|
+
derived.terminalStatusesByType[typeName] ??= [];
|
|
218
|
+
const statusNames = [];
|
|
219
|
+
const typeContext = { expanded: [], listed: [], counted: [] };
|
|
220
|
+
const typeStaleDays = {};
|
|
221
|
+
|
|
222
|
+
for (const [name, props] of Object.entries(typeDef.statuses)) {
|
|
223
|
+
const p = props ?? {};
|
|
224
|
+
statusNames.push(name);
|
|
225
|
+
|
|
226
|
+
const ctx = p.context ?? 'counted';
|
|
227
|
+
if (typeContext[ctx]) typeContext[ctx].push(name);
|
|
228
|
+
// Global context: only add if not already in any bucket (first type wins)
|
|
229
|
+
const inGlobal = derived.context.expanded.includes(name) ||
|
|
230
|
+
derived.context.listed.includes(name) || derived.context.counted.includes(name);
|
|
231
|
+
if (!inGlobal && derived.context[ctx]) derived.context[ctx].push(name);
|
|
232
|
+
|
|
233
|
+
if (p.staleDays != null) {
|
|
234
|
+
typeStaleDays[name] = p.staleDays;
|
|
235
|
+
derived.staleDays[name] = p.staleDays;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// `quiet: true` is sugar for skipStale + skipWarnings unless those are explicitly false.
|
|
239
|
+
const quietImpliesSkipStale = p.quiet && p.skipStale !== false;
|
|
240
|
+
const quietImpliesSkipWarnings = p.quiet && p.skipWarnings !== false;
|
|
241
|
+
|
|
242
|
+
// Contradiction diagnostics — emit at load so dead config doesn't fail silently.
|
|
243
|
+
const skipStaleEffective = p.skipStale === true || quietImpliesSkipStale;
|
|
244
|
+
const skipWarningsEffective = p.skipWarnings === true || quietImpliesSkipWarnings;
|
|
245
|
+
if (skipStaleEffective && p.staleDays != null) {
|
|
246
|
+
warn(`runlist config: status "${typeName}.${name}" has skipStale: true and staleDays: ${p.staleDays} — staleDays is ignored. Drop one to silence this warning.`);
|
|
247
|
+
}
|
|
248
|
+
if (skipWarningsEffective && p.requiresModule) {
|
|
249
|
+
warn(`runlist config: status "${typeName}.${name}" has skipWarnings: true and requiresModule: true — the module requirement can never fire. Drop one to silence this warning.`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (p.archive && !derived.archiveStatuses.includes(name)) derived.archiveStatuses.push(name);
|
|
253
|
+
if (p.startable && !derived.startableStatuses.includes(name)) derived.startableStatuses.push(name);
|
|
254
|
+
if ((p.skipStale || quietImpliesSkipStale) && !derived.skipStaleFor.includes(name)) derived.skipStaleFor.push(name);
|
|
255
|
+
if (p.skipWarnings || quietImpliesSkipWarnings) {
|
|
256
|
+
if (!derived.skipWarningsFor.includes(name)) derived.skipWarningsFor.push(name);
|
|
257
|
+
(derived.skipWarningsForByType[typeName] ??= []).push(name);
|
|
258
|
+
}
|
|
259
|
+
if (p.terminal) {
|
|
260
|
+
if (!derived.terminalStatuses.includes(name)) derived.terminalStatuses.push(name);
|
|
261
|
+
derived.terminalStatusesByType[typeName].push(name);
|
|
262
|
+
}
|
|
263
|
+
if (p.requiresModule && !derived.moduleRequiredFor.includes(name)) derived.moduleRequiredFor.push(name);
|
|
264
|
+
if (p.filed && !derived.filedStatuses[name]) {
|
|
265
|
+
// dirName defaults to the status name; users can override with
|
|
266
|
+
// `filed: 'custom-dir'` (string form) instead of `filed: true`.
|
|
267
|
+
derived.filedStatuses[name] = typeof p.filed === 'string' ? p.filed : name;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!derived.statusOrder.includes(name)) derived.statusOrder.push(name);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Convert to array form for downstream pipeline
|
|
274
|
+
typeDef.statuses = statusNames;
|
|
275
|
+
|
|
276
|
+
// Derive type-level context/staleDays unless user explicitly provided them
|
|
277
|
+
const userTypeDef = userConfig.types?.[typeName];
|
|
278
|
+
if (!userTypeDef?.context) typeDef.context = typeContext;
|
|
279
|
+
if (!userTypeDef?.staleDays) typeDef.staleDays = typeStaleDays;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!hasRich) return null;
|
|
283
|
+
return derived;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Apply derived values from rich status definitions into the merged config.
|
|
288
|
+
* Explicit user config always wins over derived values.
|
|
289
|
+
*/
|
|
290
|
+
function applyDerivedConfig(config, userConfig, derived) {
|
|
291
|
+
// statuses.order — derive from types if user didn't explicitly set
|
|
292
|
+
if (!userConfig.statuses?.order && derived.statusOrder.length) {
|
|
293
|
+
config.statuses.order = derived.statusOrder;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// statuses.staleDays — merge derived as base, user overrides
|
|
297
|
+
if (!userConfig.statuses?.staleDays && Object.keys(derived.staleDays).length) {
|
|
298
|
+
config.statuses.staleDays = derived.staleDays;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// lifecycle — each sub-key independently
|
|
302
|
+
if (!userConfig.lifecycle?.startableStatuses && derived.startableStatuses.length) {
|
|
303
|
+
config.lifecycle.startableStatuses = derived.startableStatuses;
|
|
304
|
+
}
|
|
305
|
+
if (!userConfig.lifecycle?.archiveStatuses && derived.archiveStatuses.length) {
|
|
306
|
+
config.lifecycle.archiveStatuses = derived.archiveStatuses;
|
|
307
|
+
}
|
|
308
|
+
if (!userConfig.lifecycle?.skipStaleFor && derived.skipStaleFor.length) {
|
|
309
|
+
config.lifecycle.skipStaleFor = derived.skipStaleFor;
|
|
310
|
+
}
|
|
311
|
+
// Assigned on BOTH branches, never conditionally: `config.lifecycle` can be the
|
|
312
|
+
// module-level DEFAULTS.lifecycle object itself (deepMerge shallow-copies, so a
|
|
313
|
+
// user config with no `lifecycle` key shares the reference), and a conditional
|
|
314
|
+
// write would leave a previous resolveConfig call's map in place.
|
|
315
|
+
if (!userConfig.lifecycle?.skipWarningsFor && derived.skipWarningsFor.length) {
|
|
316
|
+
config.lifecycle.skipWarningsFor = derived.skipWarningsFor;
|
|
317
|
+
config.lifecycle.skipWarningsForByType = derived.skipWarningsForByType;
|
|
318
|
+
} else {
|
|
319
|
+
// An explicit `lifecycle.skipWarningsFor` is a deliberate repo-wide statement
|
|
320
|
+
// and stays type-blind.
|
|
321
|
+
config.lifecycle.skipWarningsForByType = {};
|
|
322
|
+
}
|
|
323
|
+
if (!userConfig.lifecycle?.terminalStatuses && derived.terminalStatuses.length) {
|
|
324
|
+
config.lifecycle.terminalStatuses = derived.terminalStatuses;
|
|
325
|
+
config.lifecycle.terminalStatusesByType = derived.terminalStatusesByType;
|
|
326
|
+
} else {
|
|
327
|
+
// An explicit lifecycle list is intentionally repo-wide.
|
|
328
|
+
config.lifecycle.terminalStatusesByType = {};
|
|
329
|
+
}
|
|
330
|
+
if (!userConfig.lifecycle?.filedStatuses && Object.keys(derived.filedStatuses).length) {
|
|
331
|
+
config.lifecycle.filedStatuses = derived.filedStatuses;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// taxonomy.moduleRequiredFor
|
|
335
|
+
if (!userConfig.taxonomy?.moduleRequiredFor && derived.moduleRequiredFor.length) {
|
|
336
|
+
config.taxonomy.moduleRequiredFor = derived.moduleRequiredFor;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// context — only the status-related arrays, not recentDays/recentLimit/etc
|
|
340
|
+
if (!userConfig.context?.expanded) config.context.expanded = derived.context.expanded;
|
|
341
|
+
if (!userConfig.context?.listed) config.context.listed = derived.context.listed;
|
|
342
|
+
if (!userConfig.context?.counted) config.context.counted = derived.context.counted;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function findConfigFile(startDir) {
|
|
346
|
+
let dir = path.resolve(startDir);
|
|
347
|
+
const root = path.parse(dir).root;
|
|
348
|
+
|
|
349
|
+
while (dir !== root) {
|
|
350
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
351
|
+
const candidate = path.join(dir, filename);
|
|
352
|
+
if (existsSync(candidate)) return candidate;
|
|
353
|
+
}
|
|
354
|
+
dir = path.dirname(dir);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const VALID_CONFIG_KEYS = new Set(Object.keys(DEFAULTS));
|
|
361
|
+
|
|
362
|
+
function validateConfig(userConfig, config, validStatuses, indexPath) {
|
|
363
|
+
const warnings = validateStageDefinitions(config);
|
|
364
|
+
warnings.push(...validateFilingConfig(config.filing));
|
|
365
|
+
warnings.push(...validateYardstickConfig(config.yardstick));
|
|
366
|
+
|
|
367
|
+
// statuses.order must be array
|
|
368
|
+
if (config.statuses && config.statuses.order !== undefined && !Array.isArray(config.statuses.order)) {
|
|
369
|
+
warnings.push('Config: statuses.order must be an array.');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// archiveDir must be string
|
|
373
|
+
if (config.archiveDir !== undefined && typeof config.archiveDir !== 'string') {
|
|
374
|
+
warnings.push('Config: archiveDir must be a string.');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Lifecycle status lists must reference configured statuses.
|
|
378
|
+
if (config.lifecycle?.startableStatuses) {
|
|
379
|
+
for (const s of config.lifecycle.startableStatuses) {
|
|
380
|
+
if (!validStatuses.has(s)) {
|
|
381
|
+
warnings.push(`Config: lifecycle.startableStatuses contains unknown status '${s}'.`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// lifecycle.archiveStatuses values must exist in validStatuses
|
|
387
|
+
if (config.lifecycle?.archiveStatuses) {
|
|
388
|
+
for (const s of config.lifecycle.archiveStatuses) {
|
|
389
|
+
if (!validStatuses.has(s)) {
|
|
390
|
+
warnings.push(`Config: lifecycle.archiveStatuses contains unknown status '${s}'.`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// staleDays keys must exist in validStatuses — but only validate the map when
|
|
396
|
+
// the user actually wrote `statuses.staleDays`. When it's inherited from
|
|
397
|
+
// defaults (user overrode `statuses.order` but left staleDays unset), the
|
|
398
|
+
// default map is keyed by default statuses (`ready`, `scoping`, …) that the
|
|
399
|
+
// user's order may not include — warning there blames the user for keys they
|
|
400
|
+
// never authored and makes brownfield repos noisy on every command, hook
|
|
401
|
+
// included. Real typos in a user-provided map are still caught.
|
|
402
|
+
if (userConfig.statuses?.staleDays && config.statuses?.staleDays) {
|
|
403
|
+
for (const key of Object.keys(config.statuses.staleDays)) {
|
|
404
|
+
if (!validStatuses.has(key)) {
|
|
405
|
+
warnings.push(`Config: statuses.staleDays contains unknown status '${key}'.`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// taxonomy.surfaces must be null or array
|
|
411
|
+
if (config.taxonomy?.surfaces !== undefined && config.taxonomy.surfaces !== null && !Array.isArray(config.taxonomy.surfaces)) {
|
|
412
|
+
warnings.push('Config: taxonomy.surfaces must be null or an array.');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// index path file exists (if configured)
|
|
416
|
+
if (indexPath && !existsSync(indexPath)) {
|
|
417
|
+
warnings.push(`Config: index path does not exist: ${indexPath}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (config.index?.snapshot !== undefined && !['status', 'state'].includes(config.index.snapshot)) {
|
|
421
|
+
warnings.push("Config: index.snapshot must be 'status' or 'state'.");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
for (const key of ['codeRoots', 'codeRefsUntouched']) {
|
|
425
|
+
if (config[key] !== undefined && !Array.isArray(config[key])) {
|
|
426
|
+
warnings.push(`Config: ${key} must be an array.`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
for (const key of ['codeExtensions', 'codeExcludes']) {
|
|
430
|
+
if (config[key] != null && !Array.isArray(config[key])) {
|
|
431
|
+
warnings.push(`Config: ${key} must be an array.`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (userConfig.decisions != null) {
|
|
436
|
+
const d = userConfig.decisions;
|
|
437
|
+
if (typeof d !== 'object' || Array.isArray(d)) warnings.push('Config: decisions must be an object.');
|
|
438
|
+
else if (d.register != null && (typeof d.register?.file !== 'string' || typeof d.register?.statusLine !== 'string')) {
|
|
439
|
+
warnings.push('Config: decisions.register needs a file and a statusLine.');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Unknown top-level user config keys
|
|
444
|
+
for (const key of Object.keys(userConfig)) {
|
|
445
|
+
if (!VALID_CONFIG_KEYS.has(key)) {
|
|
446
|
+
warnings.push(`Config: unknown key '${key}'.`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return warnings;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function deepMerge(defaults, overrides, parentPath = '') {
|
|
454
|
+
const result = { ...defaults };
|
|
455
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
456
|
+
const keyPath = parentPath ? `${parentPath}.${key}` : key;
|
|
457
|
+
if (value != null && typeof value === 'object' && !Array.isArray(value) &&
|
|
458
|
+
result[key] != null && typeof result[key] === 'object' && !Array.isArray(result[key]) &&
|
|
459
|
+
!REPLACE_KEYS.has(keyPath)) {
|
|
460
|
+
result[key] = deepMerge(result[key], value, keyPath);
|
|
461
|
+
} else {
|
|
462
|
+
result[key] = value;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function resolveConfig(cwd, explicitConfigPath) {
|
|
469
|
+
const configPath = explicitConfigPath
|
|
470
|
+
? path.resolve(cwd, explicitConfigPath)
|
|
471
|
+
: findConfigFile(cwd);
|
|
472
|
+
|
|
473
|
+
let userConfig = {};
|
|
474
|
+
let hooks = {};
|
|
475
|
+
let configDir = cwd;
|
|
476
|
+
|
|
477
|
+
if (configPath && existsSync(configPath)) {
|
|
478
|
+
const configUrl = pathToFileURL(configPath).href;
|
|
479
|
+
let mod;
|
|
480
|
+
try {
|
|
481
|
+
mod = await import(configUrl);
|
|
482
|
+
} catch (err) {
|
|
483
|
+
die('Failed to load config: ' + configPath + '\n' + err.message + '\nCheck for syntax errors in your config file.');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
configDir = path.dirname(configPath);
|
|
487
|
+
|
|
488
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
489
|
+
if (key === 'default') continue;
|
|
490
|
+
if (typeof value === 'function') {
|
|
491
|
+
hooks[key] = value;
|
|
492
|
+
} else {
|
|
493
|
+
userConfig[key] = value;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Backwards compat: `readme` config key maps to `index`
|
|
499
|
+
if (userConfig.readme && !userConfig.index) {
|
|
500
|
+
userConfig.index = userConfig.readme;
|
|
501
|
+
delete userConfig.readme;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const config = deepMerge(DEFAULTS, userConfig);
|
|
505
|
+
// Replacement links remain repairable even with custom reference-field lists
|
|
506
|
+
// or after the repository turns off goal display.
|
|
507
|
+
config.referenceFields.unidirectional = [...new Set([
|
|
508
|
+
...(config.referenceFields.unidirectional ?? []), 'yardstick_into',
|
|
509
|
+
])];
|
|
510
|
+
|
|
511
|
+
// Normalize rich status definitions (object form → array + derived config)
|
|
512
|
+
const derived = normalizeRichStatuses(config, userConfig);
|
|
513
|
+
if (derived) applyDerivedConfig(config, userConfig, derived);
|
|
514
|
+
|
|
515
|
+
const rootPaths = Array.isArray(config.root) ? config.root : [config.root];
|
|
516
|
+
const docsRoots = rootPaths.map(r => path.resolve(configDir, r));
|
|
517
|
+
const docsRoot = docsRoots[0]; // primary root for backwards compat
|
|
518
|
+
|
|
519
|
+
const earlyWarnings = [];
|
|
520
|
+
for (const dr of docsRoots) {
|
|
521
|
+
if (!existsSync(dr)) {
|
|
522
|
+
earlyWarnings.push('Config: docs root does not exist: ' + dr);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Find repo root by walking up looking for .git
|
|
527
|
+
let repoRoot = configDir;
|
|
528
|
+
{
|
|
529
|
+
let dir = configDir;
|
|
530
|
+
const fsRoot = path.parse(dir).root;
|
|
531
|
+
while (dir !== fsRoot) {
|
|
532
|
+
if (existsSync(path.join(dir, '.git'))) {
|
|
533
|
+
repoRoot = dir;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
dir = path.dirname(dir);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Resolve document types
|
|
541
|
+
const typesConfig = config.types ?? {};
|
|
542
|
+
const validTypes = new Set(Object.keys(typesConfig));
|
|
543
|
+
const typeStatuses = new Map();
|
|
544
|
+
const typeContextConfig = new Map();
|
|
545
|
+
for (const [typeName, typeDef] of Object.entries(typesConfig)) {
|
|
546
|
+
typeStatuses.set(typeName, new Set(typeDef.statuses ?? []));
|
|
547
|
+
if (typeDef.context) typeContextConfig.set(typeName, typeDef.context);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const statusOrder = config.statuses.order;
|
|
551
|
+
const validStatuses = new Set(statusOrder);
|
|
552
|
+
// Merge all type-specific statuses into the global valid set
|
|
553
|
+
for (const typeSet of typeStatuses.values()) {
|
|
554
|
+
for (const s of typeSet) validStatuses.add(s);
|
|
555
|
+
}
|
|
556
|
+
const staleDaysByStatus = {};
|
|
557
|
+
for (const status of statusOrder) {
|
|
558
|
+
staleDaysByStatus[status] = config.statuses.staleDays?.[status] ?? null;
|
|
559
|
+
}
|
|
560
|
+
// Merge type-specific staleDays
|
|
561
|
+
for (const typeDef of Object.values(typesConfig)) {
|
|
562
|
+
if (typeDef.staleDays) {
|
|
563
|
+
for (const [status, days] of Object.entries(typeDef.staleDays)) {
|
|
564
|
+
if (!(status in staleDaysByStatus)) staleDaysByStatus[status] = days;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Per-root additional statuses (merged with global validStatuses)
|
|
570
|
+
const rootStatusesRaw = config.statuses.rootStatuses ?? {};
|
|
571
|
+
const rootLabels = new Set(rootPaths.map(r => path.relative(configDir, path.resolve(configDir, r)).split(path.sep).join('/')));
|
|
572
|
+
const rootValidStatuses = new Map();
|
|
573
|
+
for (const [rootKey, extraStatuses] of Object.entries(rootStatusesRaw)) {
|
|
574
|
+
const merged = new Set(validStatuses);
|
|
575
|
+
for (const s of extraStatuses) merged.add(s);
|
|
576
|
+
rootValidStatuses.set(rootKey, merged);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const validSurfaces = config.taxonomy.surfaces
|
|
580
|
+
? new Set(config.taxonomy.surfaces)
|
|
581
|
+
: null;
|
|
582
|
+
const validModules = config.taxonomy.modules
|
|
583
|
+
? new Set(config.taxonomy.modules)
|
|
584
|
+
: null;
|
|
585
|
+
const moduleRequiredStatuses = new Set(config.taxonomy.moduleRequiredFor);
|
|
586
|
+
|
|
587
|
+
const indexPath = config.index?.path
|
|
588
|
+
? path.resolve(repoRoot, config.index.path)
|
|
589
|
+
: null;
|
|
590
|
+
|
|
591
|
+
// Compute docs root relative path for index link stripping
|
|
592
|
+
const docsRootRelative = path.relative(repoRoot, docsRoot).split(path.sep).join('/');
|
|
593
|
+
const docsRootPrefix = docsRootRelative ? docsRootRelative + '/' : '';
|
|
594
|
+
|
|
595
|
+
// Lifecycle config
|
|
596
|
+
const lifecycle = config.lifecycle;
|
|
597
|
+
const archiveStatuses = new Set(lifecycle.archiveStatuses);
|
|
598
|
+
const startableStatuses = new Set(lifecycle.startableStatuses ?? ['active', 'planned']);
|
|
599
|
+
const skipStaleFor = new Set(lifecycle.skipStaleFor);
|
|
600
|
+
const skipWarningsFor = new Set(lifecycle.skipWarningsFor);
|
|
601
|
+
const skipWarningsForByType = new Map(
|
|
602
|
+
Object.entries(lifecycle.skipWarningsForByType ?? {}).map(([type, names]) => [type, new Set(names)]),
|
|
603
|
+
);
|
|
604
|
+
// Does `status` suppress warning-only checks for a doc of this type? A type that
|
|
605
|
+
// declared rich statuses answers for itself — including answering "no" for a
|
|
606
|
+
// name some other type marked quiet. Everything else (no rich statuses, an
|
|
607
|
+
// explicit global `skipWarningsFor`, a doc with no type) reads the flat set.
|
|
608
|
+
const skipsWarnings = (status, type) => {
|
|
609
|
+
const own = type != null ? skipWarningsForByType.get(type) : undefined;
|
|
610
|
+
return own ? own.has(status) : skipWarningsFor.has(status);
|
|
611
|
+
};
|
|
612
|
+
const terminalStatuses = new Set(lifecycle.terminalStatuses);
|
|
613
|
+
const terminalStatusesByType = new Map(
|
|
614
|
+
Object.entries(lifecycle.terminalStatusesByType ?? {}).map(([type, names]) => [type, new Set(names)]),
|
|
615
|
+
);
|
|
616
|
+
const isTerminal = (status, type) => {
|
|
617
|
+
const own = type != null ? terminalStatusesByType.get(type) : undefined;
|
|
618
|
+
return own ? own.has(status) : terminalStatuses.has(status);
|
|
619
|
+
};
|
|
620
|
+
// F15: filedStatuses keyed by status name, value = directory name. Empty
|
|
621
|
+
// object when no status opts in via `filed: true` (or `filed: '<dirname>'`).
|
|
622
|
+
const filedStatuses = new Map(Object.entries(lifecycle.filedStatuses ?? {}));
|
|
623
|
+
// Types that archive into their own <typeDir>/<archiveDir> rather than the
|
|
624
|
+
// shared <root>/<archiveDir> (default: prompt).
|
|
625
|
+
const archiveNestedTypes = new Set(lifecycle.archiveNestedTypes ?? []);
|
|
626
|
+
|
|
627
|
+
// Warn if rootStatuses keys don't match any configured root
|
|
628
|
+
for (const rootKey of Object.keys(rootStatusesRaw)) {
|
|
629
|
+
if (!rootLabels.has(rootKey)) {
|
|
630
|
+
earlyWarnings.push(`Config: statuses.rootStatuses key '${rootKey}' does not match any configured root.`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// A floor under the scan surface. Unset (the default) is off, so no existing
|
|
635
|
+
// repo changes behavior. See `applyScanFloor` in src/validate.mjs for why a
|
|
636
|
+
// silent zero-doc pass is the failure mode worth spending a config key on.
|
|
637
|
+
let minDocs = null;
|
|
638
|
+
if (config.minDocs !== undefined && config.minDocs !== null) {
|
|
639
|
+
if (!Number.isInteger(config.minDocs) || config.minDocs < 1) {
|
|
640
|
+
earlyWarnings.push(`Config: minDocs must be a positive integer; ignoring \`${config.minDocs}\`.`);
|
|
641
|
+
} else {
|
|
642
|
+
minDocs = config.minDocs;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const configWarnings = [...earlyWarnings, ...validateConfig(userConfig, config, validStatuses, indexPath)];
|
|
647
|
+
|
|
648
|
+
return {
|
|
649
|
+
raw: config,
|
|
650
|
+
|
|
651
|
+
docsRoot,
|
|
652
|
+
docsRoots,
|
|
653
|
+
repoRoot,
|
|
654
|
+
configDir,
|
|
655
|
+
configPath: configPath ?? null,
|
|
656
|
+
configFound: Boolean(configPath),
|
|
657
|
+
archiveDir: config.archiveDir,
|
|
658
|
+
excludeDirs: new Set(config.excludeDirs),
|
|
659
|
+
externalBodyLinkRoots: Array.isArray(config.externalBodyLinkRoots) ? config.externalBodyLinkRoots : [],
|
|
660
|
+
docsRootPrefix,
|
|
661
|
+
minDocs,
|
|
662
|
+
|
|
663
|
+
codeRefs: {
|
|
664
|
+
roots: Array.isArray(config.codeRoots) ? config.codeRoots : [],
|
|
665
|
+
extensions: Array.isArray(config.codeExtensions) ? config.codeExtensions : null,
|
|
666
|
+
excludes: Array.isArray(config.codeExcludes) ? config.codeExcludes : null,
|
|
667
|
+
untouched: Array.isArray(config.codeRefsUntouched) ? config.codeRefsUntouched : [],
|
|
668
|
+
},
|
|
669
|
+
|
|
670
|
+
statusOrder,
|
|
671
|
+
validStatuses,
|
|
672
|
+
validTypes,
|
|
673
|
+
typeStatuses,
|
|
674
|
+
typeContextConfig,
|
|
675
|
+
rootValidStatuses,
|
|
676
|
+
staleDaysByStatus,
|
|
677
|
+
|
|
678
|
+
lifecycle: { archiveStatuses, startableStatuses, skipStaleFor, skipWarningsFor, skipWarningsForByType, skipsWarnings, terminalStatuses, terminalStatusesByType, isTerminal, filedStatuses, archiveNestedTypes },
|
|
679
|
+
|
|
680
|
+
validSurfaces,
|
|
681
|
+
validModules,
|
|
682
|
+
moduleRequiredStatuses,
|
|
683
|
+
|
|
684
|
+
indexPath,
|
|
685
|
+
indexStartMarker: config.index?.startMarker ?? '<!-- GENERATED:runlist:start -->',
|
|
686
|
+
indexEndMarker: config.index?.endMarker ?? '<!-- GENERATED:runlist:end -->',
|
|
687
|
+
indexSnapshot: config.index?.snapshot ?? 'status',
|
|
688
|
+
archivedHighlightLimit: config.index?.archivedLimit ?? 8,
|
|
689
|
+
|
|
690
|
+
context: config.context,
|
|
691
|
+
display: config.display,
|
|
692
|
+
referenceFields: config.referenceFields,
|
|
693
|
+
filing: config.filing,
|
|
694
|
+
yardstick: config.yardstick,
|
|
695
|
+
presets: config.presets,
|
|
696
|
+
configuredPresetNames: new Set(Object.keys(userConfig.presets ?? {})),
|
|
697
|
+
journal: config.journal === true,
|
|
698
|
+
guard: {
|
|
699
|
+
deny: config.guard?.deny !== false,
|
|
700
|
+
promptReads: config.guard?.promptReads === 'deny' ? 'deny' : 'warn',
|
|
701
|
+
},
|
|
702
|
+
hooks,
|
|
703
|
+
configWarnings,
|
|
704
|
+
};
|
|
705
|
+
}
|