Commit Graph
736 Commits
Author SHA1 Message Date
amosblomqvist 142f1978f2 reattach round: attach-first handshake, honest Reattaching placeholder 2026-10-02 02:23:20 +03:00
amosblomqvist 0751fec9d8 streaming round: thinking collapses by default, live tool-input cards, one glimmer grammar 2026-10-02 02:09:38 +03:00
amosblomqvist a99cbf3d90 dictation mechanics everywhere: graph view + new-session view
Consistency ruling (owner, 2026-10-01): the finish-first send and the
run-end focus are properties of the COMPOSER, not of one page.

- home (graph view, raw textarea): topicRef mirror; start() finishes
  dictation first (before the setup/key guards, so an intercepted draft
  is the complete one) then reads the mirror; the start square stays
  live while a run holds deferred speech; run end focuses the topic
  input.
- new-session: the shell owns the dictation instance — handleSend wraps
  the parent's send with finish-first, the composer stays sendable
  during a run, and run end focuses it via the Composer's inputRef. The
  parent's send reads a textRef mirror (applyText at every write): the
  flush lands via setState after the shell's await, and the parent's
  closure must not read a stale draft.
2026-10-02 01:42:55 +03:00
amosblomqvist 8460e96d6c dictation feel: finish-first submits, composer focus on run end
Two owner rulings, 2026-10-01 ('nothing gets lost' round):

1. SUBMIT FINISHES DICTATION FIRST. Every submit path — the dock's
   answer (Enter/Cmd+Enter/button) and the composer's send — stops any
   live run and lets Deepgram's last finals land in the target field
   BEFORE building the payload. Dictation.finish() resolves at cleanup,
   post-flush; a module finisher registry (same pattern as the question
   target) hands it to the dock without a prop bridge. Field values are
   read from REF MIRRORS (thinkingTextRef/answerTextRef, the composer's
   textRef): the flush lands via setState after an await, invisible to
   the closure. The quiz flow the owner described now holds: dictate
   reasoning → pick answer → pick confidence → Enter → dictation ends,
   reasoning saved into the thinking field, THEN the answer submits.
   The composer stays sendable while a run is live (its spoken text is
   deferred in the run's buffer) so Enter can trigger the finish-first
   send at all; nothing said → the send no-ops.

2. RUN END FOCUSES THE BARE COMPOSER. The falling edge of
   dictation.active focuses the composer's textarea — only when the
   composer is mounted: a pending question unmounts it (ref null, no
   focus), so the thinking field is deliberately left alone. Composer
   gains an optional inputRef for the page's focus handle.
2026-10-02 01:37:18 +03:00
amosblomqvist 6da192bb22 answered quiz cards show the confidence rail — the slider's record form
The rail paint is extracted to components/confidence-rail.tsx (ramp,
tick stack, geometry laws) and shared by both surfaces so they cannot
drift: the dock's live slider paints the rail under its disc, the
answered card renders it disc-less — the missing handle itself says
'record, not control'. Same look otherwise: thin rail, stop color,
solid ticks, % read-out on the right, dock-mirrored placement (below
the options, above the thinking).

Renders only where a confidence exists: quiz answers with one sent;
never on 'I don't know' (none was sent), never on explain/ask, never
on pre-feature records (optional field, absent).
2026-10-02 01:21:12 +03:00
amosblomqvist 1375295d54 slider round 5: the rail painted OVER the disc — stacking law fixed
Round-4 regression, root-caused:
- The rail overlay (position:absolute) painted above the in-flow input
  no matter the DOM order — CSS paints positioned elements after
  in-flow ones — so the bar sliced across the disc ('disc behind the
  bar').
- The wrapper's height was unpinned: the rail's top-1/2 centered
  against a stretched box while the disc rode the input at the top —
  vertical drift, which also exaggerated the disc's overhang and made
  the original-size 16px disc read oversized.

Fix: the input is absolute inset-0 too (DOM order now governs — the
disc rides above the rail), and the wrapper is pinned to h-4 so rail
center and disc track share one box. Geometry otherwise unchanged from
round 4: rail spans the thumb-center range, 0/100 flush at the very
ends, solid ticks, no rim.
2026-10-02 01:14:46 +03:00
amosblomqvist a365438833 slider round 4: flush extremes, solid ticks, arrows always live, disc rim reverted
- 0/100 at the very edges: the paint moved off the input onto a rail
  overlay spanning exactly the thumb-CENTER range (inset 8px = half
  the 16px disc per side) — the disc rests flush against the rail's
  ends, no bare stub poking past it. The input is now pure interaction:
  full width, transparent, 16px-tall hit area, native drag.
- Ticks solid, not dashed; plain p% positions (the overlay IS the
  center range), still background layers under the fill.
- Typing guard (owner): a range input is NOT a typing field — after a
  click or drag on the slider the ↑/↓ option walk and ←/→ confidence
  cycling keep working. Only real text fields (thinking, answer,
  search, renames) own their keys. The slider's own keydown is gone —
  the global machine owns arrows, and its preventDefault cancels the
  native 1% step.
- Disc rim reverted: plain bg-current disc again.
2026-10-02 01:10:07 +03:00
amosblomqvist f5fa3ca568 slider round 3: original thin rail, rimmed disc, ticks as true background
Owner refinements 2026-10-01:
- Bar back to the original h-1.5 rail (colors unchanged).
- Disc bleeds into its own fill no more: 2px #141414 rim on the thumb
  reads as a gap wherever it sits.
- Dashed stop ticks rebuilt as BACKGROUND LAYERS on the input itself:
  #141414 base color, four dashed tick gradients (30/50/70/90 only —
  the rail's ends already say 0 and 100) sized 1px x 100% at the
  thumb-center inset, and the fill gradient now TRANSPARENT past the
  thumb and painted on top — ticks live inside the bar, top to bottom,
  visible only on the empty side, never over the filled portion.
2026-10-02 01:00:44 +03:00
amosblomqvist 2f301322aa UI round 2: drag restored, no-ring law, #141414 empty side + outline, dashed stop ticks
Root-cause find: the sidebar 'highlighting' on ←/→ was :focus-visible —
pointer-clicked elements match it only after a keypress, so any key in
the quiz lit the still-focused session row. The arrows never moved
focus. Fix: the no-ring law (owner) — *:focus/:focus-visible outlines
removed app-wide in globals.css; surfaces keep their own affordances
and the dock's keyboard walk keeps its ring utilities.

Slider repairs:
- mousedown-preventDefault had killed click + drag in Chromium —
  removed (with tabIndex -1); the slider is natively interactive again,
  and the ring law makes its focus invisible anyway.
- Focused arrows step the STOP index on the input itself (native would
  step 1% and snap straight back — dead keys); the global handler
  skips INPUT targets, so no double-step. Enter still submits.
- Empty side: var(--muted) matched the card and vanished — now #141414
  (the owner's main-background pick), and the pill carries a border
  again (border-border).
- Dashed tick per snap stop below the bar, inset by the thumb's
  half-width so each tick sits under the thumb's center at that stop.
2026-10-02 00:52:08 +03:00
amosblomqvist 62cfb8112b confidence slider UI round: global ←/→, fraction positions, color ramp; plan-flank phantom fix
Owner UI round 2026-10-01, three rulings on the slider plus one en-route
bug:

- GLOBAL ←/→, the ↑/↓ pattern: confidence adjusts wherever focus sits
  (window keydown, typing-guarded), no click-in, no focus ring. The
  slider itself takes NO focus — tabIndex -1, mousedown prevented,
  removed from the Tab chain (rows → thinking directly again).
- Position = fraction: the thumb's scale is 0-100 with the rendered
  value the stop's own percent, so 50 sits halfway; onChange snaps to
  the nearest stop (magnet feel while dragged).
- Six-step ramp (owner pick over three bands): 0 red, 30 orange,
  50 yellow (the coin flip), 70 lime, 90 green, 100 emerald — fill AND
  handle wear the stop's color; taller 10px pill bar, custom track/thumb
  (webkit + moz), muted remainder via --muted.
- Plan-flank phantom: gating the marginalia button on a JSX element
  (always truthy) mounted an invisible clickable button in the
  bottom-right whenever a question was pending with no plan.
  planExists() now gates it (pinned in plan-window tests).
2026-10-02 00:44:02 +03:00
amosblomqvist 9b9f6372fa quiz answers carry a confidence slider — raw probability, snapped 0/30/50/70/90/100
Owner design round 2026-10-01: every multiple-choice quiz answer now
carries the learner's stated probability that his selected answer is
correct — calibration data for the prober and teacher, the flow itself
unchanged.

Definition (owner ruling, after weighing chance-normalization): RAW
probability. Sub-chance reports are diagnostic gold, not dead space —
'picked an option, believes it wrong' is a real state. The agents get
the sophistication: new shared/25-confidence.md composes for both
agents (chance level, sub-chance = distrusts the pick, correct+low =
fragile, wrong+high = committed misconception, IDK = genuine gap).

- question-core: AnswerRequest/QuestionAnswerData.confidence (quiz
  only, optional — old records valid), buildAnswer clamps and drops it
  on dontKnow, formatAnswerMessage states the line, CONFIDENCE_STOPS +
  DEFAULT_CONFIDENCE_INDEX exported.
- question-dock: snapped slider below 'I don't know', above 'Your
  thinking' — native range over the stop INDEX so ←/→ step the uneven
  grid cleanly, Tab chain rows → slider → thinking, Enter submits from
  the slider too. Rests at 50 and always sends (owner: 50 must mean a
  real coin flip, never 'untouched'). 'I don't know' GRAYS it out —
  layout holds, no value sent.

Deliberately untouched: explain free-recall card (quiz-only scope),
answered-card rendering of confidence, ask flow.
2026-10-02 00:24:05 +03:00
amosblomqvist d8cca61d1d partitioner fix: chain ids lead every rendered line
The partitioner was told to reference chain ids, but no rendered line
ever showed one — chainLine emitted bare 'premises → conclusion', so
the model re-typed whole rendered lines into submit_partition and every
call failed 'unknown chain ids' validation (owner live report,
2026-09-30 evening). The generator's YOUR TARGET block had the same
blindness.

- chainLine now leads with the chain's id: [arg_x] n_a, n_b → n_c;
  a bare proposition repeats its own id ([n_y] n_y (no premises)) —
  mechanically teaching that its chain id IS its proposition id.
- PARTITIONER_INSTRUCTIONS rewritten to chains-primary vocabulary
  (was still 'follows-edges, bijective re-encoding' — pre-chains
  framing) with the id-only doctrine and singular propositions
  first-class.
- partitionSchema/problemSchema gained field descriptions: ids
  VERBATIM, never re-type, never invent.
- Generator OUTPUT bullet + target-block header say the same.

Design ruling en route: partitioner stays an LLM partitioning chains
(deterministic partitioning and propositions-as-targets both
considered and declined by the owner).
2026-10-01 23:56:34 +03:00
amosblomqvist 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).
2026-09-30 17:44:37 +03:00
amosblomqvist 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.
2026-09-30 16:38:45 +03:00
amosblomqvist 5ed724c224 extractor: standalone/entailment/selection doctrines in EXTRACTION_CORE
Data analysis of the five-source corpus (490 nodes, 244 chains) found
three pathologies the goal-first core let through:

1. Narrative-adjacency chains — premises that occasioned rather than
   entailed conclusions (concentrated in the polemical sources).
2. Session-scaffolding minted as knowledge — the source's own axiom
   numbering (A1/F2/UT2), arc labels ('the crux') persisted as nodes.
3. Full-discourse volume — glossary and argument-arc material at equal
   rank with load-bearing ideas (~200 nodes per source).

Three doctrine lines, same terse register as the core:
- entailment is the only link that earns a chain; historical/narrative
  connections keep their pieces as bare propositions or drop them
- load-bearing ideas only (what a reference on the subject would teach)
- propositions must stand alone for a reader who never saw the source;
  no leaning on the source's own scaffolding

Substring pins for all three; shared by session + notes extractors by
construction.
2026-09-30 05:36:28 +03:00
amosblomqvist 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.
2026-09-30 04:52:27 +03:00
amosblomqvist 9f64019087 physics: vertical mode's central pole is horizontal-only for everyone
Owner ruling (2026-09-30, straight from the live-testing loop): when the
depth anchor is vertical, the central pole (center-of-mass gravity)
should create forces on the x axis ONLY.

Half of this was already true: structured propositions have had x-only
gravity in vertical mode since the 2026-09-13 depth projections (the
depth force owns y). The change extends it to the one branch that still
pulled on both axes — the orphan mask (unstructured propositions, and
since 02a58db the imp geometry points): in vertical mode everyone gets
the x-only pull; the full regular pull survives only in radial mode
(where the rings own the radius) and in mode-off (plain forceX/forceY).

Why this is coherent: y in vertical mode belongs to the depth rows and
to nothing else. A structured node sits on its row; an unstructured
node has no row to sit on — and now no pole to sag toward either. Its
y is free (charge/springs only), so orphans keep their seeded or
warm-started height instead of piling onto the center line, and imp
bundles settle purely by their springs between premise and conclusion
levels.

Amended the ORPHAN RULE comments to state the new law, plus the imp-
exemption comment (gravity is horizontal-only in vertical mode "like
everyone"). Radial and mode-off paths untouched.

Tests: the ORPHAN RULE pin rewritten to the amended law (orphan's y
untouched at its seed, x still converges on the pole — the old pin
asserted y converging on gravity's center); new explicit pin in the
logical-graph describe (lonely node: |x| < 50, |y − seed| < 5); the
02a58db exemption fixture's comment updated to match (springs alone
place arg_x between levels).
2026-09-30 04:36:31 +03:00
amosblomqvist 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).
2026-09-30 04:30:25 +03:00
amosblomqvist 719f7f9934 rendering arc: the imp node becomes the argument's geometry, not a fake proposition
Owner-approved brief (orchestrator): the canvas stops drawing implication
nodes as first-class nodes and starts drawing them as what they are — the
point where co-premise lines converge.

- deriveView (core/graph.ts): single-premise chains collapse to ONE direct
  premise->conclusion arrow — no imp node minted (a chain is always N-in/
  1-out; one premise is the degenerate case). Multi-premise chains keep
  the imp node + fan. Dangling guard unchanged.
- physics (core/layout.ts createGraphSimulation): per-link rest length —
  a link touching an arg_-prefixed endpoint gets HALF the base link
  distance, default or user-tuned alike; everything else about imp-node
  physics citizenship unchanged.
- canvas (components/graph-canvas.ts): imp nodes are PURE GEOMETRY —
  invisible (no disc, no label) and unpickable (excluded from pickAt's
  hit-test projection). Edges INTO an imp node: headless, shaft runs to
  its CENTER (toRadius 0, no head triangle). Edges OUT: shaft starts at
  the center (fromRadius 0), arrowhead lands on the conclusion's disc
  edge. Single-premise direct arrows render as today's plain arrows.
- comment repairs (same commit): the two stale double-shaft claims
  (CanvasEdge doc + the draw-pass block) rewritten to the truth — plain
  single shafts; bundles are headless converging lines + one arrowed
  out-edge; no double-shaft glyph exists anywhere.
- tests (+9): deriveView pins (single-premise collapse, multi-premise
  fan, shared-conclusion distinctness, dangling drop); imp-link half-
  distance settle pins; arrowGeometry zero-radius laws + isImpNodeId
  pin; graph.test.ts's snapshot invariant now counts only MULTI-premise
  live chains (its fixture's single-premise chains mint zero imp nodes);
  layout settle fixture updated for the collapse.
2026-09-30 04:04:29 +03:00
amosblomqvist 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.
2026-09-30 03:41:41 +03:00
amosblomqvist a2cb1447f7 docs: chains-primary reality in architecture.md + README; two stale comments fixed
- architecture.md section 6 rewritten: stored truth = propositions + chains
  (chains.jsonl); deriveView as the lossy image (implication nodes +
  untyped arrows, rebuilt per read); the bijection framing (independent
  arguments for one conclusion stay distinct — the corruption the edge
  store invited); new deletion-semantics paragraph (proposition death
  cascades to every referencing chain; arg_ death = one argument);
  supports retirement stated plainly; auditor's propositions-only scope;
  clusters stated as birth-session partition truth (no Louvain claim)
- section 7 practice chain model: stored-chains inclusion law replaces
  follows-edge grouping; chainsToGraph bijection claim deleted
- README: graph clauses (stored chains, derived view, chain dedupe),
  practice clause, ledger inventory, and two long-stale line-5 claims
  (ledger events, semantic top-K) corrected
- contracts/graph.ts GraphSnapshot comment: Louvain claim replaced with
  birth-session truth; extraction.ts comments no longer narrate the
  dead expandFindings/chainsToEdges seam
2026-09-30 03:27:10 +03:00
amosblomqvist 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
2026-09-30 03:22:52 +03:00
amosblomqvist a3aabb3a57 extractor redesign: chains are the deliverable (owner-approved, goal-first) 2026-09-30 02:09:25 +03:00
amosblomqvist 08e64466ad chain line: the logic is the connective — LaTeX ∧ and ⟹ via KaTeX (owner ruling)
'A → B → C' misread as chained implication when A and B are
CO-premises. The honest shape (owner, 2026-09-30): premises join
with the LaTeX LAND (∧), the conclusion separates by the LaTeX
IMPLICATION (⟹) — A ∧ B ⟹ C.

The glyphs are REAL KaTeX renders — components/latex-glyph.tsx wraps
katex.renderToString (cached per command; the app's math engine, the
stylesheet already rides app-wide from app/layout.tsx). No SVGs, no
font-dependent unicode. PROMINENT in the theme foreground (white in
the dark theme) per the owner's same-round ruling — the logic is the
payload, not the old dim gray. A lonely chip carries no glyph (it
reads as a standalone proposition); the plain-text '→' survives only
in the row's aria-label (formatChainLine, the screen-reader form).
2026-09-30 01:35:59 +03:00
amosblomqvist 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.
2026-09-30 01:19:03 +03:00
amosblomqvist 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.
2026-09-30 01:03:59 +03:00
amosblomqvist 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).
2026-09-30 00:44:08 +03:00
amosblomqvist edb10e3791 chain table round 6: the thread bracket law (owner refinement)
The tree threads no longer touch their arrows. Each thread is
BRACKETED by checkbox bottom edges (live-measured anchors):
- starts at its parent row's checkbox bottom edge — a visible gap
  below the arrow's stroke (the stroke ends ~2.4px above the anchor)
- terminates exactly at the LAST child row's checkbox bottom edge
  (cluster: top-[23px]/bottom-[8px] incl. the 1px group rule; chain:
  top-[22px]/bottom-[8px]). Verified live: 23=23 top anchor, bottom
  ends within 0.13px of the last checkbox at both levels.
2026-09-30 00:37:55 +03:00
amosblomqvist 40b0c33f93 chain table round 5: tree threads + the framed table (owner trial)
- TREE THREADS: open folders and chains drop a vertical guide from
  their expander arrow straight down through their children — at the
  arrow's own column (37px clusters / 73px chains), subtle border/50.
  Live-measured pixel-exact: arrow center = thread x (325=325, 361=361).
- The framed table: a rule under the header and under the last group
  join the between-group separators; rows stay lineless.
2026-09-30 00:28:47 +03:00
amosblomqvist b85aab6eb0 chain table round 4: double the indentation (owner refinement)
Each staircase step is now TWO columns (36px): chains pl-12 — the
checkbox aligned with the cluster's FOLDER icon — and statements pl-21
— the checkbox where a chain icon WOULD sit (the chains lost theirs;
the pattern continues anyway). Live-measured: chain checkbox = folder
icon (336=336); statement checkbox = chain checkbox + 36 (372).
2026-09-30 00:23:15 +03:00
amosblomqvist d6a7228b15 chain table round 3: waypoints icons gone + the alignment staircase (owner refinements)
- chain rows lose the WaypointsIcon (owner: not needed — the chips +
  connectors already read as chains); folders keep their icons.
- THE ALIGNMENT STAIRCASE: every level's checkbox sits directly under
  its parent's expander arrow. The expander now shares the checkbox's
  14px width (h-5 w-3.5), making each nesting step exactly 18px —
  clusters pl-3 (12px) → chains pl-7.5 (30px, the cluster expander) →
  statements pl-12 (48px, the chain expander). Live-measured: 318=318,
  336=336.
2026-09-30 00:15:47 +03:00
amosblomqvist 4fa6e2105a TopBar standardization + chain table round 2 (owner refinements, 2026-09-29 late)
- components/top-bar.tsx: THE shared top bar (standardization ruling) —
  h-15, lh-drag + carve-outs, select-none, SidebarToggle reopen with the
  Electron traffic-light gutter; adopted by chat (rename keeps its
  drag={false} escape hatch), new-session, practice-list, practice-exam,
  and the chains overlay.
- chain table: prominent indentation (pl-11 / pl-[4.75rem]), the ONLY
  lines are the separators between cluster groups (row + vertical lines
  gone), plain statement rows (no conclusion marker), all three columns
  sortable (Props too) with a small chevron on the active header.
2026-09-30 00:02:24 +03:00
amosblomqvist e2109d1c29 chain table redesign (owner refinements, 2026-09-29 evening)
- full-bleed: blur + translucent surface IS the overlay, no inner card;
  own top bar (info + search + close, lh-no-drag so home's drag mask
  never eats the X — the clickability bug)
- three levels, one click each: cluster (birth session; a chain lives in
  its CONCLUSION's cluster) → chain → proposition STATEMENTS revealed
  directly (names never twice; singletons one click); all collapsed by
  default
- proposition names as outlined chips, → connectors between; lonely chip
  = standalone proposition — the '(no premises)' annotation is dead in
  the visible UI (survives only as aria-label); Folder/Waypoints icons;
  rotating chevrons; deep statement indent
- real table look: hairline row + column lines; sortable Name + Created
  headers (click toggles asc/desc; size is text, not a control)
- selection never shifts layout: floating bottom bar (Google-Photos
  pattern) with N propositions → M chains + Clear + Generate
- fuzzy search grouped + auto-expanding clusters

live-verified end-to-end on the owner's dev server; 15 rewritten render
pins; gates: tsc 0, 1179/1179, lint 0, build 0
2026-09-29 23:21:51 +03:00
amosblomqvist 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
2026-09-29 22:41:52 +03:00
amosblomqvist 06032769d2 practice: the learner's creation note — optional free-form input carried to partitioner + generators (owner, 2026-09-29)
- dialog: optional 'Anything else?' textarea (typing-only, 2000-char mirror)
- API: body gains note? — trimmed, whitespace-only omitted, hard 400 over 2000
- pipeline: note persisted on the set and threaded to both agent stages
- agents: THE LEARNER'S NOTE prompt block (present only when set) + the
  weighed-never-owed stance pinned in both doctrines
- fix en route: doubled THE PARAMETERS heading in both prompts since slice 3
  (renderParams owns the heading; the literal blocks removed)
2026-09-29 22:16:28 +03:00
amosblomqvist cf0ec4a2f0 practice UI round — top bars + sidebar reopen, one-attempt per-problem submit with instant reveal, score-only grading (owner 2026-09-29) 2026-09-29 21:54:32 +03:00
amosblomqvist ddce919f9b practice live-test fixes — scroll containers, session math delimiters, header guidance, sweep heartbeat (owner live test 2026-09-29) 2026-09-29 21:03:46 +03:00
amosblomqvist 88eb4b2ec0 practice restructure — per-problem jobs + ONE shared background-LLM pool (owner ruling 2026-09-29)
Rulings: each problem generation is its own visible dock job (the
extraction per-chunk pattern), and practice shares extraction's
semaphore — one process-wide 3-permit pool for ALL background LLM
jobs; total background LLM concurrency bounded at 3.

A. SHARED POOL: new src/agent/subagents/bg-llm-semaphore.ts —
   globalThis __lhLlmSlots, PERMIT_COUNT 3 (the ruling), the
   fresh-take/handed-off-permit deadlock discipline preserved for
   BOTH callers. extraction.ts's withExtractSlot is now a thin
   wrapper (job-row signal → withBgLlmSlot); __lhExtractSlots and
   __lhPracticeSlots both die. The partitioner now acquires a permit
   too (it's a background LLM job — it didn't before).

B. REGISTRY ROWS: the aggregate setId:generation / setId:grading rows
   are replaced by setId:partition (phase partitioning, label
   'Partitioning — title'), setId:gen:problemId (queued→generating,
   'Problem i/N — title'), setId:grade:problemId (queued→grading,
   'Grading i/N — title'). Each row carries its OWN abort controller
   and its OWN usage (one PracticeStepLog per row — the aggregate
   partitioner/generator/grader folding dies). Row key = feed id =
   abort key (the dock dup-key law).

C. ABORT SEMANTICS: abortPracticeRow(key) routes by shape — gen row:
   THAT problem only (error 'Aborted', siblings continue, set settles
   ready with one errored problem); partition row: the whole run;
   grade row: that grade drops (answer kept, re-submittable).
   abortPractice(setId) composes from row aborts + the synchronous
   persisted marks (a set-abort's verdict stands before the settles
   re-read it — the settle step keeps an existing error status).
   /api/activity/abort kind practice now takes the ROW key (charset
   admits ':').

D. FEED + DOCK: ActivityPractice/FeedPractice carry per-row identity
   (id = row key, problemIndex/problemTotal); dock blobs render per
   row, labels carry the i/N; the aggregate row is gone.

E. Boot sweep unchanged at set grain; the live-row check is now a
   key-prefix scan (any live row of the set protects it). The
   updatedAt heartbeat covers the new row scheme as-is.

Tests (20 in the pipeline suite): per-problem row shape/uniqueness/
i-N labels (partition row frozen behind a gated partitioner; per-row
usage ISOLATION pin — b's log never shows on c's row); abort-one-gen
(sibling completes, set ready with one errored problem); partition-
row abort; queued grade-row abort (dead-ticket path: answer kept,
re-submittable, sibling lands); shared-pool identity + contention
(practice queues behind extraction-style holders); registry/dock
feed mapping. Test isolation hardened: unique per-store set ids,
afterEach registry/lock/pool reset (a timed-out test can no longer
poison the rest).

architecture.md: jobs-pattern paragraph rewritten (per-problem rows,
shared pool, abort routing).

1149/1149, tsc 0, lint 0, build 0.
2026-09-29 20:42:33 +03:00
amosblomqvist 25cc2a238a practice — formatting doctrine for generator + grader (owner-approved 2026-09-29)
Prior state: zero formatting guidance anywhere in the agent module —
models wrote prose-only by default.

GENERATOR_DOCTRINE Part III gains the FORMATTING bullet (after the
THE PARAMETERS bullet, before PREVIOUS PROBLEMS): statement, solution,
and rubric render as full Markdown — math always LaTeX (inline $…$,
display $$…$$), code/pseudocode/formal systems in fenced blocks with
a language tag, identifiers inline as `code`, tables for parallel
structure, lists for enumerations; prose-only renderings of
mathematical or structured content are a defect.

GRADER_INSTRUCTIONS gains the feedback-formatting paragraph (before
the OUTPUT section): full Markdown, LaTeX math, fenced code, lists/
tables where they carry structure — teach the boundary in properly
formatted prose, not plain text.

PARTITIONER gets nothing (machine-shaped output).

Tests: substring pins on the exported doctrine constants — the exact
FORMATTING sentence + ordering (after presentation, before previous
problems), the exact grader paragraph + before-OUTPUT ordering.
Architecture.md §7 one clause (the formatting doctrine in the
generator's prompt inventory).

1144/1144, tsc 0, lint 0, build 0.
2026-09-29 20:21:12 +03:00
amosblomqvist 8d39f3717c practice bug fix — the boot sweep stamped a LIVE run (owner live test 2026-09-29)
Owner report: created a set, 'Interrupted (server restarted)' showed
on list + set views, generation proceeded and overwrote it. The
sweep's 'generating + no registry row' test had a false-positive
mode: it ran at EVERY pipeline instantiation, and an instance could
evaluate against a registry whose fresh row it couldn't observe.

Three guards, per ruling:

1. HEARTBEAT: ProblemSet.updatedAt? (contract + doc) — bumped on
   every store write; createSet stamps it = createdAt; saveSet
   re-stamps (validator tolerates absence for legacy rows).

2. ONCE PER PROCESS: the sweep guards on globalThis
   .__lhPracticeSweepDone — no instance re-evaluates after the
   process's first sweep.

3. GRACE WINDOW: only sets whose heartbeat (updatedAt ?? createdAt)
   is older than 120s are stamped. A live run saves at every phase/
   problem transition, so it is structurally unreachable; a true
   orphan is minutes stale at next boot. One [practice <id>] log
   line per stamped set (the extraction/jobs console.error idiom).

Tests: stale orphan stamped; FRESH generating set untouched (grace);
once-guard (second factory instantiation never re-sweeps); live-row
protection re-armed via resetSweep() test seam; store pins for
updatedAt birth-stamp + re-stamp + legacy tolerance. Architecture.md
sweep sentence updated (once-per-process + grace semantics).

1142/1142, tsc 0, lint 0, build 0.
2026-09-29 20:19:46 +03:00
amosblomqvist e92679254d practice — discretized parameter axes: named levels, never a number (owner-approved 2026-09-29)
Contracts: LadderRung 0-4, CueDistance 0-2, Presentation theory|
application; PracticeParams carries the three named levels. Doc
comments hold the three definition tables verbatim (rung names +
definitions from the ladder doctrine; cue levels; presentation
levels).

Core: LADDER_RUNGS/CUE_LEVELS/PRESENTATIONS tables in src/core/
practice.ts (definitions verbatim) — ONE source spoken by the agents'
prompts, the dialog's stepped controls, and the exam header; core
barrel re-exports practice.ts (pure domain, browser-safe).

Agents: Part I's ladder intro now 'five rungs, numbered; the request
arrives as a rung' with bullets numbered 0-4 (definitions untouched);
Part III's parameters block replaced with the named-level doctrine
(rung = exact class, no continuum; cue counts lesson examples AND
previous problems' situations; presentation dresses, never changes);
partitioner's RESPECT THE PARAMETERS replaced (low rungs = many small
assignments, high = larger; far cue makes confusables-together MORE
important; presentation does not shape the partition). renderParams
outputs the exact named-level block. Anchor/0..1 language swept dead.

Service + store: strict union validation — floats between levels,
out-of-range, wrong strings, legacy dial shapes all 400 at the API
and fail isProblemSet (legacy sets skipped from lists; no migration —
data/practice is disposable scratch, owner wipes before testing).

UI: dialog = 5-detent ladder slider + 3-detent cue slider (each shows
the level NAME + one-line definition) + Theory|Application segmented
toggle; defaults rung 2 / cue 1 / theory; Create posts the new shape.
Exam header gains the levels line ('Direct exercise · Adjacent
context · Theory').

Tests: renderParams exact-block pin; strict-validation pins (new
tests/practice-service.test.ts — bad unions 400, legacy shape 400);
store pins (legacy floats + 2.5 rung fail); render pins (detents +
names + definitions, defaults, detent-swap, toggle active segment,
levels line); every params fixture moved off floats. 1138/1138, tsc
0, lint 0, build 0.

Docs: architecture.md — discretization paragraph (compact tables,
named-level invocation, defaults, one-table source), UI mentions;
README axes sentence.
2026-09-29 20:06:35 +03:00
amosblomqvist a53127a764 practice fix follow-up — the picker lists EVERY birth-session group (owner ruling)
Owner escalation on deviation 1: practice availability must not
inherit a visual-rendering threshold. graphClusters() now runs
birthSessionPartition(snap.nodes) directly (same size-desc/id-asc
ordering) instead of reading the snapshot's MIN_CLUSTER_SIZE-filtered
clusters array — MIN_CLUSTER_SIZE stays a hull-drawing rule only.
Label join mirrors the snapshot's (chat title / notes path, null when
the naming record is gone). Contract doc + architecture.md sentence
corrected to match; empty-picker copy reworded ('once a session births
its first proposition' — no 3+ mechanics). Render pins: new empty line
+ a 1- and 2-proposition group listing pin. 1130/1130, tsc 0, lint 0,
build 0.
2026-09-29 19:28:51 +03:00
amosblomqvist f39e1b4294 practice fix — creation is graph-domain: cluster picker, nodeIds-only API (owner ruling 2026-09-29)
Owner ruling: 'problem sets always take in nodes — graph objects' —
practice is graph-domain, not chat-domain. No session-created sets.

API: POST /api/practice/sets takes {nodeIds, params, title?} ONLY —
the chatId branch, UnknownChatError, and the any-provenance filter are
deleted; the selector is nodeIds (readNodes filtered to the selection,
unknown ids drop, empty live intersection → 400 empty_selection);
title optional from the UI (the picker's cluster label), dated default
stays. chatStore leaves the pipeline deps entirely (labels resolve
store-side in the clusters service).

GET /api/graph/clusters (new): the birth-session groups the graph view
renders — graphSnapshot's clusters verbatim (same ordering, same
MIN_CLUSTER_SIZE presentation filter), mapped to {key, kind
chat|notes, label (null when the naming record is gone — honest,
never guessed), nodeIds, nodeCount}; corrupt/empty graph → empty
list. GraphClusterGroup contract in core/contracts/graph.ts (client-
safe — the dialog imports it).

practice-dialog: session picker → cluster picker (rows show label +
proposition count, notes clusters marked); Create posts the cluster's
nodeIds + label as title + params. 400 empty-selection copy reworded
for the vanished-nodes edge ('Nothing practicable is left in that
cluster — the graph changed.'); 503 deep link unchanged. Preselected
mode gone — the dialog is the only creation entry for now (the graph-
view cluster entry comes later with free-form selection).

Sidebar: session right-click 'Practice this session's propositions'
removed (item + state + dialog wiring); the /practice nav row stays.

Tests: pipeline — chatId/any-provenance tests die, nodeIds selection
pins added (unknown-id filtering, dated-default title, title hint
rides, empty/vanished → EmptySelectionError); render — cluster picker
pins (label+count rows, notes kind, unlabeled cluster muted, empty
graph line), preselected pins removed; 16 render + 13 pipeline.

Docs: architecture.md §7 API+UI paragraph rewritten (graph-domain
ruling, /api/graph/clusters, cluster picker as the only entry, no
404-unknown-chat); README creation-flow line updated.

1130/1130, tsc 0, lint 0, build 0.
2026-09-29 19:26:37 +03:00
amosblomqvist 451caba1b1 practice slice 6 — docs: architecture practice section, README feature line
docs/architecture.md: new §7 'Practice — strengthening, not assessment'
(purpose ruling + parked exam machinery; the chain bijection with
follows-edges, supports invisible, exactly-once partition; self-
contained sets frozen at creation; two-stage generation — partitioner,
one generator per problem with YOUR TARGET echo law + THE FULL SET
minimal-use doctrine, commit-before-pose, failure isolation, dedup
across sets; feedback-only grading; registry-not-JobRecords jobs
pattern and WHY — not chat-scoped, never wakes the teacher, boot
sweep; API + UI surfaces; three strong-pinned roles on the shared
runSubmitAgent seam). Sections 7-13 renumbered to 8-14; the §9
cross-ref in the deferred list updated. Cross-referenced enumerations
updated: §1 six-systems table + row, §2 layer count 25→28 routes,
§2.2 seven contract files + practice.ts, §3 PracticeStore port +
adapter + composition-root bullets + PORTSBOX, §4 six-systems heading
+ PRACTICE mermaid box + practice-request protocol, §6 practice-
territory sentence now points at the landed module, §10 observability
practice rows + abort kinds, §11 testing strata, §12 seam directory
row.

README.md: practice paragraph in Key mechanisms at the same grain
(feedback-only problem sets, partitioner/generator/grader, registry
jobs); the graph paragraph's 'assessment becomes practice-module
territory' forward-reference resolved to the landed module.

Docs-only — no code changes. 1129/1129, tsc 0, lint 0, build 0.
2026-09-29 18:44:15 +03:00
amosblomqvist 832c4f4cef practice slice 5 — UI: list page, exam page, creation dialog, sidebar entries
components/practice-dialog.tsx: portal dialog (delete-session shape),
three dial rows (SettingSlider markup, anchor text as display:
flashcard-competition / near the lesson-far transfer / theory-
application, defaults 0.5), session picker (generic mode, /api/chats)
or preselected lock (right-click path); 400 empty_selection renders
'This session has no propositions yet', 503 renders the server line +
Open settings deep link (useSettingsDialog, zero-keys precedent).
Pure body exported for SSR pins.

components/practice-list.tsx: rows (title, date, ready/total problems,
graded count, status word; destructive tint on error), click-through,
delete affordance → portaled confirm (DeleteSetBody: no mode checkbox
— a set has no preserve-graph question), 2s reveal-poll while any set
is partitioning/generating. components/practice-exam.tsx: numbered
problems; generating stubs (BrailleSpinner + 'Generating problem…',
propositions never surface); ready statements/solutions/rubrics/
feedback as MessageResponse markdown with restoreMathEscapes at the
display seam; per-problem textarea + useDictation instance with the
DictationBar rendered only on box focus; one Submit answers POST per
set; graded reveal (verdict chip, feedback, model solution, rubric)
with read-only answer; polling while not ready OR grading-live
(activity feed practice rows — dropped grades terminate the poll).

app/practice/page.tsx (force-dynamic, listPracticeSets) + app/practice/
[id]/page.tsx (getPracticeSet + notFound). Sidebar: quiet Practice nav
row under New session (GraduationCapIcon, active on /practice) +
'Practice this session's propositions' in the session right-click
below the hairline, above CoverageMenuBlock — opens the dialog
preselected.

tests/render-practice.test.tsx — 15 pins: dials + anchors, picker
selection gating, preselected lock, empty-selection line, 503 deep
link, list rows/counts/empty, delete confirm + inline failure, exam
generating (no proposition text, no answer boxes, no submit), ready
(markdown statement, gated submit), graded reveal + read-only + sibling
answerable, failed problem/set lines, math healing. 1129/1129, tsc 0,
lint 0, build 0.
2026-09-29 18:40:07 +03:00
amosblomqvist 27c5d9df19 practice slice 4 — pipeline, registry, API, feed
src/agent/subagents/practice-pipeline.ts (sibling of practice.ts —
orchestration vs calls): createPracticePipeline(graphStore+practiceStore
+chatStore+PracticeModels). Fire-and-forget front doors returning
{set, done}: createProblemSet (refuse → resolve selection (chatId =
ANY-provenance membership per brief; nodeIds raw) → chains → snapshot →
createSet partitioning → registry row → runPartitioner → stubs land
(generating) → ONE generator per problem behind the process-wide
3-permit abort-aware semaphore (__lhPracticeSlots, extraction idiom) →
per-problem failure isolation → ready (errored problems included);
abortPractice (idempotent: controllers + 'Aborted' marks; dropped
grades keep answers, re-submittable); submitAnswers (ready-only,
already-graded rejects, one grading job per answer under the same
semaphore); deleteProblemSet (abort-first). Per-set write mutex on
globalThis (__lhPracticeLocks, audit-lock idiom); registry
__lhPracticeRuns one row per set per kind; boot sweep marks orphaned
partitioning/generating sets 'Interrupted (server restarted)'. Typed
errors: EmptySelection/UnknownChat/SetNotFound/SetNotReady/BadAnswers.
Usage folds stage logs into the registry row (v1: completion-folded).

src/server/practice.ts: thin service — strict params parsing, typed
error → response mapping (503 envelope family, 400/404/409), list/get.
Routes: POST+GET /api/practice/sets (202 {id}), GET+DELETE
/api/practice/sets/[id], POST /api/practice/sets/[id]/answers (202).
Composition root: chat-handler wires createPracticePipeline (engineFor
fresh + providerKeyMissing gate) exported as ; barrel re-
exports. Feed: observability activityFeed() += practice rows
(ActivityPractice); dock blobs (process 'practice', GraduationCap
icon, set-keyed abort); /api/activity/abort kind 'practice'.

tests/practice-pipeline.test.ts — 13 tests: end-to-end (stubs mid-
flight via gated generators, coverage complete), failure isolation,
3-permit semaphore pin, abort mid-generation, registry shape pins,
refusal-before-create, chatId any-provenance selection, typed errors,
answers→grades land + graded rejects, boot sweep (orphan + live-row),
delete. 1114/1114, tsc 0, lint 0, build 0.
2026-09-29 18:31:53 +03:00
amosblomqvist da0cd558d0 practice slice 3B — the practice agents: partitioner, generator, grader
src/agent/subagents/practice.ts: createPracticeAgents(deps) with the
ExtractionModels mirror (per-stage engine overrides + engineFor +
providerKeyMissing + refuseIfStagesUnready, all three roles strong-tier).
Generator doctrine = the owner's problem-engineering framework verbatim
(minus Sources) + Part III wiring section per the green-lit amendments
(FULL SET input with minimal-use doctrine; degradation never in
meta-commentary). Partitioner: true-partition doctrine, validate via
chainCoverage + exactly-once + unknown-ids, error-back retries through
runSubmitAgent's new validate hook (ToolInputError → error tool result).
Generator: echo law (chains set === target). Grader: verdict union +
nonempty feedback (schema-enforced). renderChains/renderPropositions/
renderParams/renderPreviousProblems. BACKGROUND_MODEL_ROLES extended with
the practice trio (settings override symmetry). runSubmitAgent gains the
validate seam (null = accept). 16-test faux-engine suite incl. retry
paths, refusal gates, neverSubmitted.
2026-09-29 18:14:30 +03:00
amosblomqvist 00b3c23e06 practice slice 3A — runSubmitAgent extracted to shared module; practice model roles
runSubmitAgent moved verbatim from extraction.ts to src/agent/subagents/
submit-agent.ts (exported; extraction imports it — zero behavior change,
extraction suites green untouched). New optional prepareArguments seam:
extraction wires its healStringifiedArrays heal; default is identity
pass-through. ModelRole += partitioner | problemGenerator | grader;
ROLE_SLOT pins all three to strong (quality-critical trio, plan default).
Note: BACKGROUND_MODEL_ROLES (per-role settings overrides) deliberately
NOT extended — settings-surface decision, flagged for the planning
session.
2026-09-29 17:57:53 +03:00
amosblomqvist b7a19609cb practice slice 2 — store, paths, self-contained sets
Contract amendment (planning ruling): ProblemSet gains propositions
(nodeId → {name, statement}) and chains (the frozen derivation) — sets
are documents of what WAS selected; grading/dedup/coverage never touch
the live graph. config.ts PATHS.practice → data/practice/. PracticeStore
port (createSet assigns ps_<ts36>_<rand>, getSet, listSets createdAt-desc,
saveSet replace-whole, deleteSet). src/store/practice.ts: one JSON per
set, hand-written isProblemSet read guard (types-only; corrupt/missing →
null/skipped, never thrown; saveSet refuses invalid records). Wired in
src/server/stores.ts. 10-test suite: round-trip, corrupt-tolerance,
missing-dir, validator garbage-reject + optional-tolerate + verdict
union, ps_ id shape.
2026-09-29 17:53:36 +03:00
amosblomqvist 05dcaafc68 practice slice 1 — contracts, the chain bijection, coverage
src/core/contracts/practice.ts (new domain): ReasoningChain (group by
conclusion, id = conclusion node id), PracticeParams (three 0..1 dials),
ProblemSet/Problem lifecycle, feedback-only ProblemGrade. src/core/
practice.ts: chainsForSelection (follows-only, both-ends-in-selection,
group-by-conclusion, singletons for edge-less nodes), chainsToGraph
(the exact inverse — bijection pinned), chainCoverage (the completeness
law: union of problems' chains = the set's chains; duplicated allowed).
13-test suite: diamond, premise-only, singleton, supports-invisible,
outside-endpoint, dedupe, determinism, round-trips, coverage cases.
2026-09-29 17:48:20 +03:00