0b2e4e03fbd1c0d934986a90abe2d4de3cb7d844
103
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
962d3086c0 |
graph: the rendering switch — chains view as the home (release polish 2)
The settings Graph section splits into two switches (owner, 2026-10-06): - EXTRACTION (was the lone "Graph" row): the DATA channel — pipeline, auditor, prober graph tools. Key unchanged (graph.enabled), label now names its job; off = the graph stops growing, nothing is deleted. - GRAPH RENDERING (new, graph.rendering, default on): display-only. Off = the home's main view is the CHAINS VIEW instead of the canvas; data, reasoning-chain model, and the pipeline keep working untouched. The chains home (components/chains-home.tsx): ChainTableBody in flow — same body as the overlay, same powers (query + sort, selection, the delete fence with its confirm dialog), but onClose is absent so the top-bar X does not render (the table IS the page; ChainTableBody now takes onClose optionally). The chrome survivors float top-right below the top bar: Import notes (ALWAYS — the entry-point law) + the cost pill (GraphCostControl, now exported). The composer blob persists over everything; the table scrolls CLEAR of it via a fixed pb-40 band (text under a floating box is noise — unlike the canvas, which runs under). Pick flow mirrors the overlay contract exactly (home lifts the pick set; selectMode swaps the bar's confirm to Done). The chip relabels to "Pick propositions" (no "from the map" — there is no map); while picking, the chip row hides so the floating bar above the composer is the one voice. Settings resolve rendering through the one core resolver + renderingEnabled(); read server-side per navigation, so the switch applies on the next visit home. Smoke-verified live (headless browser): chains home renders without the X, import + search + composer clearance confirmed, pick flow checked 2 propositions -> Done -> chip count -> practice session started with the graph context carried (2 props, 2 chains), settings restored. Known follow-ups (flagged, not built): after Done the bar morphs to the table's Clear/Delete management bar while the chip also shows the count (two voices, same set — the table's own editor law); /chat/new's pick chip still says "from the map". Tests: rendering in factory/sanitize/parse/resolve pins, the GraphSection render pins (Extraction label + rendering row), the home-variant chain-table pin (no close X). 1414 pass; tsc, lint, build green. |
||
|
|
87f8e1e6f4 |
proposition pick: persistent lifted selection, live count, honest affordances
Root cause of 'doesn't register': the slice-6 floating-bar edit never landed (a silent no-match replace) — the Body accepted selectMode but the bar ignored it, so the picker showed Clear + Delete and nothing ever confirmed. Found by re-reading; the test suite never pinned the select-mode bar. The owner vision instead: the chain table's selection is PERSISTENT and caller-owned. - ChainTableOverlay gains controlledSelection/onSelectionChange (every check, clear, and delete-prune reports up immediately; uncontrolled fallback keeps the delete flow self-contained). - Select mode's floating bar: Done (closes; the pick already registered) — the delete fence never renders while picking. - GraphView threads the lifted set through, resolving ids → nodes against the live snapshot. - Home: the pick bar above the composer shows 'N propositions selected' + Clear in EITHER mode (dashed 'Pick propositions from the map' hint only in practice mode with nothing picked); visible hover (bg-accent) so the chip reads clickable. Practice sends still seed from the pick set. |
||
|
|
e76e42f764 |
practice entry points: sidebar filter, practice draft, graph picker
- Sidebar: the three-way kind filter (Teaching/All/Practice, persisted
in localStorage) above the search; two new-session doors (Teaching →
/chat/new, Practice → /chat/new?mode=practice); practice rows carry
the dumbbell badge and 'Practicing…' busy meta.
- Draft: ?mode=practice swaps the copy (title, empty state, 'What do
you want to practice?') and startDraftSession posts kind:'practice'.
- Graph entry (home composer): teach|practice switch on the stats row's
left edge (StatsBar leadingSlot); practice mode shows the propositions
chip that opens the chain table as a picker (its floating bar becomes
'Practice these' — no delete fence); the picks seed the opening prompt
('Propositions from the map:…') while the title stays the typed text.
|
||
|
|
814c2ad412 |
delete the batch practice-problems module (superseded by practice sessions)
Owner ruling 2026-10-04: the module is a sunk cost, retrievable from history if ever revisited. Removed: core practice contracts + pipeline helpers, the PracticeStore port + adapter + data dir path, server service + API routes (sets, clusters picker, practice aborts), the practice pages + dialog/exam/list UI, chain-table's Generate handoff, sidebar nav, activity-dock practice rows, and the partitioner/ problemGenerator/grader model roles (BACKGROUND_MODEL_ROLES 8 → 5). Survivors, moved to core/chains.ts (pure + browser-safe): ReasoningChain, chainsForSelection, formatChainLine — the chain table and graph emission still consume them. |
||
|
|
26131b3599 | push architecture slice 7: graph-status + practice surfaces ride the events channel; coverage polls accepted-by-design; stale poll comments + architecture.md observability section rewritten to the push model | ||
|
|
91341a93da |
digest-aware serialized extraction + chain simplification replace the spine stage
Owner live-test verdict on the spine (2026-09-30): no perceptible gain — the real problems (restatement nodes, shallow re-derivations, same conclusion from many angles) are mint-time issues a post-hoc chain minter cannot fix. This arc moves assembly INTO the extractor and adds the deterministic dedupe the owner specified: - Digest-aware extraction: each stretch's extractor prompt carries THE GRAPH SO FAR (its own source's merged propositions + chains, ids verbatim); build on existing ids instead of restating, lean on established conclusions as premises. Mixed-namespace chain endpoints (fresh p-ids OR existing n_-ids) validated at the submit seam. - Serialized per source: a source's batches/chunks run strictly in order (each extract reads every earlier merge); cross-source runs stay parallel; source-wide abort stops the loop. The spine stage, role, phase, usage stage, cost row, and setup roster are gone. - Core-ideas prompt: goal reframed as uncovering core ideas and showing how everything derives from the foundations; direction upward, prefer lean derivations, never re-derive what the graph already holds. - Chain simplification (merger maintenance): simplifyChains drops provably redundant chains at every merge — prefer-fewest-premises + ancestry-cover ([A,B]⟹X with [X,C]⟹Y covers [A,B,C]⟹Y, transitive); same-key provenance lines drop together; rewriteLedgers under the audit lock; droppedChains counted separately in stats. Chunk budget stays 8192 (owner: keep for now, maybe lower later). |
||
|
|
52c02533d1 |
extraction: the spine stage — cross-stretch assembly between extract and audit
Per-batch pipeline is now extract → spine → audit → merge (owner order, 2026-09-30), the spine inside the serialized audit section so it reads the graph exactly as the following audit will. Measured motive: 0 of 133 chains in the live corpus crossed a stretch boundary — no stage minted cross-stretch chains. - src/agent/subagents/spine.ts: SESSION/NOTES spine preambles + shared SPINE_CORE (span doctrine; extraction doctrines carried over), the submit_spine TypeBox schema, validateSpine (mechanical span law: every chain touches >=1 fresh endpoint AND >=1 existing digest endpoint; unknown ids, self-reference, and id collisions are loud), the digest prompt builder, and the runner (strong tier, retry-only). - extraction.ts: 'assembling' phase, spine stage in the per-need gate, usage/billing stage, assemble() inside withAuditLock for both front doors; first stretch of a source skips the call (empty digest, no spend); the audit sees COMBINED propositions (chunk + connectives), never chains; merger receives the combined findings. - core/graph.ts mergeFindings: chain endpoints resolve through the job-local map OR pass through verbatim as existing n_ ids (the spine's mixed namespace). - EXTRACTION_CORE: steps-of-reasoning selection bar (examples, notation glosses, and restatements that no argument needs are not steps). - Factory chunk budget 4096 -> 8192 (fewer seams, more local context). - healStringifiedArrays moved to the shared submit seam (submit-agent.ts). - Cost panel: Assembly row; usage records carry a spine stage (optional on legacy records). - Tests +5: prompt pins, span-law matrix, digest rendering, seeded end-to-end flow (spanning chain + connective, verbatim n_ endpoint, spine billed as its own stage), non-spanning refused + retried. |
||
|
|
35d8bc1a0c |
physics: imp nodes get the MIDPOINT depth anchor between premises and conclusion
Owner ruling (2026-09-30, live-testing loop — supersedes 02a58db's exemption): implication nodes ARE depth-anchored after all. Their anchor is the average of the HIGHEST premise row and the conclusion's row — depth(imp) = (max(premise depths) + depth(conclusion)) / 2 — so the junction always sits strictly above every premise and strictly below the conclusion (a conclusion is >= highest premise + 1 in the logical graph). Uniform across modes: a half-row in vertical, a ring midway in radial (owner confirmed both). Implementation: - layout.ts: new ImpLink type; SimOptions/incrementalLayout gain impLinks (the derived chain linkage). The rank map is now computed once alongside the orphan mask: proposition ranks via Tarjan over the logical edges, then each supplied link adds its midpoint entry. An imp node WITH an anchor is structured (leaves the orphan mask — gravity projections treat it like any ranked node); WITHOUT supplied links, arg_ ids keep the legacy 02a58db exemption (honest fallback for callers that predate the field). The depth force reuses the precomputed map — one pass, shared. - deriveView (core/graph.ts) emits impLinks (one per multi-premise live chain — single-premise chains mint no imp node); GraphSnapshot carries them; graph-view maps them into CanvasData; settleGraphLayout and replay-layout forward them (replay activates a junction's link exactly when its chain is delivered). Tests (+3, 1 rewritten): the 02a58db exemption pin became the MIDPOINT pin (premises row 0, conclusion row -500, junction pinned to -250); mixed-depth pin (premises at 0 and 1, conclusion 2, junction at the 1.5 half-row — highest premise, not average-of-premises); legacy fallback pin (no impLinks = never depth-anchored); deriveView impLinks emission pin (multi-premise only). The horizontal-pole and ORPHAN RULE pins from 5554c3a survive unchanged — proposition orphans are untouched. |
||
|
|
af666aaa3f |
physics: bundles join their cluster; depth runs over the logical graph
Owner-approved brief (orchestrator, physics arc): two laws.
1. IMP-NODE CLUSTER ASSIGNMENT. Imp nodes were unclustered (the partition
runs over stored propositions only) — no centroid pull, outside the
painted hulls, bundles sitting on the periphery pointing outward.
Now: a derived imp node's cluster is a function of its derivation —
MAJORITY among its PREMISES' birth sessions (owner: premises, NOT the
conclusion), ties to the chain's OWN birth (its provenance's first
point — the asserting session; deterministic, coincides with the
unanimous common case). Imp nodes are always re-derived, never
birthed, so the assignment must be exactly this.
Seam chosen: core/session-clusters.ts exports impClusterKey + a
shared attachImpToCommunities that mutates a communities list's
members IN PLACE. graphSnapshot attaches AFTER the hull filter (a
2-proposition community + its bundle never resurrects a hull — the
filter counts propositions); settleGraphLayout attaches to the
unfiltered partition (settle pulls every community; hulls are a
canvas-only concern). Both cluster-membership consumers — the sim's
centroid pull and buildRegions' rasterization — read it through the
members lists that went through the one helper, so the resting
ledger and the live sim agree on where bundles belong.
2. DEPTHS OVER THE LOGICAL GRAPH. inferenceDepths ran Tarjan over the
DRAWN edges (premise→imp→conclusion), inflating every multi-premise
conclusion one hop (depth(imp)+1 instead of depth(premises)+1) and
ranking the geometry point itself. Now:
- deriveView additionally emits logicalEdges — every chain contracted
to direct premise→conclusion arrows over PROPOSITION ids only
(per-premise, deduped — a conclusion's rank is max over premises,
which a single arbitrary premise edge could not express).
- GraphSnapshot carries logicalEdges; createGraphSimulation /
incrementalLayout accept a logicalEdges option: the depth
machinery (orphan detection AND the rank pass) reads it, springs
keep the drawn edges. Absent = edges (legacy behavior).
- Imp nodes join the ORPHAN-EXEMPTION mask explicitly — never pinned
to a level, keep the full regular forces (gravity, cluster pull,
springs), settle naturally between premise and conclusion levels;
the rank map is computed over propositions only, so arg_ ids are
absent from it by construction.
Wiring: settle + canvas (CanvasData.logicalEdges → buildSim) +
replay-layout all pass the logical set.
EXTRA FIX (in-slice discovery, one line, graph-server domain):
server/graph.ts graphClusters — the practice picker's /api/graph/
clusters — partitioned snap.NODES (the canvas image, imp nodes riding
it since chains-primary), leaking arg_ ids into nodeIds: pickable ids
that silently vanish inside chainsForSelection while nodeCount lied.
Now partitions snap.propositions — same purity law as the chain
table's 767a559 fix.
Tests (+13): impClusterKey pins (unanimous; cross-session majority; tie
→ chain's own birth, not the conclusion's); attach pins (members gain
the imp id, sorted; single-premise attaches nothing — no node exists;
dangling mints nothing; home absent from the list stays unattached);
snapshot pins (imp id rides cluster members to both consumers; the hull
filter counts propositions); logical-depth pins (multi-premise
conclusion at premises'+1 NOT +2; imp absent from the rank map);
depth-force exemption pin (rows pin crisply, arg_x floats unpinned —
old behavior stamped it onto the conclusion's row); deriveView logical
arrows (per-premise, deduped, proposition ids only).
|
||
|
|
8d72de596a |
fix: chain table reads the stored propositions, not the derived canvas image
The owner's live test surfaced it: after importing notes, the chains view listed implication nodes as bogus singleton "chains" — and each carried the same arg_ id as the real stored chain, tripping React's duplicate-key error (chain-table.tsx:372). Root cause: graph-view passed snapshot.nodes — the derived canvas view (propositions + implication nodes) — into ChainTableOverlay, whose chainsForSelection turns every unchained node into a singleton chain. An implication node is never a chain member (members are proposition ids), so every imp node minted a bogus singleton with a colliding id; its row was even selectable, leaking arg_ ids into practice input. Fix (owner-approved shape): GraphSnapshot gains `propositions` — the stored-truth node list riding beside `chains` for exactly this class of consumer. The chain table now receives snapshot.propositions; the canvas keeps snapshot.nodes (imp nodes are meant to be drawn). Practice pipeline unaffected (it already read getGraph, which is propositions-only). Test: the snapshot split pinned as an invariant in graph.test.ts — image = truth + one imp node per fully-live stored chain; truth never carries an arg_ id. |
||
|
|
4985b6f9fa |
chains-primary: stored chains replace typed edges; edges become a derived view
Owner rulings 2026-09-30: reasoning chains are the stored truth; the graph canvas renders a lossy derived image. No migration, no backcompat — old graph data was wiped. - contracts: GraphChain (arg_-minted id, premises, conclusion) replaces typed edges; GraphEdge survives only as the untyped derived arrow; GraphSnapshot carries stored chains; supports edges retired entirely - store: chains.jsonl ledger (edges.jsonl purged at load alongside legacy events.jsonl); readChains/appendChain seam; merge dedupes by (sorted premises, conclusion), re-assertions append under the same arg_ id - core: deriveView mints implication nodes (name ⟹, statement via formatChainLine) plus premise→imp→conclusion arrows; clusters stay proposition-only (birthSessionPartition); chainsForSelection now groups STORED chains (chain rides iff every member selected) — independent arguments for one conclusion never union; chainsToGraph/chainsToEdges die - extraction: extractor submits chains natively (expandFindings deleted); auditor receives propositions only; supports fan-out gone - deletion: full death of a proposition kills every referencing chain; arg_-prefixed node deletes kill that chain; chainsRemoved stats - UI: every arrow renders the ⟹ double shaft; supports toggle + thickness slider and detail-card edge sections deleted (card keeps name + proposition); chain table + practice pipeline read stored chains - tests: 1188 green across the new seams |
||
|
|
e7f4923159 |
deletion screen minimal + 'node' as THE UI word (owner ruling)
Owner 2026-09-30: the confirm dialog's pre-flight impact read had to go — what full death takes is obvious, the per-open fetch flickered, and richer facts would only confuse as deletion semantics evolve (rewiring is on the table). The screen is now: Delete node? / Delete N nodes?, the names in the mono block, one permanence line, Cancel / Delete. No loading state, no gone state (a 404 mid-run lands in the honest failure line), no sessions join — the joins prop died too. The dead path is deleted end to end: GET /api/graph/nodes/[id], graphNodeImpact (server), nodeDeletionImpact (core) and their tests. WORDING: node/node(s) is THE UI word for graph objects, standardized across every surface's copy (owner, same ruling): the dialog titles, the chain table's counts/search/summary/delete aria-labels, the practice dialog's cluster counts, selection line, and empty-cluster hint. Internal identifiers (selection.propositions) unchanged. Rewritten dialog pins (tests/delete-node-dialog.test.tsx): minimal contract — no facts/loading/fetch copy, singular/plural titles, the cap, 409 dock phrasing, busy states, no 'proposition' left in the dialog. |
||
|
|
40dc1b7f85 |
chain table: the fenced Delete action + multi-node delete dialog (owner ruling)
"This is really the view deletion should be done in" — the chain table
becomes the graph's MANAGEMENT surface. The floating selection bar
gains Delete FENCED at its right end (owner-approved placement): a
divider quarantines the destructive action from Clear and Generate,
red destructive pair + trash icon, opening the SAME full-death confirm
dialog the canvas uses.
DeleteNodeDialog extended from one node to an array:
- one node renders exactly as always; many pluralize the title
(Delete N propositions?) and list the names in the mono block,
capped at 8 + 'and K more'
- the pre-flight impact read aggregates: summed edge counts, unioned
referencing sessions (deduped, first-seen order); all-404 = gone
- confirm runs the per-node DELETEs SEQUENTIALLY — a failure stops
the run (extraction_running is never plowed through); onDeleted
now carries every id that actually died
- the chain table prunes its selection on delete and announces
lh:graph-revision so the snapshot refetches
Drift fix found en route (surfaced as NaN in the aggregated facts):
the server's NodeDeletionImpact has been {edges, sessions} only since
the quiz-events ledger died in the practice-arc schema slim — the
dialog kept rendering quizEvents/observations as undefined. The dead
fact lines are removed; the dialog now shows exactly what the server
can honestly report.
|
||
|
|
4d58aa74b1 |
top bar drag standardization: auto carve-out + the overlay DOM law (owner ruling)
The chains-overlay TopBar had dead drag zones (counts, around search, close gap) — cause: app-region subtraction resolves by DOM ORDER, not z-index; the graph chrome's lh-no-drag column came later in the DOM and punched holes through the overlay's drag band despite sitting visually underneath (z-40 vs z-10). Two laws, per the owner's standardization ruling: - AUTO CARVE-OUT (globals.css): inside any .lh-drag band, every native interactive element (button/input/textarea/select/a/summary/ [contenteditable]) is automatically a no-drag island — draggable anywhere that is not a clickable element becomes structural. Manual lh-no-drag is only for clickable non-natives. - THE OVERLAY DOM LAW: a surface containing a TopBar that covers other content must be the LAST child of its container — ChainTableOverlay moved after the chrome column (verified live: last child, after the column; auto-carve rule compiled; search/close/sidebar-toggle all still click). |
||
|
|
155d065fd1 |
practice: the chain table — every reasoning chain in one selectable overlay (owner-approved plan, 2026-09-29)
- core formatChainLine: names + arrow one-liner, singleton marker, pure
- graph-view Chains button (chrome row, empty-graph hidden) opens the
overlay: fuzzy search (names/aliases/statements), collapsible chain →
member → statement rows, birth-session + created columns, no
understanding-levels column
- pinned Selected section derives the agents' actual input live
(chainsForSelection over the selection; premise without its
conclusion shows as the singleton it becomes)
- Generate opens the creation dialog in CUSTOM mode: selection summary
+ dated default title, posts {nodeIds, params, note}; cluster mode
untouched on /practice
- live-verified on the owner's dev server: 219 chains, derived preview,
search-never-filters-Selected, dialog handoff (selection pass-through
bug caught + fixed en route), Esc/backdrop dismiss, zero writes
- practice UI files re-normalized to tabs (my earlier prettier runs had
drifted them to the 2-space default; whitespace-only)
17 render/unit pins added; gates: tsc 0, 1174/1174, lint 0, build 0
|
||
|
|
f5a9ed59c7 |
supports-thickness slider — relative shaft width for supports edges
A LINEAR 0–1 multiplier of Edge width (default 0.5, dead-center at step 0.1 — 11 stops like the ladder rows but ratio-valued), grayed out by the supports-edges toggle (the Dilation dependency pattern). Renderer splits supports shafts into their own Path2D and strokes a second pass at edgeWidth × supportsWidth; arrowheads stay full-geometry in the one shared path (owner pick: shaft only). A true 0-width stroke is undefined on canvas — the 0 end floors to a 0.5px on-screen hairline; hiding remains the toggle's job. Sanitizer: real numbers clamp to [0,1], everything else resets to 0.5; clamps pinned in render-settings.test. |
||
|
|
6543d4f886 |
supports-edges draw toggle — last row of the graph settings' first section
Off hides supports edges from the canvas: the renderer skips them when building the shaft/head paths (draw-only — the physics sim still feels them and detail cards still list them; settings never shift the graph). Server-persisted with the rest (render-settings.json); sanitizer takes real booleans only, anything else (missing key, legacy file) loads ON — today's look. Toggle row styled as the Background-colors row; sanitiser pinned in render-settings.test. |
||
|
|
92146248d2 |
Per-chunk reveal: refetch the graph when a JOB row leaves the feed, not when the source drains
The reveal rule (c902bc6: a run leaving the feed means its merge is on disk, so refetch immediately) was tracking chatId sets — fine when one row meant one source, but per-chunk jobs made it re-batch the reveal: chunks finish one by one, each row leaves, but the source's chatId stays present until its LAST chunk drains, so a big import showed nothing until it fully completed. The exact batching bug the rule exists to prevent. Track by row id (the registry job key) in both the snapshot-reveal poll and the per-run costs refresh — a chunk row disappearing IS that chunk's merge landing. |
||
|
|
4001c3465b |
Graph view: unique pill keys for chunk jobs (second duplicate-key consumer)
The graph page's pipeline pills keyed rows by run.chatId — the source id, shared by every chunk job of a fan-out (the dock got the same fix in 8f66f47; this consumer was missed). /api/graph/status rows now carry an id (the registry job key: source id, or 'src#i'), and the pills key by it. chatId stays the SOURCE id — the reveal refetch and per-run cost refresh track chatId SETS, which dedupe naturally and fire when the source's LAST row leaves the feed. |
||
|
|
14a7f0de1d |
per-run graph reveal: refetch the snapshot when a run leaves the status feed, not when the feed empties
The old refresh trigger was the some→none transition of the /api/graph/status poll — with parallel runs under the serialized audit, run A's merge landed while B was still queued, but the feed still held B, so nothing refreshed until B finished too: both runs' nodes appeared in one batched reveal (owner's live-test finding, 2026-09-28). The ledgers were correct the whole time (each merge is a synchronous append inside the gate; the serialization test pins B's auditor seeing A's node) — only the reveal was batched. Now: refetch the snapshot whenever a run DISAPPEARS from the feed (finished / aborted / failed). A run leaves the registry only after its merge + settle + usage persist complete, so disappearance implies the graph change is already on disk; each run surfaces within poll latency (~2s). Redundant fetches on aborted runs are free — the deletion-refresh doctrine. The costs pill follows the same disappearance rule (usage persists per run). Confirmed the status pills already render the raw phase, so the 'waiting' stage displays as Waiting with no change. |
||
|
|
fc8d91a043 |
first live-extraction findings: fix backwards edge-section labels; extractor naming/self-merge/completeness doctrines
Owner's first live test of the relinked pipeline surfaced three issues,
diagnosed against the ledgers:
1. Detail-card edge labels were backwards (UI only — data and canvas
arrows were correct). Semantics pinned 2026-09-28: an edge A→B
means A implies B (follows) or A supports B (supports). Outgoing
edges now list under 'What follows from this' / 'What this
supports', incoming under 'Follows from' / 'Supported by'.
Verified against the ledger: Invariance --supports--> Twin
paradox / Relativity of simultaneity; Argumentation ethics
--follows--> Homesteading — arrows correct, only the words were
swapped. docs/architecture.md wording fixed to match.
2. Session-local terminology leaked into node names ('Criterion C',
'Action axiom (F1)'). Both extractor prompts gain a naming
doctrine: names use established, discipline-canonical terminology
(glossary-headword test); never source-local bookkeeping (letters,
numbering, positional labels); local shorthand goes to aliases.
3. Near-duplicate propositions and lonely nodes (~40% of fresh-run
nodes edge-less; 'Argumentation ethics' vs 'A priori of
argumentation' duplicated despite the auditor having linked that
phrase once). Root cause is upstream of the auditor: the extractor
had no self-merge or edge-completeness duty. Both prompts gain:
MERGE YOUR OWN OUTPUT (same claim at different precision = one
proposition, phrasings to aliases; nothing downstream dedupes) and
edge COMPLETENESS (a proposition that arose as evidence for,
consequence of, or prerequisite of another must carry the edge —
a graph of lonely nodes is a failed extraction).
Owner rulings on the open question: keep two edge types (no
'relates to' junk drawer); re-measure loneliness after these fixes.
Gates: typecheck, 1059/1059, lint, build.
|
||
|
|
1b50013551 |
extraction cleanup: events ledger dies, markdown source view, auditor-as-linker (2026-09-27 rulings)
The graph becomes raw reasoning chains only. Quiz outcomes and understanding observations are no longer formalized; assessment moves to the future practice module. Events ledger removed end-to-end: LedgerEvent/LedgerEventKind/ UnderstandingLevel/ExtractionAttribution/AuditResult.dropped and snapshot nodeObservations deleted from contracts; quizEvent builder and the events step of mergeFindings gone; appendEvent/readEvents dropped from the port and store; LEDGER_FILES loses events.jsonl with a one-time purge of a stale file next to the nodes ledger; graph-deletion and the delete dialog lose event purge + quiz-answer impact counts; GraphSessionProposition becomes a pure birth list. Whole-session markdown source view: new core/session-markdown.ts is the shared formatting engine (id-anchored speaker blocks, quiz/ recall/question blocks, tool one-liners) — the extractor's prompt view and the future user-facing export render from one engine. Coverage fingerprints deliberately stay on the old line format (content-hash semantics; re-formatting never re-opens covered history). The whole session goes to the extractor — no phase cut. Auditor refocused as the identity linker: never sees the source; explores the graph via new auditor-specific tools in agent/tools/graph.ts (find — glob over name/statement/aliases; list_sessions; list_propositions with full statement text; no timestamps) instead of an injected full-graph dump; equivalence bias — paraphrases merge, only genuinely distinct propositions stay apart; no drop channel, the extractor is fully trusted; may draw bridging edges (typed follows/supports) between existing nodes and the batch, resolved by the merger with chat-scoped message-less provenance. Agentic loop, maxTurns 16. UI: the node detail card gains an edges section above the sessions list (follows-from / supported-by / what-follows / what-it-supports, rendered only when present, click-to-select neighbors); the understanding-note chips are gone. Prober list_propositions reworded to what a session covered — names only, no levels. docs/architecture.md + README updated to the as-built pipeline. Gates: typecheck, 1059/1059, lint, build. |
||
|
|
6b83d3248b |
Tooltip subtlety round: 500ms rest delay, quiet chip, parens suffixes, drops + trims
Owner round 2026-09-25, over 09c0f6e. The wave's tooltips read as distracting; this round makes them quiet: - Delay: TOOLTIP_DELAY_MS = 500 in the primitive — tooltips appear when you pause on a control, never in passing (Radix focus-opens stay instant by design; keyboard users don't wait). - Vanish on leave: disableHoverableContent — the chip is un-hoverable, so the instant the cursor leaves the trigger it's gone, even mid-move onto the chip's own pixels. - Look: bg-accent (#2a2a2a, the hover-chip tone) instead of the white bg-foreground slab; 11px; px-2 py-1; arrow culled (nothing purely decorative); text-foreground/85. - Suffix: parentheses, not em-dash — "Dictate (⌘D)"; the registry source unchanged (still override-aware). - Drops (owner-selected): send square, start square, stats summary — the icons/numbers describe themselves. - Home logo kept but to the RIGHT with its real shortcut "Home (⌘G)" (jump-graph routes to /, the same destination). - Trims: "Model selector (⌘K)", "Thinking level", "Graph costs — click for details", "How much of this session isn't mapped yet". - Added: Import Notes gets "Import notes into the graph" — the one button whose destination wasn't obvious. Pins: TOOLTIP_DELAY_MS=500; hintText parens expectations updated. Live-verified on :3001 — 508ms hover delay measured, close within 10ms of pointerout, chip styles probed (rgb(42,42,42), 11px, 4px/8px), arrow gone, all new labels + sides confirmed, drops carry no tooltip and no aria-describedby. |
||
|
|
b19e7e7eae |
Production tooltips: styled tooltips everywhere, shortcuts surfaced, dev-speak culled
Owner-approved full pass (tooltip wave, 2026-09-25). Mechanism: icon/pill controls ride the styled Radix tooltip via a new Hint composition (components/ui/hint.tsx) — the shared primitive adopted as-is, no restyling; the shortcut appends as an em-dash suffix off the LIVE registry (a rebind in Settings → Keyboard changes the suffix): "Dictate — ⌘D". Surface-owned keys (Esc closes, Enter submits) ride a raw binding prop. Native title= stays only for truncation-reveals (dock spans, the rename field) and menu-interior abbreviations (MenuShell untouched). One em-dash pattern; per-state truth on the send square (busy = "Stop the reply — Esc", idle = "Send — ↵"), Enter on the plan verdict's accept, "Stop dictation (Esc)" kept verbatim per the owner's flag. Copy: the dev-speak cull — no pipeline/prober/tier/wave vocabulary anywhere a user reads. New pins in tests/tooltip-mechanism.test.tsx (hintText composition, asChild no-wrapper-DOM, the auto-update hint constant); dock truncation titles pinned native; graph-chrome + settings pins updated to the new copy. Live-verified on :3001 — 12.6ms hover appearance (no native 1s lag); glyphs render on every suffixed surface (⌘D, ⌘K, ⌘⇧F, ⌘,, ⌘B, F2, ↵, Esc); per-page probes confirm Settings entry, Home logo, session search, Start session, Close sidebar, model + thinking pills, stats summary, rename title, plan chip, coverage ring, send square, cost button, node search (⌘F), graph gear, settings close (Close — Esc), keyboard section "Fixed — part of this screen", auto-update hint, light-mode "Coming soon", the three slot rosters, graph-tools hint. Esc still closes the settings dialog. |
||
|
|
81be55f35c |
Empty-graph chrome: Import Notes stays, search + settings wait for nodes
Owner ruling on the empty graph (honesty principle — dead controls are lies): the top-chrome actions row now gates per control. - Import Notes renders ALWAYS (graph enabled) — the one action that works on an empty graph, the entry point itself. It used to hide with everything else, leaving the empty state a dead end with no entry. - Search + settings render only when the snapshot has nodes, and their appear-once mount carries a deliberate 200ms fade (graph-chrome-fade-in, globals.css — the settings-pop register, reduced-motion appears instantly). Computed animationName verified live. - The row's body is now the exported pure GraphChromeRow (the DeleteNodeBody pattern) — the no-match note's truth (visibleIds + snapshot) stays upstream, computed by the wrapper. - The empty-state placeholder copy is LEFT AS-IS (SCOPE rule: it already has copy — icon, title, and the session-mapping paragraph). Pins (tests/render-graph-chrome.test.tsx): empty → Import Notes present, search/settings absent, no fade carriers; populated → all three, exactly two fade carriers; the no-match note follows its upstream truth. SSR of GraphChromeRow both ways. Live-verified on :3001 with scratch data: empty graph → Import Notes only + placeholder intact; a real import through the button (file input via DataTransfer) → nodes land, search + settings fade in (computed chrome-fade-in on both carriers); wipe → reload → back to Import Notes only (the reverse path). Scratch graph restored byte-identical. 932/932, tsc/lint/build green. |
||
|
|
73027f49bf |
Cost channel cull: the cluster-naming fold dies — two stages, two channels
Off UI's d651f93 flag (displayCostTotals already isolates the display to extraction + audit; the naming stage row and naming runs were filtered out client-side). This culls the server side of the same ruling. THE STANDING RULING, REVERSED BY ITS OWN PREMISE: NamingRunUsage / readNamingRuns / appendNamingUsage were kept at the wave-7 namer kill ONLY because cost-summary's HISTORICAL naming stage read them. With the fold culled, that premise dies — so the whole ledger read path goes too, following the wave-7 embeddings precedent chain: consumer-less ledger seams are deleted, and the on-disk naming-runs.jsonl stays INERT (real trees keep their historical spend bytes; they are simply not displayed anywhere — the same treatment as historical stages.embedding spend). - core/cost-summary.ts: two channels in (chat + notes front doors), two stages out; RecentRun.kind collapses to "extraction" (labels field gone); CostSummaryInput.naming gone; totalDollars = extraction + audit. - ports/store/contracts/config: appendNamingUsage + readNamingRuns ports, store impls + guards, the NamingRunUsage interface, and LEDGER_FILES.namingRuns die. graphRevision no longer counts the file (a smaller revision value is harmless — reads never compare). - components/graph-view.tsx (mechanical, flagged): the defensive .filter(run.kind !== "naming") guards a kind that no longer exists. - Tests: cost-summary's both-channels fold pins; the naming fixtures and the appendNamingUsage store pin die with the seams. |
||
|
|
2cba283009 |
Graph cost data drops the dead cluster-naming channel from the UI
The cluster namer is dead (owner refinement round, 2026-09-24): cluster labels are just session titles / file names now, coined without an LLM, so there is no reason to keep tracking naming spend in the cost UI. components/graph-view.tsx (UI-only; the core fold is untouched): - NEW pure export displayCostTotals(costs) = extraction + audit dollars and unpriced counts. Both the cost pill and the panel headline read THIS — never GraphCostSummary.totalDollars, which still includes the historical naming channel — so the two can never disagree. - GraphCostPanel's stage rows lose the "Cluster naming" line; the headline and unpriced counts come from displayCostTotals. - The recent-runs list filters out kind === "naming" runs entirely (the "N labels" tail died with them). - Mount comment updated: the fold covers chat + notes extraction runs. Note for master/Platform: src/core/cost-summary.ts still folds naming-runs.jsonl into stages.naming / totalDollars (Platform's lane). The UI no longer reads those fields; Platform can cull the seam and the dead ledger whenever convenient — displayCostTotals already isolates the display from whatever the core returns. Pinned in tests/graph-view-filters.test.ts: displayCostTotals with a naming stage of $9.99 + 2 unpriced proves the display sees extraction + audit only ($2.00, 1 unpriced). Live-verified on :3001 — the panel renders Extraction and Audit rows, no naming row, no naming runs in the recent list. 905/905. |
||
|
|
bc1f668e2a |
Extraction surfaces: sidebar coverage block, chat-bar ring, phase un-pill, panel death
The owner-locked extraction-UX redesign (dispatch 2026-09-24), UI chunk over Platform's data layer (eed4b4e — GET /api/chats/[id] carries `coverage`, the threshold is Graph ▸ extractionChunkTokens): 1. SIDEBAR BLOCK (components/extraction-coverage.tsx + sidebar's session menu): the coverage info block at the TOP of the context menu — uncovered tokens, uncovered messages, running state — then the Extract affordance (optimistic "extracting… / Running" until the server says running; 409 = benign, the trigger raced us), fetched ON MENU OPEN (the block mounts with the menu — on-demand, never eagerly), polled at 2s while a run is in flight. Visually separated from Rename/Delete by a hairline; those stay the two session-management actions. Failed read → nothing (never fake zeros). 2. RING METER (CoverageRing, chat top bar, before the PhaseChip): a 16px donut filling uncoveredTokens / extractionChunkTokens — "how close this session is to its next extraction run". Click → the exact numbers (covered/uncovered tokens, messages, the threshold, lastFoldedAt via timeAgo). Refresh chains: mount, the turn-settle bump (chat.tsx coverageTick), the settings signal (threshold/kill-switch re-read live), a 2s drain-poll while a run is in flight, and the ACTIVITY FEED — a run for this session appearing or settling in the dock's feed re-reads coverage (runs fire from other surfaces — the sidebar menu, the turn pipeline — and no turn-settle bump announces them; the runs-empty-transition the graph view rides). Vanishes entirely when the graph is off or before the first honest read. Running = the whole ring PULSES (implementer's visual call: motion is the working-state signal in this app's quiet register). 3. PHASECHIP UN-PILL: the phase label is a flat tag — no border, no rounding, no padding. The Plan chip is the only button-shaped thing in the top bar. Drag-surface (pointer-events-none) and aria semantics unchanged; the transcript PhaseDivider keeps its look. 4. PANEL DEATH: components/graph-coverage.tsx, its graph-view mount, and tests/render-graph-coverage.test.tsx die whole. Confirmed: after this, NOTHING client-side consumes GET /api/graph/coverage — the only remaining client use of the coverage endpoints is the Extract POST (kept, by design); the routes stay (Platform's call). Pure seams exported for tests: ringFill (clamp math), CoverageMenuBody, CoverageRingGlyph, CoverageRingDetail. 901/901. Live-verified on :3001: ring fill arcs match the API byte-for-byte (483/4096 = 4.45/37.70; 1991/4096 = 18.32/37.70); the sidebar block reads "2k uncovered / 4 messages unextracted"; a real Extract run went optimistic → dock run → menu settled "fully extracted" AND the ring drained to the empty arc (the feed chain); graph off → the ring vanishes; the flat phase label renders chrome-free beside the Plan pill. |
||
|
|
e826088cc8 |
Rename-shuffle fix: label-only channel + three hygiene items
The live-rename interlude routed label changes through a fresh snapshot object, which flowed setData -> geometry rebuild - and the rebuild treats the last-fetched position ledger as authoritative and cancels any pending drag commit, so a rename visibly rewound the whole graph whenever display had drifted (recent drag, mid-anneal, physics re-anneal). - Renderer gains setClusterLabels (a setPhysicsTuning sibling): pure applyClusterLabels patches the data-side cluster records and the built region meta (re-wrapping the changed entries with the same measure the build uses), markDirty only. Never touches nodes, the sim, positions, or the pending commit. The hull-silence guard travels inside it: a null-label cluster is a gone record - no rename can reach it. - The lh:chat-renamed listener stops building snapshot objects: renameSessionsPatch writes the sessions entry in place (pure display state - the detail card's provenance line), the cluster label goes through the new channel, and a tick bump re-renders the view. Identity preserved everywhere -> the [snapshot]-keyed setData feed cannot fire. - Hygiene (a): a fresh sim starts at alpha 1 and never decays while stopped - the first post-rebuild drag re-annealed the whole graph at full strength. buildSim now initializes alpha at the settle floor, so first-drag matches steady-state drag. setPhysicsTuning still rehearses alpha(1) when a full re-anneal is the point. - Hygiene (b): nodes missing from the position ledger used to dump at (0,0) via the ?? 0 fallback. seedMissingNodePositions seeds them on a deterministic ring around the known positions' centroid (FNV-1a hashed angle, golden-angle spiral offset). - Hygiene (c) is store-band (writePositionMap's writeFileSync -> tmp+rename): flagged for Platform, not crossed. Tests: applyClusterLabels suite (patch + re-wrap, hull silence, no-op, explicit null, unknown ids), renameSessionsPatch identity pins (sessions map and clusters array identity preserved - the contract that keeps setData from firing), seedMissingNodePositions determinism + ring math. Live-verified: mid-anneal rename shows a 4-world-unit max disc jump (no teleport), the commit PUT survives the rename, rest renames leave the disc build byte-identical, and a held first drag displaces discs ~14 units instead of re-forming the graph. |
||
|
|
976be50a81 |
Polish round: node-delete chrome + rename alignment
Owner polish dispatch (2026-09-23): - Right-click no longer pins a node or opens the detail card: the canvas pointerdown/up handlers now gate on the primary button, so the context menu arrives alone. - Node context menu: label is just "Delete" and the shell hugs its content (w-44 -> w-fit); the menu body keeps focus for arrow cycling but never shows the browser's default focus ring (outline-none on the MenuShell body - applies to every menu). - Delete confirm: "Delete node?" title in CMU Serif, red (text-destructive); the node name sits below in JetBrains Mono inline-code; the Delete button carries the destructive treatment (border + tint, fill deepens on hover). Rest of the dialog stays neutral. - Sidebar rename row: text and metadata stay at the idle row's x - the status spacer and the input's border/padding are gone; the underline is the only editing cue. |
||
|
|
92943864c4 |
Node deletion affordance (node interlude): right-click menu + full-death confirm
Platform's 50bfcba engine, surfaced. Three pieces: 1. THE RIGHT-CLICK (graph-canvas.ts): a contextmenu listener — always preventDefault (the native menu on a full-bleed custom canvas is never useful) — picks the node under the pointer and hands the canvas-relative position to a new onContext callback (null on empty stage). pickAt already respected the visibility filters: a hidden node cannot be right-clicked, same as it cannot be clicked. The view opens a MenuShell anchored at a zero-size point-div positioned at the pointer — MenuShell's placement machine measures it like any element, so the menu gets the full discipline: placement, focus on open, arrow cycling, honest-fit flip. Delete… is its one action; the dismissal contract is the sidebar's (wave 6): outside-click / Esc (claimed) close, and a camera move closes it too — a menu floating over a moved graph is anchored to nothing. 2. THE CONFIRM (delete-node-dialog.tsx, portaled z-50 like the session-delete dialog whose tone it follows): the pre-flight impact read (GET /api/graph/nodes/[id]) renders the plain facts — link count, quiz-answer count, session-note count (zeros stay visible — hiding a zero fact would understate the blast), and the referencing sessions with display names joined CLIENT-SIDE from the snapshot (chat titles; imported-notes file paths; unknown ids honestly themselves). "This cannot be undone" says the permanence. The 409 extraction_running renders the dock phrasing (EXTRACTION_RETRY_LINE — the server names the extraction, the ACTIVITY DOCK is the surface the user actually watches drain) with a Try-again; every other failure shows the server's line verbatim. The honest 404 (deleted elsewhere meanwhile) states it plainly and its Close rides the same refresh. The pure body is exported separately (the settings-dialog pattern) — SSR pins everything the live dialog shows. 3. SUCCESS: onDeleted dispatches lh:graph-revision with the returned graphRevision — the wave-6 chain refetches snapshot + costs. The detail card closes if it pointed at the dying node (the refetch would close it anyway; the same frame should not show a card for a node the user just killed). Clusters shrink / unrender below the minimum on the server's read — the client just renders the refreshed snapshot. Tests: 867/867 (+8 — the join helper, honest plurals, the 409 phrasing pin, the dialog body at loading/gone/impact/failure states). Live-verified on :3001 against the real engine, scratch data: impact facts match the API, the intercepted 409 shows the dock line and Try-again recovers into a real delete, the three-member cluster died below MIN_CLUSTER_SIZE server-side, the labeled seedplan cluster + its canvas label died with it (cold load draws nothing stale; the in-session refresh refetches and the dying node's label stops drawing), other nodes survived, and the full menu contract holds (empty stage → no menu; Esc, outside-click, camera move all close). |
||
|
|
7479dce8ef |
Wave 7 interlude: prober tool-call chrome + live rename cluster labels
Graph tool-call visibility — the prober's list_sessions /
list_propositions rendered like nothing happened in the transcript
(the registry default is silence for unregistered tools). They now join
the EXACT read/ls/find/grep discipline: artifactRenderers maps both to
the ToolCallCard — collapsed one-liner, expandable Input/Output card.
The results ride the card's string path verbatim (the tools' own
compact name/level/date line format), exactly how read results render.
- list_propositions gets a headline arg: the session id is the call's
subject — the collapsed line reads "list_propositions chat_…".
Shown as given, never resolved (the same no-resolution rule as
read's path). list_sessions carries no arg (empty input — the
ls-with-no-path discipline).
- No bespoke row-layout card: the outputs are pre-formatted strings
(the model-facing format IS the learner-facing format here), and
the dispatch's "SAME look" directive settles the shape question.
Live rename labels — renaming a session anywhere (sidebar inline row,
chat header) already announces lh:chat-renamed {id, title}; the graph
view now listens. The birth-session partition means the title IS the
cluster label, so the patch is targeted and in-place, no refetch: the
snapshot's sessions map (the detail card's provenance line) and the
one matching cluster. The new snapshot object flows the existing
setData → geoVersion → rebuild chain, so the wrapped label (wave 7's
wrapRegionLabel) re-wraps for the new text. Unlabeled clusters keep
their hull-only silence — a null label is a GONE record, the guard
holds the line.
Tests: registry pins for both tools + the prober-card render (collapsed
headline, verbatim rows, no-arg discipline). 855/855; live-verified on
:3001 with fillText instrumentation: both cards render collapsed and
expandable in the transcript, and a sidebar rename flipped the canvas
cluster label old→new with the old label's draws stopping cold.
|
||
|
|
c86430e8bf |
Wave 7 chunk B: remove the embeddings machinery — the ladder collapses
The owner's wave-7 ruling: embeddings die entirely (git history is the
stash); clustering is birth-session derived (chunk A, 950ddfd), labels
are session names, and the prober's graph tools replace injection.
- settings: GraphSettings collapses to { enabled } — embedding,
embeddingModel, clustering, labeling all die. Migration story: stale
record keys are UNREAD and dropped by the sanitizer's reconstruction;
the next graph-section PUT (replace-whole) persists the collapsed
shape. The settings-panel GraphSection renders one master row.
- deleted: src/server/embeddings.ts (adapters, resolveEmbeddingPortFor,
embedNewNodes, embeddingPort, embeddingModelCatalog), the
GET /api/models/embeddings route, scripts/embed-graph.ts, tests for
all three, and the /api/models/embeddings picker surfaces in the UI.
- prices: EMBEDDING_PRICES overlay + callCost fallback die. cost-summary:
the embedding stage fold dies — back to three stages (extraction/
audit/naming) per the owner ruling; historical stages.embedding spend
in old run records is simply not displayed.
- ports: EmbeddingPort/EmbeddingResult/EmbeddingTask die. The embeddings
LEDGER surface (readEmbeddings/writeEmbeddings + EmbeddingsLedger +
EMBEDDINGS_FILE + graph-deletion's vector pruning) STAYS — inert data,
kept only so graph-context.ts compiles until Teacher's pass removes
it; the seam dies in the follow-up interlude.
- chat-handler (approved cross-lane unwiring, wave-6 precedent): the
extraction pipeline's embedder becomes a documented no-op (nothing
embedded, nothing spent, ledger untouched) until Teacher's pass
removes the phase; the graph-context injection dispatch gate, the
graphContext deps wiring, the setContextRespawn seam, and
turn-service's triggerGraphContext are gone — injection is dead
code pending Teacher's file deletions.
- scripts: backfill-graph/import-notes pass the same no-op embedder;
replay-layout derives clusters from birthSessionPartition (the same
partition the real settle uses) instead of the embeddings ledger.
- settings-body: parseGraphSettingsBody reads the master switch only —
old-shape keys in a body are ignored, not rejected; bad shapes 400.
- next.config: the @huggingface tracing-exclusion guards die with the
last reference.
Cross-lane flags: extraction.ts/graph-context.ts keep stale embed
comments + the embed phase until Teacher's full pass (their parallel
interlude owns those files — zero overlap kept by design);
semantic-clusters.ts + its test stay one more round (extraction.test.ts,
Teacher's file, still imports it — they delete it with the namer cases);
ChatSettings.graphContextTokens + the graph-context-control UI stay
until Teacher+UI.
Gates: tsc clean, eslint clean, 885/885 tests, build compiles.
Live-verified on the port-3004 dev tree: PUT /api/settings/graph with
dead keys collapses on write, bad shapes 400, GET reads {enabled: true},
/api/models/embeddings 404s.
|
||
|
|
fb66606555 |
Wave 6 chunk B: the embeddings surfaces — picker, re-embed gate, gray-outs, cost pill
- Settings ▸ Graph grows the embedding-model picker (graph-row-embedding-model):
GET /api/models/embeddings' live-federated BYOK catalog (re-fetched on mount
and every settings save), rows priced from EMBEDDING_PRICES ("$x.xx/1M in",
honestly "—" when unlisted), ids shown AND stored as full colon-safe slugs
(openrouter:liquid/…:free is never split), plus an Unset row writing null.
Empty catalog = nothing keyed: the trigger BECOMES the deep link to Keys
("Add a provider key first…") — and deep-links fired while the dialog is
open now actually switch sections (initialSection was read once at mount;
the body follows it now — pre-existing gap, first exercised by this link).
- Re-embed gate (owner design): picking a model that differs from the stored
one holds the write behind a confirm strip carrying the honest node count
from GET /api/graph; zero propositions skip the strip (nothing to re-embed)
and land directly. Unset never confirms — the port just stops resolving.
The copy says the re-embed runs with the next background pass (the staleness
machinery — a settings PUT alone never triggers it; no server endpoints
were added).
- Gray-out, the searchStatus precedent: with no model configured, the ladder
rungs Embedding/Clustering/Labeling dim + disable with a "Needs an
embedding model" hint (master switch stays lit), and the stats-bar
GraphContextControl swaps its slider for a dimmed deep-link button to the
Graph section. Loading never grays; every settings save re-reads (signal).
- Cost chrome: the pipeline popover grows the Embeddings stage row
(costs.stages.embedding, wave 6's folded stage — "— · 0 in / 0 out" until
runs exist, same as its siblings).
- Master's ride-along: the core-barrel ChatDeleteMode comment now says two
type-only things (it always exported both).
Gates: tsc clean, 912/912 (8 new: price lines, model-dependency predicate,
unset/set SSR dimming; 2 ladder fixtures updated to the new no-model
semantics), lint 0, build ok. Live-verified on :3001: unset deep-link →
graph section, menu pricing + colon-safety (37 options), confirm strip with
"20 propositions", Cancel keeps the record, Unset writes immediately,
in-dialog empty-catalog deep-link lands Keys, Embeddings row in the popover.
|
||
|
|
1c27694dbc |
Wave 6 chunk A: sidebar context menu (Rename/Delete) + the two-option delete dialog
- Right-click any session row: MenuShell context menu, exactly two actions (owner ruling): Rename and Delete… — MenuShell discipline (placement, focus, arrow cycling, lh:menu-open announcements), agent-section's outside-click/Esc contract with [data-lh-menu] carve-outs. - Rename is INLINE in the row (owner ruling): select-all entry, Enter/blur commits through the existing plumbing — PUT /api/chats/:id then lh:chat-renamed. The open chat header now LISTENS for the event too, so renaming the active session from the sidebar updates the header in place (it only dispatched before). - Delete opens the two-option dialog (locked design, DELETE_OPTIONS pure seam for render pins): "Delete the session only" (preserve-graph — the graph keeps every contribution) vs "Also remove its graph traces" (delete-nodes — shared topics survive, only-this-session topics die). 409 turn_in_flight/jobs_running and 503 extraction_winding_down render the server's line verbatim with a same-mode Try-again. - Success: the sidebar patches its snapshot instantly, the active session navigates home (chat unmounts — drafts die with it), and lh:graph-revision announces the returned revision — the graph view refetches the snapshot AND costs (deletion is the one writer the status poll can't see). - ChatDeleteMode type re-exported through the core barrel (the shell's one needed seam from core/graph-deletion.ts). Gates: tsc clean, 909/909, lint 0, build ok. Live-verified on :3001: context menu open/Esc, inline rename (row + API + active header), both delete modes on scratch sessions, simulated 409 + retry-then-succeed, active-session navigation home, graph+costs refetch deltas on home. |
||
|
|
d90b1caa23 |
Settings dropdown: widen for a clean scrollbar gutter
Owner report: the scrollbar pressed against the slider content. Panel w-64 -> w-72 and the inner scroll column gains pr-2.5, so the spacing is concentric: content -> 10px -> scrollbar -> 10px -> panel edge. The scrollbar itself is NOT moved — it keeps its distance from the panel's right border; the panel simply widens to give it air. |
||
|
|
7555da03b9 |
Top-row chrome: composer-matching 16px corners, equal 8px gutters
- Corner rounding back (owner, 2026-09-20): the four controls return to rounded-xl — 16px in this theme, identical to the composer shell's rounded-2xl (verified computed-style equal). Inner elements (search input, import button, cost hover) stay concentric: 16 - 4px padding = rounded-lg (12px), the composer's own concentric math. - Equal spacing: the cluster pins 8px from BOTH edges (top-2 + right-2), still flush with the sidebar toggle's header band (top 8px). |
||
|
|
3de911aa58 |
Top-row chrome: dropdowns layer above the cost number; slight (4px) corners
- Stacking: the actions row is now relative + z-20 — an open settings or coverage dropdown paints over the cost number below it (owner report: it layered ON TOP of the panel, still hoverable/clickable). The row is a stacking context inside the column, so its dropdowns cover the number and clicks land on the panel; the number resurfaces untouched when the panel closes (verified with elementFromPoint). - Rectangular look (owner, 2026-09-20): the four top-row controls drop rounded-xl (16px theme) to rounded-sm (4px) — gear, coverage, search container + input, import container + button; the cost number's hover highlight joins the family. Dropdown panels keep their softer cards. |
||
|
|
4e39f04cfa |
Header band: one row of controls, 36px, aligned with the sidebar toggle
Owner report: the controls folded onto two rows (search+coverage / import+gear) — the 2026-09-17 flex-wrap kicking in, plus a real bug: my previous wave moved max-w-[calc(100%-9rem)] onto the shrink-wrapped column wrapper, where the percentage reference broke and the wrap fired far too early. Owner wants the controls to read as header content. - The width cap moves to the absolute column itself (100% resolves against the full-bleed parent, as before): max-w-[calc(100%-5.5rem)]. - One row, NEVER wraps (owner rejected the two-row fold): search is the flexible element (w-56 + min-w-0, shrinks under the cap); the other controls are shrink-0. - All controls slimmed into the header band: top-2 (8px) matching the header's py-2, 36px tall matching the toggle's h-9 — size-9 buttons, h-7 import button, px-1 py-[3px] containers (border included = 36). - Cost number + pipeline pills stay below the band, unchanged. |
||
|
|
138d3d8ce6 |
Cost control: bare top-right number with dropdown; pipeline pills move under it
Owner-directed placement wave (2026-09-20): the bottom-left stack overflowed the composer in narrow windows, and the pill chrome was too prominent for a passive number. - The cost surface is now JUST a number (cumulative dollars) top right, below the actions row — no pill container, hover-highlight kept, dropdown breakdown kept, viewport-capped + scrollable. - Same dismiss pattern as the settings gear: outside click or Escape closes (shared usePopoverDismiss; two dropdowns never open at once). - Running-pipeline pills moved under the cost number in the same top-right column — one right-aligned column grows downward from the top, so no window width can push chrome over the composer. |
||
|
|
3e263a18cc |
Graph pipeline cost metrics: capture naming spend, fold all channels, cost pill + panel
- Cluster namer bills the money channel now: per-step usage + model captured via onStep, persisted as NamingRunUsage records to data/graph/naming-runs.jsonl (idempotent per startedAt; failed/aborted runs persist too — money spent either way). callUsage promoted to extraction.ts module scope for reuse. - core/cost-summary.ts: the pure fold behind the pill — chat runs, notes-source runs, and naming runs into stage totals (extraction / audit / naming) + newest-first recent runs. Dollars INFERRED read-time via prices.ts, never stored; unpriced models excluded-but-counted (render '—', never a silent zero); tokens always sum. - GET /api/graph/costs (src/server graphCosts composition, thin route). - Graph view: bottom-left cost pill (cumulative dollars headline) + expandable breakdown panel (stage rows with tokens secondary, recent runs with time/title/merged/cost); fetched on mount, refetched when a pipeline run finishes; merged into one bottom-left stack with the running-pipeline pills. - Tests: pure-fold suite, naming capture wiring (full importNotes -> naming path), store idempotency + strict reads. |
||
|
|
487f5d4cea |
wave 1 (platform stream): background-observability accuracy + owner-driven follow-ups
- observability: liveness-gated stale-job recovery, extraction run registry on globalThis with explicit 'embedding' phase, /api/jobs force-dynamic, client refresh nudges at every seam - headless: providerSafePng canonicalizer (multi-IDAT Chrome PNGs → single-IDAT) fixing qwen 400s - composer: Enter routes through pure enterAction — accepts a pending plan verdict (owner decision) - markdown chrome: session-scoped mermaid overlay + md-blocks component set + pan-zoom math, corner-copy (clipboard grant in electron main), math/table/code block fixes, heading weight 900 543/543 tests; typecheck/lint/build clean |
||
|
|
8b158972db |
feat: keyboard waves A/B — global shortcut registry, question row machine, Esc-cancel dictation
Wave A (owner key map): registry in src/core/keyboard.ts + KeyboardShortcuts
provider; Cmd+D dictation, Cmd+K model picker, Shift+Tab thinking cycle,
Cmd+B sidebar, Cmd+Shift+F sessions search, Alt+arrows session cycle,
Alt+Enter open, Cmd+G graph, Cmd+N new, F2 rename, ? cheat sheet. Tab
fully suppressed; Esc chain popover > dictation > abort (local stream).
Esc now CANCELS dictation (discard) — flush stays on Cmd+D/Stop. Owner
bug reports fixed: focus outline removed, Alt+Enter setState-in-render,
Cmd+F graph node search (surface-scoped lh:kbd-find).
Wave B: question dock row machine — arrows walk options + dont-know/Other
with wrap + ring highlight, Space picks (mirrors click semantics), Enter
submits when canSubmit; typing surfaces and in-dock keys keep native
behavior. aria-checked={isSel} restored on option buttons.
docs/keyboard.md records the full map.
|
||
|
|
d1ba0c5c3c |
fix: graph settings panel viewport-capped like the coverage dropdown
The settings panel (16 sliders ≈ 900px) overflowed past the window bottom at short heights and stretched the whole page scrollable. The coverage dropdown's pattern is the standard for top-anchored popups overflowing viewport bounds: max-h-[60vh] keeps a floor of free space above the window bottom at any height, and the inner column gets overflow-y-auto (which also zeroes the flex item's automatic min-height so it actually shrinks) — the panel scrolls internally, the page never scrolls. Live-verified in the dev shell at emulated 640px and 1000px viewports: panel caps at exactly 60vh (384px @ 640h, 192px bottom gap), inner scroll reaches the physics section (412px travel), docScrollable false at both heights. |
||
|
|
d46c3dae93 |
feat: narrow-window behavior — sidebar auto-close, chrome wrap, 640px floor
The sidebar no longer squeezes the page when the window narrows; the page itself never goes below a comfortable floor (owner decisions, 2026-09-17: 640px min, widen-window on toggle, auto-restore): - minWidth 640 / minHeight 600 on the BrowserWindow (macOS enforces) - Below SIDEBAR_AUTOCLOSE (928 = 640 + 288 sidebar) the sidebar auto-closes, and auto-RESTORES when the window widens past it again — both fall out of one expression: rendered = pref && !narrow, where pref is the persisted user choice. Manual closes persist and stay closed; auto-close never touches the preference. - Opening the sidebar while narrow asks the shell to widen the window to 928 first (new lh:expand-window IPC, preload.cjs contextBridge — the renderer's first and only Electron bridge): the toggle is never a dead button, and the layout never has to survive narrow + sidebar. In the plain browser there is no suppression and no widening — a narrow tab opens the sidebar if the user says so. - Graph chrome row wraps: max-w-[calc(100%-9rem)] + flex-wrap keeps the row clear of the header toggle cluster and stacks it onto extra lines below ~640px instead of overflowing left. State machine verified in the Electron dev shell across all six transitions (auto-close, auto-restore, manual-close-persists, reopen-widens, browser fallback, wrap at 640/500/360). |
||
|
|
fb759b7aa0 |
fix: top-bar polish — lights on the pill line, rename escape hitbox, inert phase pill, chrome carve-outs
Traffic lights (owner report: 'way too low, not aligned with the phase pill'): measured in the running app — circles centered at 30.75pt vs the pill box's 29.25pt (band 0-59.5, pill box 19-39.5, pill ink 28.75; uppercase glyphs sit high). trafficLightPosition y 24 -> 22.5 lands the circle centers at 29.25, exactly on the pill. Owner confirmed aligned after this change. Rename escape hitbox (owner report): entering rename was already width-of- title only, but the editing input spanned flex-1 so clicking the rest of the bar stayed inside the input. The input is now shrink-to-fit ([field-sizing:content], min 8ch / max 70%) and any mousedown outside the input blurs it, running the existing onBlur commit path; the header drops lh-drag while renaming so the drag region cannot eat the escape click. Verified: click-away commits (exactly one PUT), Escape discards, Enter commits. Phase pill is pure decoration (owner report: 'should not be interactable in any way'): no tooltip, pointer-events-none, select-none — part of the drag surface instead of a carved-out target. Graph chrome was unclickable in Electron (owner report): home.tsx's floating drag strip underlays the z-10 controls row, and Electron's app-region mask ignores z-index, so the region ate their clicks (plain browser unaffected — DOM hit-testing is fine). lh-no-drag carve-outs on the chrome row and the node detail card (it is placed imperatively anywhere, including the top strip). LhMark home link now mirrors the ghost toggle: same size-9 chip geometry and hover:bg-accent treatment. |
||
|
|
769a3da186 |
feat: session rename + graph chrome polish (popovers dismiss, no-shift no-match, coverage reseat)
Rename (owner ask): clicking the header title opens edit mode with the whole title selected — typing replaces, arrows move inside. Enter or a click away commits (PUT /api/chats/:id); Escape and an empty commit discard. renameChat joins the ChatStore port; like saveSettings it is NOT conversation activity — updatedAt stays put so sidebar order never churns on a rename. The sidebar patch is instant: the header dispatches lh:chat-renamed and SessionsSidebar folds it into its snapshot (the 2.5s poll is only a backstop). Sidebar right-click rename is DELIBERATELY deferred: it needs a real context-menu system, which gets its own pass later (owner decision, 2026-09-17). Graph chrome: - The extraction-coverage button moves between search and import; the gear keeps the far-right corner. Coverage still renders on an EMPTY graph (that is exactly when it matters). - The no-match search note no longer shifts its siblings: the chrome row is items-start (every button is 44px tall, so top alignment pins the row while the search container grows downward). - Popovers dismiss properly: pointerdown anywhere outside the control's root (button + panel) closes it, and so does Escape. Two panels can never be open at once — the sibling chrome buttons are outside the open root, so pointerdown closes the first before the click opens the second. New shared hook components/popover-dismiss.ts, wired into both the settings gear and the coverage control. Gates: typecheck, eslint, 498/498 (+2 rename store tests), build. Live-verified: select-all/focus/Enter/Escape/empty/blur round-trip, updatedAt untouched, sidebar instant patch; chrome order + frozen positions under a no-match query; popover close via canvas, search input, Esc, mutual exclusion, toggle intact. |
||
|
|
0f93124c7e |
feat: canonical world-scale renderer — sizes become world units + baked tuning + nice-number ladders
The world-scale pivot (2026-09-17): node/label/arrowhead sizes were
calibrated as "screen px at fit zoom", a unit whose meaning depended on
the graph's own extent — a 13-node graph at kFit 3.65 rendered 5.3x
smaller nodes than a 338-node graph at kFit 0.69, with no slider values
able to reproduce the dense feel. Sizes are now canonical WORLD units
(graph-canvas.ts drops the ÷kFit division in geometry/regions/hit-test);
kFit survives only as the label-gate 1x reference. The renderer's module
doc and the SIZE SEMANTICS block in render-settings.ts carry the story.
Camera becomes pure persisted view state: {k, x, y} stored to localStorage
(lh.graph.camera.v1, 250ms debounce, flush on unmount), restored on mount
via the new initialCamera constructor arg; the hardcoded fit() runs only
on first-ever open. A pin test locks the d3 zoomIdentity composition.
Region blur becomes world units too (the last span-coupled quantity): the
old bitmap-px reach scaled with graph span, so the 2026-09-14
search-filter collapse fix is now structural (blurW = tuning.blur; the
cellRef anchor is gone). Charge default bumps to -240 (single source:
CHARGE_STRENGTH in layout.ts; CHARGE_RANGE re-anchored so -240 sits
dead-center).
The 2026-09-17 bake — the user's live-tuned defaults (nodeSize 2,
edgeWidth 1, arrowLength 2, labelSize 3, labelZoom 4 / "8x", dilation 20,
blur 20, edgeAlpha 0.2, bitmap 256) — plus the ladder pivot: every
non-physics knob moves on a hand-authored 11-stop ladder (x1.5/x1.33
series, quarter/half steps at the low end), default dead-center at slider
position 50, step 10 = one stop per detent. The sanitizer snaps ANY
incoming value to the nearest stop (ties break low), so stored files can
never hold 2.18137...977-style artifacts; pre-pivot files (sizeUnits !=
"world") reset only the four resemanticized size fields. Physics sliders
keep the fine-grained log mapping.
The label cross-fade becomes PROPORTIONAL to the gate (span = gate x
LABELS_FADE_RATIO): the old absolute 0.5-ratio span read as a 100%
relative change at the old 0.5 gate but a 12% sliver at the tuned 8x —
"lower zoom levels naturally produce a longer transitionary period".
At the 8x default the node→cluster handoff now fades across one full
ladder stop (8x -> 12x). labelZoomDisplay is absolute (ratio / 0.5) so
the tuned gate keeps reading "8x" now that 4 IS the default.
Gates: 495/495 tests, tsc/eslint/build clean.
|
||
|
|
5ce78d0754 |
Merge lh-graph refinement wave (feat/edge-glyph @ 66e6212) into main
Edge glyph (⟹ follows arrow) WITHDRAWN by owner after two live-rejected variants — CanvasEdge.type plumbing + ArrowGeom retained, differentiation approach still open (docs/vision-edge-glyph.md decision record). Keepers: regions.enabled "Background colors" compute off-switch (skips splat/blur/readback/blit), clusterLabelSize (default 14, topmost draw layer, screen-px) with Cluster labels toggle + size slider in the Regions section, dependency graying across the settings panels, physics panel reorder (general forces first, Depth anchor + conditionals last). Merged cleanly over master's coverage-chrome changes in graph-view.tsx (their gated fragment + the ungated GraphCoverageControl coexist); data/ scratch not carried (untracked, master's real render-settings.json picked up clusterLabelSize=14 via sanitizer fallback). Gates 488/488; live- verified: chrome intact, Background colors graying works, physics order, coverage panel opens (11 sessions). |
||
|
|
926ae2f43d |
Physics panel: general forces first, depth anchor block last
Presentation-only reorder (user 2026-09-17): the Depth anchor select and its three gray-out-able sliders (Depth strength, Depth spacing, Base ring radius) move below Friction to the very bottom of the Physics section, so the always-active general forces come first and the conditional group reads as a self-contained sub-section at the end. No schema, wiring, or behavior changes — verified identical DOM order live, gray-out still follows the stored depth mode. |