`.editor-body` was removed from editor-shared.css in c5faaf3 — nothing used
it once TaskEditorView replaced it — so "Replace .editor-body for task
editor" now names something a reader cannot find. Say what .task-body is and
record that its predecessor is gone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integration cover for the seam the unit tests only meet at either end: a
template that names no class at all (`<transition-group name="toast">`) and
one that builds its class (`` `status-${s}` ``) run through the real
extractor, through resolve_consumers, into `flag="unused-css"` — and the
rules they reach stop being reported, while a rule nothing can reach stays
listed. The prefix credits BOTH status rows, which is the documented
reading: the template does not say which one it built.
The manifest bump is what the last commit owed — it edited the
shape-accounting skill, and the installer compares versions to decide
whether to refresh the cache that actually executes (#2209), so plugin
edits that ship without a bump reach the repo and stop there. CI caught it
on 4323; 0.1.44 -> 0.1.45.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paying down #2962 measured `flag="unused-css"` against a hand audit and
found it had a permanent false-positive floor: roughly sixty classes it
called unused are alive and always would be, because two ordinary authoring
forms produce names no reader of `class=` attributes can see. A flag whose
list you cannot act on line by line is worse than no flag — act on it and
you delete live UI.
- A transition `name=` IS a class reference. `<Transition name="toast">`
makes Vue apply `.toast-enter-active` and its siblings at runtime, and
React's `<CSSTransition classNames="fade">` does the same with a different
suffix set. Every spelling of the tag is read (`Transition`,
`TransitionGroup`, `transition-group`), and the emitted suffix set is the
union of Vue 3, Vue 2 and React: naming a class no rule defines costs
nothing, since it resolves to no row. A bound `:name` stays unknowable.
- A concatenated name contributes its static head as a PREFIX reference.
`` `status-${s}` ``, `'pri-' + p` and `class="card-{{ v }}"` all leave a
head behind once the hole is blanked — and `_CLASS_TOKEN_RE` accepts a
trailing hyphen, so until now the extractor emitted a junk token
`"status-"` that matched nothing. It is now `status-*`, and
resolve_consumers credits every row whose symbol starts with that head,
each under the same own-file-else-fan-out rule as an exact token. `*`
cannot occur in a class token, so the marker rides the existing
dict[str, int] with no schema change. A head shorter than two characters
says nothing and is dropped.
Crediting every candidate row is the honest reading: the template genuinely
does not say which one it built, and the alternative is reporting live rules
as dead. What the map still cannot see is a name assembled in a script —
`classList.add` — which stays deliberately out of scope; the skill, the
`flag=` docstring and the refresh payload docs all say so now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first sweep asked "does this class token appear anywhere outside a
<style> block?". That is too generous for a `<style scoped>` rule, which can
only ever match its own template, the component's root element, or whatever
`:deep()` reaches — so a scoped rule whose name lives only in some OTHER file
is dead regardless. The server's map had this right and my local pass did
not; this closes the gap.
79 scoped classes are absent from their own file. 48 are names their own file
BUILDS — `status-${task.status}`, `pri-${p}`, `toast--${type}`,
`perm-${permission}`, `diff-${op}`, `is-${kind}` — and 12 more are Vue
transition classes. Those are the map's documented blind spot and they stay.
The remaining 19 match nothing:
- AppHeader (9, -39 lines): the whole connection-status indicator —
.status-indicator/.status-dot/.status-text and the five colour states,
plus @keyframes pulse-dot and status-pulse, which had no other user. Same
Phase 7 residue as the last commit. `.btn-icon.active` goes too: `active`
is never applied in this component (its nav uses router-link-active).
- SettingsView (6): .status-badge/.status-on/.status-off,
.perm-granted/.perm-denied, .location-row. This view renders no child
components at all, so nothing can inherit its scope.
- WorkspaceNoteEditor: .note-row:hover .btn-delete and .btn-suggest-tags —
those buttons are .btn-danger-outline/.btn-ghost now.
- ProjectListView .loading-msg; SnippetEditorView .field-row.three, whose
media-query companion keeps its live .field-row half.
Checked against child-component roots before deleting, since a child's root
element does inherit the parent's scope id — none of the 19 is one. Template
and script regions byte-identical; both style checks still pass with only
the six pre-existing reports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Milestone 302's map flagged 195 css rows no template names. Re-derived the
list locally with a stricter test than the map's — a class is dead only if
its token appears NOWHERE outside a <style> block, in markup or script, and
is not the static half of a concatenated name (`priority-${p}`) — which
takes the map's known blind spot off the table. 123 survived that; 21 of
those are Vue <Transition>/<transition-group> classes generated at runtime
from a name= attribute (detail-fade, peek-slide, shortcuts-fade, tab-fade,
toast) and one is ProseMirror's vendor class. Those stay. The other 102 go.
- SettingsView.vue (80, -417 lines): whole features whose UI was removed —
Ollama model management (model-*, pull-bar-*, suggestion-chip), push
notifications (push-*), the voice library and voice blending (voice-*,
blend-*), geo status (geo-*), MCP package rows, retention, the learned
summary, and the form furniture that served them.
- editor-shared.css (16, -151): the standalone assist panel, superseded by
the sidebar assist section — streaming now renders as .stream-preview in
the main area, so .assist-panel*, .assist-sections*, .assist-preview-box
and .typing-indicator have no markup left. @keyframes blink went with the
last rule that animated it. .editor-body/.editor-main are dead too:
TaskEditorView replaced them with .task-body/.task-main.
- theme.css: .btn-new-conv/.btn-send dropped from the touch-target list
(names from another app), and the .hide-desktop utility no one used. The
generated --fs-* token block is untouched.
- ShareDialog .user-result-email, KnowledgeView .today-link,
ProjectView .edit-input (the remainder rule keeps its rationale comment
and its two live selectors).
A selector dies only when every class in its descendant chain is checked —
`.live .dead` matches nothing either — and a rule only when its whole comma
list is dead, so mixed lists keep their live half. Template and script
regions are byte-identical; check_dangling_styles.py and
check_design_tokens.py both pass, with only the six pre-existing reports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tests/helpers.http_sink replaces three module-local _Sink handlers (the
write-path tests and the after-write test). ProjectView + SettingsView
parsed `(e as {body?:{error?}}).body?.error || fallback` by hand ten times
beside the apiErrorMessage canon (#2853) - all ten now call it. The
extractor (server + the hook awk mirror) no longer reads `import { type Foo }`
as a definition of Foo - that was the last "identical body" sym family.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
KnowledgeView's own comment asked for this promotion once a second view grew the
panel. The sheet carries .dup-panel / .dup-empty,.dup-head / .dup-group /
.dup-members / .dup-member(+:hover) / .dup-score; each view keeps only its
extras (.dup-claimed, .dup-action).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The derive queue said the biggest duplicate families were whole views:
TaskViewerView, UserManagementView and LogsView were imported nowhere — left
behind when tasks moved to the editor and users/logs became SettingsView
tabs. Deleted (rule 22). Note/TaskEditorView carried six identical scoped
rules -> editor-shared.css (the .tag-suggest-row gap the scoped copies
actually rendered wins). Three views wrapped the page under three names ->
.page-container in components.css. Three scoped input recipes -> the design
system fs-input recipe (snippet #2336), verbatim, in components.css; width/
box-sizing stay with the caller. The three rules panes share .pane and the
pane heading via rules-shared.css (the auth-shared pattern, #2852).
Ledger: a single-declaration CSS rule keeps its selector in its fingerprint,
so `color: var(--fs-text-tertiary)` under five different names is no longer
a five-file "identical body" family — the first pay-down found that most of
the 148 dup families were exactly this, and nobody would consolidate them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run 4240: two pre-write hook tests pinned the skip case and scribe_defs()
inside scribe_prior_art.sh, which moved to the shared library; the after-write
test saw the same derive key appended once per changed file. Keep each token
once (sort -u after the appends) and point the pins at the library.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Edits made through sed/heredocs/scripts never reached the PreToolUse
Write|Edit hook, so a whole class of writes got no prior-art hint, no
ledger feed and no duplicate-family warning. scribe_after_write.sh asks git
what changed since it last looked (per-session path+blob snapshot; first
call = files touched in the last minute), extracts the definitions in the
added lines and calls /api/plugin/prior-art with the same three dedup
channels the pre hook keeps. Never blocks; silent on any failure. The
extractor, the prose/data skip list and the local by-name arm move to
scribe_defs.sh, sourced by both hooks. Version bump covers the step-2 hook
change too (run 4239 failed only on the bump check).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
shape_ledger.write_time_derive asks the ledger what it knows about each
named (kind, symbol): a derive-grouped family (identical body / same name
in N other files) -> "derive it now, do not add a copy"; a canonical row
at another path -> "canon #N at <path>, reuse". Judged rows at the path and
the canon own file stay silent. Rendered by _derive_line beside the
divergence line; keyed (group id / canon:<id>) on a third per-session dedup
channel in the hook (.derive.ids -> exclude_derive=).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Since the scoped bucket (#2869) the ledger reads 100% accounted while 439
derive rows stand; the standing block was gated on unclassified > 0 and so
went silent. Build it whatever the todo count ("; standing: ..."), and add
derive_new — derive-grouped rows first seen after the previous refresh
stamp — so entering a project names the drift ("+2 new copies since last
refresh: .error-msg in InceptionCard.vue") instead of waiting for an audit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- components/InceptionCard.vue: the one form, two homes — mode="create" in
the New-project modal's second step (emits the choices; the create carries
`inception`), mode="decide" on ProjectView for the owner of an undecided
project (loads that project's defaults, records the decision). Always-on
rulebooks listed checked (uncheck = exclude), others unchecked (check =
subscribe), design system select, seed-Systems toggle (disabled once the
project has Systems). Tokens only; modal canon (#2855); .btn-* canon.
- ProjectView: the card while undecided, one "Inheritance decided <date> via
… · …" line after; onDecided refreshes the project.
- ProjectRulesTab: "Excluded always-on rulebooks" section with include-back.
- api/inception.ts (types, fetchInceptionDefaults, decideInception);
api/rulebooks.ts: ApplicableRules.excluded_always_on, exclude/include
wrappers; REST POST/DELETE /api/projects/<id>/exclusions/rulebooks/<rb>.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- MCP create_project(..., exclude_always_on_rulebooks, subscribe_rulebooks,
design_system_id (0 unstated / -1 none / n), seed_systems): any inception
arg → inception.decide(via="mcp") after the create; none → undecided with
an inception_hint. New decide_project_inception(project_id, …) records or
re-records; nothing given = an inherit-all decision, stated.
- enter_project carries `inception` ONLY for the caller's own, undecided
project: inception_ask() = the project's current defaults + what to ask the
operator once + the exact call (the #2683 ask shape). Absent otherwise.
- REST: POST /api/projects accepts `inception` (validated before the create);
POST /api/projects/<id>/inception decides/re-decides; GET …/inception/defaults
is the card's payload; GET project already carries inception via to_dict.
- _INSTRUCTIONS: ORIENT names the ask; START a project names the questions —
never create a project bare by default (product behaviour, P#119).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- inception.decide(user, project, choices=, via=): owner-only; validates the
choices (pure) and every target (owned rulebook / always-on for an
exclusion / readable design system) BEFORE any effect; then, each
idempotent: exclude always-on rulebooks, subscribe rulebooks, point the
design system (None = explicitly none), seed the standard Systems if asked
and the project has none; writes projects.inception LAST. Re-deciding is
additive for exclusions/subscriptions, replaces the design system, never
re-seeds.
- inception.current_defaults(): what binds if nobody decides — the ask's
payload (always-on / other rulebooks, standing exclusions + subscriptions,
design system + the choices, Systems count).
- services/systems.STANDARD_SYSTEMS (name + generic charter) + seed_standard_systems();
mcp/tools/systems names the same list in the bootstrap ask — one vocabulary.
- Integration tests: effects land and the record says why; bad targets apply
nothing; outsiders cannot decide.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A project that opted out of an always-on rulebook at inception must not see
it anywhere: list_always_on_rules(project_id=) and the subscription-derived
set skip it (an exclusion is total), get_applicable_rules / rules_payload
carry `excluded_always_on` as the seventh key so the departure is visible
wherever the rules are, and the SessionStart block built for a bound project
names it ("Excluded for this project by its inception decision …"). MCP:
list_always_on_rules takes project_id; exclude_always_on_rulebook /
include_always_on_rulebook mirror suppress/unsuppress (owner-only; the
rulebook must be always_on — subscribed rulebooks are left by unsubscribing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A project's inheritance becomes a decision, not a default (milestone 297).
- projects.inception (JSONB, NULL = undecided): {decided_at, decided_by, via
mcp|ui|legacy, choices {exclude_always_on_rulebooks, subscribe_rulebooks,
design_system_id, seed_systems}}; on to_dict.
- project_rulebook_exclusions: a project's opt-out of a whole always-on
rulebook — the sibling of the rule/topic suppressions, CASCADE both ways.
- services/inception.py (first cut): the vocabulary, validate_inception
(pure, all-or-nothing), normalize_choices, is_decided. Effects come in
step 3.
- Migration 0085 backfills every existing project via="legacy" with its
current standing (no exclusions, its subscriptions, its design_system_id,
no seed) so the ask fires only for projects created after this ships.
- Backup v10: rulebook_exclusions section; project rows carry inception and
design_system_id, restored in a post-pass once rulebooks/design systems are
mapped (design_system_id was not restored before — fixed in passing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First deploy of v26.08.21.1 showed two gaps:
- Every one-line CSS rule followed by a blank line hashed to sha1("") — the
declarations live on the selector line, which the #2872 "declarations only"
fingerprint dropped — so 68 unrelated one-liners across 17 files read as
one body-identical copy at the top of the derive readout. The selector
line's tail after "{" is now part of the hash; an all-blank remainder falls
back to the whole block.
- The proposer only examined unjudged rows, so consumers that were already
classified (auth.create_invitation → hash_token) never got a uses edge: 3
edges for hash_token after the first refresh. Judged rows are now scanned
for references (once per body), no proposal is made on them.
- 0084 migration docstring reworded: "function that …" at a line start parsed
as a definition (extractor false positive).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A shape can follow one convention canon AND call several helper canons; the
row's single snippet_id made the 2026-08 audit pick (hash_token won, the
service-function convention lost), and hook evidence — pulled a snippet, then
wrote code naming it — was stamped as instance when it is a uses fact.
- code_shape_uses (migration 0084): shape → snippet, basis, evidence; unique
per pair; cascades with both ends. USE_BASES: reference | hook | agent |
audit | import. A judgment-grade basis overwrites a mechanical one, never
the reverse.
- classify_shapes items and classify_shapes_by_rule take uses=[snippet ids]
(targets validated like snippet_id; all-or-nothing).
- The write-path hook writes a uses edge for every pulled canon the payload
names (the instance stamp is unchanged); the proposer writes a uses edge
for every canon a body names (reference_canons: kind + language family +
stoplist, same rules as the reference basis) — the mechanical form of
"auto-confirm own-import references" deferred from #2871.
- list_shapes(uses=N) lists the consumers of a canon; get_snippet's consumer
map gains `uses` beside instances/variants.
Operator decision on #2870 (2026-08-21): keep one snippet_id, add uses edges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- code_shapes.reason_code (migration 0083): an optional code from a fixed
catalogue (scoped-css, one-off-handler, test-helper, convention-plumbing,
pure-helper, generated, script, typed-record) beside the prose reason, so
the ledger can be filtered/aggregated by kind of one-off; validated in
classify_shapes and classify_shapes_by_rule; on to_dict/to_compact.
- Snippet location lookups match repo case-insensitively in both dialects
(location_matches / location_jsonpath via like_regex flag "i") — "Scribe"
vs "FabledScribe" vs "fabledscribe" recorded free-form hid half the canon
from list_snippets(repo=, path=).
- coverage line names the next action: "top canon #N ×k" (biggest proposal
queue) and "top copy <label> ×files" (widest body-identical group).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Project 2 is bound to main, so every consolidation of the 2026-08 audit was
invisible to the ledger until the dev→main merge; the operator works on dev
(rule 1). repo_bindings.ref (migration 0082, nullable) is the branch the
coverage refresh reads; NULL keeps the forge default branch. set_binding takes
ref (name sets, "" clears, None leaves standing); bindings_for_project feeds
the refresh; bind_repo exposes ref ("-" clears). to_dict carries it.
Operator decision on #2873 (2026-08-21): per-binding ref, chosen at bind time,
default the repo default branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-08 audit consolidated identical bodies under different names and
files (five auth views' CSS, two Workspace formatDate()s, four modal blocks)
while the derive readout led with name groups (to_dict ×25, main ×5, load ×6)
that were convention or coincidence.
- proposal_summary ranks dup:<sha> groups above name groups, wider file spread
first, and carries `files`; scoped rows (#2869) are in the readout, since
that is where view-level copies live.
- extract_definitions fingerprints a CSS rule by its declarations — the row
identity already carries the selector — so .closed-msg / .error-block /
.success-msg with one body are one dup group. One-time effect: judged css
rows whose stored fingerprint predates this may show a recheck on the next
refresh (the judgment stands; re-confirm).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-08 audit left 77% of Scribe's ledger `exempt`, most of it a Vue
component's scoped <style> rules and <script setup> functions — one-offs by
construction (unreachable from any other file) that add nothing when judged
one by one and bury the rows a person should look at.
- coverage: Definition carries its line; scoped_definitions() names, per
.vue file, every sym and every css rule inside <style scoped>; ArchiveShape
carries the flag.
- sync: such rows are stamped status=scoped / classified_by=mechanical with
the by-construction reason (history event recorded); un-stamped back to
unclassified if a later tree makes them ordinary; a judgment overrides.
- The machine still sees them: proposer, derive grouping, divergence, hook
evidence, canonical stamping and classify_shapes_by_rule's default all
treat unclassified + scoped as the unjudged set (_MECHANICAL_TODO). Only
the human todo (status=unclassified) and largest_gaps exclude them.
- accounting counts `scoped`; coverage line and the project card legend show
it; SHAPE_STATUSES gains it (no DB CHECK on status — no migration).
- shape-accounting skill documents the bucket; plugin 0.1.37.
Operator decision on #2869 (2026-08-21): keep extracting everything, stamp
mechanically, keep `exempt` a human judgment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-08 audit judged 3,427 rows in 14 hand-driven batches through a raw
MCP client because a list_shapes page overflowed the tool budget and every
row had to be sent back one by one. Now:
- list_shapes(compact=True): path · symbol · kind · status · signature, plus
snippet_id / by / proposal / diverges_from / recheck only when set. A full
500-row page fits. CodeShape.to_compact() is the row shape.
- classify_shapes_by_rule(project_id, path, status, pattern=, kind=,
snippet_id=, reason=, via=, include_judged=): ONE judgment over every
unclassified live row under a directory whose symbol matches a glob;
judged rows are untouched unless include_judged; canonical is refused;
same gates as the row form; one transaction; returns count + sample.
shape_ledger.classify_shapes_where / rule_matches (pure) carry it.
Tests: compact row pinned, rule_matches directory/glob/kind semantics, the
tool mount, and an integration sweep (unclassified-only, include_judged,
gates).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 2026-08 audit examined 407 proposals. Every cross-language hit was wrong:
the Python MCP tool-module canon (symbol `register`) was offered for each auth
view's handleSubmit (it calls authStore.register()) and for the TS auth store's
own `register`; Minstrel/Forge TS canon matched Python bodies by resemblance.
Every cross-project semantic proposal was noise.
- Canon carries the snippet's language; match_canon skips a sym canon whose
family (py / js / css / sh / sql, by language ↔ by path extension) differs
from the shape's. Unknown on either side = no gate.
- The reference basis ignores a stoplist of generic verbs (register, load,
save, get, …): a bare mention is not a call site of THIS canon; the symbol
basis still catches a second definition, and the call-site relation moves
to `uses` edges with #2870.
- The semantic arm only reaches canon in the shape's own project and family;
symbol/text still reach family canon elsewhere (note 2786).
- _PROPOSER_VERSION 2 → 3 so standing proposals re-examine on the next refresh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>