Part of #4092.
Motivation
Every sub-issue under #4092 needs the same 3-state gating logic: config true ⇒ feature ON regardless of the env-var+allowlist gate; config false ⇒ feature OFF regardless of it; config unset/null ⇒ defer to today's envFlag && isConvergenceRepoAllowed(env, repo) gate (byte-identical default). Building this ONCE as a shared, reusable helper avoids 10+ near-identical bespoke implementations and keeps the semantics consistent across every feature.
review.visual.enabled (#4083) already implements this exact 3-state shape, but inline in src/queue/processors.ts rather than as a reusable helper — it was scoped narrowly on purpose. This issue generalizes it.
Requirements
- A small, pure, unit-testable helper (e.g. in
src/review/cutover-gate.ts alongside isConvergenceRepoAllowed, or its own module) with a signature roughly like resolveConvergedFeature(configValue: boolean | null, envFlag: boolean, repoAllowed: boolean): boolean implementing the 3-state precedence above.
- Decide the NAMESPACING question explicitly (this is the one design decision every other sub-issue depends on): extend
CONVERGED_FEATURE_KEYS/FocusManifestFeaturesConfig (packages/gittensory-engine/src/focus-manifest.ts:176-182) — which already models rag/reputation/unifiedComment/safety as a boolean-or-null record — to be the ONE shared home for every simple on/off feature toggle (grounding, impactMap, cultureProfile, reviewMemory, selftune, parityAudit, inlineComments, fixHandoff, etc.), OR give each feature its own review.<feature>.enabled key mirroring visual's shape. Recommendation: extend features: for simple booleans (reuses an existing, already-parsed schema surface instead of adding N new ones); keep review.visual.enabled and contentLane in their own richer namespaces since they carry more than a single boolean.
- Write the resolver's own unit tests covering all 9 combinations of {config: true/false/null} × {envFlag: true/false} × {repoAllowed: true/false}.
- Do NOT wire it into any specific feature in this PR — that's each subsequent sub-issue's job. This issue ships the shared primitive only.
Deliverables
Expected outcome
A single, tested primitive every other sub-issue under #4092 imports instead of reimplementing.
Effort
S — a small pure function plus a config-schema extension decision. No feature migration yet.
Part of #4092.
Motivation
Every sub-issue under #4092 needs the same 3-state gating logic: config
true⇒ feature ON regardless of the env-var+allowlist gate; configfalse⇒ feature OFF regardless of it; config unset/null⇒ defer to today'senvFlag && isConvergenceRepoAllowed(env, repo)gate (byte-identical default). Building this ONCE as a shared, reusable helper avoids 10+ near-identical bespoke implementations and keeps the semantics consistent across every feature.review.visual.enabled(#4083) already implements this exact 3-state shape, but inline insrc/queue/processors.tsrather than as a reusable helper — it was scoped narrowly on purpose. This issue generalizes it.Requirements
src/review/cutover-gate.tsalongsideisConvergenceRepoAllowed, or its own module) with a signature roughly likeresolveConvergedFeature(configValue: boolean | null, envFlag: boolean, repoAllowed: boolean): booleanimplementing the 3-state precedence above.CONVERGED_FEATURE_KEYS/FocusManifestFeaturesConfig(packages/gittensory-engine/src/focus-manifest.ts:176-182) — which already modelsrag/reputation/unifiedComment/safetyas a boolean-or-null record — to be the ONE shared home for every simple on/off feature toggle (grounding, impactMap, cultureProfile, reviewMemory, selftune, parityAudit, inlineComments, fixHandoff, etc.), OR give each feature its ownreview.<feature>.enabledkey mirroring visual's shape. Recommendation: extendfeatures:for simple booleans (reuses an existing, already-parsed schema surface instead of adding N new ones); keepreview.visual.enabledandcontentLanein their own richer namespaces since they carry more than a single boolean.Deliverables
features:: theCONVERGED_FEATURE_KEYSarray andFocusManifestFeaturesConfigtype, parse/overlay/present/serialize updated to accept the new keys (mirroringparseVisualConfig's pattern)Expected outcome
A single, tested primitive every other sub-issue under #4092 imports instead of reimplementing.
Effort
S — a small pure function plus a config-schema extension decision. No feature migration yet.