From 55af11342d175d4c1af1a0d4f342027b7c0997e0 Mon Sep 17 00:00:00 2001 From: Bernd Worsch Date: Sun, 29 Mar 2026 21:03:00 +0000 Subject: [PATCH] =?UTF-8?q?feat(P6/T01):=20Phase=206=20schema=20=E2=80=94?= =?UTF-8?q?=20WidgetAdapterSpec,=20contracts,=20widgets.adapter=5Fspec=5Fi?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...-ihf-phase6-cross-framework-adaptation.sql | 91 +++++++++++++++++++ Application/Schema.sql | 67 ++++++++++++++ Web/FrontController.hs | 7 ++ Web/Routes.hs | 5 + Web/Types.hs | 22 ++++- 5 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 Application/Migration/1743465600-ihf-phase6-cross-framework-adaptation.sql diff --git a/Application/Migration/1743465600-ihf-phase6-cross-framework-adaptation.sql b/Application/Migration/1743465600-ihf-phase6-cross-framework-adaptation.sql new file mode 100644 index 0000000..97c8977 --- /dev/null +++ b/Application/Migration/1743465600-ihf-phase6-cross-framework-adaptation.sql @@ -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' +); diff --git a/Application/Schema.sql b/Application/Schema.sql index a388b61..e785b55 100644 --- a/Application/Schema.sql +++ b/Application/Schema.sql @@ -312,3 +312,70 @@ CREATE TABLE confidence_annotations ( ); 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; diff --git a/Web/FrontController.hs b/Web/FrontController.hs index 5c2855e..6520910 100644 --- a/Web/FrontController.hs +++ b/Web/FrontController.hs @@ -17,6 +17,9 @@ import Web.Controller.Requirements () import Web.Controller.DecisionRecords () import Web.Controller.DeploymentRecords () import Web.Controller.AgentProposals () +import Web.Controller.EnvelopeEmissionContracts () +import Web.Controller.InteractionReportingContracts () +import Web.Controller.WidgetAdapterSpecs () import Web.Controller.Sessions () instance FrontController WebApplication where @@ -32,6 +35,9 @@ instance FrontController WebApplication where , parseRoute @DecisionRecordsController , parseRoute @DeploymentRecordsController , parseRoute @AgentProposalsController + , parseRoute @EnvelopeEmissionContractsController + , parseRoute @InteractionReportingContractsController + , parseRoute @WidgetAdapterSpecsController ] instance InitControllerContext WebApplication where @@ -62,6 +68,7 @@ defaultLayout inner = [hsx| Decisions Deployments Agent + Adapters diff --git a/Web/Routes.hs b/Web/Routes.hs index a0ecf5d..052b660 100644 --- a/Web/Routes.hs +++ b/Web/Routes.hs @@ -34,5 +34,10 @@ instance AutoRoute DeploymentRecordsController -- Agent Proposals (Phase 5) instance AutoRoute AgentProposalsController +-- Phase 6 — Cross-Framework UI Adaptation +instance AutoRoute EnvelopeEmissionContractsController +instance AutoRoute InteractionReportingContractsController +instance AutoRoute WidgetAdapterSpecsController + -- Sessions instance AutoRoute SessionsController diff --git a/Web/Types.hs b/Web/Types.hs index 1ee7d90..3ff6ec6 100644 --- a/Web/Types.hs +++ b/Web/Types.hs @@ -26,7 +26,8 @@ data HubsController | TriageDashboardAction { hubId :: !(Id Hub) } | GovernanceDashboardAction { hubId :: !(Id Hub) } | AntifragilityDashboardAction { hubId :: !(Id Hub) } - | AgentAuditDashboardAction { hubId :: !(Id Hub) } + | AgentAuditDashboardAction { hubId :: !(Id Hub) } + | AdapterCompatibilityDashboardAction { hubId :: !(Id Hub) } deriving (Eq, Show, Data) data WidgetsController @@ -111,6 +112,25 @@ data AgentProposalsController | RejectProposalAction { agentProposalId :: !(Id AgentProposal) } 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 = NewSessionAction | CreateSessionAction