Humuhumu33 commited on
Commit
96ca84e
Β·
verified Β·
1 Parent(s): aea923e

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);