holo-evicted-publish: player +3 object(s)
Browse files
b/9bbb7ace4be8185e95239e7c58dc63cf66f811531a2aa67c42dd8ec6ca0326e9
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-flow.mjs β the FLOW PLAN sequencer (F2 of HOLO-FLOW-PROMPT.md). PURE and deterministic:
|
| 2 |
+
// (cards, continue, scores, hour, seed, mood, history) β the next 3 items + why β no DOM, no fetch,
|
| 3 |
+
// no Date.now(). The LLM/voice are garnish ABOVE this; with no brain, no GPU, no network, the channel
|
| 4 |
+
// flows identically. The plan is a ΞΊ-object (JCS+sha256 sealed) β inspectable, private, tamper-refusing.
|
| 5 |
+
// All taste lives in ONE weights table below: tunable, witnessable.
|
| 6 |
+
|
| 7 |
+
import { jcs, sha256hex } from "./holo-media-card.mjs";
|
| 8 |
+
|
| 9 |
+
export const WEIGHTS = {
|
| 10 |
+
resume: 100, // finish what you started β resume-first is the strongest pull
|
| 11 |
+
tastePerPlay: 6, // learned score (bumpPlay/bumpSkip) via scoreOf
|
| 12 |
+
hourPrior: 30, // time-of-day fit (table below)
|
| 13 |
+
rhythmPenalty: 60, // same kind as the previous item β variety unless bingeing
|
| 14 |
+
bingeOverride: 3, // β₯3 consecutive finishes of one kind = bingeing β rhythm penalty off
|
| 15 |
+
historyPenalty: 1000, // already played this session β effectively never within a session
|
| 16 |
+
skipKindPenalty: 25, // a kind vetoed this session appears less, immediately
|
| 17 |
+
wildcard: 40, // one discovery slot: boost the least-seen candidate
|
| 18 |
+
mood: { // explicit steering β additive nudges, not filters (never a dead end)
|
| 19 |
+
calmer: { audiobook: 45, film: 20, game: -45, live: -25 },
|
| 20 |
+
short: { game: 45, live: 25, film: -30, audiobook: -10 },
|
| 21 |
+
surprise:{ _unseen: 60 },
|
| 22 |
+
},
|
| 23 |
+
};
|
| 24 |
+
export const HOUR_PRIOR = (h) =>
|
| 25 |
+
h < 9 ? { live: 1, audiobook: .7, game: .1, film: .1 } :
|
| 26 |
+
h < 17 ? { audiobook: .8, game: .6, live: .5, film: .3 } :
|
| 27 |
+
h < 23 ? { film: 1, audiobook: .5, game: .4, live: .3 } :
|
| 28 |
+
{ audiobook: 1, film: .5, game: .2, live: .1 };
|
| 29 |
+
|
| 30 |
+
const mulberry32 = (a) => () => { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
|
| 31 |
+
|
| 32 |
+
// plan({cards, continueList, scoreOf, hour, seed, mood, history, prevKind, skippedKinds, bingeKind})
|
| 33 |
+
// cards: [{id, kind, title, ...}] β the whole eligible pool (from the ΞΊ-index)
|
| 34 |
+
// β { items: [cardΓβ€3], why: [stringΓ], seedUsed }
|
| 35 |
+
export function plan(inp) {
|
| 36 |
+
const { cards = [], continueList = [], scoreOf = () => 0, hour = 20, seed = 1, mood = null,
|
| 37 |
+
history = [], prevKind = null, skippedKinds = [], bingeKind = null } = inp;
|
| 38 |
+
const rng = mulberry32(seed);
|
| 39 |
+
const played = new Set(history);
|
| 40 |
+
const contById = new Map(continueList.map((r) => [r.id, r]));
|
| 41 |
+
const seen = new Set(Object.keys(inp.statsAll || {}));
|
| 42 |
+
const prior = HOUR_PRIOR(hour);
|
| 43 |
+
const moodW = (mood && WEIGHTS.mood[mood]) || null;
|
| 44 |
+
|
| 45 |
+
const items = [], why = [];
|
| 46 |
+
let lastKind = prevKind;
|
| 47 |
+
for (let slot = 0; slot < 3; slot++) {
|
| 48 |
+
let best = null, bestScore = -Infinity, bestWhy = "";
|
| 49 |
+
for (const c of cards) {
|
| 50 |
+
if (!c || !c.id || items.includes(c)) continue;
|
| 51 |
+
let s = rng() * 4; // seeded jitter: ties break deterministically
|
| 52 |
+
const reasons = [];
|
| 53 |
+
if (played.has(c.id)) s -= WEIGHTS.historyPenalty;
|
| 54 |
+
const cont = contById.get(c.id);
|
| 55 |
+
if (cont) { s += WEIGHTS.resume; reasons.push("you're " + (cont.dur ? Math.round(100 * cont.pos / cont.dur) + "% in" : "mid-way")); }
|
| 56 |
+
const taste = scoreOf(c.id); if (taste) { s += taste * WEIGHTS.tastePerPlay; if (taste > 0) reasons.push("you come back to this"); }
|
| 57 |
+
s += (prior[c.kind] || 0) * WEIGHTS.hourPrior; if ((prior[c.kind] || 0) >= 0.8) reasons.push("fits the hour");
|
| 58 |
+
if (c.kind === lastKind && bingeKind !== c.kind) s -= WEIGHTS.rhythmPenalty;
|
| 59 |
+
if (skippedKinds.includes(c.kind)) s -= WEIGHTS.skipKindPenalty;
|
| 60 |
+
if (moodW) { s += moodW[c.kind] || 0; if (moodW._unseen && !seen.has(c.id)) s += moodW._unseen; if ((moodW[c.kind] || 0) > 0) reasons.push("matches β" + mood + "β"); }
|
| 61 |
+
if (slot === 2 && !seen.has(c.id) && !played.has(c.id)) { s += WEIGHTS.wildcard; reasons.push("something new"); }
|
| 62 |
+
if (s > bestScore) { bestScore = s; best = c; bestWhy = reasons.join(" Β· ") || "in your library"; }
|
| 63 |
+
}
|
| 64 |
+
if (!best || bestScore <= -WEIGHTS.historyPenalty / 2) break; // pool exhausted this session β stop honestly
|
| 65 |
+
items.push(best); why.push(bestWhy); lastKind = best.kind;
|
| 66 |
+
}
|
| 67 |
+
return { items, why, seedUsed: seed };
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// seal / verify a plan as a ΞΊ-object β same discipline as every card in the hub
|
| 71 |
+
export async function sealPlan(p) {
|
| 72 |
+
const body = { v: 1, kind: "flow-plan", items: p.items.map((c) => c.id), why: p.why, seed: p.seedUsed };
|
| 73 |
+
const hex = await sha256hex(new TextEncoder().encode(jcs(body)));
|
| 74 |
+
return { ...body, kappa: "sha256:" + hex };
|
| 75 |
+
}
|
| 76 |
+
export async function verifyPlan(sealed) {
|
| 77 |
+
const { kappa, ...body } = sealed;
|
| 78 |
+
const hex = await sha256hex(new TextEncoder().encode(jcs(body)));
|
| 79 |
+
return "sha256:" + hex === kappa ? body : null;
|
| 80 |
+
}
|
b/c8fe5a6a74b6aaeada74254a8d622893345ca5cebbbd563538f1e429151d0429
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// holo-continue.mjs β ONE continue store + taste stats (F1 of HOLO-FLOW-PROMPT.md; absorbs K5-P1/P4).
|
| 2 |
+
//
|
| 3 |
+
// Three players used to remember separately (film β localStorage "holoplayer.resume.<id>", book β
|
| 4 |
+
// "holo.book.<ΞΊ>", game β IndexedDB holo-games/saves). This is the ONE ΞΊ-sealed record store they all
|
| 5 |
+
// write, back-filled from the legacy keys at read time so history appears with no migration. Records are
|
| 6 |
+
// JCS-sealed; a tampered record is dropped (named on console), the list lives. Device-local by design β
|
| 7 |
+
// personalization without surveillance. Pure ESM, browser+Node (inject `ls`/`idb` for the witness).
|
| 8 |
+
|
| 9 |
+
import { jcs, sha256hex } from "./holo-media-card.mjs";
|
| 10 |
+
|
| 11 |
+
const K_CONT = "holo.continue.v1", K_STATS = "holo.stats.v1", CAP = 40;
|
| 12 |
+
const enc = new TextEncoder();
|
| 13 |
+
const sealOf = async (r) => { const { seal, ...body } = r; return sha256hex(enc.encode(jcs(body))); };
|
| 14 |
+
|
| 15 |
+
export function makeContinue({ ls, idbFactory } = {}) {
|
| 16 |
+
const LS = ls || (typeof localStorage !== "undefined" ? localStorage : new Map() && { getItem: () => null, setItem: () => {}, key: () => null, length: 0 });
|
| 17 |
+
const load = (k) => { try { return JSON.parse(LS.getItem(k) || "null"); } catch { return null; } };
|
| 18 |
+
const save = (k, v) => { try { LS.setItem(k, JSON.stringify(v)); } catch {} };
|
| 19 |
+
|
| 20 |
+
return {
|
| 21 |
+
// record progress for any kind β call at the same sites the legacy stores are written
|
| 22 |
+
async record({ kind, id, title, pos = 0, dur = 0, extra = null }) {
|
| 23 |
+
if (!kind || !id) return;
|
| 24 |
+
const map = load(K_CONT) || {};
|
| 25 |
+
const r = { kind, id, title: title || "", pos, dur, extra, ts: Date.now() };
|
| 26 |
+
r.seal = await sealOf(r);
|
| 27 |
+
map[id] = r;
|
| 28 |
+
const keys = Object.keys(map).sort((a, b) => map[b].ts - map[a].ts);
|
| 29 |
+
for (const k of keys.slice(CAP)) delete map[k];
|
| 30 |
+
save(K_CONT, map);
|
| 31 |
+
},
|
| 32 |
+
// newest-first, seal-verified (tampered β dropped loudly), legacy stores folded in
|
| 33 |
+
async list() {
|
| 34 |
+
const out = new Map();
|
| 35 |
+
const map = load(K_CONT) || {};
|
| 36 |
+
for (const r of Object.values(map)) {
|
| 37 |
+
if (r.seal && (await sealOf(r)) !== r.seal) { try { console.warn("[continue] record refused (seal):", r.id); } catch {} continue; }
|
| 38 |
+
out.set(r.id, r);
|
| 39 |
+
}
|
| 40 |
+
// back-fill: films (holoplayer.resume.*) and books (holo.book.*) from localStorage
|
| 41 |
+
try {
|
| 42 |
+
for (let i = 0; i < LS.length; i++) {
|
| 43 |
+
const k = LS.key(i);
|
| 44 |
+
if (/^holoplayer\.resume\./.test(k)) { const v = load(k); const id = k.slice("holoplayer.resume.".length); if (v && v.pos && !out.has(id)) out.set(id, { kind: "film", id, title: v.title || "", pos: v.pos, dur: v.dur || 0, ts: v.ts || 0 }); }
|
| 45 |
+
else if (/^holo\.book\./.test(k)) { const v = load(k); const id = k.slice(10); if (v && !out.has("book:" + id)) out.set("book:" + id, { kind: "audiobook", id: "book:" + id, kappa: id, title: "", pos: v.t || 0, extra: { chapter: v.i || 0 }, ts: v.ts || 0 }); }
|
| 46 |
+
}
|
| 47 |
+
} catch {}
|
| 48 |
+
// back-fill: game ΞΊ-saves (same-origin IndexedDB holo-games/saves), latest per game
|
| 49 |
+
try {
|
| 50 |
+
const req = (idbFactory || (typeof indexedDB !== "undefined" ? indexedDB : null));
|
| 51 |
+
if (req) {
|
| 52 |
+
const db = await new Promise((res) => { const r = req.open("holo-games", 3); r.onsuccess = () => res(r.result); r.onerror = () => res(null); r.onupgradeneeded = () => { try { r.transaction.abort(); } catch {} res(null); }; });
|
| 53 |
+
if (db && db.objectStoreNames.contains("saves")) {
|
| 54 |
+
const rows = await new Promise((res) => { const o = []; const c = db.transaction("saves", "readonly").objectStore("saves").openCursor(); c.onsuccess = () => { const cur = c.result; if (cur) { o.push(cur.value); cur.continue(); } else res(o); }; c.onerror = () => res(o); });
|
| 55 |
+
const latest = new Map();
|
| 56 |
+
for (const s of rows) { const cur = latest.get(s.kappa); if (!cur || s.ts > cur.ts) latest.set(s.kappa, s); }
|
| 57 |
+
for (const s of latest.values()) if (!out.has("game:" + s.kappa)) out.set("game:" + s.kappa, { kind: "game", id: "game:" + s.kappa, kappa: s.kappa, title: s.title || "", pos: 0, ts: s.ts || 0, extra: { system: s.system || "" } });
|
| 58 |
+
}
|
| 59 |
+
try { db && db.close(); } catch {}
|
| 60 |
+
}
|
| 61 |
+
} catch {}
|
| 62 |
+
return [...out.values()].sort((a, b) => (b.ts || 0) - (a.ts || 0));
|
| 63 |
+
},
|
| 64 |
+
// taste stats β every ΞΊ-play bumps; scores order rows and feed the flow sequencer
|
| 65 |
+
bumpPlay(id, kind) { const s = load(K_STATS) || {}; const r = s[id] || { n: 0, kind }; r.n++; r.kind = kind || r.kind; r.last = Date.now(); s[id] = r; save(K_STATS, s); },
|
| 66 |
+
bumpSkip(id, kind) { const s = load(K_STATS) || {}; const r = s[id] || { n: 0, kind }; r.skips = (r.skips || 0) + 1; r.kind = kind || r.kind; r.last = Date.now(); s[id] = r; save(K_STATS, s); },
|
| 67 |
+
scoreOf(id) { const s = load(K_STATS) || {}; const r = s[id]; return r ? r.n * 2 - (r.skips || 0) * 3 : 0; },
|
| 68 |
+
kindScore(kind) { const s = load(K_STATS) || {}; let n = 0; for (const r of Object.values(s)) if (r.kind === kind) n += r.n * 2 - (r.skips || 0) * 3; return n; },
|
| 69 |
+
stats() { return load(K_STATS) || {}; },
|
| 70 |
+
};
|
| 71 |
+
}
|
b/d79ee97c9de8ee50c5e5578c333bf79a7ca8458d863b9f3ea44a8db10d8e1ab6
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env node
|
| 2 |
+
// holo-flow-witness.mjs β Gate F2 (HOLO-FLOW-PROMPT.md). The sequencer is PURE: prove determinism,
|
| 3 |
+
// resume-first, kind-rhythm, mood steering, session dedupe, and plan-ΞΊ tamper refusal β 1,000 seeded runs.
|
| 4 |
+
|
| 5 |
+
import { plan, sealPlan, verifyPlan } from "./holo-flow.mjs";
|
| 6 |
+
|
| 7 |
+
const R = { pass: 0, fail: 0 };
|
| 8 |
+
const check = (n, ok, note = "") => { R[ok ? "pass" : "fail"]++; console.log(`${ok ? "β" : "β"} ${n}${note ? " β " + note : ""}`); };
|
| 9 |
+
|
| 10 |
+
// synthetic pool: 4 kinds Γ 6 cards
|
| 11 |
+
const KINDS = ["film", "game", "live", "audiobook"];
|
| 12 |
+
const cards = KINDS.flatMap((k) => Array.from({ length: 6 }, (_, i) => ({ id: k + ":" + i, kind: k, title: k + " " + i })));
|
| 13 |
+
const base = { cards, hour: 20, seed: 42, history: [], scoreOf: () => 0 };
|
| 14 |
+
|
| 15 |
+
// 1. determinism β same inputs β byte-identical plan
|
| 16 |
+
{
|
| 17 |
+
const a = plan(base), b = plan(base);
|
| 18 |
+
check("deterministic", JSON.stringify(a.items.map((c) => c.id)) === JSON.stringify(b.items.map((c) => c.id)), a.items.map((c) => c.id).join(" β "));
|
| 19 |
+
}
|
| 20 |
+
// 2. resume-first β a continue-record leads the plan
|
| 21 |
+
{
|
| 22 |
+
const p = plan({ ...base, continueList: [{ id: "audiobook:3", pos: 100, dur: 400 }] });
|
| 23 |
+
check("resume-first", p.items[0] && p.items[0].id === "audiobook:3", "why: " + p.why[0]);
|
| 24 |
+
}
|
| 25 |
+
// 3. kind-rhythm β across 1,000 seeds, never three identical kinds in one plan (no binge signal)
|
| 26 |
+
{
|
| 27 |
+
let bad = 0;
|
| 28 |
+
for (let s = 1; s <= 1000; s++) { const p = plan({ ...base, seed: s }); const k = p.items.map((c) => c.kind); if (k.length === 3 && k[0] === k[1] && k[1] === k[2]) bad++; }
|
| 29 |
+
check("kind-rhythm (1000 seeds)", bad === 0, bad + " violations");
|
| 30 |
+
}
|
| 31 |
+
// 4. binge override β 3 finishes of one kind β repeats allowed
|
| 32 |
+
{
|
| 33 |
+
const p = plan({ ...base, bingeKind: "film", prevKind: "film", continueList: [{ id: "film:1" }], scoreOf: (id) => (id.startsWith("film") ? 8 : 0) });
|
| 34 |
+
check("binge override", p.items.filter((c) => c.kind === "film").length >= 2, p.items.map((c) => c.kind).join(","));
|
| 35 |
+
}
|
| 36 |
+
// 5. mood steers β "calmer" removes games from the front of the plan; plans differ
|
| 37 |
+
{
|
| 38 |
+
const a = plan({ ...base, seed: 7 }), b = plan({ ...base, seed: 7, mood: "calmer" });
|
| 39 |
+
const changed = JSON.stringify(a.items.map((c) => c.id)) !== JSON.stringify(b.items.map((c) => c.id));
|
| 40 |
+
check("mood re-plans", changed && b.items[0].kind !== "game", b.items.map((c) => c.kind).join(","));
|
| 41 |
+
}
|
| 42 |
+
// 6. session dedupe β played ids never reappear; exhausted pool stops honestly
|
| 43 |
+
{
|
| 44 |
+
const hist = cards.slice(0, 21).map((c) => c.id);
|
| 45 |
+
const p = plan({ ...base, history: hist });
|
| 46 |
+
check("session dedupe", p.items.every((c) => !hist.includes(c.id)), p.items.length + " fresh items from a nearly-drained pool");
|
| 47 |
+
}
|
| 48 |
+
// 7. plan is a ΞΊ-object β seal verifies; one flipped byte refuses
|
| 49 |
+
{
|
| 50 |
+
const sealed = await sealPlan(plan(base));
|
| 51 |
+
check("plan seals + verifies", !!(await verifyPlan(sealed)), sealed.kappa.slice(0, 20) + "β¦");
|
| 52 |
+
const tampered = { ...sealed, why: [...sealed.why] }; tampered.why[0] = (tampered.why[0] || "") + "!";
|
| 53 |
+
check("tampered plan REFUSED", (await verifyPlan(tampered)) === null);
|
| 54 |
+
}
|
| 55 |
+
// 8. hour priors β morning plans lead with live/book, evening with film (aggregate over seeds)
|
| 56 |
+
{
|
| 57 |
+
let am = 0, pm = 0;
|
| 58 |
+
for (let s = 1; s <= 200; s++) {
|
| 59 |
+
if (["live", "audiobook"].includes(plan({ ...base, seed: s, hour: 7 }).items[0].kind)) am++;
|
| 60 |
+
if (plan({ ...base, seed: s, hour: 20 }).items[0].kind === "film") pm++;
|
| 61 |
+
}
|
| 62 |
+
check("hour priors", am > 150 && pm > 150, `7am live/book ${am}/200 Β· 8pm film ${pm}/200`);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
console.log(`\nF2 ${R.fail === 0 ? "GREEN" : "RED"} β ${R.pass}/${R.pass + R.fail}`);
|
| 66 |
+
process.exit(R.fail === 0 ? 0 : 1);
|