Context
Part of the review-stack architecture audit (parent epic — config-sprawl dimension). Three related
config-surface-reduction opportunities, bundled together since they share the same motivation:
RepositorySettings has grown to 102 top-level fields (src/types.ts:678-1099), pushing the
DB-backed surface to roughly 140 discrete settable values, plus another ~60-80 yml-only knobs in the
separate FocusManifest type — 200+ discrete knobs total. Reducing genuinely redundant surface area
directly benefits both current self-hosters (less to learn) and a future hosted multi-tenant version
(less for a per-tenant settings UI to render).
1. Three independent knobs for one logical setting ("require a linked issue")
settings.requireLinkedIssue: boolean (DB + dashboard + yml settings.requireLinkedIssue)
gate.linkedIssue → linkedIssueGateMode: off|advisory|block (DB + dashboard + yml)
- top-level yml
linkedIssuePolicy: required|preferred|optional (focus-manifest, config-as-code only)
src/signals/focus-manifest.ts:585-589 auto-promotes requireLinkedIssue: true +
linkedIssueGateMode: "off" → "block", but there is no cross-promotion with linkedIssuePolicy,
and the two feed genuinely different finding codes (missing_linked_issue vs
manifest_linked_issue_required, confirmed at focus-manifest.ts:683-701 and
src/signals/engine.ts:2561/2772). A self-hoster who sets linkedIssuePolicy: required (in the section
titled "FOCUS / GUARDRAILS") gets an advisory nudge but not a gate blocker — they'd have to
separately discover gate.linkedIssue: block exists too, in a completely different section of the same
file. Even src/signals/repo-policy-readiness.ts:109,120 has to cross-reference both.
Fix: fold linkedIssuePolicy into linkedIssueGateMode's promotion logic, or rename/relocate one of
them so their relationship is visible in the config file rather than only in code comments.
2. Redundant boolean+enum pair kept alive only for back-compat
gateCheckMode: "off"|"enabled" (src/types.ts:685) is a legacy shadow of
reviewCheckMode: "required"|"visible"|"disabled" (src/types.ts:697), the actual runtime authority
(its own doc comment: "gateCheckMode above stays wired for API/back-compat display but no longer drives
the publish decision on its own"). Both are separate DB columns (src/db/schema.ts:53), both appear in
4 places in src/openapi/schemas.ts, and src/api/routes.ts:2382-2383 needs explicit dual-write sync
code to keep them from diverging on a legacy-only write.
Fix: deprecate gateCheckMode from new writes/docs; keep only as a computed read-back property.
3. 13 near-identical "disable-this-label" fields
gittensorLabel, blacklistLabel, contributorCapLabel, reviewNagLabel, newAccountLabel,
moderationWarningLabel, moderationBannedLabel, manualReviewLabel, readyToMergeLabel,
changesRequestedLabel, migrationCollisionLabel, pendingClosureLabel, reviewEvasionLabel — 13
separate top-level fields, each independently string | null | undefined with the same "explicit null
disables the label, undefined uses a hardcoded default" idiom repeated verbatim 10+ times
(src/types.ts:920-1091). Real, working design (not a bug), but 13x the type-surface, 13x the DB
columns, 13x the OpenAPI entries, and 13x the yml documentation a single labels: Record<LabelKey, string | null> map would need.
Fix: collapse into one labels map, with a migration path for existing per-field DB values.
Acceptance criteria
Context
Part of the review-stack architecture audit (parent epic — config-sprawl dimension). Three related
config-surface-reduction opportunities, bundled together since they share the same motivation:
RepositorySettingshas grown to 102 top-level fields (src/types.ts:678-1099), pushing theDB-backed surface to roughly 140 discrete settable values, plus another ~60-80 yml-only knobs in the
separate
FocusManifesttype — 200+ discrete knobs total. Reducing genuinely redundant surface areadirectly benefits both current self-hosters (less to learn) and a future hosted multi-tenant version
(less for a per-tenant settings UI to render).
1. Three independent knobs for one logical setting ("require a linked issue")
settings.requireLinkedIssue: boolean(DB + dashboard + ymlsettings.requireLinkedIssue)gate.linkedIssue→linkedIssueGateMode: off|advisory|block(DB + dashboard + yml)linkedIssuePolicy: required|preferred|optional(focus-manifest, config-as-code only)src/signals/focus-manifest.ts:585-589auto-promotesrequireLinkedIssue: true+linkedIssueGateMode: "off"→"block", but there is no cross-promotion withlinkedIssuePolicy,and the two feed genuinely different finding codes (
missing_linked_issuevsmanifest_linked_issue_required, confirmed atfocus-manifest.ts:683-701andsrc/signals/engine.ts:2561/2772). A self-hoster who setslinkedIssuePolicy: required(in the sectiontitled "FOCUS / GUARDRAILS") gets an advisory nudge but not a gate blocker — they'd have to
separately discover
gate.linkedIssue: blockexists too, in a completely different section of the samefile. Even
src/signals/repo-policy-readiness.ts:109,120has to cross-reference both.Fix: fold
linkedIssuePolicyintolinkedIssueGateMode's promotion logic, or rename/relocate one ofthem so their relationship is visible in the config file rather than only in code comments.
2. Redundant boolean+enum pair kept alive only for back-compat
gateCheckMode: "off"|"enabled"(src/types.ts:685) is a legacy shadow ofreviewCheckMode: "required"|"visible"|"disabled"(src/types.ts:697), the actual runtime authority(its own doc comment: "
gateCheckModeabove stays wired for API/back-compat display but no longer drivesthe publish decision on its own"). Both are separate DB columns (
src/db/schema.ts:53), both appear in4 places in
src/openapi/schemas.ts, andsrc/api/routes.ts:2382-2383needs explicit dual-write synccode to keep them from diverging on a legacy-only write.
Fix: deprecate
gateCheckModefrom new writes/docs; keep only as a computed read-back property.3. 13 near-identical "disable-this-label" fields
gittensorLabel,blacklistLabel,contributorCapLabel,reviewNagLabel,newAccountLabel,moderationWarningLabel,moderationBannedLabel,manualReviewLabel,readyToMergeLabel,changesRequestedLabel,migrationCollisionLabel,pendingClosureLabel,reviewEvasionLabel— 13separate top-level fields, each independently
string | null | undefinedwith the same "explicitnulldisables the label,
undefineduses a hardcoded default" idiom repeated verbatim 10+ times(
src/types.ts:920-1091). Real, working design (not a bug), but 13x the type-surface, 13x the DBcolumns, 13x the OpenAPI entries, and 13x the yml documentation a single
labels: Record<LabelKey, string | null>map would need.Fix: collapse into one
labelsmap, with a migration path for existing per-field DB values.Acceptance criteria
linkedIssuePolicyandlinkedIssueGateModeproduce consistent gate behavior, documentedin one place.
gateCheckModeis deprecated (read-only, derived fromreviewCheckMode); dual-write synccode removed.
data loss.
coverage on each.