mirror of
https://github.com/samvallad33/vestige.git
synced 2026-04-25 00:36:22 +02:00
Sam asked: "Build EVERY SINGLE MISSING UI PIECE." 10 parallel agents shipped
10 new viewports over the existing Rust backend, then 11 audit agents
line-by-line reviewed each one, extracted pure-logic helpers, fixed ~30
bugs, and shipped 549 new unit tests. Everything wired through the layout
with single-key shortcuts and a live theme toggle.
**Eight new routes**
- `/reasoning` — Reasoning Theater: Cmd+K ask palette → animated 8-stage
deep_reference pipeline + FSRS-trust-scored evidence cards +
contradiction arcs rendered as live SVG between evidence nodes
- `/duplicates` — threshold-driven cluster detector with winner selection,
Merge/Review/Dismiss actions, debounced slider
- `/dreams` — Dream Cinema: trigger dream + scrubbable 5-stage replay
(Replay → Cross-reference → Strengthen → Prune → Transfer) + insight
cards with novelty glow
- `/schedule` — FSRS Review Calendar: 6×7 grid with urgency color
bands (overdue/today/week/future), retention sparkline, expand-day list
- `/importance` — 4-channel radar (Novelty/Arousal/Reward/Attention) with
composite score + top-important list
- `/activation` — live spreading-activation view: search → SVG concentric
rings with decay animation + live-mode event feed
- `/contradictions` — 2D cosmic constellation of conflicting memories,
arcs colored by severity, tooltips with previews
- `/patterns` — cross-project pattern transfer heatmap with category
filters, top-transferred sidebar
**Three layout additions**
- `AmbientAwarenessStrip` — slim top band with retention vitals, at-risk
count, active intentions, recent dream, activity sparkline, dreaming
indicator, Sanhedrin-watch flash. Pure `$derived` over existing stores.
- `ThemeToggle` — dark/light/auto cycle with matchMedia listener,
localStorage persistence, SSR-safe, reduced-motion-aware. Rendered in
sidebar footer next to the connection dot.
- `MemoryAuditTrail` — per-memory Sources panel integrated as a
Content/Audit tab into the existing /memories expansion.
**Pure-logic helper modules extracted (for testability + reuse)**
reasoning-helpers, duplicates-helpers, dream-helpers, schedule-helpers,
audit-trail-helpers, awareness-helpers, contradiction-helpers,
activation-helpers, patterns-helpers, importance-helpers.
**Bugs fixed during audit (not exhaustive)**
- Trust-color inconsistency between EvidenceCard and the page confidence
ring (0.75 boundary split emerald vs amber)
- `new Date('garbage').toLocaleDateString()` returned literal "Invalid Date"
in 3 components — all now return em-dash or raw string
- NaN propagation in `Math.max(0, Math.min(1, NaN))` across clamps
- Off-by-one PRNG in audit-trail seeded mock (seed === UINT32_MAX yielded
rand() === 1.0 → index out of bounds)
- Duplicates dismissals keyed by array index broke on re-fetch; now keyed
by sorted cluster member IDs with stale-dismissal pruning
- Empty-cluster crash in DuplicateCluster.pickWinner
- Undefined tags crash in DuplicateCluster.safeTags
- Debounce timer leak in threshold slider (missing onDestroy cleanup)
- Schedule day-vs-hour granularity mismatch between calendar cell and
sidebar list ("today" in one, "in 1d" in the other)
- Schedule 500-memory hard cap silently truncated; bumped to 2000 + banner
- Schedule DST boundary bug in daysBetween (wall-clock math vs
startOfDay-normalized)
- Dream stage clamp now handles NaN/Infinity/floats
- Dream double-click debounce via `if (dreaming) return`
- Theme setTheme runtime validation; initTheme idempotence (listener +
style-element dedup on repeat calls)
- ContradictionArcs node radius unclamped (trust < 0 or > 1 rendered
invalid sizes); tooltip position clamp (could push off-canvas)
- ContradictionArcs $state closure capture (width/height weren't reactive
in the derived layout block)
- Activation route was MISSING from the repo — audit agent created it
with identity-based event filtering and proper RAF cleanup
- Layout: ThemeToggle was imported but never rendered — now in sidebar
footer; sidebar overflow-y-auto added for the 16-entry nav
**Tests — 549 new, 910 total passing (0 failures)**
ReasoningChain 42 | EvidenceCard 50
DuplicateCluster 64 | DreamStageReplay 19
DreamInsightCard 43 | FSRSCalendar 32
MemoryAuditTrail 45 | AmbientAwareness 60
theme (store) 31 | ContradictionArcs 43
ActivationNetwork 54 | PatternTransfer 31
ImportanceRadar 35 | + existing 361 tests still green
**Gates passed**
- `npm run check`: 0 errors, 0 warnings across 623 files
- `npm test`: 910/910 passing, 22 test files
- `npm run build`: clean adapter-static output
**Layout wiring**
- Nav array expanded 8 → 16 entries (existing 8 + 8 new routes)
- Single-key shortcuts added: R/A/D/C/P/U/X/N (no conflicts with
existing G/M/T/F/E/I/S/,)
- Cmd+K palette search works across all 16
- Mobile nav = top 5 (Graph, Reasoning, Memories, Timeline, Feed)
- AmbientAwarenessStrip mounted as first child of <main>
- ThemeToggle rendered in sidebar footer (was imported-but-unmounted)
- Theme initTheme() + teardown wired into onMount cleanup chain
Net branch delta: 47 files changed, +13,756 insertions, -6 deletions
293 lines
8.6 KiB
TypeScript
293 lines
8.6 KiB
TypeScript
/**
|
|
* Pure helpers for MemoryAuditTrail.
|
|
*
|
|
* Extracted for isolated unit testing in a Node (vitest) environment —
|
|
* no DOM, no Svelte runtime, no fetch. Every function in this module is
|
|
* deterministic given its inputs.
|
|
*/
|
|
|
|
export type AuditAction =
|
|
| 'created'
|
|
| 'accessed'
|
|
| 'promoted'
|
|
| 'demoted'
|
|
| 'edited'
|
|
| 'suppressed'
|
|
| 'dreamed'
|
|
| 'reconsolidated';
|
|
|
|
export interface AuditEvent {
|
|
action: AuditAction;
|
|
timestamp: string; // ISO
|
|
old_value?: number;
|
|
new_value?: number;
|
|
reason?: string;
|
|
triggered_by?: string;
|
|
}
|
|
|
|
export type MarkerKind =
|
|
| 'dot'
|
|
| 'arrow-up'
|
|
| 'arrow-down'
|
|
| 'pencil'
|
|
| 'x'
|
|
| 'star'
|
|
| 'circle-arrow'
|
|
| 'ring';
|
|
|
|
export interface Meta {
|
|
label: string;
|
|
color: string; // hex for dot + glow
|
|
glyph: string; // optional inline symbol
|
|
kind: MarkerKind;
|
|
}
|
|
|
|
/**
|
|
* Event type → visual metadata. Each action maps to a UNIQUE marker `kind`
|
|
* so the 8 event types are visually distinguishable without relying on the
|
|
* colour palette alone (accessibility).
|
|
*/
|
|
export const META: Record<AuditAction, Meta> = {
|
|
created: { label: 'Created', color: '#10b981', glyph: '', kind: 'ring' },
|
|
accessed: { label: 'Accessed', color: '#3b82f6', glyph: '', kind: 'dot' },
|
|
promoted: { label: 'Promoted', color: '#10b981', glyph: '', kind: 'arrow-up' },
|
|
demoted: { label: 'Demoted', color: '#f59e0b', glyph: '', kind: 'arrow-down' },
|
|
edited: { label: 'Edited', color: '#facc15', glyph: '', kind: 'pencil' },
|
|
suppressed: { label: 'Suppressed', color: '#a855f7', glyph: '', kind: 'x' },
|
|
dreamed: { label: 'Dreamed', color: '#c084fc', glyph: '', kind: 'star' },
|
|
reconsolidated: { label: 'Reconsolidated', color: '#ec4899', glyph: '', kind: 'circle-arrow' }
|
|
};
|
|
|
|
export const VISIBLE_LIMIT = 15;
|
|
|
|
/**
|
|
* All 8 `AuditAction` values, in the canonical order. Used both by the
|
|
* event generator (`actionPool`) and by tests that verify uniqueness of
|
|
* the marker mapping.
|
|
*/
|
|
export const ALL_ACTIONS: readonly AuditAction[] = [
|
|
'created',
|
|
'accessed',
|
|
'promoted',
|
|
'demoted',
|
|
'edited',
|
|
'suppressed',
|
|
'dreamed',
|
|
'reconsolidated'
|
|
] as const;
|
|
|
|
/**
|
|
* Hash a string id into a 32-bit unsigned seed. Stable across runs.
|
|
*/
|
|
export function hashSeed(id: string): number {
|
|
let seed = 0;
|
|
for (let i = 0; i < id.length; i++) seed = (seed * 31 + id.charCodeAt(i)) >>> 0;
|
|
return seed;
|
|
}
|
|
|
|
/**
|
|
* Linear congruential PRNG bound to a mutable seed. Returns a function
|
|
* that yields floats in `[0, 1)` — critically, NEVER 1.0, so callers
|
|
* can safely use `Math.floor(rand() * arr.length)` without off-by-one.
|
|
*/
|
|
export function makeRand(initialSeed: number): () => number {
|
|
let seed = initialSeed >>> 0;
|
|
return () => {
|
|
seed = (seed * 1664525 + 1013904223) >>> 0;
|
|
// Divide by 2^32, not 2^32 - 1 — the latter can yield exactly 1.0
|
|
// when seed is UINT32_MAX, breaking array-index math.
|
|
return seed / 0x100000000;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Deterministic mock audit-trail generator. Same `memoryId` + `nowMs`
|
|
* ALWAYS yields the same event sequence (critical for snapshot stability
|
|
* and for tests). An empty `memoryId` yields no events — the audit trail
|
|
* panel should never invent history for a non-existent memory.
|
|
*
|
|
* `countOverride` lets tests force a specific number of events (e.g.
|
|
* to cross the 15-event visibility threshold, which the default range
|
|
* 8-15 cannot do).
|
|
*/
|
|
export function generateMockAuditTrail(
|
|
memoryId: string,
|
|
nowMs: number = Date.now(),
|
|
countOverride?: number
|
|
): AuditEvent[] {
|
|
if (!memoryId) return [];
|
|
|
|
const rand = makeRand(hashSeed(memoryId));
|
|
const count = countOverride ?? 8 + Math.floor(rand() * 8); // default 8-15 events
|
|
if (count <= 0) return [];
|
|
|
|
const out: AuditEvent[] = [];
|
|
|
|
const createdAt = nowMs - (14 + rand() * 21) * 86_400_000; // 14-35 days ago
|
|
out.push({
|
|
action: 'created',
|
|
timestamp: new Date(createdAt).toISOString(),
|
|
reason: 'smart_ingest · prediction-error gate opened',
|
|
triggered_by: 'smart_ingest'
|
|
});
|
|
|
|
let t = createdAt;
|
|
let retention = 0.5 + rand() * 0.2;
|
|
const actionPool: AuditAction[] = [
|
|
'accessed',
|
|
'accessed',
|
|
'accessed',
|
|
'accessed',
|
|
'promoted',
|
|
'demoted',
|
|
'edited',
|
|
'dreamed',
|
|
'reconsolidated',
|
|
'suppressed'
|
|
];
|
|
|
|
for (let i = 1; i < count; i++) {
|
|
t += rand() * 5 * 86_400_000 + 3_600_000; // 1h-5d between events
|
|
const action = actionPool[Math.floor(rand() * actionPool.length)];
|
|
const ev: AuditEvent = { action, timestamp: new Date(t).toISOString() };
|
|
|
|
switch (action) {
|
|
case 'accessed': {
|
|
const old = retention;
|
|
retention = Math.min(1, retention + rand() * 0.04 + 0.01);
|
|
ev.old_value = old;
|
|
ev.new_value = retention;
|
|
ev.triggered_by = rand() > 0.5 ? 'search' : 'deep_reference';
|
|
break;
|
|
}
|
|
case 'promoted': {
|
|
const old = retention;
|
|
retention = Math.min(1, retention + 0.1);
|
|
ev.old_value = old;
|
|
ev.new_value = retention;
|
|
ev.reason = 'confirmed helpful by user';
|
|
ev.triggered_by = 'memory(action=promote)';
|
|
break;
|
|
}
|
|
case 'demoted': {
|
|
const old = retention;
|
|
retention = Math.max(0, retention - 0.15);
|
|
ev.old_value = old;
|
|
ev.new_value = retention;
|
|
ev.reason = 'user flagged as outdated';
|
|
ev.triggered_by = 'memory(action=demote)';
|
|
break;
|
|
}
|
|
case 'edited': {
|
|
ev.reason = 'content refined, FSRS state preserved';
|
|
ev.triggered_by = 'memory(action=edit)';
|
|
break;
|
|
}
|
|
case 'suppressed': {
|
|
const old = retention;
|
|
retention = Math.max(0, retention - 0.08);
|
|
ev.old_value = old;
|
|
ev.new_value = retention;
|
|
ev.reason = 'top-down inhibition (Anderson 2025)';
|
|
ev.triggered_by = 'suppress(dashboard)';
|
|
break;
|
|
}
|
|
case 'dreamed': {
|
|
const old = retention;
|
|
retention = Math.min(1, retention + 0.05);
|
|
ev.old_value = old;
|
|
ev.new_value = retention;
|
|
ev.reason = 'replayed during dream consolidation';
|
|
ev.triggered_by = 'dream()';
|
|
break;
|
|
}
|
|
case 'reconsolidated': {
|
|
ev.reason = 'edited within 5-min labile window (Nader)';
|
|
ev.triggered_by = 'reconsolidation-manager';
|
|
break;
|
|
}
|
|
case 'created':
|
|
// Created is only emitted once, as the first event. If the pool
|
|
// ever yields it again, treat it as a no-op access marker with
|
|
// no retention change — defensive, not expected.
|
|
ev.triggered_by = 'smart_ingest';
|
|
break;
|
|
}
|
|
|
|
out.push(ev);
|
|
}
|
|
|
|
// Newest first for display.
|
|
return out.reverse();
|
|
}
|
|
|
|
/**
|
|
* Humanised relative time. Uses supplied `nowMs` for deterministic tests;
|
|
* defaults to `Date.now()` in production.
|
|
*
|
|
* Boundaries (strictly `<`, so 60s flips to "1m", 60m flips to "1h", etc.):
|
|
* <60s → "Ns ago"
|
|
* <60m → "Nm ago"
|
|
* <24h → "Nh ago"
|
|
* <30d → "Nd ago"
|
|
* <12mo → "Nmo ago"
|
|
* else → "Ny ago"
|
|
*
|
|
* Future timestamps (nowMs < then) clamp to "0s ago" rather than returning
|
|
* a negative string — the audit trail is a past-only view.
|
|
*/
|
|
export function relativeTime(iso: string, nowMs: number = Date.now()): string {
|
|
const then = new Date(iso).getTime();
|
|
const diff = Math.max(0, nowMs - then);
|
|
const s = Math.floor(diff / 1000);
|
|
if (s < 60) return `${s}s ago`;
|
|
const m = Math.floor(s / 60);
|
|
if (m < 60) return `${m}m ago`;
|
|
const h = Math.floor(m / 60);
|
|
if (h < 24) return `${h}h ago`;
|
|
const d = Math.floor(h / 24);
|
|
if (d < 30) return `${d}d ago`;
|
|
const mo = Math.floor(d / 30);
|
|
if (mo < 12) return `${mo}mo ago`;
|
|
const y = Math.floor(mo / 12);
|
|
return `${y}y ago`;
|
|
}
|
|
|
|
/**
|
|
* Retention delta formatter. Behaviour:
|
|
* (undef, undef) → null — no retention movement on this event
|
|
* (undef, 0.72) → "set 0.72" — initial value, no prior state
|
|
* (0.50, undef) → "was 0.50" — retention cleared (rare)
|
|
* (0.50, 0.72) → "0.50 → 0.72"
|
|
*
|
|
* The `retention ` prefix is left to the caller so tests can compare the
|
|
* core formatted value precisely.
|
|
*/
|
|
export function formatRetentionDelta(
|
|
oldValue: number | undefined,
|
|
newValue: number | undefined
|
|
): string | null {
|
|
const hasOld = typeof oldValue === 'number' && Number.isFinite(oldValue);
|
|
const hasNew = typeof newValue === 'number' && Number.isFinite(newValue);
|
|
if (!hasOld && !hasNew) return null;
|
|
if (!hasOld && hasNew) return `set ${newValue!.toFixed(2)}`;
|
|
if (hasOld && !hasNew) return `was ${oldValue!.toFixed(2)}`;
|
|
return `${oldValue!.toFixed(2)} → ${newValue!.toFixed(2)}`;
|
|
}
|
|
|
|
/**
|
|
* Split an event list into (visible, hiddenCount) per the 15-event cap.
|
|
* Exactly 15 events → no toggle (hiddenCount = 0). 16+ → toggle.
|
|
*/
|
|
export function splitVisible(
|
|
events: AuditEvent[],
|
|
showAll: boolean
|
|
): { visible: AuditEvent[]; hiddenCount: number } {
|
|
if (showAll || events.length <= VISIBLE_LIMIT) {
|
|
return { visible: events, hiddenCount: Math.max(0, events.length - VISIBLE_LIMIT) };
|
|
}
|
|
return {
|
|
visible: events.slice(0, VISIBLE_LIMIT),
|
|
hiddenCount: events.length - VISIBLE_LIMIT
|
|
};
|
|
}
|