generated from coulomb/repo-seed
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:
6
SCOPE.md
6
SCOPE.md
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -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
97
docs/phase6-summary.md
Normal 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`
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user