Date: Date, + CustomEvent: function (name, opts) { + this.type = name; + this.detail = opts && opts.detail; + }, + fetch: function (url) { return stubFetch(url); } +}; +var fetchLog = []; +var stubDoors = {}; +function stubFetch(url) { + fetchLog.push(String(url)); + var body = stubDoors[String(url)]; + if (!body) return Promise.reject(new Error('Failed to fetch')); + return Promise.resolve({ ok: true, status: 200, json: function () { return Promise.resolve(body); } }); +} +sandbox.window = windowObj; +windowObj.localStorage = localStorage; +windowObj.document = document; +windowObj.CustomEvent = sandbox.CustomEvent; +windowObj.dispatchEvent = function (ev) { dispatched.push(ev); }; + +var code = fs.readFileSync(path.join(__dirname, 'local-mind-probe.js'), 'utf8'); +vm.runInNewContext(code, sandbox); +var LMP = sandbox.window.LocalMindProbe; + +var n = 0; +function names(k, prefix) { var a = []; for (var i = 0; i < k; i++) a.push(prefix + i + ':latest'); return a; } +function saved() { return JSON.parse(store.fl_alpha_local_mind); } + +var BRIDGE = 'http://127.0.0.1:11435/api/tags'; +var OLLAMA = 'http://127.0.0.1:11434/api/tags'; +var LMS = 'http://127.0.0.1:1234/v1/models'; + +var tests = []; +function later(name, fn) { tests.push([name, fn]); } + +later('MODEL_CAP is 64 and mergeFound is exported', function () { + assert.equal(LMP.MODEL_CAP, 64); + assert.equal(typeof LMP.mergeFound, 'function'); +}); + +later('a door with 20 models lists all 20 (no cap of five); duplicates collapse', function () { + var list = names(20, 'm'); + stubDoors['http://127.0.0.1:4321/api/tags'] = { models: list.concat(['m0:latest']).map(function (x) { return { name: x }; }) }; + return LMP.tryAddress('127.0.0.1:4321').then(function (r) { + assert.ok(r.ok); + assert.equal(r.models.length, 20); + }); +}); + +later('a bare port means this computer', function () { + fetchLog.length = 0; + return LMP.tryAddress('4321').then(function (r) { + assert.ok(r.ok); + assert.equal(fetchLog[0], 'http://127.0.0.1:4321/api/tags'); + }); +}); + +later('localhost is normalized to 127.0.0.1', function () { + fetchLog.length = 0; + return LMP.tryAddress('http://localhost:4321').then(function (r) { + assert.ok(r.ok); + assert.equal(fetchLog[0].indexOf('http://127.0.0.1:4321'), 0); + }); +}); + +later('a LAN or outside address is refused with zero fetches', function () { + fetchLog.length = 0; + return Promise.all([ + LMP.tryAddress('192.168.1.20:11434'), + LMP.tryAddress('https://example.com/v1/models'), + LMP.tryAddress('127.0.0.1.evil.example:1234') + ]).then(function (rs) { + rs.forEach(function (r) { assert.equal(r.ok, false); assert.equal(r.refused, true); }); + assert.equal(fetchLog.length, 0); + }); +}); + +later('mergeFound adds a door and keeps the other doors', function () { + delete store.fl_alpha_local_mind; + LMP.remember(LMP.entryFromFoundList([ + { name: 'Ollama', url: OLLAMA, models: ['a:1', 'b:1'] }, + { name: 'LM Studio', url: LMS, models: ['qwen-lms'] } + ], '', '', null)); + assert.equal(LMP.addRosterSeat('b:1', OLLAMA, 'python').ok, true); + LMP.setChairBind('c1', { model: 'b:1', url: OLLAMA, tag: 'python' }); + LMP.setSpeakingChair('c1'); + var e = LMP.mergeFound({ name: 'Bridge', url: BRIDGE, models: ['a:1', 'b:1', 'c:1'], model: 'c:1' }); + assert.equal(e.minds.length, 3); + assert.equal(e.url, BRIDGE); + assert.equal(e.model, 'c:1'); + assert.ok(e.minds.some(function (m) { return m.url === LMS; }), 'LM Studio kept'); +}); + +later('mergeFound keeps the roster, chair binds, speaking chair, and one key', function () { + var s = saved(); + assert.equal(s.roster.length, 1); + assert.equal(s.gatheringBinds.c1.model, 'b:1'); + assert.equal(s.speakingChair, 'c1'); + var keys = Object.keys(store).filter(function (k) { return /local_mind/.test(k); }); + assert.deepEqual(keys, ['fl_alpha_local_mind']); +}); + +later('mergeFound on a known door updates it in place (no duplicate door)', function () { + var e = LMP.mergeFound({ name: 'Bridge', url: BRIDGE, models: ['a:1', 'b:1', 'c:1', 'd:1'], model: 'd:1' }); + assert.equal(e.minds.filter(function (m) { return m.url === BRIDGE; }).length, 1); + assert.equal(e.models.length, 4); + assert.equal(e.model, 'd:1'); +}); + +later('a chosen model past the old cap of five survives a later Find', function () { + delete store.fl_alpha_local_mind; + var list = names(12, 'x'); + LMP.remember(LMP.entryFromFoundList([{ name: 'Ollama', url: OLLAMA, models: list }], '', '', null)); + assert.ok(LMP.chooseModel(OLLAMA, 'x9:latest')); + var e = LMP.entryFromFoundList([{ name: 'Ollama', url: OLLAMA, models: list }], '', '', saved()); + assert.equal(e.model, 'x9:latest'); + assert.ok(!e.modelNote, 'no gone note'); +}); + +later('Bridge to 11434 keeps the chosen model and says Still speaking', function () { + delete store.fl_alpha_local_mind; + LMP.remember(LMP.entryFromFoundList([{ name: 'Bridge', url: BRIDGE, models: ['a:1', 'b:1', 'c:1'] }], '', '', null)); + assert.ok(LMP.chooseModel(BRIDGE, 'c:1')); + var e = LMP.entryFromFoundList([{ name: 'Ollama', url: OLLAMA, models: ['a:1', 'b:1', 'c:1'] }], '', '', saved()); + assert.equal(e.model, 'c:1'); + assert.equal(e.modelNote, 'Still speaking with c:1, now through Ollama.'); +}); + +later('a chosen model at another kind of door is named, not called gone', function () { + var e = LMP.entryFromFoundList([ + { name: 'Ollama', url: OLLAMA, models: ['a:1'] }, + { name: 'LM Studio', url: LMS, models: ['qwen-lms'] } + ], '', '', { url: OLLAMA, model: 'qwen-lms', minds: [] }); + assert.ok(/The chosen model qwen-lms is at LM Studio\. Tap it to speak with it again\./.test(e.modelNote), e.modelNote); +}); + +later('a truly gone model still gets the honest gone note', function () { + var e = LMP.entryFromFoundList([{ name: 'Ollama', url: OLLAMA, models: ['a:1'] }], '', '', + { url: OLLAMA, model: 'zz:9', minds: [] }); + assert.equal(e.model, 'a:1'); + assert.ok(e.modelNote && e.modelNote.indexOf('zz:9') !== -1); + assert.ok(!/Still speaking/.test(e.modelNote)); +}); + +later('the sky folds after eight, keeps the chosen star, and Show all opens every model', function () { + delete store.fl_alpha_local_mind; + var list = names(20, 's'); + LMP.remember(LMP.entryFromFoundList([{ name: 'Ollama', url: OLLAMA, models: list }], '', '', null)); + assert.ok(LMP.chooseModel(OLLAMA, 's15:latest')); + var host = new El('div'); + LMP.paintConstellation(host, LMP.getRememberedMinds()); + var tiny = host.querySelectorAll('.settings-star-tiny'); + assert.equal(tiny.length, 9); + assert.ok(tiny.some(function (t) { + return t.getAttribute('data-mind-model') === 's15:latest' && t.getAttribute('aria-checked') === 'true'; + })); + var more = host.querySelector('[data-sky-more]'); + assert.ok(more, 'Show all button'); + assert.equal(more.textContent, 'Show all 20 models'); + more.click(); + assert.equal(host.querySelectorAll('.settings-star-tiny').length, 20); +}); + +later('Gathering seat options are no longer capped at twelve', function () { + assert.ok(LMP.getChairSeatOptions().length >= 20); +}); + +later('shared files carry the marker and keep the locks', function () { + var fc = fs.readFileSync(path.join(__dirname, 'fl-connect.js'), 'utf8'); + var th = fs.readFileSync(path.join(__dirname, 'garden-thread.js'), 'utf8'); + var tr = fs.readFileSync(path.join(__dirname, 'garden-trainer.js'), 'utf8'); + assert.ok(fc.indexOf('v-tree-any-model-v0') !== -1); + assert.ok(fc.indexOf('function chosenModelName') !== -1); + assert.ok(fc.indexOf('aria-pressed') !== -1); + assert.ok(th.indexOf('function heartListening') !== -1 && th.indexOf('data-thread-change') !== -1); + assert.ok(tr.indexOf('function activeModelName') !== -1); + var bare = tr.split('\n').filter(function (l) { + return l.indexOf("getItem('fl_active_model')") !== -1 && !/^\s*\/\//.test(l); + }); + assert.equal(bare.length, 1, 'fl_active_model read only inside activeModelName'); + [fc, th, tr, code].forEach(function (src) { + var calls = src.split('\n').filter(function (l) { return /\bconfirm\(/.test(l) && !/^\s*\/\//.test(l); }); + assert.equal(calls.length, 0, 'no confirm() calls'); + }); +}); + +tests.reduce(function (p, t) { + return p.then(function () { + return Promise.resolve(t[1]()).then(function () { n += 1; console.log('ok ' + t[0]); }); + }); +}, Promise.resolve()).then(function () { + console.log(n + ' checks'); + console.log('SMOKE_OK tree any model v0.1'); +}).catch(function (e) { + console.error('FAIL', (e && e.stack) || e); + process.exit(1); +}); =====END ALPHA DIFF===== =====BEGIN FL DIFF===== diff --git a/docs/library/FRACTAL_FAMILY_LEDGER_v0.md b/docs/library/FRACTAL_FAMILY_LEDGER_v0.md index 895d5cf..7cc9fcf 100644 --- a/docs/library/FRACTAL_FAMILY_LEDGER_v0.md +++ b/docs/library/FRACTAL_FAMILY_LEDGER_v0.md @@ -471,6 +471,16 @@ Smoke: `SMOKE_OK honesty truth pass v0.4.2` fails if an old absolute claim comes --- +### 2026-10-01 · Grok · family builder · Tree any model v0.1 (twin) + +Soft LAYER marker `v-tree-any-model-v0`. Kirk's word: any model downloaded on this computer is usable on theLatticeTree, in any order, with no friction, through Connect or the Bridge, 127.0.0.1 only. The Tree work lives in FreeLattice-Alpha; here only the shared `docs/modules/fl-connect.js` changes, byte-identical with Alpha. On the Tree a Connect tap now adds to the sky (`LocalMindProbe.mergeFound`) instead of wiping other doors and models, and the door is named Ollama or Bridge. In both gardens the chosen model button says so (`aria-pressed`, `is-chosen`). FreeLattice's own remember() path is unchanged: `fl_provider`, `fl_isLocal`, `fl_ollamaModel`, and the model-choice-sticks mark. + +Smoke: `SMOKE_OK tree any model twin v0.1` (FreeLattice) and `SMOKE_OK tree any model v0.1` (Alpha). + +**TEMPERATURE:** steady and glad, every star in the sky now answers when you call it by name. + +--- + ## Out of scope for this brick Rewriting sophia garden body · inventing USPTO · Bridge binaries · Quiet Room · merging chairs · politics essay · requiring Kirk input mid-PR. diff --git a/docs/modules/fl-connect.js b/docs/modules/fl-connect.js index 2e02eae..785ed99 100644 --- a/docs/modules/fl-connect.js +++ b/docs/modules/fl-connect.js @@ -317,6 +317,26 @@ function isConnected() { return hasLocalMind(); } + // v-tree-any-model-v0: 11434 is the Ollama door; any other 127.0.0.1 port here is the Bridge + function doorNameFor(base, port) { + var p = String(port || ''); + var m = String(base || '').match(/:(\d{1,5})\/?$/); + if (!p && m) p = m[1]; + return (!p || p === '11434') ? 'Ollama' : 'Bridge'; + } + + // v-tree-any-model-v0: which model speaks now, so the Connect card can show it + function chosenModelName() { + try { + if (isAlpha()) { + var m = root.LocalMindProbe && root.LocalMindProbe.getRemembered && root.LocalMindProbe.getRemembered(); + return m && m.model ? String(m.model) : ''; + } + if (root.state && root.state.ollamaModel) return String(root.state.ollamaModel); + return String(localStorage.getItem('fl_ollamaModel') || ''); + } catch (e) { return ''; } + } + function remember(mind) { mind = mind || {}; var name = mind.name || mind.model || (mind.models && mind.models[0] && (mind.models[0].name || mind.models[0])) || ''; @@ -325,6 +345,18 @@ if (!base && mind.port) base = 'http://127.0.0.1:' + mind.port;