Files
cgalo5758 e581bf81c3 Add typed config keys and config CLI
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.
2026-09-05 17:47:04 -05:00

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 ConfigSpec declares a key with a bool default and a key with a time.Duration default
  • THEN member-console start --help SHALL 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, and fedwiki-swap-cooldown SHALL be declared only by fedwiki's ConfigSpec, not hand-declared in core

Scenario: Discourse's sync knobs are declared, not implicit

  • WHEN discourse's ConfigSpec is inspected
  • THEN discourse-sync-interval and discourse-sync-trigger-immediately SHALL 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-url is 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-target is 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-interval receives the value hello, 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-url is declared
  • THEN it carries the url type explicitly, and the value hello is refused at save and at boot while https://admin.wiki.example.com is accepted

Scenario: A negative cooldown is refused

  • WHEN fedwiki-swap-cooldown receives -100000 or -1h
  • THEN the first is refused as not a duration and the second as a negative duration, and 0 is accepted

Scenario: A type that contradicts its default is a declaration error

  • WHEN an integration declares a key with a bool default and the duration type
  • THEN the declaration check refuses it naming the key and both types