cd9aa87aa49fc8af20cdb2c30017cd712312d9f6
147
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cd9aa87aa4 |
feat(rules): tier 1 preloads, tier 2 arrives by area — and a split rule can no longer be read half-way (#3031, milestone 307 step 5)
CI & Build / Python lint (push) Failing after 6s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Failing after 26s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Skipped
The payoff step: a rule stops having to be resident to be honoured. - list_always_on_rules returns the ALWAYS-ON tier only. It is the session-start call, made before any project is in scope, so there is no area vocabulary to match a conditional rule against yet. - get_applicable_rules carries a conditional rule when the project works in an area the rule is tagged to — resolved through systems.canonical_id, so the project's own NAME for the area is irrelevant, which is the entire reason the catalog exists. The gate is applied IN SQL, so `limit` counts rules that will actually surface rather than rules about to be dropped. - Bindingness is a deterministic TAG match, never a similarity score (D7). The vector channel stays a suggestion, in search. co_surfaced_partners is the fix that rule 144 never had. It was split off rule 46 and folded back the same day because "either rule could surface without the other and miss exposing a project to what the entire shape is intended to be" — correct, and merging was the only remedy available. Now a partner ARRIVES with its other half even when nothing else selected it, tagged `via: co_surfaces` so the payload says why. Two limits, both deliberate: only rules the caller owns, because an edge is not a back door into someone else's rulebook; and a project's suppressions are passed as exclusions, because an explicit mute is a decision and an edge does not outrank it. COMPATIBILITY, asserted first in the integration test rather than reasoned about: a rule with no tier, no areas and no edges binds exactly as it did before any of this existed. `tier` defaults to always_on, so an install upgrades and every rule it already had keeps arriving. Getting that backwards would silently stop enforcing rules people rely on, which is worse than any amount of payload bloat. Four unit tests were coupled to the ORDER of a mocked session's execute() calls, so a new query broke them. Rather than pad the sequence and deepen that coupling, the three post-query lookups are stubbed by name — they have their own coverage, and the real wiring is proven against Postgres. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6ada97bb0b |
fix(rules): resolve the Rule forward ref for ruff and readers (#3030)
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 33s
CI & Build / TypeScript typecheck (push) Successful in 44s
CI & Build / Python tests (push) Successful in 1m11s
CI & Build / Build & push image (push) Successful in 23s
F821: the quoted `"Rule"` in semantic_search_rules' return annotation evaluates fine at runtime — a string inside a subscript is a value, not a name lookup — but it points at nothing a reader or a type checker can follow, which is what ruff is objecting to and it is right to. A TYPE_CHECKING import resolves it at zero runtime cost, while the real import stays inside the function so this module still does not pull in the rulebook models. Placed after the import block, where isort wants the guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
95a37318fc |
feat(rules): rules become findable by meaning (#3030, milestone 307 step 4)
CI & Build / Python lint (push) Failing after 9s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 44s
CI & Build / integration (push) Successful in 45s
CI & Build / Python tests (push) Successful in 1m26s
CI & Build / Build & push image (push) Skipped
Rules were the only major record type with no vector, so `search` could never return one and a rule could arrive only by being preloaded. That single fact is what made every rule compete for one always-on budget. THE DECISION THE TASK ASKED FOR, made explicitly: a sibling rule_embeddings table, not a polymorphic embedding row. The ROW could have been generalised; the SEARCH could not. semantic_search_notes is Note-specific scoping end to end — the visibility clause, the supersession penalty, note_type/task_kind/system filters — and a rule shares none of it, scoping instead by rulebook ownership or project. Generalising the row while still needing two searches is the worst of both: a key with referential integrity to neither table, on the path every session start runs, to share four columns. What is genuinely common is BEHAVIOUR — get_embedding, chunk_document, embedding_text, CHUNKER_VERSION — and those are reused as-is. Sharing them is the DRY win; sharing the table would have been the DRY costume. The document shape is measured, not chosen (note 2485). That pass found the snippet was the only discriminative record in the corpus — a 0.153 top-to-second gap against 0.010-0.023 — and that the cause was its SHAPE: purpose stated twice in a short single-topic document. rule_document reproduces it: the trigger in the title AND as the body's first line. And it excludes `why`, which matters more than any of it. `why` is dated incident narrative — rule 46's runs to 4,300 characters — and long multi-topic prose is exactly what made sixteen dev-logs mutually indistinguishable. Adding it would not give the vector more to work with; it would give every rule the SAME thing to work with. rule_document takes no `why` parameter at all, so a well-meaning caller cannot pass one. A rule with no trigger degrades to title + statement — findable, less sharp. That is an argument for backfilling triggers (step 6), not for padding the document with whatever text is nearby. search(content_type="rule") returns the rule WITH its why and how_to_apply: they are its operational half, the session payload never carries them, and a caller who went looking should not have to re-fetch. Writes re-index fire-and-forget like notes; startup backfills in its own try block so neither backfill can skip the other. rule_embeddings is derived, so it joins note_embeddings in the backup's explicitly-NOT-included list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ffb7a0fe38 |
feat(rules): both doors carry the trigger, the tier, the areas and the edges (#3029, milestone 307 step 3, surfaces)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 21s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Successful in 24s
MCP and REST both gain when_to_apply / tier / system_ids / arose_from_id on create and update, plus relate_rules / unrelate_rules for the typed edges, and get_rule now returns a rule's areas and relations alongside it. rule_detail() is a SERVICE function, not one per door. It started as a copy in each — identical, and the prior-art hook flagged it immediately, which is the same lesson rules_payload (#2858) already recorded: a second copy drifts. Both doors call the one seam, so create, update and get cannot disagree about what a rule looks like coming back. The authoring guidance lands in create_rule's docstring rather than in a rule, per rule 119 as the operator described it: this is behaviour every instance should inherit, not one operator's preference. It states the test — ONE RULE = ONE THING YOU COULD VIOLATE. Rules that FAIL TOGETHER get linked with relate_rules(kind="co_surfaces"), never merged into one row. — and names why merging loses: a merged rule cannot be cited, surfaced or suppressed a clause at a time, and it grows without limit because adding to it is always cheaper than adding a rule. create_project_rule says the same about "overrides", which is what FabledCurator's 85/86 should have been instead of near-copies that drift from their parent. The tier arg carries the test itself: can you name the trigger WITHOUT naming a system, an artifact type or a moment? If the honest answer is "whenever you are working", it is always_on. Tests: the applicable-rules cases fabricated raw tuples matching the old column lists, so they move to the entity shape via fake_rule; new cases pin rule_brief (a DATE not a stamp, the depth left to get_rule, no null keys) and that an unknown tier falls back to BINDING. fake_rule gains when_to_apply / tier / arose_from_id for the note-2109 reason the helper exists: unnamed, they would be truthy MagicMocks. The tool tests stub the new rule_detail seam — they are about argument forwarding and have no database. The module header's "Sixteen tools" had been wrong for two milestones; the registration count test is what actually catches that, so the header now says so instead of carrying a number. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6ddb8bf859 |
feat(rules): a rule can say when it applies, which area it is about, and what it belongs with (#3029, milestone 307 step 3, schema)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 47s
CI & Build / Build & push image (push) Skipped
A rule could not state its trigger, its area, or its siblings, so all three were being written as prose instead: a System's charter restating rule text, a `why` naming the note that caused it, and two halves of one shape merged into a single row because either could surface without the other. Migration 0088 adds the four fields those workarounds stood in for: - `when_to_apply` — the trigger. Nullable in the DB and required at the service layer: existing rules have none and a migration cannot invent one. - `tier` — always_on | conditional, defaulting to always_on. This migration therefore changes NOTHING about which rules bind; an install upgrades and every rule keeps arriving exactly as before. Getting that backwards is the one failure this milestone exists to prevent, so _valid_tier falls back to always_on rather than silently un-binding a rule with a typo'd tier. - `arose_from_id` — the record that caused the rule, the edge notes and tasks already have. SET NULL: trashing the source does not repeal the rule. - `rule_systems` / `rule_relations` — the canon tag and the typed edges (co_surfaces / overrides / elaborates), each earned from a workaround its absence forced. rule_brief() replaces the THREE hand-written trim dicts that had already diverged — two carried topic_id, one didn't, and none carried the timestamps the model has held all along. That omission is why a rule written before the capability it duplicates was indistinguishable at read time from one still doing work. It now carries updated_at as a DATE: the question is "how old is this", and a full stamp across the always-on set is ~2k characters for precision nobody reads. The two callers select the ENTITY rather than a column list, so rule_brief stays the single place deciding what a surfaced rule says. Backup: both new tables carried, area tags by canonical SLUG (ids are per-install). The rule-relation restore runs after ALL rules exist and after the catalog, because an edge names two rules and a tag names a global row — sections renumbered so the file reads in dependency order. A pre-0088 payload restores with tier=always_on, i.e. binding exactly as when it was taken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c58529718b |
feat(systems): the catalog reaches the moment a name is minted, and gets a face (#3028, milestone 307 step 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 48s
CI & Build / integration (push) Successful in 38s
CI & Build / Python tests (push) Failing after 57s
CI & Build / Build & push image (push) Skipped
Step 1 found the reason the standard names never held, and it is sharper than "prose doesn't fire": the list WAS real and it WAS seeded — but only on the inception path, for a project with zero Systems. Ad-hoc create_system never consulted it, which is how Forge minted "CI and Release" and Portal minted "CI & release" after the constant already existed. This wires the vocabulary to the moment that mints a name. - services/systems.assess_system_name: the local duplicate gate AND the catalog lookup, in ONE service function both doors call. The gate lived only in the MCP tool, which is exactly how the web UI shipped without a check the agent surface enforced (#2482). REST now answers 409 with the System that already covers the area. - An `exact` catalog hit is APPLIED (mechanical — the names differ only in spelling). An `overlap` is only OFFERED, on both doors: applying a judgment call silently is how a cross-project rule surfaces in the wrong project. - canonical_systems.best_overlap is the ONE scorer behind the create-time offer and the review sweep, so the two surfaces can never name different areas for one System. It also takes the catalog the caller already holds, so the review is not an N+1. UI (folded in from step 1 — rule 27, that step shipped with no human surface): - SystemsSection: a Shared area picker on create and edit, the area on each card, and a collapsed review of proposals that appears only when there is something to decide. `exact` and `overlap` never share a style — one is mechanical, the other is the reviewer's judgment, and presenting them alike is how a wrong mapping gets waved through. - Settings → Admin → Areas: the catalog itself, showing each entry's slug, because the slug is what decides whether two names are the same area and a rename moves it. - A picker rather than a live matcher: reproducing the slug rule in TypeScript would give this feature two matchers to keep in step — the exact drift the catalog exists to end. The server stays authoritative. tests/helpers.fake_system gains canonical_id=None: an unnamed attribute is an auto-MagicMock and therefore truthy, which is the trap that helper exists for (note 2109) and a nullable FK walks straight into it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a97547fbc6 |
feat(systems): the area vocabulary becomes a global table so a rule can point at one (#3027, milestone 307 step 1)
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 39s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 30s
CI & Build / Python tests (push) Failing after 56s
CI & Build / Build & push image (push) Skipped
The eight standard area names already existed — as STANDARD_SYSTEMS, a tuple in
services/systems.py that milestone 297 seeds at inception. A constant cannot be
a foreign key, so nothing outside a project could reference an area: systems.
project_id is NOT NULL, and a rule that spans projects would have to chain
itself to one project's row. And because the list only ever applied on the
inception-seed path, three spellings of one area reached this instance anyway
(CI & runners / CI and Release / CI & release).
- canonical_systems: global, no user_id — a shared project inherits the
vocabulary instead of re-earning it. Migration 0087 seeds the same eight.
- systems.canonical_id: nullable, SET NULL. Association only — no System is
renamed and record_systems is untouched, so no record's tags move.
- canonical_slug folds &/and, case and punctuation, so spelling variants map
mechanically and a real difference ("CI & runners") becomes a proposal a
human confirms. propose_mappings reports; set_system_canonical is the only
writer.
- seed_standard_systems now reads the catalog and maps as it mints, so a
project born standard never needs a reconciliation pass.
- Catalog writes are admin-only; reads are open — a global list anyone can
extend stops being shared.
- backup: carried by SLUG, not id (ids are per-install). Restore reuses the
target's own rows and only creates entries an admin added on the source; an
unknown slug restores unmapped rather than failing.
Rule 22: STANDARD_SYSTEMS is removed, not deprecated. Rule 115: nothing seeded
names an app, repo or house convention. Design in note 3026.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
64bfa5725f |
feat(telemetry): a read surface over retrieval_logs — the tuning loop had no read half (#2975)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m3s
CI & Build / Build & push image (push) Successful in 20s
`retrieval_logs` was write-only. `record_retrieval` inserted rows and nothing in the tree ever selected from them: the only `select()` over RetrievalLog lived in a test. So #1038's gate — "build the reranker once telemetry shows precision is the bottleneck" — was unsatisfiable by construction, and the one real tuning decision on record (the 0.68 write-path threshold, #2223) had to be reached by hand-probing the live instance with eight payloads. This adds the half that was missing. `retrieval_summary(user_id, days=30)` returns two aggregates side by side, each read from the table built for it — NOT a join. NoteUsageEvent's docstring is explicit that the two are complements ("RetrievalLog tunes the threshold, this tunes the corpus") and that RetrievalLog's JSONB `result_ids` cannot be indexed at the per-note grain, so correlating through it would be both slower and less honest than reading each source directly. That corrects the approach sketched on the task. - `sources`, per surface: calls, zero_result_calls, cleared_threshold (how often the best hit beat the threshold in force for THAT call), the top_score spread as p10/p50/p90/min/max, avg_result_count, p90 duration. Zero-result calls are counted apart from low-scoring ones — they are a different failure and averaging them together would hide both. - `usage`, from note_usage_events: ranked surfacings, ambient surfacings, and pulls split into `pulled_by_agent` / `pulled_by_human`. That split is not decoration. NoteUsageEvent's own comment says the mcp_/rest_ prefix is load-bearing and names #1038 while saying so: "is this dead weight?" is answered by any pull, "was that injected line useful?" only by an agent pull. `pull_through` exists to answer the second, so it counts agent pulls over ranked surfacings; both halves ship so the first stays answerable. Two things the code made me get right rather than guess: - Distinct-note counts get their own queries. `count(distinct note_id)` per (event, source) group cannot be summed across groups — a note surfaced by two sources is one distinct note and would be counted twice. A wrong number labelled "distinct" is worse than no number. - No CASE in the GROUP BY. #2663 is the bug where a second case() rendered its own expanding bind names, Postgres rejected the query, a broad except swallowed it, and every counter read zero in production while mocked tests passed. Grouping on raw `source` and classifying in Python cannot fail that way. For the same reason the readout distinguishes `read_failed` from an empty window, and its tests are integration against real Postgres — percentile_cont ... WITHIN GROUP only proves it parses against a database. Exposed as the `retrieval_telemetry` MCP tool, added to `_READ_ONLY_TOOLS`: it mutates nothing, but its name carries no read prefix, so the completeness test cannot derive it and it would otherwise have failed closed for read-only keys in silence — the same reason `enter_project` is spelled out there. Docs updated to name both exceptions rather than leave the rule looking derivable. Scoped to the caller's own telemetry: a retrieval log records what one user's agent asked for, query text included, and is not a shared record kind — the owner filter is the whole access rule, not a shortcut past access.py. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
446d6da0d7 |
fix(snippets): an annotated record is not a diverged one — a standing verdict vouches at its commit (#2782)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / integration (push) Successful in 29s
CI & Build / Python tests (push) Successful in 1m12s
CI & Build / Build & push image (push) Successful in 25s
Pull-time freshness confirms a cached body by containment: normalised cached code must appear inside the fetched file. That is right for a record kept verbatim and permanently wrong for a deliberately annotated one. A record whose job is to say WHY the shape is what it is carries commentary the source does not, so containment fails on every pull, forever — #2508 was reading `diverged` although its declarations match the source exactly, and always would. Annotation is a sanctioned record style, so this was two deliberate designs colliding, and it was quietly poisoning the one honest drift signal: decision #2707's scoreboard watches body_freshness for `diverged` and was accruing false positives it could never age out. The escape hatch is the verdict itself. verify_snippet is exactly where a human or agent already judged this body a faithful rendering of that source, and `verification.commit_sha` records the repo commit they judged it at — a field whose own docstring (#2688) anticipated this: "makes 'the REPO moved on since the check' computable, once the forge integration can compare it against the current head." This is that comparison. When containment fails, a standing verdict can still vouch, on four conditions and no fewer: - the verdict says `ok`; - it has not EXPIRED — verification_view recomputes code_sha against the record's current body, so editing the record retires the verdict; - it was not INVALIDATED by a push touching the location (#2691); - the file just fetched is at the very commit the verdict was stamped at. That last one is what keeps it honest: the hatch vouches for a body against ONE known commit, never against whatever the source became since. The moment the file moves, containment resumes as the authority and the record reads `diverged` until someone re-verifies — correct, because at that point nobody has looked. The first three are checked by reusing verification_view rather than restating its rule, so "expired" keeps meaning one thing. Nothing is rewritten and no new freshness value is minted; `verification` already travels in the same payload, so a reader can see the basis rather than take "current" on faith. Verdicts predating commit stamping carry no commit to compare and therefore do not vouch — they fall through to containment rather than passing on age alone. Tests pin the fix and, more usefully, every condition that switches it back off: moved commit, expired verdict, push-invalidated verdict, non-ok verdict, no verdict, and a pre-#2688 verdict with no commit_sha. Plus a regression that a verbatim record still takes the containment path untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
df18e897af |
feat(ledger): the consumer map reads transition names and concatenated prefixes (#2970)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 10s
CI & Build / integration (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 23s
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> |
||
|
|
8664d8ad14 |
feat(ledger): the consumer map surfaces — every css row carries used_by on list_shapes, flag="unused-css" (the map's negative space, surfaced never deleted), derive groups and the write-path family carry consumers, the derive line says "used by N template(s)", coverage payload unused_css + the standing block; docs, SKILL, plugin 0.1.44; the two step-2 tests expected 5 edges where fan-out makes 6 (milestone 302 step 3, #2936)
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python lint (push) Successful in 4s
CI & Build / TypeScript typecheck (push) Successful in 38s
CI & Build / integration (push) Failing after 44s
CI & Build / Python tests (push) Successful in 1m26s
CI & Build / Build & push image (push) Successful in 31s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ffbdf19116 |
feat(ledger): the CSS consumer map — code_shape_consumers edges (shape → file whose markup names the class, count), migration 0086, resolve_consumers (own-file row when the template defines the class, else every other definition), sync_repo_consumers rebuilt from the archive on every refresh, consumers_of; derived, so not backed up (milestone 302 step 2, #2935)
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Failing after 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 52s
CI & Build / Build & push image (push) Skipped
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dffbf43d84 |
feat(coverage): class_references + scan_archive — each template-bearing file's class tokens (static class=/className=, Vue :class object/array/ternary, React className={…}, Svelte class:x) read in the same tar walk as definitions; the CSS consumer map's extractor (milestone 302 step 1, #2934)
CI & Build / Plugin hooks (push) Failing after 1s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / integration (push) Successful in 29s
CI & Build / Python tests (push) Successful in 1m2s
CI & Build / Build & push image (push) Successful in 29s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
85111442a6 |
feat(ledger): CSS derive families are names, never bodies — name floor 2 for css, dup: grouping sym-only; derive line says "repeated name" and dismisses scoped-css; plugin 0.1.41 (note #2917)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 25s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
590203a293 |
refactor(tests+frontend): one http_sink helper for the hook tests; apiErrorMessage replaces ten hand-rolled error-body parses; type X, import specifiers are not definitions (#2904, milestone 299 step 6)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 9s
CI & Build / integration (push) Successful in 28s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m11s
CI & Build / Build & push image (push) Successful in 38s
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>
|
||
|
|
a72605de8f |
refactor(frontend): the last pay-down, part 1 — three dead views deleted, editor rules shared, .page-container + .fs-input canon, rules-shared.css; a one-declaration CSS body is not a shape (#2903, milestone 299 step 5)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / integration (push) Successful in 34s
CI & Build / Python tests (push) Failing after 54s
CI & Build / Build & push image (push) Skipped
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> |
||
|
|
2324c15418 |
feat(write-path): the derive arm — the hint names a duplicate family (no canon) or a canon elsewhere for the shapes being written; exclude_derive channel (#2900, milestone 299 step 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Failing after 9s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 23s
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> |
||
|
|
bb242ca566 |
feat(coverage): the line names standing work with 0 unclassified + derive_new, copies that joined a family since the previous refresh (#2899, milestone 299 step 1)
CI & Build / TypeScript typecheck (push) Successful in 40s
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Successful in 24s
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> |
||
|
|
c7a58bb610 |
feat(inception): the doors — create_project/decide_project_inception take the decision, enter_project asks until decided, REST inception endpoints, _INSTRUCTIONS (#2882, milestone 297 step 4)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 26s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Failing after 45s
CI & Build / Build & push image (push) Skipped
- 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> |
||
|
|
34734bf84a |
feat(inception): services/inception.decide() + current_defaults(); the standard Systems vocabulary moves to the service and seeds at inception (#2881, milestone 297 step 3)
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / integration (push) Successful in 36s
CI & Build / Python tests (push) Failing after 53s
CI & Build / Build & push image (push) Skipped
- 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> |
||
|
|
ff5f6438c4 |
feat(inception): always-on exclusions reach every rule surface — list_always_on_rules(project_id), get_applicable_rules, rules_payload.excluded_always_on, session context, exclude/include tools (#2880, milestone 297 step 2)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / integration (push) Successful in 1m3s
CI & Build / Python tests (push) Failing after 1m12s
CI & Build / Build & push image (push) Skipped
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>
|
||
|
|
e9b8f525c8 |
feat(inception): projects.inception record + project_rulebook_exclusions — migration 0085 with legacy backfill; backup v10 (#2879, milestone 297 step 1)
CI & Build / Python lint (push) Successful in 6s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 42s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m36s
CI & Build / Build & push image (push) Successful in 36s
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>
|
||
|
|
aba16583ab |
fix(ledger): one-line CSS rules fingerprint their own declarations; the proposer writes uses edges for judged rows too (#2872, #2870)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 22s
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>
|
||
|
|
6a0f8ad328 |
feat(backup): code_shape_uses travels (v9); consumer-map test expects uses (#2870)
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / Python lint (push) Successful in 3s
CI & Build / integration (push) Successful in 22s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 25s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d4c7b0e48d |
feat(ledger): uses edges — consumption is its own relation, conformance keeps one snippet_id (#2870, milestone 294)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Failing after 29s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / Python tests (push) Failing after 39s
CI & Build / Build & push image (push) Skipped
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> |
||
|
|
1a8e5787e8 |
feat(ledger): reason codes, case-insensitive repo filter, next action on the coverage line (#2874, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Canceled after 32s
CI & Build / Python tests (push) Canceled after 31s
CI & Build / Build & push image (push) Canceled after 0s
- 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> |
||
|
|
1209e1c2d9 |
feat(ledger): a repo binding names the branch its ledger follows — bind_repo(ref=) (#2873, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Canceled after 30s
CI & Build / Python tests (push) Canceled after 30s
CI & Build / Build & push image (push) Canceled after 0s
CI & Build / integration (push) Failing after 25s
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>
|
||
|
|
57d68c9355 |
feat(ledger): derive readout ranks body-identical groups first; CSS fingerprints are declarations, not selectors (#2872, milestone 294)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 32s
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> |
||
|
|
1ab614bfbe |
feat(ledger): the scoped bucket — by-construction one-offs are stamped by the sync, not judged by a person (#2869, milestone 294)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 23s
CI & Build / integration (push) Failing after 25s
CI & Build / Python tests (push) Canceled after 51s
CI & Build / Build & push image (push) Canceled after 0s
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> |
||
|
|
9abc4443fb |
feat(ledger): audit surfaces — list_shapes(compact=True) and classify_shapes_by_rule, the sweep form of a judgment (#2868, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m0s
CI & Build / Build & push image (push) Successful in 26s
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> |
||
|
|
3a4031d7f8 |
fix(ledger): proposer v3 — sym bases gated by language family, reference skips generic verbs, semantic held to the shape's own project (#2871, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 57s
CI & Build / Build & push image (push) Successful in 26s
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> |
||
|
|
7d48eb0b1b |
refactor(services): one periodic-task shape, one APScheduler job shape, one token hash, one summary rule — the services pass of the shape audit (#2830, milestone 296)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 12s
CI & Build / integration (push) Successful in 24s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 24s
- 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> |
||
|
|
64c641ce80 |
refactor(routes): one supersession seam for REST and MCP; PUT/PATCH notes share a handler; shared mask/not-found/caller helpers (#2829, milestone 296 area 5)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
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> |
||
|
|
c211e12b61 |
refactor(mcp): one rules_payload() for every surface that hands rules to an agent; drop the dead bearer resolver (#2828, milestone 296 area 4)
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Successful in 24s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 56s
CI & Build / Build & push image (push) Successful in 26s
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> |
||
|
|
dad56a51bd |
fix(ledger): the proposer matches signatures against a canon's own definition only and floors semantic at 0.8; ruleset version re-examines standing proposals
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 31s
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> |
||
|
|
efc650390e |
fix(plugin-context): the empty write-path payload carries the divergence key too
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 19s
CI & Build / TypeScript typecheck (push) Successful in 22s
CI & Build / Python tests (push) Successful in 1m3s
CI & Build / Build & push image (push) Successful in 42s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d74a244b3a |
feat(ledger): divergence readout — button B where button A is canon, shape history, and judged-shape recheck (#2793, milestone 294 step 7)
CI & Build / TypeScript typecheck (push) Failing after 2s
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 / Python tests (push) Failing after 37s
CI & Build / Build & push image (push) Skipped
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> |
||
|
|
386b27e422 |
fix(ledger): the proposer prefers a same-project canon on a tie — family canon elsewhere is the fallback, not the first hit
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / Python tests (push) Successful in 1m17s
CI & Build / Python lint (push) Successful in 5s
CI & Build / integration (push) Successful in 35s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / Build & push image (push) Successful in 40s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ba0030e51d |
feat(ledger): mechanical proposer — every refresh proposes instances against canon and groups derive-first candidates; agents confirm in batches (#2792, milestone 294 step 6)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Failing after 34s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 1m17s
CI & Build / Build & push image (push) Successful in 48s
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> |
||
|
|
a9e1cddba7 |
fix(tests): provisional ledger rows carry the empty seen-marker default, not NULL
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 31s
CI & Build / TypeScript typecheck (push) Successful in 39s
CI & Build / Python tests (push) Successful in 1m1s
CI & Build / Build & push image (push) Successful in 25s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
475f0857c9 |
feat(ledger): write-path stamping — a pulled canon the session then instantiates lands as a hook instance row (#2791, milestone 294 step 5)
CI & Build / Plugin hooks (push) Failing after 2s
CI & Build / Python lint (push) Successful in 4s
CI & Build / integration (push) Failing after 28s
CI & Build / TypeScript typecheck (push) Successful in 36s
CI & Build / Python tests (push) Successful in 1m5s
CI & Build / Build & push image (push) Successful in 39s
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> |
||
|
|
e2a084f1fb |
feat(ledger): coverage self-seeds — enter_project background refresh + refresh_pattern_coverage tool + shape-accounting skill (#2802, milestone 294)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 23s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Failing after 33s
CI & Build / Build & push image (push) Skipped
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> |
||
|
|
942edd1eb5 |
feat(ledger): classify_shapes + list_shapes MCP tools; get_snippet carries the consumer map (#2789, milestone 294 step 3)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 27s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Failing after 42s
CI & Build / Build & push image (push) Skipped
The judgment write path. classify_shapes applies a batch of classifications to a project's live ledger rows — all-or-nothing (#2709's lesson: the whole batch is validated, write-ACL'd, and every snippet target proven readable before any row is touched); rows match by exact (path, symbol), kind narrows, and shapes no live row matches come back as 'unmatched' rather than errors. variant/exempt REQUIRE the reason — the why is the record (note 2786) — and 'unclassified' deliberately withdraws a judgment back to the todo. The 'via' channel is caller-restricted to agent|audit|import; hook and mechanical stay server-internal so a caller can't launder judgment as machinery. list_shapes is the todo query (status=unclassified) with composable filters: path is exact-or-under like recorded locations, snippet_id reads a consumer map, include_vanished reads history; paged with the true total. get_snippet now attaches and — the structured consumer map, filtered to projects the CALLER can read so a shared snippet never side- channels another project's file layout; attached only when non-empty (#2483). Integration tests pin the batch atomicity, ACL gates, filter composition, the consumer map on the MCP pull, and the SET NULL companion: a judgment whose snippet was purged rejoins the todo on the next sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9b1597a3c9 |
feat(ledger): coverage refresh feeds the shape ledger; the readout inverts to accounting (#2788, milestone 294 step 2)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 27s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 44s
compute_coverage is now the ledger's sync point: every walk upserts the extracted shapes (new → unclassified, the todo state; surviving → last-seen bump; vanished → stamped, kept as history), re-files judgments whose snippet target went away, and mechanically stamps snippet reference locations as canonical — the one always-safe rule, self-healing only for its own stamps (an agent's judgment is never unwound by machinery). The covering predicate moves to shape_ledger.location_covers as the single home (match_shapes retired with its consumer); coverage's payload and line invert from 'N/M shapes recorded' to shape ACCOUNTING per note 2786: accounted/total with a canonical·instance·variant·exempt breakdown, and unclassified — THE todo — with its largest directories. Cache key bumps to v2 so pre-ledger blobs honestly read 'not measured yet' instead of rendering in a shape no longer spoken. Readout is deliberately project-wide (all repos' live rows), while the walk serves whichever repos the owner's keyring reaches this refresh. Integration tests pin the new contract: rows for every extracted shape, mechanical canonical stamps carrying snippet ids, idempotent recompute, agent judgments surviving recompute AND vanish/return, vanished rows leaving the readout but keeping their history. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
19fdc9aa89 |
feat(ledger): code_shapes — the shape ledger schema (#2787, milestone 294 step 1)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / TypeScript typecheck (push) Successful in 13s
CI & Build / integration (push) Successful in 25s
CI & Build / Python tests (push) Successful in 54s
CI & Build / Build & push image (push) Successful in 45s
The accounting half of the pattern system (governing note 2786): the snippet library records canon (small), this table accounts for EVERY extracted shape (total). Identity is (project, repo_key, path, symbol, kind) — kind included because one file can define '.foo' (css) and 'foo' (sym) as distinct shapes. Status vocabulary: canonical / instance / variant / exempt / unclassified, with unclassified as the default and THE todo state; classifications carry who judged (agent|audit|hook|mechanical|import), when, and the why for variants/exemptions. first/last-seen commits + vanished_at keep history instead of deleting it; a rename reads as vanish+new (accepted for v1). snippet_id is SET NULL on snippet deletion so accounting rows outlive their target and rejoin the todo via the step-2 sync, never dangle silently. Backups: v7 carries code_shapes (judgment data, worth moving) — full and per-user export sections, and a restore that keeps a judgment only when its snippet survives the id re-mapping, downgrading to unclassified otherwise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1faf8f3ece |
feat(forge): per-user forge connections — keyring, host-keyed resolution, project pin (#2778)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 41s
CI & Build / integration (push) Successful in 37s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 40s
A forge token is a user's credential, not an instance's. The single admin-settings config is replaced by per-user keyring rows (one per forge host), and every server-side forge read runs on the PROJECT OWNER's keyring: - forge_connections table + projects.forge_connection_id pin (migration 0078, which also carries the existing admin config into the first admin's row and deletes the old setting keys — no legacy dual-read) - get_forge() replaced by get_forges(owner_id, project_id) -> ForgeSelector; resolve(repo) picks the connection whose host serves the repo. A pinned project uses ONLY its pinned connection; a stale pin (ownership moved) is ignored, never honored across users - env FORGE_* config survives as an implicit entry for admin owners only; a stored row for the same host beats it - consumers threaded: pull-time freshness (owner of the note), coverage (owner of the project), coverage routes' configured flag - routes: /api/settings/forge-connections CRUD + per-connection test (own-rows only, tokens never returned); /api/admin/forge shrinks to /api/admin/forge-webhook (secret only); PUT /api/projects/<id>/forge pins, owner-or-admin asking, owner's connections only - UI: Git Forges card moves to Settings -> Integrations as a connection list; webhook secret stays in the admin Config tab; owner-only forge select on the project coverage card - backups exclude forge_connections (credentials, api_keys precedent) and the pin, so restores fall back to keyring resolution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3162332a13 |
feat(prior-art): edit-time record-sync nudge — the sync class (#2708)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 24s
CI & Build / integration (push) Successful in 26s
CI & Build / Python tests (push) Successful in 56s
CI & Build / Build & push image (push) Successful in 43s
A snippet recorded AT the exact file being edited is not a reuse suggestion — it IS the record of the file being changed. The write-path hint now renders those as their own SYNC class: 'snippet #N records this file — updating the record is part of the edit (update_snippet / verify_snippet)'. Nearby and semantic hits stay the reuse menu. The two classes dedup on separate per-session channels (exclude_ids vs exclude_sync_ids, .ids vs .sync.ids in the hook), so a reuse hint shown early in a session can no longer silence the record-sync nudge when the recorded file itself is edited later. Sync surfacing is measured under its own note_usage source (write_path_sync) — its pull-through rate is the scoreboard for whether edit-time sync actually happens, per decision #2707 (no forge connection; records stay current in the session that has the context). Plugin 0.1.31. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
765635bbf2 |
feat(forge): GitHub adapter — second implementation keeps the seam a contract (#2693, milestone 288 step 8)
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 27s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m8s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Build & push image (push) Successful in 45s
ForgeAdapter is now a named base class carrying the shared plumbing (host join, error taxonomy, contents decoding, archive, default_branch, latest_commit); GiteaForge keeps its exact behavior and GitHubForge joins with the real differences: api.github.com / GHE /api/v3 host mapping, Bearer auth, a commits call for the provenance stamp (GitHub's contents payload only carries the blob sha), and the codeload tarball redirect. The contract grew latest_commit, and with it the cached-SHA short-circuit in pull-time freshness: a stored provenance commit that still heads the recorded path confirms 'current' without a content transfer — the economy that fits pulls inside GitHub's rate limits; every surprise falls back to the full fetch. Webhook deliveries now also accept X-Hub-Signature-256 (sha256=<hex>); the payload shape was already common. Settings card copy covers both forges' token scopes; the kind selector already flowed from the server. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
cbccb6bd5d |
feat(coverage): pattern-library coverage measurement (#2692, milestone 288 step 7)
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 25s
CI & Build / Build & push image (push) Successful in 41s
Server-side shape enumeration per bound repo — one archive download via the forge adapter, definitions extracted with a Python mirror of the write-path hook's awk rules (shared test vectors pin the two together) — compared against recorded snippet locations by path+symbol. Summary is cached in the settings KV with a freshness stamp; recomputed on webhook push (spawned off the delivery path) or explicit refresh, never in a request path. Surfaces: GET/POST /api/projects/<id>/coverage[/refresh], a project-page card (estimate-labeled, largest-gaps chips), and a one-line evidence-carrying entry in enter_project read from cache only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
89b07f7857 |
feat(forge): push webhook flags drift at the moment the repo moves (#2691)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 22s
CI & Build / Python tests (push) Successful in 53s
CI & Build / TypeScript typecheck (push) Successful in 22s
CI & Build / Build & push image (push) Successful in 39s
Second adapter consumer. POST /api/webhooks/forge validates Gitea's
X-Gitea-Signature (HMAC-SHA256, constant-time; no secret configured =
the endpoint 404s out of existence), extracts changed/removed paths,
and flags matched snippets by writing verification.invalidated_by
{commit_sha, at, path, removed} — the existing attention vocabulary
extended, not a new flag: needs_attention includes it, both filter
dialects (Python + jsonpath SQL) include it in 'attention' and exclude
it from 'ok', and recording ANY fresh verdict clears it by construction
because compose_verification builds a new dict. Unverified snippets are
skipped (already in their own bucket); replayed deliveries at the same
head commit are no-ops; processing failures return 200 with a WARNING +
AppLog canary so the forge never marks deliveries failed and operators
never disable the hook over a transient (#2663's lesson).
Matching goes through repo BINDINGS: recorded location repos are
free-form names ('Scribe') that cannot address a forge, so a snippet
reaches its forge repo through its project's binding — which also fixes
step 5's pull-time resolution for every real record via the same
fallback. O(bindings + snippets-in-project + changed files).
Settings: webhook secret beside the forge config (masked, sentinel-
skipped, Docker-secret env channel, endpoint documented in the UI).
Tests: signature gate, payload parsing, path semantics, both filter
dialects extended in the drift-check guard file, and real-Postgres
end-to-end (flag lands, attention lists it, replay quiet, re-verify
clears, unbound repo untouched).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|