v1.8.0
·
5 commits
to main
since this release
Added
core item list: multi-type filtering, absolute created bounds, sort, and pagination.--typeis now repeatable and OR-joins within the set (--tagstays AND-joined); new inclusive--created-after/--created-beforeepoch-ms bounds compose (AND) with the duration-typed--sincewindow;--sort created|updated|title|type|access_countis validated against an allowlist (unknown key →invalid_argument) with a deterministicid ASCtie-break;--ascflips the default DESC direction;--offsetpages through the filtered set (negative →invalid_argument). The envelope gainstotal— the filtered count independent of limit/offset — so callers can drive pagination, and rows now carrysummary+access_count. Spec:cli-shape:item-listupdated (including the stale--since TIMESTAMP→ duration correction), acceptance rowsA-item-5/A-item-6added, and the off-vocabulary--type Documentfixtures inA-item-1..4lowercased to the stored type vocabulary.- Dashboard
/memorybrowse + search rebuild: type facet chips with global counts plus an "All" chip; browse defaults to the knowledge facet (document/reference/project/feedback) so code symbols stop swamping the page — an explicit?type=allclears the filter and round-trips as such (an absent param would re-apply the default facet); search (?q=) never default-narrows. Both modes take repeatable AND-joined?tag=filters and an inclusive?after/?beforecalendar date range (UTC day-start / day-end). Browse adds sortable column headers (title / type / created / access count, direction-flipping, backed by theitem listsort allowlist), a total-items count, page-size-50 pagination via the newly vendored templUI pagination component (filter-preserving links,aria-currenton the current page, bfcache-safe loading skeleton), and richer rows (summary, created date, access count). The detail page (/memory/{id}) gains created/last-accessed dates, tag chips sorted human-authored-first with machineprefix:valuetags grouped last, full content, and a "View in graph" link deep-linking/graph?focus=<id>. Under the hood, the facet counts come from a new global per-type count read (RunItemTypeCounts, dashboard-internal — no CLI verb), and the search date range rides new inclusiveCreatedAfter/CreatedBeforeepoch-ms bounds on the recall filters (applied in both the no-query seed path and the post-filter pass); no CLI flags expose the recall bounds. - Dashboard 3D graph (
graph.js): filter-rail state persistence. The rail's durable state — type/repo checkbox truth plus the heat-slider cutoff — now round-trips throughlocalStorage["anton.graph.rail"]({types, groups, heatCutoff}, written on every manual checkbox change, slider input, and?focus=force-enable; a Go asset-guard pins the key), so a reload lands on the same filtered view. Restore is defensive (storage throws, parse errors, and shape/range violations all fall back to defaults and are overwritten by the next persist); stale keys for vanished types/repos are dropped, payload values missing from the snapshot default to checked (a new repo appears visible), and the sessions-default-off rule now fires only on a first-ever visit — an existing snapshot is the user's curated view.?focus=wins over stored state for the target's facets and re-persists the result; isolate/neighborhood overrides stay deliberately session-transient. Riders from Task 16's review: a slider drag during an active isolate/neighborhood now behaves like any manual rail gesture — it drops the override (restoring the pre-solo checkbox snapshot, since a drag carries no checkbox intent) before applying the cutoff, so a "blind" cutoff can never be set or persisted under an override; the global Escape handler skips the in-graph search box (Escape there means "clear my input", not "clear the isolate"); and the drawer route-prefix guard test asserts the code-level'<prefix>' + encodeURIComponent(expressions instead of bare prefixes that comments would satisfy. - Dashboard 3D graph (
graph.js+/graphview):?focus=deep-link, isolate modes, and type-aware drawer routing + summaries.GET /graph?focus=<id>(the target of memory detail's "View in graph" link) embeds the id as adata-focus-idattribute on the canvas mount (HTML-escaped by templ; a Go test pins the escaping); after the first render graph.js looks the node up in the FULL set —?focus=wins over current facet state, so a default-hidden session node gets its type/group facets force-enabled — then flies the camera to it and flashes it (the search's fly/highlight helpers, after polling briefly for the node's seeded coordinates + mesh); an unknown id — a legitimate state, since the node cap can evict the link's target — surfaces a visible notice strip plus the aria-live announcement, and poll exhaustion leaves aconsole.warnevidence trail. Two isolate overrides land as an internal layer over the checkbox state (thecodegroup shares its types with every repo well, so a solo is NOT expressible through checkboxes), exposed via the rail's closed verb API (kept/ensureVisible/isolate/neighborhood/clearIsolate): legend rows become the wells' click-to-solo labels (event-delegated — rows are rebuilt every filter pass; repo checkboxes mirror the solo where expressible, from a snapshot; re-clicking the soloed row toggles off), and the drawer gains a "Focus neighborhood" button showing exactly the node ∪ its direct link-neighbors (facets + heat cutoff deliberately bypassed). Escape or the new Clear chip (data-graph-clear-isolate) restores the pre-isolate state; a manual checkbox change instead drops the override and keeps the visible state. The drawer's detail link now routes by node type —symbol|module→/graph/symbol?id=…, everything else →/memory/<id>(a string-level Go guard pins both prefixes in the served asset) — and non-code nodes render their payloadsummaryunder the title (absent summary renders nothing). Riders from review: the in-graph search gains a visually-hiddenrole="status"live region for its no-match message, its placeholder now mentions Enter, and a pending highlight is preempted before the capability guards so a failed follow-up highlight can't strand the previous node white. - Dashboard 3D graph (
graph.js+/graphview): in-graph search + heat-threshold slider. A client-side search box above the filter rail (distinct from the header's/graph?q=symbol-search form, which navigates server-side — both stay): Enter substring-matches node labels case-insensitively over the visible (filter-kept) set, and the first match gets a 1200mscameraPositionfly-to plus a 2.5s white colour-flash on its mesh (base colour stored + restored; a second search before the restore fires restores the previous node first, so no highlight leaks — fly/highlight helpers are reused by the upcoming?focus=deep-link). No match → the input shakes (.graph-shakekeyframes ininput.css, shortened to ~instant underprefers-reduced-motion) and carries atitleexplaining why; both clear on the next keystroke. The heat slider (0–100, default 0 with a live % readout) hides nodes whoseheatfalls below the cutoff — composed as a third conjunct inside the SAME single filter pass as the type/repo facets (onegraphData()re-feed per change, links keep the both-endpoints-visible rule), with the sameclampHeatnormalisation the renderer's brightness curve uses. - Dashboard 3D graph (
graph.js): gravity-well group clustering — every distinct nodegroupgets a fixed anchor on a horizontal ring (radius 320; groups sorted so anchor assignment is deterministic across loads), and a weak hand-rolled positional force (the vendored bundle exposesGraph.d3Forcebut no global d3, so noforceX/Y/Zto borrow) nudges each node toward its group's anchor per simulation tick. Repo wells, knowledge, and sessions settle into visually distinct clusters while the deliberately weak strength (0.045) lets link forces win locally, so cross-group edges arc between wells. Anchors are computed once from the full dataset, so type-filtergraphDatare-feeds cannot reshuffle which well a group lives in. /anton-core:dashboard [surface]skill — launch the browser dashboard without tying up a terminal. Probes127.0.0.1:7777/healthzand reuses a live server (LISTEN-filtered + anchored binary-basename match across install layouts, so a foreign app on the port is reported, never reused, never killed) or startscore dashboardas a session-scoped background task (deliberately not detached: the server dies with the Claude Code session — verified empirically; an orphan from an abnormal exit is absorbed by the next probe as a warm start). Includes a guarded cross-session stop gesture with post-kill verification, a wedged-server path (busy port + dashboard-identified listener → report unresponsive, offer stop, not a port suggestion), and explicit-only port handling (no silent port bouncing). Thesession-endhook wrapper gains a best-effort default-port reap as deterministic teardown insurance (scripts/session-end.sh:21-41), logging each kill or failed attempt to the data-dirdashboard-reap.log; the match-and-kill guard is pinned byscripts/lib/wrapper_test/session_end_reap.bats. Skill count 21 → 22 (files_checked27 → 28); CLAUDE.md fragment gains the Intent-Routing row,fragment-version1.1.0 → 1.2.0. No binary changes — the skill orchestrates the existingcore dashboardcommand.
Changed
core item get(andRunItemGet): item rows are now fully resolved — every row carries its sortedtagspluscreated/last_accessedms-epoch timestamps (previously declared-but-never-populated outside the dashboard detail handler, which carried its own back-fill reads; that back-fill is deleted).cli-shape:item-getreplaces the stale never-emittedaccessed_atproperty withcreated/last_accessed.- Retrieval read paths now share one created-bounds contract: negative
CreatedAfter/CreatedBeforevalues returninvalid_argumentfrom bothRunItemListandRunRecall(previouslyRunItemListsilently treated negatives as unset while the recall filters applied them — same field names, two behaviors). - Dashboard
/memory: an unknown?sort=key now returns 400 in both modes (parse-time validation against the newly exportedretrieval.ItemListSortKeysvocabulary); previously the search branch ignored it and returned 200, contradicting the spec's error taxonomy. The sort vocabulary, itscreateddefault, the knowledge-type family (intake.KnowledgeTypes— shared by the browse default, the graph'sknowledgegroup facet, and the chip fallback), and the item-type normalisation (intake.NormalizeType) each gain a single exported definition so their consumers cannot drift. - Dashboard 3D graph filter rail (
graph.js+/graphview): the static module/symbol checkboxes are replaced by a dynamic two-facet rail + legend overlay, both built client-side from the loaded payload (the server now ships only an empty#graph-railshell and a[data-graph-legend]container). "Node types" renders one checkbox per distinct nodetype— thesessiontype defaults unchecked (sessions are noisy until asked for) — and "Repos" one perrepo:<slug>group, labelled by bare slug. The visibility predicate generalizes totypes[n.type] && groups[n.group]: unchecking a repo hides that whole well regardless of type, unchecking a type hides it across every well (non-repo groups carry no group checkbox — their types already govern them fully); links keep the both-endpoints-visible rule. The legend overlay (canvas bottom-left) is rebuilt by the same filter pass — onegroupColor-swatched row per group with ≥1 visible node, rows carryingdata-legend-groupfor upcoming isolate-on-click wiring — and hides itself when nothing is visible.groupColorand the gravity wells now share a singlenormGroupnormalisation helper. - Dashboard 3D graph rendering (
graph.js): the two-tone warm TYPE palette +HEAT_DIMlinear dimming is replaced by a group palette + perceptual "tiny suns" glow. Hue now keys off the node'sgroup— knowledge#7ab8ff, sessions#5fd4b0, eachrepo:<slug>well a distinct warm hue assigned in first-seen order (modulo-wrapped beyond four repos), lavender fallback for untagged code/unknown groups — and brightness follows0.30 + 0.70·heat^0.45(node radius rides the same curve), so every node stays faintly visible at the 0.30 floor. The bloom threshold drops to 0.012 — UnrealBloom's high-pass measures Rec.709 luminance on the composer's linear-sRGB target, where the dimmest group hue (hsl(14,85%,62%) at the 0.30 floor) sits at ≈0.026, so with the smoothstep gate's 0.01 width every node carries a faint halo and hot ones (full brightness, ≈0.3–0.8 luma depending on hue) burn clearly brightest — at strength 0.9, both operator-tunable starting values. The truncation banner now reads "least-connected code nodes trimmed first", matching the knowledge-exempt retention. The now-unread--graph-anchorCSS token is dropped (--graph-lightremains, feeding the dimmed link web). GET /graph/data(ReadGraphData) now serves the unified memory graph: nodes are ALL items rows — knowledge (document/reference/project/feedback), sessions, and anything else alongside code — not just symbol/module. Each node gains agroupfacet (repo:<slug>from the item'srepo:tag for code,codewhen untagged,knowledge,sessions, or the raw type name as fallback) and, for non-code nodes only, asummary(drawer preview, server-truncated rune-safely to 200 chars).GraphNode.Typewidens fromcodegraph.ItemTypeto plainstring. Heat is re-weighted usage-first:0.75·accessNorm + 0.25·degNorm(shares sum to 1, replacing the clamped degree-floor + freshness-gain blend). The node cap becomes knowledge-exempt — when the limit bites, least-connected code nodes are evicted first (non-code sorts ahead of the LIMIT; the exemption holds until non-code alone exceeds the cap, past which non-code is subject to the same least-connected-first eviction) — andtotalnow counts all items.co_access_pairsrows surface as a second link source withkind: "co-access", under the same both-endpoints-kept filter. A consequence of the unified payload: the node-click touch (POST /graph/touch, unchanged) now warms every item type — its existence guard always checked the whole items table, so knowledge and session nodes accrueaccess_countfrom graph exploration too, feeding both the recall freshness signal and the heat map.
Fixed
- Dashboard
/memory/{id}detail page: the content block now wraps (whitespace-pre-wrap break-wordson the<pre>, which previously keptwhite-space: preand scrolled a single unbroken line horizontally), and the "← back to Memory" / "View in graph" links move from below the content to the top of the page so they no longer sink out of view under long bodies. - Dashboard 3D graph: stored XSS via the node hover tooltip. The vendored bundle renders the
nodeLabelaccessor's return value through an innerHTML sink (float-tooltip's.html(content)), and node labels areitems.title— user-controlled free text stored verbatim at intake; the unified payload (which began serving user-titled knowledge/session items alongside SCIP-named code) made a hostile title (<img src=x onerror=…>) executable on hover. The accessor now routes through anescapeHtmlhelper, pinned by a string-level asset-guard test (TestGraphJSNodeLabelEscaped). - Dashboard
/memoryfacet chips: a failed global type-count read no longer renders fabricated zero counts next to a table full of matching rows — the chips render without count markers (honest degradation), and the knowledge-family chips survive as the fallback vocabulary instead of every non-active chip vanishing from the rail. - Dashboard
renderPage: a mid-body templ render failure is now logged (dashboard.render) — previously the error was discarded entirely, leaving a truncated page with zero server-side evidence (the 200 + partial HTML are usually already on the wire, so the 500 attempt was the only — futile — response). - item-type hygiene: migration
0015_core_item_type_normalize.sqlrewrites the three stray-typed rows (Document,Rule,note— all carryingref-id prefixes) toreference, and the intake write path now lowercases + whitespace-trims every item type before the id prefix is minted, theknowledge/<type>/durable-copy directory is named, and the row is stored — so the three consumers always agree and case/whitespace variants of vocabulary types cannot regrow. The normalization is mechanical only: a genuinely off-vocabulary type still stores (lowercased) rather than being rejected. Thedb.yaml/db.mdacceptance pins (A-db-5,A-db-migrate-1/2) advance toschema_versions: {core: 15, events: 12}accordingly. - Memory recall: type/tag/recency/created-bounds filters and the default completed-task exclusion now apply before the relative-threshold cutoff in the fusion pass, so the
0.5×topfloor is computed against the top of the filtered pool — an unflagged recall's floor now keys off the top non-completed hit. Previously the floor keyed off the unfiltered top, so a filtered search on a query dominated by another type purged every in-facet hit before the filter ran — e.g. the dashboard's knowledge-facet chips + a code-swamped query (?q=templunder the defaultdocument/reference/project/feedbackchips) returned zero rows even though matching references existed. The two FTS arms also gain anORDER BYon bm25 so theirLIMITkeeps the best matches instead of FTS5's default rowid (insertion) order — unordered, a swamped query sampled the oldest matches and could starve the pool of relevant hits before fusion even began. - Dashboard
renderErrornow maps error kinds to HTTP status —invalid_argument→ 400,entity_not_found→ 404, everything else → 500 — instead of flattening every failure (including bad query params like an unknown sort key or malformed date) to a plain 500. Stalegraph.gocomments claiming it renders an HTML error page are corrected; the response stays plain text.