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 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
- const interp = interpret(text, { existsSync: (p) => existsSync(p), intentOverride: opts.intentOverride, scopes: mergedScopes.length ? mergedScopes : undefined });
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
- try { dailyMaintenance(); } catch { /* maintenance never kills the server */ }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "futura-scion",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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
+ }