phase: 113_source_chip_quality
All gates green — no defects found; this pass was verification only. **Phase 113 final verification pass — report** - Verified (no code changes needed): `select_documents_tiered` cited/related tiering + `select_documents` wrapper, `TurnPlan.related_docs`, `ChatDoneEvent.related` (additive, old payloads parse), `appendRelated` UI row (`.related-doc`, never `.source-chip`), done-frame + restore-path wiring, two settings with validators, `.env.example` entries - `uv run pytest --cov=app --cov-report=term-missing` → 2422 passed, app/ coverage **99%** (>90% gate) - `uv run pytest tests/e2e/test_source_chip_quality.py -v --no-cov` (isolated) → 2 passed - Regression E2E `test_retrieval_quality.py` + `test_honest_deflection.py` + `test_chat_rag.py` + `test_sources_midstream_bug.py` → 17 passed - `uv run ruff check . && uv run pyright` → clean (0 errors); `bash .agents/validate.sh` → "validation OK" Completion criteria: 1. Single-doc question → exactly one `.source-chip` (E2E): ✅ passed 2. Weak 2nd doc only in de-emphasized related row, never `.source-chip` (unit + E2E): ✅ passed 3. Deflected turn → zero citation chips, weak hits in related row: ✅ passed 4. Full suite green, coverage >90%, isolated E2E green, lint/types clean: ✅ passed 5. `--no-gpg-sign` commit + phase dir move: left to harness per pass rules (task files already in `complete/`) No deviations. Next pending phase: `114_embed_question_length`.
This commit is contained in:
@@ -1395,6 +1395,51 @@ function appendSources(wrap, sources) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 113 (task 02): the DE-EMPHASIZED related-docs row — the done
|
||||
* frame's second tier (phase 113 task 01): documents that scored but
|
||||
* did not clear the usefulness bar. It must never read as a citation:
|
||||
* the links carry the .related-doc class (NOT .source-chip — the
|
||||
* citation surface stays appendSources' alone) while behaving exactly
|
||||
* like the chips — the same documentUrl href (the /document.html escape
|
||||
* hatch) and the same left-click → same-page modal (phase 26). The
|
||||
* labeled row ("Nearby docs, in case:") joins the bubble's .msg-meta
|
||||
* family and stacks below the citation row in the .msg-body flex gap.
|
||||
* app.js appends it LAST among the meta rows — after appendTuneButton /
|
||||
* appendSaveAsDocButton / appendRetryButton claimed the FIRST
|
||||
* .msg-meta row — so a deflected turn (no citation row) never lets a
|
||||
* meta action join this row. Empty/absent input → no DOM at all
|
||||
* (pre-phase saved chats carry no `related` — the row is simply absent).
|
||||
*/
|
||||
function appendRelated(wrap, related) {
|
||||
if (!related || !related.length) return;
|
||||
const body = wrap.querySelector(".msg-body");
|
||||
const row = document.createElement("div");
|
||||
row.className = "msg-meta related-docs";
|
||||
row.setAttribute("role", "list");
|
||||
row.setAttribute("aria-label", "Nearby docs, in case");
|
||||
const label = document.createElement("span");
|
||||
label.className = "related-docs-label";
|
||||
label.textContent = "Nearby docs, in case:";
|
||||
row.appendChild(label);
|
||||
for (const s of related) {
|
||||
const docLabel = `${s.source}/${s.path}`;
|
||||
const link = document.createElement("a");
|
||||
link.className = "related-doc";
|
||||
link.setAttribute("role", "listitem");
|
||||
link.href = documentUrl(s.source, s.path, "/"); // back → the chat page
|
||||
link.addEventListener("click", (e) => {
|
||||
e.preventDefault(); // no new tab (phase 26) — the modal takes over
|
||||
e.stopPropagation();
|
||||
openDocumentModal(s.source, s.path, link);
|
||||
});
|
||||
link.textContent = docLabel;
|
||||
link.title = docLabel; // full path as the native tooltip (chip pattern)
|
||||
link.setAttribute("aria-label", docLabel); // the accessible name is the full path
|
||||
row.appendChild(link);
|
||||
}
|
||||
body.appendChild(row);
|
||||
}
|
||||
|
||||
/* "Maybe try:" chips under a deflected bubble (honesty gate, phase 04,
|
||||
shared component + one-tap submit, phase 05). The group is accessible
|
||||
(role=list + aria-label) and wraps cleanly at every width. */
|
||||
@@ -1565,6 +1610,11 @@ function renderStoredMessage(m) {
|
||||
// — no button (the live stop path adds none either).
|
||||
if (!m.stopped) appendSaveAsDocButton(wrap, m.text);
|
||||
if (m.stopped) appendStoppedNote(wrap); // phase 48: the stop marker restores
|
||||
// Phase 113 (task 02): the related tier restores with the bubble
|
||||
// (LAST — after the meta-row claimers, exactly like the live done
|
||||
// path). Pre-phase records carry no `related` → appendRelated
|
||||
// no-ops and the row is simply absent (graceful).
|
||||
appendRelated(wrap, m.related);
|
||||
lastBrainWrap = wrap; // phase 49: the LAST restored brain bubble wins
|
||||
}
|
||||
|
||||
@@ -2433,6 +2483,15 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
appendMaybeTry(wrap, ev.suggestions);
|
||||
}
|
||||
appendSources(wrap, ev.sources);
|
||||
// Phase 113 (task 02): the cited tier stays the citation
|
||||
// surface (the chips above); the related tier (scored docs
|
||||
// under the usefulness bar — and the weak hits of a DEFLECTED
|
||||
// turn, whose `ev.sources` is empty → zero chips) renders as
|
||||
// the de-emphasized labeled row, appended LAST among the meta
|
||||
// rows below (after the appendTuneButton/SaveAsDoc/Retry
|
||||
// claimers took the first .msg-meta row, so an action never
|
||||
// joins the related row — a deflected turn with related docs
|
||||
// still gets its own meta row for the buttons).
|
||||
// Thinking-without-answer (reasoning can exhaust max_tokens): the
|
||||
// bubble gets the empty-answer fallback — what the user saw is
|
||||
// what gets persisted.
|
||||
@@ -2457,10 +2516,16 @@ async function runTurn(text, { reask = false } = {}) {
|
||||
tools: toolAcc.length ? toolAcc : undefined,
|
||||
deflected: !!ev.deflected,
|
||||
sources: ev.sources,
|
||||
// Phase 113 (task 02): the related tier persists with the
|
||||
// turn (undefined drops the key from the JSON — the house
|
||||
// optional-meta pattern), so the restore path re-renders the
|
||||
// row exactly as it looked live.
|
||||
related: ev.related?.length ? ev.related : undefined,
|
||||
suggestions: ev.suggestions,
|
||||
}, leavePartialIndex);
|
||||
lastBrainWrap = wrap; // this bubble is now the last brain answer
|
||||
markLastRetryable(); // phase 49: the Retry button is last-bubble-only
|
||||
appendRelated(wrap, ev.related); // phase 113 (task 02) — LAST meta row
|
||||
} else if (ev.type === "tool_result") {
|
||||
// Phase 95 (A15 extension, task 02): the truncation the LLM is
|
||||
// told about is told to the USER. One frame per truncated read,
|
||||
|
||||
@@ -692,6 +692,51 @@ details.thinking .thinking-text ul { margin: 0 0 0.5rem; }
|
||||
}
|
||||
.source-chip:hover { background: var(--brand-soft); text-decoration: underline; }
|
||||
|
||||
/* Phase 113 (task 02): the related-docs row — the SECONDARY tier of
|
||||
scored docs (phase 113 task 01's usefulness bar demotes the
|
||||
sub-floor hits out of the citation surface; on a deflected turn the
|
||||
weak hits live HERE and the chip row is absent). It must never read
|
||||
as a citation: smaller mono links, DASHED border (the solid brand
|
||||
pill is the citation look), transparent fill, muted --ink-soft text
|
||||
— 8.6:1 on --bg (verified ≥4.5:1, WCAG 2.1 AA), palette variables
|
||||
only (phase-92 zero-literal: no new hue, the monochrome theme grays
|
||||
it automatically). The row joins .msg-meta (the .msg-body flex gap
|
||||
stacks it below the citation row — no extra margin) but the meta
|
||||
ACTIONS never do: app.js appends it last, after the first-row
|
||||
claimers. :focus-visible via the global 3px outline rule. The hover
|
||||
is deliberately flat — NO background swap (the chip's brand-soft
|
||||
hover is the citation affordance): only the ink-soft → ink step-up
|
||||
plus the underline. */
|
||||
.related-docs-label {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.related-doc {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.7rem;
|
||||
color: var(--ink-soft);
|
||||
background: transparent;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 0.12rem 0.55rem;
|
||||
text-decoration: none;
|
||||
max-width: 100%;
|
||||
/* min-width: 0 so the nowrap pill ellipsizes (same as the chips) */
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.related-doc:hover {
|
||||
color: var(--ink); /* ink on --bg 16.7:1 — the flat secondary hover */
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* "Maybe try" chips under a deflected bubble (phase 04). Unlike the
|
||||
onboarding row (which scrolls horizontally on mobile), this group wraps
|
||||
at every width: the chips are the actionable follow-up, not decoration.
|
||||
|
||||
Reference in New Issue
Block a user