Compare commits

...
2 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 699a4cd986 docs(6208): ProjectView, KnowledgeView, SnippetListView and components.css comments drop citations and stories
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m10s
CI & Build / Python tests (push) Successful in 1m54s
CI & Build / Build & push image (push) Successful in 52s
components.css says why shared recipes live there without the five-copy
.btn-primary count; KnowledgeView's editor-routing and chip comments state
the present reason instead of the bug that once routed snippets wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:10:45 -04:00
bvandeusenandClaude Opus 5.5 637ec66b79 docs(6208): SettingsView comments state the reason, not the instance that taught it
Record and milestone citations go; the ACT-arm bar split and the per-arm
budgets state why they exist without the measurements that motivated them.
A comment about a removed think_enabled setting, pointing at a file that no
longer exists, is deleted, as is the story of a removed design-system picker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:09:53 -04:00
5 changed files with 74 additions and 100 deletions
+11 -16
View File
@@ -3,11 +3,10 @@
*
* WHY THIS FILE EXISTS
*
* `.btn-primary` was defined five times, in five scoped stylesheets, and all
* five had drifted: three paddings, three font sizes, three disabled opacities,
* and one view with no disabled style at all (#2273). Nothing detected that,
* because a scoped duplicate is invisible to every tool — it isn't a rule
* violation, isn't a broken reference, and isn't a recorded snippet.
* A recipe copied into several scoped stylesheets drifts — paddings, font
* sizes, disabled states — and nothing detects it, because a scoped duplicate
* is invisible to every tool: it isn't a rule violation, isn't a broken
* reference, and isn't a recorded snippet. Shared recipes live here instead.
*
* GEOMETRY LIVES HERE. Every value is a design-system token, so a palette or
* scale change moves the buttons rather than stranding a copy that no longer
@@ -301,9 +300,7 @@
/* ── Page container ─────────────────────────────────────────────────────────
The one wrapper a top-level view sits in: page width from the layout
tokens, centred, clipped horizontally so a wide child (a kanban, a table)
scrolls inside itself instead of the page. ProjectListView, ProjectView and
SnippetListView each carried this rule under their own name until #2903
(milestone 299). */
scrolls inside itself instead of the page. */
.page-container {
max-width: var(--fs-layout-page-max);
margin: 2rem auto;
@@ -311,12 +308,11 @@
overflow-x: clip;
}
/* ── Form input (fs-surfaces, snippet #2336) ────────────────────────────────
/* ── Form input (fs-surfaces) ───────────────────────────────────────────────
Inputs sit DARKER than the page they're on — an inset well rather than a
raised panel; that inversion is what makes a field read as writable. The
design system's recipe, verbatim; width/box-sizing stay the caller's
(an inline select and a full-width textarea differ there). Three scoped
copies of an older input recipe were folded into this in #2903. */
(an inline select and a full-width textarea differ there). */
.fs-input {
background: var(--fs-surface-page);
border: var(--fs-border);
@@ -331,8 +327,8 @@
.fs-input:focus { outline: none; box-shadow: var(--fs-focus-ring); }
.fs-input:disabled { opacity: var(--fs-disabled-opacity); cursor: not-allowed; }
/* Page scaffold + feedback text recipes (milestone 302, note 2917): name
families the consumer map showed to be one recipe living in many views.
/* Page scaffold + feedback text recipes: families that are one recipe
living in many views.
A view keeps only its deviation as a scoped remainder/override. */
.page-header {
display: flex;
@@ -355,9 +351,8 @@
/* --- usage badge ----------------------------------------------------------
"surfaced N×, opened M×" on a list row, for any record kind the retrieval
surfaces can choose: snippets and notes from note_usage_events, rules from
rule_usage_events. Promoted here from SnippetListView's scoped block when
the rule list needed the same chip (milestone 333 step 5) — a second scoped
copy is how the ninth duplicated CSS family starts (#3207).
rule_usage_events. Shared here because a second scoped copy is how a
duplicated CSS family starts.
Geometry and colour only. A view keeps its own spacing as a scoped
remainder, the way it does for every other recipe in this file. */
+14 -23
View File
@@ -48,7 +48,7 @@ interface KnowledgeItem {
created_at: string;
updated_at: string;
/** Surfaced-vs-opened counts, zero-filled by the route for EVERY row, so
* "never surfaced" is a value here rather than a missing field (#4230).
* "never surfaced" is a value here rather than a missing field.
* `UsageBadge` renders nothing at all below one surfacing. */
usage?: RecordUsage | null;
// Set only when another user owns this record — their suggestion, not one of
@@ -69,10 +69,9 @@ interface KnowledgeItem {
// a task KIND (`task` for any, else issue / spike) — because that is what this
// feed actually holds.
//
// `plan` is still a valid facet at the API, for the 90 legacy plan-tasks, but
// it has no chip: retired in 0066, it kept a chip of its own for longer than
// `issue` — 17% of every task here — went without one (#3128). Those rows are
// still reachable under Tasks, wearing a Plan badge.
// `plan` is still a valid facet at the API, for legacy plan-tasks, but it has
// no chip: plans are milestones now. Those rows are still reachable under
// Tasks, wearing a Plan badge.
type Facet = "" | "note" | "task" | "issue" | "spike" | "snippet" | "process" | "lesson";
// The facets that select TASKS. Kinds are subsets of `task`, so any of them
@@ -93,7 +92,7 @@ const FACET_CHIPS: [Exclude<Facet, "">, string][] = [
// The sweep is cross-cutting — a note that has gone false does not care which
// facet it sits under — so it REPLACES the browse list rather than filtering
// it. Filtering would mean the answer depended on which chip was active, which
// is the under-reporting the sweep exists to prevent (milestone 317 step 4).
// is the under-reporting the sweep exists to prevent.
const sweepActive = ref(false);
// ─── Filter state ─────────────────────────────────────────────────────────────
@@ -201,8 +200,8 @@ const items = ref<KnowledgeItem[]>([]);
// True only when a search actually returned matched passages — not merely when
// the box has text in it. A keyword-only result set, or an embedder that is
// down, carries `body_opening` rows, and claiming otherwise would be the same
// species of lie this whole change is about (#4243).
// down, carries `body_opening` rows, and labelling those as matched passages
// would misstate why the row is there.
const showsMatchedPassages = computed(
() =>
searchQuery.value.trim().length > 0 &&
@@ -351,12 +350,8 @@ function isOverdue(item: KnowledgeItem): boolean {
return new Date(item.due_date) < new Date(new Date().toDateString());
}
// Each record kind opens in ITS OWN editor. A snippet used to fall through to
// /notes/:id, whose save is a plain PATCH of the body — which left the snippet's
// derived `data` mirror describing the previous version (#3128). The service now
// recomposes the mirror either way, so this is no longer the guard; it is simply
// that the note editor cannot edit a snippet's signature, language or locations,
// and offering it as the way in was always wrong. Processes stay here on
// Each record kind opens in ITS OWN editor: the note editor cannot edit a
// snippet's signature, language or locations, so it is not the way in. Processes stay here on
// purpose: they have no editor of their own and the note editor knows the type.
function openItem(item: KnowledgeItem) {
if (item.note_type === 'task') {
@@ -666,7 +661,7 @@ onUnmounted(() => {
class="shared-tag"
:title="`Shared by ${item.owner ?? 'another user'} — their record, not yours`"
>by {{ item.owner ?? "another user" }}</span>
<!-- Surfaced-vs-opened (#4230). This is the only list in the UI
<!-- Surfaced-vs-opened. This is the only list in the UI
that browses notes and lessons, so it is the only place
those two kinds can show the counter at all. The advice is
looked up per row because this feed is mixed — the remedy
@@ -1015,9 +1010,7 @@ onUnmounted(() => {
.badge--note { background: color-mix(in srgb, var(--fs-accent) 15%, transparent); color: #7A6DA8; }
.badge--task { background: rgba(212,160,23,0.15); color: #fbbf24; }
.badge--plan { background: rgba(99,102,241,0.18); color: #818cf8; }
/* Snippet and process are NEUTRAL on purpose. Both were unstyled — and the
snippet had no label either, so all 90 of them rendered an empty chip in
this feed (#3128). Giving them hues would put a third and fourth colour
/* Snippet and process are NEUTRAL on purpose. Giving them hues would put a third and fourth colour
beside KindBadge's warm/cool pair on the same card; a record type that
isn't an alarm reads better as plain. Standard body pair, so the contrast
is the one the palette already guarantees. */
@@ -1158,11 +1151,9 @@ onUnmounted(() => {
border-bottom: 1px solid var(--fs-border-color);
flex-shrink: 0;
}
/* RESTORED (#2444). The panel is a flex COLUMN and its header is
`flex-shrink: 0`, so this is the item that takes the remaining height — and
without it the `height: 100%` below resolves against `auto` and does
nothing, which made the comment underneath a spec for a rule that could not
work. `min-height: 0` is the companion that lets a flex item shrink under
/* The panel is a flex COLUMN and its header is `flex-shrink: 0`, so this is
the item that takes the remaining height — without it the `height: 100%`
below resolves against `auto` and does nothing. `min-height: 0` is the companion that lets a flex item shrink under
its content instead of overflowing the panel. */
.graph-embed {
flex: 1;
+12 -12
View File
@@ -96,7 +96,7 @@ const tasksStore = useTasksStore();
const project = ref<Project | null>(null);
// Design system the project is styled from. Loaded separately because an
// install with none is the ordinary case (rule #115) and the picker simply
// install with none is the ordinary case and the picker simply
// doesn't render — a failed fetch must not take the project page with it.
const designSystems = ref<DesignSystem[]>([]);
const editDesignSystemId = ref<number | null>(null);
@@ -189,8 +189,8 @@ function groupKey(group: MilestoneGroup): number {
}
// A group's Done column starts COLLAPSED, to its header and count. Done work is
// the part of a board nobody is reading, and listed in full it pushed the open
// columns of every group below it off the screen (operator, 2026-09-15). Not
// the part of a board nobody is reading, and listed in full it pushes the open
// columns of every group below it off the screen. Not
// persisted, matching the milestone collapse beside it: each load starts from
// the same defaults.
const expandedDone = ref<Set<number>>(new Set());
@@ -418,7 +418,7 @@ async function loadTasks() {
}
}
} catch {
// Say so. This used to swallow the error and leave an empty board, which is
// Say so. A swallowed error leaves an empty board, which is
// indistinguishable from a project with no tasks — the same "hidden with no
// indicator" failure as the truncation above, one layer up.
tasksError.value = "Could not load tasks. Refresh to try again.";
@@ -465,7 +465,7 @@ async function loadNotes() {
}
}
/* ── Pattern-library coverage (#2692) ─────────────────────────── */
/* ── Pattern-library coverage ─────────────────────────────────── */
interface CoverageGap {
dir: string;
@@ -488,11 +488,11 @@ interface Coverage {
computed_at: string;
repos: { repo: string; ref: string; total: number; accounted: number }[];
largest_gaps: CoverageGap[];
// The mechanical proposer's standing (#2792): canon proposals awaiting an
// The mechanical proposer's standing: canon proposals awaiting an
// agent's confirm, and the biggest repeats-with-no-canon families.
proposed?: number;
derive_groups?: DeriveGroup[];
// The divergence readout (#2793): button B where button A is canon, and
// The divergence readout: button B where button A is canon, and
// judged shapes whose bodies moved since they were judged.
divergent?: number;
divergence?: Divergence[];
@@ -543,7 +543,7 @@ async function refreshCoverage() {
}
}
/* ── Forge pin (#2778) — which of the OWNER's connections serves this
/* ── Forge pin — which of the OWNER's connections serves this
project. Owner-only UI: the select lists the viewer's own keyring, which
is only the eligible set when the viewer IS the owner. ── */
@@ -737,7 +737,7 @@ async function confirmDelete() {
</p>
</div>
<!-- Inception (milestone 297): the owner of an undecided project is asked
<!-- Inception: the owner of an undecided project is asked
what it inherits; once recorded, one line says what was decided. -->
<InceptionCard
v-if="project.inception == null && isProjectOwner"
@@ -779,7 +779,7 @@ async function confirmDelete() {
</div>
</div>
<!-- Pattern-library coverage (#2692) — only when a forge is configured;
<!-- Pattern-library coverage — only when a forge is configured;
forge-less installs never see this card at all. -->
<div v-if="coverageConfigured" class="coverage-card">
<div class="coverage-head">
@@ -874,7 +874,7 @@ async function confirmDelete() {
the shape ledger and reports how many are classified against canon.
</p>
<p v-if="coverageError" class="coverage-error">{{ coverageError }}</p>
<!-- Forge pin (#2778): owner-only, because the eligible set is the
<!-- Forge pin: owner-only, because the eligible set is the
owner's own keyring. Automatic = resolve by repo host. -->
<div v-if="isProjectOwner && forgeOptions.length" class="coverage-forge-row">
<label for="forge-pin" class="coverage-gaps-label">Forge</label>
@@ -1200,7 +1200,7 @@ async function confirmDelete() {
:design-system-id="project.design_system_id ?? null"
/>
<!-- Family tab (milestone 463): which platforms this project is. -->
<!-- Family tab: which platforms this project is. -->
<ProjectFamilyTab
v-if="activeTab === 'family'"
:project-id="projectId"
+35 -47
View File
@@ -20,7 +20,7 @@ const store = useSettingsStore();
const authStore = useAuthStore();
const toastStore = useToastStore();
// ── Shared areas (milestone 307) ────────────────────────────────────────
// ── Shared areas ────────────────────────────────────────────────────────
// The global vocabulary a project's Systems map onto. Admin-only to WRITE —
// a global list anyone can extend stops being a shared list — but every user
// reads it, which is why the catalog lives in a store rather than here.
@@ -75,7 +75,7 @@ async function saveArea() {
savingArea.value = false;
}
}
// ── Platforms (milestone 463) ───────────────────────────────────────────
// ── Platforms ───────────────────────────────────────────────────────────
// The global list of what a project can be built on or ship as. Same split
// as the areas: admin-only to write, readable by everyone. Markers are the
// repo-relative globs that let a coverage refresh DETECT a platform; they
@@ -157,46 +157,43 @@ const kbInjectThreshold = ref("0.55");
const kbInjectTopK = ref("3");
const kbWritePathEnabled = ref(true);
// The write-path arm's OWN threshold, stricter than auto-inject's 0.55 above:
// code embeddings sit on a much higher similarity floor than prose, so 0.55 let
// unrelated code through (#2223). Shares top-k, not the threshold.
// code embeddings sit on a much higher similarity floor than prose, so 0.55
// lets unrelated code through. Shares top-k, not the threshold.
const kbWritePathThreshold = ref("0.68");
const kbRuleHintThreshold = ref("0.72");
const kbCheckpointThreshold = ref("0.80");
// The two ACT arms no longer share a bar (#3853). A write-path query is a code
// payload; a pre-tool query is a shell command, often under a dozen words —
// less text, less signal, lower scores for the same relevance. Measured at one
// shared 0.72 the two behaved like different subsystems: the write-path arm
// spoke on 37% of its calls, the command arm on 2% of 11,768.
// The two ACT arms do not share a bar. A write-path query is a code payload; a
// pre-tool query is a shell command, often under a dozen words — less text,
// less signal, lower scores for the same relevance — so one shared bar makes
// them behave like different subsystems.
const kbToolRuleThreshold = ref("0.68");
// And the prompt boundary is a third query shape again — the operator's own
// prose rather than anything a tool produced (#3852).
// prose rather than anything a tool produced.
const kbPromptRuleThreshold = ref("0.72");
// The reply arm (milestone 458): its bar is the bar at which a finished reply
// The reply arm: its bar is the bar at which a finished reply
// is HELD for one read, so it sits with the checkpoint, not with the hints.
const kbReplyRuleThreshold = ref("0.8");
// The BUDGETS, one per arm (#4102). Until this step only auto-inject had one
// and every other arm's ceiling was a module constant nobody could reach — so
// the only control an operator had over a noisy surface was to raise its bar,
// which discards the surface's best candidates along with its worst. A budget
// keeps the top of the ranking and drops the tail, which is what was wanted.
// The BUDGETS, one per arm. Without one, the only control over a noisy surface
// is raising its bar, which discards the surface's best candidates along with
// its worst. A budget keeps the top of the ranking and drops the tail.
const kbWritePathTopK = ref("3");
const kbRuleHintTopK = ref("5");
const kbToolRuleTopK = ref("5");
const kbPromptRuleTopK = ref("3");
const kbReplyRuleTopK = ref("1");
// What has been changed about retrieval, newest first — the review surface for
// changes the model made on the operator's behalf (#4102).
// changes the model made on the operator's behalf.
const tuningEvents = ref<TuningEvent[]>([]);
const loadingTuning = ref(false);
// Near-duplicate report floors, one per record kind (services/dedup.py).
// Snippets are single-chunk, so their floor sits below the 0.90 write-time
// gate and catches what it lets through. Notes/tasks are scored at chunk
// grain (#280) — related families clear 0.90 easily — so their floor sits
// grain — related families clear 0.90 easily — so their floor sits
// above the gate to keep the report pointed at genuinely-alike records.
const kbDupThresholdSnippet = ref("0.82");
const kbDupThresholdNote = ref("0.93");
const kbDupThresholdTask = ref("0.93");
// The create gate's bars (#4385). Defaults mirror services/dedup.py.
// The create gate's bars. Defaults mirror services/dedup.py.
const kbGateThreshold = ref("0.9");
const kbGateThresholdSnippet = ref("0.96");
const kbGateThresholdLesson = ref("0.96");
@@ -207,11 +204,6 @@ const kbPlanMatchThreshold = ref("0.80");
const savingKbInject = ref(false);
const kbInjectSaved = ref(false);
// think_enabled setting removed 2026-05-23. The chat+curator architecture
// has tools=[] on the chat model; think on a no-tools conversational pass
// is pure latency cost. See generation_task.py:run_generation comment for
// the full reasoning.
function detectTimezone() {
userTimezone.value = Intl.DateTimeFormat().resolvedOptions().timeZone;
}
@@ -246,7 +238,7 @@ async function saveRetention() {
}
}
// One entry in the retrieval tuning trail (#4102). Mirrors
// One entry in the retrieval tuning trail. Mirrors
// RetrievalTuningEvent.to_dict() — `old_value` is null for the first change to
// a surface, which is a different event from moving off a value somebody chose
// and renders differently below.
@@ -262,7 +254,7 @@ interface TuningEvent {
}
// What a dial's number was measured against, and whether that space has moved
// (#4104). Mirrors the `calibration` block `current_settings` returns per dial.
// Mirrors the `calibration` block `current_settings` returns per dial.
// `stale` is nullable on purpose: a dial moved before Scribe recorded stamps is
// UNKNOWN rather than fine, and rendering the two the same would hide the case
// most likely to be wrong.
@@ -335,7 +327,7 @@ async function loadTuningHistory() {
}
}
/** Who moved a dial. Three answers, not two (#4225).
/** Who moved a dial. Three answers, not two.
*
* `release` means the shipped default itself changed between versions — no
* user turned anything. Folding that into the `human ? 'you' : 'Claude'`
@@ -445,9 +437,8 @@ async function saveKbInject() {
// corpus confident enough to be read first" — and a number that moves
// when another moves is a number nobody can reason about.
kb_checkpoint_threshold: String(cpT),
// A FOURTH and FIFTH bar, and they are separate keys on purpose: the
// whole finding of #3853 is that one number cannot serve arms whose
// queries are different shapes. Moving one must not move the others.
// A FOURTH and FIFTH bar, and they are separate keys on purpose: one
// number cannot serve arms whose queries are different shapes. Moving one must not move the others.
kb_toolrule_threshold: String(trT),
kb_promptrule_threshold: String(prT),
// The reply backstop's own bar: it HOLDS a reply, so like the
@@ -497,15 +488,15 @@ const exporting = ref(false);
const restoring = ref(false);
// Backup, export and restore walk the whole store, so they are slow BY DESIGN
// and the client's ordinary 30s default would cut them off mid-work. They are
// still bounded: rule 156 asks for a deadline, not a short one, and "no ceiling
// at all" is what leaves a restore that died server-side spinning forever.
// still bounded — every wait has a deadline, not necessarily a short one — and
// "no ceiling at all" is what leaves a restore that died server-side spinning forever.
const BULK_TRANSFER_TIMEOUT_MS = 10 * 60 * 1000;
function bulkDeadline(): AbortSignal {
return AbortSignal.timeout(BULK_TRANSFER_TIMEOUT_MS);
}
// ── What's running (#3127 checklist 12) ─────────────────────────────────
// ── What's running ──────────────────────────────────────────────────────
// Three states kept apart, because collapsing any two of them is the defect
// this readout exists to remove: `null` + no error = not asked yet (the Config
// tab has not been opened); a payload = answered, with each ABSENT field shown
@@ -822,7 +813,7 @@ const baseUrl = ref("");
const savingBaseUrl = ref(false);
const baseUrlSaved = ref(false);
// Git forge connections (#2778) — the user's keyring: one read-only
// Git forge connections — the user's keyring: one read-only
// credential per forge host, used server-side for every forge read on
// projects this user owns. The token round-trips masked; the server treats
// the mask as "unchanged". Tokens are stored sealed under SECRET_KEY:
@@ -1996,12 +1987,12 @@ async function deleteUser(userId: number) {
/>
<p class="field-hint">How many of the best-scoring rules a reply is checked against (1–10). Only the best can hold the reply; the rest are counted, which is what tells you how close the others came.</p>
</div>
<!-- THE REVIEW SURFACE (#4102). These numbers are maintained by the
<!-- THE REVIEW SURFACE. These numbers are maintained by the
model that uses them: it reads which records each bar refused and
moves the dial with the argument attached. This panel is the other
half of that bargain — a change made on your behalf is one you can
read, disagree with, and set back by hand above. -->
<!-- STALENESS (#4104). A bar is a cosine similarity, which only means
<!-- STALENESS. A bar is a cosine similarity, which only means
something inside one embedding model's geometry over documents cut
one particular way. Change either and every number above keeps
applying while describing nothing. Shown ONLY when something has
@@ -2067,10 +2058,7 @@ async function deleteUser(userId: number) {
</div>
<!-- A design system belongs to a PROJECT, and the picker for it lives on
the project. There was a setting here that designated the system
this install's own interface was built from; it only ever described
the app you were already looking at, which is not what the feature
is for (#274). -->
the project, not here. -->
<div class="field">
<label for="kb-duplicate-threshold-snippet">Near-duplicate report threshold — snippets</label>
@@ -3712,7 +3700,7 @@ async function deleteUser(userId: number) {
color: var(--fs-text-primary);
}
/* An absent field reads as absent — never as a blank, and never styled to look
like a value it does not have (#3127 checklist 12). */
like a value it does not have. */
.version-grid dd.version-unknown {
font-family: inherit;
font-style: italic;
@@ -3760,7 +3748,7 @@ async function deleteUser(userId: number) {
margin-bottom: 0.35rem;
color: var(--fs-text-primary);
}
/* remainder over .fs-input (components.css, canon #2336; m302) */
/* remainder over .fs-input (components.css) */
.input {
width: 100%;
box-sizing: border-box;
@@ -4127,7 +4115,7 @@ async function deleteUser(userId: number) {
.cell-duration { color: var(--fs-text-tertiary); font-size: 0.8rem; white-space: nowrap; }
.text-error { color: var(--fs-error); }
/* Bare by design: a `<tr>` has nothing to style
that its cells don't carry (#2444). */
that its cells don't carry. */
.detail-row td { padding: 0 0.75rem 0.75rem; border-bottom: 1px solid var(--fs-border-color); }
.detail-ip { font-family: monospace; font-size: 0.8rem; color: var(--fs-text-tertiary); margin-bottom: 0.4rem; }
.detail-json {
@@ -4495,7 +4483,7 @@ async function deleteUser(userId: number) {
font-weight: 500;
}
/* ── Shared areas (milestone 307) ──────────────────────────────────
/* ── Shared areas ──────────────────────────────────────────────────
The slug is shown deliberately: it is what decides whether two names are
the same area, and an admin renaming an entry needs to see it move. */
.area-admin-list { list-style: none; margin: 1rem 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--fs-space-2); }
@@ -4527,7 +4515,7 @@ async function deleteUser(userId: number) {
.area-admin-markers { font-family: var(--fs-font-mono); font-size: 0.82rem; }
.area-admin-desc .area-admin-slug { margin-right: 0.25rem; }
/* The retrieval tuning trail (#4102). Reads as a record, not a control panel:
/* The retrieval tuning trail. Reads as a record, not a control panel:
the operator is reviewing what was done, and the reason is the part worth
reading, so it gets the full-width line under a compact header row. */
.tuning-history {
@@ -4581,7 +4569,7 @@ async function deleteUser(userId: number) {
color: var(--fs-accent);
border-color: var(--fs-accent);
}
/* A shipped default that moved between releases (#4225). Muted rather than
/* A shipped default that moved between releases. Muted rather than
accented: it is the answer to "did I do this?" being NO for both of the
other two, and it wants to be legible without competing with the changes
somebody actually made. */
@@ -4595,7 +4583,7 @@ async function deleteUser(userId: number) {
font-size: 0.85rem;
color: var(--fs-text-secondary);
}
/* Stale calibration (#4104). Tinted rather than loud: the numbers still work,
/* Stale calibration. Tinted rather than loud: the numbers still work,
they have merely stopped being measurements — that is a "come back to this",
not an outage. It only renders when something is actually stale, which is
what earns it the tint at all. */
+2 -2
View File
@@ -42,7 +42,7 @@ function toggleLocationFilter() {
if (!showLocationFilter.value && locationActive.value) clearLocation();
}
// Drift check (#2086) — "attention" is everything whose recorded location or
// Drift check — "attention" is everything whose recorded location or
// code no longer checks out, plus everything whose verdict expired because the
// snippet was edited since it was checked.
const needsAttentionOnly = ref(false);
@@ -58,7 +58,7 @@ const showMergeModal = ref(false);
const canonicalId = ref<number | null>(null);
const merging = ref(false);
// Near-duplicate report (#2088). Loaded on demand, not with the list: it's a
// Near-duplicate report. Loaded on demand, not with the list: it's a
// pairwise scan and most visits to this page aren't a tidy-up.
const duplicateGroups = ref<DuplicateGroup[]>([]);
const dupLoading = ref(false);