zam-core 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/app.js +106 -40
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/commands/mcp.js +105 -39
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +90 -33
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/commands/mcp.js
CHANGED
|
@@ -585,7 +585,12 @@ CREATE TABLE IF NOT EXISTS tokens (
|
|
|
585
585
|
deprecated_at TEXT,
|
|
586
586
|
question TEXT,
|
|
587
587
|
provider TEXT,
|
|
588
|
-
topic_id TEXT
|
|
588
|
+
topic_id TEXT,
|
|
589
|
+
-- Who authored the current question ('manual' | 'llm' | 'template');
|
|
590
|
+
-- validated in code, not via CHECK. Column default is 'llm' so unlabeled
|
|
591
|
+
-- writes (pre-M013 rows, old snapshot restores) count as LLM-era content;
|
|
592
|
+
-- createToken() defaults to 'manual' for API callers instead.
|
|
593
|
+
question_source TEXT NOT NULL DEFAULT 'llm'
|
|
589
594
|
);
|
|
590
595
|
|
|
591
596
|
-- Prerequisite dependency graph: "to learn A, first know B"
|
|
@@ -1109,6 +1114,11 @@ async function runMigrations(db) {
|
|
|
1109
1114
|
await db.exec(`
|
|
1110
1115
|
CREATE INDEX IF NOT EXISTS idx_token_contexts_context ON token_contexts(context_id)
|
|
1111
1116
|
`);
|
|
1117
|
+
if (tokenCols.length > 0 && !tokenCols.some((c) => c.name === "question_source")) {
|
|
1118
|
+
await db.exec(
|
|
1119
|
+
`ALTER TABLE tokens ADD COLUMN question_source TEXT NOT NULL DEFAULT 'llm'`
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1112
1122
|
}
|
|
1113
1123
|
var DEFAULT_DB_DIR, DEFAULT_DB_PATH, require2;
|
|
1114
1124
|
var init_connection = __esm({
|
|
@@ -1269,7 +1279,11 @@ var init_snapshot = __esm({
|
|
|
1269
1279
|
"review_logs",
|
|
1270
1280
|
"session_syntheses",
|
|
1271
1281
|
"user_config",
|
|
1272
|
-
"agent_skills"
|
|
1282
|
+
"agent_skills",
|
|
1283
|
+
"sources",
|
|
1284
|
+
"token_sources",
|
|
1285
|
+
"contexts",
|
|
1286
|
+
"token_contexts"
|
|
1273
1287
|
];
|
|
1274
1288
|
}
|
|
1275
1289
|
});
|
|
@@ -1754,6 +1768,13 @@ var init_knowledge_context = __esm({
|
|
|
1754
1768
|
|
|
1755
1769
|
// src/kernel/models/token.ts
|
|
1756
1770
|
import { ulid as ulid4 } from "ulid";
|
|
1771
|
+
function validateQuestionSource(value) {
|
|
1772
|
+
if (!VALID_QUESTION_SOURCES.includes(value)) {
|
|
1773
|
+
throw new Error(
|
|
1774
|
+
`Invalid question_source: ${value} (expected one of ${VALID_QUESTION_SOURCES.join(", ")})`
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1757
1778
|
async function createToken(db, input) {
|
|
1758
1779
|
const id = ulid4();
|
|
1759
1780
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1762,9 +1783,11 @@ async function createToken(db, input) {
|
|
|
1762
1783
|
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1763
1784
|
}
|
|
1764
1785
|
const title = input.title ?? "";
|
|
1786
|
+
const questionSource = input.question_source ?? "manual";
|
|
1787
|
+
validateQuestionSource(questionSource);
|
|
1765
1788
|
await db.prepare(`
|
|
1766
|
-
INSERT INTO tokens (id, slug, title, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, provider, topic_id, created_at, updated_at)
|
|
1767
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1789
|
+
INSERT INTO tokens (id, slug, title, concept, domain, bloom_level, context, symbiosis_mode, source_link, question, question_source, provider, topic_id, created_at, updated_at)
|
|
1790
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1768
1791
|
`).run(
|
|
1769
1792
|
id,
|
|
1770
1793
|
input.slug,
|
|
@@ -1776,6 +1799,7 @@ async function createToken(db, input) {
|
|
|
1776
1799
|
input.symbiosis_mode ?? null,
|
|
1777
1800
|
input.source_link ?? null,
|
|
1778
1801
|
input.question ?? null,
|
|
1802
|
+
questionSource,
|
|
1779
1803
|
input.provider ?? null,
|
|
1780
1804
|
input.topic_id ?? null,
|
|
1781
1805
|
now,
|
|
@@ -1799,6 +1823,18 @@ async function getTokenBySlug(db, slug) {
|
|
|
1799
1823
|
parseTokenFallback(token);
|
|
1800
1824
|
return token;
|
|
1801
1825
|
}
|
|
1826
|
+
async function getTokensBySlugs(db, slugs) {
|
|
1827
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
1828
|
+
const unique = [...new Set(slugs)];
|
|
1829
|
+
if (unique.length === 0) return tokens;
|
|
1830
|
+
const placeholders = unique.map(() => "?").join(",");
|
|
1831
|
+
const rows = await db.prepare(`SELECT * FROM tokens WHERE slug IN (${placeholders})`).all(...unique);
|
|
1832
|
+
for (const token of rows) {
|
|
1833
|
+
parseTokenFallback(token);
|
|
1834
|
+
tokens.set(token.slug, token);
|
|
1835
|
+
}
|
|
1836
|
+
return tokens;
|
|
1837
|
+
}
|
|
1802
1838
|
async function getTokenById(db, id) {
|
|
1803
1839
|
const token = await db.prepare("SELECT * FROM tokens WHERE id = ?").get(id);
|
|
1804
1840
|
parseTokenFallback(token);
|
|
@@ -1852,6 +1888,12 @@ async function updateToken(db, slug, updates) {
|
|
|
1852
1888
|
fields.push("question = ?");
|
|
1853
1889
|
values.push(updates.question);
|
|
1854
1890
|
}
|
|
1891
|
+
const questionSource = updates.question_source ?? (updates.question !== void 0 ? "manual" : void 0);
|
|
1892
|
+
if (questionSource !== void 0) {
|
|
1893
|
+
validateQuestionSource(questionSource);
|
|
1894
|
+
fields.push("question_source = ?");
|
|
1895
|
+
values.push(questionSource);
|
|
1896
|
+
}
|
|
1855
1897
|
if (updates.provider !== void 0) {
|
|
1856
1898
|
fields.push("provider = ?");
|
|
1857
1899
|
values.push(updates.provider);
|
|
@@ -2180,6 +2222,9 @@ async function importCurriculumCards(db, userId, cards) {
|
|
|
2180
2222
|
symbiosis_mode: symbiosisMode,
|
|
2181
2223
|
source_link: card.source_link || null,
|
|
2182
2224
|
question: card.question || null,
|
|
2225
|
+
// Curriculum cards are LLM-extracted; their questions are LLM
|
|
2226
|
+
// inventions, not human-authored content.
|
|
2227
|
+
question_source: "llm",
|
|
2183
2228
|
provider: card.provider || null,
|
|
2184
2229
|
topic_id: card.topic_id || null
|
|
2185
2230
|
});
|
|
@@ -2503,10 +2548,12 @@ async function assertPrerequisiteDoesNotCreateCycle(db, tokenId, prerequisiteId)
|
|
|
2503
2548
|
throw new Error("Cannot add prerequisite: would create a cycle");
|
|
2504
2549
|
}
|
|
2505
2550
|
}
|
|
2551
|
+
var VALID_QUESTION_SOURCES;
|
|
2506
2552
|
var init_token = __esm({
|
|
2507
2553
|
"src/kernel/models/token.ts"() {
|
|
2508
2554
|
"use strict";
|
|
2509
2555
|
init_card();
|
|
2556
|
+
VALID_QUESTION_SOURCES = ["manual", "llm", "template"];
|
|
2510
2557
|
}
|
|
2511
2558
|
});
|
|
2512
2559
|
|
|
@@ -3942,25 +3989,29 @@ async function cascadeBlock(db, userId, tokenSlug) {
|
|
|
3942
3989
|
};
|
|
3943
3990
|
}
|
|
3944
3991
|
async function unblockReady(db, userId) {
|
|
3945
|
-
const blockedCards = await db.prepare(
|
|
3946
|
-
`SELECT c.token_id, t.slug, t.concept
|
|
3947
|
-
FROM cards c
|
|
3948
|
-
JOIN tokens t ON t.id = c.token_id
|
|
3949
|
-
WHERE c.user_id = ? AND c.blocked = 1`
|
|
3950
|
-
).all(userId);
|
|
3951
3992
|
const unblocked = [];
|
|
3952
|
-
for (
|
|
3953
|
-
const
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
JOIN
|
|
3957
|
-
WHERE
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3993
|
+
for (; ; ) {
|
|
3994
|
+
const readyCards = await db.prepare(
|
|
3995
|
+
`SELECT c.token_id, t.slug, t.concept
|
|
3996
|
+
FROM cards c
|
|
3997
|
+
JOIN tokens t ON t.id = c.token_id
|
|
3998
|
+
WHERE c.user_id = ? AND c.blocked = 1
|
|
3999
|
+
AND (SELECT COUNT(*) FROM prerequisites p
|
|
4000
|
+
WHERE p.token_id = c.token_id) =
|
|
4001
|
+
(SELECT COUNT(*) FROM prerequisites p
|
|
4002
|
+
JOIN cards pc ON pc.token_id = p.requires_id
|
|
4003
|
+
AND pc.user_id = c.user_id
|
|
4004
|
+
WHERE p.token_id = c.token_id
|
|
4005
|
+
AND pc.reps >= 1 AND pc.blocked = 0)`
|
|
4006
|
+
).all(userId);
|
|
4007
|
+
if (readyCards.length === 0) break;
|
|
4008
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4009
|
+
const placeholders = readyCards.map(() => "?").join(",");
|
|
4010
|
+
await db.prepare(
|
|
4011
|
+
`UPDATE cards SET blocked = 0, due_at = ?
|
|
4012
|
+
WHERE user_id = ? AND token_id IN (${placeholders})`
|
|
4013
|
+
).run(now, userId, ...readyCards.map((card) => card.token_id));
|
|
4014
|
+
for (const card of readyCards) {
|
|
3964
4015
|
unblocked.push({ slug: card.slug, concept: card.concept });
|
|
3965
4016
|
}
|
|
3966
4017
|
}
|
|
@@ -4137,10 +4188,14 @@ async function prepareSessionSynthesis(db, input) {
|
|
|
4137
4188
|
await buildSkillPatterns(db),
|
|
4138
4189
|
input.explicitPatterns ?? []
|
|
4139
4190
|
);
|
|
4191
|
+
const patternTokens = await getTokensBySlugs(
|
|
4192
|
+
db,
|
|
4193
|
+
patterns.map((pattern) => pattern.slug)
|
|
4194
|
+
);
|
|
4140
4195
|
const validPatterns = [];
|
|
4141
4196
|
const tokens = /* @__PURE__ */ new Map();
|
|
4142
4197
|
for (const pattern of patterns) {
|
|
4143
|
-
const token =
|
|
4198
|
+
const token = patternTokens.get(pattern.slug);
|
|
4144
4199
|
if (!token || token.deprecated_at) continue;
|
|
4145
4200
|
validPatterns.push(pattern);
|
|
4146
4201
|
tokens.set(pattern.slug, token);
|
|
@@ -4153,13 +4208,13 @@ async function prepareSessionSynthesis(db, input) {
|
|
|
4153
4208
|
const minConfidence = input.minConfidence ?? "medium";
|
|
4154
4209
|
if (session.execution_context === "ui") {
|
|
4155
4210
|
const reports = readUiObservationLog(input.sessionId);
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4211
|
+
const candidateTokens = await getTokensBySlugs(
|
|
4212
|
+
db,
|
|
4213
|
+
reports.flatMap((report) => report.candidateTokens).map((candidate) => candidate.slug).filter((slug) => !tokens.has(slug))
|
|
4214
|
+
);
|
|
4215
|
+
for (const [slug, token] of candidateTokens) {
|
|
4216
|
+
if (!token.deprecated_at) {
|
|
4217
|
+
tokens.set(slug, token);
|
|
4163
4218
|
}
|
|
4164
4219
|
}
|
|
4165
4220
|
const { candidates: candidates2, skippedLowConfidence: skippedLowConfidence2 } = buildUiSynthesisCandidates(
|
|
@@ -5122,7 +5177,8 @@ async function buildReviewQueue(db, options) {
|
|
|
5122
5177
|
c.state AS state,
|
|
5123
5178
|
c.due_at AS due_at,
|
|
5124
5179
|
t.source_link AS source_link,
|
|
5125
|
-
t.question AS question
|
|
5180
|
+
t.question AS question,
|
|
5181
|
+
t.question_source AS question_source
|
|
5126
5182
|
FROM cards c
|
|
5127
5183
|
JOIN tokens t ON t.id = c.token_id
|
|
5128
5184
|
WHERE c.user_id = ?
|
|
@@ -5152,7 +5208,8 @@ async function buildReviewQueue(db, options) {
|
|
|
5152
5208
|
c.state AS state,
|
|
5153
5209
|
c.due_at AS due_at,
|
|
5154
5210
|
t.source_link AS source_link,
|
|
5155
|
-
t.question AS question
|
|
5211
|
+
t.question AS question,
|
|
5212
|
+
t.question_source AS question_source
|
|
5156
5213
|
FROM cards c
|
|
5157
5214
|
JOIN tokens t ON t.id = c.token_id
|
|
5158
5215
|
WHERE c.user_id = ?
|
|
@@ -5221,7 +5278,8 @@ function rowToItem(row) {
|
|
|
5221
5278
|
state: row.state,
|
|
5222
5279
|
dueAt: row.due_at,
|
|
5223
5280
|
sourceLink: row.source_link,
|
|
5224
|
-
question: row.question
|
|
5281
|
+
question: row.question,
|
|
5282
|
+
questionSource: row.question_source
|
|
5225
5283
|
};
|
|
5226
5284
|
}
|
|
5227
5285
|
function intersperseNew(reviews, newCards, interval) {
|
|
@@ -7023,6 +7081,9 @@ async function generateQuestionViaLLM(db, input) {
|
|
|
7023
7081
|
const bloom = input.bloomLevel >= 1 && input.bloomLevel <= 5 ? input.bloomLevel : 1;
|
|
7024
7082
|
const verb = BLOOM_VERBS2[bloom];
|
|
7025
7083
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
7084
|
+
const existingQuestion = input.existingQuestion?.trim();
|
|
7085
|
+
const variationGuideline = existingQuestion ? `
|
|
7086
|
+
5. A canonical question for this token already exists. Generate a fresh VARIATION of it: test the same knowledge, but with different wording or from a different angle, so the learner cannot memorize the exact phrasing. Never repeat the canonical question verbatim.` : "";
|
|
7026
7087
|
const systemPrompt = `You are ZAM, a highly precise agentic skills trainer.
|
|
7027
7088
|
Your task is to generate a single, clear, conceptual active-recall question (flashcard front) in ${langName} for a knowledge token.
|
|
7028
7089
|
|
|
@@ -7030,12 +7091,13 @@ Guidelines:
|
|
|
7030
7091
|
1. The question MUST match the Bloom level: ${verb} (Level ${bloom}).
|
|
7031
7092
|
2. CRITICAL: The question MUST NOT contain or reveal the concept text itself! The concept is the answer (flashcard back) that the learner needs to recall.
|
|
7032
7093
|
3. Keep the question concise, highly specific, and clear. Avoid generic prompts like "What is the concept of..." if possible, and ask about the core mechanism, function, or purpose of the slug/concept without giving the answer away.
|
|
7033
|
-
4. Output ONLY the raw question text in ${langName}. Do not include any preamble, headers, markdown fences, or conversational filler
|
|
7094
|
+
4. Output ONLY the raw question text in ${langName}. Do not include any preamble, headers, markdown fences, or conversational filler.${variationGuideline}`;
|
|
7034
7095
|
const userPrompt = `Domain: ${input.domain}
|
|
7035
7096
|
Slug: ${input.slug}
|
|
7036
7097
|
Concept to Recall (DO NOT REVEAL IN QUESTION): ${input.concept}
|
|
7037
7098
|
Context: ${input.context || "(none)"}
|
|
7038
|
-
${
|
|
7099
|
+
${existingQuestion ? `Canonical Question (vary, do not repeat verbatim): ${existingQuestion}
|
|
7100
|
+
` : ""}${input.sourceLinkContent ? `Source Reference:
|
|
7039
7101
|
${input.sourceLinkContent}` : ""}
|
|
7040
7102
|
|
|
7041
7103
|
Active-Recall Question:`;
|
|
@@ -7228,7 +7290,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
|
7228
7290
|
}
|
|
7229
7291
|
async function ensureHighQualityQuestion(db, token) {
|
|
7230
7292
|
const { enabled } = await getLlmConfig(db);
|
|
7231
|
-
|
|
7293
|
+
const variationEnabled = await getSetting(db, "llm.dynamic_questions") !== "false";
|
|
7294
|
+
if (enabled && variationEnabled) {
|
|
7232
7295
|
try {
|
|
7233
7296
|
let sourceLinkContent = null;
|
|
7234
7297
|
if (token.sourceLink) {
|
|
@@ -7244,10 +7307,10 @@ async function ensureHighQualityQuestion(db, token) {
|
|
|
7244
7307
|
concept: token.concept,
|
|
7245
7308
|
domain: token.domain,
|
|
7246
7309
|
bloomLevel: token.bloomLevel,
|
|
7247
|
-
sourceLinkContent
|
|
7310
|
+
sourceLinkContent,
|
|
7311
|
+
existingQuestion: token.question
|
|
7248
7312
|
});
|
|
7249
7313
|
if (generated.text.trim().length > 0) {
|
|
7250
|
-
await updateToken(db, token.slug, { question: generated.text });
|
|
7251
7314
|
return {
|
|
7252
7315
|
question: generated.text,
|
|
7253
7316
|
source: "llm",
|
|
@@ -7850,7 +7913,10 @@ async function addToken(db, params) {
|
|
|
7850
7913
|
context: params.context,
|
|
7851
7914
|
symbiosis_mode: params.symbiosisMode,
|
|
7852
7915
|
source_link: params.sourceLink ?? null,
|
|
7853
|
-
question: params.question ?? null
|
|
7916
|
+
question: params.question ?? null,
|
|
7917
|
+
// Bridge/MCP callers are agents: their questions are LLM-authored and
|
|
7918
|
+
// stay refreshable. Humans author questions via the token CLI instead.
|
|
7919
|
+
question_source: params.question ? "llm" : void 0
|
|
7854
7920
|
});
|
|
7855
7921
|
for (const context of assignedContexts) {
|
|
7856
7922
|
await assignTokenToContext(db, token.id, context.id);
|