Files
cgalo5758 9ff516ba95 Introduce the forms library and migrate all forms
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.
2026-09-05 03:16:45 -05:00

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_posting to 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

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