0045 Config Scope Docs Taxonomy Intake
ADR-0045: Local/global config boundary, docs taxonomy, and a single intake zone
- Date: 2026-06-23
- Status: accepted
- Deciders: Construct maintainers (operator + cx-architect)
- Relates to: ADR-0002 (layered architecture), ADR-0027 (host footprint), ADR-0029 (install scopes & hook budgets), ADR-0044 (tool-repo root layout)
Problem
Three adjacent sources of confusion, each validated against community-adopted patterns rather than assumed:
-
"content vs docs." There is no
content/directory. The docs site (apps/docs/) already build-time-reads the single canonical source — repo-rootdocs/— viaapps/docs/lib/docs-source.ts(DOCS_ROOT = ../../docs); no second copy exists. The real confusion lives insidedocs/: ~25 type-named subdirs including look-alike pairs with opposite meanings —adr/decisions,prd/prds,rfc/rfcs— where the plurals are init-lane templates (construct initclones them downstream) and the singulars are this repo's own records, plus an emptychangelog/shadowing the canonicalchangelog.md. A second axis: bothtemplates/docs/and roottemplates/docs/exist. -
Intake has three doors. Intake is already fully project-scoped (no global intake), but it watches three drop zones — root
inbox/,.cx/inbox/, anddocs/intake/(lib/config/intake-policy.mjs) — with no single canonical entry point. -
The local/global boundary leaks. ADR-0029 split user vs project scope, but: project
.envand user~/.construct/config.envsilently shadow each other (warn fires only for a hardcoded key subset);~/.construct/ignores the XDG base-directory spec; there is no gitignored project-local tier; and precedence/merge semantics are undocumented.
Community validation (no assumptions)
- Docs: a plain
docs/as source-of-pages is idiomatic when markdown files are the pages (Docusaurus, default VitePress). Acontent/collection is only idiomatic with a typed/validated pipeline (Astro content collections, Nextra/Fumadocs, Velite/Zod) — which Construct neither has nor needs. For a monorepo with rootdocs/+ a site app, the recommended pattern is one canonical source the app ingests, not a second copy. Construct already does this. - Config: the XDG Base Directory spec plus git (
local > global > system, honors XDG), npm (project > user > global), Prettier (deliberately no global config, for reproducibility), and Claude Code (Managed > args > Local .claude/settings.local.json > Project .claude/settings.json > User ~/.claude, with list-shaped settings merged) converge on one principle: user/global = identity, secrets, personal prefs (must not travel with the repo); project-committed = reproducible & team-shared; project-local = gitignored personal overrides; most-specific wins; lists merge. - Intake: the Maildir contract (
tmp/→new/→cur/, atomic rename for lock-free, race-free handoff) and the.next//.turbo//.cache/convention (project working state in a gitignored dot-dir; visible at root only when humans must drop files) both point to one canonical drop zone with atomic handoff.
Decision
A. Docs taxonomy — regroup; docs/ stays the single source
docs/remains the one canonical markdown source;apps/docs/keeps reading it (nocontent/collection, no second copy).- Collapse the type-named subdirs into a small, intent-revealing taxonomy:
| New top-level | Absorbs | Holds |
|---|---|---|
docs/decisions/ | adr/, rfc/, old decisions/ façade | This repo's numbered ADRs + RFCs (records) |
docs/specs/ | prd/ | This repo's numbered PRDs/specs |
docs/guides/ | concepts/, cookbook/, start/, reference/, contributing/ | How-to + reference for humans |
docs/operations/ | deploy/, maintenance/, runbooks/, incidents/, operations/, releases/, audit/ | Run/operate the system |
docs/notes/ | memos/, meetings/, notes/, research/ | Working notes |
- Segregate init-lane templates out of the records tree: move the plural template dirs (
prds/,rfcs/) and any template content under the shippedtemplates/docs/(e.g.templates/docs/lanes/), so "templates cloned downstream" never sit beside "this repo's records." Reconciletemplates/docs/vs roottemplates/docs/into the single shippedtemplates/docs/. - Delete the empty
changelog/dir (changelog.mdis canonical). - Update
apps/docs/lib/docs-source.ts(SKIP_DIRS+ nav), theconstruct initdoc-lane source paths (lib/init/doc-lanes.mjs), and internal cross-links to the new taxonomy. Extendscripts/audit/03c-root-layout.mjsto ratchet the taxonomy.
B. Config — XDG base dirs + Claude Code tier model
-
Adopt XDG for user scope: config →
$XDG_CONFIG_HOME/construct(default~/.config/construct); durable state (vector DB, logs) →$XDG_STATE_HOME/construct; regenerable caches →$XDG_CACHE_HOME/construct. Honor the env vars; fall back to spec defaults. Replaces the catch-all~/.construct/. -
Tier model mirroring Claude Code, most-specific wins:
user (global, ~/.config/construct)◁project committed (construct.config.json)◁project-local gitignored (construct.config.local.json)List-shaped settings (permission allowlists,
sources.targets, intake dirs) merge + dedupe across tiers; scalars override. -
Resolve
.envshadowing: document the precedence explicitly and warn on every shadowed key, not a hardcoded subset. -
Clean break (no back-compat): write XDG paths only; legacy
~/.construct/*is not read. A user re-runsconstruct install --scope=userto repopulate user scope at the new paths. -
Document the full model in
docs/guides/reference/config.md; update ADR-0029's user-scope paths.
C. Intake — one visible root inbox/
- The single canonical human drop zone is root
inbox/(visible, discoverable). - Remove the
projectInbox(.cx/inbox/) anddocsIntake(docs/intake/) watch zones outright — no back-compat, no deprecation reads.docs/intake/was feature documentation, not drops, so it moves todocs/guides/intake/.inbox/is the sole, unconditional drop zone;intakePolicykeeps onlymaxDepth+ opt-inadditionalDirs. - Machine/runtime state stays under gitignored
.cx/(sessions, traces, lancedb, oracle, processed intake). - Adopt Maildir-style atomic handoff: writers stage then rename into
inbox/; the watcher consumes only complete files and moves processed items to.cx/intake/processed/. - Update
lib/config/intake-policy.mjsdefaults +schemas/project-config.schema.json,lib/init-unified.mjs, the dashboard intake route, and docs.
Migration & phasing
Each phase is its own bead with a functional test in tests/functional/, construct doctor + construct sync green, docs updated, then merge.
- Phase 1 — docs taxonomy. Pure maintainer/repo hygiene; no user-facing migration (ADR-0044 precedent). Lowest risk.
- Phase 2 — single intake. Project-scoped clean break: the old zones are removed, not deprecated.
- Phase 3 — XDG + tiers. User-machine clean break to XDG paths (no legacy read); a user re-runs
construct install. Highest blast radius; last.
Consequences
release:check/03c-root-layoutratchets extend to the docs taxonomy and a config-scope check inconstruct doctor.- ADR-0029 user-scope paths move to XDG; ADR-0044's physical root map gains
inbox/and thedocs/taxonomy. - Clean break: no dual-read or deprecation windows. Existing user-scope installs re-run
construct installafter the XDG move (Phase 3); existing.cx/inbox/drops are moved toinbox/by hand (Phase 2).
Verification
node ./bin/construct doctor
node scripts/audit/03c-root-layout.mjs
node ./bin/construct sync && node ./bin/construct list
npm test && npm run test:functional