futura-scion 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/bin/scion.js +147 -4
- package/knowledge/lexicon.de.yaml +24 -0
- package/knowledge/lexicon.ja.yaml +25 -0
- package/knowledge/lexicon.tr.yaml +24 -0
- package/package.json +1 -1
- package/src/brain/db.js +81 -1
- package/src/brain/embedding.js +120 -0
- package/src/brain/forgetting.js +222 -0
- package/src/brain/memory.js +84 -6
- package/src/http.js +54 -0
- package/src/kernel/maintain.js +22 -2
- package/src/kernel/trail.js +37 -4
- package/src/mind/brief.js +385 -0
- package/src/mind/claims.js +348 -0
- package/src/mind/completeness.js +23 -4
- package/src/mind/langs.js +270 -0
- package/src/mind/nlu.js +96 -2
- package/src/mind/tools.js +1 -1
- package/src/ui/app.js +42 -0
- package/src/ui/index.html +15 -0
package/README.md
CHANGED
|
@@ -53,6 +53,63 @@ scion run "fix var in src/legacy.js" --intent fix # fix + gate-verify + certi
|
|
|
53
53
|
scion brain --all # fleet-brain memory inventory
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## The brief organ — context in, verified contract out
|
|
57
|
+
|
|
58
|
+
FS's rungs act on commands, but real work arrives as CONTEXT — tickets,
|
|
59
|
+
specs, conversations. The brief organ (`scion brief`, `docs/brief.md`)
|
|
60
|
+
ingests a context document and produces a structured problem model:
|
|
61
|
+
entities, business rules (`MUST`/`never` sentences), acceptance criteria
|
|
62
|
+
(Gherkin `Given/When/Then`, bullets). Each requirement is classified
|
|
63
|
+
**checkable** (compiles to a Gate verifier command — the same oracle that
|
|
64
|
+
judges code judges the business rules) or **judgment** (kept visible for
|
|
65
|
+
the clarify/review loop, never dropped). A failing requirement **vetoes
|
|
66
|
+
completeness**, so FS cannot declare work done while the contract is
|
|
67
|
+
broken.
|
|
68
|
+
|
|
69
|
+
## Temporal memory: forgetting, supersession, semantic recall (cortex lineage)
|
|
70
|
+
|
|
71
|
+
The brain never grows stale or noisy — two deterministic organs (ported from
|
|
72
|
+
agentic-cortex, LLM-free throughout) keep it sharp:
|
|
73
|
+
|
|
74
|
+
- **Temporal expiry** — a memory whose text carries a deadline (`2026-12-25`,
|
|
75
|
+
`in 3 days`, `tomorrow`, or an explicit `ttlDays`) gets that lifespan on
|
|
76
|
+
save; `scion brain --sweep` soft-deletes everything whose time has passed.
|
|
77
|
+
- **Supersession** — saving a statement-like fact with a near-identical title
|
|
78
|
+
(`deploy is Monday` over `deploy is Friday`) marks the old fact
|
|
79
|
+
`superseded_by` the new one and excludes it from retrieval. Gate-verified
|
|
80
|
+
memories are append-only evidence and never auto-supersede.
|
|
81
|
+
- **Semantic recall** — every memory carries a deterministic 512-dim
|
|
82
|
+
hashed-feature vector (no model, no download, byte-identical on every
|
|
83
|
+
machine). Hybrid search blends FTS keyword rank with vector similarity;
|
|
84
|
+
when FTS cannot match at all (morphological near-misses like `pools
|
|
85
|
+
exhausting` vs `pool exhaustion`), retrieval falls back to vector-only.
|
|
86
|
+
`scion brain` reports the forgetting ledger (`superseded` / `expired`).
|
|
87
|
+
|
|
88
|
+
## FS Desktop (the UI)
|
|
89
|
+
|
|
90
|
+
The web UI ships inside the npm package — one command after install:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
scion ui # boots the kernel + opens FS Desktop in the browser
|
|
94
|
+
# or: scion serve, then open http://127.0.0.1:5107/ui
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Windows installer (two-tier)
|
|
98
|
+
|
|
99
|
+
A native Windows app is also built from this repo:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
cd desktop && npm install && npm run dist # → dist-desktop/FS-Desktop-Setup-<ver>.exe
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The shell is a thin Electron host (~78 MB installer) that **detects the npm kernel at
|
|
106
|
+
launch**: if `futura-scion` is installed globally (or `SCION_BIN` is set) it spawns
|
|
107
|
+
`scion serve` itself and opens the UI window; otherwise it shows a guided setup screen
|
|
108
|
+
with the one-line install command and a Retry button. The kernel always runs under the
|
|
109
|
+
user's own Node — updates flow through `npm update -g futura-scion` without
|
|
110
|
+
re-installing the shell. Silent install: `FS-Desktop-Setup-<ver>.exe /S`.
|
|
111
|
+
|
|
112
|
+
|
|
56
113
|
`better-sqlite3` ships prebuilt binaries for common platforms (Linux/macOS/Windows,
|
|
57
114
|
x64/arm64); exotic platforms need build tools. FS keeps its brain per project
|
|
58
115
|
(`.scion/`), reads config from `config/scion.config.yaml` (or shipped defaults),
|
package/bin/scion.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
import { createInterface } from 'node:readline';
|
|
19
|
-
import { existsSync } from 'node:fs';
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { resolve } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
21
22
|
import { spawnSync } from 'node:child_process';
|
|
22
23
|
import * as trail from '../src/kernel/trail.js';
|
|
23
24
|
import {
|
|
@@ -91,7 +92,7 @@ async function oneShot(text, opts = {}, depth = 0) {
|
|
|
91
92
|
// persisted scopes (brain config). interpret() strips the @tokens itself.
|
|
92
93
|
const projectScopes = opts.noProjectScopes ? [] : getProjectScopes();
|
|
93
94
|
const mergedScopes = resolveScopes(text, { flag: opts.scope ?? null, projectScopes });
|
|
94
|
-
|
|
95
|
+
let interp = interpret(text, { existsSync: (p) => existsSync(p), intentOverride: opts.intentOverride, scopes: mergedScopes.length ? mergedScopes : undefined });
|
|
95
96
|
trail.journal('nlu.interpret', {
|
|
96
97
|
utterance: String(text).slice(0, 200),
|
|
97
98
|
intent: interp.intent,
|
|
@@ -103,6 +104,26 @@ async function oneShot(text, opts = {}, depth = 0) {
|
|
|
103
104
|
override: opts.intentOverride ?? null,
|
|
104
105
|
});
|
|
105
106
|
|
|
107
|
+
// Tier 3 — the System-1 language-parse rung: ONLY when the deterministic
|
|
108
|
+
// interpreter found no evidence at all, and an llm provider is configured
|
|
109
|
+
// AND budgeted (daily_tokens > 0 — off by default). The model parses the
|
|
110
|
+
// utterance into intent JSON; the SAME schema check validates it; an
|
|
111
|
+
// abstention falls through to the clarify loop. Never a guess.
|
|
112
|
+
if (interp.intentEvidence.length === 0 && interp.confidence === 0 && !interp.question && !opts.intentOverride) {
|
|
113
|
+
const cfg = loadConfig();
|
|
114
|
+
if (cfg.llm?.daily_tokens > 0 && cfg.llm?.baseUrl && cfg.llm?.model) {
|
|
115
|
+
const { llmInterpret } = await import('../src/mind/nlu.js');
|
|
116
|
+
const parsed = await llmInterpret(text, { llmCfg: cfg.llm });
|
|
117
|
+
trail.journal('nlu.llm-parse', {
|
|
118
|
+
utterance: String(text).slice(0, 200),
|
|
119
|
+
accepted: !!parsed,
|
|
120
|
+
intent: parsed?.intent ?? null,
|
|
121
|
+
confidence: parsed?.confidence ?? null,
|
|
122
|
+
});
|
|
123
|
+
if (parsed) interp = parsed;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
// Route pure service intents to their kernel surface — a question about
|
|
107
128
|
// state should not become a queue task.
|
|
108
129
|
if (interp.intent === 'show') {
|
|
@@ -159,7 +180,7 @@ async function oneShot(text, opts = {}, depth = 0) {
|
|
|
159
180
|
// Unresolved answer: fall through and surface the question.
|
|
160
181
|
}
|
|
161
182
|
}
|
|
162
|
-
return { ok: false, intent: 'clarify', prompt: cl.prompt, options: cl.options, open: cl.open, gap_id: gap.id };
|
|
183
|
+
return { ok: false, intent: 'clarify', prompt: cl.prompt, options: cl.options, open: cl.open, gap_id: gap.id, lang_notice: interp.lang_notice ?? null };
|
|
163
184
|
}
|
|
164
185
|
|
|
165
186
|
// Ordinary task — enriched with extracted slots.
|
|
@@ -311,17 +332,130 @@ switch (cmd || '') {
|
|
|
311
332
|
const db = getDb();
|
|
312
333
|
const scope = restArgs[0] === '--all' ? 'all'
|
|
313
334
|
: restArgs[0] === '--project' ? resolve(restArgs[1] || '.').replace(/\\/g, '/') : null;
|
|
335
|
+
// Temporal forgetting (cortex lineage): --sweep expires every memory
|
|
336
|
+
// whose lifespan has passed; --sweep --dry-run previews it.
|
|
337
|
+
if (arg === '--sweep' || restArgs.includes('--sweep')) {
|
|
338
|
+
const { sweepExpires } = await import('../src/brain/forgetting.js');
|
|
339
|
+
console.log(JSON.stringify(sweepExpires({ dryRun: arg === '--dry-run' || restArgs.includes('--dry-run') }), null, 2));
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
314
342
|
const perProject = db.prepare(
|
|
315
343
|
'SELECT project, COUNT(*) as memories, SUM(is_active) as active FROM memories GROUP BY project ORDER BY memories DESC'
|
|
316
344
|
).all();
|
|
345
|
+
const forgetting = db.prepare(
|
|
346
|
+
`SELECT SUM(CASE WHEN is_active = 0 AND superseded_by IS NOT NULL THEN 1 ELSE 0 END) as superseded,
|
|
347
|
+
SUM(CASE WHEN is_active = 0 AND expires_at IS NOT NULL THEN 1 ELSE 0 END) as expired,
|
|
348
|
+
SUM(CASE WHEN is_active = 1 AND expires_at IS NOT NULL THEN 1 ELSE 0 END) as expiring_future
|
|
349
|
+
FROM memories`
|
|
350
|
+
).get();
|
|
351
|
+
// Claims (YOINK lineage): open bets, settled record, the overconfidence
|
|
352
|
+
// gap, the Brier score, and the dead-memory census.
|
|
353
|
+
const { calibration, deadMemories } = await import('../src/mind/claims.js');
|
|
354
|
+
const claims = db.prepare(
|
|
355
|
+
'SELECT COUNT(*) as total, SUM(CASE WHEN outcome IS NULL THEN 1 ELSE 0 END) as open, SUM(CASE WHEN outcome IS NOT NULL THEN 1 ELSE 0 END) as settled FROM claims'
|
|
356
|
+
).get();
|
|
317
357
|
console.log(JSON.stringify({
|
|
318
358
|
...brainStats(),
|
|
319
359
|
...(scope ? { scope } : {}),
|
|
320
360
|
projects: perProject,
|
|
361
|
+
forgetting,
|
|
362
|
+
claims: { ...claims, record: calibration() },
|
|
363
|
+
dead_memories: deadMemories().length,
|
|
321
364
|
replay_conflicts: listConflicts(20),
|
|
322
365
|
}, null, 2));
|
|
323
366
|
break;
|
|
324
367
|
}
|
|
368
|
+
case 'brief': {
|
|
369
|
+
// The problem-understanding stage: ingest a context document (file or
|
|
370
|
+
// inline text), compile its requirements to Gate checks, optionally
|
|
371
|
+
// verify now. scion brief <file> [--verify] | scion brief show <id>
|
|
372
|
+
const briefs = await import('../src/mind/brief.js');
|
|
373
|
+
if (arg === 'show') {
|
|
374
|
+
try { console.log(JSON.stringify(briefs.getBrief(Number(restArgs[0])), null, 2)); }
|
|
375
|
+
catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
if (arg === 'list') {
|
|
379
|
+
console.log(JSON.stringify(briefs.listBriefs(), null, 2));
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
try {
|
|
383
|
+
const { readFileSync } = await import('node:fs');
|
|
384
|
+
const text = arg && arg !== '-' && existsSync(arg) ? readFileSync(arg, 'utf8') : (arg || '');
|
|
385
|
+
if (!text.trim()) { console.error('✗ brief: provide a file path or inline text'); process.exitCode = 1; break; }
|
|
386
|
+
const b = briefs.ingest({ text, title: restArgs.find(x => !x.startsWith('--')), source: 'cli' });
|
|
387
|
+
const c = briefs.compileBrief(b.brief.id);
|
|
388
|
+
const out = { brief: b.brief, entities: b.entities.length, requirements: b.requirements.length, compiled: c.compiled, judgment: c.judgment };
|
|
389
|
+
if (process.argv.includes('--verify')) {
|
|
390
|
+
const v = await briefs.verifyBrief(b.brief.id);
|
|
391
|
+
out.verification = { passed: v.passed, failed: v.failed, coverage: v.coverage, uncheckable: v.uncheckable };
|
|
392
|
+
out.results = v.results;
|
|
393
|
+
}
|
|
394
|
+
console.log(JSON.stringify(out, null, 2));
|
|
395
|
+
} catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
case 'lang': {
|
|
399
|
+
// The multilingual layer: list configured languages, or set the active
|
|
400
|
+
// one (persisted per brain, like project scopes).
|
|
401
|
+
const langs = await import('../src/mind/langs.js');
|
|
402
|
+
if (!arg || arg === 'list') {
|
|
403
|
+
const packs = langs.loadPacks();
|
|
404
|
+
console.log(JSON.stringify({
|
|
405
|
+
active: langs.activeLanguage(),
|
|
406
|
+
languages: ['en', ...langs.configuredLanguages()],
|
|
407
|
+
packs: Object.values(packs).map(p => ({ lang: p.lang, name: p.name, script: p.script, entries: p.entries.length })),
|
|
408
|
+
}, null, 2));
|
|
409
|
+
} else if (arg === 'set') {
|
|
410
|
+
try { console.log(JSON.stringify(langs.setActiveLanguage(restArgs[0]), null, 2)); }
|
|
411
|
+
catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
|
|
412
|
+
} else {
|
|
413
|
+
try { console.log(JSON.stringify(langs.setActiveLanguage(arg), null, 2)); }
|
|
414
|
+
catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case 'claim': {
|
|
419
|
+
// YOINK lineage: turn a memory into a bet with a date on it.
|
|
420
|
+
// scion claim <memoryId> --claim "..." --settles 2027-01-01 --reads file:...#json.path --test "gte 10" --confidence 0.7
|
|
421
|
+
const { declare } = await import('../src/mind/claims.js');
|
|
422
|
+
const flag = (name) => {
|
|
423
|
+
const i = restArgs.indexOf('--' + name);
|
|
424
|
+
return i >= 0 ? restArgs[i + 1] : undefined;
|
|
425
|
+
};
|
|
426
|
+
try {
|
|
427
|
+
const r = declare({
|
|
428
|
+
memoryId: Number(arg),
|
|
429
|
+
claim: flag('claim'),
|
|
430
|
+
settles: flag('settles'),
|
|
431
|
+
reads: flag('reads'),
|
|
432
|
+
test: flag('test'),
|
|
433
|
+
confidence: Number(flag('confidence')),
|
|
434
|
+
});
|
|
435
|
+
console.log(JSON.stringify(r, null, 2));
|
|
436
|
+
} catch (e) {
|
|
437
|
+
console.error('✗ ' + e.message); process.exitCode = 1;
|
|
438
|
+
}
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case 'settle': {
|
|
442
|
+
// Settle everything due: read the sources, apply the tests, write the
|
|
443
|
+
// outcomes back beside the thoughts. Unreadable sources stay open.
|
|
444
|
+
const { settleAll } = await import('../src/mind/claims.js');
|
|
445
|
+
const r = await settleAll();
|
|
446
|
+
console.log(JSON.stringify(r, null, 2));
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case 'calibrate': {
|
|
450
|
+
// The record: said vs right — the gap and the Brier score.
|
|
451
|
+
const { calibration, deadMemories } = await import('../src/mind/claims.js');
|
|
452
|
+
console.log(JSON.stringify({
|
|
453
|
+
record: calibration(),
|
|
454
|
+
dead: deadMemories({ minAgeDays: Number(restArgs[0]) || 14 }),
|
|
455
|
+
trail_chain: (await import('../src/kernel/trail.js')).verifyChain(),
|
|
456
|
+
}, null, 2));
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
325
459
|
case 'conflicts': {
|
|
326
460
|
const { listConflicts, clearConflicts } = await import('../src/mind/replay.js');
|
|
327
461
|
if (arg === '--clear') {
|
|
@@ -732,7 +866,7 @@ switch (cmd || '') {
|
|
|
732
866
|
opener.unref();
|
|
733
867
|
}
|
|
734
868
|
const sweep = setInterval(() => {
|
|
735
|
-
|
|
869
|
+
dailyMaintenance().catch(() => { /* maintenance never kills the server */ });
|
|
736
870
|
}, 60_000);
|
|
737
871
|
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
738
872
|
process.on(sig, async () => {
|
|
@@ -876,6 +1010,15 @@ switch (cmd || '') {
|
|
|
876
1010
|
}
|
|
877
1011
|
break;
|
|
878
1012
|
}
|
|
1013
|
+
case '--version':
|
|
1014
|
+
case '-v': {
|
|
1015
|
+
// Self-reported version — the FS Desktop shell probes this to detect the
|
|
1016
|
+
// kernel and warn on shell/kernel version skew.
|
|
1017
|
+
let kernelPkg = { version: 'dev' };
|
|
1018
|
+
try { kernelPkg = JSON.parse(readFileSync(resolve(fileURLToPath(import.meta.url), '../../package.json'), 'utf8')); } catch { /* dev checkout without package.json */ }
|
|
1019
|
+
console.log(kernelPkg.version);
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
879
1022
|
default:
|
|
880
1023
|
console.error(`unknown command: ${cmd}\nusage: scion [run "<task>" | swarm [n] | plan <target> [--out m.json] [--run] | workflow <manifest.json> | analyze [dir] | fix <file> | watch [dir] [--auto-fix] [--concurrency N] | economy | brain | remember "<c>" | search "<q>" | reason "<topic>" | forge [--dry-run] | review [-i] | resolve <id> approve "<fix>"|reject|defer | gaps [teach <id> <intent> | forget <id> | --all] | scopes [list|add <name>|rm <name>] | ui | serve [port] [--follower --lease-ms N --leader-poll-ms N --primary-url URL] | remote --url URL --workerId ID | leader | recipes [doctor | show <name>]`);
|
|
881
1024
|
process.exitCode = 1;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# knowledge/lexicon.de.yaml — German language pack (data, not code).
|
|
2
|
+
# Loaded by mind/langs.js; consulted by interpret() as static evidence
|
|
3
|
+
# BEFORE learned terms. Adding a language = adding a file.
|
|
4
|
+
lang: de
|
|
5
|
+
name: German
|
|
6
|
+
script: latin
|
|
7
|
+
description: German command vocabulary — surface forms map to core intents
|
|
8
|
+
entries:
|
|
9
|
+
- intent: fix
|
|
10
|
+
words: [beheben, reparieren, korrigieren, flicken]
|
|
11
|
+
- intent: run
|
|
12
|
+
words: [ausführen, starten, laufen, abschicken]
|
|
13
|
+
- intent: analyze
|
|
14
|
+
words: [analysieren, prüfen, scannen, untersuchen]
|
|
15
|
+
- intent: show
|
|
16
|
+
words: [zeigen, anzeigen, auflisten, status, bericht]
|
|
17
|
+
- intent: search
|
|
18
|
+
words: [suchen, finden, nachschlagen]
|
|
19
|
+
- intent: remember
|
|
20
|
+
words: [merken, speichern, notieren, aufzeichnen]
|
|
21
|
+
- intent: explain
|
|
22
|
+
words: [erklären, warum, wie, beschreiben]
|
|
23
|
+
- intent: plan
|
|
24
|
+
words: [planen, plan, terminieren]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# knowledge/lexicon.ja.yaml — Japanese language pack (data, not code).
|
|
2
|
+
# CJK script: phrases have no word boundaries, so matching is Tier-2
|
|
3
|
+
# closed-set longest-match over the raw text — enough for command
|
|
4
|
+
# vocabularies, which are small fixed lists.
|
|
5
|
+
lang: ja
|
|
6
|
+
name: Japanese
|
|
7
|
+
script: cjk
|
|
8
|
+
description: Japanese command vocabulary — closed-set phrase matching
|
|
9
|
+
entries:
|
|
10
|
+
- intent: fix
|
|
11
|
+
words: [修正, 修復, 直す, 改修]
|
|
12
|
+
- intent: run
|
|
13
|
+
words: [実行, 起動, 走らせる]
|
|
14
|
+
- intent: analyze
|
|
15
|
+
words: [解析, 分析, 調査]
|
|
16
|
+
- intent: show
|
|
17
|
+
words: [表示, 一覧, 状態, 報告]
|
|
18
|
+
- intent: search
|
|
19
|
+
words: [検索, 探す, 見つける]
|
|
20
|
+
- intent: remember
|
|
21
|
+
words: [記憶, 保存, 覚える, 記録]
|
|
22
|
+
- intent: explain
|
|
23
|
+
words: [説明, なぜ, どうして]
|
|
24
|
+
- intent: plan
|
|
25
|
+
words: [計画, 計画する, 見積]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# knowledge/lexicon.tr.yaml — Turkish language pack (data, not code).
|
|
2
|
+
# Loaded by mind/langs.js; consulted by interpret() as static evidence
|
|
3
|
+
# BEFORE learned terms. Adding a language = adding a file.
|
|
4
|
+
lang: tr
|
|
5
|
+
name: Turkish
|
|
6
|
+
script: latin
|
|
7
|
+
description: Turkish command vocabulary — surface forms map to core intents
|
|
8
|
+
entries:
|
|
9
|
+
- intent: fix
|
|
10
|
+
words: [düzelt, düzeltme, onar, tamir, hallet]
|
|
11
|
+
- intent: run
|
|
12
|
+
words: [çalıştır, koş, yürüt, başlat]
|
|
13
|
+
- intent: analyze
|
|
14
|
+
words: [analiz, incele, tara, denetle]
|
|
15
|
+
- intent: show
|
|
16
|
+
words: [göster, listele, durum, rapor]
|
|
17
|
+
- intent: search
|
|
18
|
+
words: [ara, bul, hatırla]
|
|
19
|
+
- intent: remember
|
|
20
|
+
words: [hatırla, not al, kaydet, kayıt]
|
|
21
|
+
- intent: explain
|
|
22
|
+
words: [açıkla, neden, nasıl, anlat]
|
|
23
|
+
- intent: plan
|
|
24
|
+
words: [planla, plan, çizelgele]
|
package/package.json
CHANGED
package/src/brain/db.js
CHANGED
|
@@ -93,13 +93,35 @@ export function ensureSchema(db) {
|
|
|
93
93
|
layer INTEGER DEFAULT 1,
|
|
94
94
|
access_count INTEGER DEFAULT 0,
|
|
95
95
|
last_accessed_at TEXT,
|
|
96
|
+
expires_at TEXT,
|
|
97
|
+
superseded_by INTEGER,
|
|
96
98
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
97
99
|
);
|
|
98
100
|
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
|
|
99
101
|
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
100
102
|
CREATE INDEX IF NOT EXISTS idx_memories_active ON memories(is_active);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_memories_expires ON memories(is_active, expires_at);
|
|
101
104
|
`);
|
|
102
105
|
|
|
106
|
+
// Semantic vectors (cortex embedding.js lineage): one BLOB per memory —
|
|
107
|
+
// a deterministic 256-dim hashed-feature vector (brain/embedding.js).
|
|
108
|
+
db.exec(`
|
|
109
|
+
CREATE TABLE IF NOT EXISTS memory_vectors (
|
|
110
|
+
id INTEGER PRIMARY KEY,
|
|
111
|
+
vec BLOB NOT NULL
|
|
112
|
+
);
|
|
113
|
+
`);
|
|
114
|
+
|
|
115
|
+
// Temporal forgetting migration (cortex forgetting.js lineage): existing
|
|
116
|
+
// DBs gain expires_at + superseded_by. Idempotent via PRAGMA inspection.
|
|
117
|
+
const memoryCols = db.prepare(`PRAGMA table_info(memories)`).all().map(c => c.name);
|
|
118
|
+
if (!memoryCols.includes('expires_at')) {
|
|
119
|
+
db.exec(`ALTER TABLE memories ADD COLUMN expires_at TEXT`);
|
|
120
|
+
}
|
|
121
|
+
if (!memoryCols.includes('superseded_by')) {
|
|
122
|
+
db.exec(`ALTER TABLE memories ADD COLUMN superseded_by INTEGER`);
|
|
123
|
+
}
|
|
124
|
+
|
|
103
125
|
// FTS5 virtual table + sync triggers for keyword search
|
|
104
126
|
try {
|
|
105
127
|
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(title, content, tags, content=memories, content_rowid=id)`);
|
|
@@ -171,11 +193,69 @@ export function ensureSchema(db) {
|
|
|
171
193
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
172
194
|
ts INTEGER NOT NULL,
|
|
173
195
|
kind TEXT NOT NULL,
|
|
174
|
-
data TEXT NOT NULL
|
|
196
|
+
data TEXT NOT NULL,
|
|
197
|
+
prev_hash TEXT,
|
|
198
|
+
hash TEXT
|
|
175
199
|
);
|
|
176
200
|
CREATE INDEX IF NOT EXISTS idx_trail_kind ON trail_events(kind);
|
|
177
201
|
`);
|
|
178
202
|
|
|
203
|
+
// Hash-chain migration (YOINK ledger lineage): existing trails gain the
|
|
204
|
+
// linkage columns. Old rows chain from NULL backward — the chain verifier
|
|
205
|
+
// treats pre-migration rows as the genesis segment.
|
|
206
|
+
const trailCols = db.prepare(`PRAGMA table_info(trail_events)`).all().map(c => c.name);
|
|
207
|
+
if (!trailCols.includes('prev_hash')) db.exec(`ALTER TABLE trail_events ADD COLUMN prev_hash TEXT`);
|
|
208
|
+
if (!trailCols.includes('hash')) db.exec(`ALTER TABLE trail_events ADD COLUMN hash TEXT`);
|
|
209
|
+
|
|
210
|
+
// ── Mind: claims (YOINK lineage) — falsifiable forecasts with a settle
|
|
211
|
+
// date, a named source, and a confidence that gets graded. ──
|
|
212
|
+
db.exec(`
|
|
213
|
+
CREATE TABLE IF NOT EXISTS claims (
|
|
214
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
215
|
+
memory_id INTEGER NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
|
|
216
|
+
claim TEXT NOT NULL,
|
|
217
|
+
settles TEXT NOT NULL,
|
|
218
|
+
source TEXT NOT NULL,
|
|
219
|
+
test TEXT NOT NULL,
|
|
220
|
+
confidence REAL NOT NULL,
|
|
221
|
+
outcome INTEGER,
|
|
222
|
+
reading TEXT,
|
|
223
|
+
settled_at TEXT,
|
|
224
|
+
read_from TEXT,
|
|
225
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
226
|
+
);
|
|
227
|
+
CREATE INDEX IF NOT EXISTS idx_claims_due ON claims(outcome, settles);
|
|
228
|
+
`);
|
|
229
|
+
|
|
230
|
+
// ── Mind: briefs (the problem-understanding stage) — a body of human
|
|
231
|
+
// context (ticket/spec/conversation) becomes a STRUCTURED problem model:
|
|
232
|
+
// entities, business rules, acceptance criteria. Requirements carry their
|
|
233
|
+
// own checkability class; compiled checks are the Gate's handles. ──
|
|
234
|
+
db.exec(`
|
|
235
|
+
CREATE TABLE IF NOT EXISTS briefs (
|
|
236
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
237
|
+
title TEXT,
|
|
238
|
+
source TEXT,
|
|
239
|
+
text TEXT NOT NULL,
|
|
240
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
241
|
+
);
|
|
242
|
+
CREATE TABLE IF NOT EXISTS requirements (
|
|
243
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
244
|
+
brief_id INTEGER NOT NULL REFERENCES briefs(id) ON DELETE CASCADE,
|
|
245
|
+
kind TEXT NOT NULL,
|
|
246
|
+
text TEXT NOT NULL,
|
|
247
|
+
subject TEXT,
|
|
248
|
+
checkable INTEGER NOT NULL DEFAULT 0,
|
|
249
|
+
check_spec TEXT,
|
|
250
|
+
compiled_cmd TEXT,
|
|
251
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
252
|
+
last_verified_at TEXT,
|
|
253
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
254
|
+
);
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_req_brief ON requirements(brief_id);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_req_status ON requirements(status);
|
|
257
|
+
`);
|
|
258
|
+
|
|
179
259
|
// ── Mind: replay index (Tier-0 learned decisions, now persistent) ──
|
|
180
260
|
db.exec(`
|
|
181
261
|
CREATE TABLE IF NOT EXISTS replay_index (
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* brain/embedding.js — deterministic semantic vectors (the embeddings organ,
|
|
3
|
+
* ported from agentic-cortex's embedding.js with a doctrine-conscious swap).
|
|
4
|
+
*
|
|
5
|
+
* Cortex embeds with @xenova/transformers (BGE-base, ~400 MB download, model
|
|
6
|
+
* inference). FS's doctrine is zero-dependency and least-LLM, so the same
|
|
7
|
+
* CAPABILITY arrives as a MECHANISM instead: hashed feature embeddings.
|
|
8
|
+
*
|
|
9
|
+
* embed(text) → a 256-dim L2-normalized vector built from word unigrams,
|
|
10
|
+
* bigrams, and character trigrams, each feature hashed (FNV-1a) into the
|
|
11
|
+
* dimension space with a sign. Fully deterministic: the same text always
|
|
12
|
+
* yields the same vector, on every machine, with no model, no download,
|
|
13
|
+
* no network — and the quality floor is exactly the token-overlap signal
|
|
14
|
+
* the memory graph already uses, plus SUB-WORD and WORD-ORDER tolerance
|
|
15
|
+
* that bag-of-tokens misses ("authfailure" ≈ "auth failure", "deploy
|
|
16
|
+
* failed" ≈ "failure on deploy").
|
|
17
|
+
*
|
|
18
|
+
* Honest limits (vs a neural encoder): no synonymy ("car" ≉ "automobile"),
|
|
19
|
+
* no cross-lingual transfer. Synonymy is already handled separately by the
|
|
20
|
+
* NLU layer's synonym expansion at query time, which is why this floor is
|
|
21
|
+
* acceptable. A neural encoder can replace `embed()` later without touching
|
|
22
|
+
* any call site — the vector is opaque to consumers.
|
|
23
|
+
*
|
|
24
|
+
* @module brain/embedding
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
export const DIMS = 512;
|
|
30
|
+
|
|
31
|
+
/** FNV-1a 32-bit — fast, deterministic, well-distributed for short strings. */
|
|
32
|
+
function fnv1a(str) {
|
|
33
|
+
let h = 0x811c9dc5;
|
|
34
|
+
for (let i = 0; i < str.length; i++) {
|
|
35
|
+
h ^= str.charCodeAt(i);
|
|
36
|
+
h = (h + (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
|
|
37
|
+
}
|
|
38
|
+
return h;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Tokenize into words (letters/digits), lowercased. */
|
|
42
|
+
function words(text) {
|
|
43
|
+
return String(text || '').toLowerCase().match(/[a-z0-9_]+/g) || [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Pure-function stopwords: contribute no signal to vector space. */
|
|
47
|
+
export const STOPWORDS = new Set([
|
|
48
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'is', 'are', 'was', 'were', 'be',
|
|
49
|
+
'been', 'being', 'to', 'of', 'in', 'on', 'at', 'for', 'with', 'by', 'from',
|
|
50
|
+
'that', 'this', 'it', 'its', 'as', 'when', 'then', 'than', 'so', 'if',
|
|
51
|
+
'we', 'you', 'i', 'they', 'he', 'she', 'do', 'does', 'did', 'not', 'no',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/** Is this token a pure-function stopword (contentless for retrieval)? */
|
|
55
|
+
export function isStopword(w) {
|
|
56
|
+
return STOPWORDS.has(String(w || '').toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Embed text into a fixed-dim L2-normalized vector.
|
|
61
|
+
* Features: word unigrams (weight 1.0), adjacent bigrams (weight 1.5 —
|
|
62
|
+
* word-order signal), character trigrams of each word (weight 0.5 —
|
|
63
|
+
* sub-word/morphology tolerance: "login" ≈ "logins" ≈ "logging-in").
|
|
64
|
+
*
|
|
65
|
+
* @param {string} text
|
|
66
|
+
* @returns {Float64Array} DIMS-length normalized vector
|
|
67
|
+
*/
|
|
68
|
+
export function embed(text) {
|
|
69
|
+
const v = new Float64Array(DIMS);
|
|
70
|
+
const add = (feature, weight) => {
|
|
71
|
+
const h = fnv1a(feature);
|
|
72
|
+
const idx = h % DIMS;
|
|
73
|
+
const sign = (h & 0x80000000) ? -1 : 1;
|
|
74
|
+
v[idx] += sign * weight;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const ws = words(text);
|
|
78
|
+
const content = ws.filter(w => !STOPWORDS.has(w));
|
|
79
|
+
for (let i = 0; i < ws.length; i++) {
|
|
80
|
+
if (!STOPWORDS.has(ws[i])) {
|
|
81
|
+
add(ws[i], 1.5);
|
|
82
|
+
const w = ws[i];
|
|
83
|
+
if (w.length >= 3) {
|
|
84
|
+
for (let j = 0; j + 3 <= w.length; j++) add('§' + w.slice(j, j + 3), 1.0);
|
|
85
|
+
} else {
|
|
86
|
+
add('§' + w, 1.0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Bigrams bridge stopwords — "pool of connections" keeps the pair.
|
|
90
|
+
if (i + 1 < ws.length) add(ws[i] + '_' + ws[i + 1], 1.5);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let norm = 0;
|
|
94
|
+
for (let i = 0; i < DIMS; i++) norm += v[i] * v[i];
|
|
95
|
+
norm = Math.sqrt(norm);
|
|
96
|
+
if (norm > 0) for (let i = 0; i < DIMS; i++) v[i] /= norm;
|
|
97
|
+
return v;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Cosine similarity (inputs expected L2-normalized; safe for zero vectors). */
|
|
101
|
+
export function cosine(a, b) {
|
|
102
|
+
let dot = 0;
|
|
103
|
+
for (let i = 0; i < DIMS; i++) dot += a[i] * b[i];
|
|
104
|
+
return Math.max(-1, Math.min(1, dot));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Convenience: similarity of two texts, 0..1. */
|
|
108
|
+
export function similarity(textA, textB) {
|
|
109
|
+
return cosine(embed(textA), embed(textB));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Serialize a vector for SQLite BLOB storage. */
|
|
113
|
+
export function pack(vec) {
|
|
114
|
+
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Deserialize a vector from a SQLite BLOB. */
|
|
118
|
+
export function unpack(buf) {
|
|
119
|
+
return new Float64Array(buf.buffer, buf.byteOffset, buf.byteLength / 8);
|
|
120
|
+
}
|