Context
config.template.mjs is the declarative config SSOT — each value is leaf(default, envVarName, type). Several leaves embed imperative process.env.UNIT_TEST_MODE === 'true' ? test : prod branching in the default expression — env-resolution leaking into the canonical config (the same root as the resolveAiDataRoot over-engineering). Reviews can't reliably catch this class.
This is the lint-guard half of #12451 (which also covers the declarative reshape of the existing instances). Split per the 1-PR-per-ticket close-target contract (#12367): this leaf is fully delivered by PR #12472; #12451 stays open for the reshape.
The Fix (delivered in PR #12472)
ai/scripts/lint/lint-config-template-ssot.mjs — bans inline process.env reads in leaf() defaults across every config.template.mjs under ai/. Lands enforcing via a frozen BASELINE of the 4 known instances (NEW occurrences fail; the historical debt burns down; a stale baseline row also fails — burndown hygiene). Wired as npm run ai:lint-config-template-ssot + a CI gate. Matches the repo's existing GRANDFATHERED_* baseline idiom.
Contract Ledger
| Target Surface |
Source of Authority |
Enforced Behavior |
Fallback |
Docs |
Evidence |
ai:lint-config-template-ssot / lint-config-template-ssot.mjs |
the lint script |
scans every config.template.mjs under ai/; flags inline process.env in a leaf() default |
env access must use the leaf env-var-name arg |
script JSDoc |
node ai/scripts/lint/lint-config-template-ssot.mjs → OK - 4 baselined (exit 0) |
| Baseline (4 known inline-env leaves) |
BASELINE frozen array |
the 4 historical instances are suppressed so the build stays green; each row carries its reshape shape |
— |
BASELINE JSDoc |
spec: a baselined violation is suppressed |
| Fresh (unbaselined) violation |
lintConfigTemplateSsot().newViolations |
a NEW inline-env leaf default → exit 1 (un-mergeable) |
leaf(default, 'ENV', type) + relocate the test branch to the test layer |
FIX_HINT |
spec: a fresh violation fails → exit 1 |
| Stale baseline row |
lintConfigTemplateSsot().staleBaseline |
a baseline row with no live violation (reshape landed) → exit 1 |
drop the row |
report message |
spec: a stale baseline row fails → exit 1 |
| CI gate |
.github/workflows/config-template-ssot-lint.yml |
runs the lint on PRs/pushes touching config.template.mjs / the lint / its workflow |
— |
workflow file |
workflow + lint-pr-body green on PR #12472 |
Acceptance Criteria
Scope
The lint guard only. The declarative reshape of the 4 existing instances (zeroing the baseline; the dynamic per-worker collection-name case is the harder sub-case) remains on #12451.
Refs #12451 (broader: reshape + lint). Part of Epic #12456 (AiConfig reactive Provider SSOT cleanup).
Origin Session ID: 3ecb40bf-bfef-40b1-8693-a8aae5afa1b7
Authored by Claude Opus 4.8 (Claude Code), /lead-role.
Context
config.template.mjsis the declarative config SSOT — each value isleaf(default, envVarName, type). Several leaves embed imperativeprocess.env.UNIT_TEST_MODE === 'true' ? test : prodbranching in the default expression — env-resolution leaking into the canonical config (the same root as theresolveAiDataRootover-engineering). Reviews can't reliably catch this class.This is the lint-guard half of #12451 (which also covers the declarative reshape of the existing instances). Split per the 1-PR-per-ticket close-target contract (#12367): this leaf is fully delivered by PR #12472; #12451 stays open for the reshape.
The Fix (delivered in PR #12472)
ai/scripts/lint/lint-config-template-ssot.mjs— bans inlineprocess.envreads inleaf()defaults across everyconfig.template.mjsunderai/. Lands enforcing via a frozenBASELINEof the 4 known instances (NEW occurrences fail; the historical debt burns down; a stale baseline row also fails — burndown hygiene). Wired asnpm run ai:lint-config-template-ssot+ a CI gate. Matches the repo's existingGRANDFATHERED_*baseline idiom.Contract Ledger
ai:lint-config-template-ssot/lint-config-template-ssot.mjsconfig.template.mjsunderai/; flags inlineprocess.envin aleaf()defaultnode ai/scripts/lint/lint-config-template-ssot.mjs→OK - 4 baselined(exit 0)BASELINEfrozen arrayBASELINEJSDoclintConfigTemplateSsot().newViolationsleaf(default, 'ENV', type)+ relocate the test branch to the test layerFIX_HINTlintConfigTemplateSsot().staleBaseline.github/workflows/config-template-ssot-lint.ymlconfig.template.mjs/ the lint / its workflowlint-pr-bodygreen on PR #12472Acceptance Criteria
process.envin aconfig.template.mjsleaf default; the 4 known instances are baselined so the build stays green. (PR #12472)config.template.mjs/ the lint / its workflow. (PR #12472)Scope
The lint guard only. The declarative reshape of the 4 existing instances (zeroing the baseline; the dynamic per-worker collection-name case is the harder sub-case) remains on #12451.
Refs #12451 (broader: reshape + lint). Part of Epic #12456 (AiConfig reactive Provider SSOT cleanup).
Origin Session ID: 3ecb40bf-bfef-40b1-8693-a8aae5afa1b7 Authored by Claude Opus 4.8 (Claude Code), /lead-role.