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>
Reading the 16 tool modules against each other: the six-key applicable-rules
block (applicable_rules, applicable_rules_truncated, subscribed_rulebooks,
project_rules, suppressed_rules, suppressed_topics) was hand-built in five
places — enter_project, get_project, get_task (legacy plans), get_milestone
(three of the six) and services/planning.start_planning. rulebooks_svc.
rules_payload() is now the one place that names them; get_milestone gains the
three it lacked, so every rules-carrying payload reads the same. list_rules /
list_always_on_rules share _rule_summary. mcp/auth.resolve_bearer_to_user_id
duplicated resolve_bearer's parsing and had no product caller (only its own
tests) — removed; the tests now exercise resolve_bearer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reading all 28 models against each other: 54 `x.isoformat() if x else None` /
`x.isoformat()` expressions in 23 to_dict methods, in two guarded/unguarded
wordings, become iso() from models/base.py — uniform, and a row read before
flush serialises as null instead of raising. Rulebook / RulebookTopic / Rule
carried byte-identical copies of TimestampMixin's two columns;
InvitationToken / PasswordResetToken / NoteUsageEvent carried CreatedAtMixin's —
all six now use the mixin. AppLog and RetrievalLog keep their explicit
created_at, commented: their composite index orders on `created_at.desc()`,
which needs the column object in the class body. Schema-neutral (same column
definitions) — no migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Second pass over the tests/ ledger after bbee0d0. fake_record(**attrs) is the
one MagicMock-with-real-attributes builder (to_dict mirrors them; the
note-2109 hazard documented once); fake_note/fake_task/fake_snippet/
fake_project/fake_milestone/fake_system/fake_rulebook/fake_topic/fake_rule
carry each model's ordinary defaults on top of it, replacing 14 per-file
factories (two rulebook trios in tool-vs-service wordings, _fake_task, _fake_ms,
_fake_project, _plan_note, _fake_snippet, two _snippet adapters now one-liners
over fake_snippet). FakeMCP replaces the five closure-over-a-list registrar
fakes (+ _Recorder); loc() and design_token_stub() replace the paired _loc /
_token / _T stand-ins; every hand-built async_session mock (9 helper defs and
14 inline copies) now starts from make_mock_session(). Call sites rewritten by
AST with each file's former defaults made explicit, so behaviour is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The shape ledger showed the same test scaffolding defined over and over:
_bind_user x12 (byte-identical), _dispose_engine x10 in three wordings,
_no_supersession x3, _make_mock_session x7 in three subsets, a get-or-create
User helper x2 (+3 inlined), and fifteen hand-rolled MagicMock note factories
each re-explaining the same "an auto-MagicMock attribute is truthy" hazard
(note 2109).
Now: conftest.py carries _bind_user / _dispose_engine / _no_supersession as
opt-in fixtures (pytestmark = usefixtures(...) per module, so unit tests pay
nothing), and tests/helpers.py carries make_mock_session(), ensure_user() and
fake_note(**attrs) — the hazard documented once, real values on every
attribute the product reads. Call sites were rewritten by AST so titles with
dashes and commas survived; the three SimpleNamespace _note stand-ins that
only feed a single function stay local.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First live run: a canon whose recorded code opens with a call-site example
(confirmed()'s onTrash) made every `async function x(): Promise<void>`
resemble it at 0.86, and the write-path semantic floor paired alembic
upgrade()/downgrade() bodies with unrelated canons at 0.68-0.75. Rows now
remember (body, ruleset) so a tightened rule looks again once.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every judgment now goes through one helper that remembers the fingerprint
judged (classified_sha) and writes a code_shape_events row; the sync writes
vanished / reappeared / drifted events and flags recheck_at when a body
moves under an instance/variant. The refresh flags diverges_from on shapes
new since the previous computation that sit where one canon dominates the
judged siblings of their directory+kind and were not proposed as that canon
(a first seed flags nothing); the write-path hint asks the same question
in-band for the shapes the hook names. list_shapes(flag=divergence|recheck),
shape_history(project_id, path, symbol) (read-only), coverage line/payload/
card carry divergent + recheck. Backup v8 carries the history. Plugin 0.1.36.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shapes now carry a content fingerprint (signature + whitespace/comment-
insensitive body_sha; migration 0080) and the proposer runs inside the
coverage refresh, the one moment bodies exist: symbol elsewhere → textual
containment → body references the canon → signature resemblance → semantic
(capped per refresh, unreached rows stay unexamined for the next). A hit is
a proposal on the row (proposed_snippet_id/basis/score), never a
classification; rows with no canon hit group by the derive-first rule
(identical body in ≥2 places, same name in ≥3 files) as proposal_basis=
derive + a group key. list_shapes(proposal=any|canon|derive|<basis>) is the
queue; confirm_shape_proposals(project_id, snippet_id|path|basis) confirms
in batches as agent instances; any classify_shapes/hook stamp retires the
proposal. Readout carries proposed + derive_groups (line, payload, card).
Plugin 0.1.35 (skill: the machine proposes, judgment classifies).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prior-art hook now names the shapes being written (shapes=kind:name —
every definition in the payload, or the one enclosing an Edit found by
walking the file upward) and the server stamps them as instance rows when
the session PULLED a snippet inside PULL_WINDOW that the payload references
by symbol or that the semantic arm scored for this very payload.
classified_by=hook, evidence in reason; never overrides a judgment or a
canonical row, overridable by classify_shapes. Offered-but-unopened stamps
nothing. Pulled-and-already-seen snippets stay in the semantic query as
evidence without re-entering the deduped menu. A brand-new shape gets a
provisional row the next sync confirms or vanishes. Read-scoped keys get
the hint, never the stamp. Plugin 0.1.34.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The UI Refresh button must not be the only seed path (operator directive,
hit live: the P7 backfill stalled waiting for a click). Three parts:
- enter_project fire-and-forgets refresh_if_stale on the project OWNER —
absent or day-old readouts recompute in the background (same spawn the
webhook path uses), the enter stays fast, forge-less owners exit quietly
(rule #115 baseline), and an in-flight guard keeps concurrent enters from
fetching the same tarball N times.
- refresh_pattern_coverage(project_id): the synchronous agent-facing form —
write-gated, owner-keyring resolution, and ValueError messages that name
the fix (add a connection / bind_repo) instead of measuring nothing
silently.
- plugin 0.1.33 ships the shape-accounting skill: the five statuses, the
seed/todo/judge loop, and the derive-first rule, triggered by the
coverage line or any proved code-to-canon relationship.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>