Design-system delivery, the contrast fix, recall scoping, and backup coverage #93

Merged
bvandeusen merged 10 commits from dev into main 2026-07-31 23:21:25 -04:00
Owner

Ten commits, all CI-green on 2cc9e13.

What to verify after deploying

  1. Light/dark toggle on any page with buttons. 76 color: #fff became var(--fs-text-on-action). Dark should look unchanged; light is the one to check — button labels must stay parchment on the coloured fill, not flip to near-black.
  2. A new session in a bound repo. The SessionStart context should now carry a ## Design system block under the active project.
  3. Plugin 0.1.21 — the planning-guidance edits only reach a session after the plugin cache refreshes.

The changes

Design systems can reach a session (731ca28). Rules were pushed into every session by the hook and enter_project; a design system had neither channel, so its standards were reachable only by an agent that already knew to ask. That gap was invisible while the visual standards also lived in a rulebook — retiring that rulebook (done separately, in the instance data) would have silently removed design guidance from nine family projects. enter_project now returns design_system with the guidance chain-merged ancestor-first, because a child system holds only what it changes and the leaf alone is a fragment.

Text on a filled colour needs its own token (4c9a637). The design system said pure white should supersede to --fs-text-primary. That token inverts by mode; the surfaces under those 76 declarations do not — action colours, semantics, the accent, the CTA gradient are all single-valued. Sweeping as recorded would have put obsidian on moss green at roughly 2.4:1, against a stated WCAG-AA floor. --color-accent-fg had the same defect independently.

Embedding moved to the service (5c51e29). It fired at five REST handlers and nowhere else, so anything created through MCP was not semantically searchable until the next restart's backfill. Invisible on an instance that redeploys constantly; permanent on one that doesn't.

Two mirror-image retrieval scoping mistakes (f8522fb, da2383b). Auto-inject let every kind compete on raw cosine, so snippets lost to project records that share coding vocabulary; the write-path hook was snippets-only, so an issue recording "we tried this and it broke" could never reach the moment that code was written. Both fixed.

Backup was missing six tables (84541f3, 2cc9e13) — systems, record_systems, design_systems, design_tokens, note_usage_events, repo_bindings. BACKUP_VERSION 4 → 5; older payloads restore unchanged. The deliverable is the guard, not the six sections: a new table now fails a test until someone either backs it up or states that it shouldn't be.

Planning guidance stopped mandating milestones (6eedb0f, 1e139d0). Five surfaces said to call start_planning FIRST for any non-trivial work; a sixth had the criterion right. Now stated once and deferred to.

CI checks the app's own components against the tokens (378a4b8).

Known-unverified

A real backup round-trip. CI has no database in the unit lane, so the FK-remapping path — including the new design-system parent ordering — is covered by pure row-builders, a mocked-session shape test, and the coverage guard. Worth exercising manually before relying on a restore.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs

Ten commits, all CI-green on `2cc9e13`. ## What to verify after deploying 1. **Light/dark toggle on any page with buttons.** 76 `color: #fff` became `var(--fs-text-on-action)`. Dark should look unchanged; **light is the one to check** — button labels must stay parchment on the coloured fill, not flip to near-black. 2. **A new session in a bound repo.** The SessionStart context should now carry a `## Design system` block under the active project. 3. **Plugin 0.1.21** — the planning-guidance edits only reach a session after the plugin cache refreshes. ## The changes **Design systems can reach a session** (`731ca28`). Rules were pushed into every session by the hook and `enter_project`; a design system had neither channel, so its standards were reachable only by an agent that already knew to ask. That gap was invisible while the visual standards also lived in a rulebook — retiring that rulebook (done separately, in the instance data) would have silently removed design guidance from nine family projects. `enter_project` now returns `design_system` with the guidance **chain-merged ancestor-first**, because a child system holds only what it changes and the leaf alone is a fragment. **Text on a filled colour needs its own token** (`4c9a637`). The design system said pure white should supersede to `--fs-text-primary`. That token inverts by mode; the surfaces under those 76 declarations do not — action colours, semantics, the accent, the CTA gradient are all single-valued. Sweeping as recorded would have put obsidian on moss green at roughly 2.4:1, against a stated WCAG-AA floor. `--color-accent-fg` had the same defect independently. **Embedding moved to the service** (`5c51e29`). It fired at five REST handlers and nowhere else, so anything created through MCP was not semantically searchable until the next restart's backfill. Invisible on an instance that redeploys constantly; permanent on one that doesn't. **Two mirror-image retrieval scoping mistakes** (`f8522fb`, `da2383b`). Auto-inject let every kind compete on raw cosine, so snippets lost to project records that share coding vocabulary; the write-path hook was snippets-only, so an issue recording "we tried this and it broke" could never reach the moment that code was written. Both fixed. **Backup was missing six tables** (`84541f3`, `2cc9e13`) — `systems`, `record_systems`, `design_systems`, `design_tokens`, `note_usage_events`, `repo_bindings`. `BACKUP_VERSION` 4 → 5; older payloads restore unchanged. The deliverable is the guard, not the six sections: a new table now fails a test until someone either backs it up or states that it shouldn't be. **Planning guidance stopped mandating milestones** (`6eedb0f`, `1e139d0`). Five surfaces said to call `start_planning` FIRST for any non-trivial work; a sixth had the criterion right. Now stated once and deferred to. **CI checks the app's own components against the tokens** (`378a4b8`). ## Known-unverified A real backup round-trip. CI has no database in the unit lane, so the FK-remapping path — including the new design-system parent ordering — is covered by pure row-builders, a mocked-session shape test, and the coverage guard. Worth exercising manually before relying on a restore. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
bvandeusen added 10 commits 2026-07-31 23:21:18 -04:00
feat(ci): check the app's own components against the tokens, not just snippets
CI & Build / Python lint (push) Successful in 7s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 34s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m3s
CI & Build / Build & push image (push) Successful in 41s
378a4b8f99
Closes the gap the theme.css repoint exposed (#2319, part of #2277).

`check_code_against_tokens` could always answer "does this code use the sheet
correctly?" — it was only ever fed recorded SNIPPETS. The app's own components,
where sixteen unresolvable references were living quietly, were checked by
nothing at all.

That was structural rather than an oversight: the drift panel runs in the browser
and cannot read source files, and the server has no repo access. CI is the only
place holding both the sources and the ability to run the check — and it only
became cheap once theme.css became a generated artifact, so the source of truth
is a committed file with no network and no credentials.

**The sheet now carries its own SUPERSEDES block.** That is what keeps the
checker instance-agnostic (rule #115): it knows nothing about any palette, and
reads both the declarations and the discouraged literals out of whatever
stylesheet it is pointed at. Hardcoding "#fff means use the text token" would
have baked one install's kit into the tool.

Two severities, split on whether the count is already zero:

  FAIL   an unresolvable var() reference — zero today, so this is a ratchet
         holding a line already reached. It cannot false-positive either: the
         name is declared or it is not.
  REPORT superseded literals (32 files) and raw colour literals (246). Gating
         those means a permanently-red job, and a check nobody reads is worse
         than no check.

**Comments are stripped before scanning, and that fired on the first real run.**
A comment explaining why a literal is avoided necessarily contains that literal —
DesignSystemsView's stylesheet documents exactly that about `#fff`, and the
checker reported the explanation as a violation. A checker that flags the
documentation of a rule teaches people to stop documenting rules.

Also narrowed `--fs-weight-medium`'s supersedes to the keywords. `600` and `700`
are real violations of the two-weight rule, but a bare number matches too much to
find by literal scan — `z-index: 600` is not a font weight. That needs a
property-aware check, which is a different tool.

Verified end to end: the generator's output parses back through the checker's
reader, so the two halves cannot drift into disagreeing about the format.
fix(instructions): stop mandating a milestone for every non-trivial task
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 11s
CI & Build / integration (push) Successful in 19s
CI & Build / Python tests (push) Successful in 43s
CI & Build / TypeScript typecheck (push) Successful in 26s
CI & Build / Build & push image (push) Successful in 28s
6eedb0f6b9
Four product surfaces told the agent to call start_planning FIRST for any
"non-trivial" work, while a fifth — the milestone bullet four lines up in the
same file — had the criterion right: use one when the work has an arc. The
loudest surface won, so sessions wrapped bug fixes and one-file changes in
milestones that never meant anything.

Mandating one project shape is what rule #115 forbids: some projects are
milestone-shaped, others are a flat task list and always will be.

Now the arc test is stated ONCE in full, in writing-plans, along with what to
do when there is no arc (a task, driven by status and work-logs). The other
surfaces name it and defer:

- writing-plans/SKILL.md gains a "first decide whether this work wants a plan"
  section; its frontmatter trigger is the arc, not "non-trivial"
- using-scribe reflex #4 points at the skill instead of restating it
- server.py's Plan bullet adopts the milestone bullet's own criterion
- server.py's planning paragraph drops from 11 lines to 6: it keeps the claim
  MCP instructions should make (a plan's HOME is a milestone, not a local .md)
  and drops the how, which the skill carries
- start_planning's docstring gains the when

Also removed "call start_planning FIRST — before any brainstorming, design, or
plan-writing skill runs." That was the server asserting priority over the skill
layer. Tools describe what they do; skills decide when they apply.

The structural point outlasts the wording: a surface that restates a rule is a
surface that will eventually contradict it, and nothing checks prose against
prose.

Closes #2322.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
chore(plugin): bump to 0.1.21 for the planning-guidance edits
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / integration (push) Successful in 34s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 21s
1e139d0d18
The two skills changed in 6eedb0f are shipped plugin content, and the
installer compares manifest versions to decide whether to refresh the cache
that actually executes. Without the bump the edits reach the repo and stop
there (#2209) — which is exactly the silent no-op the check exists to catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
feat(design-systems): give a design system a way to reach the session
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 28s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Python tests (push) Successful in 51s
CI & Build / Build & push image (push) Successful in 31s
731ca284c3
Storing a design system never made a session aware of one. Rules get pushed
into every session by the SessionStart hook and returned by enter_project; a
design system had neither, so its standards were reachable only by an agent
that already knew to call resolve_design_system — the same silent failure as a
token nobody declares.

That gap was invisible while the operator's visual standards also lived in a
rulebook. Retiring that rulebook (which is what this unblocks) would have
deleted design guidance from every session with nothing to say so.

- services/design_systems.design_context() — the delivery side. Guidance is
  chain-merged ANCESTOR-FIRST: a child system holds only what it CHANGES, so
  its own guidance describes a departure from a house style it never restates,
  and the leaf alone is a fragment. Tokens are summarised (count + group
  names), not listed — a hundred declarations would crowd out the context they
  are meant to inform.
- enter_project returns `design_system`, null when the project has none.
- The SessionStart context gains a Design system block with pointers to the
  values, alongside the always-on rules.
- server.py's entity list gains Design system, including the negative: do NOT
  record one as a rulebook, because a token kept as prose cannot be resolved,
  inherited, rendered or checked.
- The rulebook-tier passage used "a design-system rulebook" as its worked
  example of a subscribed rulebook — it now teaches the opposite, plus a new
  "is this a rule at all?" test pointing at design systems, processes and
  snippets.
- using-scribe gains a section on building UI against the project's system.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
fix(theme): text on a filled colour needs its own token — the old one inverts
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 41s
4c9a637507
76 hardcoded `color: #fff` now resolve to --fs-text-on-action, a new token
that is parchment in BOTH modes.

The design system said they should supersede to --fs-text-primary, on the
recorded reasoning that "there is no 'text on action' colour, there is just the
text colour." That is true on dark and wrong on light. --fs-text-primary
inverts to #14171A; the surfaces underneath it do not invert at all — every one
of these 76 sits on an action colour, a semantic colour, the accent, or the CTA
gradient, all of which hold a single value across modes.

Sweeping as recorded would have put obsidian text on moss green: roughly 2.4:1,
against a house style whose stated floor is WCAG AA. It would have looked
correct to me, because I checked it in the mode where it was correct.

--color-accent-fg had the same defect independently and is repointed too.

The token check now reports zero superseded literals, down from 30 files, and
raw colour literals drop 246 -> 169.

Closes #2275.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
fix(embeddings): embed in the service, so every caller gets it (#2056)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Build & push image (push) Successful in 29s
5c51e29f26
A note or task created through MCP was not semantically searchable until the
next restart's backfill ran. Embedding fired at the five REST route handlers
and nowhere else; the MCP tools call the service directly, so they skipped it.

The shape of this bug is the reason to care: it is invisible on an instance
that redeploys constantly (this one does, per rule 46) and permanent on one
that doesn't. Rule 115 — the product has to stand up for the install that
restarts twice a year, not just for the one that restarts hourly.

Moved to services/notes.embed_note(), called from create_note and update_note,
and deleted from all five routes. Every caller — REST, MCP, recurrence,
snippets — now gets it by construction rather than by remembering.

Two things fall out of having one implementation instead of six:

- It uses note.user_id, the OWNER. The routes were inconsistent: some passed
  the caller's uid, some the owner's. On a shared record the caller's id mints
  a second embedding row that nothing reads.
- services/snippets.py's _embed_snippet existed only because snippets are
  created via MCP and the routes couldn't cover them. Every one of its four
  call sites goes through notes_svc, so the helper and its four calls are gone,
  along with the eight test patches that existed to neutralise it.

RuntimeError (no running loop — unit tests, scripts) and any indexing failure
are both swallowed: a write that succeeded must not be failed by its index.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
fix(retrieval): give reuse a slot, and let experience reach the write path
CI & Build / TypeScript typecheck (push) Failing after 2s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 22s
CI & Build / Python tests (push) Failing after 35s
CI & Build / Build & push image (push) Skipped
f8522fb28f
Two mirror-image scoping mistakes, neither deliberate (#2246).

AUTO-INJECT let every kind compete on raw cosine. That is fatal rather than
merely imperfect here, because Scribe's project records are ABOUT software
work: a task titled "surface snippets before the agent writes code" is a
near-perfect lexical match for "write a function…" while answering none of it.
Measured live, a prompt asking for a helper returned three records about
BUILDING the retrieval system and zero snippets. Snippets are ~0.5% of the
corpus, and the ratio worsens as the project record grows — which is the
direction Scribe is meant to grow, so no threshold tuning fixes it.

Now the best snippet or process takes the LAST slot when none won on score.
Deliberately NOT held to the margin band: that band measures distance from the
top overall score, and the top score is the very thing snippets lose to. It
still must clear the configured threshold, so a weak snippet cannot buy the
slot — silence stays the default. Skipped entirely when reuse already won,
so the fix is invisible in the case it isn't needed.

WRITE-PATH was snippets-only — the same mistake inverted. An issue recording
"we tried this and it deadlocked" could never reach the moment that code was
about to be written, though it is arguably the better prior art: it says what
NOT to do. Widened to snippets plus recorded experience.

That needed a filter the search layer couldn't express. "Experience" is issues
plus dev-logs, which differ on is_task, so neither note_type nor is_task alone
covers it. semantic_search_notes gains task_kind, which restricts TASKS to the
given kinds while leaving non-task notes untouched — so note_type=("snippet",
"note") + task_kind="issue" yields snippets, fixed problems and durable notes,
without the open to-do list. note_type now accepts a sequence too.

Non-snippet hits are labelled with their kind, because an unlabelled issue on
that menu reads as "here is code to reuse", the opposite of what it says.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
fix(retrieval): verify the reserved slot's kind instead of trusting the query
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 18s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 34s
da2383b079
CI caught six failures on f8522fb. Five were fixtures; one was a real
assumption.

THE REAL ONE: _reserve_slot_for_reuse trusted that a query filtered by
note_type could only return reuse kinds. It now checks _record_kind on the way
in. That slot exists FOR snippets and processes — one silently spent on
something else is worse than no slot at all, because the resulting line is
indistinguishable from one that earned its place on score.

THE FIXTURES, all the same shape: MagicMock notes with is_task left to
auto-create. It is truthy, and _record_kind reads task-ness FIRST — so every
mock snippet in three test modules was rendering as "task". Two of those
fixtures already carried a comment explaining this exact hazard about `.data`;
the same reasoning applies to `.is_task` and nobody had needed it until the
menu started naming kinds.

One assertion was genuinely stale rather than broken: test_write_path_trigger
pinned note_type == "snippet", which was the behaviour the widening replaced.
Updated to the new contract, including the task_kind="issue" filter that keeps
the open to-do list out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
fix(backup): six tables were silently absent, and nothing would catch a seventh
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 25s
CI & Build / Python tests (push) Failing after 38s
CI & Build / Build & push image (push) Skipped
84541f392b
services/backup.py enumerated its tables as hand-maintained literals with
nothing tying them to the schema. Tables added since that list was last
extended were absent from every backup — no error, no warning, and a restore
that reports success.

Missing: systems, record_systems (0065), note_usage_events (0071),
design_systems, design_tokens (0072), and repo_bindings — which the issue
itself had not spotted, found only by diffing the model tablenames against the
two lists instead of trusting either.

_NOT_INCLUDED was worse than incomplete: it named "embeddings", "invitations"
and "password_resets", none of which are tables. It read as coverage while
naming nothing the schema could confirm. Now real names, plus retrieval_logs —
observational telemetry that grows per query and that nothing reads for
correctness.

THE DELIVERABLE IS THE GUARD, not the six sections. Extending a list fixes
today and changes nothing about the next table; a new one now fails a test
until someone either backs it up or states that it shouldn't be. It checks
both directions — an unaccounted table, and a listed name that no longer
exists, which is what the three phantom entries above would have tripped.

Design systems need ordering care: parent_id is a self-FK. The export orders
parent-first (parent_id NULLS FIRST, then id — a parent always has the smaller
id), so restore resolves each parent from the map as it goes, with no second
pass. A child whose parent is missing lands as a root rather than failing the
whole restore.

Usage events are kept because pull-through is the evidence base for whether
recall works, and it only ever accumulates — a restore that dropped it would
reset that measurement to zero while everything still looked fine.

BACKUP_VERSION 4 -> 5. Every new restore section is data.get()-guarded, so
v2/v3/v4 payloads restore unchanged.

Closes #2293.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
test(backup): assert the version constant, not a copy of it
CI & Build / integration (push) Successful in 21s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 41s
2cc9e1380e
The export test pinned `out["version"] == 4`, so bumping BACKUP_VERSION broke a
test that was only ever checking the payload carries the version — which it
still did. Asserts against backup.BACKUP_VERSION now, and covers the six v5
sections alongside the v3 ones.

The guard itself passed on the first run: every table in Base.metadata was
accounted for, in both directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
bvandeusen merged commit 8288c6e4a7 into main 2026-07-31 23:21:25 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledScribe#93