feat(P6/T01): Phase 6 schema — WidgetAdapterSpec, contracts, widgets.adapter_spec_id

Adds Phase 6 tables: envelope_emission_contracts, interaction_reporting_contracts,
widget_adapter_specs. Adds adapter_spec_id FK to widgets and api_key to hubs.
Seeds v1.0 contracts in migration. Registers Phase 6 controller types and routes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:03:00 +00:00
parent e00aa126c8
commit 55af11342d
5 changed files with 191 additions and 1 deletions

View File

@@ -0,0 +1,91 @@
-- IHF Phase 6 — Cross-Framework UI Adaptation Layer
-- Adds: envelope_emission_contracts, interaction_reporting_contracts,
-- widget_adapter_specs, widgets.adapter_spec_id, hubs.api_key
-- Seeds: v1.0 contracts
CREATE TABLE envelope_emission_contracts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
contract_version TEXT NOT NULL UNIQUE,
required_attributes JSONB NOT NULL,
optional_attributes JSONB NOT NULL DEFAULT '[]',
validation_rules JSONB NOT NULL DEFAULT '{}',
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX envelope_emission_contracts_status_idx ON envelope_emission_contracts (status);
CREATE TABLE interaction_reporting_contracts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
contract_version TEXT NOT NULL UNIQUE,
endpoint_path TEXT NOT NULL,
accepted_event_types JSONB NOT NULL,
required_fields JSONB NOT NULL,
auth_scheme TEXT NOT NULL DEFAULT 'bearer',
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX interaction_reporting_contracts_status_idx ON interaction_reporting_contracts (status);
CREATE TABLE widget_adapter_specs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE,
framework TEXT NOT NULL,
version TEXT NOT NULL,
envelope_contract_id UUID REFERENCES envelope_emission_contracts(id),
reporting_contract_id UUID REFERENCES interaction_reporting_contracts(id),
status TEXT NOT NULL DEFAULT 'draft',
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX widget_adapter_specs_framework_idx ON widget_adapter_specs (framework);
CREATE INDEX widget_adapter_specs_status_idx ON widget_adapter_specs (status);
ALTER TABLE widgets
ADD COLUMN adapter_spec_id UUID REFERENCES widget_adapter_specs(id);
CREATE INDEX widgets_adapter_spec_id_idx ON widgets (adapter_spec_id);
ALTER TABLE hubs
ADD COLUMN api_key TEXT;
-- Seed: v1.0 EnvelopeEmissionContract
INSERT INTO envelope_emission_contracts (
contract_version,
required_attributes,
optional_attributes,
validation_rules,
description,
status
) VALUES (
'1.0',
'["data-widget-id", "data-view-context", "data-hub-id"]',
'["data-policy-scope", "data-widget-version"]',
'{"data-widget-id": "uuid", "data-hub-id": "uuid"}',
'Canonical IHF widget envelope contract v1.0. Requires widget identity, view context, and hub attribution on every rendered widget element.',
'active'
);
-- Seed: v1.0 InteractionReportingContract
INSERT INTO interaction_reporting_contracts (
contract_version,
endpoint_path,
accepted_event_types,
required_fields,
auth_scheme,
description,
status
) VALUES (
'1.0',
'/api/v1/interaction-events',
'["clicked", "viewed", "submitted", "dismissed", "errored"]',
'["widget_id", "hub_id", "event_type", "occurred_at"]',
'bearer',
'Canonical IHF interaction reporting contract v1.0. External adapters POST events to this endpoint using a hub-scoped bearer token.',
'active'
);

View File

@@ -312,3 +312,70 @@ CREATE TABLE confidence_annotations (
); );
CREATE INDEX confidence_annotations_proposal_id_idx ON confidence_annotations (proposal_id); CREATE INDEX confidence_annotations_proposal_id_idx ON confidence_annotations (proposal_id);
-- ============================================================
-- Phase 6 — Cross-Framework UI Adaptation Layer
-- ============================================================
-- Formalises the rules for widget envelope emission: which data-* attributes
-- are required, their format, and the contract version.
CREATE TABLE envelope_emission_contracts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
contract_version TEXT NOT NULL UNIQUE, -- e.g. "1.0", "1.1"
required_attributes JSONB NOT NULL,
-- e.g. ["data-widget-id", "data-view-context", "data-hub-id"]
optional_attributes JSONB NOT NULL DEFAULT '[]',
validation_rules JSONB NOT NULL DEFAULT '{}',
-- machine-readable rules: format checks, presence guards
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
-- status values: draft | active | superseded
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX envelope_emission_contracts_status_idx ON envelope_emission_contracts (status);
-- Standardised REST interface contract for external event and annotation
-- submission — used by non-IHP adapters.
CREATE TABLE interaction_reporting_contracts (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
contract_version TEXT NOT NULL UNIQUE, -- e.g. "1.0"
endpoint_path TEXT NOT NULL, -- e.g. "/api/v1/interaction-events"
accepted_event_types JSONB NOT NULL, -- e.g. ["clicked","viewed","submitted"]
required_fields JSONB NOT NULL,
-- minimum payload: widget_id, hub_id, event_type, occurred_at
auth_scheme TEXT NOT NULL DEFAULT 'bearer',
description TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX interaction_reporting_contracts_status_idx ON interaction_reporting_contracts (status);
-- Describes how a specific UI technology maps to IHF widget protocol obligations.
CREATE TABLE widget_adapter_specs (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
name TEXT NOT NULL UNIQUE, -- e.g. "react-18", "vue-3", "web-component"
framework TEXT NOT NULL, -- e.g. "react", "vue", "vanilla"
version TEXT NOT NULL, -- adapter spec version, e.g. "1.0"
envelope_contract_id UUID REFERENCES envelope_emission_contracts(id),
reporting_contract_id UUID REFERENCES interaction_reporting_contracts(id),
status TEXT NOT NULL DEFAULT 'draft',
-- status values: draft | active | deprecated
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL
);
CREATE INDEX widget_adapter_specs_framework_idx ON widget_adapter_specs (framework);
CREATE INDEX widget_adapter_specs_status_idx ON widget_adapter_specs (status);
-- Link widgets to their adapter spec (null = native IHP widget).
ALTER TABLE widgets
ADD COLUMN adapter_spec_id UUID REFERENCES widget_adapter_specs(id);
CREATE INDEX widgets_adapter_spec_id_idx ON widgets (adapter_spec_id);
-- Per-hub API key for bearer-token auth on the interaction reporting endpoint.
ALTER TABLE hubs
ADD COLUMN api_key TEXT;

View File

@@ -17,6 +17,9 @@ import Web.Controller.Requirements ()
import Web.Controller.DecisionRecords () import Web.Controller.DecisionRecords ()
import Web.Controller.DeploymentRecords () import Web.Controller.DeploymentRecords ()
import Web.Controller.AgentProposals () import Web.Controller.AgentProposals ()
import Web.Controller.EnvelopeEmissionContracts ()
import Web.Controller.InteractionReportingContracts ()
import Web.Controller.WidgetAdapterSpecs ()
import Web.Controller.Sessions () import Web.Controller.Sessions ()
instance FrontController WebApplication where instance FrontController WebApplication where
@@ -32,6 +35,9 @@ instance FrontController WebApplication where
, parseRoute @DecisionRecordsController , parseRoute @DecisionRecordsController
, parseRoute @DeploymentRecordsController , parseRoute @DeploymentRecordsController
, parseRoute @AgentProposalsController , parseRoute @AgentProposalsController
, parseRoute @EnvelopeEmissionContractsController
, parseRoute @InteractionReportingContractsController
, parseRoute @WidgetAdapterSpecsController
] ]
instance InitControllerContext WebApplication where instance InitControllerContext WebApplication where
@@ -62,6 +68,7 @@ defaultLayout inner = [hsx|
<a href={DecisionRecordsAction} class="text-sm text-gray-600 hover:text-gray-900">Decisions</a> <a href={DecisionRecordsAction} class="text-sm text-gray-600 hover:text-gray-900">Decisions</a>
<a href={DeploymentRecordsAction} class="text-sm text-gray-600 hover:text-gray-900">Deployments</a> <a href={DeploymentRecordsAction} class="text-sm text-gray-600 hover:text-gray-900">Deployments</a>
<a href={AgentProposalsAction} class="text-sm text-gray-600 hover:text-gray-900">Agent</a> <a href={AgentProposalsAction} class="text-sm text-gray-600 hover:text-gray-900">Agent</a>
<a href={WidgetAdapterSpecsAction} class="text-sm text-gray-600 hover:text-gray-900">Adapters</a>
<div class="ml-auto"> <div class="ml-auto">
<a href={DeleteSessionAction} class="text-sm text-gray-500 hover:text-gray-700">Sign out</a> <a href={DeleteSessionAction} class="text-sm text-gray-500 hover:text-gray-700">Sign out</a>
</div> </div>

View File

@@ -34,5 +34,10 @@ instance AutoRoute DeploymentRecordsController
-- Agent Proposals (Phase 5) -- Agent Proposals (Phase 5)
instance AutoRoute AgentProposalsController instance AutoRoute AgentProposalsController
-- Phase 6 — Cross-Framework UI Adaptation
instance AutoRoute EnvelopeEmissionContractsController
instance AutoRoute InteractionReportingContractsController
instance AutoRoute WidgetAdapterSpecsController
-- Sessions -- Sessions
instance AutoRoute SessionsController instance AutoRoute SessionsController

View File

@@ -26,7 +26,8 @@ data HubsController
| TriageDashboardAction { hubId :: !(Id Hub) } | TriageDashboardAction { hubId :: !(Id Hub) }
| GovernanceDashboardAction { hubId :: !(Id Hub) } | GovernanceDashboardAction { hubId :: !(Id Hub) }
| AntifragilityDashboardAction { hubId :: !(Id Hub) } | AntifragilityDashboardAction { hubId :: !(Id Hub) }
| AgentAuditDashboardAction { hubId :: !(Id Hub) } | AgentAuditDashboardAction { hubId :: !(Id Hub) }
| AdapterCompatibilityDashboardAction { hubId :: !(Id Hub) }
deriving (Eq, Show, Data) deriving (Eq, Show, Data)
data WidgetsController data WidgetsController
@@ -111,6 +112,25 @@ data AgentProposalsController
| RejectProposalAction { agentProposalId :: !(Id AgentProposal) } | RejectProposalAction { agentProposalId :: !(Id AgentProposal) }
deriving (Eq, Show, Data) deriving (Eq, Show, Data)
data EnvelopeEmissionContractsController
= EnvelopeEmissionContractsAction
| ShowEnvelopeEmissionContractAction { envelopeEmissionContractId :: !(Id EnvelopeEmissionContract) }
deriving (Eq, Show, Data)
data InteractionReportingContractsController
= InteractionReportingContractsAction
| ShowInteractionReportingContractAction { interactionReportingContractId :: !(Id InteractionReportingContract) }
deriving (Eq, Show, Data)
data WidgetAdapterSpecsController
= WidgetAdapterSpecsAction
| ShowWidgetAdapterSpecAction { widgetAdapterSpecId :: !(Id WidgetAdapterSpec) }
| NewWidgetAdapterSpecAction
| CreateWidgetAdapterSpecAction
| EditWidgetAdapterSpecAction { widgetAdapterSpecId :: !(Id WidgetAdapterSpec) }
| UpdateWidgetAdapterSpecAction { widgetAdapterSpecId :: !(Id WidgetAdapterSpec) }
deriving (Eq, Show, Data)
data SessionsController data SessionsController
= NewSessionAction = NewSessionAction
| CreateSessionAction | CreateSessionAction