zam-core 0.7.0 → 0.7.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/.agent/skills/zam/SKILL.md +12 -0
- package/.agents/skills/zam/SKILL.md +12 -0
- package/.claude/skills/zam/SKILL.md +12 -0
- package/dist/cli/index.js +1140 -117
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +71 -3
- package/dist/index.js +150 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync as readFileSync16 } from "fs";
|
|
5
5
|
import { dirname as dirname10, join as join25 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
7
|
-
import { Command as
|
|
7
|
+
import { Command as Command26 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/cli/commands/agent.ts
|
|
10
10
|
import { existsSync as existsSync13 } from "fs";
|
|
@@ -543,6 +543,7 @@ var SCHEMA = `
|
|
|
543
543
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
544
544
|
id TEXT PRIMARY KEY,
|
|
545
545
|
slug TEXT UNIQUE NOT NULL,
|
|
546
|
+
title TEXT NOT NULL DEFAULT '',
|
|
546
547
|
concept TEXT NOT NULL,
|
|
547
548
|
domain TEXT NOT NULL DEFAULT '',
|
|
548
549
|
bloom_level INTEGER NOT NULL DEFAULT 1 CHECK (bloom_level BETWEEN 1 AND 5),
|
|
@@ -691,6 +692,8 @@ CREATE INDEX IF NOT EXISTS idx_cards_token_user ON cards(token_id, user_id);
|
|
|
691
692
|
CREATE INDEX IF NOT EXISTS idx_review_logs_card ON review_logs(card_id);
|
|
692
693
|
CREATE INDEX IF NOT EXISTS idx_review_logs_user ON review_logs(user_id, reviewed_at);
|
|
693
694
|
CREATE INDEX IF NOT EXISTS idx_session_steps_session ON session_steps(session_id);
|
|
695
|
+
CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title);
|
|
696
|
+
CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain);
|
|
694
697
|
`;
|
|
695
698
|
|
|
696
699
|
// src/kernel/db/sync-adapter.ts
|
|
@@ -1024,6 +1027,15 @@ async function runMigrations(db) {
|
|
|
1024
1027
|
embedded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1025
1028
|
)
|
|
1026
1029
|
`);
|
|
1030
|
+
if (!tokenCols.some((c) => c.name === "title")) {
|
|
1031
|
+
await db.exec(
|
|
1032
|
+
`ALTER TABLE tokens ADD COLUMN title TEXT NOT NULL DEFAULT ''`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_tokens_title ON tokens(title)`);
|
|
1036
|
+
await db.exec(
|
|
1037
|
+
`CREATE INDEX IF NOT EXISTS idx_tokens_domain ON tokens(domain)`
|
|
1038
|
+
);
|
|
1027
1039
|
}
|
|
1028
1040
|
|
|
1029
1041
|
// src/kernel/db/snapshot.ts
|
|
@@ -1516,12 +1528,14 @@ async function createToken(db, input8) {
|
|
|
1516
1528
|
if (bloom < 1 || bloom > 5) {
|
|
1517
1529
|
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1518
1530
|
}
|
|
1531
|
+
const title = input8.title ?? "";
|
|
1519
1532
|
await db.prepare(`
|
|
1520
|
-
INSERT INTO tokens (id, slug, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
|
|
1521
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1533
|
+
INSERT INTO tokens (id, slug, title, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
|
|
1534
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1522
1535
|
`).run(
|
|
1523
1536
|
id,
|
|
1524
1537
|
input8.slug,
|
|
1538
|
+
title,
|
|
1525
1539
|
input8.concept,
|
|
1526
1540
|
input8.domain ?? "",
|
|
1527
1541
|
bloom,
|
|
@@ -1564,6 +1578,10 @@ async function updateToken(db, slug, updates) {
|
|
|
1564
1578
|
}
|
|
1565
1579
|
const fields = [];
|
|
1566
1580
|
const values = [];
|
|
1581
|
+
if (updates.title !== void 0) {
|
|
1582
|
+
fields.push("title = ?");
|
|
1583
|
+
values.push(updates.title ?? "");
|
|
1584
|
+
}
|
|
1567
1585
|
if (updates.concept !== void 0) {
|
|
1568
1586
|
fields.push("concept = ?");
|
|
1569
1587
|
values.push(updates.concept);
|
|
@@ -1669,13 +1687,14 @@ async function deleteToken(db, slug) {
|
|
|
1669
1687
|
await db.transaction(async (tx) => {
|
|
1670
1688
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1671
1689
|
const skillRows = await tx.prepare("SELECT id, token_slugs FROM agent_skills").all();
|
|
1690
|
+
const skillUpdateStmt = tx.prepare(
|
|
1691
|
+
"UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
|
|
1692
|
+
);
|
|
1672
1693
|
for (const row of skillRows) {
|
|
1673
1694
|
const tokenSlugs = JSON.parse(row.token_slugs);
|
|
1674
1695
|
const filtered = tokenSlugs.filter((tokenSlug) => tokenSlug !== slug);
|
|
1675
1696
|
if (filtered.length !== tokenSlugs.length) {
|
|
1676
|
-
await
|
|
1677
|
-
"UPDATE agent_skills SET token_slugs = ?, updated_at = ? WHERE id = ?"
|
|
1678
|
-
).run(JSON.stringify(filtered), now, row.id);
|
|
1697
|
+
await skillUpdateStmt.run(JSON.stringify(filtered), now, row.id);
|
|
1679
1698
|
}
|
|
1680
1699
|
}
|
|
1681
1700
|
await tx.prepare("DELETE FROM tokens WHERE id = ?").run(token.id);
|
|
@@ -1689,10 +1708,16 @@ async function findTokens(db, query) {
|
|
|
1689
1708
|
const shortTerms = searchTokens.filter((t2) => t2.length <= 2);
|
|
1690
1709
|
const longTerms = searchTokens.filter((t2) => t2.length > 2);
|
|
1691
1710
|
const scoreMap = /* @__PURE__ */ new Map();
|
|
1692
|
-
const likeSQL = `SELECT * FROM tokens WHERE deprecated_at IS NULL AND (lower(slug) LIKE ? OR lower(concept) LIKE ? OR lower(domain) LIKE ?)`;
|
|
1711
|
+
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 ?)`;
|
|
1712
|
+
const likeStmt = db.prepare(likeSQL);
|
|
1693
1713
|
for (const term of longTerms) {
|
|
1694
1714
|
const pattern = `%${term}%`;
|
|
1695
|
-
const rows = await
|
|
1715
|
+
const rows = await likeStmt.all(
|
|
1716
|
+
pattern,
|
|
1717
|
+
pattern,
|
|
1718
|
+
pattern,
|
|
1719
|
+
pattern
|
|
1720
|
+
);
|
|
1696
1721
|
for (const row of rows) {
|
|
1697
1722
|
const entry = scoreMap.get(row.id);
|
|
1698
1723
|
if (entry) {
|
|
@@ -1705,7 +1730,7 @@ async function findTokens(db, query) {
|
|
|
1705
1730
|
if (shortTerms.length > 0 || longTerms.length === 0) {
|
|
1706
1731
|
const allTokens = await db.prepare("SELECT * FROM tokens WHERE deprecated_at IS NULL").all();
|
|
1707
1732
|
for (const token of allTokens) {
|
|
1708
|
-
const words = `${token.slug} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
|
|
1733
|
+
const words = `${token.slug} ${token.title} ${token.concept} ${token.domain}`.toLowerCase().split(/[\s,.\-_/\\:;!?()[\]{}]+/).filter(Boolean);
|
|
1709
1734
|
let matchCount = 0;
|
|
1710
1735
|
for (const term of shortTerms.length > 0 ? shortTerms : searchTokens) {
|
|
1711
1736
|
for (const w of words) {
|
|
@@ -1739,6 +1764,11 @@ async function listTokens(db, options) {
|
|
|
1739
1764
|
tokens = await db.prepare(
|
|
1740
1765
|
"SELECT * FROM tokens WHERE domain = ? AND deprecated_at IS NULL ORDER BY bloom_level, slug"
|
|
1741
1766
|
).all(options.domain);
|
|
1767
|
+
} else if (options?.domainPrefix) {
|
|
1768
|
+
const prefix = options.domainPrefix;
|
|
1769
|
+
tokens = await db.prepare(
|
|
1770
|
+
`SELECT * FROM tokens WHERE (domain = ? OR domain LIKE ?) AND deprecated_at IS NULL ORDER BY bloom_level, slug`
|
|
1771
|
+
).all(prefix, `${prefix}/%`);
|
|
1742
1772
|
} else {
|
|
1743
1773
|
tokens = await db.prepare(
|
|
1744
1774
|
"SELECT * FROM tokens WHERE deprecated_at IS NULL ORDER BY bloom_level, domain, slug"
|
|
@@ -1750,7 +1780,20 @@ async function listTokens(db, options) {
|
|
|
1750
1780
|
return tokens;
|
|
1751
1781
|
}
|
|
1752
1782
|
function slugify(text) {
|
|
1753
|
-
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1783
|
+
return text.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1784
|
+
}
|
|
1785
|
+
function getShortSlug(slug, domainPrefix) {
|
|
1786
|
+
if (domainPrefix) {
|
|
1787
|
+
const folded = domainPrefix.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1788
|
+
if (folded && slug.startsWith(`${folded}-`)) {
|
|
1789
|
+
return slug.substring(folded.length + 1);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
return slug;
|
|
1793
|
+
}
|
|
1794
|
+
function getDisplayTitle(t2, activeDomainScope) {
|
|
1795
|
+
if (t2.title?.trim()) return t2.title.trim();
|
|
1796
|
+
return getShortSlug(t2.slug, activeDomainScope);
|
|
1754
1797
|
}
|
|
1755
1798
|
async function generateTokenSlug(db, domain, concept, question) {
|
|
1756
1799
|
const baseText = question && question.trim().length > 0 ? question : concept;
|
|
@@ -1780,6 +1823,7 @@ async function listPersonalCards(db, userId, options) {
|
|
|
1780
1823
|
SELECT
|
|
1781
1824
|
t.id AS tokenId,
|
|
1782
1825
|
t.slug,
|
|
1826
|
+
t.title,
|
|
1783
1827
|
t.concept,
|
|
1784
1828
|
t.domain,
|
|
1785
1829
|
t.bloom_level AS bloomLevel,
|
|
@@ -1881,6 +1925,7 @@ async function importCurriculumCards(db, userId, cards) {
|
|
|
1881
1925
|
);
|
|
1882
1926
|
token = await createToken(tx, {
|
|
1883
1927
|
slug: finalSlug,
|
|
1928
|
+
title: card.title,
|
|
1884
1929
|
concept: card.concept,
|
|
1885
1930
|
domain: card.domain,
|
|
1886
1931
|
bloom_level: bloom,
|
|
@@ -1998,23 +2043,28 @@ async function confirmCardSplit(db, userId, originalSlug, action, originalQuesti
|
|
|
1998
2043
|
(/* @__PURE__ */ new Date()).toISOString(),
|
|
1999
2044
|
originalToken.id
|
|
2000
2045
|
);
|
|
2046
|
+
const insertPrereqStmt = tx.prepare(
|
|
2047
|
+
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
2048
|
+
);
|
|
2001
2049
|
for (const propToken of proposalTokens) {
|
|
2002
|
-
await
|
|
2003
|
-
"INSERT OR IGNORE INTO prerequisites (token_id, requires_id) VALUES (?, ?)"
|
|
2004
|
-
).run(originalToken.id, propToken.id);
|
|
2050
|
+
await insertPrereqStmt.run(originalToken.id, propToken.id);
|
|
2005
2051
|
}
|
|
2006
2052
|
await tx.prepare(
|
|
2007
2053
|
"UPDATE cards SET blocked = 1 WHERE token_id = ? AND user_id = ?"
|
|
2008
2054
|
).run(originalToken.id, userId);
|
|
2055
|
+
const checkPrereqsStmt = tx.prepare(
|
|
2056
|
+
"SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
|
|
2057
|
+
);
|
|
2058
|
+
const unblockCardStmt = tx.prepare(
|
|
2059
|
+
"UPDATE cards SET blocked = 0, due_at = ? WHERE id = ?"
|
|
2060
|
+
);
|
|
2009
2061
|
for (const propToken of proposalTokens) {
|
|
2010
2062
|
const card = await ensureCard(tx, propToken.id, userId);
|
|
2011
2063
|
if (card.blocked === 1) {
|
|
2012
|
-
const prereqOfPrereq = await
|
|
2013
|
-
"SELECT COUNT(*) as n FROM prerequisites WHERE token_id = ?"
|
|
2014
|
-
).get(propToken.id);
|
|
2064
|
+
const prereqOfPrereq = await checkPrereqsStmt.get(propToken.id);
|
|
2015
2065
|
if (prereqOfPrereq.n === 0) {
|
|
2016
2066
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2017
|
-
await
|
|
2067
|
+
await unblockCardStmt.run(now, card.id);
|
|
2018
2068
|
}
|
|
2019
2069
|
}
|
|
2020
2070
|
}
|
|
@@ -2085,6 +2135,7 @@ async function confirmFoundations(db, userId, originalSlug, proposals) {
|
|
|
2085
2135
|
);
|
|
2086
2136
|
token = await createToken(tx, {
|
|
2087
2137
|
slug: finalSlug,
|
|
2138
|
+
title: card.title,
|
|
2088
2139
|
concept: card.concept,
|
|
2089
2140
|
domain: card.domain,
|
|
2090
2141
|
bloom_level: bloom,
|
|
@@ -2151,6 +2202,7 @@ async function confirmSourceImport(db, userId, sourceId, proposals) {
|
|
|
2151
2202
|
}
|
|
2152
2203
|
token = await createToken(tx, {
|
|
2153
2204
|
slug: finalSlug,
|
|
2205
|
+
title: card.title,
|
|
2154
2206
|
concept: card.concept,
|
|
2155
2207
|
domain: card.domain,
|
|
2156
2208
|
bloom_level: bloom,
|
|
@@ -2219,9 +2271,9 @@ async function buildAncestorMap(db) {
|
|
|
2219
2271
|
}
|
|
2220
2272
|
return map;
|
|
2221
2273
|
}
|
|
2222
|
-
async function wouldCreateCycle(db, tokenId, requiresId) {
|
|
2274
|
+
async function wouldCreateCycle(db, tokenId, requiresId, ancestors) {
|
|
2223
2275
|
if (tokenId === requiresId) return true;
|
|
2224
|
-
const
|
|
2276
|
+
const map = ancestors ?? await buildAncestorMap(db);
|
|
2225
2277
|
const visited = /* @__PURE__ */ new Set();
|
|
2226
2278
|
const queue = [requiresId];
|
|
2227
2279
|
while (queue.length > 0) {
|
|
@@ -2229,7 +2281,7 @@ async function wouldCreateCycle(db, tokenId, requiresId) {
|
|
|
2229
2281
|
if (current === tokenId) return true;
|
|
2230
2282
|
if (visited.has(current)) continue;
|
|
2231
2283
|
visited.add(current);
|
|
2232
|
-
const parents =
|
|
2284
|
+
const parents = map.get(current);
|
|
2233
2285
|
if (parents) {
|
|
2234
2286
|
for (const parent of parents) {
|
|
2235
2287
|
if (!visited.has(parent)) queue.push(parent);
|
|
@@ -2253,7 +2305,7 @@ async function addPrerequisite(db, tokenId, requiresId) {
|
|
|
2253
2305
|
}
|
|
2254
2306
|
async function getPrerequisites(db, tokenId) {
|
|
2255
2307
|
return await db.prepare(
|
|
2256
|
-
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
2308
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
|
|
2257
2309
|
FROM prerequisites p
|
|
2258
2310
|
JOIN tokens t ON t.id = p.requires_id
|
|
2259
2311
|
WHERE p.token_id = ?`
|
|
@@ -2261,7 +2313,7 @@ async function getPrerequisites(db, tokenId) {
|
|
|
2261
2313
|
}
|
|
2262
2314
|
async function getDependents(db, tokenId) {
|
|
2263
2315
|
return await db.prepare(
|
|
2264
|
-
`SELECT p.token_id, p.requires_id, t.slug, t.concept, t.domain, t.bloom_level
|
|
2316
|
+
`SELECT p.token_id, p.requires_id, t.slug, t.title, t.concept, t.domain, t.bloom_level
|
|
2265
2317
|
FROM prerequisites p
|
|
2266
2318
|
JOIN tokens t ON t.id = p.token_id
|
|
2267
2319
|
WHERE p.requires_id = ?`
|
|
@@ -2292,6 +2344,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
|
|
|
2292
2344
|
const toNode = (t2, card) => ({
|
|
2293
2345
|
id: t2.id,
|
|
2294
2346
|
slug: t2.slug,
|
|
2347
|
+
title: t2.title ?? t2.slug,
|
|
2295
2348
|
concept: t2.concept,
|
|
2296
2349
|
domain: t2.domain,
|
|
2297
2350
|
bloom_level: t2.bloom_level,
|
|
@@ -2311,6 +2364,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
|
|
|
2311
2364
|
{
|
|
2312
2365
|
id: p.requires_id,
|
|
2313
2366
|
slug: p.slug,
|
|
2367
|
+
title: p.title,
|
|
2314
2368
|
concept: p.concept,
|
|
2315
2369
|
domain: p.domain,
|
|
2316
2370
|
bloom_level: p.bloom_level
|
|
@@ -2323,6 +2377,7 @@ async function getTokenNeighborhood(db, tokenId, userId) {
|
|
|
2323
2377
|
{
|
|
2324
2378
|
id: d.token_id,
|
|
2325
2379
|
slug: d.slug,
|
|
2380
|
+
title: d.title,
|
|
2326
2381
|
concept: d.concept,
|
|
2327
2382
|
domain: d.domain,
|
|
2328
2383
|
bloom_level: d.bloom_level
|
|
@@ -2437,7 +2492,8 @@ import { createHash as createHash2 } from "crypto";
|
|
|
2437
2492
|
function embeddingContentForToken(t2) {
|
|
2438
2493
|
return `${t2.concept}
|
|
2439
2494
|
${t2.question ?? ""}
|
|
2440
|
-
${t2.domain}
|
|
2495
|
+
${t2.domain}
|
|
2496
|
+
${t2.title ?? ""}`;
|
|
2441
2497
|
}
|
|
2442
2498
|
function computeContentHash(text) {
|
|
2443
2499
|
return createHash2("sha256").update(text, "utf8").digest("hex");
|
|
@@ -4601,6 +4657,7 @@ async function buildReviewQueue(db, options) {
|
|
|
4601
4657
|
c.id AS card_id,
|
|
4602
4658
|
c.token_id AS token_id,
|
|
4603
4659
|
t.slug AS slug,
|
|
4660
|
+
t.title AS title,
|
|
4604
4661
|
t.concept AS concept,
|
|
4605
4662
|
t.domain AS domain,
|
|
4606
4663
|
t.bloom_level AS bloom_level,
|
|
@@ -4622,6 +4679,7 @@ async function buildReviewQueue(db, options) {
|
|
|
4622
4679
|
c.id AS card_id,
|
|
4623
4680
|
c.token_id AS token_id,
|
|
4624
4681
|
t.slug AS slug,
|
|
4682
|
+
t.title AS title,
|
|
4625
4683
|
t.concept AS concept,
|
|
4626
4684
|
t.domain AS domain,
|
|
4627
4685
|
t.bloom_level AS bloom_level,
|
|
@@ -4681,6 +4739,7 @@ function rowToItem(row) {
|
|
|
4681
4739
|
cardId: row.card_id,
|
|
4682
4740
|
tokenId: row.token_id,
|
|
4683
4741
|
slug: row.slug,
|
|
4742
|
+
title: getDisplayTitle(row),
|
|
4684
4743
|
concept: row.concept,
|
|
4685
4744
|
domain: row.domain,
|
|
4686
4745
|
bloomLevel: row.bloom_level,
|
|
@@ -4789,6 +4848,71 @@ async function searchTokensHybrid(db, query, opts) {
|
|
|
4789
4848
|
return results.slice(0, limit);
|
|
4790
4849
|
}
|
|
4791
4850
|
|
|
4851
|
+
// src/kernel/search/suggestions.ts
|
|
4852
|
+
async function suggestFoundations(db, opts) {
|
|
4853
|
+
const minSimilarity = opts.minSimilarity ?? 0.45;
|
|
4854
|
+
const maxSimilarity = opts.maxSimilarity ?? 0.85;
|
|
4855
|
+
const limit = opts.limit ?? 5;
|
|
4856
|
+
const targetBloomLevel = opts.targetBloomLevel ?? 5;
|
|
4857
|
+
if (minSimilarity >= maxSimilarity) {
|
|
4858
|
+
return [];
|
|
4859
|
+
}
|
|
4860
|
+
const embedded = await listEmbeddedTokens(db, opts.model);
|
|
4861
|
+
if (embedded.length === 0) {
|
|
4862
|
+
return [];
|
|
4863
|
+
}
|
|
4864
|
+
const queryVec = Float32Array.from(opts.queryEmbedding);
|
|
4865
|
+
const candidates = [];
|
|
4866
|
+
for (const row of embedded) {
|
|
4867
|
+
if (opts.targetTokenId && row.token.id === opts.targetTokenId) {
|
|
4868
|
+
continue;
|
|
4869
|
+
}
|
|
4870
|
+
const similarity = cosineSimilarity(queryVec, row.embedding);
|
|
4871
|
+
if (similarity >= minSimilarity && similarity < maxSimilarity) {
|
|
4872
|
+
candidates.push({ token: row.token, similarity });
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
candidates.sort((a, b) => {
|
|
4876
|
+
if (Math.abs(a.similarity - b.similarity) > 1e-9) {
|
|
4877
|
+
return b.similarity - a.similarity;
|
|
4878
|
+
}
|
|
4879
|
+
return a.token.slug.localeCompare(b.token.slug);
|
|
4880
|
+
});
|
|
4881
|
+
const topCandidates = candidates.slice(0, limit);
|
|
4882
|
+
const prereqIds = /* @__PURE__ */ new Set();
|
|
4883
|
+
let ancestors;
|
|
4884
|
+
if (opts.targetTokenId) {
|
|
4885
|
+
const prereqs = await getPrerequisites(db, opts.targetTokenId);
|
|
4886
|
+
for (const p of prereqs) {
|
|
4887
|
+
prereqIds.add(p.requires_id);
|
|
4888
|
+
}
|
|
4889
|
+
ancestors = await buildAncestorMap(db);
|
|
4890
|
+
}
|
|
4891
|
+
const results = [];
|
|
4892
|
+
for (const cand of topCandidates) {
|
|
4893
|
+
let alreadyPrerequisite = false;
|
|
4894
|
+
let wouldCreateCycleFlag = false;
|
|
4895
|
+
const bloomAboveTarget = cand.token.bloom_level > targetBloomLevel;
|
|
4896
|
+
if (opts.targetTokenId) {
|
|
4897
|
+
alreadyPrerequisite = prereqIds.has(cand.token.id);
|
|
4898
|
+
wouldCreateCycleFlag = await wouldCreateCycle(
|
|
4899
|
+
db,
|
|
4900
|
+
opts.targetTokenId,
|
|
4901
|
+
cand.token.id,
|
|
4902
|
+
ancestors
|
|
4903
|
+
);
|
|
4904
|
+
}
|
|
4905
|
+
results.push({
|
|
4906
|
+
token: cand.token,
|
|
4907
|
+
similarity: cand.similarity,
|
|
4908
|
+
alreadyPrerequisite,
|
|
4909
|
+
wouldCreateCycle: wouldCreateCycleFlag,
|
|
4910
|
+
bloomAboveTarget
|
|
4911
|
+
});
|
|
4912
|
+
}
|
|
4913
|
+
return results;
|
|
4914
|
+
}
|
|
4915
|
+
|
|
4792
4916
|
// src/kernel/system/hooks.ts
|
|
4793
4917
|
import {
|
|
4794
4918
|
appendFileSync as appendFileSync3,
|
|
@@ -6575,6 +6699,11 @@ function parseGeneratedCardArray(responseText, label, limits) {
|
|
|
6575
6699
|
);
|
|
6576
6700
|
}
|
|
6577
6701
|
}
|
|
6702
|
+
if (card.title !== void 0 && (typeof card.title !== "string" || card.title.trim().length === 0)) {
|
|
6703
|
+
throw new Error(
|
|
6704
|
+
`Invalid ${label} card at index ${index}: title must be a non-empty string if provided`
|
|
6705
|
+
);
|
|
6706
|
+
}
|
|
6578
6707
|
if (typeof card.bloom_level !== "number" || !Number.isInteger(card.bloom_level) || card.bloom_level < 1 || card.bloom_level > 5) {
|
|
6579
6708
|
throw new Error(
|
|
6580
6709
|
`Invalid ${label} card at index ${index}: bloom_level must be an integer from 1 to 5`
|
|
@@ -6588,6 +6717,7 @@ function parseGeneratedCardArray(responseText, label, limits) {
|
|
|
6588
6717
|
return {
|
|
6589
6718
|
question: card.question,
|
|
6590
6719
|
concept: card.concept,
|
|
6720
|
+
title: card.title ? card.title : void 0,
|
|
6591
6721
|
domain: card.domain,
|
|
6592
6722
|
context: card.context,
|
|
6593
6723
|
bloom_level: card.bloom_level,
|
|
@@ -6638,10 +6768,11 @@ Your task is to analyze curriculum objectives, syllabus requirements, or textboo
|
|
|
6638
6768
|
For each extracted learning card, you MUST generate:
|
|
6639
6769
|
1. "question": A clear, concise active-recall question testing the concept. The question must not reveal the answer itself.
|
|
6640
6770
|
2. "concept": The reference answer, core fact, or target conceptual explanation.
|
|
6641
|
-
3. "
|
|
6642
|
-
4. "
|
|
6643
|
-
5. "
|
|
6644
|
-
6. "
|
|
6771
|
+
3. "title": A short, human-friendly title for the knowledge graph node. Concise name for the concept. No domain prefix. Human-readable, natural language. Support Unicode (umlauts, Chinese, Japanese, Arabic, etc.). Name the core idea as well as possible.
|
|
6772
|
+
4. "domain": The category of the card. Use "${targetCategory}" as the default, but you may refine it if a specific sub-topic is evident.
|
|
6773
|
+
5. "context": The exact sentence or short excerpt from the source text that this card is based on.
|
|
6774
|
+
6. "bloom_level": Initial Bloom cognitive level (1 = Remember, 2 = Understand, 3 = Apply, 4 = Analyze, 5 = Synthesize).
|
|
6775
|
+
7. "symbiosis_mode": ZAM agent symbiosis level: "shadowing" (reading/monitoring), "copilot" (interactive helper), or "autonomy" (autonomous tasks).
|
|
6645
6776
|
|
|
6646
6777
|
Guidelines:
|
|
6647
6778
|
- Break complex requirements into multiple separate, atomic cards.
|
|
@@ -7570,6 +7701,114 @@ async function ensureHighQualityQuestion(db, token) {
|
|
|
7570
7701
|
}
|
|
7571
7702
|
return null;
|
|
7572
7703
|
}
|
|
7704
|
+
async function generateTitleViaLLM(db, input8, opts = {}) {
|
|
7705
|
+
const cfg = await getProviderForRole(db, "text");
|
|
7706
|
+
const endpoint = await resolveUsableTextEndpoint(db);
|
|
7707
|
+
const systemPrompt = `You are an expert at naming knowledge items for a personal knowledge graph.
|
|
7708
|
+
Your task: given a knowledge token's concept (the full reference answer), question, domain, and optional context, produce a short, descriptive, human-friendly TITLE.
|
|
7709
|
+
|
|
7710
|
+
Strict rules from the project ADR:
|
|
7711
|
+
- Concise name for the concept (\u2264 80 chars ideal).
|
|
7712
|
+
- Thoughtful, memorable name \u2014 NOT a definition or the first sentence of the concept.
|
|
7713
|
+
- NEVER echo the domain name (e.g. do not say "Node Drain" if domain is "axon-ivy"; just "Node Drain Protection").
|
|
7714
|
+
- Prefer a name over spoiling the full concept.
|
|
7715
|
+
- Support Unicode (umlauts, etc.).
|
|
7716
|
+
- Output the title in the same language as the concept/content (e.g., German if the concept is German, English if the concept is English).
|
|
7717
|
+
- Output ONLY the raw title text. No preamble, quotes, explanations, or markdown.
|
|
7718
|
+
|
|
7719
|
+
Example good titles:
|
|
7720
|
+
- "RAG Retrieval Quality Evaluation" (for RAG failure modes focused on retrieval)
|
|
7721
|
+
- "Pythagorean Theorem Converse Proof"
|
|
7722
|
+
- "macOS Applications Directory Layout"
|
|
7723
|
+
- "Chronotype Sleep Preference"`;
|
|
7724
|
+
const userPrompt = `Domain: ${input8.domain}
|
|
7725
|
+
Slug: ${input8.slug}
|
|
7726
|
+
Question: ${input8.question || "(none)"}
|
|
7727
|
+
Concept: ${input8.concept}
|
|
7728
|
+
Context: ${input8.context || "(none)"}
|
|
7729
|
+
|
|
7730
|
+
Title:`;
|
|
7731
|
+
const url = `${endpoint.url}/chat/completions`;
|
|
7732
|
+
const apiKey = endpoint.apiKey;
|
|
7733
|
+
const model = endpoint.model;
|
|
7734
|
+
try {
|
|
7735
|
+
const res = await fetchWithInteractiveTimeout(url, {
|
|
7736
|
+
method: "POST",
|
|
7737
|
+
headers: {
|
|
7738
|
+
"Content-Type": "application/json",
|
|
7739
|
+
Authorization: `Bearer ${apiKey}`
|
|
7740
|
+
},
|
|
7741
|
+
body: JSON.stringify({
|
|
7742
|
+
model,
|
|
7743
|
+
messages: [
|
|
7744
|
+
{ role: "system", content: systemPrompt },
|
|
7745
|
+
{ role: "user", content: userPrompt }
|
|
7746
|
+
],
|
|
7747
|
+
temperature: 0.2,
|
|
7748
|
+
max_tokens: 100
|
|
7749
|
+
}),
|
|
7750
|
+
locale: cfg.locale,
|
|
7751
|
+
timeoutMs: opts.timeoutMs,
|
|
7752
|
+
hardTimeoutMs: opts.timeoutMs
|
|
7753
|
+
});
|
|
7754
|
+
const text = await readChatContent(res, "title generation");
|
|
7755
|
+
return {
|
|
7756
|
+
text: text.trim(),
|
|
7757
|
+
model,
|
|
7758
|
+
providerName: endpoint.providerName
|
|
7759
|
+
};
|
|
7760
|
+
} catch (_e) {
|
|
7761
|
+
return {
|
|
7762
|
+
text: input8.concept.split(/[.;]/)[0].trim().substring(0, 80),
|
|
7763
|
+
model: "fallback",
|
|
7764
|
+
providerName: "heuristic"
|
|
7765
|
+
};
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7768
|
+
async function repairUmlautsViaLLM(db, input8, opts = {}) {
|
|
7769
|
+
const cfg = await getProviderForRole(db, "text");
|
|
7770
|
+
const endpoint = await resolveUsableTextEndpoint(db);
|
|
7771
|
+
const systemPrompt = `You are an expert editor specializing in German language orthography.
|
|
7772
|
+
Your task: given a text that may contain legacy ASCII-folded German umlauts (like 'ue' instead of '\xFC', 'ae' instead of '\xE4', 'oe' instead of '\xF6', 'Ue' instead of '\xDC', etc.), repair them back to proper German umlauts (\xE4, \xF6, \xFC, \xC4, \xD6, \xDC, \xDF) where appropriate.
|
|
7773
|
+
|
|
7774
|
+
Strict rules:
|
|
7775
|
+
- ONLY change character sequences that are clearly meant to be German umlauts (e.g. change "Ueber" to "\xDCber", "fuer" to "f\xFCr", "moeglich" to "m\xF6glich").
|
|
7776
|
+
- DO NOT change words that are correct in their current form (e.g. do NOT change "nahe" to "n\xE4he", do NOT change English words or technical terms, do NOT change names like "Ueda").
|
|
7777
|
+
- Output ONLY the repaired text. No preamble, no explanation, no markdown formatting.`;
|
|
7778
|
+
const userPrompt = `Text to repair:
|
|
7779
|
+
"""
|
|
7780
|
+
${input8.text}
|
|
7781
|
+
"""
|
|
7782
|
+
|
|
7783
|
+
Repaired text:`;
|
|
7784
|
+
const url = `${endpoint.url}/chat/completions`;
|
|
7785
|
+
const apiKey = endpoint.apiKey;
|
|
7786
|
+
const model = endpoint.model;
|
|
7787
|
+
try {
|
|
7788
|
+
const res = await fetchWithInteractiveTimeout(url, {
|
|
7789
|
+
method: "POST",
|
|
7790
|
+
headers: {
|
|
7791
|
+
"Content-Type": "application/json",
|
|
7792
|
+
Authorization: `Bearer ${apiKey}`
|
|
7793
|
+
},
|
|
7794
|
+
body: JSON.stringify({
|
|
7795
|
+
model,
|
|
7796
|
+
messages: [
|
|
7797
|
+
{ role: "system", content: systemPrompt },
|
|
7798
|
+
{ role: "user", content: userPrompt }
|
|
7799
|
+
],
|
|
7800
|
+
temperature: 0.1
|
|
7801
|
+
}),
|
|
7802
|
+
locale: cfg.locale,
|
|
7803
|
+
timeoutMs: opts.timeoutMs,
|
|
7804
|
+
hardTimeoutMs: opts.timeoutMs
|
|
7805
|
+
});
|
|
7806
|
+
const text = await readChatContent(res, "umlaut repair");
|
|
7807
|
+
return text.trim();
|
|
7808
|
+
} catch (_e) {
|
|
7809
|
+
return input8.text;
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7573
7812
|
|
|
7574
7813
|
// src/cli/adapters/source-reader.ts
|
|
7575
7814
|
var MAX_SOURCE_BYTES = 2 * 1024 * 1024;
|
|
@@ -7881,7 +8120,7 @@ function cleanHtmlText(html) {
|
|
|
7881
8120
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
7882
8121
|
}
|
|
7883
8122
|
function normalizeForComparison(str) {
|
|
7884
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8123
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
7885
8124
|
}
|
|
7886
8125
|
|
|
7887
8126
|
// src/cli/curriculum/providers/bildungsplan-bw/manifest.ts
|
|
@@ -8052,7 +8291,7 @@ function cleanHtmlText2(html) {
|
|
|
8052
8291
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
8053
8292
|
}
|
|
8054
8293
|
function normalizeForComparison2(str) {
|
|
8055
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8294
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8056
8295
|
}
|
|
8057
8296
|
|
|
8058
8297
|
// src/cli/curriculum/providers/bildungsplan-hamburg/manifest.ts
|
|
@@ -8210,7 +8449,7 @@ function cleanHtmlText3(html) {
|
|
|
8210
8449
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
8211
8450
|
}
|
|
8212
8451
|
function normalizeForComparison3(str) {
|
|
8213
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8452
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8214
8453
|
}
|
|
8215
8454
|
|
|
8216
8455
|
// src/cli/curriculum/providers/fachanforderungen-sh/manifest.ts
|
|
@@ -8367,7 +8606,7 @@ function cleanHtmlText4(html) {
|
|
|
8367
8606
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
8368
8607
|
}
|
|
8369
8608
|
function normalizeForComparison4(str) {
|
|
8370
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8609
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8371
8610
|
}
|
|
8372
8611
|
|
|
8373
8612
|
// src/cli/curriculum/providers/kerncurriculum-hessen/manifest.ts
|
|
@@ -8561,7 +8800,7 @@ function cleanHtmlText5(html) {
|
|
|
8561
8800
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
8562
8801
|
}
|
|
8563
8802
|
function normalizeForComparison5(str) {
|
|
8564
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8803
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8565
8804
|
}
|
|
8566
8805
|
|
|
8567
8806
|
// src/cli/curriculum/providers/kerncurriculum-niedersachsen/manifest.ts
|
|
@@ -8733,7 +8972,7 @@ function cleanHtmlText6(html) {
|
|
|
8733
8972
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
8734
8973
|
}
|
|
8735
8974
|
function normalizeForComparison6(str) {
|
|
8736
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8975
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8737
8976
|
}
|
|
8738
8977
|
|
|
8739
8978
|
// src/cli/curriculum/providers/kernlehrplan-nrw/manifest.ts
|
|
@@ -8948,7 +9187,7 @@ function cleanHtmlText7(html) {
|
|
|
8948
9187
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
8949
9188
|
}
|
|
8950
9189
|
function normalizeForComparison7(str) {
|
|
8951
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9190
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
8952
9191
|
}
|
|
8953
9192
|
|
|
8954
9193
|
// src/cli/curriculum/providers/lehrplaene-rp/manifest.ts
|
|
@@ -9105,7 +9344,7 @@ function cleanHtmlText8(html) {
|
|
|
9105
9344
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
9106
9345
|
}
|
|
9107
9346
|
function normalizeForComparison8(str) {
|
|
9108
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9347
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9109
9348
|
}
|
|
9110
9349
|
|
|
9111
9350
|
// src/cli/curriculum/providers/lehrplan-saarland/manifest.ts
|
|
@@ -9262,7 +9501,7 @@ function cleanHtmlText9(html) {
|
|
|
9262
9501
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
9263
9502
|
}
|
|
9264
9503
|
function normalizeForComparison9(str) {
|
|
9265
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9504
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9266
9505
|
}
|
|
9267
9506
|
|
|
9268
9507
|
// src/cli/curriculum/providers/lehrplan-sachsen/manifest.ts
|
|
@@ -9447,7 +9686,7 @@ function cleanHtmlText10(html) {
|
|
|
9447
9686
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
9448
9687
|
}
|
|
9449
9688
|
function normalizeForComparison10(str) {
|
|
9450
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9689
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9451
9690
|
}
|
|
9452
9691
|
|
|
9453
9692
|
// src/cli/curriculum/providers/lehrplan-thueringen/manifest.ts
|
|
@@ -9602,7 +9841,7 @@ function cleanHtmlText11(html) {
|
|
|
9602
9841
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
9603
9842
|
}
|
|
9604
9843
|
function normalizeForComparison11(str) {
|
|
9605
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9844
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9606
9845
|
}
|
|
9607
9846
|
|
|
9608
9847
|
// src/cli/curriculum/providers/lehrplanplus-bayern/manifest.ts
|
|
@@ -9846,7 +10085,7 @@ function cleanHtmlText12(html) {
|
|
|
9846
10085
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
9847
10086
|
}
|
|
9848
10087
|
function normalizeForComparison12(str) {
|
|
9849
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
10088
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
9850
10089
|
}
|
|
9851
10090
|
|
|
9852
10091
|
// src/cli/curriculum/providers/rahmenlehrplan-berlin-brandenburg/manifest.ts
|
|
@@ -10031,7 +10270,7 @@ function cleanHtmlText13(html) {
|
|
|
10031
10270
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
10032
10271
|
}
|
|
10033
10272
|
function normalizeForComparison13(str) {
|
|
10034
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
10273
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
10035
10274
|
}
|
|
10036
10275
|
|
|
10037
10276
|
// src/cli/curriculum/providers/rahmenplan-mv/manifest.ts
|
|
@@ -10188,7 +10427,7 @@ function cleanHtmlText14(html) {
|
|
|
10188
10427
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
10189
10428
|
}
|
|
10190
10429
|
function normalizeForComparison14(str) {
|
|
10191
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
10430
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
10192
10431
|
}
|
|
10193
10432
|
|
|
10194
10433
|
// src/cli/curriculum/providers/rahmenrichtlinien-st/manifest.ts
|
|
@@ -10343,7 +10582,7 @@ function cleanHtmlText15(html) {
|
|
|
10343
10582
|
return text.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
10344
10583
|
}
|
|
10345
10584
|
function normalizeForComparison15(str) {
|
|
10346
|
-
return str.toLowerCase().replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
10585
|
+
return str.toLowerCase().replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss").replace(/&[a-z0-9#]+;/gi, "").replace(/[^a-z0-9]/gi, "");
|
|
10347
10586
|
}
|
|
10348
10587
|
|
|
10349
10588
|
// src/cli/curriculum/registry.ts
|
|
@@ -10541,7 +10780,8 @@ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
|
|
|
10541
10780
|
const queryText = embeddingContentForToken({
|
|
10542
10781
|
concept: candidate.concept,
|
|
10543
10782
|
question: candidate.question ?? null,
|
|
10544
|
-
domain: candidate.domain ?? ""
|
|
10783
|
+
domain: candidate.domain ?? "",
|
|
10784
|
+
title: candidate.title ?? null
|
|
10545
10785
|
});
|
|
10546
10786
|
const q2 = await embed(db, queryText);
|
|
10547
10787
|
if (!q2) {
|
|
@@ -10559,9 +10799,7 @@ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
|
|
|
10559
10799
|
);
|
|
10560
10800
|
}
|
|
10561
10801
|
}
|
|
10562
|
-
const
|
|
10563
|
-
const parsed = thresholdStr ? Number.parseFloat(thresholdStr) : Number.NaN;
|
|
10564
|
-
const threshold = Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.85;
|
|
10802
|
+
const threshold = await resolveDedupThreshold(db);
|
|
10565
10803
|
const hits = await searchTokensHybrid(db, queryText, {
|
|
10566
10804
|
queryEmbedding: q2.vector,
|
|
10567
10805
|
model: q2.model,
|
|
@@ -10574,12 +10812,23 @@ async function findPossibleDuplicates(db, candidate, embed = embedQuery) {
|
|
|
10574
10812
|
results.push({
|
|
10575
10813
|
slug: hit.slug,
|
|
10576
10814
|
concept: hit.concept,
|
|
10815
|
+
title: hit.title,
|
|
10577
10816
|
similarity: hit.similarity
|
|
10578
10817
|
});
|
|
10579
10818
|
}
|
|
10580
10819
|
}
|
|
10581
10820
|
return results;
|
|
10582
10821
|
}
|
|
10822
|
+
async function resolveDedupThreshold(db) {
|
|
10823
|
+
const thresholdStr = await getSetting(db, "search.dedup_threshold");
|
|
10824
|
+
const parsed = thresholdStr ? Number.parseFloat(thresholdStr) : Number.NaN;
|
|
10825
|
+
return Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.85;
|
|
10826
|
+
}
|
|
10827
|
+
async function resolveSuggestMinSimilarity(db) {
|
|
10828
|
+
const minStr = await getSetting(db, "search.suggest_min_similarity");
|
|
10829
|
+
const parsed = minStr ? Number.parseFloat(minStr) : Number.NaN;
|
|
10830
|
+
return Number.isFinite(parsed) && parsed > 0 && parsed <= 1 ? parsed : 0.45;
|
|
10831
|
+
}
|
|
10583
10832
|
|
|
10584
10833
|
// src/cli/llm/vision.ts
|
|
10585
10834
|
import { randomBytes } from "crypto";
|
|
@@ -12327,10 +12576,12 @@ bridgeCommand.command("add-token").description("Create a token + card from JSON
|
|
|
12327
12576
|
const possibleDuplicates = await findPossibleDuplicates(db, {
|
|
12328
12577
|
concept: data?.concept,
|
|
12329
12578
|
question: data?.question ?? null,
|
|
12330
|
-
domain: data?.domain
|
|
12579
|
+
domain: data?.domain,
|
|
12580
|
+
title: data?.title ?? null
|
|
12331
12581
|
});
|
|
12332
12582
|
const token = await createToken(db, {
|
|
12333
12583
|
slug: data?.slug,
|
|
12584
|
+
title: data?.title,
|
|
12334
12585
|
concept: data?.concept,
|
|
12335
12586
|
domain: data?.domain,
|
|
12336
12587
|
bloom_level: data?.bloom_level ?? 1,
|
|
@@ -12410,6 +12661,8 @@ bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a
|
|
|
12410
12661
|
const card = await getCard(db, t2.id, userId);
|
|
12411
12662
|
tokens.push({
|
|
12412
12663
|
slug: t2.slug,
|
|
12664
|
+
title: t2.title,
|
|
12665
|
+
display_title: getDisplayTitle(t2),
|
|
12413
12666
|
concept: t2.concept,
|
|
12414
12667
|
domain: t2.domain,
|
|
12415
12668
|
bloom_level: t2.bloom_level,
|
|
@@ -12428,6 +12681,118 @@ bridgeCommand.command("relevant-tokens").description("Find tokens relevant to a
|
|
|
12428
12681
|
});
|
|
12429
12682
|
});
|
|
12430
12683
|
});
|
|
12684
|
+
bridgeCommand.command("suggest-foundations").description("Propose existing tokens as foundation/prerequisite candidates").option("--user <id>", "User ID (default: whoami)").action(async (_opts) => {
|
|
12685
|
+
await withDb2(async (db) => {
|
|
12686
|
+
let raw;
|
|
12687
|
+
if (isServeMode) {
|
|
12688
|
+
raw = serveStdinPayload ?? "";
|
|
12689
|
+
} else {
|
|
12690
|
+
const chunks = [];
|
|
12691
|
+
for await (const chunk of process.stdin) {
|
|
12692
|
+
chunks.push(chunk);
|
|
12693
|
+
}
|
|
12694
|
+
raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
12695
|
+
}
|
|
12696
|
+
if (!raw) {
|
|
12697
|
+
jsonError("No input received on stdin. Pipe JSON.");
|
|
12698
|
+
}
|
|
12699
|
+
let data;
|
|
12700
|
+
try {
|
|
12701
|
+
data = JSON.parse(raw);
|
|
12702
|
+
} catch {
|
|
12703
|
+
jsonError("Invalid JSON input");
|
|
12704
|
+
}
|
|
12705
|
+
let queryText = "";
|
|
12706
|
+
let targetTokenId;
|
|
12707
|
+
let targetBloomLevel;
|
|
12708
|
+
let targetJson = null;
|
|
12709
|
+
if (data?.slug !== void 0) {
|
|
12710
|
+
if (typeof data.slug !== "string" || data.slug.trim() === "") {
|
|
12711
|
+
jsonError("Invalid slug");
|
|
12712
|
+
}
|
|
12713
|
+
const token = await getTokenBySlug(db, data.slug);
|
|
12714
|
+
if (!token) {
|
|
12715
|
+
jsonError(`Token not found: ${data.slug}`);
|
|
12716
|
+
}
|
|
12717
|
+
queryText = embeddingContentForToken(token);
|
|
12718
|
+
targetTokenId = token.id;
|
|
12719
|
+
targetBloomLevel = token.bloom_level;
|
|
12720
|
+
targetJson = { slug: token.slug };
|
|
12721
|
+
} else {
|
|
12722
|
+
if (!data?.concept || typeof data.concept !== "string" || data.concept.trim() === "") {
|
|
12723
|
+
jsonError("JSON must include a non-empty 'slug' or 'concept' field");
|
|
12724
|
+
}
|
|
12725
|
+
queryText = embeddingContentForToken({
|
|
12726
|
+
concept: data.concept,
|
|
12727
|
+
question: typeof data.question === "string" ? data.question : null,
|
|
12728
|
+
domain: typeof data.domain === "string" ? data.domain : "",
|
|
12729
|
+
title: typeof data.title === "string" ? data.title : null
|
|
12730
|
+
});
|
|
12731
|
+
if (data.bloom_level !== void 0) {
|
|
12732
|
+
if (typeof data.bloom_level !== "number" || !Number.isInteger(data.bloom_level) || data.bloom_level < 1 || data.bloom_level > 5) {
|
|
12733
|
+
jsonError("bloom_level must be an integer between 1 and 5");
|
|
12734
|
+
}
|
|
12735
|
+
targetBloomLevel = data.bloom_level;
|
|
12736
|
+
}
|
|
12737
|
+
}
|
|
12738
|
+
let limit = data?.limit ?? 5;
|
|
12739
|
+
if (typeof limit !== "number" || limit <= 0 || !Number.isInteger(limit)) {
|
|
12740
|
+
limit = 5;
|
|
12741
|
+
}
|
|
12742
|
+
if (limit > 20) {
|
|
12743
|
+
limit = 20;
|
|
12744
|
+
}
|
|
12745
|
+
const q2 = await embedQuery(db, queryText);
|
|
12746
|
+
if (q2 === null) {
|
|
12747
|
+
jsonOut2({
|
|
12748
|
+
semantic: false,
|
|
12749
|
+
target: targetJson,
|
|
12750
|
+
suggestions: []
|
|
12751
|
+
});
|
|
12752
|
+
return;
|
|
12753
|
+
}
|
|
12754
|
+
try {
|
|
12755
|
+
await ensureTokenEmbeddings(db, {
|
|
12756
|
+
limit: 100,
|
|
12757
|
+
dims: q2.vector.length
|
|
12758
|
+
});
|
|
12759
|
+
} catch {
|
|
12760
|
+
}
|
|
12761
|
+
const maxSimilarity = await resolveDedupThreshold(db);
|
|
12762
|
+
const minSimilarity = await resolveSuggestMinSimilarity(db);
|
|
12763
|
+
if (minSimilarity >= maxSimilarity) {
|
|
12764
|
+
jsonOut2({
|
|
12765
|
+
semantic: true,
|
|
12766
|
+
target: targetJson,
|
|
12767
|
+
suggestions: []
|
|
12768
|
+
});
|
|
12769
|
+
return;
|
|
12770
|
+
}
|
|
12771
|
+
const suggestions = await suggestFoundations(db, {
|
|
12772
|
+
queryEmbedding: q2.vector,
|
|
12773
|
+
model: q2.model,
|
|
12774
|
+
targetTokenId,
|
|
12775
|
+
targetBloomLevel,
|
|
12776
|
+
limit,
|
|
12777
|
+
minSimilarity,
|
|
12778
|
+
maxSimilarity
|
|
12779
|
+
});
|
|
12780
|
+
jsonOut2({
|
|
12781
|
+
semantic: true,
|
|
12782
|
+
target: targetJson,
|
|
12783
|
+
suggestions: suggestions.map((s) => ({
|
|
12784
|
+
slug: s.token.slug,
|
|
12785
|
+
concept: s.token.concept,
|
|
12786
|
+
domain: s.token.domain,
|
|
12787
|
+
bloom_level: s.token.bloom_level,
|
|
12788
|
+
similarity: s.similarity,
|
|
12789
|
+
already_prerequisite: s.alreadyPrerequisite,
|
|
12790
|
+
would_create_cycle: s.wouldCreateCycle,
|
|
12791
|
+
bloom_above_target: s.bloomAboveTarget
|
|
12792
|
+
}))
|
|
12793
|
+
});
|
|
12794
|
+
});
|
|
12795
|
+
});
|
|
12431
12796
|
bridgeCommand.command("discover-skills").description(
|
|
12432
12797
|
"Analyze monitor logs across sessions to discover recurring patterns"
|
|
12433
12798
|
).option(
|
|
@@ -13593,12 +13958,18 @@ bridgeCommand.command("list-tokens").description(
|
|
|
13593
13958
|
).option(
|
|
13594
13959
|
"--user <id>",
|
|
13595
13960
|
"User ID (default: whoami) \u2014 when provided, includes personal card info"
|
|
13596
|
-
).option("--domain <domain>", "Filter by domain").
|
|
13961
|
+
).option("--domain <domain>", "Filter by exact domain").option(
|
|
13962
|
+
"--domain-prefix <prefix>",
|
|
13963
|
+
"Filter by domain prefix (e.g. docuware-cops) \u2014 uses / separator for hierarchy"
|
|
13964
|
+
).action(async (opts) => {
|
|
13597
13965
|
await withDb2(async (db) => {
|
|
13598
13966
|
const userId = opts.user ? await resolveUser(opts, db, { json: true }) : void 0;
|
|
13967
|
+
const listOpts = {};
|
|
13968
|
+
if (opts.domain) listOpts.domain = opts.domain;
|
|
13969
|
+
if (opts.domainPrefix) listOpts.domainPrefix = opts.domainPrefix;
|
|
13599
13970
|
const tokens = await listTokens(
|
|
13600
13971
|
db,
|
|
13601
|
-
|
|
13972
|
+
Object.keys(listOpts).length ? listOpts : void 0
|
|
13602
13973
|
);
|
|
13603
13974
|
const cardMap = /* @__PURE__ */ new Map();
|
|
13604
13975
|
if (userId && tokens.length > 0) {
|
|
@@ -13615,6 +13986,8 @@ bridgeCommand.command("list-tokens").description(
|
|
|
13615
13986
|
return {
|
|
13616
13987
|
id: t2.id,
|
|
13617
13988
|
slug: t2.slug,
|
|
13989
|
+
title: t2.title,
|
|
13990
|
+
display_title: getDisplayTitle(t2),
|
|
13618
13991
|
concept: t2.concept,
|
|
13619
13992
|
domain: t2.domain,
|
|
13620
13993
|
bloomLevel: t2.bloom_level,
|
|
@@ -13648,6 +14021,8 @@ bridgeCommand.command("get-neighborhood").description(
|
|
|
13648
14021
|
const mapToken = (nt) => ({
|
|
13649
14022
|
id: nt.id,
|
|
13650
14023
|
slug: nt.slug,
|
|
14024
|
+
title: nt.title,
|
|
14025
|
+
display_title: getDisplayTitle(nt),
|
|
13651
14026
|
concept: nt.concept,
|
|
13652
14027
|
domain: nt.domain,
|
|
13653
14028
|
bloomLevel: nt.bloom_level,
|
|
@@ -13679,7 +14054,7 @@ bridgeCommand.command("personal-card-list").description("List and search persona
|
|
|
13679
14054
|
jsonOut2({ cards });
|
|
13680
14055
|
});
|
|
13681
14056
|
});
|
|
13682
|
-
bridgeCommand.command("personal-card-create").description("Atomically create a token and its personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--concept <concept>", "Concept description / answer").option("--domain <domain>", "Knowledge category / domain", "").option("--question <question>", "Question prompt for recall").option("--source-link <link>", "Source file path or reference URL").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option(
|
|
14057
|
+
bridgeCommand.command("personal-card-create").description("Atomically create a token and its personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--concept <concept>", "Concept description / answer").option("--title <title>", "Human-friendly display title").option("--domain <domain>", "Knowledge category / domain", "").option("--question <question>", "Question prompt for recall").option("--source-link <link>", "Source file path or reference URL").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option(
|
|
13683
14058
|
"--mode <mode>",
|
|
13684
14059
|
"Symbiosis mode: shadowing | copilot | autonomy | none",
|
|
13685
14060
|
"none"
|
|
@@ -13707,6 +14082,7 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
|
|
|
13707
14082
|
const { token, card } = await db.transaction(async (tx) => {
|
|
13708
14083
|
const createdToken = await createToken(tx, {
|
|
13709
14084
|
slug,
|
|
14085
|
+
title: opts.title,
|
|
13710
14086
|
concept: opts.concept,
|
|
13711
14087
|
domain: opts.domain,
|
|
13712
14088
|
bloom_level: bloom,
|
|
@@ -13723,6 +14099,8 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
|
|
|
13723
14099
|
token: {
|
|
13724
14100
|
id: token.id,
|
|
13725
14101
|
slug: token.slug,
|
|
14102
|
+
title: token.title,
|
|
14103
|
+
display_title: getDisplayTitle(token),
|
|
13726
14104
|
concept: token.concept,
|
|
13727
14105
|
domain: token.domain,
|
|
13728
14106
|
bloomLevel: token.bloom_level,
|
|
@@ -13744,13 +14122,14 @@ bridgeCommand.command("personal-card-create").description("Atomically create a t
|
|
|
13744
14122
|
});
|
|
13745
14123
|
});
|
|
13746
14124
|
});
|
|
13747
|
-
bridgeCommand.command("personal-card-update").description("Update the mutable token fields of a personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--slug <slug>", "Token slug to update").option("--concept <concept>", "Updated concept text").option("--domain <domain>", "Updated domain / category").option("--bloom <level>", "Updated Bloom taxonomy level (1-5)").option("--context <context>", "Updated context").option(
|
|
14125
|
+
bridgeCommand.command("personal-card-update").description("Update the mutable token fields of a personal card (JSON)").option("--user <id>", "User ID (default: whoami)").requiredOption("--slug <slug>", "Token slug to update").option("--title <title>", "Updated display title").option("--concept <concept>", "Updated concept text").option("--domain <domain>", "Updated domain / category").option("--bloom <level>", "Updated Bloom taxonomy level (1-5)").option("--context <context>", "Updated context").option(
|
|
13748
14126
|
"--mode <mode>",
|
|
13749
14127
|
"Updated symbiosis mode: shadowing | copilot | autonomy | none"
|
|
13750
14128
|
).option("--source-link <link>", "Updated source link").option("--question <question>", "Updated question text").action(async (opts) => {
|
|
13751
14129
|
await withDb2(async (db) => {
|
|
13752
14130
|
const userId = await resolveUser(opts, db, { json: true });
|
|
13753
14131
|
const updates = {};
|
|
14132
|
+
if (opts.title !== void 0) updates.title = opts.title;
|
|
13754
14133
|
if (opts.concept !== void 0) updates.concept = opts.concept;
|
|
13755
14134
|
if (opts.domain !== void 0) updates.domain = opts.domain;
|
|
13756
14135
|
if (opts.bloom !== void 0) {
|
|
@@ -13792,6 +14171,8 @@ bridgeCommand.command("personal-card-update").description("Update the mutable to
|
|
|
13792
14171
|
token: {
|
|
13793
14172
|
id: token.id,
|
|
13794
14173
|
slug: token.slug,
|
|
14174
|
+
title: token.title,
|
|
14175
|
+
display_title: getDisplayTitle(token),
|
|
13795
14176
|
concept: token.concept,
|
|
13796
14177
|
domain: token.domain,
|
|
13797
14178
|
bloomLevel: token.bloom_level,
|
|
@@ -13824,7 +14205,12 @@ bridgeCommand.command("personal-card-remove").description(
|
|
|
13824
14205
|
success: true,
|
|
13825
14206
|
preview: true,
|
|
13826
14207
|
requiresConfirmation: true,
|
|
13827
|
-
token: {
|
|
14208
|
+
token: {
|
|
14209
|
+
id: token.id,
|
|
14210
|
+
slug: token.slug,
|
|
14211
|
+
title: token.title,
|
|
14212
|
+
display_title: getDisplayTitle(token)
|
|
14213
|
+
},
|
|
13828
14214
|
impact
|
|
13829
14215
|
});
|
|
13830
14216
|
return;
|
|
@@ -13853,7 +14239,12 @@ bridgeCommand.command("personal-card-delete").description("Hard-delete a token a
|
|
|
13853
14239
|
success: true,
|
|
13854
14240
|
preview: true,
|
|
13855
14241
|
requiresConfirmation: true,
|
|
13856
|
-
token: {
|
|
14242
|
+
token: {
|
|
14243
|
+
id: token.id,
|
|
14244
|
+
slug: token.slug,
|
|
14245
|
+
title: token.title,
|
|
14246
|
+
display_title: getDisplayTitle(token)
|
|
14247
|
+
},
|
|
13857
14248
|
impact
|
|
13858
14249
|
});
|
|
13859
14250
|
return;
|
|
@@ -14745,11 +15136,567 @@ async function setupTurso(urlArg, tokenArg, mode) {
|
|
|
14745
15136
|
}
|
|
14746
15137
|
}
|
|
14747
15138
|
|
|
15139
|
+
// src/cli/commands/doctor.ts
|
|
15140
|
+
import { Command as Command5 } from "commander";
|
|
15141
|
+
function shouldApplyFixes(opts) {
|
|
15142
|
+
return opts.fix === true && opts.dryRun !== true;
|
|
15143
|
+
}
|
|
15144
|
+
async function confirmDeterministicChanges(opts, message) {
|
|
15145
|
+
if (opts.yes) return true;
|
|
15146
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
15147
|
+
console.error(
|
|
15148
|
+
"Cannot request confirmation in a non-interactive terminal. Re-run with --yes to apply deterministic fixes."
|
|
15149
|
+
);
|
|
15150
|
+
return false;
|
|
15151
|
+
}
|
|
15152
|
+
const { confirm: confirm5 } = await import("@inquirer/prompts");
|
|
15153
|
+
return confirm5({ message, default: false });
|
|
15154
|
+
}
|
|
15155
|
+
function canRunInteractiveChoices(opts, taskName) {
|
|
15156
|
+
if (opts.yes) {
|
|
15157
|
+
console.error(
|
|
15158
|
+
`--yes cannot choose how to resolve ${taskName}. Re-run without --yes in an interactive terminal.`
|
|
15159
|
+
);
|
|
15160
|
+
return false;
|
|
15161
|
+
}
|
|
15162
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
15163
|
+
console.error(
|
|
15164
|
+
`${taskName} fixes require an interactive terminal because each change needs an explicit choice.`
|
|
15165
|
+
);
|
|
15166
|
+
return false;
|
|
15167
|
+
}
|
|
15168
|
+
return true;
|
|
15169
|
+
}
|
|
15170
|
+
function getWeakTitleReason(t2) {
|
|
15171
|
+
const title = (t2.title || "").trim();
|
|
15172
|
+
if (!title) {
|
|
15173
|
+
return "Missing or empty title";
|
|
15174
|
+
}
|
|
15175
|
+
if (title === t2.slug) {
|
|
15176
|
+
return "Title is equal to technical slug";
|
|
15177
|
+
}
|
|
15178
|
+
if (title.length < 3) {
|
|
15179
|
+
return "Title is suspiciously short";
|
|
15180
|
+
}
|
|
15181
|
+
if (title.length > 100) {
|
|
15182
|
+
return "Title is too long (> 100 chars)";
|
|
15183
|
+
}
|
|
15184
|
+
if (t2.domain) {
|
|
15185
|
+
const domParts = t2.domain.split(/[-/]/).filter((p) => p.length > 2);
|
|
15186
|
+
const titleLower2 = title.toLowerCase();
|
|
15187
|
+
for (const part of domParts) {
|
|
15188
|
+
if (titleLower2.includes(part.toLowerCase())) {
|
|
15189
|
+
return `Domain echo (contains '${part}' from domain '${t2.domain}')`;
|
|
15190
|
+
}
|
|
15191
|
+
}
|
|
15192
|
+
}
|
|
15193
|
+
const conceptClean = (t2.concept || "").trim().toLowerCase();
|
|
15194
|
+
const titleLower = title.toLowerCase();
|
|
15195
|
+
if (conceptClean.startsWith(titleLower) && t2.concept.length > title.length + 5 && !/[.!?]/.test(title)) {
|
|
15196
|
+
const titleWords = titleLower.split(/\s+/).length;
|
|
15197
|
+
if (titleWords <= 5) {
|
|
15198
|
+
return "Concept-prefix copy";
|
|
15199
|
+
}
|
|
15200
|
+
}
|
|
15201
|
+
if (title.endsWith("?") || /^(what|how|why|who|where|when|which|is|are|was|were|can|do|does|did|ueber|fuer|nahe)\b/i.test(
|
|
15202
|
+
title
|
|
15203
|
+
)) {
|
|
15204
|
+
return "Question stump or interrogative structure";
|
|
15205
|
+
}
|
|
15206
|
+
if (title.length > 3 && /[a-zA-Z]/.test(title)) {
|
|
15207
|
+
if (title === title.toUpperCase()) {
|
|
15208
|
+
return "All uppercase";
|
|
15209
|
+
}
|
|
15210
|
+
if (title === title.toLowerCase()) {
|
|
15211
|
+
return "All lowercase";
|
|
15212
|
+
}
|
|
15213
|
+
}
|
|
15214
|
+
return null;
|
|
15215
|
+
}
|
|
15216
|
+
function stripLeadingDomainWords(value, domain) {
|
|
15217
|
+
let result = value.trim();
|
|
15218
|
+
for (const word of domain.split(/[-/]/).filter(Boolean)) {
|
|
15219
|
+
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15220
|
+
result = result.replace(new RegExp(`^${escaped}\\s+`, "i"), "");
|
|
15221
|
+
}
|
|
15222
|
+
return result.trim();
|
|
15223
|
+
}
|
|
15224
|
+
function truncateTitle(value) {
|
|
15225
|
+
const trimmed = value.trim().replace(/^["']+|["']+$/g, "");
|
|
15226
|
+
return trimmed.length <= 80 ? trimmed : `${trimmed.substring(0, 77).trimEnd()}...`;
|
|
15227
|
+
}
|
|
15228
|
+
function titleFromSlug(token) {
|
|
15229
|
+
return getShortSlug(token.slug, token.domain).split(/[-_]+/).filter(Boolean).map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(" ");
|
|
15230
|
+
}
|
|
15231
|
+
function validatedTitleProposal(token, raw) {
|
|
15232
|
+
const candidates = [
|
|
15233
|
+
raw,
|
|
15234
|
+
(token.concept || "").split(/[.;]/)[0] || "",
|
|
15235
|
+
titleFromSlug(token)
|
|
15236
|
+
];
|
|
15237
|
+
for (const candidate of candidates) {
|
|
15238
|
+
const normalized = truncateTitle(
|
|
15239
|
+
stripLeadingDomainWords(candidate, token.domain || "")
|
|
15240
|
+
);
|
|
15241
|
+
if (normalized.length > 0 && getWeakTitleReason({ ...token, title: normalized }) === null) {
|
|
15242
|
+
return normalized;
|
|
15243
|
+
}
|
|
15244
|
+
}
|
|
15245
|
+
return null;
|
|
15246
|
+
}
|
|
15247
|
+
var doctorTasks = [
|
|
15248
|
+
{
|
|
15249
|
+
name: "titles",
|
|
15250
|
+
description: "Backfill missing/weak human-friendly titles (no domain echoes, descriptive names).",
|
|
15251
|
+
run: async (db, opts) => {
|
|
15252
|
+
const tokens = await listTokens(db, {});
|
|
15253
|
+
const needing = tokens.map((t2) => {
|
|
15254
|
+
const reason = getWeakTitleReason(t2);
|
|
15255
|
+
return reason ? { token: t2, reason } : null;
|
|
15256
|
+
}).filter((x) => x !== null);
|
|
15257
|
+
console.log(
|
|
15258
|
+
`Found ${needing.length} tokens needing title work (of ${tokens.length}).`
|
|
15259
|
+
);
|
|
15260
|
+
if (needing.length === 0) return;
|
|
15261
|
+
const cfg = await getLlmConfig(db);
|
|
15262
|
+
const useLLM = cfg.enabled && !opts.noLlm;
|
|
15263
|
+
console.log(
|
|
15264
|
+
`Title generation mode: ${useLLM ? "LLM" : "Heuristic fallback"}${opts.timeoutMs ? ` (timeout: ${opts.timeoutMs}ms)` : ""}`
|
|
15265
|
+
);
|
|
15266
|
+
console.log("Generating proposed titles...");
|
|
15267
|
+
const proposals = [];
|
|
15268
|
+
let rejected = 0;
|
|
15269
|
+
for (let i = 0; i < needing.length; i++) {
|
|
15270
|
+
const item = needing[i];
|
|
15271
|
+
process.stdout.write(
|
|
15272
|
+
`\r [${i + 1}/${needing.length}] ${item.token.slug.slice(0, 40)}...`
|
|
15273
|
+
);
|
|
15274
|
+
let rawProposal = "";
|
|
15275
|
+
if (useLLM) {
|
|
15276
|
+
try {
|
|
15277
|
+
const gen = await generateTitleViaLLM(
|
|
15278
|
+
db,
|
|
15279
|
+
{
|
|
15280
|
+
slug: item.token.slug,
|
|
15281
|
+
concept: item.token.concept,
|
|
15282
|
+
domain: item.token.domain,
|
|
15283
|
+
question: item.token.question,
|
|
15284
|
+
context: item.token.context
|
|
15285
|
+
},
|
|
15286
|
+
{ timeoutMs: opts.timeoutMs }
|
|
15287
|
+
);
|
|
15288
|
+
rawProposal = gen.text;
|
|
15289
|
+
} catch (_e) {
|
|
15290
|
+
rawProposal = (item.token.concept || "").split(/[.;]/)[0]?.trim() || item.token.slug.replace(/-/g, " ");
|
|
15291
|
+
}
|
|
15292
|
+
} else {
|
|
15293
|
+
rawProposal = (item.token.concept || "").split(/[.;]/)[0]?.trim() || item.token.slug.replace(/-/g, " ");
|
|
15294
|
+
}
|
|
15295
|
+
const proposed = validatedTitleProposal(item.token, rawProposal);
|
|
15296
|
+
if (!proposed) {
|
|
15297
|
+
rejected++;
|
|
15298
|
+
continue;
|
|
15299
|
+
}
|
|
15300
|
+
proposals.push({
|
|
15301
|
+
token: item.token,
|
|
15302
|
+
oldTitle: item.token.title,
|
|
15303
|
+
newTitle: proposed,
|
|
15304
|
+
reason: item.reason
|
|
15305
|
+
});
|
|
15306
|
+
}
|
|
15307
|
+
process.stdout.write("\n");
|
|
15308
|
+
if (rejected > 0) {
|
|
15309
|
+
console.warn(
|
|
15310
|
+
`Skipped ${rejected} token(s) because no proposal passed title quality validation.`
|
|
15311
|
+
);
|
|
15312
|
+
}
|
|
15313
|
+
if (proposals.length === 0) return;
|
|
15314
|
+
if (!shouldApplyFixes(opts)) {
|
|
15315
|
+
console.log("\nProposed changes (Dry run):");
|
|
15316
|
+
for (const prop of proposals) {
|
|
15317
|
+
console.log(
|
|
15318
|
+
` ${prop.token.slug}: "${prop.oldTitle}" -> "${prop.newTitle}" (Reason: ${prop.reason})`
|
|
15319
|
+
);
|
|
15320
|
+
}
|
|
15321
|
+
console.log("\nRun with --fix to apply these changes.");
|
|
15322
|
+
return;
|
|
15323
|
+
}
|
|
15324
|
+
if (!opts.yes) {
|
|
15325
|
+
console.log("\nProposed changes:");
|
|
15326
|
+
for (const prop of proposals) {
|
|
15327
|
+
console.log(
|
|
15328
|
+
` ${prop.token.slug}: "${prop.oldTitle}" -> "${prop.newTitle}" (Reason: ${prop.reason})`
|
|
15329
|
+
);
|
|
15330
|
+
}
|
|
15331
|
+
}
|
|
15332
|
+
const confirmed = await confirmDeterministicChanges(
|
|
15333
|
+
opts,
|
|
15334
|
+
`Apply these ${proposals.length} proposed titles?`
|
|
15335
|
+
);
|
|
15336
|
+
if (!confirmed) {
|
|
15337
|
+
console.log("Cancelled. No titles were updated.");
|
|
15338
|
+
return;
|
|
15339
|
+
}
|
|
15340
|
+
let fixed = 0;
|
|
15341
|
+
for (const prop of proposals) {
|
|
15342
|
+
await updateToken(db, prop.token.slug, { title: prop.newTitle });
|
|
15343
|
+
fixed++;
|
|
15344
|
+
console.log(`Set title for ${prop.token.slug} -> ${prop.newTitle}`);
|
|
15345
|
+
}
|
|
15346
|
+
console.log(`Successfully fixed ${fixed} titles.`);
|
|
15347
|
+
}
|
|
15348
|
+
},
|
|
15349
|
+
{
|
|
15350
|
+
name: "texts",
|
|
15351
|
+
description: "Repair legacy ASCII umlauts in prose fields (question/concept/context).",
|
|
15352
|
+
run: async (db, opts) => {
|
|
15353
|
+
const tokens = await listTokens(db, {});
|
|
15354
|
+
const cfg = await getLlmConfig(db);
|
|
15355
|
+
const useLLM = cfg.enabled && !opts.noLlm;
|
|
15356
|
+
const replacements = [
|
|
15357
|
+
[/\bUeber\b/g, "\xDCber"],
|
|
15358
|
+
[/\bueber\b/g, "\xFCber"],
|
|
15359
|
+
[/\bFuer\b/g, "F\xFCr"],
|
|
15360
|
+
[/\bfuer\b/g, "f\xFCr"],
|
|
15361
|
+
[/\bMuehe\b/g, "M\xFChe"],
|
|
15362
|
+
[/\bmuehe\b/g, "m\xFChe"],
|
|
15363
|
+
[/\bKuer\b/g, "K\xFCr"],
|
|
15364
|
+
[/\bkuer\b/g, "k\xFCr"],
|
|
15365
|
+
[/\bUebung\b/g, "\xDCbung"],
|
|
15366
|
+
[/\buebung\b/g, "\xFCbung"],
|
|
15367
|
+
[/\bMoeglich\b/g, "M\xF6glich"],
|
|
15368
|
+
[/\bmoeglich\b/g, "m\xF6glich"],
|
|
15369
|
+
[/\bMoeglichkeiten\b/g, "M\xF6glichkeiten"],
|
|
15370
|
+
[/\bmoeglichkeiten\b/g, "m\xF6glichkeiten"],
|
|
15371
|
+
[/\bSchoen\b/g, "Sch\xF6n"],
|
|
15372
|
+
[/\bschoen\b/g, "sch\xF6n"],
|
|
15373
|
+
[/\bKoennen\b/g, "K\xF6nnen"],
|
|
15374
|
+
[/\bkoennen\b/g, "k\xF6nnen"],
|
|
15375
|
+
[/\bMuessen\b/g, "M\xFCssen"],
|
|
15376
|
+
[/\bmuessen\b/g, "m\xFCssen"],
|
|
15377
|
+
[/\bDuerfen\b/g, "D\xFCrfen"],
|
|
15378
|
+
[/\bduerfen\b/g, "d\xFCrfen"],
|
|
15379
|
+
[/\bAerger\b/g, "\xC4rger"],
|
|
15380
|
+
[/\baerger\b/g, "\xE4rger"],
|
|
15381
|
+
[/\bGruende\b/g, "Gr\xFCnde"],
|
|
15382
|
+
[/\bgruende\b/g, "gr\xFCnde"]
|
|
15383
|
+
];
|
|
15384
|
+
const toFix = [];
|
|
15385
|
+
console.log("Analyzing prose text fields for legacy umlauts...");
|
|
15386
|
+
for (const t2 of tokens) {
|
|
15387
|
+
const fields = [
|
|
15388
|
+
{ key: "question", val: t2.question },
|
|
15389
|
+
{ key: "concept", val: t2.concept },
|
|
15390
|
+
{ key: "context", val: t2.context },
|
|
15391
|
+
{ key: "title", val: t2.title }
|
|
15392
|
+
];
|
|
15393
|
+
for (const f of fields) {
|
|
15394
|
+
if (!f.val || typeof f.val !== "string") continue;
|
|
15395
|
+
let repaired = f.val;
|
|
15396
|
+
if (useLLM) {
|
|
15397
|
+
const hasPossibleUmlautFold = /[aou]e/i.test(f.val);
|
|
15398
|
+
if (hasPossibleUmlautFold) {
|
|
15399
|
+
try {
|
|
15400
|
+
repaired = await repairUmlautsViaLLM(
|
|
15401
|
+
db,
|
|
15402
|
+
{ text: f.val },
|
|
15403
|
+
{ timeoutMs: opts.timeoutMs }
|
|
15404
|
+
);
|
|
15405
|
+
if (repaired === f.val) {
|
|
15406
|
+
for (const [from, to] of replacements) {
|
|
15407
|
+
repaired = repaired.replace(from, to);
|
|
15408
|
+
}
|
|
15409
|
+
}
|
|
15410
|
+
} catch (_e) {
|
|
15411
|
+
for (const [from, to] of replacements) {
|
|
15412
|
+
repaired = repaired.replace(from, to);
|
|
15413
|
+
}
|
|
15414
|
+
}
|
|
15415
|
+
}
|
|
15416
|
+
} else {
|
|
15417
|
+
for (const [from, to] of replacements) {
|
|
15418
|
+
repaired = repaired.replace(from, to);
|
|
15419
|
+
}
|
|
15420
|
+
}
|
|
15421
|
+
if (repaired !== f.val) {
|
|
15422
|
+
toFix.push({ token: t2, field: f.key, old: f.val, new: repaired });
|
|
15423
|
+
}
|
|
15424
|
+
}
|
|
15425
|
+
}
|
|
15426
|
+
console.log(`Found ${toFix.length} prose fields with legacy umlauts.`);
|
|
15427
|
+
if (toFix.length === 0) return;
|
|
15428
|
+
if (!shouldApplyFixes(opts)) {
|
|
15429
|
+
console.log("\nProposed changes (Dry run):");
|
|
15430
|
+
for (const fix of toFix) {
|
|
15431
|
+
console.log(` ${fix.token.slug} [${fix.field}]:`);
|
|
15432
|
+
console.log(` Old: "${fix.old}"`);
|
|
15433
|
+
console.log(` New: "${fix.new}"`);
|
|
15434
|
+
}
|
|
15435
|
+
console.log("\nRun with --fix to apply these changes.");
|
|
15436
|
+
return;
|
|
15437
|
+
}
|
|
15438
|
+
if (!opts.yes) {
|
|
15439
|
+
console.log("\nProposed changes:");
|
|
15440
|
+
for (const fix of toFix) {
|
|
15441
|
+
console.log(
|
|
15442
|
+
` ${fix.token.slug} [${fix.field}]: "${fix.old}" -> "${fix.new}"`
|
|
15443
|
+
);
|
|
15444
|
+
}
|
|
15445
|
+
}
|
|
15446
|
+
const confirmed = await confirmDeterministicChanges(
|
|
15447
|
+
opts,
|
|
15448
|
+
`Apply these ${toFix.length} prose fixes?`
|
|
15449
|
+
);
|
|
15450
|
+
if (!confirmed) {
|
|
15451
|
+
console.log("Cancelled. No changes made.");
|
|
15452
|
+
return;
|
|
15453
|
+
}
|
|
15454
|
+
let fixed = 0;
|
|
15455
|
+
for (const fix of toFix) {
|
|
15456
|
+
const updates = {};
|
|
15457
|
+
updates[fix.field] = fix.new;
|
|
15458
|
+
await updateToken(db, fix.token.slug, updates);
|
|
15459
|
+
fixed++;
|
|
15460
|
+
}
|
|
15461
|
+
console.log(
|
|
15462
|
+
`Fixed ${fixed} fields. (Re-embed will happen automatically for changed content.)`
|
|
15463
|
+
);
|
|
15464
|
+
}
|
|
15465
|
+
},
|
|
15466
|
+
{
|
|
15467
|
+
name: "duplicates",
|
|
15468
|
+
description: "List semantic duplicates for review/merge.",
|
|
15469
|
+
run: async (db, opts) => {
|
|
15470
|
+
const applyingFixes = shouldApplyFixes(opts);
|
|
15471
|
+
if (applyingFixes && !canRunInteractiveChoices(opts, "duplicate fixes")) {
|
|
15472
|
+
return;
|
|
15473
|
+
}
|
|
15474
|
+
const modelSetting = await getSetting(db, "llm.embedding.model");
|
|
15475
|
+
if (!modelSetting) {
|
|
15476
|
+
console.log(
|
|
15477
|
+
"No embedding model configured. Please configure an embedding model to scan duplicates."
|
|
15478
|
+
);
|
|
15479
|
+
return;
|
|
15480
|
+
}
|
|
15481
|
+
const model = canonicalEmbeddingModelId(modelSetting);
|
|
15482
|
+
if (model !== modelSetting.trim().toLowerCase()) {
|
|
15483
|
+
console.log(
|
|
15484
|
+
`Using canonical embedding model "${model}" for configured alias "${modelSetting}".`
|
|
15485
|
+
);
|
|
15486
|
+
}
|
|
15487
|
+
let coverage = await getEmbeddingCoverage(db, model);
|
|
15488
|
+
const initiallyIncomplete = coverage.missing + coverage.stale;
|
|
15489
|
+
if (initiallyIncomplete > 0) {
|
|
15490
|
+
if (applyingFixes) {
|
|
15491
|
+
console.log(
|
|
15492
|
+
`Embedding coverage is incomplete (${coverage.missing} missing, ${coverage.stale} stale). Attempting backfill...`
|
|
15493
|
+
);
|
|
15494
|
+
const result = await ensureTokenEmbeddings(db, {
|
|
15495
|
+
limit: coverage.tokens
|
|
15496
|
+
});
|
|
15497
|
+
coverage = await getEmbeddingCoverage(db, model);
|
|
15498
|
+
const remaining = coverage.missing + coverage.stale;
|
|
15499
|
+
if (remaining > 0) {
|
|
15500
|
+
console.warn(
|
|
15501
|
+
`Warning: Duplicate scan is incomplete: ${remaining} of ${coverage.tokens} active tokens still lack a fresh "${model}" embedding.${result.reason ? ` ${result.reason}` : ""}`
|
|
15502
|
+
);
|
|
15503
|
+
} else {
|
|
15504
|
+
console.log(
|
|
15505
|
+
`Embedding backfill complete (${result.embedded} updated).`
|
|
15506
|
+
);
|
|
15507
|
+
}
|
|
15508
|
+
} else {
|
|
15509
|
+
console.warn(
|
|
15510
|
+
`Warning: Duplicate scan is incomplete: ${initiallyIncomplete} of ${coverage.tokens} active tokens lack a fresh "${model}" embedding. Dry-run will not backfill them; run 'zam token reembed' first for a complete scan.`
|
|
15511
|
+
);
|
|
15512
|
+
}
|
|
15513
|
+
}
|
|
15514
|
+
console.log(`Listing embedded tokens for model "${model}"...`);
|
|
15515
|
+
const embedded = await listEmbeddedTokens(db, model);
|
|
15516
|
+
const threshold = await resolveDedupThreshold(db);
|
|
15517
|
+
console.log(
|
|
15518
|
+
`Scanning ${embedded.length} tokens for semantic duplicates (threshold: ${threshold})...`
|
|
15519
|
+
);
|
|
15520
|
+
const duplicates = [];
|
|
15521
|
+
const seenPairs = /* @__PURE__ */ new Set();
|
|
15522
|
+
for (let i = 0; i < embedded.length; i++) {
|
|
15523
|
+
for (let j = i + 1; j < embedded.length; j++) {
|
|
15524
|
+
const sim = cosineSimilarity(
|
|
15525
|
+
embedded[i].embedding,
|
|
15526
|
+
embedded[j].embedding
|
|
15527
|
+
);
|
|
15528
|
+
if (sim >= threshold) {
|
|
15529
|
+
const idA = embedded[i].token.id;
|
|
15530
|
+
const idB = embedded[j].token.id;
|
|
15531
|
+
const pairKey = idA < idB ? `${idA}:${idB}` : `${idB}:${idA}`;
|
|
15532
|
+
if (!seenPairs.has(pairKey)) {
|
|
15533
|
+
seenPairs.add(pairKey);
|
|
15534
|
+
duplicates.push({
|
|
15535
|
+
a: embedded[i].token,
|
|
15536
|
+
b: embedded[j].token,
|
|
15537
|
+
similarity: sim
|
|
15538
|
+
});
|
|
15539
|
+
}
|
|
15540
|
+
}
|
|
15541
|
+
}
|
|
15542
|
+
}
|
|
15543
|
+
console.log(`Found ${duplicates.length} duplicate pairs.`);
|
|
15544
|
+
if (duplicates.length === 0) return;
|
|
15545
|
+
if (!applyingFixes) {
|
|
15546
|
+
console.log("\nDuplicate pairs found (Dry run):");
|
|
15547
|
+
for (const pair of duplicates) {
|
|
15548
|
+
console.log(
|
|
15549
|
+
` - "${pair.a.slug}" and "${pair.b.slug}" (similarity: ${pair.similarity.toFixed(3)})`
|
|
15550
|
+
);
|
|
15551
|
+
}
|
|
15552
|
+
console.log("\nRun with --fix to resolve duplicates interactively.");
|
|
15553
|
+
return;
|
|
15554
|
+
}
|
|
15555
|
+
const { select: select3 } = await import("@inquirer/prompts");
|
|
15556
|
+
let deprecatedCount = 0;
|
|
15557
|
+
for (const pair of duplicates) {
|
|
15558
|
+
console.log(
|
|
15559
|
+
`
|
|
15560
|
+
Duplicate pair found (similarity: ${pair.similarity.toFixed(3)}):`
|
|
15561
|
+
);
|
|
15562
|
+
console.log(
|
|
15563
|
+
` 1. ${pair.a.slug}: "${pair.a.concept.substring(0, 100)}..."`
|
|
15564
|
+
);
|
|
15565
|
+
console.log(
|
|
15566
|
+
` 2. ${pair.b.slug}: "${pair.b.concept.substring(0, 100)}..."`
|
|
15567
|
+
);
|
|
15568
|
+
const choice = await select3({
|
|
15569
|
+
message: "Select action:",
|
|
15570
|
+
choices: [
|
|
15571
|
+
{ name: "Keep both", value: "keep" },
|
|
15572
|
+
{ name: `Deprecate ${pair.a.slug}`, value: "deprecate_a" },
|
|
15573
|
+
{ name: `Deprecate ${pair.b.slug}`, value: "deprecate_b" }
|
|
15574
|
+
]
|
|
15575
|
+
});
|
|
15576
|
+
if (choice === "deprecate_a") {
|
|
15577
|
+
await deprecateToken(db, pair.a.slug);
|
|
15578
|
+
console.log(` Deprecated ${pair.a.slug}`);
|
|
15579
|
+
deprecatedCount++;
|
|
15580
|
+
} else if (choice === "deprecate_b") {
|
|
15581
|
+
await deprecateToken(db, pair.b.slug);
|
|
15582
|
+
console.log(` Deprecated ${pair.b.slug}`);
|
|
15583
|
+
deprecatedCount++;
|
|
15584
|
+
}
|
|
15585
|
+
}
|
|
15586
|
+
console.log(
|
|
15587
|
+
`
|
|
15588
|
+
Duplicates scan complete. Deprecated ${deprecatedCount} token(s).`
|
|
15589
|
+
);
|
|
15590
|
+
}
|
|
15591
|
+
},
|
|
15592
|
+
{
|
|
15593
|
+
name: "domains",
|
|
15594
|
+
description: "Help rename/unify domains into / hierarchy (slugs untouched).",
|
|
15595
|
+
run: async (db, opts) => {
|
|
15596
|
+
const tokens = await listTokens(db, {});
|
|
15597
|
+
const domains = Array.from(
|
|
15598
|
+
new Set(tokens.map((t2) => t2.domain).filter(Boolean))
|
|
15599
|
+
).sort();
|
|
15600
|
+
console.log(`Current domains (${domains.length}):`);
|
|
15601
|
+
for (const d of domains) {
|
|
15602
|
+
const count2 = tokens.filter((t2) => t2.domain === d).length;
|
|
15603
|
+
const status = d.includes("/") ? "(hierarchical)" : "(flat, valid)";
|
|
15604
|
+
console.log(` ${d} \u2014 ${count2} token(s) ${status}`);
|
|
15605
|
+
}
|
|
15606
|
+
if (!shouldApplyFixes(opts)) {
|
|
15607
|
+
console.log("\nUse --fix to interactively rename/unify domains.");
|
|
15608
|
+
return;
|
|
15609
|
+
}
|
|
15610
|
+
if (!canRunInteractiveChoices(opts, "domain renames")) return;
|
|
15611
|
+
const { confirm: confirm5, input: input8 } = await import("@inquirer/prompts");
|
|
15612
|
+
const changes = [];
|
|
15613
|
+
for (const d of domains) {
|
|
15614
|
+
const count2 = tokens.filter((t2) => t2.domain === d).length;
|
|
15615
|
+
const wantRename = await confirm5({
|
|
15616
|
+
message: `Rename domain "${d}" (${count2} tokens)?`,
|
|
15617
|
+
default: false
|
|
15618
|
+
});
|
|
15619
|
+
if (wantRename) {
|
|
15620
|
+
const newDomain = await input8({
|
|
15621
|
+
message: `Enter new domain name for "${d}":`,
|
|
15622
|
+
validate: (val) => val.trim().length > 0 ? true : "Domain name cannot be empty"
|
|
15623
|
+
});
|
|
15624
|
+
const trimmed = newDomain.trim();
|
|
15625
|
+
if (trimmed === d) {
|
|
15626
|
+
console.log(" New name matches old name. Skipped.");
|
|
15627
|
+
continue;
|
|
15628
|
+
}
|
|
15629
|
+
changes.push({ oldDomain: d, newDomain: trimmed });
|
|
15630
|
+
}
|
|
15631
|
+
}
|
|
15632
|
+
if (changes.length > 0) {
|
|
15633
|
+
await db.transaction(async (tx) => {
|
|
15634
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15635
|
+
const stmt = tx.prepare(
|
|
15636
|
+
"UPDATE tokens SET domain = ?, updated_at = ? WHERE domain = ?"
|
|
15637
|
+
);
|
|
15638
|
+
for (const change of changes) {
|
|
15639
|
+
const res = await stmt.run(change.newDomain, now, change.oldDomain);
|
|
15640
|
+
console.log(
|
|
15641
|
+
` Renamed "${change.oldDomain}" to "${change.newDomain}" for ${res.changes} token(s).`
|
|
15642
|
+
);
|
|
15643
|
+
}
|
|
15644
|
+
});
|
|
15645
|
+
}
|
|
15646
|
+
console.log(
|
|
15647
|
+
`Domains rename complete. Renamed ${changes.length} domain(s).`
|
|
15648
|
+
);
|
|
15649
|
+
}
|
|
15650
|
+
}
|
|
15651
|
+
];
|
|
15652
|
+
function parseDoctorTimeout(value) {
|
|
15653
|
+
const parsed = Number(value);
|
|
15654
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
15655
|
+
throw new Error("--timeout must be a positive integer in milliseconds");
|
|
15656
|
+
}
|
|
15657
|
+
return parsed;
|
|
15658
|
+
}
|
|
15659
|
+
var doctorCommand = new Command5("doctor").description(
|
|
15660
|
+
"Diagnose and repair the knowledge base (titles, legacy data, duplicates, domains)."
|
|
15661
|
+
).option("--fix", "Apply changes (default is dry-run)").option("--dry-run", "Explicit dry run (default)").option(
|
|
15662
|
+
"--yes",
|
|
15663
|
+
"Auto-confirm deterministic title/text fixes (duplicate/domain choices remain interactive)"
|
|
15664
|
+
).option("--no-llm", "Skip LLM calls, use heuristic fallback only").option(
|
|
15665
|
+
"--timeout <ms>",
|
|
15666
|
+
"LLM timeout in ms per call (default: 20000)",
|
|
15667
|
+
"20000"
|
|
15668
|
+
).argument("[task]", "Specific task: titles, texts, duplicates, domains").action(async (taskName, opts) => {
|
|
15669
|
+
await withDb(async (db) => {
|
|
15670
|
+
const fix = !!opts.fix;
|
|
15671
|
+
const dryRun = !fix || !!opts.dryRun;
|
|
15672
|
+
const yes = !!opts.yes;
|
|
15673
|
+
const noLlm = opts.llm === false;
|
|
15674
|
+
const timeoutMs = parseDoctorTimeout(opts.timeout);
|
|
15675
|
+
if (!taskName) {
|
|
15676
|
+
console.log("Available doctor tasks:");
|
|
15677
|
+
for (const t2 of doctorTasks) {
|
|
15678
|
+
console.log(` ${t2.name} \u2014 ${t2.description}`);
|
|
15679
|
+
}
|
|
15680
|
+
console.log("\nRun with a task name, e.g. `zam doctor titles --fix`");
|
|
15681
|
+
return;
|
|
15682
|
+
}
|
|
15683
|
+
const task = doctorTasks.find((t2) => t2.name === taskName);
|
|
15684
|
+
if (!task) {
|
|
15685
|
+
console.error(`Unknown task: ${taskName}`);
|
|
15686
|
+
process.exit(1);
|
|
15687
|
+
}
|
|
15688
|
+
console.log(
|
|
15689
|
+
`Running doctor task: ${task.name} (fix=${fix}, dryRun=${dryRun}${noLlm ? ", noLlm=true" : ""})`
|
|
15690
|
+
);
|
|
15691
|
+
await task.run(db, { fix, dryRun, yes, noLlm, timeoutMs });
|
|
15692
|
+
});
|
|
15693
|
+
});
|
|
15694
|
+
|
|
14748
15695
|
// src/cli/commands/git-sync.ts
|
|
14749
15696
|
import { execSync as execSync5 } from "child_process";
|
|
14750
15697
|
import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync8 } from "fs";
|
|
14751
15698
|
import { join as join19 } from "path";
|
|
14752
|
-
import { Command as
|
|
15699
|
+
import { Command as Command6 } from "commander";
|
|
14753
15700
|
function installHook2() {
|
|
14754
15701
|
const gitDir = join19(process.cwd(), ".git");
|
|
14755
15702
|
if (!existsSync18(gitDir)) {
|
|
@@ -14779,7 +15726,7 @@ zam git-sync --commit HEAD --quiet
|
|
|
14779
15726
|
process.exit(1);
|
|
14780
15727
|
}
|
|
14781
15728
|
}
|
|
14782
|
-
var gitSyncCommand = new
|
|
15729
|
+
var gitSyncCommand = new Command6("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
|
|
14783
15730
|
if (opts.install) {
|
|
14784
15731
|
installHook2();
|
|
14785
15732
|
return;
|
|
@@ -14871,7 +15818,7 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
|
|
|
14871
15818
|
import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
|
|
14872
15819
|
import { resolve as resolve5 } from "path";
|
|
14873
15820
|
import { input as input2 } from "@inquirer/prompts";
|
|
14874
|
-
import { Command as
|
|
15821
|
+
import { Command as Command7 } from "commander";
|
|
14875
15822
|
async function resolveGoalsDir() {
|
|
14876
15823
|
let goalsDir;
|
|
14877
15824
|
let db;
|
|
@@ -14884,7 +15831,7 @@ async function resolveGoalsDir() {
|
|
|
14884
15831
|
}
|
|
14885
15832
|
return goalsDir ? resolve5(goalsDir) : resolve5("goals");
|
|
14886
15833
|
}
|
|
14887
|
-
var goalCommand = new
|
|
15834
|
+
var goalCommand = new Command7("goal").description(
|
|
14888
15835
|
"Manage learning goals (markdown files)"
|
|
14889
15836
|
);
|
|
14890
15837
|
goalCommand.command("list").description("List all goals").option(
|
|
@@ -15057,7 +16004,7 @@ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as
|
|
|
15057
16004
|
import { homedir as homedir11 } from "os";
|
|
15058
16005
|
import { join as join20, resolve as resolve6 } from "path";
|
|
15059
16006
|
import { confirm, input as input3 } from "@inquirer/prompts";
|
|
15060
|
-
import { Command as
|
|
16007
|
+
import { Command as Command8 } from "commander";
|
|
15061
16008
|
var HOME2 = homedir11();
|
|
15062
16009
|
function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
|
|
15063
16010
|
console.log(`${color}${char.repeat(len)}\x1B[0m`);
|
|
@@ -15094,7 +16041,7 @@ Here, I declare the core concepts and principles I want to master.
|
|
|
15094
16041
|
);
|
|
15095
16042
|
}
|
|
15096
16043
|
}
|
|
15097
|
-
var initCommand = new
|
|
16044
|
+
var initCommand = new Command8("init").description("Launch the guided interactive onboarding wizard").action(async () => {
|
|
15098
16045
|
printLine();
|
|
15099
16046
|
console.log(
|
|
15100
16047
|
"\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
|
|
@@ -15271,7 +16218,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
|
|
|
15271
16218
|
|
|
15272
16219
|
// src/cli/commands/learn.ts
|
|
15273
16220
|
import { input as input5 } from "@inquirer/prompts";
|
|
15274
|
-
import { Command as
|
|
16221
|
+
import { Command as Command9 } from "commander";
|
|
15275
16222
|
|
|
15276
16223
|
// src/cli/learn-format.ts
|
|
15277
16224
|
var BLOOM_VERBS3 = {
|
|
@@ -15287,16 +16234,22 @@ function clampBloom(n) {
|
|
|
15287
16234
|
function formatHeader(input8) {
|
|
15288
16235
|
const lvl = clampBloom(input8.bloomLevel);
|
|
15289
16236
|
const parts = [`${BLOOM_VERBS3[lvl]} (Bloom ${lvl})`];
|
|
15290
|
-
|
|
16237
|
+
const name = input8.title || input8.slug;
|
|
16238
|
+
if (name) {
|
|
16239
|
+
parts.push(name);
|
|
16240
|
+
} else if (input8.domain?.trim()) {
|
|
15291
16241
|
parts.push(input8.domain.trim());
|
|
15292
16242
|
}
|
|
15293
16243
|
return parts.join(" \xB7 ");
|
|
15294
16244
|
}
|
|
15295
16245
|
function formatReveal(input8) {
|
|
15296
16246
|
const lines = [
|
|
15297
|
-
`Token:
|
|
16247
|
+
`Token: ${input8.title || input8.slug}`,
|
|
15298
16248
|
`Concept: ${input8.concept}`
|
|
15299
16249
|
];
|
|
16250
|
+
if (input8.title) {
|
|
16251
|
+
lines.push(`ID: #${input8.slug}`);
|
|
16252
|
+
}
|
|
15300
16253
|
if (input8.context?.trim()) {
|
|
15301
16254
|
lines.push("", `Context: ${input8.context.trim()}`);
|
|
15302
16255
|
}
|
|
@@ -15609,7 +16562,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
|
|
|
15609
16562
|
function isExitPrompt(err) {
|
|
15610
16563
|
return err instanceof Error && err.name === "ExitPromptError";
|
|
15611
16564
|
}
|
|
15612
|
-
var learnCommand = new
|
|
16565
|
+
var learnCommand = new Command9("learn").description(
|
|
15613
16566
|
"Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
|
|
15614
16567
|
).option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into the revealed answer").action(async (opts) => {
|
|
15615
16568
|
let db;
|
|
@@ -15683,7 +16636,7 @@ ${t(locale, "welcome", { count: queue.items.length })}`);
|
|
|
15683
16636
|
console.log(`
|
|
15684
16637
|
${"\u2500".repeat(50)}`);
|
|
15685
16638
|
console.log(
|
|
15686
|
-
`[${index + 1}/${queue.items.length}] ${formatHeader(item)}`
|
|
16639
|
+
`[${index + 1}/${queue.items.length}] ${formatHeader({ bloomLevel: item.bloomLevel, domain: item.domain })}`
|
|
15687
16640
|
);
|
|
15688
16641
|
console.log(`
|
|
15689
16642
|
${prompt.question}`);
|
|
@@ -15747,6 +16700,7 @@ ${t(locale, "eval_skipped", { reason: err.message })}`
|
|
|
15747
16700
|
);
|
|
15748
16701
|
const reveal = formatReveal({
|
|
15749
16702
|
slug: item.slug,
|
|
16703
|
+
title: item.title,
|
|
15750
16704
|
concept: item.concept,
|
|
15751
16705
|
context: token?.context,
|
|
15752
16706
|
resolved
|
|
@@ -15854,8 +16808,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
|
|
|
15854
16808
|
});
|
|
15855
16809
|
|
|
15856
16810
|
// src/cli/commands/monitor.ts
|
|
15857
|
-
import { Command as
|
|
15858
|
-
var monitorCommand = new
|
|
16811
|
+
import { Command as Command10 } from "commander";
|
|
16812
|
+
var monitorCommand = new Command10("monitor").description(
|
|
15859
16813
|
"Shell observation for real-time task monitoring"
|
|
15860
16814
|
);
|
|
15861
16815
|
monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
|
|
@@ -16037,8 +16991,8 @@ monitorCommand.command("open").description("Open a new monitored terminal window
|
|
|
16037
16991
|
});
|
|
16038
16992
|
|
|
16039
16993
|
// src/cli/commands/observer.ts
|
|
16040
|
-
import { Command as
|
|
16041
|
-
var observerCommand = new
|
|
16994
|
+
import { Command as Command11 } from "commander";
|
|
16995
|
+
var observerCommand = new Command11("observer").description(
|
|
16042
16996
|
"Configure what the UI observer may capture (Layer 2 policy)"
|
|
16043
16997
|
);
|
|
16044
16998
|
function applyObserverListChange(current, entry, op) {
|
|
@@ -16101,7 +17055,7 @@ observerCommand.command("revoke <process>").description("Remove a process from o
|
|
|
16101
17055
|
|
|
16102
17056
|
// src/cli/commands/profile.ts
|
|
16103
17057
|
import { dirname as dirname6, resolve as resolve7 } from "path";
|
|
16104
|
-
import { Command as
|
|
17058
|
+
import { Command as Command12 } from "commander";
|
|
16105
17059
|
var C2 = {
|
|
16106
17060
|
reset: "\x1B[0m",
|
|
16107
17061
|
bold: "\x1B[1m",
|
|
@@ -16118,7 +17072,7 @@ function render(profile) {
|
|
|
16118
17072
|
console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
|
|
16119
17073
|
console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
|
|
16120
17074
|
}
|
|
16121
|
-
var profileCommand = new
|
|
17075
|
+
var profileCommand = new Command12("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
|
|
16122
17076
|
if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
|
|
16123
17077
|
console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
|
|
16124
17078
|
process.exit(1);
|
|
@@ -16158,8 +17112,8 @@ var profileCommand = new Command11("profile").description("Show or change this m
|
|
|
16158
17112
|
|
|
16159
17113
|
// src/cli/commands/provider.ts
|
|
16160
17114
|
import { password as password2 } from "@inquirer/prompts";
|
|
16161
|
-
import { Command as
|
|
16162
|
-
var providerCommand = new
|
|
17115
|
+
import { Command as Command13 } from "commander";
|
|
17116
|
+
var providerCommand = new Command13("provider").description(
|
|
16163
17117
|
"Configure role-based AI providers (url/model/flavor/key, per role)"
|
|
16164
17118
|
);
|
|
16165
17119
|
providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
|
|
@@ -16394,8 +17348,8 @@ providerCommand.command("clear-key <ref>").description("Remove a stored API key"
|
|
|
16394
17348
|
});
|
|
16395
17349
|
|
|
16396
17350
|
// src/cli/commands/review.ts
|
|
16397
|
-
import { Command as
|
|
16398
|
-
var reviewCommand = new
|
|
17351
|
+
import { Command as Command14 } from "commander";
|
|
17352
|
+
var reviewCommand = new Command14("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
|
|
16399
17353
|
let db;
|
|
16400
17354
|
try {
|
|
16401
17355
|
db = await openDatabase();
|
|
@@ -16433,9 +17387,8 @@ Review session: ${queue.items.length} card(s)`);
|
|
|
16433
17387
|
});
|
|
16434
17388
|
console.log(
|
|
16435
17389
|
`
|
|
16436
|
-
[${index + 1}/${queue.items.length}] ${
|
|
17390
|
+
[${index + 1}/${queue.items.length}] ${formatHeader({ bloomLevel: item.bloomLevel, domain: item.domain })}`
|
|
16437
17391
|
);
|
|
16438
|
-
console.log(`Domain: ${prompt.domain || "(none)"}`);
|
|
16439
17392
|
if (prompt.sourceLink) {
|
|
16440
17393
|
console.log(`Source: ${prompt.sourceLink}`);
|
|
16441
17394
|
if (opts.resolve !== false) {
|
|
@@ -16510,8 +17463,8 @@ ${"\u2550".repeat(50)}`);
|
|
|
16510
17463
|
// src/cli/commands/session.ts
|
|
16511
17464
|
import { readFileSync as readFileSync13 } from "fs";
|
|
16512
17465
|
import { input as input6, select as select2 } from "@inquirer/prompts";
|
|
16513
|
-
import { Command as
|
|
16514
|
-
var sessionCommand = new
|
|
17466
|
+
import { Command as Command15 } from "commander";
|
|
17467
|
+
var sessionCommand = new Command15("session").description(
|
|
16515
17468
|
"Manage learning sessions"
|
|
16516
17469
|
);
|
|
16517
17470
|
sessionCommand.command("start").description("Start a new learning session (review \u2192 task)").option("--user <id>", "User ID (default: whoami)").option("--task <description>", "Task description (interactive if omitted)").option(
|
|
@@ -16635,9 +17588,8 @@ Time limit reached (${maxMinutes} min). Moving to task selection.`
|
|
|
16635
17588
|
});
|
|
16636
17589
|
const elapsed = Math.round((Date.now() - startTime) / 6e4);
|
|
16637
17590
|
console.log(
|
|
16638
|
-
`[${index + 1}/${queue.items.length}] ${
|
|
17591
|
+
`[${index + 1}/${queue.items.length}] ${formatHeader({ bloomLevel: item.bloomLevel, domain: item.domain })} (${elapsed}/${maxMinutes} min)`
|
|
16639
17592
|
);
|
|
16640
|
-
console.log(`Domain: ${prompt.domain || "(none)"}`);
|
|
16641
17593
|
console.log(`
|
|
16642
17594
|
${prompt.question}
|
|
16643
17595
|
`);
|
|
@@ -16891,8 +17843,8 @@ sessionCommand.command("end").description("End a session and show summary").requ
|
|
|
16891
17843
|
|
|
16892
17844
|
// src/cli/commands/settings.ts
|
|
16893
17845
|
import { existsSync as existsSync21 } from "fs";
|
|
16894
|
-
import { Command as
|
|
16895
|
-
var settingsCommand = new
|
|
17846
|
+
import { Command as Command16 } from "commander";
|
|
17847
|
+
var settingsCommand = new Command16("settings").description(
|
|
16896
17848
|
"Manage user settings"
|
|
16897
17849
|
);
|
|
16898
17850
|
var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
|
|
@@ -17068,7 +18020,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
|
|
|
17068
18020
|
|
|
17069
18021
|
// src/cli/commands/setup.ts
|
|
17070
18022
|
import { resolve as resolve8 } from "path";
|
|
17071
|
-
import { Command as
|
|
18023
|
+
import { Command as Command17 } from "commander";
|
|
17072
18024
|
function formatDatabaseInitTarget(target) {
|
|
17073
18025
|
switch (target.kind) {
|
|
17074
18026
|
case "local":
|
|
@@ -17109,7 +18061,7 @@ async function activateMachineProviderConfig(db) {
|
|
|
17109
18061
|
` activate ${providerCount} machine-local provider(s) from ~/.zam/config.json`
|
|
17110
18062
|
);
|
|
17111
18063
|
}
|
|
17112
|
-
var setupCommand = new
|
|
18064
|
+
var setupCommand = new Command17("setup").description(
|
|
17113
18065
|
"Link ZAM skill directories into this workspace and initialize the database"
|
|
17114
18066
|
).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
|
|
17115
18067
|
"--agents <list>",
|
|
@@ -17163,8 +18115,8 @@ var setupCommand = new Command16("setup").description(
|
|
|
17163
18115
|
);
|
|
17164
18116
|
|
|
17165
18117
|
// src/cli/commands/skill.ts
|
|
17166
|
-
import { Command as
|
|
17167
|
-
var skillCommand = new
|
|
18118
|
+
import { Command as Command18 } from "commander";
|
|
18119
|
+
var skillCommand = new Command18("skill").description(
|
|
17168
18120
|
"Manage agent skill entries (task recipes)"
|
|
17169
18121
|
);
|
|
17170
18122
|
skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
|
|
@@ -17251,7 +18203,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
|
|
|
17251
18203
|
// src/cli/commands/snapshot.ts
|
|
17252
18204
|
import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
17253
18205
|
import { dirname as dirname7, join as join21 } from "path";
|
|
17254
|
-
import { Command as
|
|
18206
|
+
import { Command as Command19 } from "commander";
|
|
17255
18207
|
function defaultOutName() {
|
|
17256
18208
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
|
|
17257
18209
|
return `zam-snapshot-${stamp}.sql`;
|
|
@@ -17265,7 +18217,7 @@ function summarize(tables) {
|
|
|
17265
18217
|
}
|
|
17266
18218
|
return { total, nonEmpty };
|
|
17267
18219
|
}
|
|
17268
|
-
var exportCmd = new
|
|
18220
|
+
var exportCmd = new Command19("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
|
|
17269
18221
|
let db;
|
|
17270
18222
|
try {
|
|
17271
18223
|
db = await openDatabaseWithSync({ initialize: true });
|
|
@@ -17295,7 +18247,7 @@ var exportCmd = new Command18("export").description("Write a portable SQL-text s
|
|
|
17295
18247
|
process.exit(1);
|
|
17296
18248
|
}
|
|
17297
18249
|
});
|
|
17298
|
-
var importCmd = new
|
|
18250
|
+
var importCmd = new Command19("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
|
|
17299
18251
|
let db;
|
|
17300
18252
|
try {
|
|
17301
18253
|
if (!existsSync22(file)) {
|
|
@@ -17318,7 +18270,7 @@ var importCmd = new Command18("import").description("Restore a snapshot into the
|
|
|
17318
18270
|
process.exit(1);
|
|
17319
18271
|
}
|
|
17320
18272
|
});
|
|
17321
|
-
var verifyCmd = new
|
|
18273
|
+
var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
|
|
17322
18274
|
try {
|
|
17323
18275
|
if (!existsSync22(file)) {
|
|
17324
18276
|
console.error(`Error: Snapshot file not found: ${file}`);
|
|
@@ -17336,11 +18288,11 @@ var verifyCmd = new Command18("verify").description("Check a snapshot's manifest
|
|
|
17336
18288
|
process.exit(1);
|
|
17337
18289
|
}
|
|
17338
18290
|
});
|
|
17339
|
-
var snapshotCommand = new
|
|
18291
|
+
var snapshotCommand = new Command19("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
|
|
17340
18292
|
|
|
17341
18293
|
// src/cli/commands/stats.ts
|
|
17342
|
-
import { Command as
|
|
17343
|
-
var statsCommand = new
|
|
18294
|
+
import { Command as Command20 } from "commander";
|
|
18295
|
+
var statsCommand = new Command20("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
|
|
17344
18296
|
await withDb(async (db) => {
|
|
17345
18297
|
const userId = await resolveUser(opts, db);
|
|
17346
18298
|
const stats = await getUserStats(db, userId);
|
|
@@ -17376,8 +18328,8 @@ var statsCommand = new Command19("stats").description("Show learning dashboard f
|
|
|
17376
18328
|
});
|
|
17377
18329
|
|
|
17378
18330
|
// src/cli/commands/token.ts
|
|
17379
|
-
import { Command as
|
|
17380
|
-
var tokenCommand = new
|
|
18331
|
+
import { Command as Command21 } from "commander";
|
|
18332
|
+
var tokenCommand = new Command21("token").description(
|
|
17381
18333
|
"Manage knowledge tokens"
|
|
17382
18334
|
);
|
|
17383
18335
|
tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--user <id>", "Owner of the personal card (default: whoami)").option("--no-card", "Register the token only; do not create a personal card").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
|
|
@@ -17428,7 +18380,11 @@ tokenCommand.command("register").description("Register a new knowledge token").r
|
|
|
17428
18380
|
)
|
|
17429
18381
|
);
|
|
17430
18382
|
} else {
|
|
17431
|
-
console.log(`Registered token: ${token.
|
|
18383
|
+
console.log(`Registered token: ${token.title || token.slug}`);
|
|
18384
|
+
if (token.slug !== (token.title || token.slug)) {
|
|
18385
|
+
console.log(` Slug: ${token.slug}`);
|
|
18386
|
+
}
|
|
18387
|
+
console.log(` ID: ${token.id}`);
|
|
17432
18388
|
console.log(` Concept: ${token.concept}`);
|
|
17433
18389
|
console.log(` Domain: ${token.domain || "(none)"}`);
|
|
17434
18390
|
console.log(` Bloom: ${token.bloom_level}`);
|
|
@@ -17447,11 +18403,53 @@ tokenCommand.command("register").description("Register a new knowledge token").r
|
|
|
17447
18403
|
console.log(`
|
|
17448
18404
|
WARNING: Possible duplicate tokens found:`);
|
|
17449
18405
|
for (const dup of possibleDuplicates) {
|
|
18406
|
+
const name = dup.title || dup.slug;
|
|
17450
18407
|
console.log(
|
|
17451
|
-
` - ${dup.slug}
|
|
18408
|
+
` - ${name} (slug: ${dup.slug}, similarity: ${dup.similarity.toFixed(2)})`
|
|
17452
18409
|
);
|
|
17453
18410
|
}
|
|
17454
18411
|
}
|
|
18412
|
+
try {
|
|
18413
|
+
const queryText = embeddingContentForToken({
|
|
18414
|
+
concept: opts.concept,
|
|
18415
|
+
question: question ?? null,
|
|
18416
|
+
domain: opts.domain ?? "",
|
|
18417
|
+
title: opts.title ?? null
|
|
18418
|
+
});
|
|
18419
|
+
const q2 = await embedQuery(db, queryText);
|
|
18420
|
+
if (q2) {
|
|
18421
|
+
const maxSimilarity = await resolveDedupThreshold(db);
|
|
18422
|
+
const minSimilarity = await resolveSuggestMinSimilarity(db);
|
|
18423
|
+
const suggestions = await suggestFoundations(db, {
|
|
18424
|
+
queryEmbedding: q2.vector,
|
|
18425
|
+
model: q2.model,
|
|
18426
|
+
targetTokenId: token.id,
|
|
18427
|
+
targetBloomLevel: Number(opts.bloom),
|
|
18428
|
+
limit: 3,
|
|
18429
|
+
minSimilarity,
|
|
18430
|
+
maxSimilarity
|
|
18431
|
+
});
|
|
18432
|
+
const filtered = suggestions.filter(
|
|
18433
|
+
(s) => !s.wouldCreateCycle && !s.alreadyPrerequisite
|
|
18434
|
+
);
|
|
18435
|
+
if (filtered.length > 0) {
|
|
18436
|
+
console.log(
|
|
18437
|
+
`
|
|
18438
|
+
Related existing tokens as potential foundations:`
|
|
18439
|
+
);
|
|
18440
|
+
for (const s of filtered) {
|
|
18441
|
+
const note = s.bloomAboveTarget ? " (higher bloom than target)" : "";
|
|
18442
|
+
console.log(
|
|
18443
|
+
` - ${s.token.slug} (similarity: ${s.similarity.toFixed(2)})${note}`
|
|
18444
|
+
);
|
|
18445
|
+
}
|
|
18446
|
+
console.log(
|
|
18447
|
+
`Link with: zam token prereq --token <slug> --requires <slug> (see zam token prereq --help)`
|
|
18448
|
+
);
|
|
18449
|
+
}
|
|
18450
|
+
}
|
|
18451
|
+
} catch {
|
|
18452
|
+
}
|
|
17455
18453
|
}
|
|
17456
18454
|
});
|
|
17457
18455
|
});
|
|
@@ -17483,16 +18481,18 @@ tokenCommand.command("find").description("Fuzzy search for tokens").requiredOpti
|
|
|
17483
18481
|
console.log(`Found ${results.length} token(s):
|
|
17484
18482
|
`);
|
|
17485
18483
|
console.log(
|
|
17486
|
-
"Score Sim
|
|
18484
|
+
"Score Sim Title Concept Domain Bloom"
|
|
17487
18485
|
);
|
|
17488
18486
|
console.log("\u2500".repeat(95));
|
|
17489
18487
|
for (const t2 of results) {
|
|
17490
18488
|
const scoreStr = t2.score.toFixed(3).padEnd(6);
|
|
17491
18489
|
const simStr = (t2.similarity?.toFixed(2) ?? "-").padEnd(4);
|
|
18490
|
+
const display = getDisplayTitle(t2);
|
|
17492
18491
|
console.log(
|
|
17493
|
-
`${scoreStr} ${simStr} ${
|
|
18492
|
+
`${scoreStr} ${simStr} ${display.padEnd(28)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
|
|
17494
18493
|
);
|
|
17495
18494
|
}
|
|
18495
|
+
console.log("\n(Use slug for CLI commands -- slugs are technical IDs)");
|
|
17496
18496
|
});
|
|
17497
18497
|
});
|
|
17498
18498
|
tokenCommand.command("list").description("List all tokens").option("--domain <domain>", "Filter by domain").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
|
|
@@ -17511,16 +18511,20 @@ tokenCommand.command("list").description("List all tokens").option("--domain <do
|
|
|
17511
18511
|
return;
|
|
17512
18512
|
}
|
|
17513
18513
|
console.log(
|
|
17514
|
-
"
|
|
18514
|
+
"Title Concept Domain Bloom"
|
|
17515
18515
|
);
|
|
17516
|
-
console.log("\u2500".repeat(
|
|
18516
|
+
console.log("\u2500".repeat(85));
|
|
17517
18517
|
for (const t2 of tokens) {
|
|
18518
|
+
const display = getDisplayTitle(t2);
|
|
17518
18519
|
console.log(
|
|
17519
|
-
`${
|
|
18520
|
+
`${display.padEnd(28)} ${t2.concept.slice(0, 31).padEnd(31)} ${(t2.domain || "-").padEnd(11)} ${t2.bloom_level}`
|
|
17520
18521
|
);
|
|
17521
18522
|
}
|
|
17522
18523
|
console.log(`
|
|
17523
18524
|
${tokens.length} token(s) total.`);
|
|
18525
|
+
console.log(
|
|
18526
|
+
"(Use --slug <slug> for commands; slugs are technical identifiers)"
|
|
18527
|
+
);
|
|
17524
18528
|
});
|
|
17525
18529
|
});
|
|
17526
18530
|
tokenCommand.command("edit").description("Edit a token's mutable fields").requiredOption("--slug <slug>", "Token slug").option("--concept <concept>", "Updated concept text").option("--domain <domain>", "Updated domain (blank allowed)").option("--bloom <level>", "Updated Bloom taxonomy level (1-5)").option("--context <context>", "Updated context (blank allowed)").option(
|
|
@@ -17529,7 +18533,10 @@ tokenCommand.command("edit").description("Edit a token's mutable fields").requir
|
|
|
17529
18533
|
).option(
|
|
17530
18534
|
"--source-link <link>",
|
|
17531
18535
|
"Updated source file path or reference URL (blank allowed)"
|
|
17532
|
-
).option("--question <question>", "Updated question text (blank allowed)").option(
|
|
18536
|
+
).option("--question <question>", "Updated question text (blank allowed)").option(
|
|
18537
|
+
"--title <title>",
|
|
18538
|
+
"Updated human-friendly title for graph display (blank allowed)"
|
|
18539
|
+
).option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
|
|
17533
18540
|
await withDb(async (db) => {
|
|
17534
18541
|
const updates = {};
|
|
17535
18542
|
if (opts.concept !== void 0) updates.concept = opts.concept;
|
|
@@ -17543,6 +18550,9 @@ tokenCommand.command("edit").description("Edit a token's mutable fields").requir
|
|
|
17543
18550
|
if (opts.question !== void 0) {
|
|
17544
18551
|
updates.question = opts.question === "" ? null : opts.question;
|
|
17545
18552
|
}
|
|
18553
|
+
if (opts.title !== void 0) {
|
|
18554
|
+
updates.title = opts.title === "" ? "" : opts.title;
|
|
18555
|
+
}
|
|
17546
18556
|
if (opts.mode !== void 0) {
|
|
17547
18557
|
const validModes = ["shadowing", "copilot", "autonomy", "none"];
|
|
17548
18558
|
if (!validModes.includes(opts.mode)) {
|
|
@@ -17557,11 +18567,15 @@ tokenCommand.command("edit").description("Edit a token's mutable fields").requir
|
|
|
17557
18567
|
jsonOut(token);
|
|
17558
18568
|
return;
|
|
17559
18569
|
}
|
|
17560
|
-
console.log(`Updated token: ${token.slug}`);
|
|
18570
|
+
console.log(`Updated token: ${token.title || token.slug}`);
|
|
18571
|
+
if (token.slug !== (token.title || token.slug)) {
|
|
18572
|
+
console.log(` Slug: ${token.slug}`);
|
|
18573
|
+
}
|
|
17561
18574
|
console.log(` Concept: ${token.concept}`);
|
|
17562
18575
|
console.log(` Domain: ${token.domain || "(none)"}`);
|
|
17563
18576
|
console.log(` Bloom: ${token.bloom_level}`);
|
|
17564
18577
|
console.log(` Question: ${token.question || "(none)"}`);
|
|
18578
|
+
console.log(` Title: ${token.title || "(none)"}`);
|
|
17565
18579
|
console.log(` Context: ${token.context || "(none)"}`);
|
|
17566
18580
|
console.log(` Mode: ${token.symbiosis_mode ?? "none"}`);
|
|
17567
18581
|
console.log(` Source: ${token.source_link || "(none)"}`);
|
|
@@ -17680,7 +18694,11 @@ tokenCommand.command("status").description("Show full status of a token for a us
|
|
|
17680
18694
|
console.log(JSON.stringify(status, null, 2));
|
|
17681
18695
|
return;
|
|
17682
18696
|
}
|
|
17683
|
-
console.log(`Token: ${token.
|
|
18697
|
+
console.log(`Token: ${token.title || token.slug}`);
|
|
18698
|
+
if (token.slug !== (token.title || token.slug)) {
|
|
18699
|
+
console.log(` Slug: ${token.slug}`);
|
|
18700
|
+
}
|
|
18701
|
+
console.log(` ID: ${token.id}`);
|
|
17684
18702
|
console.log(` Concept: ${token.concept}`);
|
|
17685
18703
|
console.log(` Question: ${token.question || "(none)"}`);
|
|
17686
18704
|
console.log(` Domain: ${token.domain || "(none)"}`);
|
|
@@ -17705,7 +18723,9 @@ tokenCommand.command("status").description("Show full status of a token for a us
|
|
|
17705
18723
|
if (prereqs.length > 0) {
|
|
17706
18724
|
console.log("Prerequisites:");
|
|
17707
18725
|
for (const p of prereqs) {
|
|
17708
|
-
console.log(
|
|
18726
|
+
console.log(
|
|
18727
|
+
` - ${p.title || p.slug}: ${p.concept} (bloom ${p.bloom_level})`
|
|
18728
|
+
);
|
|
17709
18729
|
}
|
|
17710
18730
|
} else {
|
|
17711
18731
|
console.log("No prerequisites.");
|
|
@@ -17713,7 +18733,9 @@ tokenCommand.command("status").description("Show full status of a token for a us
|
|
|
17713
18733
|
if (dependents.length > 0) {
|
|
17714
18734
|
console.log("\nDependents:");
|
|
17715
18735
|
for (const d of dependents) {
|
|
17716
|
-
console.log(
|
|
18736
|
+
console.log(
|
|
18737
|
+
` - ${d.title || d.slug}: ${d.concept} (bloom ${d.bloom_level})`
|
|
18738
|
+
);
|
|
17717
18739
|
}
|
|
17718
18740
|
}
|
|
17719
18741
|
});
|
|
@@ -17775,7 +18797,7 @@ import { existsSync as existsSync23 } from "fs";
|
|
|
17775
18797
|
import { homedir as homedir12 } from "os";
|
|
17776
18798
|
import { dirname as dirname8, join as join22 } from "path";
|
|
17777
18799
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
17778
|
-
import { Command as
|
|
18800
|
+
import { Command as Command22 } from "commander";
|
|
17779
18801
|
var C3 = {
|
|
17780
18802
|
reset: "\x1B[0m",
|
|
17781
18803
|
red: "\x1B[31m",
|
|
@@ -17955,7 +18977,7 @@ function createShortcuts(appPath, repoRoot) {
|
|
|
17955
18977
|
console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
|
|
17956
18978
|
}
|
|
17957
18979
|
}
|
|
17958
|
-
var uiCommand = new
|
|
18980
|
+
var uiCommand = new Command22("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
|
|
17959
18981
|
"--build",
|
|
17960
18982
|
"Build the native installer for your OS (needs Rust, one-time)"
|
|
17961
18983
|
).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
|
|
@@ -18063,7 +19085,7 @@ import { existsSync as existsSync24, readFileSync as readFileSync15, realpathSyn
|
|
|
18063
19085
|
import { dirname as dirname9, join as join23 } from "path";
|
|
18064
19086
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
18065
19087
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
18066
|
-
import { Command as
|
|
19088
|
+
import { Command as Command23 } from "commander";
|
|
18067
19089
|
var GITHUB_REPO = "zam-os/zam";
|
|
18068
19090
|
var CHANNELS = [
|
|
18069
19091
|
"developer",
|
|
@@ -18141,7 +19163,7 @@ function render2(decision) {
|
|
|
18141
19163
|
);
|
|
18142
19164
|
}
|
|
18143
19165
|
}
|
|
18144
|
-
var checkCmd = new
|
|
19166
|
+
var checkCmd = new Command23("check").description("Check whether a newer ZAM has been released").option(
|
|
18145
19167
|
"--latest <version>",
|
|
18146
19168
|
"Compare against this version instead of fetching"
|
|
18147
19169
|
).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
|
|
@@ -18308,7 +19330,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
|
|
|
18308
19330
|
process.exit(1);
|
|
18309
19331
|
}
|
|
18310
19332
|
}
|
|
18311
|
-
var updateCommand = new
|
|
19333
|
+
var updateCommand = new Command23("update").description(
|
|
18312
19334
|
"Update ZAM to the latest release (use `update check` to only check)"
|
|
18313
19335
|
).option("-y, --yes", "Apply without confirmation").option(
|
|
18314
19336
|
"--force",
|
|
@@ -18318,8 +19340,8 @@ var updateCommand = new Command22("update").description(
|
|
|
18318
19340
|
}).addCommand(checkCmd);
|
|
18319
19341
|
|
|
18320
19342
|
// src/cli/commands/whoami.ts
|
|
18321
|
-
import { Command as
|
|
18322
|
-
var whoamiCommand = new
|
|
19343
|
+
import { Command as Command24 } from "commander";
|
|
19344
|
+
var whoamiCommand = new Command24("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
|
|
18323
19345
|
await withDb(async (db) => {
|
|
18324
19346
|
if (opts.set) {
|
|
18325
19347
|
await setSetting(db, "user.id", opts.set);
|
|
@@ -18360,7 +19382,7 @@ import { existsSync as existsSync25, writeFileSync as writeFileSync11 } from "fs
|
|
|
18360
19382
|
import { homedir as homedir13 } from "os";
|
|
18361
19383
|
import { join as join24, resolve as resolve9 } from "path";
|
|
18362
19384
|
import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
|
|
18363
|
-
import { Command as
|
|
19385
|
+
import { Command as Command25 } from "commander";
|
|
18364
19386
|
function runGit2(cwd, args) {
|
|
18365
19387
|
try {
|
|
18366
19388
|
return execFileSync4("git", args, {
|
|
@@ -18378,7 +19400,7 @@ function ghRepoCreateArgs(repoName, repoVisibility) {
|
|
|
18378
19400
|
function gitRemoteArgs(githubUrl, hasOrigin) {
|
|
18379
19401
|
return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
|
|
18380
19402
|
}
|
|
18381
|
-
var workspaceCommand = new
|
|
19403
|
+
var workspaceCommand = new Command25("workspace").description(
|
|
18382
19404
|
"Manage your ZAM learning workspace"
|
|
18383
19405
|
);
|
|
18384
19406
|
var WORKSPACE_KINDS = [
|
|
@@ -18733,13 +19755,14 @@ var __dirname = dirname10(fileURLToPath5(import.meta.url));
|
|
|
18733
19755
|
var pkg = JSON.parse(
|
|
18734
19756
|
readFileSync16(join25(__dirname, "..", "..", "package.json"), "utf-8")
|
|
18735
19757
|
);
|
|
18736
|
-
var program = new
|
|
19758
|
+
var program = new Command26();
|
|
18737
19759
|
program.name("zam").description(
|
|
18738
19760
|
"The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
|
|
18739
19761
|
).version(pkg.version);
|
|
18740
19762
|
program.addCommand(initCommand);
|
|
18741
19763
|
program.addCommand(setupCommand);
|
|
18742
19764
|
program.addCommand(tokenCommand);
|
|
19765
|
+
program.addCommand(doctorCommand);
|
|
18743
19766
|
program.addCommand(cardCommand);
|
|
18744
19767
|
program.addCommand(sessionCommand);
|
|
18745
19768
|
program.addCommand(statsCommand);
|