Problem
The public OpenAPI spec (GET /api/spec.json) and the public docs guide tree (GET /api/public/docs) are assembled by globbing every module's doc/*.yml + doc/guides/*.md (config/assets.js). That resolved file list is then filtered only by module runtime activation (config/index.js β lib/helpers/config.js filterByActivation, which reads config[moduleName].activated).
Two coupling defects fall out of this:
- Activation conflates runtime with documentation. A module's
activated flag gates BOTH its routes/models AND its doc contribution. There is no way to keep a module running while dropping only its sample docs.
- Core modules bypass the filter entirely.
CORE_MODULES = { core, auth, users, home } are never filtered, because they carry infra-critical endpoints β home serves /api/health (liveness) and /api/admin/readiness. So a core module's sample doc/guide content (home/doc/guides/00-welcome.md β slug welcome, 01-quickstart.md β slug quickstart) is always globbed into the tree, with no opt-out.
Impact
A consumer that ships its own onboarding guides under the natural slugs welcome / quickstart collides with the home samples β the docs tree builder rejects duplicate slugs. The only workaround is to physically delete the sample guides shipped by the framework β which conflicts with keeping framework files byte-identical across upgrades and so recurs on every sync.
(The sample OpenAPI doc tasks/doc/tasks.yml is not affected β tasks is non-core, so config.tasks.activated = false already strips it via the existing mechanism. This issue is specifically the core-module doc case that activation cannot reach.)
Proposed fix
Add an independent doc-exclusion config: config.docs.excludeModules: string[] (default []). A new helper filterByDocExclusion(files, config) drops doc/*.yml (spec) + doc/guides/*.md (guides) for any module in the list β regardless of activation / core status β applied as a second filter pass over the openapi + guides file keys, after filterByActivation. Default empty keeps the sample guides as a working tutorial.
This decouples documentation contribution from runtime activation (doc β runtime), is granular per-module, and works for core and non-core modules alike.
Scope
lib/helpers/config.js: add filterByDocExclusion(files, config) (reuse extractModuleName; no CORE_MODULES bypass; missing/empty list β no-op).
config/index.js: second filter pass over ['openapi', 'guides'] after the filterByActivation loop. Runtime file keys (routes/models/policies/configs/preRoutes) untouched.
config/defaults/development.config.js: docs.excludeModules: [] + comment (doc-only exclusion, independent of activation, works on core modules; default empty keeps samples).
- Unit tests: excludes a listed module's
doc/*.yml + doc/guides/*.md; keeps non-listed modules; works for a core module (the motivating regression); default [] / missing = no-op; runtime file keys unaffected by excludeModules.
MIGRATIONS.md entry: new config.docs.excludeModules knob + when a consumer would set it (a runtime-active module whose sample docs collide with the consumer's own).
Out of scope
tasks/doc/tasks.yml (already handled by config.tasks.activated = false).
- Any tree-builder dedup/precedence change.
Created via /dev:issue
Problem
The public OpenAPI spec (
GET /api/spec.json) and the public docs guide tree (GET /api/public/docs) are assembled by globbing every module'sdoc/*.yml+doc/guides/*.md(config/assets.js). That resolved file list is then filtered only by module runtime activation (config/index.jsβlib/helpers/config.jsfilterByActivation, which readsconfig[moduleName].activated).Two coupling defects fall out of this:
activatedflag gates BOTH its routes/models AND its doc contribution. There is no way to keep a module running while dropping only its sample docs.CORE_MODULES = { core, auth, users, home }are never filtered, because they carry infra-critical endpoints βhomeserves/api/health(liveness) and/api/admin/readiness. So a core module's sample doc/guide content (home/doc/guides/00-welcome.mdβ slugwelcome,01-quickstart.mdβ slugquickstart) is always globbed into the tree, with no opt-out.Impact
A consumer that ships its own onboarding guides under the natural slugs
welcome/quickstartcollides with thehomesamples β the docs tree builder rejects duplicate slugs. The only workaround is to physically delete the sample guides shipped by the framework β which conflicts with keeping framework files byte-identical across upgrades and so recurs on every sync.(The sample OpenAPI doc
tasks/doc/tasks.ymlis not affected βtasksis non-core, soconfig.tasks.activated = falsealready strips it via the existing mechanism. This issue is specifically the core-module doc case that activation cannot reach.)Proposed fix
Add an independent doc-exclusion config:
config.docs.excludeModules: string[](default[]). A new helperfilterByDocExclusion(files, config)dropsdoc/*.yml(spec) +doc/guides/*.md(guides) for any module in the list β regardless of activation / core status β applied as a second filter pass over theopenapi+guidesfile keys, afterfilterByActivation. Default empty keeps the sample guides as a working tutorial.This decouples documentation contribution from runtime activation (doc β runtime), is granular per-module, and works for core and non-core modules alike.
Scope
lib/helpers/config.js: addfilterByDocExclusion(files, config)(reuseextractModuleName; noCORE_MODULESbypass; missing/empty list β no-op).config/index.js: second filter pass over['openapi', 'guides']after thefilterByActivationloop. Runtime file keys (routes/models/policies/configs/preRoutes) untouched.config/defaults/development.config.js:docs.excludeModules: []+ comment (doc-only exclusion, independent of activation, works on core modules; default empty keeps samples).doc/*.yml+doc/guides/*.md; keeps non-listed modules; works for a core module (the motivating regression); default[]/ missing = no-op; runtime file keys unaffected byexcludeModules.MIGRATIONS.mdentry: newconfig.docs.excludeModulesknob + when a consumer would set it (a runtime-active module whose sample docs collide with the consumer's own).Out of scope
tasks/doc/tasks.yml(already handled byconfig.tasks.activated = false).Created via /dev:issue