feat(WP-0009): IHF GAAF Compliance Foundation — type registries, extension manifests, architectural contracts
Some checks failed
Test / test (push) Has been cancelled

Implements IHUB-WP-0009: closes four GAAF-2026 gaps before domain hub work begins.
- TypeRegistry helper + controllers/views (hub_kind, hub_capability_manifest)
- HubCapabilityManifest entity with validation and registry linkage
- ARCHITECTURE-LAYERS.md + CI-enforced boundary contracts
- Alembic migration 1743724800, fitness tests (Test/Architecture/)
- GAAF spec, Operational Architecture spec, domain hub extension guide
- Updates to CLAUDE.md, SCOPE.md, Schema.sql, Routes, FrontController, Types

state_hub_sync: pending (tunnel was STALE at completion time; run fix-consistency)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-31 21:17:39 +00:00
parent 1a7732d7da
commit b5d73aa18b
47 changed files with 4855 additions and 104 deletions

78
contracts/README.md Normal file
View File

@@ -0,0 +1,78 @@
# IHF Contract Catalog
**Framework:** GAAF-2026
**Last reviewed:** 2026-03-31
**Repository:** inter-hub
This directory contains the versioned, machine-readable contracts for each
GAAF-2026 layer. Contract files are the authoritative declaration of
interface, invariants, compatibility rules, and validation requirements for
every public surface in the framework.
The canonical implementation of each contract is the PostgreSQL schema and
Haskell controllers. These files are the *discoverable declaration* — the
human and agent-readable companion that makes intent explicit without
requiring the source code to be read.
---
## Core Contracts
| Contract | File | Version | Status |
|---|---|---|---|
| Widget Envelope | [core/widget-envelope-v1.md](core/widget-envelope-v1.md) | 1.0 | Active |
| Append-Only Events | [core/append-only-events-v1.md](core/append-only-events-v1.md) | 1.0 | Active |
Core contracts are **immutable after activation**. New requirements produce a
new version (v1.1, v2.0); the old version remains readable for audit.
---
## Functional Contracts
| Contract | File | Version | Status |
|---|---|---|---|
| Interaction Reporting API | [functional/interaction-reporting-v1.md](functional/interaction-reporting-v1.md) | 1.0 | Active |
| Module Maturity Labels | [functional/module-maturity-labels.md](functional/module-maturity-labels.md) | 1.0 | Active |
Functional contracts are **evolvable with minor-version notice**. Breaking
changes require a major version bump and a deprecation window.
---
## Extensions Contracts
| Contract | File | Version | Status |
|---|---|---|---|
| Hub Capability Manifest | [extensions/hub-capability-manifest-v1.md](extensions/hub-capability-manifest-v1.md) | 1.0 | Active |
Extensions contracts govern how domain hubs register their vocabulary and
capabilities with the framework.
---
## Contract Lifecycle
```
Draft → Active → Superseded
(never deleted — old versions remain for audit)
```
A contract becomes Active when:
- Its corresponding schema and code are deployed
- It is referenced in `ARCHITECTURE-LAYERS.md`
A contract is Superseded when a new version replaces it. The old file
remains with a `superseded_by` note at the top.
---
## Adding a New Contract
1. Create the file in the appropriate layer directory
2. Follow the header format: name, version, date, status, layer
3. Document: interface, invariants, compatibility rules, validation rules,
failure mode
4. Add an entry to this README table
5. Reference it in `ARCHITECTURE-LAYERS.md`

View File

@@ -0,0 +1,104 @@
# Append-Only Events Contract
**Name:** append-only-events
**Version:** 1.0
**Date:** 2026-03-31
**Status:** Active
**Layer:** Core
**Immutable:** Yes — this invariant is permanent and cannot be relaxed
---
## Purpose
Interaction events and outcome signals are the primary observational record of
the IHF. Their integrity as an append-only log is a foundational invariant:
governance, traceability, and antifragility all depend on the fact that the
historical record cannot be silently rewritten.
---
## Invariant
The following tables are **append-only**:
| Table | Trigger (no update) | Trigger (no delete) |
|---|---|---|
| `interaction_events` | `interaction_events_no_update` | `interaction_events_no_delete` |
| `outcome_signals` | `outcome_signals_no_update` | `outcome_signals_no_delete` |
No row in either table may be modified or deleted after insertion. This
invariant is enforced at the PostgreSQL level by `BEFORE UPDATE` and
`BEFORE DELETE` triggers that raise exceptions. It cannot be bypassed by
application code.
---
## Enforcement
```sql
-- Implemented in Application/Schema.sql
CREATE TRIGGER interaction_events_no_update
BEFORE UPDATE ON interaction_events
FOR EACH ROW EXECUTE FUNCTION prevent_interaction_event_mutation();
CREATE TRIGGER interaction_events_no_delete
BEFORE DELETE ON interaction_events
FOR EACH ROW EXECUTE FUNCTION prevent_interaction_event_mutation();
-- Same pattern for outcome_signals
```
The trigger function raises:
```
"interaction_events is append-only: UPDATE and DELETE are not permitted"
```
---
## Correction Policy
Erroneous events **must not** be corrected by modifying the original row.
The correct approach is to insert a new event:
```json
{
"event_type": "retracted",
"metadata": {
"retracted_event_id": "<uuid of original event>",
"reason": "incorrect actor attribution"
}
}
```
The original event remains in the table. Downstream analysis should treat
`retracted` events as markers that exclude the referenced event from
calculations.
---
## Failure Mode
Any attempt to UPDATE or DELETE a row in `interaction_events` or
`outcome_signals` raises a PostgreSQL exception with SQLSTATE `P0001`.
The calling transaction is aborted. No partial mutation is possible.
---
## Scope
This contract applies **only** to `interaction_events` and `outcome_signals`.
Other append-oriented tables (`triage_states`, `widget_ownerships`,
`stewardship_roles`) use the same conceptual pattern (soft expiry instead of
update, no hard delete) but are not covered by DB-trigger enforcement. Their
append semantics are enforced by application-layer controller conventions.
---
## Implementation Reference
- Functions: `prevent_interaction_event_mutation()`,
`prevent_outcome_signal_mutation()` in `Application/Schema.sql`
- The architectural fitness function `Test/Architecture/LayerBoundarySpec.hs`
(Test 1) verifies these trigger names are present in the schema

View File

@@ -0,0 +1,88 @@
# Widget Envelope Contract
**Name:** widget-envelope
**Version:** 1.0
**Date:** 2026-03-31
**Status:** Active
**Layer:** Core
**Immutable:** Yes — changes require v1.1 with backwards-compatible additions only
---
## Purpose
The Widget Envelope is the metadata boundary attached to every rendered widget.
It enables the IHF capture pipeline to attribute any DOM event to a governed
widget without coupling to the widget's implementation.
Any UI technology that emits the required `data-*` attributes on a DOM element
is a first-class IHF participant.
---
## Required Attributes
Every widget root element **MUST** carry all of the following:
| Attribute | Format | Example |
|---|---|---|
| `data-widget-id` | UUID (RFC 4122) | `data-widget-id="550e8400-e29b-41d4-a716-446655440000"` |
| `data-hub-id` | Registered hub slug (TEXT) | `data-hub-id="ops-hub"` |
| `data-view-context` | Dot-separated path (TEXT) | `data-view-context="ops.incidents.list"` |
| `data-widget-type` | Registered name from `widget_type_registry` | `data-widget-type="chart"` |
---
## Optional Attributes
| Attribute | Format | Notes |
|---|---|---|
| `data-capability-ref` | TEXT | Links widget to a hub capability |
| `data-policy-scope` | Registered name from `policy_scope_registry` | Defaults to `internal` if absent |
| `data-widget-version` | Integer as string | Current widget version number |
| `data-experiment-variant` | TEXT | A/B experiment identifier |
| `data-requirements-thread-ref` | UUID | Links to an open AnnotationThread |
---
## Validation Rules
1. `data-widget-id` must be a valid UUID. Non-UUID values cause the envelope to
be treated as malformed.
2. `data-hub-id` must match the `slug` of a registered hub in the `hubs` table.
Unknown hub slugs are logged but do not block event capture.
3. `data-widget-type` must exist in `widget_type_registry` with `status = 'active'`
at the time of capture. Events from deprecated types are accepted but flagged.
4. `data-view-context` has no format enforcement in v1.0 beyond non-empty.
---
## Failure Mode
- **Missing required attribute**: the capture pipeline logs a
`malformed_envelope` event with the missing attribute names in `metadata`.
The original interaction event is **not** stored. This is fail-safe, not
fail-loud — the UI does not crash.
- **Unknown hub slug**: event is stored with `metadata.hub_warning = "unknown_hub_slug"`.
- **Unregistered widget_type**: event is stored with
`metadata.type_warning = "unregistered_widget_type"`. No 422 is returned to
the frontend (retroactive registration is possible).
---
## Backwards Compatibility Rule
v1.0 is frozen. New optional attributes may be added in v1.1 without breaking
existing implementations. Removing any attribute, or making an optional
attribute required, requires a new major version (v2.0) with a migration window.
---
## Implementation Reference
- Schema enforcement: `widgets.widget_type` validated against `widget_type_registry`
on create/update in `WidgetsController`
- Client-side: `static/ihf-annotation-launcher.js` reads `data-widget-id` and
`data-hub-id` from the nearest ancestor with these attributes
- Server-side envelope rendering: `widgetEnvelope` HSX helper in
`Application/Helper/View.hs`

View File

@@ -0,0 +1,155 @@
# Hub Capability Manifest Contract
**Name:** hub-capability-manifest
**Version:** 1.0
**Date:** 2026-03-31
**Status:** Active
**Layer:** Extensions
**Maturity:** Beta
---
## Purpose
The Hub Capability Manifest is the formal extension registration mechanism of
the IHF. It is the contract by which a domain hub (dev-hub, ops-hub, fin-hub,
sec-hub) declares the vocabulary it introduces to the framework: widget types,
event types, annotation categories, and policy scopes.
Without an active manifest, a domain hub is an unregistered participant. Its
widgets and events are accepted by the framework, but its type vocabulary is
not namespaced, not validated, and not discoverable in Phase 10's marketplace.
---
## Schema
```sql
CREATE TABLE hub_capability_manifests (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
hub_id UUID NOT NULL UNIQUE REFERENCES hubs(id),
manifest_version TEXT NOT NULL DEFAULT '1.0',
declared_widget_types JSONB NOT NULL DEFAULT '[]',
declared_event_types JSONB NOT NULL DEFAULT '[]',
declared_annotation_categories JSONB NOT NULL DEFAULT '[]',
declared_policy_scopes JSONB NOT NULL DEFAULT '[]',
capability_description TEXT,
contact TEXT,
status TEXT NOT NULL DEFAULT 'draft',
activated_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
```
---
## Registration Workflow
```
1. Create manifest in draft
2. Declare types (edit declared_* arrays)
3. Activate (ActivateManifestAction)
↓ ← auto-registers declared types into their registries
4. Active manifest governs the hub's vocabulary
```
### Step 1 — Create in draft
```
POST /HubCapabilityManifests/new?hubId=<hub-id>
```
One manifest per hub (UNIQUE constraint on `hub_id`). Creating a manifest
for a hub that already has one (in any status) requires retiring the existing
active manifest first.
### Step 2 — Declare types
While in `draft` status, edit the `declared_*` JSONB arrays. Each array
contains the string names of types the hub will own:
```json
{
"declared_widget_types": ["dev-pipeline-run", "dev-build-status"],
"declared_event_types": ["dev-pipeline-started", "dev-pipeline-failed"],
"declared_annotation_categories": ["dev-blocker", "dev-flaky-test"],
"declared_policy_scopes": ["dev-internal"]
}
```
**Naming convention**: domain-owned types should be prefixed with the domain
shortcode to prevent collisions (e.g. `dev-`, `fin-`, `sec-`, `ops-`).
Framework-level types (no prefix) are owned by inter-hub and shared by all hubs.
### Step 3 — Activate
```
POST /HubCapabilityManifests/ActivateManifest?hubCapabilityManifestId=<id>
```
On activation:
- For each name in `declared_widget_types`: insert into `widget_type_registry`
with `owner_hub_id = hub.id` if not already present.
- Same for `declared_event_types`, `declared_annotation_categories`,
`declared_policy_scopes`.
- If any declared name already exists in a registry with a **different**
`owner_hub_id`, activation is rejected with a conflict error.
- If any declared name exists with `owner_hub_id = NULL` (framework-level),
activation is rejected: framework types cannot be claimed by a domain hub.
- `status` is set to `active`, `activated_at` is set to `now()`.
---
## Invariants
1. **Type names are permanent.** Once a type name is registered (either via
seed or manifest activation), it cannot be deleted from the registry —
only deprecated. Other hubs may already depend on it.
2. **Activated manifests are read-only on declared arrays.** To add new types,
retire the manifest and create a new draft, or use `DraftAmendmentAction`
which creates an amended draft pending re-activation.
3. **One active manifest per hub.** The UNIQUE constraint on `hub_id` plus the
activation workflow enforce this.
4. **Framework types cannot be claimed.** Type names with `owner_hub_id = NULL`
are owned by the framework. A domain hub manifest that attempts to declare
an existing framework type name is rejected.
---
## Status Lifecycle
```
draft → active → retired
(superseded by a new draft → active cycle)
```
A retired manifest's types remain in the registry. If the hub is decommissioned,
the types should be deprecated (not deleted) in the registry.
---
## Failure Modes
| Scenario | Behaviour |
|---|---|
| Duplicate type name (same hub) | Idempotent — skipped, not an error |
| Duplicate type name (different hub) | Activation rejected with conflict error |
| Framework type name claimed | Activation rejected |
| Edit of active manifest declared arrays | Rejected — manifest is read-only |
| Hub with no manifest creates hub-owned type | Warning in fitness function; types accepted but unmanifested |
---
## Implementation Reference
- Schema: `Application/Schema.sql` (added in IHUB-WP-0009-T05)
- Controller: `Web/Controller/HubCapabilityManifests.hs`
- Guide: `docs/domain-hub-extension-guide.md`
- Phase 10 dependency: Hub Registry = active manifests + health snapshots

View File

@@ -0,0 +1,123 @@
# Interaction Reporting API Contract
**Name:** interaction-reporting
**Version:** 1.0
**Date:** 2026-03-31
**Status:** Active
**Layer:** Functional
**Maturity:** Stable
---
## Purpose
Defines the REST API surface for external systems (non-IHP adapters, domain
hubs, third-party tools) to submit interaction events and annotations to the
IHF without coupling to the IHP server-side implementation.
---
## Canonical Source of Truth
The authoritative runtime contract is the active row in the
`interaction_reporting_contracts` table. This file is the discoverable
human-readable declaration. In case of conflict, the database row governs.
---
## Endpoint
```
POST /api/v1/interaction-events
```
Authentication: `Authorization: Bearer <hub-api-key>`
The `hub-api-key` must match the `api_key` column of a registered hub.
---
## Required Request Fields
| Field | Type | Description |
|---|---|---|
| `widget_id` | UUID | The governed widget's stable ID |
| `event_type` | TEXT | Must exist in `event_type_registry` with `status = 'active'` |
| `occurred_at` | ISO 8601 timestamp | When the event occurred on the client |
---
## Optional Request Fields
| Field | Type | Description |
|---|---|---|
| `actor_id` | UUID | The acting user's ID |
| `actor_type` | TEXT | `user`, `agent`, `automation`, `external_adapter` |
| `view_context_ref` | TEXT | View path where event occurred |
| `metadata` | JSON object | Arbitrary additional context |
---
## Response — Success (201 Created)
```json
{
"id": "<uuid>",
"widget_id": "<uuid>",
"event_type": "clicked",
"occurred_at": "2026-03-31T12:00:00Z"
}
```
---
## Response — Validation Error (422 Unprocessable Entity)
```json
{
"error": "event_type 'pipeline_started' not registered",
"help": "Register this event type via the Type Registry admin UI or hub capability manifest before submitting."
}
```
---
## Response — Auth Error (401 Unauthorized)
```json
{
"error": "Authorization: Bearer <hub-api-key> required"
}
```
---
## Accepted Event Types
Accepted event types are all entries in `event_type_registry` with
`status = 'active'`. The framework-level seed vocabulary:
`viewed`, `focused`, `clicked`, `submitted`, `abandoned`, `retried`,
`failed`, `commented`, `flagged_confusing`, `flagged_helpful`,
`blocked_by_policy`, `escalated`, `accepted_recommendation`,
`rejected_recommendation`
Domain hubs may register additional event types via `HubCapabilityManifest`.
---
## Versioning Policy
- v1.0: current version. Stable.
- Adding new optional request fields: minor version (v1.1), no breaking change.
- Changing required fields, response shape, or auth scheme: major version (v2.0).
- Phase 9 will introduce `/api/v2/` with OAuth 2.0 replacing per-hub Bearer tokens.
v1.0 will remain supported during the migration window.
---
## Implementation Reference
- Controller: `Web/Controller/ApiInteractionEvents.hs`
- Route: `Web/Routes.hs` (`CanRoute ApiInteractionEventsController`)
- DB record: `interaction_reporting_contracts` (contract_version = '1.0')

View File

@@ -0,0 +1,84 @@
# Module Maturity Labels
**Name:** module-maturity-labels
**Version:** 1.0
**Date:** 2026-03-31
**Status:** Active
**Layer:** Functional
---
## Purpose
Defines the four maturity states used to label IHF functional modules and
contract tables. These labels communicate to domain hub developers which
surfaces are safe to depend on long-term.
---
## Labels
### Stable
The public interface **will not change** within the current major version.
- Removing a field, renaming a table, or changing a column type is a **breaking
change** and requires a major version bump (e.g. IHF v1.0 → v2.0).
- Adding a nullable or defaulted column is **not** breaking and may occur
without a version bump, but is documented in the changelog.
- Suitable for all production use, including domain hub compilation dependencies.
### Beta
The interface is **finalised for typical use** but edge-case fields may change
with a minor-version notice (e.g. IHF v0.2 → v0.3).
- Core workflow fields are stable; metadata, computed, or supplementary fields
may be refined.
- Suitable for production use with awareness: monitor the changelog before
upgrading.
- A Beta module will be promoted to Stable once it has been used in at least
two deployed domain hubs without reported breaking-change issues.
### Experimental
The interface **may change without notice** between patch releases.
- Use only for internal prototyping, spike implementations, or with explicit
opt-in acknowledgement.
- Not suitable for domain hub production code.
- A module remains Experimental until its design is confirmed by real usage.
### Deprecated
The module or field **will be removed** in the next major version.
- A replacement is documented in the contract or in the schema comment.
- The deprecated item remains functional until the major version boundary.
- Domain hubs must migrate before upgrading to the next major version.
---
## Where Labels Are Used
| Surface | How the label is expressed |
|---|---|
| Functional modules | `docs/functional-modules.md` maturity column |
| `envelope_emission_contracts` table | `maturity` column (added in IHUB-WP-0009) |
| `interaction_reporting_contracts` table | `maturity` column |
| `widget_adapter_specs` table | `maturity` column |
| Type registry entries | `status` column (`active` / `deprecated`) |
| Contract files in `/contracts/` | `Maturity:` header field |
---
## Promotion Path
```
Experimental → Beta → Stable
Deprecated → (removed at next major version)
```
Promotions are recorded in the module's entry in `docs/functional-modules.md`
and the corresponding `maturity` column in the DB is updated.