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/app.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, input8) {
|
|
1758
1779
|
const id = ulid4();
|
|
1759
1780
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1762,9 +1783,11 @@ async function createToken(db, input8) {
|
|
|
1762
1783
|
throw new Error(`bloom_level must be between 1 and 5, got ${bloom}`);
|
|
1763
1784
|
}
|
|
1764
1785
|
const title = input8.title ?? "";
|
|
1786
|
+
const questionSource = input8.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
|
input8.slug,
|
|
@@ -1776,6 +1799,7 @@ async function createToken(db, input8) {
|
|
|
1776
1799
|
input8.symbiosis_mode ?? null,
|
|
1777
1800
|
input8.source_link ?? null,
|
|
1778
1801
|
input8.question ?? null,
|
|
1802
|
+
questionSource,
|
|
1779
1803
|
input8.provider ?? null,
|
|
1780
1804
|
input8.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) {
|
|
@@ -7720,6 +7758,9 @@ async function generateQuestionViaLLM(db, input8) {
|
|
|
7720
7758
|
const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
|
|
7721
7759
|
const verb = BLOOM_VERBS2[bloom];
|
|
7722
7760
|
const langName = LANGUAGE_NAMES[cfg.locale] || "English";
|
|
7761
|
+
const existingQuestion = input8.existingQuestion?.trim();
|
|
7762
|
+
const variationGuideline = existingQuestion ? `
|
|
7763
|
+
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.` : "";
|
|
7723
7764
|
const systemPrompt = `You are ZAM, a highly precise agentic skills trainer.
|
|
7724
7765
|
Your task is to generate a single, clear, conceptual active-recall question (flashcard front) in ${langName} for a knowledge token.
|
|
7725
7766
|
|
|
@@ -7727,12 +7768,13 @@ Guidelines:
|
|
|
7727
7768
|
1. The question MUST match the Bloom level: ${verb} (Level ${bloom}).
|
|
7728
7769
|
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.
|
|
7729
7770
|
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.
|
|
7730
|
-
4. Output ONLY the raw question text in ${langName}. Do not include any preamble, headers, markdown fences, or conversational filler
|
|
7771
|
+
4. Output ONLY the raw question text in ${langName}. Do not include any preamble, headers, markdown fences, or conversational filler.${variationGuideline}`;
|
|
7731
7772
|
const userPrompt = `Domain: ${input8.domain}
|
|
7732
7773
|
Slug: ${input8.slug}
|
|
7733
7774
|
Concept to Recall (DO NOT REVEAL IN QUESTION): ${input8.concept}
|
|
7734
7775
|
Context: ${input8.context || "(none)"}
|
|
7735
|
-
${
|
|
7776
|
+
${existingQuestion ? `Canonical Question (vary, do not repeat verbatim): ${existingQuestion}
|
|
7777
|
+
` : ""}${input8.sourceLinkContent ? `Source Reference:
|
|
7736
7778
|
${input8.sourceLinkContent}` : ""}
|
|
7737
7779
|
|
|
7738
7780
|
Active-Recall Question:`;
|
|
@@ -8831,7 +8873,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
|
|
|
8831
8873
|
}
|
|
8832
8874
|
async function ensureHighQualityQuestion(db, token) {
|
|
8833
8875
|
const { enabled } = await getLlmConfig(db);
|
|
8834
|
-
|
|
8876
|
+
const variationEnabled = await getSetting(db, "llm.dynamic_questions") !== "false";
|
|
8877
|
+
if (enabled && variationEnabled) {
|
|
8835
8878
|
try {
|
|
8836
8879
|
let sourceLinkContent = null;
|
|
8837
8880
|
if (token.sourceLink) {
|
|
@@ -8847,10 +8890,10 @@ async function ensureHighQualityQuestion(db, token) {
|
|
|
8847
8890
|
concept: token.concept,
|
|
8848
8891
|
domain: token.domain,
|
|
8849
8892
|
bloomLevel: token.bloomLevel,
|
|
8850
|
-
sourceLinkContent
|
|
8893
|
+
sourceLinkContent,
|
|
8894
|
+
existingQuestion: token.question
|
|
8851
8895
|
});
|
|
8852
8896
|
if (generated.text.trim().length > 0) {
|
|
8853
|
-
await updateToken(db, token.slug, { question: generated.text });
|
|
8854
8897
|
return {
|
|
8855
8898
|
question: generated.text,
|
|
8856
8899
|
source: "llm",
|
|
@@ -9058,7 +9101,7 @@ async function readWebLink(url) {
|
|
|
9058
9101
|
redirect: "manual",
|
|
9059
9102
|
signal: controller.signal,
|
|
9060
9103
|
headers: {
|
|
9061
|
-
"User-Agent": "ZAM-Content-Studio/0.9.
|
|
9104
|
+
"User-Agent": "ZAM-Content-Studio/0.9.4"
|
|
9062
9105
|
}
|
|
9063
9106
|
});
|
|
9064
9107
|
if (res.status >= 300 && res.status < 400) {
|
|
@@ -9803,7 +9846,10 @@ async function addToken(db, params) {
|
|
|
9803
9846
|
context: params.context,
|
|
9804
9847
|
symbiosis_mode: params.symbiosisMode,
|
|
9805
9848
|
source_link: params.sourceLink ?? null,
|
|
9806
|
-
question: params.question ?? null
|
|
9849
|
+
question: params.question ?? null,
|
|
9850
|
+
// Bridge/MCP callers are agents: their questions are LLM-authored and
|
|
9851
|
+
// stay refreshable. Humans author questions via the token CLI instead.
|
|
9852
|
+
question_source: params.question ? "llm" : void 0
|
|
9807
9853
|
});
|
|
9808
9854
|
for (const context of assignedContexts) {
|
|
9809
9855
|
await assignTokenToContext(db, token.id, context.id);
|