feat(web): SVG sparklines + 3D isometric mood cube (v0.20.4)

Import the updated Claude Design prototype's SVG sparkline system + 3D graph,
replacing the unicode-char sparklines:

- per-PAD-fader vertical SVG strips (stripPoints, 26x132 beside each bar — time
  down Y newest-at-bottom, value on X, gradient-faded, dot at newest); also
  answers the earlier "next to each meter" ask
- relation-row horizontal SVG sparklines (sparkPointsH, 56x13, auto-scaled,
  gradient + end dot) — fixed-width, so the old unicode overflow onto the n
  column can't recur
- mood-orbit reworked from a 2D P×A scatter into a 3D ISOMETRIC P×A×D cube
  (proj3: P right-down / A left-down / D up, 2:1 iso, scale 26, reverse-derived
  from the design's now-point + verified) with the trajectory, a pulsing
  now-marker, and a drop line + floor-shadow ellipse for depth
- gradients in one hidden <defs> svg; removed the orphaned sparkline()/_SPARK

Contract amended. Verified: pytest tests/test_web_* (84) + node Playwright
(injected 24-sample history — 3 PAD strips + 4 relation sparklines + the 3D
cube trajectory/drop/floor all render; gradients resolve).
This commit is contained in:
2026-07-07 00:29:18 -07:00
parent cc6e85cd9b
commit 459e7fa602
5 changed files with 115 additions and 46 deletions
+2 -2
View File
@@ -125,11 +125,11 @@ functions:
flexibility: "open"
- name: "renderConsole + trend (v0.20.0 — unified persona/affect console; supersedes renderAffectPane/renderPersonaPane/setPersonaStrip)"
signature: "renderConsole(snap) ; setFader(axis,v) ; setFaderTrend(axis) ; renderOrbit() ; renderDominant(snap) ; renderDerived(snap) ; renderRelations(snap) ; renderDirective(snap) ; pushAffectHistory(snap) ; sparkline(vals) ; trendDelta(vals)"
signature: "renderConsole(snap) ; setFader(axis,v) ; setFaderTrend(axis) ; renderOrbit() ; renderDominant(snap) ; renderDerived(snap) ; renderRelations(snap) ; renderDirective(snap) ; pushAffectHistory(snap) ; sparkPointsH(vals,w,h,endX) ; stripPoints(vals) ; proj3(p,a,d) ; trendDelta(vals)"
description: "ONE render path for BOTH the Tier-1 persona_state snapshot and the Tier-3 affect snapshot (renderConsole), feeding the right affect console: dominant/canonical-mood centerpiece, bipolar PAD faders (each with a turn-to-turn Δ + sparkline), a P×A mood orbit from PAD history, an affect-derived grid, relations metric rows, and the canonical directive. Replaces the v0.19.x split of renderPersonaPane (Tier-1 pane) + renderAffectPane (Tier-3 pane) + setPersonaStrip (top-bar strip, removed — PAD now lives in the console faders)."
postconditions:
- "POST-001: reads snap.relations (relation_edge/1: target_entity + trust_ability/benevolence/integrity + warmth as {value,confidence,evidence_count} + agency + relation_context) — the CURRENT Worldtree emit shape; falls back to the legacy flat snap.valence for an older emitter. Tier-1 fields (baseline_pad, mood_drift, dominant_emotion, emotions_active) render WHEN PRESENT, '—' when absent (Tier-3 lacks them)."
- "POST-002: each PAD fader + relation metric shows current value + Δ-vs-previous (▲/▼) + a unicode sparkline auto-scaled to its OWN observed range (flat ▄/— when sub-0.01 stable — no noise amplification), drawn from AFFECT_HIST (rolling, HIST_CAP=24, session-lived). setFaderTrend fills the per-meter Δ+spark slots; renderOrbit plots the last N (P,A) samples as a scaled trail with a pulsing current marker."
- "POST-002: SVG sparklines (v0.20.4, adapted from the design prototype; replace the unicode strings). Each PAD fader shows current value + Δ-vs-previous (▲/▼) + a VERTICAL SVG strip (`stripPoints`, 26×132, time down Y newest-at-bottom, value on X ±11, gradient-faded via stripFade{P,A,D}, dot at newest). Each relation metric shows a HORIZONTAL SVG sparkline (`sparkPointsH`, 56×13, auto-scaled to its OWN range, sparkFade gradient + end dot). renderOrbit is now a 3D ISOMETRIC P×A×D cube (`proj3`: P right-down / A left-down / D up, 2:1 iso, center 62,66, scale 26) plotting the trajectory + a pulsing now-marker + a drop line to the D=1 floor + a floor shadow ellipse (depth cue). All drawn from AFFECT_HIST (rolling, HIST_CAP=24, session-lived); sparkline coords are computed numerics (no upstream strings → INV-004 trivially held). Gradients live in one hidden `<defs>` svg in the console."
- "POST-003: pushAffectHistory dedupes by emitted_at||last_updated_at so the ~4x/turn post-turn PAD poll contributes ONE sample/turn; history is CLIENT-side only (lost on reload — durable cross-session history via a provider-side snapshot log is a deferred follow-up, NOT built here)."
- "POST-004: INV-001 honesty — no fabricated Tier-1 fields. The dominant-emotion centerpiece shows a real OCC dominant_emotion (Tier-1) OR the CANONICAL mood word from canonMood(pad) (Tier-3, dimmed) OR '—'; NEVER a synthesized emotion. The affect-derived grid drops non-emitted metrics (intensity/decay-τ) and shows only real/client-derived cells (baseline/drift real for Tier-1, client-derived samples/volatility). INV-004 — every dynamic value passes through esc(); numerics go through toFixed, never innerHTML-raw."
flexibility: "open"