Files
inter-hub/workplans/IHUB-WP-0013-ihf-phase12-platform-memory.md
Bernd Worsch 0f505feb2d feat(WP-0013): IHF Phase 12 — Platform Memory and Continuous Learning
Closes the long-range feedback loop: outcome signals now enrich the full
traceability chain and feed back into routing, triage, and AI proposals.

Schema (T01):
- outcome_correlations (CHECK correlation_type)
- pattern_performance_records
- adaptive_threshold_configs
- institutional_knowledge_entries (GIN tsvector FTS)
- learning_insights (CHECK insight_type)
- ALTER TABLE decision_records + requirement_candidates: outcome_summary JSONB
- AFTER INSERT trigger trg_enrich_lineage on outcome_signals
- contracts/core/ updated (outcome-summary-columns-v1, append-only addendum)

Correlation engine (T02):
- Application/Helper/CorrelationEngine.hs: pure annotation→outcome SQL
- Web/Controller/OutcomeCorrelations.hs: ComputeCorrelationsAction + index

Pattern performance (T03):
- Web/Controller/PatternPerformance.hs: ComputePatternPerformanceAction

Adaptive thresholds (T04):
- Web/Controller/AdaptiveThresholds.hs: CalibrateThresholdsAction
- Application/Helper/FrictionScore.hs: applyAdaptiveWeights

Institutional knowledge (T05):
- DistilDecisionAction in DecisionRecords controller
- Web/Controller/InstitutionalKnowledge.hs: QueryKnowledgeBaseAction

Lineage enrichment (T06):
- Web/Controller/LineageEnrichment.hs: EnrichLineageAction (batch backfill)
- enrich_lineage_on_outcome_batch() PL/pgSQL helper in migration

Learning dashboard (T07):
- Web/Controller/LearningDashboard.hs: 5-panel autoRefresh view
- "Learning" nav link in FrontController

API v2 learning endpoints (T08):
- GET /api/v2/outcome-correlations, /pattern-performance, /knowledge-base/{id}
- OpenAPI schemas: OutcomeCorrelation, PatternPerformanceRecord, InstitutionalKnowledgeEntry

GAAF scorecard + docs (T09):
- Core 3.8→3.9, Functional 3.6→3.8, overall 3.61→3.68
- CLAUDE.md: IHF v0.2 complete, no active workplan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 12:34:07 +00:00

18 KiB
Raw Permalink Blame History

id, type, title, domain, repo, status, owner, topic_slug, created, updated, state_hub_sync, state_hub_workstream_id
id type title domain repo status owner topic_slug created updated state_hub_sync state_hub_workstream_id
IHUB-WP-0013 workplan IHF Phase 12 — Platform Memory and Continuous Learning inter_hub inter-hub done custodian inter_hub 2026-04-01 2026-04-01 done baeb2891-2136-4ac5-aa03-b635e87285dd

IHF Phase 12 — Platform Memory and Continuous Learning

Goal

Close the longest feedback loop in the IHF: from deployed outcome signals and accumulated governance history back to improved distillation, better routing, and sharper AI proposals. Phase 12 makes the IHF a learning platform, not merely a record-keeping one.

Background

Phases 111 and IHUB-WP-0013 entry gates are satisfied:

  • Phase 4 OutcomeSignal append-only table operational ✓
  • Phase 7 FrictionScore + BottleneckRecord + HubHealthSnapshot operational ✓
  • Phase 10 WidgetPattern + PatternAdoption with aggregate panel ✓
  • Phase 11 AgentRegistration + ModelRoutingPolicy + AiGovernancePolicy operational ✓
  • Full traceability chain: Widget → Annotation → RequirementCandidate → Requirement → DecisionRecord → DeploymentRecord → OutcomeSignal ✓
  • GAAF scorecard at 3.61 (Strong) ✓

Reference: specs/InteractionHubFrameworkSpecification_v0.2.md §Phase 12.

GAAF Architectural Constraints

  1. outcome_correlations.correlation_type must carry a CHECK constraint (annotation_predictor, routing_quality, pattern_quality).
  2. learning_insights.insight_type must carry a CHECK constraint (annotation_predictor, threshold_calibration, pattern_ranking, routing_improvement).
  3. Core table extensionsdecision_records and requirement_candidates gain outcome_summary JSONB NULL columns via ALTER TABLE. This requires updating /contracts/core/ (GAAF rule 3).
  4. The outcome_signals enrichment trigger is read-only on core tables — it may UPDATE outcome_summary on non-append-only columns; it must never UPDATE outcome_signals or interaction_events.
  5. The knowledge base uses PostgreSQL GIN full-text search over institutional_knowledge_entries.summary, not a vector database. Simple, dependency-free, works with the existing Postgres stack.

Data Artifacts Introduced

OutcomeCorrelation, PatternPerformanceRecord, AdaptiveThresholdConfig, InstitutionalKnowledgeEntry, LearningInsight

Schema additions

-- outcome_correlations: links annotation signals to downstream outcome quality
-- GAAF: correlation_type CHECK constraint
CREATE TABLE outcome_correlations (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    hub_id UUID NOT NULL REFERENCES hubs(id),
    annotation_category TEXT NOT NULL REFERENCES annotation_category_registry(name),
    correlation_type TEXT NOT NULL DEFAULT 'annotation_predictor',
    correlation_score DOUBLE PRECISION NOT NULL,
    -- score = fraction of positive outcomes for this category in this hub
    sample_count INTEGER NOT NULL DEFAULT 0,
    computed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
    CHECK (correlation_type IN ('annotation_predictor', 'routing_quality', 'pattern_quality'))
);

CREATE INDEX outcome_correlations_hub_idx ON outcome_correlations (hub_id);
CREATE INDEX outcome_correlations_score_idx ON outcome_correlations (correlation_score DESC);

-- pattern_performance_records: per-pattern historical outcome quality
CREATE TABLE pattern_performance_records (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    widget_pattern_id UUID NOT NULL REFERENCES widget_patterns(id),
    hub_id UUID NOT NULL REFERENCES hubs(id),
    adoption_count INTEGER NOT NULL DEFAULT 0,
    positive_outcome_count INTEGER NOT NULL DEFAULT 0,
    total_outcome_count INTEGER NOT NULL DEFAULT 0,
    mean_outcome_value DOUBLE PRECISION,
    outcome_rank INTEGER,
    calibrated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
    UNIQUE (widget_pattern_id, hub_id)
);

CREATE INDEX pattern_performance_pattern_idx ON pattern_performance_records (widget_pattern_id);
CREATE INDEX pattern_performance_rank_idx ON pattern_performance_records (hub_id, outcome_rank);

-- adaptive_threshold_configs: per-hub friction weight overrides
CREATE TABLE adaptive_threshold_configs (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    hub_id UUID NOT NULL REFERENCES hubs(id) UNIQUE,
    weight_overrides JSONB NOT NULL DEFAULT '{}',
    -- keys: friction component names; values: multiplier floats
    bottleneck_threshold_override DOUBLE PRECISION,
    calibration_date TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
    notes TEXT
);

CREATE INDEX adaptive_threshold_hub_idx ON adaptive_threshold_configs (hub_id);

-- institutional_knowledge_entries: distilled decision summaries
-- GIN index for full-text search
CREATE TABLE institutional_knowledge_entries (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    hub_id UUID NOT NULL REFERENCES hubs(id),
    decision_record_id UUID REFERENCES decision_records(id),
    summary TEXT NOT NULL,
    summary_tsv TSVECTOR GENERATED ALWAYS AS (to_tsvector('english', summary)) STORED,
    tags JSONB NOT NULL DEFAULT '[]',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);

CREATE INDEX institutional_knowledge_hub_idx ON institutional_knowledge_entries (hub_id);
CREATE INDEX institutional_knowledge_fts_idx ON institutional_knowledge_entries USING GIN (summary_tsv);

-- learning_insights: platform-level insights with evidence
-- GAAF: insight_type CHECK constraint
CREATE TABLE learning_insights (
    id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
    hub_id UUID NOT NULL REFERENCES hubs(id),
    insight_type TEXT NOT NULL,
    title TEXT NOT NULL,
    body TEXT NOT NULL,
    evidence_links JSONB NOT NULL DEFAULT '[]',
    -- array of {type, id, label} objects
    is_actioned BOOLEAN NOT NULL DEFAULT FALSE,
    computed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
    CHECK (insight_type IN (
        'annotation_predictor',
        'threshold_calibration',
        'pattern_ranking',
        'routing_improvement'
    ))
);

CREATE INDEX learning_insights_hub_idx ON learning_insights (hub_id);
CREATE INDEX learning_insights_type_idx ON learning_insights (insight_type);

-- Extend core tables with outcome_summary (retroactive lineage enrichment)
-- GAAF: requires /contracts/core/ update (T06)
ALTER TABLE decision_records
    ADD COLUMN outcome_summary JSONB NULL;

ALTER TABLE requirement_candidates
    ADD COLUMN outcome_summary JSONB NULL;

Tasks

T01 — Schema: all Phase 12 tables + migration

id: IHUB-WP-0013-T01
status: done
priority: high
state_hub_task_id: "7bef90d5-7efc-488b-80ba-7f1a2220f75a"

Add all Phase 12 tables to Application/Schema.sql and write migration Application/Migration/<timestamp>-ihf-phase12-platform-memory.sql.

Includes ALTER TABLE on decision_records and requirement_candidates for outcome_summary JSONB NULL. Update /contracts/core/ to document the new columns per GAAF rule 3.


T02 — Outcome correlation engine

id: IHUB-WP-0013-T02
status: done
priority: high
state_hub_task_id: "589bf316-1a44-4726-b88c-cc7940f4dc53"

Application/Helper/CorrelationEngine.hs — pure computation:

module Application.Helper.CorrelationEngine where

import IHP.Prelude
import Generated.Types
import Database.PostgreSQL.Simple (Only(..))

-- | For a hub, compute the correlation score per annotation category:
-- fraction of traceability chains that end in a positive outcome signal
-- (signal_type IN ('success', 'adoption', 'satisfaction')).
computeAnnotationCorrelations ::
    (?modelContext :: ModelContext) =>
    Id Hub -> IO [(Text, Double, Int)]
    -- ^ [(category, score, sample_count)]
computeAnnotationCorrelations hubId =
    sqlQuery
        "SELECT a.category, \
        \       COALESCE(AVG(CASE WHEN os.signal_type IN ('success','adoption','satisfaction') \
        \                        THEN 1.0 ELSE 0.0 END), 0) AS score, \
        \       COUNT(os.id)::int AS sample_count \
        \ FROM annotations a \
        \ JOIN widgets w ON w.id = a.widget_id \
        \ JOIN requirement_candidates rc ON rc.source_widget_id = w.id \
        \ JOIN requirements r ON r.candidate_id = rc.id \
        \ JOIN decision_records dr ON dr.requirement_id = r.id \
        \ JOIN deployment_records dep ON dep.decision_id = dr.id \
        \ JOIN outcome_signals os ON os.deployment_id = dep.id \
        \ WHERE w.hub_id = ? \
        \ GROUP BY a.category \
        \ ORDER BY score DESC"
        [hubId]

Web/Controller/OutcomeCorrelations.hs:

  • ComputeCorrelationsAction { hubId } — runs engine, upserts OutcomeCorrelation records, generates a LearningInsight of type annotation_predictor for the top-scoring category
  • OutcomeCorrelationsAction — index view sorted by score

Views: Web/View/OutcomeCorrelations/{Index,Show}.hs


T03 — Pattern performance memory

id: IHUB-WP-0013-T03
status: done
priority: high
state_hub_task_id: "3790e9da-a28b-4287-a0bb-0083e2af42f7"

ComputePatternPerformanceAction { hubId } in a new Web/Controller/PatternPerformance.hs:

SELECT
    wp.id AS pattern_id,
    COUNT(DISTINCT pa.id) AS adoption_count,
    COUNT(os.id) AS total_outcome_count,
    COUNT(os.id) FILTER (
        WHERE os.signal_type IN ('success','adoption','satisfaction')
    ) AS positive_outcome_count,
    AVG(os.value) AS mean_outcome_value
FROM widget_patterns wp
JOIN pattern_adoptions pa ON pa.widget_pattern_id = wp.id
JOIN widgets w ON w.hub_id = pa.adopting_hub_id
    AND w.widget_type = wp.widget_type
JOIN deployment_records dep ON dep.id IN (
    SELECT dep2.id FROM deployment_records dep2
    JOIN decision_records dr2 ON dr2.id = dep2.decision_id
    JOIN requirements r2 ON r2.id = dr2.requirement_id
    JOIN requirement_candidates rc2 ON rc2.id = r2.candidate_id
    WHERE rc2.source_widget_id = w.id
)
JOIN outcome_signals os ON os.deployment_id = dep.id
WHERE pa.adopting_hub_id = ?
GROUP BY wp.id

Writes PatternPerformanceRecord per pattern, computes outcome_rank via RANK() OVER (ORDER BY positive_outcome_count::float / NULLIF(total_outcome_count,0) DESC).

Update Web/Controller/MarketplaceDashboard.hs: if PatternPerformanceRecord exists for a pattern, use outcome_rank for sort order.


T04 — Adaptive friction thresholds

id: IHUB-WP-0013-T04
status: done
priority: medium
state_hub_task_id: "a1de1a6b-14aa-4a3c-a103-d2630b762d30"

CalibrateThresholdsAction { hubId } in Web/Controller/AdaptiveThresholds.hs:

  1. Query OutcomeCorrelation records for the hub — find which annotation categories have correlation_score < 0.3 (weak predictors)
  2. Compute a bottleneck_threshold_override = mean friction_score for widgets with negative outcomes only
  3. Upsert AdaptiveThresholdConfig for the hub
  4. Write LearningInsight of type threshold_calibration

Update Application/Helper/FrictionScore.hs:

-- | Read per-hub AdaptiveThresholdConfig and apply weight_overrides
-- to friction component scores before summing. Falls back to global
-- defaults when no config exists for the hub.
applyAdaptiveWeights ::
    (?modelContext :: ModelContext) =>
    Id Hub -> FrictionComponents -> IO Double
applyAdaptiveWeights hubId components = do
    mConfig <- query @AdaptiveThresholdConfig
        |> filterWhere (#hubId, hubId)
        |> fetchOneOrNothing
    let overrides = maybe mempty (.weightOverrides) mConfig
    pure $ computeWeightedScore overrides components

Views: Web/View/AdaptiveThresholds/{Index,Show}.hs


T05 — Institutional knowledge base

id: IHUB-WP-0013-T05
status: done
priority: medium
state_hub_task_id: "16f03f8e-e664-4589-bdba-45cfed638595"

DistilDecisionAction { decisionRecordId } — appended to Web/Controller/DecisionRecords.hs:

action DistilDecisionAction { decisionRecordId } = do
    record <- fetch decisionRecordId
    outcomes <- sqlQuery
        "SELECT os.signal_type, os.value FROM outcome_signals os
         JOIN deployment_records dep ON dep.id = os.deployment_id
         WHERE dep.decision_id = ?" [decisionRecordId]
    let prompt = "Distil this decision into a 2-3 sentence institutional
                  knowledge entry. Include the outcome data.\n\nDecision: "
               <> record.title <> "\nRationale: " <> record.rationale
               <> "\nOutcome: " <> record.outcome
               <> "\nSignals: " <> show (outcomes :: [(Text, Double)])
    mAgent <- resolveAgent record.hubId "synthesis"
    ...
    newRecord @InstitutionalKnowledgeEntry
        |> set #hubId record.hubId
        |> set #decisionRecordId (Just decisionRecordId)
        |> set #summary content
        |> set #tags (A.toJSON ["decision" :: Text])
        |> createRecord

QueryKnowledgeBaseAction — full-text search:

SELECT id, hub_id, decision_record_id, summary, tags, created_at
FROM institutional_knowledge_entries
WHERE hub_id = ?
  AND summary_tsv @@ plainto_tsquery('english', ?)
ORDER BY ts_rank(summary_tsv, plainto_tsquery('english', ?)) DESC
LIMIT 20

Views: Web/View/InstitutionalKnowledge/{Index,Show}.hs with search form.


T06 — Retroactive lineage enrichment

id: IHUB-WP-0013-T06
status: done
priority: medium
state_hub_task_id: "cad61a11-7fdb-4e69-9dba-bb0176b2afdb"

PL/pgSQL trigger (in migration SQL):

CREATE OR REPLACE FUNCTION enrich_lineage_on_outcome()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
DECLARE
    v_dep_id  UUID;
    v_dec_id  UUID;
    v_req_id  UUID;
    v_cand_id UUID;
    v_summary JSONB;
BEGIN
    -- Walk chain upward from the new outcome_signal
    SELECT decision_id INTO v_dec_id
    FROM deployment_records WHERE id = NEW.deployment_id;

    IF v_dec_id IS NOT NULL THEN
        v_summary := jsonb_build_object(
            'signal_type', NEW.signal_type,
            'value',       NEW.value,
            'observed_at', NEW.observed_at
        );

        UPDATE decision_records
        SET outcome_summary = COALESCE(outcome_summary, '[]'::jsonb) || v_summary
        WHERE id = v_dec_id;

        SELECT requirement_id INTO v_req_id
        FROM decision_records WHERE id = v_dec_id;

        IF v_req_id IS NOT NULL THEN
            SELECT candidate_id INTO v_cand_id
            FROM requirements WHERE id = v_req_id;

            IF v_cand_id IS NOT NULL THEN
                UPDATE requirement_candidates
                SET outcome_summary = COALESCE(outcome_summary, '[]'::jsonb) || v_summary
                WHERE id = v_cand_id;
            END IF;
        END IF;
    END IF;

    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_enrich_lineage
AFTER INSERT ON outcome_signals
FOR EACH ROW EXECUTE FUNCTION enrich_lineage_on_outcome();

EnrichLineageAction { hubId } in Web/Controller/LineageEnrichment.hs — batch on-demand version: queries existing outcome_signals for the hub and calls the same enrichment logic via a SQL function call.

Update /contracts/core/append-only-events-v1.md to note that outcome_signals now has an AFTER INSERT trigger that enriches upstream records (read: the trigger never modifies outcome_signals itself).


T07 — Learning dashboard

id: IHUB-WP-0013-T07
status: done
priority: medium
state_hub_task_id: "4445282e-e87c-48fe-87ba-484da4121195"

Web/Controller/LearningDashboard.hs with autoRefresh:

data ShowView = ShowView
    { topCorrelations  :: ![OutcomeCorrelation]
    , patternRankings  :: ![PatternPerformanceRecord]
    , thresholdStatus  :: ![(Hub, Maybe AdaptiveThresholdConfig)]
    , recentInsights   :: ![LearningInsight]
    , knowledgeHighlights :: ![InstitutionalKnowledgeEntry]
    }

Five panels:

  1. Top annotation predictorsOutcomeCorrelation top 10 by score, with colour-coded bars (green ≥ 0.7, amber 0.40.7, red < 0.4)
  2. Pattern performance rankingPatternPerformanceRecord top 10 by positive_outcome_rate, with link to pattern show page
  3. Adaptive threshold status — per hub: last calibration date, drift indicator (days since last calibration > 30 = amber)
  4. Recent learning insights — last 10 LearningInsight with type badge and evidence link count
  5. Knowledge base highlights — 5 most recent InstitutionalKnowledgeEntry with excerpt and link to full entry

Add "Learning" nav link in Web/FrontController.hs.


T08 — API v2: /outcome-correlations, /pattern-performance, /knowledge-base

id: IHUB-WP-0013-T08
status: done
priority: medium
state_hub_task_id: "2b3e7f84-c8f6-42fc-bb0a-4c524efd1688"

Web/Controller/Api/V2/Learning.hs:

  • GET /api/v2/outcome-correlations — paginated; filter ?hub_id=, ?category=
  • GET /api/v2/pattern-performance — paginated; sort by positive_outcome_rate
  • GET /api/v2/knowledge-base — full-text search via ?q=; paginated
  • GET /api/v2/knowledge-base/{id} — single entry

All require BearerAuth. Add three schemas to Web/Controller/Api/V2/OpenApi.hs: OutcomeCorrelation, PatternPerformanceRecord, InstitutionalKnowledgeEntry.

Update Web/Types.hs:

data ApiV2LearningController
    = ApiV2IndexOutcomeCorrelationsAction
    | ApiV2IndexPatternPerformanceAction
    | ApiV2IndexKnowledgeBaseAction
    | ApiV2ShowKnowledgeBaseAction { knowledgeEntryId :: !(Id InstitutionalKnowledgeEntry) }
    deriving (Eq, Show, Data)

T09 — GAAF scorecard + CLAUDE.md + workplan done

id: IHUB-WP-0013-T09
status: done
priority: medium
state_hub_task_id: "a9048aeb-5e4b-49e5-b8f5-159ede9ab04c"

ARCHITECTURE-LAYERS.md scorecard update:

  • Core: 3.8 → 3.9 (lineage enrichment trigger + outcome_summary columns; contracts updated to document them)
  • Functional: 3.6 → 3.8 (outcome correlation + adaptive thresholds close the long-range feedback loop; learning dashboard makes insights visible)
  • Target overall: ≥ 3.75

Decisions Log entries:

  • Trigger-based lineage enrichment over polling (AFTER INSERT, zero app-layer overhead)
  • GIN tsvector over pgvector for knowledge base search (no extension dependency, sufficient for keyword queries)
  • outcome_summary as JSONB append (not a normalised table) to avoid joins on already-deep traceability queries

CLAUDE.md: Phase 12 complete → active workplan cleared (IHF v0.2 complete).

Commit all changes. Mark workplan status: done.