zam-core 0.7.1 → 0.8.0

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/dist/index.js CHANGED
@@ -530,6 +530,7 @@ var SCHEMA = `
530
530
  CREATE TABLE IF NOT EXISTS tokens (
531
531
  id TEXT PRIMARY KEY,
532
532
  slug TEXT UNIQUE NOT NULL,
533
+ title TEXT NOT NULL DEFAULT '',
533
534
  concept TEXT NOT NULL,
534
535
  domain TEXT NOT NULL DEFAULT '',
535
536
  bloom_level INTEGER NOT NULL DEFAULT 1 CHECK (bloom_level BETWEEN 1 AND 5),
@@ -668,6 +669,22 @@ CREATE TABLE IF NOT EXISTS agent_skills (
668
669
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
669
670
  );
670
671
 
672
+ -- Knowledge contexts: work, school, private
673
+ CREATE TABLE IF NOT EXISTS contexts (
674
+ id TEXT PRIMARY KEY,
675
+ name TEXT NOT NULL UNIQUE,
676
+ label TEXT,
677
+ language TEXT,
678
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
679
+ );
680
+
681
+ -- Join table mapping tokens to their knowledge contexts
682
+ CREATE TABLE IF NOT EXISTS token_contexts (
683
+ token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
684
+ context_id TEXT NOT NULL REFERENCES contexts(id) ON DELETE CASCADE,
685
+ PRIMARY KEY (token_id, context_id)
686
+ );
687
+
671
688
  -- Performance indexes
672
689
  CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
673
690
  CREATE INDEX IF NOT EXISTS idx_tokens_slug ON tokens(slug);
@@ -678,6 +695,8 @@ CREATE INDEX IF NOT EXISTS idx_cards_token_user ON cards(token_id, user_id);
678
695
  CREATE INDEX IF NOT EXISTS idx_review_logs_card ON review_logs(card_id);
679
696
  CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed_at);
680
697
  CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
698
+ CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title);
699
+ CREATE INDEX IF NOT EXISTS idx_token_contexts_context ON token_contexts(context_id);
681
700
  `;
682
701
 
683
702
  // src/kernel/db/sync-adapter.ts
@@ -1011,6 +1030,34 @@ async function runMigrations(db) {
1011
1030
  embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
1012
1031
  )
1013
1032
  `);
1033
+ if (!tokenCols.some((c) => c.name === "title")) {
1034
+ await db.exec(
1035
+ `ALTER TABLE tokens ADD COLUMN title TEXT NOT NULL DEFAULT ''`
1036
+ );
1037
+ }
1038
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title)`);
1039
+ await db.exec(
1040
+ `CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain)`
1041
+ );
1042
+ await db.exec(`
1043
+ CREATE TABLE IF NOT EXISTS contexts (
1044
+ id TEXT PRIMARY KEY,
1045
+ name TEXT NOT NULL UNIQUE,
1046
+ label TEXT,
1047
+ language TEXT,
1048
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1049
+ )
1050
+ `);
1051
+ await db.exec(`
1052
+ CREATE TABLE IF NOT EXISTS token_contexts (
1053
+ token_id TEXT NOT NULL REFERENCES tokens(id) ON DELETE CASCADE,
1054
+ context_id TEXT NOT NULL REFERENCES contexts(id) ON DELETE CASCADE,
1055
+ PRIMARY KEY (token_id, context_id)
1056
+ )
1057
+ `);
1058
+ await db.exec(`
1059
+ CREATE INDEX IF NOT EXISTS idx_token_contexts_context ON token_contexts(context_id)
1060
+ `);
1014
1061
  }
1015
1062
 
1016
1063
  // src/kernel/db/snapshot.ts
@@ -1474,24 +1521,27 @@ async function deleteCardForUser(db, tokenId, userId) {
1474
1521
  await db.prepare("DELETE FROM cards WHERE id = ?").run(card.id);
1475
1522
  return { card, impact };
1476
1523
  }
1477
- async function getDueCards(db, userId, now, domain) {
1524
+ async function getDueCards(db, userId, now, domain, knowledgeContext) {
1478
1525
  const cutoff = now ?? (/* @__PURE__ */ new Date()).toISOString();
1526
+ let sql = `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
1527
+ FROM cards c
1528
+ JOIN tokens t ON t.id = c.token_id
1529
+ WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ?`;
1530
+ const params = [userId, cutoff];
1479
1531
  if (domain) {
1480
- return await db.prepare(
1481
- `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
1482
- FROM cards c
1483
- JOIN tokens t ON t.id = c.token_id
1484
- WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ? AND t.domain = ?
1485
- ORDER BY t.bloom_level ASC, c.due_at ASC`
1486
- ).all(userId, cutoff, domain);
1532
+ sql += " AND t.domain = ?";
1533
+ params.push(domain);
1487
1534
  }
1488
- return await db.prepare(
1489
- `SELECT c.*, t.slug, t.concept, t.domain, t.bloom_level
1490
- FROM cards c
1491
- JOIN tokens t ON t.id = c.token_id
1492
- WHERE c.user_id = ? AND c.blocked = 0 AND c.due_at <= ?
1493
- ORDER BY t.bloom_level ASC, c.due_at ASC`
1494
- ).all(userId, cutoff);
1535
+ if (knowledgeContext) {
1536
+ sql += ` AND EXISTS (
1537
+ SELECT 1 FROM token_contexts tc
1538
+ INNER JOIN contexts context_filter ON context_filter.id = tc.context_id
1539
+ WHERE tc.token_id = t.id AND context_filter.name = ?
1540
+ )`;
1541
+ params.push(knowledgeContext);
1542
+ }
1543
+ sql += " ORDER BY t.bloom_level ASC, c.due_at ASC";
1544
+ return await db.prepare(sql).all(...params);
1495
1545
  }
1496
1546
  async function getBlockedCards(db, userId) {
1497
1547
  return await db.prepare(
@@ -1503,21 +1553,128 @@ async function getBlockedCards(db, userId) {
1503
1553
  ).all(userId);
1504
1554
  }
1505
1555
 
1506
- // src/kernel/models/token.ts
1556
+ // src/kernel/models/knowledge-context.ts
1507
1557
  import { ulid as ulid3 } from "ulid";
1508
- async function createToken(db, input) {
1558
+ function normalizeContextName(name) {
1559
+ const normalized = name.trim();
1560
+ if (!normalized) {
1561
+ throw new Error("Context name cannot be empty");
1562
+ }
1563
+ return normalized;
1564
+ }
1565
+ function normalizeOptionalText(value) {
1566
+ if (value == null) return null;
1567
+ const normalized = value.trim();
1568
+ return normalized || null;
1569
+ }
1570
+ async function createKnowledgeContext(db, input) {
1509
1571
  const id = ulid3();
1510
1572
  const now = (/* @__PURE__ */ new Date()).toISOString();
1573
+ const name = normalizeContextName(input.name);
1574
+ const existing = await getKnowledgeContextByName(db, name);
1575
+ if (existing) {
1576
+ throw new Error(`Knowledge context with name "${name}" already exists`);
1577
+ }
1578
+ await db.prepare(
1579
+ `INSERT INTO contexts (id, name, label, language, created_at)
1580
+ VALUES (?, ?, ?, ?, ?)`
1581
+ ).run(
1582
+ id,
1583
+ name,
1584
+ normalizeOptionalText(input.label),
1585
+ normalizeOptionalText(input.language),
1586
+ now
1587
+ );
1588
+ const context = await getKnowledgeContextById(db, id);
1589
+ if (!context) {
1590
+ throw new Error(
1591
+ `Failed to retrieve newly created knowledge context: ${id}`
1592
+ );
1593
+ }
1594
+ return context;
1595
+ }
1596
+ async function getKnowledgeContextByName(db, name) {
1597
+ const normalized = name.trim();
1598
+ if (!normalized) return void 0;
1599
+ return await db.prepare("SELECT * FROM contexts WHERE name = ?").get(normalized);
1600
+ }
1601
+ async function getKnowledgeContextById(db, id) {
1602
+ return await db.prepare("SELECT * FROM contexts WHERE id = ?").get(id);
1603
+ }
1604
+ async function listKnowledgeContexts(db) {
1605
+ return await db.prepare("SELECT * FROM contexts ORDER BY name ASC").all();
1606
+ }
1607
+ async function updateKnowledgeContext(db, id, updates) {
1608
+ const context = await getKnowledgeContextById(db, id);
1609
+ if (!context) {
1610
+ throw new Error(`Knowledge context not found: ${id}`);
1611
+ }
1612
+ const fields = [];
1613
+ const values = [];
1614
+ if (updates.name !== void 0) {
1615
+ const name = normalizeContextName(updates.name);
1616
+ if (name !== context.name) {
1617
+ const existing = await getKnowledgeContextByName(db, name);
1618
+ if (existing) {
1619
+ throw new Error(`Knowledge context with name "${name}" already exists`);
1620
+ }
1621
+ fields.push("name = ?");
1622
+ values.push(name);
1623
+ }
1624
+ }
1625
+ if (updates.label !== void 0) {
1626
+ fields.push("label = ?");
1627
+ values.push(normalizeOptionalText(updates.label));
1628
+ }
1629
+ if (updates.language !== void 0) {
1630
+ fields.push("language = ?");
1631
+ values.push(normalizeOptionalText(updates.language));
1632
+ }
1633
+ if (fields.length === 0) {
1634
+ return context;
1635
+ }
1636
+ values.push(id);
1637
+ await db.prepare(`UPDATE contexts SET ${fields.join(", ")} WHERE id = ?`).run(...values);
1638
+ return await getKnowledgeContextById(db, id);
1639
+ }
1640
+ async function deleteKnowledgeContext(db, id) {
1641
+ await db.prepare("DELETE FROM contexts WHERE id = ?").run(id);
1642
+ }
1643
+ async function assignTokenToContext(db, tokenId, contextId) {
1644
+ await db.prepare(
1645
+ `INSERT OR IGNORE INTO token_contexts (token_id, context_id)
1646
+ VALUES (?, ?)`
1647
+ ).run(tokenId, contextId);
1648
+ }
1649
+ async function unassignTokenFromContext(db, tokenId, contextId) {
1650
+ await db.prepare("DELETE FROM token_contexts WHERE token_id = ? AND context_id = ?").run(tokenId, contextId);
1651
+ }
1652
+ async function listContextsForToken(db, tokenId) {
1653
+ return await db.prepare(
1654
+ `SELECT c.* FROM contexts c
1655
+ INNER JOIN token_contexts tc ON tc.context_id = c.id
1656
+ WHERE tc.token_id = ?
1657
+ ORDER BY c.name ASC`
1658
+ ).all(tokenId);
1659
+ }
1660
+
1661
+ // src/kernel/models/token.ts
1662
+ import { ulid as ulid4 } from "ulid";
1663
+ async function createToken(db, input) {
1664
+ const id = ulid4();
1665
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1511
1666
  const bloom = input.bloom_level ?? 1;
1512
1667
  if (bloom < 1 || bloom > 5) {
1513
1668
  throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
1514
1669
  }
1670
+ const title = input.title ?? "";
1515
1671
  await db.prepare(`
1516
- INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
1517
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1672
+ INSERT INTO tokens (id, slug, title, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
1673
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1518
1674
  `).run(
1519
1675
  id,
1520
1676
  input.slug,
1677
+ title,
1521
1678
  input.concept,
1522
1679
  input.domain ?? "",
1523
1680
  bloom,
@@ -1560,6 +1717,10 @@ async function updateToken(db, slug, updates) {
1560
1717
  }
1561
1718
  const fields = [];
1562
1719
  const values = [];
1720
+ if (updates.title !== void 0) {
1721
+ fields.push("title = ?");
1722
+ values.push(updates.title ?? "");
1723
+ }
1563
1724
  if (updates.concept !== void 0) {
1564
1725
  fields.push("concept = ?");
1565
1726
  values.push(updates.concept);
@@ -1665,13 +1826,14 @@ async function deleteToken(db, slug) {
1665
1826
  await db.transaction(async (tx) => {
1666
1827
  const now = (/* @__PURE__ */ new Date()).toISOString();
1667
1828
  const skillRows = await tx.prepare("SELECT id, token_slugs FROM agent_skills").all();
1829
+ const skillUpdateStmt = tx.prepare(
1830
+ "UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
1831
+ );
1668
1832
  for (const row of skillRows) {
1669
1833
  const tokenSlugs = JSON.parse(row.token_slugs);
1670
1834
  const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
1671
1835
  if (filtered.length !== tokenSlugs.length) {
1672
- await tx.prepare(
1673
- "UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
1674
- ).run(JSON.stringify(filtered), now, row.id);
1836
+ await skillUpdateStmt.run(JSON.stringify(filtered), now, row.id);
1675
1837
  }
1676
1838
  }
1677
1839
  await tx.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
@@ -1685,10 +1847,16 @@ async function findTokens(db, query) {
1685
1847
  const shortTerms = searchTokens.filter((t2) => t2.length <= 2);
1686
1848
  const longTerms = searchTokens.filter((t2) => t2.length > 2);
1687
1849
  const scoreMap = /* @__PURE__ */ new Map();
1688
- const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
1850
+ const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(title) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
1851
+ const likeStmt = db.prepare(likeSQL);
1689
1852
  for (const term of longTerms) {
1690
1853
  const pattern = `%${term}%`;
1691
- const rows = await db.prepare(likeSQL).all(pattern, pattern, pattern);
1854
+ const rows = await likeStmt.all(
1855
+ pattern,
1856
+ pattern,
1857
+ pattern,
1858
+ pattern
1859
+ );
1692
1860
  for (const row of rows) {
1693
1861
  const entry = scoreMap.get(row.id);
1694
1862
  if (entry) {
@@ -1701,7 +1869,7 @@ async function findTokens(db, query) {
1701
1869
  if (shortTerms.length > 0 || longTerms.length === 0) {
1702
1870
  const allTokens = await db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
1703
1871
  for (const token of allTokens) {
1704
- const words = `${token.slug} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1872
+ const words = `${token.slug} ${token.title} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
1705
1873
  let matchCount = 0;
1706
1874
  for (const term of shortTerms.length > 0 ? shortTerms : searchTokens) {
1707
1875
  for (const w of words) {
@@ -1730,23 +1898,47 @@ async function findTokens(db, query) {
1730
1898
  return scored;
1731
1899
  }
1732
1900
  async function listTokens(db, options) {
1733
- let tokens;
1901
+ const whereClauses = ["deprecated_at IS NULL"];
1902
+ const params = [];
1734
1903
  if (options?.domain) {
1735
- tokens = await db.prepare(
1736
- "SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
1737
- ).all(options.domain);
1738
- } else {
1739
- tokens = await db.prepare(
1740
- "SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
1741
- ).all();
1742
- }
1904
+ whereClauses.push("domain = ?");
1905
+ params.push(options.domain);
1906
+ } else if (options?.domainPrefix) {
1907
+ const prefix = options.domainPrefix;
1908
+ whereClauses.push("(domain = ? OR domain LIKE ?)");
1909
+ params.push(prefix, `${prefix}/%`);
1910
+ }
1911
+ if (options?.knowledgeContext) {
1912
+ whereClauses.push(`EXISTS (
1913
+ SELECT 1 FROM token_contexts tc
1914
+ INNER JOIN contexts c ON c.id = tc.context_id
1915
+ WHERE tc.token_id = tokens.id AND c.name = ?
1916
+ )`);
1917
+ params.push(options.knowledgeContext);
1918
+ }
1919
+ const orderBy = options?.domain || options?.domainPrefix ? "ORDER BY bloom_level, slug" : "ORDER BY bloom_level, domain, slug";
1920
+ const sql = `SELECT * FROM tokens WHERE ${whereClauses.join(" AND ")} ${orderBy}`;
1921
+ const tokens = await db.prepare(sql).all(...params);
1743
1922
  for (const token of tokens) {
1744
1923
  parseTokenFallback(token);
1745
1924
  }
1746
1925
  return tokens;
1747
1926
  }
1748
1927
  function slugify(text) {
1749
- return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1928
+ return text.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
1929
+ }
1930
+ function getShortSlug(slug, domainPrefix) {
1931
+ if (domainPrefix) {
1932
+ const folded = domainPrefix.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1933
+ if (folded && slug.startsWith(`${folded}-`)) {
1934
+ return slug.substring(folded.length + 1);
1935
+ }
1936
+ }
1937
+ return slug;
1938
+ }
1939
+ function getDisplayTitle(t2, activeDomainScope) {
1940
+ if (t2.title?.trim()) return t2.title.trim();
1941
+ return getShortSlug(t2.slug, activeDomainScope);
1750
1942
  }
1751
1943
  async function generateTokenSlug(db, domain, concept, question) {
1752
1944
  const baseText = question && question.trim().length > 0 ? question : concept;
@@ -1776,6 +1968,7 @@ async function listPersonalCards(db, userId, options) {
1776
1968
  SELECT
1777
1969
  t.id AS tokenId,
1778
1970
  t.slug,
1971
+ t.title,
1779
1972
  t.concept,
1780
1973
  t.domain,
1781
1974
  t.bloom_level AS bloomLevel,
@@ -1816,6 +2009,14 @@ async function listPersonalCards(db, userId, options) {
1816
2009
  sql += " AND t.domain = ?";
1817
2010
  values.push(options.domain);
1818
2011
  }
2012
+ if (options?.knowledgeContext) {
2013
+ sql += ` AND EXISTS (
2014
+ SELECT 1 FROM token_contexts tc
2015
+ INNER JOIN contexts kc ON kc.id = tc.context_id
2016
+ WHERE tc.token_id = t.id AND kc.name = ?
2017
+ )`;
2018
+ values.push(options.knowledgeContext);
2019
+ }
1819
2020
  if (options?.query) {
1820
2021
  const terms = options.query.toLowerCase().split(/\s+/).filter(Boolean);
1821
2022
  for (const term of terms) {
@@ -1877,6 +2078,7 @@ async function importCurriculumCards(db, userId, cards) {
1877
2078
  );
1878
2079
  token = await createToken(tx, {
1879
2080
  slug: finalSlug,
2081
+ title: card.title,
1880
2082
  concept: card.concept,
1881
2083
  domain: card.domain,
1882
2084
  bloom_level: bloom,
@@ -1994,23 +2196,28 @@ async function confirmCardSplit(db, userId, originalSlug, action, originalQuesti
1994
2196
  (/* @__PURE__ */ new Date()).toISOString(),
1995
2197
  originalToken.id
1996
2198
  );
2199
+ const insertPrereqStmt = tx.prepare(
2200
+ "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
2201
+ );
1997
2202
  for (const propToken of proposalTokens) {
1998
- await tx.prepare(
1999
- "INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
2000
- ).run(originalToken.id, propToken.id);
2203
+ await insertPrereqStmt.run(originalToken.id, propToken.id);
2001
2204
  }
2002
2205
  await tx.prepare(
2003
2206
  "UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
2004
2207
  ).run(originalToken.id, userId);
2208
+ const checkPrereqsStmt = tx.prepare(
2209
+ "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
2210
+ );
2211
+ const unblockCardStmt = tx.prepare(
2212
+ "UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?"
2213
+ );
2005
2214
  for (const propToken of proposalTokens) {
2006
2215
  const card = await ensureCard(tx, propToken.id, userId);
2007
2216
  if (card.blocked === 1) {
2008
- const prereqOfPrereq = await tx.prepare(
2009
- "SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
2010
- ).get(propToken.id);
2217
+ const prereqOfPrereq = await checkPrereqsStmt.get(propToken.id);
2011
2218
  if (prereqOfPrereq.n === 0) {
2012
2219
  const now = (/* @__PURE__ */ new Date()).toISOString();
2013
- await tx.prepare("UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?").run(now, card.id);
2220
+ await unblockCardStmt.run(now, card.id);
2014
2221
  }
2015
2222
  }
2016
2223
  }
@@ -2081,6 +2288,7 @@ async function confirmFoundations(db, userId, originalSlug, proposals) {
2081
2288
  );
2082
2289
  token = await createToken(tx, {
2083
2290
  slug: finalSlug,
2291
+ title: card.title,
2084
2292
  concept: card.concept,
2085
2293
  domain: card.domain,
2086
2294
  bloom_level: bloom,
@@ -2147,6 +2355,7 @@ async function confirmSourceImport(db, userId, sourceId, proposals) {
2147
2355
  }
2148
2356
  token = await createToken(tx, {
2149
2357
  slug: finalSlug,
2358
+ title: card.title,
2150
2359
  concept: card.concept,
2151
2360
  domain: card.domain,
2152
2361
  bloom_level: bloom,
@@ -2249,7 +2458,7 @@ async function addPrerequisite(db, tokenId, requiresId) {
2249
2458
  }
2250
2459
  async function getPrerequisites(db, tokenId) {
2251
2460
  return await db.prepare(
2252
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
2461
+ `SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
2253
2462
  FROM prerequisites p
2254
2463
  JOIN tokens t ON t.id = p.requires_id
2255
2464
  WHERE p.token_id = ?`
@@ -2257,7 +2466,7 @@ async function getPrerequisites(db, tokenId) {
2257
2466
  }
2258
2467
  async function getDependents(db, tokenId) {
2259
2468
  return await db.prepare(
2260
- `SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
2469
+ `SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
2261
2470
  FROM prerequisites p
2262
2471
  JOIN tokens t ON t.id = p.token_id
2263
2472
  WHERE p.requires_id = ?`
@@ -2288,6 +2497,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2288
2497
  const toNode = (t2, card) => ({
2289
2498
  id: t2.id,
2290
2499
  slug: t2.slug,
2500
+ title: t2.title ?? t2.slug,
2291
2501
  concept: t2.concept,
2292
2502
  domain: t2.domain,
2293
2503
  bloom_level: t2.bloom_level,
@@ -2307,6 +2517,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2307
2517
  {
2308
2518
  id: p.requires_id,
2309
2519
  slug: p.slug,
2520
+ title: p.title,
2310
2521
  concept: p.concept,
2311
2522
  domain: p.domain,
2312
2523
  bloom_level: p.bloom_level
@@ -2319,6 +2530,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2319
2530
  {
2320
2531
  id: d.token_id,
2321
2532
  slug: d.slug,
2533
+ title: d.title,
2322
2534
  concept: d.concept,
2323
2535
  domain: d.domain,
2324
2536
  bloom_level: d.bloom_level
@@ -2330,12 +2542,12 @@ async function getTokenNeighborhood(db, tokenId, userId) {
2330
2542
  }
2331
2543
 
2332
2544
  // src/kernel/models/review.ts
2333
- import { ulid as ulid4 } from "ulid";
2545
+ import { ulid as ulid5 } from "ulid";
2334
2546
  async function logReview(db, input) {
2335
2547
  if (input.rating < 1 || input.rating > 4) {
2336
2548
  throw new Error(`Rating must be between 1 and 4, got ${input.rating}`);
2337
2549
  }
2338
- const id = ulid4();
2550
+ const id = ulid5();
2339
2551
  const now = (/* @__PURE__ */ new Date()).toISOString();
2340
2552
  await db.prepare(
2341
2553
  `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
@@ -2378,9 +2590,9 @@ async function getReviewsForUser(db, userId, options) {
2378
2590
  }
2379
2591
 
2380
2592
  // src/kernel/models/session.ts
2381
- import { ulid as ulid5 } from "ulid";
2593
+ import { ulid as ulid6 } from "ulid";
2382
2594
  async function startSession(db, input) {
2383
- const id = ulid5();
2595
+ const id = ulid6();
2384
2596
  const now = (/* @__PURE__ */ new Date()).toISOString();
2385
2597
  const ctx = input.execution_context ?? "shell";
2386
2598
  await db.prepare(
@@ -2414,7 +2626,7 @@ async function logStep(db, input) {
2414
2626
  if (!session) {
2415
2627
  throw new Error(`Session not found: ${input.session_id}`);
2416
2628
  }
2417
- const id = ulid5();
2629
+ const id = ulid6();
2418
2630
  const now = (/* @__PURE__ */ new Date()).toISOString();
2419
2631
  await db.prepare(
2420
2632
  `INSERT INTO session_steps (id, session_id, token_id, done_by, rating, notes, created_at)
@@ -2478,7 +2690,8 @@ import { createHash as createHash2 } from "crypto";
2478
2690
  function embeddingContentForToken(t2) {
2479
2691
  return `${t2.concept}
2480
2692
  ${t2.question ?? ""}
2481
- ${t2.domain}`;
2693
+ ${t2.domain}
2694
+ ${t2.title ?? ""}`;
2482
2695
  }
2483
2696
  function computeContentHash(text) {
2484
2697
  return createHash2("sha256").update(text, "utf8").digest("hex");
@@ -3312,10 +3525,10 @@ async function syncObserverSidecarPolicy(db, dir = getUiObserverDir()) {
3312
3525
  }
3313
3526
 
3314
3527
  // src/kernel/observation/session-synthesis.ts
3315
- import { ulid as ulid7 } from "ulid";
3528
+ import { ulid as ulid8 } from "ulid";
3316
3529
 
3317
3530
  // src/kernel/recall/evaluator.ts
3318
- import { ulid as ulid6 } from "ulid";
3531
+ import { ulid as ulid7 } from "ulid";
3319
3532
 
3320
3533
  // src/kernel/scheduler/fsrs.ts
3321
3534
  var DEFAULT_W = [
@@ -3494,7 +3707,7 @@ async function evaluateRatingWithinTransaction(db, input) {
3494
3707
  due_at: updated.dueAt.toISOString(),
3495
3708
  last_review_at: now.toISOString()
3496
3709
  });
3497
- const reviewLogId = input.reviewLogId ?? ulid6();
3710
+ const reviewLogId = input.reviewLogId ?? ulid7();
3498
3711
  await db.prepare(
3499
3712
  `INSERT INTO review_logs (id, card_id, token_id, user_id, rating, response_time_ms, reviewed_at, scheduled_at, session_id)
3500
3713
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
@@ -3828,7 +4041,7 @@ async function applySessionSynthesis(db, input) {
3828
4041
  return { applied: false, record: parseSynthesisRow(existing) };
3829
4042
  }
3830
4043
  const card = await ensureCard(tx, token.id, session.user_id);
3831
- const reviewLogId = ulid7();
4044
+ const reviewLogId = ulid8();
3832
4045
  await evaluateRatingWithinTransaction(tx, {
3833
4046
  cardId: card.id,
3834
4047
  tokenId: token.id,
@@ -4657,11 +4870,11 @@ async function buildReviewQueue(db, options) {
4657
4870
  const maxReviews = options.maxReviews ?? 50;
4658
4871
  const now = options.now ?? /* @__PURE__ */ new Date();
4659
4872
  const nowISO = now.toISOString();
4660
- const dueRows = await db.prepare(
4661
- `SELECT
4873
+ let dueSql = `SELECT
4662
4874
  c.id AS card_id,
4663
4875
  c.token_id AS token_id,
4664
4876
  t.slug AS slug,
4877
+ t.title AS title,
4665
4878
  t.concept AS concept,
4666
4879
  t.domain AS domain,
4667
4880
  t.bloom_level AS bloom_level,
@@ -4675,14 +4888,23 @@ async function buildReviewQueue(db, options) {
4675
4888
  AND c.blocked = 0
4676
4889
  AND c.due_at <= ?
4677
4890
  AND c.state IN ('review', 'relearning', 'learning')
4678
- AND t.deprecated_at IS NULL
4679
- ORDER BY c.due_at ASC`
4680
- ).all(options.userId, nowISO);
4681
- const newRows = await db.prepare(
4682
- `SELECT
4891
+ AND t.deprecated_at IS NULL`;
4892
+ const dueParams = [options.userId, nowISO];
4893
+ if (options.knowledgeContext) {
4894
+ dueSql += ` AND EXISTS (
4895
+ SELECT 1 FROM token_contexts tc
4896
+ INNER JOIN contexts ctx ON ctx.id = tc.context_id
4897
+ WHERE tc.token_id = t.id AND ctx.name = ?
4898
+ )`;
4899
+ dueParams.push(options.knowledgeContext);
4900
+ }
4901
+ dueSql += ` ORDER BY c.due_at ASC`;
4902
+ const dueRows = await db.prepare(dueSql).all(...dueParams);
4903
+ let newSql = `SELECT
4683
4904
  c.id AS card_id,
4684
4905
  c.token_id AS token_id,
4685
4906
  t.slug AS slug,
4907
+ t.title AS title,
4686
4908
  t.concept AS concept,
4687
4909
  t.domain AS domain,
4688
4910
  t.bloom_level AS bloom_level,
@@ -4695,10 +4917,19 @@ async function buildReviewQueue(db, options) {
4695
4917
  WHERE c.user_id = ?
4696
4918
  AND c.blocked = 0
4697
4919
  AND c.state = 'new'
4698
- AND t.deprecated_at IS NULL
4699
- ORDER BY t.bloom_level ASC, t.slug ASC
4700
- LIMIT ?`
4701
- ).all(options.userId, maxNew);
4920
+ AND t.deprecated_at IS NULL`;
4921
+ const newParams = [options.userId];
4922
+ if (options.knowledgeContext) {
4923
+ newSql += ` AND EXISTS (
4924
+ SELECT 1 FROM token_contexts tc
4925
+ INNER JOIN contexts ctx ON ctx.id = tc.context_id
4926
+ WHERE tc.token_id = t.id AND ctx.name = ?
4927
+ )`;
4928
+ newParams.push(options.knowledgeContext);
4929
+ }
4930
+ newSql += ` ORDER BY t.bloom_level ASC, t.slug ASC LIMIT ?`;
4931
+ newParams.push(maxNew);
4932
+ const newRows = await db.prepare(newSql).all(...newParams);
4702
4933
  const nowMs = now.getTime();
4703
4934
  const sortedDue = [...dueRows].sort((a, b) => {
4704
4935
  const overdueA = nowMs - new Date(a.due_at).getTime();
@@ -4742,6 +4973,7 @@ function rowToItem(row) {
4742
4973
  cardId: row.card_id,
4743
4974
  tokenId: row.token_id,
4744
4975
  slug: row.slug,
4976
+ title: getDisplayTitle(row),
4745
4977
  concept: row.concept,
4746
4978
  domain: row.domain,
4747
4979
  bloomLevel: row.bloom_level,
@@ -5444,6 +5676,27 @@ function detectSyncProvider(dir) {
5444
5676
  }
5445
5677
  return null;
5446
5678
  }
5679
+ function getActiveWorkspaceContext(path = defaultConfigPath()) {
5680
+ const activeWorkspace = getActiveWorkspace(path);
5681
+ return activeWorkspace?.activeKnowledgeContext;
5682
+ }
5683
+ function setActiveWorkspaceContext(contextName, path = defaultConfigPath()) {
5684
+ const config = loadInstallConfig(path);
5685
+ const activeId = config.activeWorkspaceId;
5686
+ if (activeId && config.workspaces) {
5687
+ const workspace = config.workspaces.find((w) => w.id === activeId);
5688
+ if (workspace) {
5689
+ if (contextName) {
5690
+ workspace.activeKnowledgeContext = contextName;
5691
+ } else {
5692
+ delete workspace.activeKnowledgeContext;
5693
+ }
5694
+ saveInstallConfig(config, path);
5695
+ return true;
5696
+ }
5697
+ }
5698
+ return false;
5699
+ }
5447
5700
 
5448
5701
  // src/kernel/system/installer.ts
5449
5702
  import { execFileSync, execSync } from "child_process";
@@ -5939,6 +6192,7 @@ export {
5939
6192
  analyzeObservation,
5940
6193
  appendUiObservationReport,
5941
6194
  applySessionSynthesis,
6195
+ assignTokenToContext,
5942
6196
  buildAncestorMap,
5943
6197
  buildReviewQueue,
5944
6198
  buildUiSynthesisCandidates,
@@ -5956,12 +6210,14 @@ export {
5956
6210
  createAgentSkill,
5957
6211
  createFSRS,
5958
6212
  createGoal,
6213
+ createKnowledgeContext,
5959
6214
  createToken,
5960
6215
  decidePostCapture,
5961
6216
  decidePreCapture,
5962
6217
  decideUpdate,
5963
6218
  decodeEmbedding,
5964
6219
  deleteCardForUser,
6220
+ deleteKnowledgeContext,
5965
6221
  deleteSetting,
5966
6222
  deleteToken,
5967
6223
  deprecateToken,
@@ -5993,6 +6249,7 @@ export {
5993
6249
  generateZshUnhooks,
5994
6250
  getADOCredentials,
5995
6251
  getActiveWorkspace,
6252
+ getActiveWorkspaceContext,
5996
6253
  getActiveWorkspaceId,
5997
6254
  getAgentSkill,
5998
6255
  getAllSettings,
@@ -6005,6 +6262,7 @@ export {
6005
6262
  getDatabaseTargetInfo,
6006
6263
  getDefaultDbPath,
6007
6264
  getDependents,
6265
+ getDisplayTitle,
6008
6266
  getDomainCompetence,
6009
6267
  getDueCards,
6010
6268
  getEmbeddingCoverage,
@@ -6012,6 +6270,8 @@ export {
6012
6270
  getGoalTree,
6013
6271
  getInstallChannel,
6014
6272
  getInstallMode,
6273
+ getKnowledgeContextById,
6274
+ getKnowledgeContextByName,
6015
6275
  getMachineAiConfig,
6016
6276
  getMonitorDir,
6017
6277
  getMonitorLogStats,
@@ -6025,6 +6285,7 @@ export {
6025
6285
  getSessionSummary,
6026
6286
  getSessionSynthesisRecords,
6027
6287
  getSetting,
6288
+ getShortSlug,
6028
6289
  getSystemProfile,
6029
6290
  getTokenById,
6030
6291
  getTokenBySlug,
@@ -6047,8 +6308,10 @@ export {
6047
6308
  isOllamaInstalled,
6048
6309
  isUiObservationReport,
6049
6310
  listAgentSkills,
6311
+ listContextsForToken,
6050
6312
  listEmbeddedTokens,
6051
6313
  listGoals,
6314
+ listKnowledgeContexts,
6052
6315
  listPersonalCards,
6053
6316
  listProviderApiKeyRefs,
6054
6317
  listTokens,
@@ -6095,6 +6358,7 @@ export {
6095
6358
  searchTokensHybrid,
6096
6359
  serializeGoal,
6097
6360
  setADOCredentials,
6361
+ setActiveWorkspaceContext,
6098
6362
  setActiveWorkspaceId,
6099
6363
  setInstallChannel,
6100
6364
  setInstallMode,
@@ -6109,9 +6373,11 @@ export {
6109
6373
  toSidecarPrivacyPolicy,
6110
6374
  uiObservationLogExists,
6111
6375
  uiObservationTimeSpan,
6376
+ unassignTokenFromContext,
6112
6377
  unblockReady,
6113
6378
  updateCard,
6114
6379
  updateGoalStatus,
6380
+ updateKnowledgeContext,
6115
6381
  updateToken,
6116
6382
  upsertConfiguredWorkspace,
6117
6383
  upsertTokenEmbedding,