Files
cgalo5758 ad7a219adf Enforce schema and boot invariants
Enforce 10j's verified gaps (schema-hardening change):

- Migration 00010: partial unique indexes for one default pool and one
  primary assignment per workspace, plus CHECKs pinning
  pool/provider/subscription vocabularies and provider lifecycle
  timestamps.
- Workspace creation shares a transactional provisioning function;
  extension validates its target pool; last-tier deletion of a defaulted
  ladder is guarded; signup completes plan-less on a broken ladder.
- Boot asserts integration slug parity and validates declared config
  enums; Stripe invoice amounts are range-checked; domain cancellation
  runs a final evidence probe; rule authoring is additive-only.
2026-08-22 18:02:46 -05:00

7.3 KiB

integration-registration Specification

Purpose

TBD - created by archiving change integration-extraction. Update Purpose after archive.

Requirements

Requirement: Single registration point

An installed integration SHALL consist of one self-contained tree under internal/integrations/<slug>/ and exactly one entry in the integration registry. The composition root SHALL derive the integration's migration source, provider manifest, routes, workflow registration, startup hooks, and configuration keys from the registry. Adding or removing an integration SHALL NOT require edits to internal/server, internal/workflows, internal/config, or cmd beyond the registry entry.

Scenario: Adding an integration touches only its tree and the registry

  • WHEN a new integration is added to the codebase
  • THEN the diff outside internal/integrations/<slug>/ SHALL be limited to the registry entry (import + slice element)

Scenario: Removing an integration

  • WHEN an integration's registry entry is removed
  • THEN the application SHALL build and boot without its routes, workflows, config keys, or migration stream

Requirement: Import direction is integration to core only

Integration trees MAY import core packages; core packages SHALL NOT import integration trees. Only composition roots (cmd, internal/migrate) SHALL import the registry. internal/server and internal/workflows SHALL contain zero imports of integration packages.

Scenario: Core package attempts integration import

  • WHEN a change adds an internal/integrations/... import to a package outside the composition roots
  • THEN review (and the verification grep gate) SHALL reject it

Requirement: Capability hooks are optional

The mandatory integration surface SHALL be: slug, provider manifest, and migration source. Routes, workflow registration, startup hooks, configuration declarations, and UI assets SHALL be optional capability hooks; an integration that does not implement a hook SHALL contribute nothing to that subsystem and SHALL NOT be required to stub it.

Scenario: Integration without UI

  • WHEN a registered integration implements no UI-assets hook
  • THEN the server SHALL compose templates and static assets without it and boot normally

Requirement: Integration configuration is declared, not hand-wired

An integration SHALL declare its configuration keys — name, default, secret flag (implying a *-file variant resolved at boot), required-together groups, and optionally a closed value enumeration — through its registration. Flag/env/default binding and boot-time validation SHALL be driven by these declarations. The same declarations SHALL drive the runtime settings surface: they are the sole source for which keys are operator-manageable (non-secret keys), how values are validated (enumeration membership) and typed (declared default type), and how keys are presented (usage text). Integration defaults SHALL NOT be duplicated in core configuration files; secrets SHALL remain env/file-based.

Scenario: Declared required-together group is enforced

  • WHEN an integration declares two keys required together and only one is set at boot
  • THEN validation SHALL fail before any service initializes, naming both keys

Scenario: Declarations are the single source for the settings surface

  • WHEN an integration adds a non-secret key to its declared configuration spec
  • THEN the key SHALL appear on that integration's runtime settings surface with no change outside the integration's own declaration

Requirement: Dispatch transport is chosen per provider

The extension contract SHALL describe provider capabilities (lifecycle verbs), not a mandated transport. A provider MAY dispatch mutating verbs via direct Temporal workflows or via the transactional outbox; the outbox drainer for a provider SHALL be owned by that provider's tree. Core code SHALL write core.outbox rows only through the shared enqueue helper, never with raw SQL.

Scenario: Two providers use different transports

  • WHEN one provider dispatches via Temporal and another via the outbox
  • THEN both SHALL be conformant without contract exceptions

Scenario: Raw outbox write in core

  • WHEN core code needs to enqueue an outbox row
  • THEN it SHALL call the shared enqueue helper

Requirement: UI assets are slug-namespaced

An integration's template files and defined template names SHALL be prefixed with its slug; its static assets SHALL be served from a per-integration mount under the existing static route, same-origin. Name collisions across integrations SHALL be structurally impossible under the prefix rule.

Scenario: Template name collision

  • WHEN two integrations each register templates following the slug prefix rule
  • THEN their template names SHALL NOT collide

Requirement: Member dashboard contribution is an optional capability hook

An integration MAY contribute member dashboard cards by implementing a dashboard-card capability interface, declared at the point of use in the server package (matching the existing route-provider pattern and its import-direction rationale). A card declaration SHALL consist of a title, an HTMX partial path, an optional refresh event name, and zero or more page-level script paths under the integration's slug-namespaced static mount. The composition root SHALL collect declarations by type-asserting registered integrations; an integration that declares no cards SHALL require no dashboard-related code. Card body markup SHALL be delivered by the integration's own routes; core SHALL NOT render integration-owned templates inline into the dashboard page.

Scenario: Integration without member dashboard cards

  • WHEN an integration does not implement the dashboard-card interface
  • THEN it SHALL register and operate normally with no dashboard presence

Scenario: Declarations are collected at the composition root

  • WHEN the application composes registered integrations at startup
  • THEN dashboard-card declarations SHALL be collected from every integration implementing the hook and passed to the server
  • AND adding a card-declaring integration SHALL touch only its own tree and the registry

Scenario: Core never renders integration templates inline

  • WHEN the dashboard page renders declared cards
  • THEN core SHALL emit only the generic card shell; all card body content SHALL be fetched from the integration's declared partial route

Requirement: Slug declarations agree at boot

For every registered integration, boot SHALL assert that the integration's Slug() method and the slug in its provider manifest are equal, and SHALL fail startup with an error naming the integration and both strings when they differ. This turns the documented must-match rule into an enforced one; a mismatch previously registered the provider under one slug while mounting its UI and settings under the other.

Scenario: Mismatched slugs stop the boot

  • WHEN an integration's Slug() returns a different string than its manifest's slug
  • THEN the application fails to start, and the error names the integration and both slug values

Scenario: Agreeing slugs boot normally

  • WHEN every integration's two slug declarations agree
  • THEN registration proceeds exactly as before