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>
- assets/auth-shared.css: the five auth views carried byte-identical scoped
copies of the page/card/brand/footer/field/input/error rules (~60 lines
each); they now load one stylesheet the way the editors load
editor-shared.css. .closed-msg/.error-block/.success-msg (identical bodies)
are one .auth-note; the form rules are scoped under .auth-card so nothing
leaks into the rest of the app.
- api/client.apiErrorMessage(e, fallback): the one place the {"error"} envelope
is unpacked; replaces ten six-line `"body" in e` catch blocks.
- utils/dateFormat: fmtDate / fmtStamp / fmtLogStamp replace eight local
formatDate/formatTime copies (three byte-identical pairs); the file’s old
Calendar/Home helpers had no callers and are gone. useRelativeTime gains
relativeTimeOrDate for the two workspace panels’ identical variant.
- components.css now owns the .modal-* shape (overlay/card/title/message/
actions/btn/primary/danger). It was copied into four views and lived in
editor-shared.css, which ConfirmDialog — styleless, teleported to <body> —
silently depended on: opened from SnippetDetailView before any editor view
had loaded, it rendered unstyled. Views keep only their own overrides.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- background.start_periodic(interval, work, label=) replaces the three hand-rolled
while-True/sleep/try loops in logging, auth and notifications.
- services/scheduler.ScheduledJob replaces the four private BackgroundScheduler
copies in recurrence/version_pinning/trash/db_maintenance schedulers; public
start_/stop_/reschedule_ surfaces unchanged.
- api_keys.hash_token is the one sha256 helper; auth.py used to inline it 5x.
- auth.is_registration_open reads via settings.get_admin_setting; notification
prefs read via settings.get_setting; _fire_share_email uses _get_user_email.
- projects.get_project_summary / milestones.get_project_milestone_summary are
now the one-id view of their batch siblings instead of a second copy of the
queries; sharing.best_permission_by (was _deduplicate_by_permission) is the
one rank-dedup, now also used by list_projects_for_user.
- backup: the row builders for every section both exporters carry are named
functions, so a column added to one export cannot silently miss the other.
- iso() from models.base replaces the attr.isoformat()-if-attr-else-None idiom
and db_maintenance._iso across services; backup keeps its explicit shape.
- trash.py hoists the sql_delete/timedelta imports it re-imported per function.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reading the 28 route modules against each other and against the MCP tools:
- routes/notes.py carried a PUT and a PATCH handler that were the same
function minus the supersedes contract on one of them — one handler now
serves both verbs, so both carry it.
- The two _attach_supersession copies (REST + MCP) become
supersession_svc.attach_relations(uid, note_id, data, hint=) — the seam
the two surfaces must agree through; only the agent surface adds the
one-sentence reading hint.
- Three local _uid() wrappers over g.user.id → scribe.auth.get_current_user_id
like every other module; design_systems' private _not_found → routes.utils.
not_found; the four "********" literals → settings_svc.SECRET_MASK with the
read/write contract written once.
- routes/plugin.py: the project_id/repo resolution block and the
comma-separated id parse were copied into three endpoints — _project_scope()
and _int_list() now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>