Response System
Core documentation for Amble's response system, including edge scoring and scoped criteria sets.
Response System
The response system allows users and agents to capture structured feedback, assessments, and data points against entities.
Scoped Criteria Sets (Edge Scoring)
Amble supports Scoped Criteria Sets, which allow you to score or evaluate relationships (edges) connected to an entity, rather than just the entity itself. This is often referred to as "Edge Scoring."
Concept
By default, a criteria set has an entity scope ({ type: "entity" }), meaning the dimensions apply directly to the primary record.
With a relation scope ({ type: "relation", fieldKey: "target_markets" }), the evaluation is distributed across the entities connected to the primary record via the specified fieldKey.
When a scoped criteria set is submitted:
- The
valuespayload is a nested matrix keyed by the connected entity ID:{ [connectionId: string]: { [dimensionKey: string]: value } }. - The system resolves all target entities and validates that the dimensions match the criteria set.
- Scores are computed as the mean across all evaluated edges. A 100 on one connection and a 50 on another results in an overall normalized score of 75.
UI Integration
- EdgeMatrixForm: Scoped criteria sets are rendered using a matrix interface where each row represents a connected entity.
- MatrixAggregationView: Relation-scoped responses aggregate into a per-connection matrix view — a
BubbleChartplots each connection on two numeric dimensions (X/Y with optional Z bubble size), backed by a sortable leaderboard that shows each connection's aggregate per dimension plus a weighted score. Users pick the aggregation strategy (mean / median / min / max), toggle a respondent-dot overlay, and everything persists in the URL so chart views are shareable. - Compare Views: Still a fallback for now — captured as a follow-up.
- Promotions: Scoped responses cannot be directly promoted to the primary entity, as they represent data about the relationships, not scalar fields on the entity itself.
Matrix aggregation internals
computeResponseAggregate()infeatures/responses/lib/aggregate-matrix.tsis the pure aggregation function. It walks each response's per-connection value map, collects numeric values per dimension, applies the strategy, and computes a weighted/normalized score per connection using the criteria set's weights and scales. Both the server'saggregateResponses()and the client'sMatrixAggregationViewcall this fn so strategy switches update without a server round-trip.ResponseAggregation.byConnectionis an additive field on the server's aggregate payload. Entity-scope responses leave it undefined; relation-scope responses populate it.- The
BubbleChartcomponent was extended with an optionaloverlayDataprop — a second<Scatter>layer rendered underneath the primary dots at reduced opacity for the respondent overlay.
URL state keys for MatrixAggregationView
x=<dim> y=<dim> z=<dim> agg=mean|median|min|max overlay=1 sort=<dim|score|title> dir=asc|desc — all optional; defaults are the first two numeric dimensions, mean aggregation, overlay off, and score-desc leaderboard sort.
Claim aggregates (source-item → claim rollup)
For graphs where source-items evidence claims (DOC'S and any tenant that reuses the pattern), promoted evidence-quality responses roll up onto every supporting claim as two denormalized keys on entities.content:
source_count— number of unique source-items supporting the claimsource_quality_avg— mean of per-source-item dimension means
Data flow
- A user or agent promotes a source-item's
evidence-qualityresponse → Supabase emitsresponse.promoted. recomputeClaimAggregatesOnResponse(Inngest listener) slug-gates on the source-item entity type +evidence-qualitycriteria set.step.run("list-supported-claims", getSupportedClaimIds)resolves the claim set for that source-item.step.run("fanout", () => inngest.send([...]))emits oneclaim.aggregate.recomputeevent per claim.recomputeClaimAggregateForClaimconsumes each event with per-claim concurrency limit 1. It callsrecomputeSingleClaimAggregate, which re-derivessource_count/source_quality_avgand writes via thepublic.apply_claim_aggregateRPC.
Concurrency contract
recomputeClaimAggregateForClaim declares:
concurrency: [
{ limit: 10 },
{ limit: 1, key: "event.data.tenant_id + ':' + event.data.claim_id" },
]The per-claim limit-1 tier is the correctness contract — two source-items that land on the same claim inside the same Inngest window are serialized end-to-end (read → derive → write), which prevents the stale-aggregate race that burst traffic previously surfaced. The global cap protects Postgres from unbounded write pressure. Inngest SDK 3.x caps the tuple at 2, so a per-tenant fairness tier is tracked as a follow-up.
Atomic write invariant
public.apply_claim_aggregate(p_tenant_id, p_claim_id, p_source_count, p_quality_avg) performs a single row-locked UPDATE entities SET content = content || jsonb_build_object('source_count', ..., 'source_quality_avg', ...). This eliminates the cross-key clobber that the previous TypeScript read-merge-write suffered — any other writer touching content between the SELECT and UPDATE is no longer silently overwritten. The function is SECURITY DEFINER with empty search_path; EXECUTE is revoked from authenticated + anon and granted only to service_role, so direct PostgREST invocation from a user session is impossible. A 0-row result (deleted claim or cross-tenant id) is a graceful no-op — recomputeSingleClaimAggregate returns { updated: false } without throwing and Inngest does not retry.
PR #842 dedupe invariant
recomputeSingleClaimAggregate preserves the newestByEntity Map dedupe pattern from PR #842: when a source-item has multiple promoted responses for the same criteria set (re-promotion after reviewer correction), the newest row wins. Counting every promoted row would double-count that source-item in the claim average. Covered by a dedicated test case in recompute-single-claim-aggregate.test.ts.
Generic across tenants
The listener keys on tenant-configurable slugs via getSourceItemEntityTypeSlug() and getEvidenceQualityCriteriaSlug() (honoring SOURCE_CHILD_TYPE_SLUG / EVIDENCE_QUALITY_CRITERIA_SLUG env overrides). Tenants that use different slugs for the same pattern can set those envs; tenants that use a fundamentally different graph shape can wire their own listener and call recomputeSingleClaimAggregate / apply_claim_aggregate directly.
Unified Response System
One typed submission layer for people and agents — criteria sets define templates, entity_responses preserve versioned evidence, and promotion writes approved values into canonical record fields.
Skills
Reusable instruction modules that agents load on demand, with CRUD management, markdown import, agent assignment, and prompt injection.