Every form on both surfaces is now a declared FormSpec in internal/forms, rendered through the shared form part and parsed by its handler through the same declaration. Create and edit share one field list, refusals answer 422 with values carried back, and layout, buttons and errors come from one part. Also adds the form registry with its invariant and route-mapping tests, the raw-form, filler-copy and Go em-dash lint rules, declared action triggers, and the capture-coverage cross-check. The forms-library change is archived under openspec/changes/archive/2026-09-05-forms-library.
3.4 KiB
discourse-operator-surface Specification
Purpose
TBD - created by archiving change discourse-integration. Update Purpose after archive.
Requirements
Requirement: Entitlement-to-group mapping management
The integration SHALL provide operator pages, registered through the contract's route hook under the Integrations section, for viewing, adding, and removing entries of the entitlement→group mapping stored in the discourse schema. The mapping form SHALL be a declared form rendered through the form part (form-library), like every other form on either surface, and SHALL NOT be built by hand in the integration's templates. Its entitlement select SHALL open with a disabled, selected first option reading "Choose an entitlement", so the browser cannot preselect a resource key the operator never chose; a submission whose entitlement value is not one of the offered options SHALL be refused with an error on that field, never resolved to a default (finding FA-8, form-conventions, "A select that needs a choice opens with a placeholder option"). On save the integration SHALL validate against the Discourse API that the named group exists and is not an automatic group, rejecting the mapping otherwise. A rejected mapping SHALL answer 422 with the form re-rendered, the submitted values carried back, and the reason under the field it belongs to or in the form-level slot. The integration SHALL NOT create or delete Discourse groups.
Scenario: Mapping a valid group
- WHEN an operator maps
discourse_postingto an existing non-automatic Discourse group - THEN the mapping is saved and the group becomes managed by the reconciler
Scenario: The entitlement select cannot preselect
- WHEN an operator opens the mapping form
- THEN its first option reads "Choose an entitlement", disabled and selected, and no resource key is preselected
- WHEN the operator submits without choosing one
- THEN the submission is refused with an error on the entitlement field and no mapping is stored
Scenario: A value outside the options is refused
- WHEN a submission carries an entitlement value the select did not offer
- THEN the response is 422 with an error on that field, and no mapping is stored
Scenario: Mapping a nonexistent group
- WHEN an operator submits a mapping naming a group the Discourse API does not report
- THEN the save is rejected with an error identifying the unknown group and no mapping is stored
Scenario: Unmapping a group
- WHEN an operator removes a group mapping
- THEN the reconciler stops managing that group and existing forum-side membership is left as-is
Requirement: Link and sync visibility
The operator surface SHALL display: entitled-but-unlinked persons, links in conflict status, and the outcome and timestamp of the most recent sweep. These states SHALL be distinguishable from one another and from configuration absence.
Scenario: Operator reviews sync health
- WHEN an operator opens the Discourse integration page after a sweep
- THEN they see the last sweep time and outcome, a count and list of entitled-but-unlinked persons, and any conflicted links
Scenario: Unconfigured integration
- WHEN an operator opens the Discourse integration page while bootstrap config is absent
- THEN the page states the integration is unconfigured and shows no error-state sync data