zam-core 0.9.3 → 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 +60 -14
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/commands/mcp.js +59 -13
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +44 -7
- 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,
|
|
@@ -1864,6 +1888,12 @@ async function updateToken(db, slug, updates) {
|
|
|
1864
1888
|
fields.push("question = ?");
|
|
1865
1889
|
values.push(updates.question);
|
|
1866
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
|
+
}
|
|
1867
1897
|
if (updates.provider !== void 0) {
|
|
1868
1898
|
fields.push("provider = ?");
|
|
1869
1899
|
values.push(updates.provider);
|
|
@@ -2192,6 +2222,9 @@ async function importCurriculumCards(db, userId, cards) {
|
|
|
2192
2222
|
symbiosis_mode: symbiosisMode,
|
|
2193
2223
|
source_link: card.source_link || null,
|
|
2194
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",
|
|
2195
2228
|
provider: card.provider || null,
|
|
2196
2229
|
topic_id: card.topic_id || null
|
|
2197
2230
|
});
|
|
@@ -2515,10 +2548,12 @@ async function assertPrerequisiteDoesNotCreateCycle(db, tokenId, prerequisiteId)
|
|
|
2515
2548
|
throw new Error("Cannot add prerequisite: would create a cycle");
|
|
2516
2549
|
}
|
|
2517
2550
|
}
|
|
2551
|
+
var VALID_QUESTION_SOURCES;
|
|
2518
2552
|
var init_token = __esm({
|
|
2519
2553
|
"src/kernel/models/token.ts"() {
|
|
2520
2554
|
"use strict";
|
|
2521
2555
|
init_card();
|
|
2556
|
+
VALID_QUESTION_SOURCES = ["manual", "llm", "template"];
|
|
2522
2557
|
}
|
|
2523
2558
|
});
|
|
2524
2559
|
|
|
@@ -5142,7 +5177,8 @@ async function buildReviewQueue(db, options) {
|
|
|
5142
5177
|
c.state AS state,
|
|
5143
5178
|
c.due_at AS due_at,
|
|
5144
5179
|
t.source_link AS source_link,
|
|
5145
|
-
t.question AS question
|
|
5180
|
+
t.question AS question,
|
|
5181
|
+
t.question_source AS question_source
|
|
5146
5182
|
FROM cards c
|
|
5147
5183
|
JOIN tokens t ON t.id = c.token_id
|
|
5148
5184
|
WHERE c.user_id = ?
|
|
@@ -5172,7 +5208,8 @@ async function buildReviewQueue(db, options) {
|
|
|
5172
5208
|
c.state AS state,
|
|
5173
5209
|
c.due_at AS due_at,
|
|
5174
5210
|
t.source_link AS source_link,
|
|
5175
|
-
t.question AS question
|
|
5211
|
+
t.question AS question,
|
|
5212
|
+
t.question_source AS question_source
|
|
5176
5213
|
FROM cards c
|
|
5177
5214
|
JOIN tokens t ON t.id = c.token_id
|
|
5178
5215
|
WHERE c.user_id = ?
|
|
@@ -5241,7 +5278,8 @@ function rowToItem(row) {
|
|
|
5241
5278
|
state: row.state,
|
|
5242
5279
|
dueAt: row.due_at,
|
|
5243
5280
|
sourceLink: row.source_link,
|
|
5244
|
-
question: row.question
|
|
5281
|
+
question: row.question,
|
|
5282
|
+
questionSource: row.question_source
|
|
5245
5283
|
};
|
|
5246
5284
|
}
|
|
5247
5285
|
function intersperseNew(reviews, newCards, interval) {
|
|
@@ -7043,6 +7081,9 @@ async function generateQuestionViaLLM(db, input) {
|
|
|
7043
7081
|
const bloom = input.bloomLevel >= 1 && input.bloomLevel <= 5 ? input.bloomLevel : 1;
|
|
7044
7082
|
const verb = BLOOM_VERBS2[bloom];
|
|
7045
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.` : "";
|
|
7046
7087
|
const systemPrompt = `You are ZAM, a highly precise agentic skills trainer.
|
|
7047
7088
|
Your task is to generate a single, clear, conceptual active-recall question (flashcard front) in ${langName} for a knowledge token.
|
|
7048
7089
|
|
|
@@ -7050,12 +7091,13 @@ Guidelines:
|
|
|
7050
7091
|
1. The question MUST match the Bloom level: ${verb} (Level ${bloom}).
|
|
7051
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.
|
|
7052
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.
|
|
7053
|
-
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}`;
|
|
7054
7095
|
const userPrompt = `Domain: ${input.domain}
|
|
7055
7096
|
Slug: ${input.slug}
|
|
7056
7097
|
Concept to Recall (DO NOT REVEAL IN QUESTION): ${input.concept}
|
|
7057
7098
|
Context: ${input.context || "(none)"}
|
|
7058
|
-
${
|
|
7099
|
+
${existingQuestion ? `Canonical Question (vary, do not repeat verbatim): ${existingQuestion}
|
|
7100
|
+
` : ""}${input.sourceLinkContent ? `Source Reference:
|
|
7059
7101
|
${input.sourceLinkContent}` : ""}
|
|
7060
7102
|
|
|
7061
7103
|
Active-Recall Question:`;
|
|
@@ -7248,7 +7290,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
|
7248
7290
|
}
|
|
7249
7291
|
async function ensureHighQualityQuestion(db, token) {
|
|
7250
7292
|
const { enabled } = await getLlmConfig(db);
|
|
7251
|
-
|
|
7293
|
+
const variationEnabled = await getSetting(db, "llm.dynamic_questions") !== "false";
|
|
7294
|
+
if (enabled && variationEnabled) {
|
|
7252
7295
|
try {
|
|
7253
7296
|
let sourceLinkContent = null;
|
|
7254
7297
|
if (token.sourceLink) {
|
|
@@ -7264,10 +7307,10 @@ async function ensureHighQualityQuestion(db, token) {
|
|
|
7264
7307
|
concept: token.concept,
|
|
7265
7308
|
domain: token.domain,
|
|
7266
7309
|
bloomLevel: token.bloomLevel,
|
|
7267
|
-
sourceLinkContent
|
|
7310
|
+
sourceLinkContent,
|
|
7311
|
+
existingQuestion: token.question
|
|
7268
7312
|
});
|
|
7269
7313
|
if (generated.text.trim().length > 0) {
|
|
7270
|
-
await updateToken(db, token.slug, { question: generated.text });
|
|
7271
7314
|
return {
|
|
7272
7315
|
question: generated.text,
|
|
7273
7316
|
source: "llm",
|
|
@@ -7870,7 +7913,10 @@ async function addToken(db, params) {
|
|
|
7870
7913
|
context: params.context,
|
|
7871
7914
|
symbiosis_mode: params.symbiosisMode,
|
|
7872
7915
|
source_link: params.sourceLink ?? null,
|
|
7873
|
-
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
|
|
7874
7920
|
});
|
|
7875
7921
|
for (const context of assignedContexts) {
|
|
7876
7922
|
await assignTokenToContext(db, token.id, context.id);
|