CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 52s
CI & Build / Python tests (push) Failing after 1m2s
CI & Build / Build & push image (push) Skipped
`usage_for_notes` is named for notes and works on every note row, yet the chip reached snippets and rules only. Notes had it nowhere. Lessons had it collected and shown nowhere a person could reach, because #4196 taught `/api/lessons` to attach it and `KnowledgeView` — the only lesson list in the UI — browses through `/api/knowledge`, so `listLessons` still has no consumer. The cause was not a missing line. SEVEN call sites carried their own copy of the same few lines: two REST lists, two REST details, two MCP lists, one MCP detail. Each read perfectly well alone, so "which doors attach usage?" had no answer anywhere in the code — the same asymmetry test_system_tagging_door_parity.py records for System tagging (#4249), where whichever door nobody exercised for a kind is the one that never grew the feature. `attach_usage(rows, key="id")` is now that answer, and all seven go through it. A detail payload is a one-row list, so the single-record doors share the seam rather than keeping a second shape beside it. Deliberately NO try/except: the fail-open already lives in `usage_for_notes`, which reports through `_report_failure("readout")` and returns the zero-filled map. Wrapping it again would swallow the REPORT as well as the error, and a silently-swallowed readout failure is exactly #2663 — every counter reading zero in production for weeks while the writes landed fine. `/api/knowledge` now attaches usage, which closes both holes at once: it is how notes, lessons and processes are all browsed. `KnowledgeView` renders the badge on the card footer, looking the advice up per row because the feed is mixed. The advice moves to utils/deadWeight.ts. Canon #3460 says each caller owns its own const, and that held while each caller showed ONE kind; a mixed feed would need five of its own and the next surface another five. The canon's actual invariant — advice is kind-specific and never baked into the badge — is kept: it is still a prop. The three existing callers now read the same table, so the sentence has one home rather than four. Recorded against #3460 so the next reader is not left re-litigating it. `_row_id` rejects bools explicitly: `int(True)` is 1, so a row carrying a flag under the key would be credited with note #1's counts, and a wrong chip is worse than no chip because it reads as a measurement. A row with no usable id is skipped rather than failing the page. Tests pin the PROPERTY, not one route: no door calls the aggregate directly (AST, so a comment naming it is not a false positive), and every door that shows usage reaches the seam. Plus the N+1 guard — one aggregate per page, asserted on await_count, because the per-row version reads more naturally and is invisible in review. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
94 lines
4.4 KiB
Vue
94 lines
4.4 KiB
Vue
<script setup lang="ts">
|
|
import type { RuleHeader } from "@/api/rulebooks";
|
|
import UsageBadge from "@/components/UsageBadge.vue";
|
|
import { DEAD_WEIGHT_ADVICE } from "@/utils/deadWeight";
|
|
|
|
defineProps<{ topicId: number; rules: RuleHeader[] }>();
|
|
const emit = defineEmits<{
|
|
"open-rule": [id: number];
|
|
"create-rule": [topicId: number];
|
|
}>();
|
|
</script>
|
|
|
|
<template>
|
|
<section class="pane">
|
|
<header><h2>Rules</h2></header>
|
|
<ul>
|
|
<li v-for="r in rules" :key="r.id" @click="emit('open-rule', r.id)">
|
|
<div class="title" :class="{ 'is-preference': r.kind === 'preference' }">
|
|
{{ r.title }}
|
|
<!-- Force is stated, never inferred. A preference does not bind and
|
|
is the one kind a session rewrites on its own, so an unmarked
|
|
row would teach the opposite of both. Rules carry no chip:
|
|
they are the default reading of a rulebook, and marking every
|
|
row marks nothing. -->
|
|
<span
|
|
v-if="r.kind === 'preference'"
|
|
class="rule-chip rule-chip-preference"
|
|
title="How you want work done. It does not bind, and Scribe updates it as the work teaches it."
|
|
>preference</span>
|
|
<!-- Marked only when something is WRONG: every rule arrives by
|
|
retrieval now, so "conditional" stopped distinguishing anything.
|
|
A missing trigger does — it means nothing can retrieve this. -->
|
|
<span
|
|
v-if="!r.when_to_apply"
|
|
class="rule-chip rule-chip-inert"
|
|
title="No trigger, so nothing can retrieve it — this rule will never reach a session"
|
|
>never surfaces</span>
|
|
<!-- Present only on a rule carrying a check, so the chip's very
|
|
presence says "this one asserts a fact that can go false". -->
|
|
<span
|
|
v-if="r.last_verified"
|
|
class="rule-chip check-chip"
|
|
:class="{ unchecked: r.last_verified === 'never' }"
|
|
:title="r.last_verified === 'never'
|
|
? 'Asserts a fact nobody has confirmed yet'
|
|
: `Check last passed ${r.last_verified}`"
|
|
>{{ r.last_verified === "never" ? "unverified" : `checked ${r.last_verified}` }}</span>
|
|
<UsageBadge :usage="r.usage" :dead-weight-advice="DEAD_WEIGHT_ADVICE.rule" />
|
|
</div>
|
|
<div class="statement">{{ r.statement }}</div>
|
|
<div v-if="r.when_to_apply || r.updated_at" class="meta">
|
|
<span v-if="r.when_to_apply" class="trigger">{{ r.when_to_apply }}</span>
|
|
<span v-if="r.updated_at" class="age" :title="`Last changed ${r.updated_at}`">{{ r.updated_at }}</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<button class="new-rule" @click="emit('create-rule', topicId)">+ New rule</button>
|
|
</section>
|
|
</template>
|
|
|
|
<style src="@/assets/rules-shared.css" />
|
|
<style scoped>
|
|
ul { list-style: none; padding: 0; margin: 1rem 0; }
|
|
li {
|
|
padding: 0.75rem;
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
border-left: 2px solid var(--fs-accent);
|
|
margin-bottom: 0.5rem;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
}
|
|
li:hover { background: var(--fs-surface-hover); }
|
|
.title { font-family: Fraunces, serif; font-style: italic; font-size: 1.05em; }
|
|
/* The chip says which kind; this says it again at a glance, for scanning a
|
|
long topic rather than reading one row. Weight, not colour — the chip
|
|
already carries the accent, and a second coloured thing would compete
|
|
with it for the same job. */
|
|
.title.is-preference { font-weight: 500; }
|
|
|
|
.statement { font-size: 0.9em; opacity: 0.8; margin-top: 0.25rem; }
|
|
.meta { display: flex; align-items: baseline; gap: 0.5rem; margin-top: 0.35rem; font-size: 0.75em; }
|
|
.trigger { flex: 1; min-width: 0; color: var(--fs-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.age { color: var(--fs-text-tertiary); font-variant-numeric: tabular-nums; flex-shrink: 0; }
|
|
/* Only the departures from .rule-chip (rules-shared.css) live here. */
|
|
.check-chip { font-variant-numeric: tabular-nums; }
|
|
/* No age-graded colour on purpose. The sweep is already ordered by urgency, so
|
|
a red/amber ramp would restate the ordering AND require an invented "stale
|
|
after N days" threshold — a magic number nobody could defend and the first
|
|
thing to go out of date. Only "never" is marked, because it is categorically
|
|
different from a date rather than a worse one. */
|
|
.check-chip.unchecked { font-style: italic; color: var(--fs-text-tertiary); }
|
|
.new-rule { cursor: pointer; }
|
|
</style>
|