Parse every config value through a single declared type at save, boot, and validate, and expose the settings seam from the terminal so operators can list, set, clear, and validate overrides without SQL.
6.1 KiB
integration-config-declaration Specification
Purpose
Keep configuration ownership aligned with code ownership: every key an integration
consumes is declared by that integration's ConfigSpec and registered generically by
the composition root, whatever its type, so core never hand-declares or reads an
integration-namespaced key. Core capabilities configured by deployment get
core-namespaced keys, and integrations that need a core-resolved value receive it as
threaded data rather than reaching for viper themselves.
Requirements
Requirement: Integration config keys travel the ConfigSpec seam regardless of type
Every configuration key an integration consumes SHALL be declared in that integration's ConfigSpec, and the composition root SHALL register a matching CLI flag and default for each declared key generically, for every value type in the closed set: string, url, duration, int, bool, list, enum. A key's type SHALL be carried by its declaration (inferred from its default or declared explicitly) and SHALL decide the flag constructor, the parser every path uses, and the control the settings surface renders; nothing in core SHALL hand-declare an integration's key or hand-check its value.
Scenario: Boolean and duration keys register through the generic seam
- WHEN an integration's
ConfigSpecdeclares a key with abooldefault and a key with atime.Durationdefault - THEN
member-console start --helpSHALL list both keys as flags of the matching type - AND their declared defaults SHALL apply when the keys are unset
Scenario: FedWiki's sync knobs are integration-declared
- WHEN the composition root's flag declarations are inspected
- THEN
fedwiki-sync-enabled,fedwiki-sync-interval,fedwiki-sync-trigger-immediately, andfedwiki-swap-cooldownSHALL be declared only by fedwiki'sConfigSpec, not hand-declared in core
Scenario: Discourse's sync knobs are declared, not implicit
- WHEN discourse's
ConfigSpecis inspected - THEN
discourse-sync-intervalanddiscourse-sync-trigger-immediatelySHALL be declared with defaults matching the integration's runtime fallbacks - AND unset keys SHALL produce the same behavior as before declaration
Scenario: A URL key's flag and value go through the seam
- WHEN
discourse-base-urlis declared with the url type - THEN it registers as a string flag, and a non-URL value is refused by the same parser at boot and at save
Requirement: Core reads no integration-namespaced configuration key
Core packages (cmd/, internal/server/, internal/workflows/) SHALL NOT read integration-namespaced configuration keys to configure core capabilities. Core capabilities configured by deployment SHALL use core-namespaced keys; where an integration needs a core-resolved value, the composition root SHALL resolve it once and thread it as data. Exception, tracked as existing debt: the composition root still reads Stripe's declared keys to construct core's payments baseline — that extraction is payments-provider-seam (issues.md), not this requirement's scope.
Scenario: The domains connect target is a core key
- WHEN a deployment sets
domains-connect-target - THEN the member Domains surface, the claim-verification worker probe, and fedwiki's create-flow gate SHALL all observe that value
- AND no code SHALL read
fedwiki-custom-domain-target(the key no longer exists)
Scenario: Empty core connect target disables external claims deployment-wide
- WHEN
domains-connect-targetis unset - THEN external-claim creation SHALL be disabled everywhere the gate applies, matching the domains-registry gate requirement
Scenario: FedWiki receives the connect target as data
- WHEN fedwiki's route registration constructs its external-claim gate
- THEN the connect target SHALL arrive via
server.Deps, not via a viper read inside the integration
Requirement: A declared key carries a value type and one parser serves every path
Every declared configuration key SHALL have a value type from a closed set: string, url, duration, int, bool, list, enum. The type SHALL be inferred from the key's default where the default's Go type says enough (string, bool, int, []string, time.Duration; a non-empty enumeration implies enum; no default implies string) and SHALL be declared explicitly where it does not (a URL, or a string key with no default that is not free text). A declared type that disagrees with the default's Go type SHALL be refused as a declaration error. One parser SHALL turn a raw value into a typed one for every path that reads a raw value: the operator's save, the boot overlay of stored overrides, and boot validation of environment, flag and config-file values. A URL SHALL be absolute with an http or https scheme and a host; a duration SHALL be a Go duration string that is not negative; an int an integer; a bool true or false; a list comma-separated and trimmed; an enum a member. The parser's error SHALL name the value and the shape with a format example, and SHALL be the text every path reports.
Scenario: A duration key refuses a word at save and at boot
- WHEN
fedwiki-sync-intervalreceives the valuehello, from the settings page or from the environment - THEN the save answers 422 with "value "hello" is not a duration; for example 30m or 1h30m" under the control, and a boot with that environment value fails with the same sentence naming the key
Scenario: A URL key is declared, not inferred
- WHEN
fedwiki-farm-api-urlis declared - THEN it carries the url type explicitly, and the value
hellois refused at save and at boot whilehttps://admin.wiki.example.comis accepted
Scenario: A negative cooldown is refused
- WHEN
fedwiki-swap-cooldownreceives-100000or-1h - THEN the first is refused as not a duration and the second as a negative duration, and
0is accepted
Scenario: A type that contradicts its default is a declaration error
- WHEN an integration declares a key with a
booldefault and the duration type - THEN the declaration check refuses it naming the key and both types