feat(P6): IHF Phase 6 complete — agent-assisted distillation

T08 gate: integration tests for all Phase 6 artifacts (EnvelopeEmissionContract,
InteractionReportingContract, WidgetAdapterSpec, adapter assignment, dashboard
coverage logic); SCOPE.md updated to Phase 6 complete; docs/phase6-summary.md
written (contract model, adapter pattern, known limitations, Phase 7 readiness);
workplan IHUB-WP-0006 marked done.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:27:10 +00:00
parent ae81dfd484
commit b1d1f5066b
4 changed files with 217 additions and 6 deletions

View File

@@ -65,9 +65,9 @@ IHF treats every meaningful UI element as a **governed interaction artifact** ra
## Current State
- Status: Phase 5 complete — agent-assisted distillation and suggestion implemented
- Implementation: Phase 0 complete (specification); Phase 1 complete (widget registry, event capture, annotations, hub dashboard, auth); Phase 2 complete (annotation severity, annotation threads, requirement candidates, triage lifecycle, reviewer assignment, triage dashboard); Phase 3 complete (requirement promotion, decision records, policy references, implementation change references, governance dashboard); Phase 4 complete (deployment records, outcome signals, pre/post comparison, regression detection, change evaluation, recurrence tracking, antifragility dashboard); Phase 5 complete (agent proposals, review records, confidence annotations, cluster summarization, requirement drafting, duplicate detection, policy sensitivity, implementation proposals, agent audit dashboard)
- Stability: core artifact model and schema are stable; Phase 5 data model (AgentProposal, AgentReviewRecord, ConfidenceAnnotation) is additive; all AI outputs are attributed and reviewer-controlled
- Status: Phase 6 complete — cross-framework UI adaptation layer implemented
- Implementation: Phase 0 complete (specification); Phase 1 complete (widget registry, event capture, annotations, hub dashboard, auth); Phase 2 complete (annotation severity, annotation threads, requirement candidates, triage lifecycle, reviewer assignment, triage dashboard); Phase 3 complete (requirement promotion, decision records, policy references, implementation change references, governance dashboard); Phase 4 complete (deployment records, outcome signals, pre/post comparison, regression detection, change evaluation, recurrence tracking, antifragility dashboard); Phase 5 complete (agent proposals, review records, confidence annotations, cluster summarization, requirement drafting, duplicate detection, policy sensitivity, implementation proposals, agent audit dashboard); Phase 6 complete (EnvelopeEmissionContract, InteractionReportingContract, WidgetAdapterSpec, REST API for cross-framework event submission, annotation launcher JS, React adapter, adapter compatibility dashboard)
- Stability: core artifact model and schema are stable; Phase 6 contracts are immutable once active; native IHP widgets unaffected (adapter_spec_id nullable); JS adapters are thin ESM modules with no build toolchain requirement
- Usage: reference implementation running on IHP v1.5 + PostgreSQL; `devenv up` to start
---

View File

@@ -1090,3 +1090,117 @@ main = do
length pending `shouldBe` 1
length accepted `shouldBe` 1
deleteRecord hub
-- ----------------------------------------------------------------
-- Phase 6 — Cross-Framework UI Adaptation Layer
-- ----------------------------------------------------------------
describe "EnvelopeEmissionContract" do
it "can create and fetch with required attributes" do
contract <- newRecord @EnvelopeEmissionContract
|> set #contractVersion "test-1.0"
|> set #requiredAttributes (toJSON (["data-widget-id","data-hub-id"] :: [Text]))
|> set #optionalAttributes (toJSON ([] :: [Text]))
|> set #validationRules (toJSON (object []))
|> set #status "active"
|> createRecord
contract.contractVersion `shouldBe` "test-1.0"
contract.status `shouldBe` "active"
fetched <- fetch contract.id
fetched.requiredAttributes `shouldBe` contract.requiredAttributes
deleteRecord contract
describe "InteractionReportingContract" do
it "can create and fetch" do
rc <- newRecord @InteractionReportingContract
|> set #contractVersion "test-1.0"
|> set #endpointPath "/api/v1/interaction-events"
|> set #acceptedEventTypes (toJSON (["clicked","viewed"] :: [Text]))
|> set #requiredFields (toJSON (["widget_id","hub_id"] :: [Text]))
|> set #authScheme "bearer"
|> set #status "active"
|> createRecord
rc.endpointPath `shouldBe` "/api/v1/interaction-events"
rc.authScheme `shouldBe` "bearer"
fetched <- fetch rc.id
fetched.contractVersion `shouldBe` "test-1.0"
deleteRecord rc
describe "WidgetAdapterSpec" do
it "can create with draft status and transition to active" do
ec <- newRecord @EnvelopeEmissionContract
|> set #contractVersion "test-env-1.0"
|> set #requiredAttributes (toJSON ([] :: [Text]))
|> set #status "active"
|> createRecord
rc <- newRecord @InteractionReportingContract
|> set #contractVersion "test-rep-1.0"
|> set #endpointPath "/api/v1/interaction-events"
|> set #acceptedEventTypes (toJSON ([] :: [Text]))
|> set #requiredFields (toJSON ([] :: [Text]))
|> set #status "active"
|> createRecord
spec <- newRecord @WidgetAdapterSpec
|> set #name "test-react-18"
|> set #framework "react"
|> set #version "1.0"
|> set #envelopeContractId (Just ec.id)
|> set #reportingContractId (Just rc.id)
|> set #status "draft"
|> createRecord
spec.status `shouldBe` "draft"
spec.framework `shouldBe` "react"
-- Transition to active
spec |> set #status "active" |> updateRecord
fetched <- fetch spec.id
fetched.status `shouldBe` "active"
deleteRecord spec
deleteRecord ec
deleteRecord rc
describe "Widget adapter assignment" do
it "widget can be assigned to an adapter spec and badge resolves" do
hub <- newRecord @Hub |> set #name "AdapterHub" |> createRecord
spec <- newRecord @WidgetAdapterSpec
|> set #name "badge-test-adapter" |> set #framework "vue"
|> set #version "1.0" |> set #status "active"
|> createRecord
widget <- newRecord @Widget
|> set #hubId hub.id
|> set #name "AdapterWidget"
|> set #widgetType "chart"
|> set #adapterSpecId (Just spec.id)
|> createRecord
widget.adapterSpecId `shouldBe` Just spec.id
mSpec <- case widget.adapterSpecId of
Nothing -> pure Nothing
Just sid -> fetchOneOrNothing sid
fmap (.name) mSpec `shouldBe` Just "badge-test-adapter"
deleteRecord widget
deleteRecord spec
deleteRecord hub
describe "Adapter compatibility dashboard data" do
it "correctly categorises native vs adapter-backed widgets" do
hub <- newRecord @Hub |> set #name "CompatHub" |> createRecord
spec <- newRecord @WidgetAdapterSpec
|> set #name "compat-test" |> set #framework "react"
|> set #version "1.0" |> set #status "active"
|> createRecord
w1 <- newRecord @Widget
|> set #hubId hub.id |> set #name "Native"
|> set #widgetType "chart"
|> createRecord
w2 <- newRecord @Widget
|> set #hubId hub.id |> set #name "Adapted"
|> set #widgetType "form" |> set #adapterSpecId (Just spec.id)
|> createRecord
hubWidgets <- query @Widget |> filterWhere (#hubId, hub.id) |> fetch
let adapted = length (filter (\w -> isJust w.adapterSpecId) hubWidgets)
native = length (filter (\w -> isNothing w.adapterSpecId) hubWidgets)
adapted `shouldBe` 1
native `shouldBe` 1
deleteRecord w1
deleteRecord w2
deleteRecord spec
deleteRecord hub

97
docs/phase6-summary.md Normal file
View File

@@ -0,0 +1,97 @@
# IHF Phase 6 Summary — Cross-Framework UI Adaptation Layer
## What Was Built
Phase 6 ensures that widget identity, interaction capture, and annotation capability are preserved when UI components are authored outside of IHP HSX — React, Vue, or any JS-based component — without bypassing the IHF core.
### Contract Artifacts
**EnvelopeEmissionContract** (`envelope_emission_contracts`)
- Formalises the widget envelope as a versioned, immutable contract
- v1.0 seed: required `["data-widget-id", "data-view-context", "data-hub-id"]`, optional `["data-policy-scope", "data-widget-version"]`
- `widgetEnvelope` helper updated to validate required attributes at render time (non-crashing warning)
- Read-only controller + index/show pages under "Contracts" nav
**InteractionReportingContract** (`interaction_reporting_contracts`)
- Standardised REST interface contract for external event and annotation submission
- v1.0 seed: endpoint `/api/v1/interaction-events`, accepted event types `["clicked","viewed","submitted","dismissed","errored"]`, required fields `["widget_id","hub_id","event_type","occurred_at"]`
- Read-only controller + index/show pages
**WidgetAdapterSpec** (`widget_adapter_specs`)
- Registry of supported UI framework adapters; links to envelope and reporting contracts
- Status lifecycle: `draft → active → deprecated`
- Full CRUD controller (no delete — audit artifact)
- Widgets gain an optional `adapter_spec_id` FK (null = native IHP widget)
### REST API
`POST /api/v1/interaction-events` — JSON body, Bearer token auth (per-hub `api_key`):
- Validates payload against active `InteractionReportingContract`
- Creates `InteractionEvent` record
- Returns `201 Created` with `{id, widget_id, event_type, occurred_at}` or `422` with errors
- Custom `CanRoute`/`HasPath` instances (not AutoRoute) for the `/api/v1/` path prefix
### JavaScript Client
**`static/js/ihf-annotation-launcher.js`** — vanilla JS IIFE (no framework dependency):
- Scans DOM for `[data-widget-id]` on `DOMContentLoaded`
- Injects inline "annotate" trigger adjacent to each enrolled element
- On click: opens an inline form (textarea + category select) and POSTs to existing `/widgets/:widgetId/annotations` endpoint
- Works in React-rendered pages where IHP does not own the DOM
- Feature-flagged via `IHP_ANNOTATION_LAUNCHER=true` env var (`AnnotationLauncherEnabled` typed config)
**`static/js/ihf-react-adapter.js`** — ESM module for React 18+:
- `useWidgetEnvelope(widgetId, hubId, viewContext)` — returns `data-*` props conforming to envelope v1.0
- `withWidgetEnvelope(WrappedComponent, widgetId, hubId, viewContext)` — HOC wrapping root element
- `useInteractionReporter(widgetId, hubId)` — returns `reportEvent(type)` POSTing to `/api/v1/interaction-events`
**`static/ihf-react-test.html`** — static test fixture with CDN React 18, a React-rendered widget, and an IHP-rendered widget on the same page, both enrolled with the annotation launcher.
### Dashboards and Navigation
**Adapter Compatibility Dashboard** (`AdapterCompatibilityDashboardAction { hubId }`):
- Panel 1: Adapter spec counts by status (active / draft / deprecated)
- Panel 2: Widget coverage — native IHP vs adapter-backed with percentage bar and per-spec breakdown
- Panel 3: Active contract versions in use (envelope + reporting)
- Panel 4: Unassigned widgets (no `adapter_spec_id`)
- Panel 5: Active adapter specs table with widget counts
- `autoRefresh` — live-updates on any DB change
- Linked from hub Show page and global nav ("Adapters")
Widget show page renders an adapter badge (purple, links to spec) when `adapter_spec_id` is set.
## Contract Model
Contracts are **immutable once active**. A new version supersedes the old; old versions remain readable for audit. The status lifecycle is:
```
draft → active → superseded (EnvelopeEmissionContract, InteractionReportingContract)
draft → active → deprecated (WidgetAdapterSpec)
```
This prevents retroactive mutation of the rules that governed historical interaction events.
## Adapter Pattern
Any UI technology participates by honouring three obligations:
1. **Envelope** — root DOM element carries `data-widget-id`, `data-view-context`, `data-hub-id`
2. **Event reporting** — interactions submitted to `POST /api/v1/interaction-events` with bearer auth
3. **Annotation** — annotation launcher picks up `data-widget-id` automatically; no adapter-specific code required
Native IHP widgets are unaffected. `adapter_spec_id` is nullable. The `widgetEnvelope` helper continues to work unchanged.
## Known Limitations
- **No local JS build toolchain.** `ihf-react-adapter.js` is a plain ESM module; consumers must bundle it themselves (e.g. Vite, esbuild). Phase 6 does not add npm/webpack to the IHP project.
- **Bearer token scope.** The per-hub `api_key` is a simple secret stored in `hubs.api_key`. Phase 8 (federated) should replace this with OAuth or a proper token management service.
- **Annotation launcher requires no CSP violations.** The inline form approach works in permissive CSP environments; strict `script-src` policies may require nonce injection.
- **Cross-machine consistency checks.** The State Hub consistency checker runs on the server and cannot reach the local repo filesystem; C-01/C-07 errors in the check output are path false-positives for this deployment topology.
## Phase 7 Readiness
Phase 6 leaves the IHF core stable and extensible:
- All Phase 6 contracts are versioned and immutable — Phase 7 can introduce v1.1 contracts without breaking existing adapters
- Widget `adapter_spec_id` is nullable and indexed — adapter assignment is opt-in
- The REST API surface is defined by the `InteractionReportingContract` record — contract evolution is data-driven
- The JS adapter is a thin ESM module — framework-specific adapters for Vue, Web Components, etc. follow the same pattern as `ihf-react-adapter.js`

View File

@@ -4,7 +4,7 @@ type: workplan
title: "IHF Phase 6 — Cross-Framework UI Adaptation Layer"
domain: inter_hub
repo: inter-hub
status: todo
status: done
owner: custodian
topic_slug: inter_hub
created: "2026-03-29"
@@ -280,7 +280,7 @@ picks up the React widget's `data-widget-id`; docs written.
```task
id: IHUB-WP-0006-T07
status: todo
status: done
priority: medium
state_hub_task_id: "dc8fa48a-7195-4410-a77e-717b53127c2e"
```
@@ -311,7 +311,7 @@ adapter changes; stale adapter detection works.
```task
id: IHUB-WP-0006-T08
status: todo
status: done
priority: high
state_hub_task_id: "90ea4814-7603-4016-be34-d41ae091f7e1"
```