> ## Documentation Index
> Fetch the complete documentation index at: https://haico.gr/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-0005: User-feedback frame

> One turn-anchored feedback table with a frontend-owned instrument, per-artifact keying, and branch-safe copies, plus admin observability.

# 0005. Model user feedback as one turn-anchored table with a frontend-owned instrument

## Context and problem statement

HAI-Co² is a research instrument, so it needs structured feedback on the co-construction: the reply itself, the objective, plan, artifact(s) and preferences, plus a whole-trajectory reflection and technical reports. Before this change there was no way to give any of it. The hard constraint is that feedback must survive **snapshots and branching** (the same forces as [ADR-0003](/docs/adr/0003-conversation-branching-via-new-thread-and-state-seeding)): a fork must never lose or mis-attribute the prefix it shares with its parent, and the research aggregates must count each genuine opinion exactly once. Two further tensions make the design non-obvious: the question set is a research instrument that will keep changing, and a single turn can produce more than one artifact, each of which the user may rate separately.

## Decision drivers

* **Branch and snapshot safety.** Feedback must line up with the exact key the rest of the system already uses, and forks must inherit their shared prefix without double-counting.
* **An evolvable instrument.** Researchers must be able to add, rename, or re-word questions and options without a backend change or a migration.
* **Per-artifact granularity.** Several artifacts in one turn must be rateable independently, on Postgres and on the SQLite test database alike.
* **One opinion, counted once.** Admin aggregates must reflect genuine opinions only.
* **Admin observability.** An admin must be able to see what happened behind a piece of feedback (the trace, the conversation) to triage it.

## Considered options

1. **Anchor: per-message index, `turn_index`, or a raw feedback id.** Where a feedback row attaches.
2. **Vocabulary: backend enum vs frontend-owned taxonomy.** Where the question/answer set lives and what the backend validates.
3. **Artifact identity: descriptive nullable column vs part of the unique key with a `0` sentinel.** How several artifacts of one turn stay distinct.
4. **Branch feedback: read-through vs copy-on-branch with an `origin` flag.** How a fork shows its inherited prefix feedback.

## Decision outcome

**One unified `feedback` table**, discriminated by `scope` + `aspect`, with these choices:

* **Anchor on `(thread_id, turn_index)`.** This is exactly the key `workspace_snapshots`, `artifacts` and the trajectory-graph nodes already use, and the frontend already maps every assistant message to its turn. Feedback therefore lines up 1:1 with graph nodes and is branch/snapshot-safe by construction.
* **Structural-only backend validation; the instrument lives in the frontend taxonomy.** The backend checks only the shape of a row (a known scope, a well-formed `aspect` slug, and the report/sentiment split). The full vocabulary (questions, options, reasons, conditions) lives only in `frontend/src/lib/feedback-taxonomy.ts`, so the instrument evolves with no backend change or migration.
* **`artifact_id` is part of the unique key, `NOT NULL` with a `0` sentinel.** The unique key is `(thread_id, turn_index, user_id, scope, aspect, artifact_id)`. `0` means "no specific artifact" (every non-artifact row, and a rating of the live document). The sentinel avoids the SQL trap where a nullable column in a unique index treats every `NULL` as distinct, which would silently break per-aspect uniqueness for all the non-artifact rows.
* **Copy-on-branch with an `origin` flag.** On a fork the parent's prefix feedback is copied (`origin='branch_copy'`) so the shared history is visible; admin aggregates count only `origin='user'` so copies never inflate figures; editing a copy promotes it to `origin='user'`, a genuine, independent opinion on that path (and re-stamps its `created_at`). Per-artifact ids are re-pointed through the map `copy_artifacts` returns.

Layered on top, config-gated so they are inert when unset: **admin observability**. Wherever a conversation is listed, a **Phoenix trace deep-link** is resolved once from the Phoenix REST API (spans are grouped by `session.id = thread_id`) and cached on `conversation_users.phoenix_url`; and a report can be filed as a **GitHub issue** carrying the report, the trace link, and the conversation transcript, with the issue URL stored on the row so it is not filed twice.

### Positive consequences

* Feedback survives branching and snapshots with no special read-through logic on the hot path; reads are keyed by the viewed thread's turn.
* The instrument can change UI-side without touching the backend or the schema; there is no vocabulary list to keep in sync.
* Several artifacts per turn are each rated independently, portably across Postgres and SQLite.
* Every genuine opinion is counted once; forks are first-class for new feedback.
* An admin can jump from a piece of feedback to its trace and its conversation, and turn a report into a fully-contextualised issue.

### Negative consequences

* Copy-on-branch duplicates the prefix feedback per fork (cheap rows, but O(branch\_point x forks)); at wide-usage scale this should move to copy-on-write (tracked in #164).
* Promotion semantics ("editing a copy makes it a per-path opinion") are subtle and needed several correctness fixes (mass-promotion, mis-dating, viewer leakage) caught by adversarial validation.
* Structural-only validation means a malformed but well-shaped `aspect`/`category` is accepted; correctness of the vocabulary is the frontend's responsibility.
* Trace links and issue creation depend on external services (Phoenix, GitHub) and configuration; they degrade to no-ops when absent.

## Pros and cons of the options

### Anchor

* **`turn_index`**: **+** identical to the snapshot/artifact/graph key, branch-safe, already mapped on the client. **−** requires the client to know a message's turn (it already does).
* **Per-message index**: **+** trivial to compute. **−** not stable across branches/snapshots; breaks on fork.
* **Raw feedback id**: **+** simplest table. **−** cannot line feedback up with turns/nodes at all.

### Vocabulary

* **Frontend-owned taxonomy**: **+** the instrument evolves with no migration; no cross-file enum to sync. **−** the backend cannot reject an unknown-but-well-formed aspect.
* **Backend enum**: **+** the server validates the exact vocabulary. **−** every wording/option change is a backend deploy and possibly a migration; couples research iteration to backend releases.

### Artifact identity

* **In the key, `0` sentinel**: **+** per-artifact rating, portable, no `NULL` trap. **−** a sentinel value to remember; a new column semantics.
* **Descriptive nullable**: **+** simplest. **−** only one artifact per aspect per turn (the second overwrites the first).

### Branch feedback

* **Copy-on-branch + `origin`**: **+** the fork shows inherited feedback, no read-through, no double-count, promotable per path. **−** duplicates data; promotion semantics are subtle.
* **Read-through lineage**: **+** no duplication. **−** every read walks the ancestry; copy-on-write on edit; more moving parts (revisited in #164).

## Links

* Related ADRs: [0003-conversation-branching-via-new-thread-and-state-seeding](https://github.com/petrosrapto/HAICO/blob/main/docs/adr/0003-conversation-branching-via-new-thread-and-state-seeding.md)
* Related issues / PRs: #161 (the feature), #162 (the PR), #164 (branch copy-on-write follow-up), #40 (artifact object-storage follow-up)
* Reference: [User feedback](/docs/feedback), [Database schema](/docs/database-schema)
