futura-scion 0.2.1 → 0.2.3

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 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 {
@@ -83,6 +84,33 @@ const oracle = realVerifiers ? { verifiers: realVerifiers } : { verifyFn };
83
84
  */
84
85
  const CLARIFY_MAX_DEPTH = 3;
85
86
 
87
+ /** Human chat summary of a brief model (+ optional verification). */
88
+ function briefSummary(model, verification, intake) {
89
+ const reqs = model.requirements || [];
90
+ const lines = [];
91
+ if (intake) {
92
+ lines.push(intake.added > 0
93
+ ? `Noted — ${intake.added} new requirement${intake.added > 1 ? 's' : ''} added to brief #${model.id} (${intake.total} total).`
94
+ : `Already captured — brief #${model.id} has ${intake.total} requirement${intake.total === 1 ? '' : 's'}.`);
95
+ } else {
96
+ lines.push(`Brief #${model.id}: ${reqs.length} requirement${reqs.length === 1 ? '' : 's'}.`);
97
+ }
98
+ for (const r of reqs.slice(0, 8)) {
99
+ const mark = r.status === 'pass' ? '✓' : r.status === 'fail' ? '✗' : r.checkable ? '◇' : '·';
100
+ lines.push(` ${mark} ${r.kind}: ${String(r.text).slice(0, 90)}${r.checkable ? '' : ' (judgment — needs a human or a model)'}`);
101
+ }
102
+ if (reqs.length > 8) lines.push(` …and ${reqs.length - 8} more`);
103
+ if (verification) {
104
+ lines.push(`Gate verification: ${verification.passed} passed, ${verification.failed} failed` +
105
+ (verification.coverage != null ? `, coverage ${(verification.coverage * 100).toFixed(0)}%` : '') +
106
+ (verification.failed > 0 ? ' — failing requirements block completion.' :
107
+ verification.passed > 0 ? ' — the contract holds.' : ''));
108
+ } else {
109
+ lines.push('Say "verify the brief" to run the checks against the tree.');
110
+ }
111
+ return lines.join('\n');
112
+ }
113
+
86
114
  async function oneShot(text, opts = {}, depth = 0) {
87
115
  const { interpret, slotsToPayload, clarify, resolveClarification, resetLearnedTermsCache } = await import('../src/mind/nlu.js');
88
116
  const { getProjectScopes, resolveScopes } = await import('../src/mind/scopes.js');
@@ -91,7 +119,7 @@ async function oneShot(text, opts = {}, depth = 0) {
91
119
  // persisted scopes (brain config). interpret() strips the @tokens itself.
92
120
  const projectScopes = opts.noProjectScopes ? [] : getProjectScopes();
93
121
  const mergedScopes = resolveScopes(text, { flag: opts.scope ?? null, projectScopes });
94
- const interp = interpret(text, { existsSync: (p) => existsSync(p), intentOverride: opts.intentOverride, scopes: mergedScopes.length ? mergedScopes : undefined });
122
+ let interp = interpret(text, { existsSync: (p) => existsSync(p), intentOverride: opts.intentOverride, scopes: mergedScopes.length ? mergedScopes : undefined });
95
123
  trail.journal('nlu.interpret', {
96
124
  utterance: String(text).slice(0, 200),
97
125
  intent: interp.intent,
@@ -103,6 +131,26 @@ async function oneShot(text, opts = {}, depth = 0) {
103
131
  override: opts.intentOverride ?? null,
104
132
  });
105
133
 
134
+ // Tier 3 — the System-1 language-parse rung: ONLY when the deterministic
135
+ // interpreter found no evidence at all, and an llm provider is configured
136
+ // AND budgeted (daily_tokens > 0 — off by default). The model parses the
137
+ // utterance into intent JSON; the SAME schema check validates it; an
138
+ // abstention falls through to the clarify loop. Never a guess.
139
+ if (interp.intentEvidence.length === 0 && interp.confidence === 0 && !interp.question && !opts.intentOverride) {
140
+ const cfg = loadConfig();
141
+ if (cfg.llm?.daily_tokens > 0 && cfg.llm?.baseUrl && cfg.llm?.model) {
142
+ const { llmInterpret } = await import('../src/mind/nlu.js');
143
+ const parsed = await llmInterpret(text, { llmCfg: cfg.llm });
144
+ trail.journal('nlu.llm-parse', {
145
+ utterance: String(text).slice(0, 200),
146
+ accepted: !!parsed,
147
+ intent: parsed?.intent ?? null,
148
+ confidence: parsed?.confidence ?? null,
149
+ });
150
+ if (parsed) interp = parsed;
151
+ }
152
+ }
153
+
106
154
  // Route pure service intents to their kernel surface — a question about
107
155
  // state should not become a queue task.
108
156
  if (interp.intent === 'show') {
@@ -121,6 +169,29 @@ async function oneShot(text, opts = {}, depth = 0) {
121
169
  const saved = save({ type: 'observation', title: content.slice(0, 80), content, tags: ['remembered'], confidence: 90, provenance: 'explicit' });
122
170
  return { ok: true, intent: 'remember', result: saved };
123
171
  }
172
+ // Brief intent — context arrives IN THE CHAT, not as a file. The message
173
+ // itself is ingested into the active brief (accumulated across turns);
174
+ // "verify"/"check" phrasing settles the active brief instead.
175
+ if (interp.intent === 'brief') {
176
+ const briefs = await import('../src/mind/brief.js');
177
+ if (arg === 'verify' || restArgs.includes('--verify') || /\b(verify|check|settle)\b/i.test(text)) {
178
+ const id = briefs.activeBriefId();
179
+ if (!id) return { ok: false, intent: 'brief', reply: 'no active brief — state a requirement first (e.g. "the report must contain totals").' };
180
+ const c = briefs.compileBrief(id);
181
+ const v = await briefs.verifyBrief(id);
182
+ return {
183
+ ok: true, intent: 'brief',
184
+ reply: briefSummary(briefs.getBrief(id), v),
185
+ result: { briefId: id, compiled: c.compiled, verification: v },
186
+ };
187
+ }
188
+ const intake = await briefs.chatIntake(text);
189
+ return {
190
+ ok: true, intent: 'brief',
191
+ reply: briefSummary(intake.model, null, intake),
192
+ result: { briefId: intake.briefId, added: intake.added, total: intake.total },
193
+ };
194
+ }
124
195
 
125
196
  // Ambiguous: ask ONE deterministic question with the top-N ranked
126
197
  // interpretations instead of a cold escalation or a silent guess. Every
@@ -159,7 +230,7 @@ async function oneShot(text, opts = {}, depth = 0) {
159
230
  // Unresolved answer: fall through and surface the question.
160
231
  }
161
232
  }
162
- return { ok: false, intent: 'clarify', prompt: cl.prompt, options: cl.options, open: cl.open, gap_id: gap.id };
233
+ 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
234
  }
164
235
 
165
236
  // Ordinary task — enriched with extracted slots.
@@ -311,17 +382,130 @@ switch (cmd || '') {
311
382
  const db = getDb();
312
383
  const scope = restArgs[0] === '--all' ? 'all'
313
384
  : restArgs[0] === '--project' ? resolve(restArgs[1] || '.').replace(/\\/g, '/') : null;
385
+ // Temporal forgetting (cortex lineage): --sweep expires every memory
386
+ // whose lifespan has passed; --sweep --dry-run previews it.
387
+ if (arg === '--sweep' || restArgs.includes('--sweep')) {
388
+ const { sweepExpires } = await import('../src/brain/forgetting.js');
389
+ console.log(JSON.stringify(sweepExpires({ dryRun: arg === '--dry-run' || restArgs.includes('--dry-run') }), null, 2));
390
+ break;
391
+ }
314
392
  const perProject = db.prepare(
315
393
  'SELECT project, COUNT(*) as memories, SUM(is_active) as active FROM memories GROUP BY project ORDER BY memories DESC'
316
394
  ).all();
395
+ const forgetting = db.prepare(
396
+ `SELECT SUM(CASE WHEN is_active = 0 AND superseded_by IS NOT NULL THEN 1 ELSE 0 END) as superseded,
397
+ SUM(CASE WHEN is_active = 0 AND expires_at IS NOT NULL THEN 1 ELSE 0 END) as expired,
398
+ SUM(CASE WHEN is_active = 1 AND expires_at IS NOT NULL THEN 1 ELSE 0 END) as expiring_future
399
+ FROM memories`
400
+ ).get();
401
+ // Claims (YOINK lineage): open bets, settled record, the overconfidence
402
+ // gap, the Brier score, and the dead-memory census.
403
+ const { calibration, deadMemories } = await import('../src/mind/claims.js');
404
+ const claims = db.prepare(
405
+ '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'
406
+ ).get();
317
407
  console.log(JSON.stringify({
318
408
  ...brainStats(),
319
409
  ...(scope ? { scope } : {}),
320
410
  projects: perProject,
411
+ forgetting,
412
+ claims: { ...claims, record: calibration() },
413
+ dead_memories: deadMemories().length,
321
414
  replay_conflicts: listConflicts(20),
322
415
  }, null, 2));
323
416
  break;
324
417
  }
418
+ case 'brief': {
419
+ // The problem-understanding stage: ingest a context document (file or
420
+ // inline text), compile its requirements to Gate checks, optionally
421
+ // verify now. scion brief <file> [--verify] | scion brief show <id>
422
+ const briefs = await import('../src/mind/brief.js');
423
+ if (arg === 'show') {
424
+ try { console.log(JSON.stringify(briefs.getBrief(Number(restArgs[0])), null, 2)); }
425
+ catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
426
+ break;
427
+ }
428
+ if (arg === 'list') {
429
+ console.log(JSON.stringify(briefs.listBriefs(), null, 2));
430
+ break;
431
+ }
432
+ try {
433
+ const { readFileSync } = await import('node:fs');
434
+ const text = arg && arg !== '-' && existsSync(arg) ? readFileSync(arg, 'utf8') : (arg || '');
435
+ if (!text.trim()) { console.error('✗ brief: provide a file path or inline text'); process.exitCode = 1; break; }
436
+ const b = briefs.ingest({ text, title: restArgs.find(x => !x.startsWith('--')), source: 'cli' });
437
+ const c = briefs.compileBrief(b.brief.id);
438
+ const out = { brief: b.brief, entities: b.entities.length, requirements: b.requirements.length, compiled: c.compiled, judgment: c.judgment };
439
+ if (process.argv.includes('--verify')) {
440
+ const v = await briefs.verifyBrief(b.brief.id);
441
+ out.verification = { passed: v.passed, failed: v.failed, coverage: v.coverage, uncheckable: v.uncheckable };
442
+ out.results = v.results;
443
+ }
444
+ console.log(JSON.stringify(out, null, 2));
445
+ } catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
446
+ break;
447
+ }
448
+ case 'lang': {
449
+ // The multilingual layer: list configured languages, or set the active
450
+ // one (persisted per brain, like project scopes).
451
+ const langs = await import('../src/mind/langs.js');
452
+ if (!arg || arg === 'list') {
453
+ const packs = langs.loadPacks();
454
+ console.log(JSON.stringify({
455
+ active: langs.activeLanguage(),
456
+ languages: ['en', ...langs.configuredLanguages()],
457
+ packs: Object.values(packs).map(p => ({ lang: p.lang, name: p.name, script: p.script, entries: p.entries.length })),
458
+ }, null, 2));
459
+ } else if (arg === 'set') {
460
+ try { console.log(JSON.stringify(langs.setActiveLanguage(restArgs[0]), null, 2)); }
461
+ catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
462
+ } else {
463
+ try { console.log(JSON.stringify(langs.setActiveLanguage(arg), null, 2)); }
464
+ catch (e) { console.error('✗ ' + e.message); process.exitCode = 1; }
465
+ }
466
+ break;
467
+ }
468
+ case 'claim': {
469
+ // YOINK lineage: turn a memory into a bet with a date on it.
470
+ // scion claim <memoryId> --claim "..." --settles 2027-01-01 --reads file:...#json.path --test "gte 10" --confidence 0.7
471
+ const { declare } = await import('../src/mind/claims.js');
472
+ const flag = (name) => {
473
+ const i = restArgs.indexOf('--' + name);
474
+ return i >= 0 ? restArgs[i + 1] : undefined;
475
+ };
476
+ try {
477
+ const r = declare({
478
+ memoryId: Number(arg),
479
+ claim: flag('claim'),
480
+ settles: flag('settles'),
481
+ reads: flag('reads'),
482
+ test: flag('test'),
483
+ confidence: Number(flag('confidence')),
484
+ });
485
+ console.log(JSON.stringify(r, null, 2));
486
+ } catch (e) {
487
+ console.error('✗ ' + e.message); process.exitCode = 1;
488
+ }
489
+ break;
490
+ }
491
+ case 'settle': {
492
+ // Settle everything due: read the sources, apply the tests, write the
493
+ // outcomes back beside the thoughts. Unreadable sources stay open.
494
+ const { settleAll } = await import('../src/mind/claims.js');
495
+ const r = await settleAll();
496
+ console.log(JSON.stringify(r, null, 2));
497
+ break;
498
+ }
499
+ case 'calibrate': {
500
+ // The record: said vs right — the gap and the Brier score.
501
+ const { calibration, deadMemories } = await import('../src/mind/claims.js');
502
+ console.log(JSON.stringify({
503
+ record: calibration(),
504
+ dead: deadMemories({ minAgeDays: Number(restArgs[0]) || 14 }),
505
+ trail_chain: (await import('../src/kernel/trail.js')).verifyChain(),
506
+ }, null, 2));
507
+ break;
508
+ }
325
509
  case 'conflicts': {
326
510
  const { listConflicts, clearConflicts } = await import('../src/mind/replay.js');
327
511
  if (arg === '--clear') {
@@ -732,7 +916,7 @@ switch (cmd || '') {
732
916
  opener.unref();
733
917
  }
734
918
  const sweep = setInterval(() => {
735
- try { dailyMaintenance(); } catch { /* maintenance never kills the server */ }
919
+ dailyMaintenance().catch(() => { /* maintenance never kills the server */ });
736
920
  }, 60_000);
737
921
  for (const sig of ['SIGINT', 'SIGTERM']) {
738
922
  process.on(sig, async () => {
@@ -876,6 +1060,15 @@ switch (cmd || '') {
876
1060
  }
877
1061
  break;
878
1062
  }
1063
+ case '--version':
1064
+ case '-v': {
1065
+ // Self-reported version — the FS Desktop shell probes this to detect the
1066
+ // kernel and warn on shell/kernel version skew.
1067
+ let kernelPkg = { version: 'dev' };
1068
+ try { kernelPkg = JSON.parse(readFileSync(resolve(fileURLToPath(import.meta.url), '../../package.json'), 'utf8')); } catch { /* dev checkout without package.json */ }
1069
+ console.log(kernelPkg.version);
1070
+ break;
1071
+ }
879
1072
  default:
880
1073
  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
1074
  process.exitCode = 1;
@@ -11,8 +11,12 @@ ladder:
11
11
  min_insight_confidence: 0.55 # reasoner rung admission floor
12
12
 
13
13
  llm:
14
- daily_tokens: 0 # 0 = LLM rung off (zero-LLM doctrine)
14
+ daily_tokens: 500
15
15
 
16
+ apiKey: null
17
+ model: "qwen2.5-coder:7b"
18
+ baseUrl: "http://localhost:11434/v1"
19
+ provider: "ollama"
16
20
  bounds:
17
21
  max_turns: 25
18
22
  timeout_ms: 300000
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "futura-scion",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "The fused scion of cortex-os-agent + persona: one zero-LLM-dependent agent stack — Mind proposes, Muscle executes, Gate disposes.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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
+ }