File size: 9,428 Bytes
8494d00 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | // flow.js — RUNG 3 PROTOTYPE: composition as a DYNAMICAL SYSTEM. Instead of
// scoring candidates by hand-laws (beam), the reply is a TRAJECTORY through
// embedding space: from the current state, PREDICT the next point, then SNAP
// to the nearest LEGAL fragment (VQ — the corpus is the codebook, the bound).
//
// The dynamics here are NON-PARAMETRIC (no training yet): the corpus contains
// the entity's REAL trajectories (the embedding sequence of each real reply).
// Predict next = kernel-weighted average of the successors of the corpus states
// most similar to where we are now. "When she was in a state like this, where
// did she go next?" — her own motion through meaning, generalized.
//
// This is the scaffold Rung-3-full replaces the kernel with a trained SSM/
// transformer. Proving predict→snap→bounded works first.
'use strict';
const { seam } = require('./compose');
const { wordsOnly } = require('./fragments');
// gather the entity's REAL trajectories: ordered sentence-fragment indices per
// reply (skip clauses/spans — we want the actual spoken path), then transition
// pairs (predecessor index -> successor index).
function buildTransitions(store) {
const bySrc = new Map();
store.fragments.forEach((f, i) => {
if (f.tier === 1 || f.posTag === 'clause' || f.isSpan) return;
if (!bySrc.has(f.src)) bySrc.set(f.src, []);
bySrc.get(f.src).push(i);
});
const pred = [], succ = [];
for (const seq of bySrc.values()) for (let k = 0; k + 1 < seq.length; k++) { pred.push(seq[k]); succ.push(seq[k + 1]); }
return { pred, succ };
}
// seedable PRNG so creative runs are reproducible for tests but vary in use
function mulberry32(a) { return function () { 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; }; }
function vecOf(emb, i) { const d = emb.d, off = i * d, v = new Float32Array(d); for (let k = 0; k < d; k++) v[k] = emb.vectors[off + k]; return v; }
function cos(emb, i, q) { const d = emb.d, off = i * d; let s = 0; for (let k = 0; k < d; k++) s += emb.vectors[off + k] * q[k]; return s; }
function dot(a, b) { let s = 0; for (let k = 0; k < a.length; k++) s += a[k] * b[k]; return s; }
// TRAINED DYNAMICS (Rung-3-full): load an MLP (from train_flow.py) and do the
// forward pass in JS — out = normalize(x + W2·relu(W1·x + b1) + b2). The kernel
// becomes learned weights; inference stays in-process and fast.
function loadFlowMLP(filePath) {
try { const m = JSON.parse(fs.readFileSync(filePath, 'utf8')); return m; } catch (_) { return null; }
}
function predictNextMLP(mlp, emb, curIdx) {
const d = mlp.d, H = mlp.H, x = new Float32Array(d);
const off = curIdx * d; for (let k = 0; k < d; k++) x[k] = emb.vectors[off + k];
const h = new Float32Array(H);
for (let j = 0; j < H; j++) { let s = mlp.b1[j]; const row = mlp.W1[j]; for (let k = 0; k < d; k++) s += row[k] * x[k]; h[j] = s > 0 ? s : 0; }
const out = new Float32Array(d);
// W2 is [d×H]
for (let i = 0; i < d; i++) { let s = mlp.b2[i]; const row = mlp.W2[i]; for (let j = 0; j < H; j++) s += row[j] * h[j]; out[i] = x[i] + s; }
let n = 0; for (let k = 0; k < d; k++) n += out[k] * out[k]; n = Math.sqrt(n) || 1;
for (let k = 0; k < d; k++) out[k] /= n;
return out;
}
// weighted (diagonal-metric) similarity for learned attention: Σ w_k a_k b_k
function wsim(emb, i, cur, w) { const d = emb.d, off = i * d; let s = 0; for (let k = 0; k < d; k++) s += w[k] * emb.vectors[off + k] * cur[k]; return s; }
// low-rank projection: project a d-vector through P (r×d) → r-vector
function project(P, vec, d, r) { const out = new Float32Array(r); for (let j = 0; j < r; j++) { const row = P[j]; let s = 0; for (let k = 0; k < d; k++) s += row[k] * vec[k]; out[j] = s; } return out; }
// precompute projected predecessor keys (n×r) once per attn — cached on attn obj
function projKeys(emb, trans, attn) {
if (attn._keys) return attn._keys;
const d = emb.d, r = attn.r, n = trans.pred.length;
const keys = new Float32Array(n * r);
for (let t = 0; t < n; t++) { const off = trans.pred[t] * d; for (let j = 0; j < r; j++) { const row = attn.P[j]; let s = 0; for (let k = 0; k < d; k++) s += row[k] * emb.vectors[off + k]; keys[t * r + j] = s; } }
attn._keys = keys; return keys;
}
// predict the next-state embedding via attention over real transitions
// (query=current, keys=predecessors, values=successors). attn={w,tau} uses a
// LEARNED diagonal metric + temperature; else raw cosine kernel. Either way the
// output is a weighted avg of REAL successors — always in the data manifold, so
// it CANNOT collapse (the MLP failure mode).
function predictNext(emb, trans, curIdx, K, attn) {
const cur = vecOf(emb, curIdx);
const d = emb.d;
const scored = [];
if (attn && attn.P) { // low-rank learned attention
const r = attn.r, keys = projKeys(emb, trans, attn), pq = project(attn.P, cur, d, r);
for (let t = 0; t < trans.pred.length; t++) { const off = t * r; let s = 0; for (let j = 0; j < r; j++) s += pq[j] * keys[off + j]; scored.push([t, s]); }
} else if (attn) for (let t = 0; t < trans.pred.length; t++) scored.push([t, wsim(emb, trans.pred[t], cur, attn.w)]);
else for (let t = 0; t < trans.pred.length; t++) scored.push([t, cos(emb, trans.pred[t], cur)]);
scored.sort((a, b) => b[1] - a[1]);
const top = scored.slice(0, K || 40);
const tau = attn ? attn.tau : 8;
const out = new Float32Array(d);
let wsum = 0;
for (const [t, s] of top) { const w = Math.exp((s - top[0][1]) * tau); wsum += w; const off = trans.succ[t] * d; for (let k = 0; k < d; k++) out[k] += w * emb.vectors[off + k]; }
if (wsum > 0) for (let k = 0; k < d; k++) out[k] /= wsum;
let n = 0; for (let k = 0; k < d; k++) n += out[k] * out[k]; n = Math.sqrt(n) || 1;
for (let k = 0; k < d; k++) out[k] /= n;
return out;
}
// greedy flow: anchor by relevance, then at each step predict-next + snap to the
// nearest LEGAL, on-topic, unused fragment. Bounded by construction.
function composeFlow(store, vp, query, opts = {}) {
const { fragments, oracle } = store;
const emb = opts.emb;
const rel = opts.relevance || new Map(); // fragmentIndex -> 0..1 (from recall.js, optional)
const trans = opts._trans || buildTransitions(store);
const target = opts.targetLength || 90;
// CREATIVITY: temperature on the snap. temp=0 → argmax (steady, R28). temp>0 →
// sample among the legal fragments NEAR the predicted next-state. Every
// sample is a real, corpus-legal fragment, so higher temp = more surprising
// BUT NEVER unbounded. Safe wildness — the bound makes randomness harmless.
const temp = opts.temp || 0;
const rng = mulberry32((opts.seed || 1) >>> 0);
const sampleTop = (cands) => {
// cands: [idx, score] sorted desc. temp→0: take best. temp>0: softmax-sample top-N.
if (temp <= 0.001 || cands.length === 1) return cands[0][0];
const N = Math.min(cands.length, 8);
const top = cands.slice(0, N);
const s0 = top[0][1];
const ws = top.map(([, s]) => Math.exp((s - s0) / Math.max(0.05, temp)));
const sum = ws.reduce((a, b) => a + b, 0);
let r = rng() * sum;
for (let k = 0; k < N; k++) { r -= ws[k]; if (r <= 0) return top[k][0]; }
return top[N - 1][0];
};
// anchor: most relevant sentence-initial tier-0 fragment
let anchor = -1, best = -Infinity;
for (let i = 0; i < fragments.length; i++) {
const f = fragments[i];
if (f.tier === 1 || !f.sentenceInitial || f.posTag === 'clause' || f.isSpan) continue;
const r = (rel.get(i) || 0);
if (r > best) { best = r; anchor = i; }
}
if (anchor < 0) anchor = fragments.findIndex(f => f.sentenceInitial && f.tier !== 1);
const chain = [anchor];
const used = new Set([anchor]);
let len = wordsOnly(fragments[anchor].text).length;
for (let step = 0; step < 12 && len < target * 1.25; step++) {
const tail = chain[chain.length - 1];
const tailF = fragments[tail];
const terminal = /[.!?…]["')\]]*$/.test(tailF.text.trim());
if (len >= target * 0.7 && terminal) break;
const eNext = opts.mlp ? predictNextMLP(opts.mlp, emb, tail) : predictNext(emb, trans, tail, opts.K || 40, opts.attn);
// candidate legal successors (seam-legal, tier-0, unused), scored by
// closeness to the predicted next-state + relevance
const cands = [];
for (let i = 0; i < fragments.length; i++) {
if (used.has(i) || fragments[i].tier === 1 || fragments[i].isSpan) continue;
if (!seam(tailF, fragments[i], oracle)) continue;
const flowSim = cos(emb, i, eNext);
cands.push([i, flowSim * 0.7 + (rel.get(i) || 0) * 0.3]);
}
if (!cands.length) break;
cands.sort((a, b) => b[1] - a[1]);
const bestI = sampleTop(cands); // temp=0 → argmax; temp>0 → creative sample
chain.push(bestI); used.add(bestI); len += wordsOnly(fragments[bestI].text).length;
}
const chainF = chain.map(i => fragments[i]);
let out = chainF[0].text;
for (let k = 1; k < chainF.length; k++) { const sm = seam(chainF[k - 1], chainF[k], oracle); out += (sm === 'sent' ? ' ' : ' ') + chainF[k].text; }
return { text: out, fragmentsUsed: chainF.map(f => f.text), words: wordsOnly(out).length, target, method: 'flow' };
}
module.exports = { composeFlow, buildTransitions, loadFlowMLP, predictNextMLP, predictNext };
|