Commit Graph
719 Commits
Author SHA1 Message Date
amosblomqvist 951c7704b9 wave E stage 1: pdf_render goes native - pdfjs paints, Skia hosts
The browser was never the PDF engine - pdfjs always painted the pages;
Chrome only hosted the canvas tab. Now pdfjs-dist v6's own Node
integration (@napi-rs/canvas, its optionalDependency) hosts in-process:
same paint code, Skia canvas, zero browsers, zero temp files, no base64
hop. The whole pdf-reader runs browser-free - the Chrome gate on
pdf_render is gone.

The Skia binary (27MB per platform, exactly one per build - npm installs
only the building machine's) rides the standalone via the same
outputFileTracingIncludes pattern as pdfjs-dist. Verified: builds green
(1515), native render pinned against the real fixture (canonical
single-IDAT PNGs, dpi/72 arithmetic, page-range skipping), and the
canvas loads + paints from inside .next/standalone.

Stages 2-3 (resvg for SVG, the --browser-service demo helper) follow.
2026-10-11 06:49:13 +03:00
amosblomqvist 0af005d343 shell hardening: log() guards stdout (EPIPE) + uncaughtException log-and-continue
Exposed by the update drill: a packaged launch whose stdout consumer
exits (piped run, launcher) died on its next log line with an uncaught
EPIPE dialog. The stdout write is now guarded; the durable channel is
userData/logs/main.log. Stray main-process exceptions log and continue -
the window + child server are the product.
2026-10-10 14:20:11 +03:00
amosblomqvist e8e00a5237 auto-update wiring: silent download, restart banner, local-feed drill seam
electron-updater in the shell (packaged builds only; dev never updates).
Checks at startup + every 24h, downloads silently, and the renderer shows
a Restart banner once the update is staged (it installs on quit either
way). Manual "Check for Updates..." under the app menu answers via dialog.
Every updater event routes through the shell log and is caught - the
updater can never take the app down (verified live: a feed failure logs
three lines and the app runs on).

LH_UPDATE_FEED overrides the baked GitHub feed with a generic local
server - the pre-release drill: serve latest-mac.yml + zip + blockmap
from localhost and a real installed copy updates itself, no release, no
public repo.

The asar allow-list re-includes electron-updater + its 16-package runtime
tree explicitly (gitignore last-match-wins; asar verified at 2.1 MB).
The banner rides the preload bridge (onUpdateStatus/installUpdate) and
renders nothing in the browser. window.lh typing centralized in
sidebar.tsx's declaration.
2026-10-10 14:11:19 +03:00
amosblomqvist eda5491010 schema stamps: lazy v1 for chats + the graph family, honest refusal
Chat records gain version (CURRENT_CHAT_VERSION), stamped by a single
writeChat choke point in the store; absence reads as v1 — existing files
migrate on their next write, no mass rewrite. The graph family (four JSONL
ledgers, sources.json, render-settings.json) shares one lazy sidecar,
graph/format.json; positions stays unstamped (rebuildable) and jobs stay
unstamped (transient).

Downgrade scenario ruled honest-refusal: records stamped newer than the
running build read as absent (chats, logged) or throw (graph family) — a
read-modify-write store never rewrites what it doesn't understand.

Golden fixtures stay the unstamped legacy shape on purpose (lazy v1 is
current-valid by design); the stamped write path is pinned by
tests/schema-stamps.test.ts. Reads never write — the golden byte-identical
pin holds.
2026-10-10 13:54:17 +03:00
amosblomqvist bc09ec25e1 fixtures: golden data dir — real records, production read paths
Owner-manufactured teach master (chat_mv27rrra: compaction, subagent-abort,
every teach tool) + practice lifecycle (chat_mv293wv5: the full marker cycle
including the first-ever problem-dismissed), the real graph ledgers, five
jobs across the kind x status matrix, and settings. tests/golden-fixtures
reads all of it through the stores via the LH_DATA_DIR seam and pins the
fixture law: real data parses, flattens to a legal LLM wire, derives the
graph, covers every custom-marker kind, and reads never write.

Fixture law: an intentional format change updates these files in the same
commit; an unintentional one fails the suite.
2026-10-10 13:40:39 +03:00
amosblomqvist 53112d2861 practice round: dead session starts, prompt laws, preamble, label unify
THE START BUG (owner report, reproduced live both kinds): new sessions
(?prompt flow — practice and teach alike) sat dead until a second
message, which then persisted WITHOUT the first. Root cause, proven
both directions: the submit POST rode the view's consumeAbort
controller; React StrictMode's simulated remount fires Diet A's
unmount cleanup (model.stop()) ~8ms in and killed the POST mid-flight;
the page's sentInitial guard had already flipped, so it never retried.
Teach was equally broken — only unnoticed, because every teach chat in
use predates the diet and practice starts are always fresh. Fix: the
submit law — POSTs are not abortable by view teardown (they are ~100ms
acks since Diet B and cannot starve the pool); after the ack, a
consumeAbort identity check guards the stream attach. Pinned with the
stop-mid-submit regression test. Live: StrictMode on, practice draft
POST 200/67ms, stream attached, message persisted, problem posed.

PRACTICE PROMPT (00-base): the opening rule — material arrives two
ways, both first-class (picked propositions primary; free-form —
topics, course material, files/PDFs — fully supported, never refused);
as soon as material exists the reply poses, at most one short line
before the problem call, no warm-up interview. Plus the hard law: no
problem is ever conversation — every problem through the tool (no
thinking field/no reveal/no verdict attaches to prose).

GRAPH-CONTEXT PREAMBLE: custom markers flatten to user-role on the
wire, so the old "Contextualization — the learner picked…" read like a
system broadcast in the learner's voice channel. Now the bracket
convention: "[Session context] — picked from the learner's knowledge
map…".

SIDEBAR: busy rows unify to "Working…" for both kinds (the spinner's
default label included); "Practicing…"/"Teacher is working…" retired.

Gates: 1490/1490, typecheck, lint. doctrine's stale doc pointer fixed.
2026-10-10 12:19:31 +03:00
amosblomqvist 05f465942a readme: the storefront rewrite
The README becomes the storefront for the public repo — what it is, why
it exists, BYOK/local/inspectable values, download, quick start —
written from the canonical articulation (owner doc, 2026-10-05): the
dependency structure of understanding, measure-first teaching, one
reasoning step at a time, the accumulating world model. All developer
content (commands, env, architecture, mechanisms, testing, lineage)
moves verbatim to docs/development.md (stale suite count and a private
machine path dropped on the way).

Owner will edit before the release; this is the base coat.
2026-10-10 11:22:17 +03:00
amosblomqvist 0a7d12c364 repo: public housekeeping — license, product name, docs diet
- LICENSE: AGPL-3.0-only (holder Amos Blomqvist), package.json gains "license".
- Product/package name unified: package.json "name" is now "learnharness"
  (matches the GitHub repo); electron-builder productName/appId already
  were LearnHarness/com.learnharness.LearnHarness.
- Deterministic userData: app.setName("LearnHarness") early in main.cjs
  — Application Support dir is exactly "LearnHarness" in dev and
  packaged runs (packaged Electron would otherwise derive it from the
  package name). Dev shell becomes "LearnHarness-dev". Pre-release
  rename: zero external users, no migration code; the local test dir
  was moved by hand.
- docs/ diet: nine vision-*.md splinters, the completed pi-ai migration
  plan, and research/ notes leave the tree (retained privately in the
  git-ignored memory; full history stays in git). Public docs remain:
  architecture, vision, plan, probe-teach-split, pi-engine-swap,
  keyboard, teaching-methodology (+ release-0.3 until the ship).

Gates: 1489/1489, typecheck, lint.
2026-10-10 11:20:20 +03:00
amosblomqvist 26bc305d66 ui: plan canvas ink extent + the keyboard-first round
Plan canvas (owner report: arrows clipped at the viewBox edge in dist —
measured 14px/20px cuts in the owner's real plans): blockLayout now
unions node boxes with every edge path's control-point hull (a bezier
never leaves it; the layout calls edgePathD with the renderer's exact
inputs). Dagre's node extent stays the floor, so ordinary plans render
byte-identical; the viewBox origin shifts only when ink bulges past
zero. Verified on both real chats; pins use the owner's real
linear-algebra tree (golden-real-data).

Keyboard-first (owner's law: Enter always submits, arrows always cycle;
text fields the only exception):
- Enter guard split (enter-submits): a focused BUTTON keeps native
  Enter only when keyboard-walked (:focus-visible) or inside an open
  overlay (menus/dialogs); pointer-clicked transcript buttons submit,
  preventDefault suppressing the re-activation underneath.
- Focus hygiene (focus-hygiene): pointer clicks on transient controls
  (agent/user thinking toggles, copy, mermaid expand, plan chip) blur
  to body — the state clicking empty space produces.
- Session search owns the walk: Up/Down wrap-cycle the filtered rows,
  Enter opens the walked row or the first match (quick-open), then
  blurs and keeps the query. The Alt-arrow session cycle is RETIRED.
- Registry: model-menu walk/pick entries removed (obvious in-field
  navigation, not settings material); question-dock keys stay
  displayOnly per owner ruling.

Gates: 1489/1489, typecheck, lint. Verified live end-to-end.
2026-10-10 09:43:12 +03:00
amosblomqvist 643e7596f0 streaming: connection diet — the parallel-session freeze, fixed
Root cause (postmortem, verified live): Chromium caps 6 concurrent
HTTP/1.1 connections per origin. Every running turn pinned slots for
its whole life — the POST /api/chat response stream plus a reattach
GET stream per visit that never closed on navigate-away — so two
concurrent thinking sessions starved navigation RSC fetches in the
browser queue (147s measured) until the turns settled. Server was
exonerated (3ms curls during the freeze); main thread idle throughout.

- COMPACTION LAW + WIRE DIET (ui-stream): store the states, not the
  chunks — replace-in-place snapshot retention keyed by updateSlots,
  ~100ms coalesced latest-wins fan-out, slim wire drops pi's duplicate
  assistantMessageEvent payload.
- DIET B: POST /api/chat acks and closes ({ok:true}) — the turn wire
  is GET /stream for everyone, submitter included; send() reattaches
  via refetch-first resumeStream(true).
- DIET A: unmount cleanup calls model.stop() — navigating away aborts
  the view connection (turns are server-side background objects).
- Answer-flicker fix: resumeStream's eager tail clear moved off the
  refetch path (the refetch swaps base+tail atomically; clean-slate
  guard restored on refetch failure) — the optimistic answer card
  survives refetch latency (1.7s stalls measured on dev).
- Tests converted to the ack contract (drainTurn/drainRunning helpers,
  dietFetch mock) + a pin that the POST body reads while the turn runs.

Gates: 1489/1489, typecheck, lint. Live: dual streaming sessions,
navigations 335-648ms, in-page settings 19ms (was 147s).
2026-10-10 09:42:47 +03:00
amosblomqvist 5c1eb47740 packaging: signing + notarization + zip target + GitHub Releases feed
The 2026-10-08 flip, verified end-to-end (notarized build accepted,
stapled, spctl clean): Developer ID auto-detect, LH_NOTARY keychain
profile, zip + blockmap beside the DMG for Squirrel.Mac, publish block
pointing at GitHub Releases (repo goes public before 0.3.0; builds never
auto-upload — publishing is a release-time act).
2026-10-09 07:59:32 +03:00
amosblomqvist 1f41fb05a5 question tools: phase-visible names, hardened teach schemas, context-slim seams
The tools_fails_2 diagnosis (glm-5.3 dropping trailing required fields):
- probe-phase quiz/explain renamed to probe/recall — a shared name carried
  the probe call shape into teaching (the first teach quiz dropped nodeId);
  display seams accept the old names forever
- teach quiz+explain schemas lead with the required pair (type, nodeId);
  explain gains the predict/check type and joins the reveal law (checks
  collapse, predictions arrive open; no-type records render check-style)
- plan_init/probe_done results go context-slim (owner, context diet):
  content = validated nodeIds + binding-id reminder / fixed ack; details
  keeps the tree for the popup and {ok:true} — serializer-verified that
  details never reaches the model; failure echoes dropped
- the cards carry the tool's own name (Probe/Recall) and the quiz type
  reads on the label (Prediction/Check) in composer and transcript
2026-10-09 07:59:23 +03:00
amosblomqvist c23eb1f15b tools: the standard argument heal — every registry, both seams
The whitehead incident (2026-10-09): glm-5.3 emitted quiz type with
quotes baked in ('"predict"') — rejected ten times, byte-identical
retries, the lesson never started — and correctAnswer with the JSON
key-value colon glued on ([':parmenides']). Third GLM wobble incident,
and each prior heal was wired only where its incident landed; this one
landed precisely where the practice heal never reached.

Layer 1 — healEncodedArguments moves to core/argument-heal (the display
seam needs it too — the tool-parts law) with withArgumentHeal +
healToolRecord (agent/tools/heal) wired at EVERY registry: prober,
teacher, practice, bareQuestionTools (it had no prepareArguments at
all), demo-builder, and submit-agent (composing after
healStringifiedArrays; the auditor's extraTools wrapped).

Layer 2 — stripModelValueDebris (core/value-debris): leading/trailing
colons, quotes, backticks, whitespace strip at the MEMBERSHIP seams —
coerceCorrectAnswer (boundary check + display + grading in one),
normalizeQuizType (persisted wobbled records keep their role),
resolveTargetId (copy-syntax on targets).

Layer 3 — schema description hygiene: the quiz type and practice
mode/outcome/rung/missClass values now read as bare words — the quoted
JSON-key style invited quote-copying.

Display mirror — parseQuizInput heals the raw record on read, so a
boundary-healed quiz whose record keeps raw args renders instead of
becoming an invisible pending quiz (learner lockout).

Pins: tests/tool-heal.test.ts (the incident shapes end-to-end through
pi-ai validation, the wiring, the strips, the mirror). The stringy-
options pins flip to boundary-parity per tool-parts' own law; the
chat-flow garbage pin keeps its honest-reject intent with an unhealable
shape. 1471 tests, typecheck, lint green.
2026-10-09 04:54:34 +03:00
amosblomqvist e4207e869f setup flow: the dead dropdowns, save-never-sends, the pill hover
- The setup pickers opened invisible: React attaches a host element's ref
  AFTER descendants' layout effects, so MenuShell's mount-time place()
  measured a null anchor and gave up — the menu stayed hidden until a
  window resize re-ran place(). The stage-2 slot and thinking menus now
  anchor by anchorTestid (querySelector at place time, the settings-menu
  pattern) instead of the conditionally-attached triggerRef.
- Saving models NEVER auto-sends the queued message (owner ruling,
  2026-10-09): the draft waits in the composer and the learner sends it.
  The replay/draft-send calls removed from onDone in new-session, home,
  and chat; the dead force machinery removed from send/start/begin.
- The setup CTA pills get the app-wide hover language (hover:bg-accent +
  cursor-pointer) — hover:bg-muted was invisible because --muted and
  --card are the same #1c1c1c.
2026-10-09 04:54:24 +03:00
amosblomqvist 4355667d96 ui: the reading field's phantom scrollbar — the 1px font-box surplus
Empirically diagnosed via live browser measurement: Source Sans 3's
font box is 22px at 16px, but lh-input's 1.25 leading gives a 20px line
box - the font's content area sticks 1px below the line, Chromium
counts it in scrollHeight, and the zero-padding field-sizing textarea
turned it into a permanent 1px scroll: the bar showed EMPTY and at
every expansion size below max-h (owner report). The composer and dock
fields never showed it because their py-2 absorbs the surplus
implicitly. Fix: py-0.5 on the reading field - the same absorption,
line-height untouched (the caret law, typography round 2), and the
max-h-40 cap still scrolls honestly past 160px of text.
2026-10-08 19:25:47 +03:00
amosblomqvist d790384b72 ui: the invert disc's half is actually filled — and the demos match
Lucide's Contrast leaves its half-moon unfilled (fill=none default) -
the owner's catch. Hand-rolled InvertDiscIcon: same geometry, the half
carries fill=currentColor, so the filled side is filled. The demo cards
get the identical disc + 'Invert colors' tooltip on their invert
toggles, matching the figures.
2026-10-08 19:13:54 +03:00
amosblomqvist 1beb54d3d2 ui: figures join the document — no card chrome, caption row below
Owner round: the figure card loses its container entirely (no border,
no padding box) - the figure is document material. Image first, then
the CAPTION ROW below it: 'Figure' label aligned to the image's left
edge, the controls (invert toggle, download, fullscreen) on the same
row aligned to its right edge. The wrapper carries the natural-sizing
law, so for a declared-width svg the row hugs the centered figure's
own edges, not the column's.

The invert affordance: the house Toggle gains an optional icon disc -
the figure's toggle renders lucide's Contrast (the half-filled circle)
AS the sliding disc, plus a native 'Invert colors' tooltip; every
plain toggle elsewhere is unchanged. Interactive demo cards keep their
boxed stage (owner ruling: an interactive surface reads as its own
stage).
2026-10-08 19:01:04 +03:00
amosblomqvist 0f2519a349 ui: round five — no band in cards, the rail narrows again
Owner verdict on the accent step: way too light - in-card thinking
drops the band entirely (rail only, identical heights). The rail
narrows again: 14px gap inside the 3ch inset (rail calc(3ch-14px),
about 8px) - the wide rail was distracting. Total inset still 3ch,
chevron law intact.
2026-10-08 18:45:49 +03:00
amosblomqvist d30ad42d9c ui: round four — thinner rail, tighter band, the accent step, early wrap
Owner round four on the user rows: (1) the rail thins - the gap inside
the 3ch inset grows 5px -> 8px, so the rail is calc(3ch-8px); total
inset still 3ch, chevron law intact. (2) The band padding drops again,
5px -> 4px (py-1). (3) The in-card thinking band steps to bg-accent -
the same ONE-STEP lift the chat band gets (background->card on the page,
card->accent on the card); card-color on card-color was invisible, the
accent gives the card rows a visible band with identical heights. (4)
pr-4 on the band rows - the expanded text wraps EARLY instead of hugging
the band's clear right edge.
2026-10-08 18:38:07 +03:00
amosblomqvist 497b29804c ui: round three — the tight band, one geometry everywhere
Owner round three on the user rows: (1) the band padding halves,
py-2.5 -> py-[5px] — subtle, not tall; same for the messages. (2) The
in-card thinking joins the band: identical styling and heights for
collapsed thinking in the chat and inside question cards (card-on-card
is quiet by design; revert if it reads too light). (3) The left gap
MATCHES the new 5px padding, so the rail fills the rest of the 3ch
inset - border-l-[calc(3ch-5px)] pl-[5px]: a thick muted rail, total
inset still 3ch (the chevron-alignment law survives). The banded prop
dies - one geometry, cards included.
2026-10-08 18:32:21 +03:00
amosblomqvist 3691e07118 ui: the document, all-in — card-color bands and the 6px rail
Owner round two on user-vs-agent clarity (the chat-comeback was tried
and set aside): we go all in on documents. No chat sides. User messages
and every transcript user-thinking row are full-width CARD-COLOR BANDS
- the band is the authorship mark against the agent's transparent
prose. No rounded corners, no borders besides the rail.

The rail keeps its muted color but is now 6px wide (was 2px): the
artificially wide 3ch inset (the chevron-alignment law with the agent's
flush rows) is re-split - rail eats the air, the rail-to-text gap drops
from 3ch-2px to a natural 3ch-6px, and the total inset stays 3ch so the
chevrons still align. The 'You' label stays on messages - one register
with the thinking labels.

In-card thinking (inside answered cards) takes the rail but NO band:
card-color on card-color would be invisible; geometry alone carries it.
Transcript rows arrive expanded (clearly sent) - the reading lift now
opens on arrival too; the in-card row stays collapsed. Cmd+T still
rules all rows.

Also this round: the answered-card last-child rhythm law
([&>*:last-child]:mb-0) - on ask cards the thinking row's my-3 stacked
with the card's p-4 into a 28px void below (12px above); the card's own
padding is the bottom rhythm when a row ends the card.
2026-10-08 18:17:11 +03:00
amosblomqvist 8cb5e14691 desktop: the payload fixed — 7.4GB→144MB, dev keys out, boot-smoke gate
- desktop:dist cleans dist/ + .next/standalone before next build: the
  trace sweep was shipping the PREVIOUS build's DMG inside every new one
  (standalone/dist = 8.0GB inside the app; measured 7.4GB DMG → 144MB).
- the same sweep shipped the repo-root auth.json (dev BYOK keys,
  byte-identical) in every DMG ever built — excluded in the trace and
  REFUSED by a new build-server.mjs guard (.env* belt-and-braces).
- WARNING (next.config.ts): dir-style trace excludes (dist/** etc.,
  "./"-anchored or not) strip server-external packages to bare
  package.jsons — pi-ai/pi-agent-core lost their dist/, the packaged
  server 500'd on ERR_MODULE_NOT_FOUND. Only the three verified-safe
  entries stay.
- new scripts/smoke-server.mjs gates desktop:dist between payload
  curation and packaging: boots the standalone exactly as the Electron
  shell does (temp LH_DATA_DIR, empty auth) and requires 200s on / +
  graph/models/settings. Caught both defects above on its first day.
- entitlements: allow-unsigned-executable-memory dropped (2019-era
  relic per current electron-builder docs; weakens hardened runtime).
- pi-ai + pi-agent-core pinned exact 1.0.2 — persisted chat records
  carry their wire shapes; post-release a bump is a migration event.

Gates: typecheck, lint, 1459/1459 tests, full desktop:dist + smoke PASS.
2026-10-08 17:35:15 +03:00
amosblomqvist d5c4c3bf9f ui: the chat comeback — side-based authorship, restored quotes, tucked whys
Blockquotes: the callout override had displaced Streamdown's own styled
blockquote component, so plain quotes rendered as bare unstyled text
since that commit. The pass-through branch now restores Streamdown's
verbatim classes (border-l-4, muted italic).

Explanations tucked for ALL questions (owner): the prediction's Why /
Hide-why toggle is universal — every quiz answered card sits its
explanation behind the same collapsed button. Same pattern, one law.

The thinking row's rhythm: my-1 -> my-3. The 4px margin hugged the
options on non-graded answered cards (owner report); 12px matches the
card's mt-2/mt-3 spacing grammar everywhere the row renders.

Side-based authorship (owner round, 2026-10-07): the document shape
blurred user into agent — three left-rail idioms (user text, user
thinking, quoted prose) collided once quotes rendered. User rows go
RIGHT: 'You' messages and practice thinking cards hug the right edge at
85% width with the rail mirrored (border-r), text still reading
left-to-right. In-card thinking (inside the answered card, the agent's
context) stays left. Transcript thinking rows arrive EXPANDED — the
thinking was clearly sent. Cmd+T (toggle-user-thinking, chat scope,
cheat-sheet row) sets every user-thinking row to one state via a module
store: explicit sets stomp individual toggles, late mounts honor the
last set (the state initializer reads the store, so static render
reflects it too).
2026-10-08 02:29:05 +03:00
amosblomqvist cdf9305eea prompts: no size ceilings, the detour keeps the pace, the probe enters at the top
Limits out (owner rulings): the 'Typically 5-15 nodes' cap and the
'keep the map small' pressure are dead — a plan is as elaborate as the
subject needs; what keeps it readable is correct structure (unconditional
truths as the hanging points), not compression. Labels stay short; size
is free. The method section now carries the goal reiteration: thorough,
deep, real understanding — sessions run as long as the understanding
needs, plans as elaborate as the subject requires; no schedule, nothing
to rush. (Code was already limit-free: no maxTurns, no parser caps.)

The adaptation + detour law, merged into one section ('The map moves
with him'): when the ground shifts mid-lesson — a missing foundation, a
deeper confusion — never rush the repair; no wall of text that 'covers'
the ground to get back on plan. Update the map if the structure changed,
then build the missing ground the only way anything is built here: one
reasoning step at a time, verified, at the session's one and only pace.
Detours are the teaching, not interruptions to get through.

The probe enters at the top (owner-observed failure: eigenvectors, then
high-school chain rule, then partials — strands opened at textbook
floors). New entry-point rules: open near the hardest thing the goal
needs and let a miss walk you down; strands inform each other (a strand
crushed at height says neighbors start high); the verdict + confidence +
thinking field TOGETHER set the next question's difficulty — confident
correct jumps sharply, hesitant correct steps gently. Efficiency is
resolution: never descend into already-bracketed territory; the
questions you save buy the specific, nuanced ones that locate the edge.
2026-10-08 01:13:05 +03:00
amosblomqvist 5d9cebc3d5 prose: the key-idea callout — GitHub alerts, one sharp white box; tables hug
The one emphasis device for load-bearing ideas: GitHub-style alert
callouts. The prompt teaches only > [!IMPORTANT] (one device, one
meaning — an unconditional truth laid down, a payoff landed, a hinge
definition) with the restraint law: everything else stays in prose, if
everything is boxed nothing is. The renderer accepts all five GitHub
markers and boxes them identically — a stray [!NOTE] still meant 'this
deserves emphasis', and a marker line must never leak as text.

Rendering (owner rulings): SHARP corners, white stroke, no background,
no label, no icon — just the outline — and the box hugs its text
(width: fit-content capped at the column) instead of stretching
full-width into empty space. Same grammar as the plan blocks: this is
structure, not chrome.

Seam: the Streamdown blockquote override in md-blocks. parseAlertMarker
is pure and exported — Streamdown wraps blocks in its own styled
components, so detection reads the hast node's tagName, and a standalone
marker drops its <br> hard-break artifact along with the empty
paragraph. Plain blockquotes pass through untouched.

Rider (owner, same round): tables hug their contents — width:
max-content on the data-streamdown table (width:auto would still
stretch, the classic table trap); wide ones keep the md-blocks
overflow-x scroller. Graceful in exports: a callout is still a
blockquote there.

6 render pins: the box mounts with white stroke + fit-content + no
rounding, all five markers box, standalone-marker drops the empty
paragraph, multi-paragraph structure kept, plain quotes untouched.
2026-10-08 00:27:30 +03:00
amosblomqvist c34cc029ae prompts: the fact-check bar, the thinking fields, adaptation, LaTeX everywhere
Accuracy (shared, one law across every agent): the target shifts from
surface facts to understanding itself — 'the deeper risk isn't a wrong
name or date, it's a wrong understanding: an interpretation of an idea
that seems solid and isn't.' The bar is the doubt itself: checks are
cheap, and weighing whether a doubt is worth checking is how errors slip
through. Thinking stays the agent's own.

Research (teacher 40-jobs): floor framing — verify your understanding
before it becomes his; dispatch before the fact crosses your lips, teach
around the gap until the brief lands. Planning (10-planning): planning is
the natural moment for verification; the map is a hypothesis and the
probe only its first sketch — rewriting is a normal, common event (the
session adapts to what teaching reveals); and plan_init IS the
presentation — never draw the plan again in prose (the double-plan bug).

New shared segment 16-mental-model.md: the thinking fields are the
session's primary data — read them in full, measure what they reveal,
adapt everything on the model they build. Composes into teacher, prober,
and practice (shared-fragments list updated).

Practice: the rarely-needed framing is dead — fact checks get their own
law (a wrong model solution grades the learner against a lie; pose once
the brief lands), files and map readers become plain side tools.

LaTeX: the question-tools craft list gains the point-of-use bullet
('LaTeX everywhere — plain-text math in a question is a craft failure'),
the formatting law names EVERY field, and the practice problem statement
line carries it too.

Prompt pins updated: felt-doubt bar + mental-model doctrine in both
teacher and practice compositions, no-double-plan rule, rewrite-is-
normal law, rarely-needed framing asserted gone.
2026-10-07 23:56:21 +03:00
amosblomqvist 8dfe621748 researcher: PDFs from the web — the pdf-reader's text tools in the loop (design B)
web_fetch_pdf downloads (25MB cap, probe-validated) into a per-job temp
dir and returns the triage summary — page count, size class (the pdf
doctrine's <=10/<=40 thresholds), flagged math/image-heavy pages. The
researcher's toolset then gains pdf_info / pdf_extract / pdf_search over
that dir, scoped by a resolve-and-startswith wall: the researcher's
filesystem ends at the temp dir. Render is deliberately absent — the
researcher slot has no vision; visual reading stays the pdf job's job.

web_fetch: a PDF URL now returns a POINTER to web_fetch_pdf instead of
dead-ending, and every output carries 'Source: <final url after
redirects> (HTTP <status>)' — the 'ill-formed request' lesson: a
200-served error body must be distinguishable from a real page.

The runner owns the temp lifecycle (mkdtemp before, recursive cleanup in
the job's finally — never enters the record) and web_fetch_pdf urls ride
the sources ledger. Doctrine step 5 added: fetch, triage, extract only
the pages that matter, cite as (p. N).

Tests build a REAL minimal PDF programmatically (correct xref offsets —
pdfjs parses it like any download): zero network, real parse.
2026-10-07 22:36:11 +03:00
amosblomqvist d450689a8b fix(web_fetch): the protocol gate never accepted a URL — the colon bug
URL.protocol carries a trailing colon ("https:", WHATWG); the colon-less
comparison (a direct port from the py harness, where urlparse().scheme
has none) rejected EVERY valid URL ever fetched — the tool never fetched
a page in its life. Owner-reported via OpenRouter logs: 8 valid URLs in,
8 'invalid URL' results out.

The new pin stubs globalThis.fetch so the happy path runs hermetically —
a valid https URL must REACH the fetch stage (and ftp:// still rejected).

dist rebuilt (desktop:dist) — bundle verified: "http:"!==t.protocol
2026-10-07 22:08:29 +03:00
amosblomqvist 34f498b128 artifacts: the height ratchet fixed, invert switch persisted + restyled (release polish 7.2)
- bridge measures the BODY (scrollHeight + rect + margins) instead of the
  viewport-clamped documentElement — documentElement.scrollHeight latches
  at the parent's own height, so demos could grow but never shrink on
  window resize (owner-reported); smoke: 1036 → 2386 → 1036 px
- invert now PERSISTS as details.ui.inverted on the steer record, written
  by the silent artifact-ui route (dismissal pattern: no wake, no SSE) and
  seeded through the projection (SubagentArtifactData.ui); survives
  navigation + reload, never enters teacher context or exports
- invert restyled as the house Toggle switch (settings controls) on both
  cards; demo header is the caption grammar: 'INTERACTIVE DEMO — title'
  left (title normal-case, same size/tracking), switch + download far
  right; figure header: label left, switch/download/open far right

1439 tests green (route + pure-seam pins added); live smoke passed
2026-10-07 19:13:24 +03:00
amosblomqvist 4070cc7926 artifacts: demo scrollbar death, figure natural sizing + fullscreen, downloads, invert toggles (release polish 7)
- demo iframe: scrolling=no (the bridge auto-sizes; a scrollable iframe
  trapped the wheel) + content-box so the bridge height is the viewport
- figures: svgNaturalWidth hug + center (w-fit max-w-full mx-auto),
  full-width fallback for viewBox-only svgs
- fullscreen: MermaidOverlay figure mode (raw svg, white stage, invert
  carried); openFigure context; figureFocus rides the mermaid Esc layer
- downloads: figure.svg + <slug>.html (raw pre-bridge), shared download.ts
- invert: manual ContrastIcon toggle on both cards — invert(1)
  hue-rotate(180deg) on the white-bearing element, default off, no
  detection, no theme wiring (owner rulings 2026-10-07)

1433 tests green; live smoke passed (hug/center measured, filters verified,
downloads fired, Esc closed; wheel-chaining structurally sound)
2026-10-07 13:45:03 +03:00
amosblomqvist a763b9fc5c graph: the sliders re-centered on the new defaults (release polish 6.2)
The owner speaks in display values: "labels until zoom" shows ratio/0.5,
so the old default displayed 8× — and "default one" means display 1× =
ratio 0.5, one stop tighter than the round-1 value. Both ladders are
re-centered so the new defaults sit dead-center again (stop #5):

  labelZoom: [0.125, 0.175, 0.25, 0.35, 0.45, 0.5, 0.75, 1, 1.5, 2, 3]
    — default 0.5 (displays 1×); finer steps below the default, the old
    familiar run above; old stored 4/24 snap to 3.
  blur: [10, 15, 20, 25, 30, 40, 50, 60, 80, 100, 120]
    — default 40 dead-center, min 10 per the owner's suggestion, old
    ceiling kept; old 4/5/7.5 snap to the new floor.

The dead-center ladder law is restored with no exceptions; migration
pins updated (blur 8.1 → 10, labelZoom 0.5 now a member).

1428 tests, tsc, lint, build green.
2026-10-07 12:55:11 +03:00
amosblomqvist 22ab520b5c graph: new default render tuning — blur 40, labels until zoom (release polish 6)
Two defaults moved by owner ruling (2026-10-07): the region blur
doubles 20 → 40 (softer, wider cluster tints) and labelZoom drops 4 → 1
(node labels hidden at the overview, shown once the camera reaches fit
zoom or closer). Stored values are untouched — only the absent-field
fallbacks moved. The dead-center ladder law gains two owner-moved
exceptions (labelZoom index 1, regions.blur index 7); every other
default stays stop #5.

1428 tests, tsc, lint, build green.
2026-10-07 12:46:28 +03:00
amosblomqvist bab0d3f94d copy: the thinking-field placeholders, the brief round (release polish 5)
Four placeholders rewritten per the owner's set (2026-10-07): the
thinking fields are an invitation to stream-of-consciousness transfer,
and everything else is implied by the field's existence — no "the
teacher reads all of this", no mechanism, no em dashes, nothing that
wraps. The reading field's placeholder no longer breaks to a second
line.

  quiz:     "Why this answer? Whatever comes to mind."
  reading:  "Whatever comes to mind as you read."
  practice: "Work here, think out loud."
  pause:    "Ask anything, or press Enter for the next one."

The answer-field placeholders ("Type your answer…") stay: brief and
functional already (owner ruling). Quiz placeholder pin updated.

1428 tests, tsc, lint, build green.
2026-10-07 11:03:54 +03:00
amosblomqvist f979e35f57 plan: the hug calibrated — measured rates, 16px symmetric, white outlines
The first cut's flat 6.8px/char table was calibrated for a generic 12px
sans; the plan titles actually render in Source Sans 3 at 11px — much
narrower — so every box carried ~10px of invisible slack per side (the
owner's "a lot of empty space left and right of the text"). The class
rates are now least-squares-fitted against getComputedTextLength
measurements of the live plan titles (narrow 3.1, wide 9.2, caps+digits
6.0, default 5.4): residuals land within ±4px per line, and PAD_X stays
16 — equal to the vertical (44−11)/2. Smoke-verified: per-side padding
now reads 14.4–19.1px across all 12 nodes (was 22.4–28.6, 10/12 over
25). And per the owner's follow-up: the in-transcript boxes keep their
WHITE outlines (gray card fill good, box edges stay white).

1428 tests, tsc, lint, build green.
2026-10-07 09:40:33 +03:00
amosblomqvist fa61251cfb plan: the hug — text-first boxes, centered titles, dock-colored transcript
The plan skin drops its fixed 160px slabs: every box now sizes to its
wrapped text (px-per-char estimate + symmetric 12px padding, floored at
96px), the wrap limit rises 20→28 chars — the early-wrap fix, boxes
grow so lines stop breaking with room to spare — and every title line
anchors to its box midline (textAnchor middle, was a x=12 left inset).
The in-transcript snapshot block speaks the dock language now: boxes
fill-card stroke-border like the composer shells, text fill-foreground,
retiring the white-on-transparent mono look. The popup and the margin
diagram keep their status colors. Mermaid proper is untouched (owner:
it's fine as is).

Character-count estimation, not canvas measurement: the layout path
stays deterministic, synchronous, DOM-free — SSR and exact render pins
keep working. Verified live on the Relational-databases plan (12 nodes,
widths 174–215 tracking the longest line, no overflow, glow intact).

1428 tests, tsc, lint, build green.
2026-10-07 09:25:14 +03:00
amosblomqvist 8aa1e23cba keyboard: home's Tab targets its OWN composer — the smoke catch (keyboard 3 follow-up)
The smoke found it: Tab from body on home preventDefault'd but never
focused — home does not render the shared Composer component; its
composer is the bespoke topic textarea, and the selector matched
nothing. The chain is now topicInputRef itself.

Also verified live this round: the one-Esc law on plan/chains/settings
(exactly one action, nothing stacked), Enter-submits from the Other
input (a real session filed the custom answer immediately), and
Shift+Tab staying the thinking-level cycle everywhere.

1427 tests, tsc, lint, build green.
2026-10-07 00:32:05 +03:00
amosblomqvist e523bea670 keyboard: the one-Esc law — a surfaced layer claims at the document level (keyboard 4 of 4)
Owner ruling (keyboard round 2026-10-06): Esc (or any key) does ONE
action, on the topmost active layer. The plan-open-while-dictating
double-act was the symptom: the plan popup listened at WINDOW level
with no claim, and same-node window listeners all fire — the global
chain (registered first) cancelled the dictation while the plan's own
listener closed the plan. One Esc, two actions.

The law (now the house pattern, documented at the chain):
- field-level handlers claim at their element (focus in a field: the
  field's action) — unchanged, already correct
- every SURFACED layer claims at the DOCUMENT level with preventDefault
  + stopPropagation — document fires before the window chain, so the
  chain stays silent under any surface
- the window chain is the bottom-up fallback: dictation (a live capture,
  NOT a surface — it survives the Esc that closes a plan or overlay),
  then abort (escAbortTarget layers its own targets)

Converted to the claim: plan popup, session overlay, chain table,
settings dialog, setup dialog, the delete/abort dialogs (window-level
stopPropagation was never enough — same-node listeners all fire), and
the stats popover. Popovers (popover-dismiss), menus, model picker, and
the sidebar context menu already claimed correctly.

1427 tests, tsc, lint, build green.
2026-10-07 00:08:49 +03:00
amosblomqvist 94470815ab keyboard: Tab cycles the surface's text fields, forward-only (keyboard 3 of 4)
Plain Tab now navigates instead of being pure dead weight: the global
handler dispatches lh:kbd-cycle-fields and each composer-bearing
surface answers with its own chain, gathered from live DOM at event
time (the docks mount and unmount — refs would go stale):

- chat: reading-thinking → the active dock's field → the composer,
  visual order, wrap; Tab from anywhere else focuses the first live
  field (navigate in)
- new-session + home: the composer is the whole chain — Tab navigates
  into it

Shift+Tab is untouched (the thinking-level cycle keeps it — owner
ruling); the quiz dock's rows↔thinking Tab claim stops propagation at
document level, so a claimed Tab never reaches the dispatch — no
double act.

1427 tests, tsc, lint, build green.
2026-10-07 00:04:03 +03:00
amosblomqvist 9007e6eb7a keyboard: Enter submits from the custom answer; Enter opens the waiting check (keyboard 1-2 of 4)
Slice 1 — the ask's custom-answer field: Enter submits directly. A
single-line input with no newline to protect, and typing has already
selected the row — every Enter (shift or not) rides the same submit()
seam as the button, dictation finish-first included. No more the
Esc-then-Enter dance to file a custom answer.

Slice 2 — Enter opens the waiting check (owner, keyboard round
2026-10-06): while a question waits with the dock CLOSED (behind its
reveal invitation, or collapsed by scroll) Enter is never dead. Any
live dictation finishes FIRST — the flush lands in the reading-thinking
field, the live claim, per the nothing-gets-lost law — then the check
reveals/expands. Two seams: the reading-thinking field's own Enter
(plain or ⌘/Ctrl; Shift+Enter keeps the newline convention — the dump
convention survives for deliberate breaks, and Enter stays a newline
once the check is OPEN), and a document-level listener for plain-focus
Enter — gated to skip typing surfaces and focused buttons, which own
their own Enter.

1427 tests, tsc, lint, build green.
2026-10-07 00:00:57 +03:00
amosblomqvist 95ba9c8272 compaction: not viewable — the dividers stay, inert (release polish 4)
Owner ruling (release polish round, 2026-10-06): the compaction
summary dialog is gone. The content is model-facing plumbing — anyone
who wants to inspect it reads their provider logs. Both dividers
survive as INERT markers: the moment divider's 'Context compacted'
note and the cut divider's 'HISTORY COMPACTED · 152k tok' seam chip
render as plain text; no button, no dialog, no state. The marker parts
and the server fold are untouched — this is a pure render-side
removal.

Pins: dividers render without <button> or cursor affordances, and the
marker's summary text never reaches the DOM. 1427 tests, tsc, lint,
build green.
2026-10-06 14:18:05 +03:00
amosblomqvist 0b2e4e03fb sessions: export as markdown — the sidebar right-click, the route, the practice join (release polish 3)
Owner round (2026-10-06): markdown export of sessions, actioned from
the sidebar context menu — "Export as Markdown…" sits with the
management actions, above Delete (the destructive action stays last).

- GET /api/chats/:id/markdown (404 unknown): the whole session as one
  attachment — text/markdown; charset=utf-8, filename from the slugified
  title ("relational-databases-joins-normal-forms.md", non-ascii titles
  fall back to "session.md"). Both session kinds export.
- The client is a same-origin anchor download; the route's
  Content-Disposition names the file.
- sessionMarkdown's user-facing debut — minted ANCHORLESS (message-id
  anchors are extraction provenance, not learner-facing). The engine
  joins the core barrel (pure, browser-safe) for its first consumer.

The practice join (owner-approved mid-round): practice tool results are
bare acks — statement, outcome, verdict, and the model solution all
live in the assistant's call arguments. The engine now folds them the
same way the view seam folds cards: Problem blocks render the pose's
statement (solution never leaks at pose time); Concluded blocks render
the outcome as prose ("Solved with hints.") + verdict, then the
pose-committed model solution as a blockquoted reveal via
problemInputForConclude (clean-poses-only, its refusal law). A
dismissed problem concludes as "The learner skipped this problem." —
no reveal. Errored poses render nothing (the failed-tool law).

1427 tests (route pins: headers, slug, anchorless body, 404; engine
pins: pose-time statement, the reveal, the dismissal refusal, the
errored-pose silence); tsc, lint, build green.
2026-10-06 10:23:16 +03:00
amosblomqvist 8a7b6b30d9 minimal home: the buttons carve out of the drag band; own-line summary (release polish 2.3)
Owner report (Electron): the Chains and Import-notes buttons were not
clickable — clicking dragged the window. Root cause: the chrome column
lacked lh-no-drag. The drag mask resolves by DOM order, and the graph
chrome's column carries the carve exactly for this reason (the
2026-09-17 mask law). The minimal home's column now carries the same
class on the same shape — byte-for-byte the graph chrome's law — and
the pointer-events juggling is gone (it was browser-thinking; Electron
needs the region carve, not the pointer filter).

Summary refinement (owner, same round): every count on its OWN line —
no dot separators; the two blocks (propositions/chains, teaching/
practice sessions) part with one empty line (a dedicated h-4 spacer);
imported notes render as a COUNT ("4 imported notes"), never the file
list. KnowledgeSummary's contract changes sources: string[] →
importedNotes: number.

1419 tests (own-line pins: no " · " anywhere, h-4 parting, count-not-
paths, singulars); tsc, lint, build green.
2026-10-06 10:04:26 +03:00
amosblomqvist 0cc108615b graph: the MINIMAL home — the no-graph home, take two (release polish 2.2)
The chains-as-home experiment is retired (owner, 2026-10-06): dragging
the table out of its overlay home caused every mess we saw — checkboxes
by default, the bar/pill collision, the clearance band. The eventual
plan is the DATA VIEW as home; until that is designed and built, the
rendering-off home is deliberately minimal:

- The SAME composer blob home already floats — teach/practice switcher
  and pick pill untouched.
- The graph chrome's survivors in the graph view's OWN places: the
  top-right column — actions row (Chains + Import, the same 36px band
  pills) with the cost pill BELOW it. Import always renders (the
  entry-point law). No node search, no render gear (canvas things).
- A quiet centered summary (KnowledgeSummary, a pure body — pinned by
  the render suite): propositions · chains, teaching · practice session
  counts, the imported-sources list as truncated muted rows; the empty
  knowledge base says so and names the import entry point. Biased up
  off the composer (pb-40), pointer-events-none.
- NO recent sessions (the sidebar owns that, owner ruling).

The chain table returns to overlay-only duty: ChainTableBody's
onClose-optional and topBarExtra experiment hooks are reverted, and
components/chains-home.tsx is deleted. MinimalHome renders the STANDARD
ChainTableOverlay behind its Chains button — the pick flow mirrors
GraphView's contract verbatim (chainSelectOpen raises the overlay in
select mode; Done reports the lifted pick set down).

Smoke-verified live, twice: the summary shows real counts (262 props ·
160 chains, 195 teaching · 17 practice, 4 source paths); overlay
open/close via button + X; pick flow — cluster check → bar "23 nodes →
17 chains · Clear · Done" → uncheck 2 props → Done → chip "21
propositions selected"; practice session starts from the composer;
settings restore returns the canvas home. (First run's "select mode
missing" was a tester artifact: the bar is selection-gated and prop
checkboxes render only when expanded — both confirmed working.)

1417 tests (new KnowledgeSummary pins); tsc, lint, build green.
2026-10-06 09:47:26 +03:00
amosblomqvist 83103062f5 graph: import + cost ride the chains home's TOP BAR (release polish 2.1)
Owner refinement on the chains home: the floating top-right chrome row
sat ON TOP of the propositions table — covering content reads badly.
The two survivors (Import notes — always, the entry-point law; the
cost pill when any run exists) now ride the shared TopBar's right
cluster, left of the search input, carved out of the drag band with
lh-no-drag — consistent with the graph view, where they already live in
the top band.

ChainTableBody gains topBarExtra?: React.ReactNode (absent on the
overlay — nothing changes there); ChainsHome's floating chrome row is
deleted; the empty state keeps its own centered import button.

Parked for the next design pass (owner, this round): (1) the pick flow
reads messy — checkboxes show by default, so the pill's select mode is
near-invisible and after Done the Clear/Delete bar + the practice pill
coexist with two Clear buttons; the original vision was checkboxes
appear ONLY when picking. (2) the composer's clearance band has no
outline — the docked area needs a visual edge.

1415 tests (new pin: topBarExtra renders in the bar with the drag
carve; overlay variant injects nothing); tsc, lint, build green.
2026-10-06 08:58:43 +03:00
amosblomqvist 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.
2026-10-06 08:26:55 +03:00
amosblomqvist 8a6eab0b6a titles: smart truncation — the cut snaps to word boundaries
Owner round (2026-10-06, release polish #3): first messages are always
long, and the auto-title's hard 72-char cut landed mid-word. The cut now
snaps to the last word boundary at or under TITLE_MAX_CHARS (72, exported
and tunable); whitespace collapses first so boundaries are single-space.
The one special case (owner-specified): a single word longer than the cap
— rare but real: identifiers, URLs — splits at the cap. A boundary landing
exactly at the cap is a clean cut. Seam unchanged: titleFromPrompt
(core/contracts/graph.ts), applied once server-side at chat creation.
Pinned in tests/title-from-prompt.test.ts (6).
2026-10-06 07:49:38 +03:00
amosblomqvist e6f52d3ad1 docs: the assessment-levels vision (post-release arc) + the 0.3.0 release list
Vision-assessment records the 2026-10-06 thinking round: propositions as
the unit, forecast semantics (mastery = P(correct unaided retrieval)), the
Beta-with-decay estimator (BKT rejected: binary-only, guess-less, never
forgets, unidentifiable at our volume; the weighted average completed with
prior + recency + uncertainty), the e-table over outcome x rung x missClass,
uniform 1/k credit (optional missed-field upgrade unruled), no new outcome
enum values, PRACTICE-ONLY perimeter (ruled — quiz nodes are session-scoped
plan objects, no honest mapping), mastery computed-on-read over the already
durable problem/conclude calls (no stored ledger, graph untouched).

Release-0.3 captures the owner's polish brain-dump (graph rendering toggle
separate from extraction + open home-view question, question keybinds/VIM,
session naming, markdown + graph exports, graph defaults, compaction dialog
Esc law, keyboard audit) and the schema-lock position with the persistent
surface inventory and pre-release gap list (schemaVersion stamps, pi-ai
pins).
2026-10-06 07:34:51 +03:00
amosblomqvist c79b0d4e6e dock: the practice fold morphs like teaching — every docked variant carries its fold state
Owner report 2026-10-06: practice sessions 'open and close instantly'
while teaching morphs. Live instrumented run (Element.prototype.animate
wrapped, three transitions recorded) proved the pose/conclude morphs
fire identically in practice (54px↔327.5px, 120ms, same easing) — the
asymmetry was the FOLD: the shell's height-morph key read
`mode:${pending ? expanded : "-"}` and pending (the question's tool
call) is null in practice, so a practice dock's bar↔form toggle never
changed the key — no height morph, instant jump, only the 160ms content
fade. The ternary predates practice sessions (aeef28d, August); the
remembered Oct-5 fix (ec8315b) added the fade only. Round 16's pause
flow raised fold exposure until the seam surfaced.

Fix: dockStateKey(mode, docked, expanded) — components/dock-state-key.ts
— widened to !!pending || !!practice (the fold machine's own ownership
rule). Typing growth keeps the key (never animates); the fold rides the
exact branch teaching has used for months. Pinned in
tests/dock-state-key.test.ts.
2026-10-06 05:31:00 +03:00
amosblomqvist f82318c9e4 graph: persist the arg_ implication nodes — the splatter fix
Diagnosed (owner report 2026-10-06, 'splattered from the middle on
every navigation'): the canvas's world is 350 nodes — 262 stored
propositions + 88 derived arg_ implication nodes the chain arrows
bend through — but the drag-commit sanitizer accepted only stored
proposition ids, and writePositions REPLACES the file: every
drag-commit purged the arg_ entries, so every mount re-seeded them
on a ~1306-unit ring (seedMissingNodePositions) — arrows streaking
out of the middle, bbox inflated, fit zoomed out. A drag annealed
them back (impLinks springs), the commit stripped them again.

Fix: the sanitizer's known set = deriveView(getGraph()).nodes —
propositions AND arg_ ids (stable: minted from the chain ledger).
The grew-check becomes the honest test — some CURRENT view node
absent from the payload (a merge landed mid-drag) → exactly one
absorbing settle; full coverage → none (the old proposition-count
comparison broke once args counted).

Live ledger backfilled once: 88 arg_ positions seeded at their
chains' centroids, propositions byte-identical (no settle, no
re-anneal). Pinned in tests/commit-positions.test.ts (hermetic
LH_DATA_DIR): args persist, unknown ids drop, no-settle on full
coverage, absorbing settle on a stale payload, garbage sanitizing.
2026-10-06 04:46:26 +03:00
amosblomqvist cbbe1da825 practice: the metadata box rides a body portal — never clipped again (round 17 fix)
Diagnosed: the transcript's MessageContent boxes carry overflow-hidden
(ai-elements/message.tsx) — the absolutely-positioned box rendered fine
over its own message block's content (the closing-words text under the
conclude card) and clipped the moment it crossed that block's bottom
edge onto the next message. Exactly the reported cut-off.

Fix: the box portals to document.body in fixed position (the dialogs'
law) — placed from the trigger's viewport rect at open time, below the
trigger, left-clamped to the viewport; z-50. Any-scroll-close means
there is nothing to track.
2026-10-06 04:31:48 +03:00