phase: 191_source_name_labels_tags
All gates re-verified against a stable tree. Report: **Verified (all 6 tasks in `complete/`; this was the final pass)** - `SourceLabel`+alembic `0030` (dev DB `alembic current` = `0030 (head)`, table present, 0 rows); admin-only `GET/PUT/DELETE /api/source-labels` (`require_admin` router-wide, `resolve_source_name`, partial-update caps/404s); Sources-page "Names, labels and tags" card (read-only derived-name `<code>`, per-row `saveInFlight`, 404 note that survives the settle, chips reuse `.git-source-hidden-count`); `question_name_hits` D2 rule; ONE `all_labels` read per turn in `chat.py` feeding both the `source_name_hit` flag and the prompt surfaces; `source_name_hit_bonus=0.005` in `_selection_order`; ` name=`/` label=` in `build_high_prompt` + `build_overview_prompt(names=…)`. - **Completion criteria:** E2E `tests/e2e/test_source_name_labels.py` ✅ 7 passed (arms a–e: editor all kinds, control/named question, rename-never-breaks-identity, 404 note, reset) · full suite ✅ · coverage ✅ · README/AGENTS ✅ · phase dir in `complete/` ✅ · commit left to the harness. - **Gates:** `uv run pytest --cov=app --cov-report=term-missing` → **exit 0, TOTAL 7160 stmts / 16 missed = 99%** (>90%) · `uv run pytest tests/e2e/test_source_name_labels.py -v --no-cov` → **7 passed, exit 0** · regression E2E isolated, exit 0: `test_document_viewer`, `test_admin_auth`, `test_api_tokens`, `test_git_sources_admin`, `test_kb_tree`, `test_local_directory_sources` · `uv run ruff check .` → All checks passed · `uv run pyright` → 0 errors. - **Battery (task 04 criterion):** `uv run python -m scripts.eval_retrieval --from-file tests/fixtures/retrieval_battery.txt` at default vs `BOR_SOURCE_NAME_HIT_BONUS=0` → **`diff` empty (line-for-line identical)**, matching the recorded arms (dev KB is empty, so every question deflects; the observable boost is unit/E2E-pinned). - **Documentation:** README — fixed the `/git-sources.html` rows (nav label is **Sources**) and cross-linked the phase-191 card from that section; full audit otherwise clean: all 86 `BOR_*` settings documented with defaults matching `app/config.py`, 0 broken file/dir references and 0 broken anchors in README+AGENTS, AGENTS rule 17 re-checked line-by-line against the code. - **Notable / deviations:** (1) a **concurrent pipeline process** (`phased-execution/progress.mjs` for `191_…_00_phase`) was mutating this same working tree mid-pass — it rewrote `tests/e2e/test_source_name_labels.py` (11→7 tests), `frontend/assets/git-sources.js`, `tests/unit/test_source_labels_editor_ui.py` and `AGENTS.md` at 05:27; every gate above was (re-)run after those edits with the tree no source file newer than the green full run. (2) The task-06 report/phase text claims regression suites `test_chat_token_gate.py`, `test_git_sources_page.py`, `test_rag_browser.py` are green — **those files have never existed in git history** (those runs exit 4); I ran the real equivalents listed above instead (phase files are not mine to edit). (3) **No live changes**: the VM DB already has `source_labels` (0 rows) but the deployed app predates this uncommitted code, so deploying `/api/source-labels` to the live service is the owner's post-commit step, not part of this phase's criteria. - **Next pending phase:** none — `191_source_name_labels_tags` is the only entry in `.agents/phases/todo/`.
This commit is contained in:
+259
-4
@@ -4349,9 +4349,16 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
|
||||
.git-source-ignore-count idiom (TEXT + background, never
|
||||
color alone — WCAG 1.4.1: --ink on --bg 16.7:1). Phase 135 (A6):
|
||||
the "branch: <name>" state tag joins the same rule — stored git
|
||||
rows with a branch only, the text IS the signal. */
|
||||
rows with a branch only, the text IS the signal.
|
||||
Phase 191 (task 03): the TWO new tags of the name/label/tags row join
|
||||
the SAME rule — .source-label-state ("named", beside the mono identity)
|
||||
and .source-label-tag (one chip of the Tags token input). That is the
|
||||
reuse the phase asks for: the chips are the house tag, not a new
|
||||
widget, so no tag-input styling is added anywhere below. */
|
||||
.git-source-hidden-count,
|
||||
.git-source-branch-count {
|
||||
.git-source-branch-count,
|
||||
.source-label-state,
|
||||
.source-label-tag {
|
||||
display: inline-block;
|
||||
margin-left: 0.55rem;
|
||||
padding: 0.08rem 0.5rem;
|
||||
@@ -4376,8 +4383,11 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
|
||||
}
|
||||
|
||||
/* Empty state: the tuning page's quiet centered line at full table
|
||||
width — no stored rows AND no env fallback to show. */
|
||||
.git-sources-empty {
|
||||
width — no stored rows AND no env fallback to show. Phase 191 (task
|
||||
03): the name/label/tags list shares the line verbatim (its own
|
||||
no-sources case, the same quiet card). */
|
||||
.git-sources-empty,
|
||||
.source-labels-empty {
|
||||
margin: 0;
|
||||
padding: 1.4rem 1rem;
|
||||
text-align: center;
|
||||
@@ -4389,6 +4399,251 @@ a.answer-image:hover { text-decoration: underline; } /* flat — the caption und
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* ---------- Source names, labels and tags (phase 191, task 03) ----------
|
||||
The Sources page's SECOND list: every source the knowledge base knows
|
||||
(git, local, upload — the union the phase-191 catalogue GET returns),
|
||||
each with the optional name / label / tags the model is told about
|
||||
(D1: an ALIAS — the mono identity cell is read-only text, nothing here
|
||||
renames a document, a citation or a locator).
|
||||
Nothing new is invented: the card is the phase-156 .table-wrap (bounded
|
||||
panel + sticky header, inherited by the class alone), the identity cell
|
||||
is the .git-source-url-cell mono, the state tag and the tag CHIPS are
|
||||
the phase-89 tag rule above, the field boxes are the .ignore-editor-
|
||||
token / #ignore-editor-branch language at the 44px floor, Save is the
|
||||
page's brand pill, Reset and the row's toggle are the neutral
|
||||
.git-source-ignore language, and the dirty mark is the phase-168
|
||||
the phase-168 dirty-row stripe. PALETTE VARIABLES ONLY — no new hue
|
||||
(every pair is a pair this page already measures: ink 15.3:1 and
|
||||
ink-soft 8.2:1 on --surface, brand-ink on brand-soft 12.4:1, the err
|
||||
pair 9.3:1, --bg ink on --brand 5.2:1); :focus-visible rides the global
|
||||
3px outline rule; no CDN, no blur. The editor expands UNDER its own row
|
||||
(one row open at a time keeps a 27-source list scannable) and its three
|
||||
fields stack in a single column below 60rem instead of pushing the card
|
||||
into a horizontal scroll at 375px. */
|
||||
.source-labels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.source-labels-title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.3;
|
||||
color: var(--ink);
|
||||
}
|
||||
/* The alias note (the contract in the owner's words) — the page-sub
|
||||
language, ink-soft on the page canvas (8.2:1 on --surface). */
|
||||
.source-labels-sub {
|
||||
margin: 0;
|
||||
max-width: 60rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
/* The list's own load failure (role=alert) — the .git-source-error
|
||||
pair (err-ink on err-bg 9.3:1). */
|
||||
.source-labels-error {
|
||||
margin: 0;
|
||||
padding: 0.45rem 0.8rem;
|
||||
background: var(--err-bg);
|
||||
color: var(--err-ink);
|
||||
border: 1px solid var(--err-line);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* No min-width on purpose: the editor column has to SHRINK (the fields
|
||||
stack), so this table never scrolls the card sideways at 375px — the
|
||||
panel still bounds vertically (.table-wrap). */
|
||||
.source-labels-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.93rem;
|
||||
}
|
||||
.source-labels-table th, .source-labels-table td {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
padding: 0.7rem 1rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.source-labels-table th {
|
||||
color: var(--brand-ink);
|
||||
font-size: 0.82rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.source-labels-table tbody tr:last-child td { border-bottom: 0; }
|
||||
/* The identity cell: the derived name, mono like the sources table's
|
||||
location cell, wrapping (overflow-wrap) instead of widening the card —
|
||||
it is text, never an input (D1/A1). */
|
||||
.source-labels-table td.source-label-id-cell {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.82rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.source-labels-table td.source-label-id-cell code { font-family: inherit; }
|
||||
/* The dirty row mark — the phase-168 dirty-row stripe (the brand edge is
|
||||
beside the row's own "unsaved changes" TEXT, so the state is never color
|
||||
alone — WCAG 1.4.1). The literal is NOT written in this comment: the
|
||||
phase-168 pin (test_settings_frontend_view.py) regex-matches that
|
||||
selector to its own rule and a comment mention would steal the match. */
|
||||
.source-label-row.is-dirty > td:first-child { box-shadow: inset 3px 0 0 var(--brand); }
|
||||
/* The collapsed row's read-out of the stored label (name · label · tags).
|
||||
textContent-only (a label is owner text — the XSS contract). */
|
||||
.source-label-summary {
|
||||
margin: 0 0 0.45rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.85rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* The row's expand trigger — the .git-source-ignore language (neutral
|
||||
secondary fill, >=44px, hovers to the brand pair 12.4:1). */
|
||||
.source-label-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--ink-soft);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
.source-label-toggle:hover:not(:disabled) {
|
||||
background: var(--brand-soft);
|
||||
color: var(--brand-ink);
|
||||
}
|
||||
/* The expanded editor panel — the row's own form card. The fields go in
|
||||
one column narrow, three across from 60rem (the house grid). */
|
||||
.source-label-editor {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.9rem;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
@media (min-width: 60rem) {
|
||||
.source-label-fields { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
.source-label-fields {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.source-label-field {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
/* The field label — the .ignore-editor-label language (ink-soft 8.2:1 on
|
||||
the panel, 600 weight, the hint span the .field-hint secondary read). */
|
||||
.source-label-field-label {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* The field box — the .ignore-editor-token / #ignore-editor-branch box
|
||||
language VERBATIM (full width, --bg… panel fill, the line border, mono)
|
||||
at the 44px touch floor; the global [hidden] rule and the global
|
||||
:focus-visible outline cover the rest. */
|
||||
.source-label-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 0.55rem 0.65rem;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.85rem;
|
||||
color: var(--ink);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
/* The Tags token input's chip list — the LAYOUT only (the chips
|
||||
themselves are the shared tag rule above, so the widget adds no tag
|
||||
styling). list-style is dropped, so role="list" stays in the markup
|
||||
(the VoiceOver list-semantics idiom the house uses). */
|
||||
.source-label-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin: 0 0 0.45rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.source-label-tags .source-label-tag { margin-left: 0; }
|
||||
.source-label-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
/* Save — the page's brand pill at the row scale (--bg ink on --brand,
|
||||
5.2:1); disabled until the row is dirty, and again while the PUT is
|
||||
out (§7.4 — the row's own in-flight state). */
|
||||
.source-label-save {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
padding: 0.4rem 1.1rem;
|
||||
border: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--brand);
|
||||
color: var(--bg);
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
.source-label-save:hover:not(:disabled) { background: var(--brand-hover); }
|
||||
.source-label-save:disabled { opacity: 0.6; cursor: default; }
|
||||
/* Reset — the neutral secondary button (the .git-source-ignore language):
|
||||
it clears a STORED label, it is not the destructive row Remove. */
|
||||
.source-label-reset {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
padding: 0.35rem 0.9rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--ink-soft);
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 0.82rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.source-label-reset:hover:not(:disabled) {
|
||||
background: var(--brand-soft);
|
||||
color: var(--brand-ink);
|
||||
}
|
||||
.source-label-reset:disabled { opacity: 0.5; cursor: wait; }
|
||||
/* The §7.4 save-state line (role=status): "Saving…" → "saved" / the
|
||||
one-line error note. The hint (unsaved) is the muted italic; the error
|
||||
takes the err pair (9.3:1) so the note reads as a failure, not as a
|
||||
label — TEXT carries it either way, never color alone. */
|
||||
.source-label-status {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.source-label-status.is-hint { font-style: italic; }
|
||||
.source-label-status.is-error {
|
||||
padding: 0.35rem 0.6rem;
|
||||
color: var(--err-ink);
|
||||
background: var(--err-bg);
|
||||
border: 1px solid var(--err-line);
|
||||
border-radius: var(--radius-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---------- History refresh button (phase 77 task 03, TODO.md L3) ----------
|
||||
The #view-history page-head is a flex row: the title block left, the
|
||||
actions slot right (the row wraps below 640px). Scoped to
|
||||
|
||||
Reference in New Issue
Block a user