dsh-aha-tutor 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -826,6 +826,13 @@ function normalizeConcepts(raw, fallbackName) {
826
826
  function keepId(value, prefix) {
827
827
  return typeof value === "string" && new RegExp(`^${prefix}_[a-z0-9]{6,40}$`, "u").test(value) ? value : newId(prefix);
828
828
  }
829
+ const LESSON_DEPTHS = Object.freeze([
830
+ "intuition",
831
+ "definition",
832
+ "derivation",
833
+ "application",
834
+ "extension"
835
+ ]);
829
836
  function normalizeLesson(raw) {
830
837
  const title = text(raw?.title, 120);
831
838
  if (!title) return null;
@@ -841,7 +848,8 @@ function normalizeLesson(raw) {
841
848
  "remedial",
842
849
  "review",
843
850
  "challenge"
844
- ].includes(raw?.kind) ? raw.kind : "lesson"
851
+ ].includes(raw?.kind) ? raw.kind : "lesson",
852
+ ...LESSON_DEPTHS.includes(raw?.depth) ? { depth: raw.depth } : {}
845
853
  };
846
854
  }
847
855
  function normalizeUnits(rawUnits) {
@@ -1044,6 +1052,49 @@ function normalizePractice(raw, conceptIds = []) {
1044
1052
  id: `i${index + 1}`
1045
1053
  })) };
1046
1054
  }
1055
+ function normalizeHandout(raw) {
1056
+ const points = list(raw?.points ?? raw?.keyPoints, 6, (point) => {
1057
+ const title = text(typeof point === "string" ? point : point?.title ?? point?.point, 160);
1058
+ return title ? {
1059
+ title,
1060
+ oneLine: text(point?.oneLine ?? point?.summary, 300),
1061
+ detail: text(point?.detail, 600)
1062
+ } : null;
1063
+ });
1064
+ if (!points.length) throw new Error("the handout has no key points");
1065
+ const example = raw?.example && typeof raw.example === "object" ? {
1066
+ problem: text(raw.example.problem ?? raw.example.question, 800),
1067
+ steps: strings(raw.example.steps, 10, 400),
1068
+ answer: text(raw.example.answer, 300)
1069
+ } : null;
1070
+ return {
1071
+ title: text(raw?.title, 160),
1072
+ summary: text(raw?.summary ?? raw?.overview, 800),
1073
+ points,
1074
+ example: example?.problem ? example : null,
1075
+ mistakes: list(raw?.mistakes, 5, (item) => {
1076
+ const mistake = text(typeof item === "string" ? item : item?.mistake, 300);
1077
+ return mistake ? {
1078
+ mistake,
1079
+ fix: text(item?.fix ?? item?.instead, 300)
1080
+ } : null;
1081
+ }),
1082
+ glossary: list(raw?.glossary, 10, (item) => {
1083
+ const term = text(item?.term, 80);
1084
+ return term ? {
1085
+ term,
1086
+ meaning: text(item?.meaning ?? item?.definition, 300)
1087
+ } : null;
1088
+ }),
1089
+ questions: list(raw?.questions ?? raw?.selfCheck, 5, (item) => {
1090
+ const question = text(typeof item === "string" ? item : item?.question, 400);
1091
+ return question ? {
1092
+ question,
1093
+ answer: text(item?.answer, 400)
1094
+ } : null;
1095
+ })
1096
+ };
1097
+ }
1047
1098
  function normalizeCards(raw) {
1048
1099
  return list(raw?.cards ?? raw, LIMITS.cards, (card, index) => {
1049
1100
  const front = text(card?.front ?? card?.q ?? card?.question, 600);
@@ -1662,6 +1713,171 @@ function parseJsonLines(raw, { final = true } = {}) {
1662
1713
  };
1663
1714
  }
1664
1715
  //#endregion
1716
+ //#region src/core/retrieval.js
1717
+ const CJK_RUN = /[぀-ヿ㐀-䶿一-鿿가-힯]+/gu;
1718
+ const WORD = /[\p{L}\p{N}][\p{L}\p{N}'_-]*/gu;
1719
+ const STOP = /* @__PURE__ */ new Set([
1720
+ "the",
1721
+ "a",
1722
+ "an",
1723
+ "and",
1724
+ "or",
1725
+ "of",
1726
+ "to",
1727
+ "in",
1728
+ "on",
1729
+ "for",
1730
+ "is",
1731
+ "are",
1732
+ "was",
1733
+ "be",
1734
+ "with",
1735
+ "as",
1736
+ "by",
1737
+ "at",
1738
+ "it",
1739
+ "this",
1740
+ "that",
1741
+ "from"
1742
+ ]);
1743
+ function tokenize(value) {
1744
+ const textValue = String(value ?? "").toLowerCase();
1745
+ const tokens = [];
1746
+ for (const run of textValue.match(CJK_RUN) ?? []) {
1747
+ if (run.length === 1) tokens.push(run);
1748
+ for (let index = 0; index < run.length - 1; index += 1) tokens.push(run.slice(index, index + 2));
1749
+ }
1750
+ for (const word of textValue.replace(CJK_RUN, " ").match(WORD) ?? []) if (word.length > 1 && !STOP.has(word)) tokens.push(word);
1751
+ return tokens;
1752
+ }
1753
+ function buildIndex(chunks) {
1754
+ const docs = chunks.map((chunk) => {
1755
+ const counts = /* @__PURE__ */ new Map();
1756
+ const tokens = tokenize(chunk.text);
1757
+ for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
1758
+ return {
1759
+ chunk,
1760
+ counts,
1761
+ length: tokens.length
1762
+ };
1763
+ });
1764
+ const df = /* @__PURE__ */ new Map();
1765
+ for (const doc of docs) for (const token of doc.counts.keys()) df.set(token, (df.get(token) ?? 0) + 1);
1766
+ return {
1767
+ docs,
1768
+ df,
1769
+ avg: docs.reduce((sum, doc) => sum + doc.length, 0) / Math.max(1, docs.length)
1770
+ };
1771
+ }
1772
+ function search(index, query, { limit = 6, k1 = 1.4, b = .75 } = {}) {
1773
+ const terms = [...new Set(tokenize(query))];
1774
+ if (!terms.length || !index.docs.length) return [];
1775
+ const total = index.docs.length;
1776
+ return index.docs.map((doc) => {
1777
+ let score = 0;
1778
+ for (const term of terms) {
1779
+ const tf = doc.counts.get(term);
1780
+ if (!tf) continue;
1781
+ const df = index.df.get(term) ?? 0;
1782
+ const idf = Math.log(1 + (total - df + .5) / (df + .5));
1783
+ score += idf * (tf * (k1 + 1) / (tf + k1 * (1 - b + b * (doc.length / (index.avg || 1)))));
1784
+ }
1785
+ return {
1786
+ chunk: doc.chunk,
1787
+ score
1788
+ };
1789
+ }).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
1790
+ }
1791
+ function selectPassages(chunks, query, { maxChars = 12e3, limit = 12 } = {}) {
1792
+ if (!chunks?.length) return [];
1793
+ const picked = query ? search(buildIndex(chunks), query, { limit }).map((entry) => entry.chunk) : chunks.slice(0, limit);
1794
+ const result = [];
1795
+ let used = 0;
1796
+ for (const chunk of picked) {
1797
+ if (used + chunk.text.length > maxChars) {
1798
+ const room = maxChars - used;
1799
+ if (room > 400) result.push({
1800
+ ...chunk,
1801
+ text: `${chunk.text.slice(0, room)}…`
1802
+ });
1803
+ break;
1804
+ }
1805
+ result.push(chunk);
1806
+ used += chunk.text.length;
1807
+ }
1808
+ return result;
1809
+ }
1810
+ function spreadPassages(chunks, { maxChars = 12e3, samples = 16 } = {}) {
1811
+ if (!chunks?.length) return [];
1812
+ if (chunks.reduce((sum, chunk) => sum + chunk.text.length, 0) <= maxChars) return chunks.slice();
1813
+ const count = Math.min(chunks.length, Math.max(2, samples));
1814
+ const share = Math.max(200, Math.floor(maxChars / count));
1815
+ const picked = [];
1816
+ for (let index = 0; index < count; index += 1) {
1817
+ const at = Math.round(index * (chunks.length - 1) / (count - 1));
1818
+ if (picked.at(-1) !== at) picked.push(at);
1819
+ }
1820
+ return picked.map((at) => {
1821
+ const chunk = chunks[at];
1822
+ return chunk.text.length > share ? {
1823
+ ...chunk,
1824
+ text: `${chunk.text.slice(0, share)}…`
1825
+ } : chunk;
1826
+ });
1827
+ }
1828
+ function documentParts(chunks, { partChars = 2e4, maxParts = 24, maxPartChars = 4e4 } = {}) {
1829
+ if (!chunks?.length) return [];
1830
+ const total = Math.max(1, chunks.reduce((sum, chunk) => sum + chunk.text.length, 0));
1831
+ const count = Math.min(maxParts, Math.max(1, Math.ceil(total / partChars)));
1832
+ const parts = Array.from({ length: count }, () => []);
1833
+ let seen = 0;
1834
+ for (const chunk of chunks) {
1835
+ parts[Math.min(count - 1, Math.floor(seen / total * count))].push(chunk);
1836
+ seen += chunk.text.length;
1837
+ }
1838
+ return parts.filter((part) => part.length).map((part) => {
1839
+ return part.reduce((sum, chunk) => sum + chunk.text.length, 0) > maxPartChars ? spreadPassages(part, {
1840
+ maxChars: maxPartChars,
1841
+ samples: 24
1842
+ }) : part;
1843
+ });
1844
+ }
1845
+ function locationOf(passage) {
1846
+ return passage?.page ? `p.${passage.page}` : passage?.slide ? `slide ${passage.slide}` : passage?.section ?? "";
1847
+ }
1848
+ function passageRange(passages) {
1849
+ const first = locationOf(passages?.[0]);
1850
+ const last = locationOf(passages?.at(-1));
1851
+ return first && last && first !== last ? `${first}–${last}` : first || last;
1852
+ }
1853
+ function formatPassages(passages, sourceTitle) {
1854
+ return passages.map((passage) => {
1855
+ const where = locationOf(passage);
1856
+ return `<<${[sourceTitle ?? passage.source ?? "source", where].filter(Boolean).join(" · ") || "excerpt"}>>\n${passage.text}`;
1857
+ }).join("\n\n");
1858
+ }
1859
+ function snippetOf(text, query, { width = 140 } = {}) {
1860
+ const value = String(text ?? "").replace(/\*\*|__|`/gu, "").replace(/^#{1,6}\s+/gmu, "").replace(/\s*\|\s*(?:\|\s*)*/gu, " · ").replace(/\s+/gu, " ").replace(/^ · | · $/gu, "").trim();
1861
+ if (value.length <= width) return value;
1862
+ const lower = value.toLowerCase();
1863
+ const terms = [...new Set(tokenize(query))];
1864
+ const hits = [];
1865
+ for (const term of terms) for (let at = lower.indexOf(term); at !== -1 && hits.length < 200; at = lower.indexOf(term, at + term.length)) hits.push(at);
1866
+ if (!hits.length) return `${value.slice(0, width)}…`;
1867
+ hits.sort((a, b) => a - b);
1868
+ let best = hits[0];
1869
+ let bestCount = 0;
1870
+ for (const start of hits) {
1871
+ const count = hits.filter((at) => at >= start && at < start + width).length;
1872
+ if (count > bestCount) {
1873
+ best = start;
1874
+ bestCount = count;
1875
+ }
1876
+ }
1877
+ const from = Math.max(0, Math.min(best - Math.round(width / 4), value.length - width));
1878
+ return `${from > 0 ? "…" : ""}${value.slice(from, from + width)}${from + width < value.length ? "…" : ""}`;
1879
+ }
1880
+ //#endregion
1665
1881
  //#region src/core/mastery.js
1666
1882
  const BKT_DEFAULTS = Object.freeze({
1667
1883
  pInit: .2,
@@ -1866,152 +2082,9 @@ function summarizeForPrompt(states, limit = 12) {
1866
2082
  }));
1867
2083
  }
1868
2084
  //#endregion
1869
- //#region src/core/retrieval.js
1870
- const CJK_RUN = /[぀-ヿ㐀-䶿一-鿿가-힯]+/gu;
1871
- const WORD = /[\p{L}\p{N}][\p{L}\p{N}'_-]*/gu;
1872
- const STOP = /* @__PURE__ */ new Set([
1873
- "the",
1874
- "a",
1875
- "an",
1876
- "and",
1877
- "or",
1878
- "of",
1879
- "to",
1880
- "in",
1881
- "on",
1882
- "for",
1883
- "is",
1884
- "are",
1885
- "was",
1886
- "be",
1887
- "with",
1888
- "as",
1889
- "by",
1890
- "at",
1891
- "it",
1892
- "this",
1893
- "that",
1894
- "from"
1895
- ]);
1896
- function tokenize(value) {
1897
- const textValue = String(value ?? "").toLowerCase();
1898
- const tokens = [];
1899
- for (const run of textValue.match(CJK_RUN) ?? []) {
1900
- if (run.length === 1) tokens.push(run);
1901
- for (let index = 0; index < run.length - 1; index += 1) tokens.push(run.slice(index, index + 2));
1902
- }
1903
- for (const word of textValue.replace(CJK_RUN, " ").match(WORD) ?? []) if (word.length > 1 && !STOP.has(word)) tokens.push(word);
1904
- return tokens;
1905
- }
1906
- function buildIndex(chunks) {
1907
- const docs = chunks.map((chunk) => {
1908
- const counts = /* @__PURE__ */ new Map();
1909
- const tokens = tokenize(chunk.text);
1910
- for (const token of tokens) counts.set(token, (counts.get(token) ?? 0) + 1);
1911
- return {
1912
- chunk,
1913
- counts,
1914
- length: tokens.length
1915
- };
1916
- });
1917
- const df = /* @__PURE__ */ new Map();
1918
- for (const doc of docs) for (const token of doc.counts.keys()) df.set(token, (df.get(token) ?? 0) + 1);
1919
- return {
1920
- docs,
1921
- df,
1922
- avg: docs.reduce((sum, doc) => sum + doc.length, 0) / Math.max(1, docs.length)
1923
- };
1924
- }
1925
- function search(index, query, { limit = 6, k1 = 1.4, b = .75 } = {}) {
1926
- const terms = [...new Set(tokenize(query))];
1927
- if (!terms.length || !index.docs.length) return [];
1928
- const total = index.docs.length;
1929
- return index.docs.map((doc) => {
1930
- let score = 0;
1931
- for (const term of terms) {
1932
- const tf = doc.counts.get(term);
1933
- if (!tf) continue;
1934
- const df = index.df.get(term) ?? 0;
1935
- const idf = Math.log(1 + (total - df + .5) / (df + .5));
1936
- score += idf * (tf * (k1 + 1) / (tf + k1 * (1 - b + b * (doc.length / (index.avg || 1)))));
1937
- }
1938
- return {
1939
- chunk: doc.chunk,
1940
- score
1941
- };
1942
- }).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
1943
- }
1944
- function selectPassages(chunks, query, { maxChars = 12e3, limit = 12 } = {}) {
1945
- if (!chunks?.length) return [];
1946
- const picked = query ? search(buildIndex(chunks), query, { limit }).map((entry) => entry.chunk) : chunks.slice(0, limit);
1947
- const result = [];
1948
- let used = 0;
1949
- for (const chunk of picked) {
1950
- if (used + chunk.text.length > maxChars) {
1951
- const room = maxChars - used;
1952
- if (room > 400) result.push({
1953
- ...chunk,
1954
- text: `${chunk.text.slice(0, room)}…`
1955
- });
1956
- break;
1957
- }
1958
- result.push(chunk);
1959
- used += chunk.text.length;
1960
- }
1961
- return result;
1962
- }
1963
- function spreadPassages(chunks, { maxChars = 12e3, samples = 16 } = {}) {
1964
- if (!chunks?.length) return [];
1965
- if (chunks.reduce((sum, chunk) => sum + chunk.text.length, 0) <= maxChars) return chunks.slice();
1966
- const count = Math.min(chunks.length, Math.max(2, samples));
1967
- const share = Math.max(200, Math.floor(maxChars / count));
1968
- const picked = [];
1969
- for (let index = 0; index < count; index += 1) {
1970
- const at = Math.round(index * (chunks.length - 1) / (count - 1));
1971
- if (picked.at(-1) !== at) picked.push(at);
1972
- }
1973
- return picked.map((at) => {
1974
- const chunk = chunks[at];
1975
- return chunk.text.length > share ? {
1976
- ...chunk,
1977
- text: `${chunk.text.slice(0, share)}…`
1978
- } : chunk;
1979
- });
1980
- }
1981
- function documentParts(chunks, { partChars = 2e4, maxParts = 24, maxPartChars = 4e4 } = {}) {
1982
- if (!chunks?.length) return [];
1983
- const total = Math.max(1, chunks.reduce((sum, chunk) => sum + chunk.text.length, 0));
1984
- const count = Math.min(maxParts, Math.max(1, Math.ceil(total / partChars)));
1985
- const parts = Array.from({ length: count }, () => []);
1986
- let seen = 0;
1987
- for (const chunk of chunks) {
1988
- parts[Math.min(count - 1, Math.floor(seen / total * count))].push(chunk);
1989
- seen += chunk.text.length;
1990
- }
1991
- return parts.filter((part) => part.length).map((part) => {
1992
- return part.reduce((sum, chunk) => sum + chunk.text.length, 0) > maxPartChars ? spreadPassages(part, {
1993
- maxChars: maxPartChars,
1994
- samples: 24
1995
- }) : part;
1996
- });
1997
- }
1998
- function locationOf(passage) {
1999
- return passage?.page ? `p.${passage.page}` : passage?.slide ? `slide ${passage.slide}` : passage?.section ?? "";
2000
- }
2001
- function passageRange(passages) {
2002
- const first = locationOf(passages?.[0]);
2003
- const last = locationOf(passages?.at(-1));
2004
- return first && last && first !== last ? `${first}–${last}` : first || last;
2005
- }
2006
- function formatPassages(passages, sourceTitle) {
2007
- return passages.map((passage) => {
2008
- const where = locationOf(passage);
2009
- return `<<${[sourceTitle ?? passage.source ?? "source", where].filter(Boolean).join(" · ") || "excerpt"}>>\n${passage.text}`;
2010
- }).join("\n\n");
2011
- }
2012
- //#endregion
2013
2085
  //#region src/host/context.js
2014
2086
  const EMPTY_PROFILE = Object.freeze({
2087
+ name: "",
2015
2088
  background: "",
2016
2089
  goals: [],
2017
2090
  strengths: [],
@@ -2056,7 +2129,7 @@ function masteryFor(course, progress, lessonId) {
2056
2129
  }
2057
2130
  async function readMaterials(store, materialIds = []) {
2058
2131
  const items = [];
2059
- for (const id of materialIds.slice(0, 8)) try {
2132
+ for (const id of materialIds.slice(0, 12)) try {
2060
2133
  const meta = await store.readJson(store.paths.material(id), null);
2061
2134
  if (!meta) continue;
2062
2135
  const chunks = meta.kind === "image" ? [] : await store.readJson(store.paths.materialChunks(id), []) ?? [];
@@ -2323,6 +2396,7 @@ function join(...parts) {
2323
2396
  }
2324
2397
  function learnerBlock({ profile, mastery } = {}) {
2325
2398
  const lines = [];
2399
+ if (profile?.name) lines.push(`The learner likes to be called: ${profile.name} (use it sparingly, as a friendly tutor would)`);
2326
2400
  if (profile?.background) lines.push(`Background: ${profile.background}`);
2327
2401
  if (profile?.goals?.length) lines.push(`Goals: ${profile.goals.join("; ")}`);
2328
2402
  if (profile?.preferences?.style) lines.push(`Prefers explanations that are: ${profile.preferences.style}`);
@@ -2414,10 +2488,11 @@ function blueprintPrompt({ goal, language, intake, materialsSummary, profile, ti
2414
2488
  - If materials were provided, follow their scope and order where sensible.
2415
2489
  - If web sources are given, use them for scope, ordering and current practice, and prefer what several of them agree on.
2416
2490
  - Each lesson lists 1–3 learning objectives (observable: "compute…", "explain why…") and 1–4 key concepts (short names).
2491
+ - Give each lesson a "depth": "intuition" (a feel for the idea through pictures and cases), "definition" (stating it precisely), "derivation" (deriving or proving it), "application" (using it on problems) or "extension" (edge cases, generalizations, connections). Let ideas be felt before they are formalized.
2417
2492
  - Add 1–3 projects (one is enough for a short course) that apply the course to something real for this learner, placed after the units they depend on. Use "kind":"code" only when the project is mainly programming or data work.
2418
2493
  - Pick one emoji that fits the subject.
2419
2494
 
2420
- Return {"title":"…","subtitle":"one line promise of the course","emoji":"…","summary":"2–3 sentences: who it is for, what they will be able to do","level":"beginner|intermediate|advanced","units":[{"title":"…","summary":"…","objectives":["…"],"lectures":[{"title":"…","summary":"…","lessons":[{"title":"…","summary":"…","objectives":["…"],"concepts":["…"],"estMinutes":15}]}]}],"projects":[{"title":"…","kind":"code|writing|analysis|design|practice|other","brief":"…","skills":["…"],"unitIndex":<0-based unit after which it fits>,"milestones":["…"],"rubric":[{"criterion":"…","points":10}]}]}`)
2495
+ Return {"title":"…","subtitle":"one line promise of the course","emoji":"…","summary":"2–3 sentences: who it is for, what they will be able to do","level":"beginner|intermediate|advanced","units":[{"title":"…","summary":"…","objectives":["…"],"lectures":[{"title":"…","summary":"…","lessons":[{"title":"…","summary":"…","objectives":["…"],"concepts":["…"],"estMinutes":15,"depth":"intuition|definition|derivation|application|extension"}]}]}],"projects":[{"title":"…","kind":"code|writing|analysis|design|practice|other","brief":"…","skills":["…"],"unitIndex":<0-based unit after which it fits>,"milestones":["…"],"rubric":[{"criterion":"…","points":10}]}]}`)
2421
2496
  };
2422
2497
  }
2423
2498
  function revisePrompt({ blueprint, instruction, language }) {
@@ -2427,10 +2502,16 @@ function revisePrompt({ blueprint, instruction, language }) {
2427
2502
  };
2428
2503
  }
2429
2504
  const ITEM_FIGURE = "When a figure makes a question clearer (a shape to measure, a graph to read, a circuit, a diagram to label), give the item \"svg\": a small diagram following the same rules as the board's svg op (viewBox at most 480×320). For the graph of a formula give \"plot\" instead: an object shaped like the board's plot op, without \"params\". Most items need no figure.";
2505
+ function materialsRevisePrompt({ blueprint, materials, passages, instruction, language }) {
2506
+ return {
2507
+ system: join(PERSONA, `You revise course blueprints to make good use of materials the learner has added, keeping everything else intact. ${languageRule(language)}`, JSON_ONLY),
2508
+ user: join(block("current_blueprint", blueprint), block("new_materials", materials), block("material_passages", passages), block("learner_request", instruction), `The learner added these materials to the course. Revise the blueprint so the course uses them: add lessons, or a unit, for what they cover that the course lacks, placed where the prerequisites allow; take existing lessons deeper where the materials go further; follow any request above. Keep everything else as it is, and remove a lesson only if the materials show it is wrong. Keep the "id" of every unit, lecture, lesson and concept you do not substantially change; new items get no id. Give every new lesson a "depth". Return the complete revised blueprint in the same JSON shape.`)
2509
+ };
2510
+ }
2430
2511
  function reworkPrompt({ outline, unitTitle, lectureTitle, lesson, mode, language }) {
2431
2512
  return {
2432
2513
  system: join(PERSONA, `You design personal courses with the care of an experienced curriculum designer: clear prerequisites first, one new idea per lesson. ${languageRule(language)}`, JSON_ONLY),
2433
- user: join(block("course_outline", outline), block("position", `Unit: ${unitTitle}\nLecture: ${lectureTitle}`), block("lesson", lesson), mode === "split" ? "Split this lesson into two consecutive lessons that together cover everything it covers. Each teaches one coherent idea in 10–20 minutes, and the first prepares the second. Stay consistent with the rest of the course." : "Rewrite this lesson to go deeper. Keep its topic and its place in the course, but aim at why it works, its edge cases and its connections to the rest of the course; adjust the objectives, concepts and minutes (at most 25) to match.", "Return {\"lessons\":[{\"title\":\"…\",\"summary\":\"…\",\"objectives\":[\"…\"],\"concepts\":[\"…\"],\"estMinutes\":15}]}")
2514
+ user: join(block("course_outline", outline), block("position", `Unit: ${unitTitle}\nLecture: ${lectureTitle}`), block("lesson", lesson), mode === "split" ? "Split this lesson into two consecutive lessons that together cover everything it covers. Each teaches one coherent idea in 10–20 minutes, and the first prepares the second. Stay consistent with the rest of the course." : "Rewrite this lesson to go deeper. Keep its topic and its place in the course, but aim at why it works, its edge cases and its connections to the rest of the course; adjust the objectives, concepts and minutes (at most 25) to match.", "Return {\"lessons\":[{\"title\":\"…\",\"summary\":\"…\",\"objectives\":[\"…\"],\"concepts\":[\"…\"],\"estMinutes\":15,\"depth\":\"intuition|definition|derivation|application|extension\"}]}")
2434
2515
  };
2435
2516
  }
2436
2517
  function practicePrompt({ course, lecture, lessons, language, passages }) {
@@ -2460,12 +2541,19 @@ const DIRECTIVES = Object.freeze({
2460
2541
  challenge: "The learner has mastered the prerequisites easily: move briskly through basics, add depth (why it works, edge cases, connections) and make the checks harder.",
2461
2542
  review: "This is a REVIEW lesson: retrieve before re-explaining. Open with a question, then consolidate the key ideas with a compact board and mixed checks."
2462
2543
  });
2544
+ const DEPTH_GUIDANCE = Object.freeze({
2545
+ intuition: "This lesson builds intuition: lead with pictures, analogies and concrete cases, keep the formalism light, and leave proofs for later lessons.",
2546
+ definition: "This lesson makes the idea precise: state the definitions and notation carefully, with examples and non-examples.",
2547
+ derivation: "This lesson derives or proves the result: work every step on the board and say why each step holds.",
2548
+ application: "This lesson applies the idea: work through varied problems, from routine to less familiar, and show how to choose the method.",
2549
+ extension: "This lesson goes further: edge cases, generalizations and connections to other topics."
2550
+ });
2463
2551
  function lessonPrompt({ course, unit, lecture, lesson, language, learner, passages, previous, directive, style }) {
2464
2552
  const steps = Math.min(18, Math.max(7, Math.round((lesson.estMinutes || 15) * .7)));
2465
2553
  return {
2466
2554
  system: join(PERSONA, BOARD_SPEC, STEP_SPEC, languageRule(language), JSONL_ONLY),
2467
- user: join(block("course", `${course.title} (${course.level || "mixed level"}) — ${course.summary ?? ""}`), block("position", `Unit: ${unit.title}\nLecture: ${lecture.title}\nLesson: ${lesson.title}\nObjectives: ${lesson.objectives.join("; ")}\nConcepts (id=name): ${lesson.concepts.map((concept) => `${concept.id}=${concept.name}`).join(", ")}`), block("previous_lesson", previous), learnerBlock(learner), block("source_passages", passages), style ? block("style_request", style) : "", DIRECTIVES[directive] ?? "", `Teach this lesson in about ${steps} steps (~${lesson.estMinutes || 15} minutes).
2468
- - Open with a hook: a question, a surprising fact, or a concrete situation the idea explains. Connect to the previous lesson when there is one.
2555
+ user: join(block("course", `${course.title} (${course.level || "mixed level"}) — ${course.summary ?? ""}`), block("position", `Unit: ${unit.title}\nLecture: ${lecture.title}\nLesson: ${lesson.title}\nObjectives: ${lesson.objectives.join("; ")}\nConcepts (id=name): ${lesson.concepts.map((concept) => `${concept.id}=${concept.name}`).join(", ")}`), block("previous_lesson", previous), learnerBlock(learner), block("source_passages", passages), style ? block("style_request", style) : "", DIRECTIVES[directive] ?? "", DEPTH_GUIDANCE[lesson.depth] ?? "", `Teach this lesson in about ${steps} steps (~${lesson.estMinutes || 15} minutes).
2556
+ - Open with a hook: a question, a surprising fact, or a concrete situation the idea explains. Connect to the previous lesson when there is one.${learner?.profile?.name ? " Call the learner by name once, in the opening step." : ""}
2469
2557
  - Build intuition before formalism; then make it precise; then work at least one example fully on the board.
2470
2558
  - Draw a diagram, chart or plot whenever a picture makes the idea click; a plot with a slider is often the clearest way to show how a formula behaves.
2471
2559
  - Put 2–4 checks after the key ideas, plus one at the end; every check must name its conceptIds.
@@ -2515,6 +2603,12 @@ function correctionPrompt({ course, lesson, language, boardSoFar, item, response
2515
2603
  user: join(block("course", course ? course.title : ""), block("lesson", lesson ? `${lesson.title}: ${lesson.objectives?.join("; ") ?? ""}` : ""), block("board_so_far", boardSoFar), block("problem", item.prompt), block("model_working", item.answer), block("learner_working", response), block("grader_notes", notes), "The learner just wrote out their working for this problem and went wrong somewhere. Correct it at the board in 2–3 steps, continuing on the current board: (1) write the learner's wrong line as they wrote it, in an op with an \"id\", then {\"t\":\"mark\",\"ref\":\"<that id>\",\"style\":\"box\"}, and say kindly what went wrong there and why; (2) write the corrected line and carry the working on to the right answer; (3) end with a short {\"t\":\"note\",\"tone\":\"key\",\"text\":\"…\"} holding the rule to remember. Speak to the learner directly and keep it brief. Add no check, and do not continue the lesson itself.")
2516
2604
  };
2517
2605
  }
2606
+ function handoutPrompt({ course, lesson, language, board }) {
2607
+ return {
2608
+ system: join(PERSONA, `You turn a lesson into a one-page handout a learner keeps: faithful to what was taught, short, exact. ${languageRule(language)}`, JSON_ONLY),
2609
+ user: join(block("course", course ? course.title : ""), block("lesson", `${lesson.title}: ${lesson.objectives?.join("; ") ?? ""}`), block("what_the_board_taught", board), "Write the handout from what the board taught; add nothing it did not teach. \"points\": the 3–6 key points in order, each with a \"oneLine\" a learner could repeat to a friend. \"example\": one worked example with numbers, solved step by step (one step per line). \"mistakes\": 2–4 common mistakes with the fix. \"glossary\": the terms the lesson introduced. \"questions\": 3 short questions to check oneself, with answers. Use inline math $…$ for formulas.\n\nReturn {\"title\":\"…\",\"summary\":\"2–3 sentences: what this lesson was about\",\"points\":[{\"title\":\"…\",\"oneLine\":\"…\",\"detail\":\"…\"}],\"example\":{\"problem\":\"…\",\"steps\":[\"…\"],\"answer\":\"…\"},\"mistakes\":[{\"mistake\":\"…\",\"fix\":\"…\"}],\"glossary\":[{\"term\":\"…\",\"meaning\":\"…\"}],\"questions\":[{\"question\":\"…\",\"answer\":\"…\"}]}")
2610
+ };
2611
+ }
2518
2612
  function verifyStepPrompt({ course, lesson, language, board, step, stepId, note }) {
2519
2613
  return {
2520
2614
  system: join(PERSONA, `The learner thinks a step of your lesson is wrong. Check it the way a careful examiner would: recompute every number, re-derive every formula, and check every fact, unit, sign, label, graph and answer key. Never defend a mistake and never invent one; a different but valid method or notation is not a mistake. ${languageRule(language)}`, JSON_ONLY),
@@ -2647,4 +2741,4 @@ function getRuntime() {
2647
2741
  return runtime;
2648
2742
  }
2649
2743
  //#endregion
2650
- export { BOARD_LIMITS as $, TutorError as A, passagesFor as B, researchQueriesPrompt as C, text as Ct, summaryPrompt as D, shortPrompt as E, newId as Et, EMPTY_PROFILE as F, formatPassages as G, readProfile as H, emptyProgress as I, applyEvidence as J, passageRange as K, masteryFor as L, generateSteps as M, generateText as N, verifyItemPrompt as O, streamCounter as P, extractJson as Q, materialImages as R, reexplainPrompt as S, safeRelativePath as St, reworkPrompt as T, assertId as Tt, readProgress as U, readMaterials as V, documentParts as W, masteryPercent as X, createConceptState as Y, recommendNext as Z, profilePrompt as _, normalizeLesson as _t, blueprintPrompt as a, conceptIndex as at, questionnairePrompt as b, normalizeQuestionnaire as bt, correctionPrompt as c, flattenLessons as ct, digestPrompt as d, lessonAfter as dt, boardToPlainText as et, flashcardsPrompt as f, nextLesson as ft, practicePrompt as g, normalizeItem as gt, materialBriefPrompt as h, normalizeCheatsheet as ht, assistPrompt as i, blueprintOutline as it, generateJson as j, verifyStepPrompt as k, detectLanguage as l, insertLesson as lt, lessonPrompt as m, normalizeCards as mt, publishRuntime as n, normalizeBoard as nt, challengePrompt as o, courseProgress as ot, gradePrompt as p, normalizeBlueprint as pt, spreadPassages as q, askPrompt as r, normalizeOp as rt, cheatsheetPrompt as s, findLesson as st, getRuntime as t, estimateSpeechMs as tt, digestPartPrompt as u, intakeSummary as ut, projectReviewPrompt as v, normalizePractice as vt, revisePrompt as w, plotToText as wt, quizPrompt as x, outlineForPrompt as xt, projectsPrompt as y, normalizeProject as yt, materialsSummary as z };
2744
+ export { passageRange as $, verifyItemPrompt as A, plotToText as At, materialImages as B, quizPrompt as C, normalizeLesson as Ct, reworkPrompt as D, outlineForPrompt as Dt, revisePrompt as E, normalizeQuestionnaire as Et, generateText as F, readProgress as G, passagesFor as H, streamCounter as I, masteryPercent as J, applyEvidence as K, EMPTY_PROFILE as L, TutorError as M, newId as Mt, generateJson as N, shortPrompt as O, safeRelativePath as Ot, generateSteps as P, formatPassages as Q, emptyProgress as R, questionnairePrompt as S, normalizeItem as St, researchQueriesPrompt as T, normalizeProject as Tt, readMaterials as U, materialsSummary as V, readProfile as W, buildIndex as X, recommendNext as Y, documentParts as Z, materialsRevisePrompt as _, nextLesson as _t, blueprintPrompt as a, boardToPlainText as at, projectReviewPrompt as b, normalizeCheatsheet as bt, correctionPrompt as c, normalizeOp as ct, digestPrompt as d, courseProgress as dt, search as et, flashcardsPrompt as f, findLesson as ft, materialBriefPrompt as g, lessonAfter as gt, lessonPrompt as h, intakeSummary as ht, assistPrompt as i, BOARD_LIMITS as it, verifyStepPrompt as j, assertId as jt, summaryPrompt as k, text as kt, detectLanguage as l, blueprintOutline as lt, handoutPrompt as m, insertLesson as mt, publishRuntime as n, spreadPassages as nt, challengePrompt as o, estimateSpeechMs as ot, gradePrompt as p, flattenLessons as pt, createConceptState as q, askPrompt as r, extractJson as rt, cheatsheetPrompt as s, normalizeBoard as st, getRuntime as t, snippetOf as tt, digestPartPrompt as u, conceptIndex as ut, practicePrompt as v, normalizeBlueprint as vt, reexplainPrompt as w, normalizePractice as wt, projectsPrompt as x, normalizeHandout as xt, profilePrompt as y, normalizeCards as yt, masteryFor as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-aha-tutor",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "开窍 Aha Tutor: a personal AI tutor for DeepSeek Harness that builds a course around you and teaches it on a narrated handwriting whiteboard",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",