keating 0.3.6
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/README.md +334 -0
- package/SYSTEM.md +33 -0
- package/bin/keating.js +31 -0
- package/dist/src/cli/main.js +357 -0
- package/dist/src/cli/setup.js +197 -0
- package/dist/src/cli/web.js +84 -0
- package/dist/src/core/animation.js +304 -0
- package/dist/src/core/ax-optimizer.js +81 -0
- package/dist/src/core/ax-prompt-learner.js +59 -0
- package/dist/src/core/ax-trial.js +181 -0
- package/dist/src/core/benchmark.js +253 -0
- package/dist/src/core/commands.js +57 -0
- package/dist/src/core/config.js +120 -0
- package/dist/src/core/engagement.js +235 -0
- package/dist/src/core/env.js +9 -0
- package/dist/src/core/evolution.js +242 -0
- package/dist/src/core/flashcards.js +133 -0
- package/dist/src/core/learner-state.js +108 -0
- package/dist/src/core/lesson-plan.js +155 -0
- package/dist/src/core/map-elites.js +228 -0
- package/dist/src/core/map.js +89 -0
- package/dist/src/core/mastery.js +207 -0
- package/dist/src/core/paths.js +100 -0
- package/dist/src/core/pi-agent.js +82 -0
- package/dist/src/core/policy.js +79 -0
- package/dist/src/core/project.js +337 -0
- package/dist/src/core/projects.js +281 -0
- package/dist/src/core/prompt-evolution.js +344 -0
- package/dist/src/core/quiz.js +194 -0
- package/dist/src/core/random.js +19 -0
- package/dist/src/core/self-improve.js +425 -0
- package/dist/src/core/speech.js +54 -0
- package/dist/src/core/terminal.js +117 -0
- package/dist/src/core/theme.js +101 -0
- package/dist/src/core/topics.js +620 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/core/util.js +30 -0
- package/dist/src/core/verification.js +162 -0
- package/dist/src/pi/hyperteacher-extension.js +573 -0
- package/dist/src/runtime/pi.js +343 -0
- package/package.json +78 -0
- package/pi/prompts/bridge.md +14 -0
- package/pi/prompts/diagnose.md +15 -0
- package/pi/prompts/improve.md +39 -0
- package/pi/prompts/learn.md +21 -0
- package/pi/prompts/quiz.md +14 -0
- package/pi/skills/adaptive-teaching/SKILL.md +33 -0
- package/scripts/install/install.sh +308 -0
- package/web/dist/.well-known/llms.txt +44 -0
- package/web/dist/apple-touch-icon.svg +10 -0
- package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/web/dist/assets/_baseFor-B_cjfMB6.js +1 -0
- package/web/dist/assets/anthropic-BT6Vfzb1.js +36 -0
- package/web/dist/assets/arc-x2nTilpc.js +1 -0
- package/web/dist/assets/architecture-YZFGNWBL-B1hlUWjX.js +1 -0
- package/web/dist/assets/architectureDiagram-Q4EWVU46-CMApWFyw.js +36 -0
- package/web/dist/assets/array-B9UHiPd-.js +1 -0
- package/web/dist/assets/azure-openai-responses-CommX3YJ.js +1 -0
- package/web/dist/assets/blockDiagram-DXYQGD6D-DOQbsNRY.js +132 -0
- package/web/dist/assets/c4Diagram-AHTNJAMY-VFfRZWWA.js +10 -0
- package/web/dist/assets/channel-KY2Tg8Ba.js +1 -0
- package/web/dist/assets/chunk-2KRD3SAO-B-AqvS0u.js +1 -0
- package/web/dist/assets/chunk-336JU56O-DlYgPyl6.js +2 -0
- package/web/dist/assets/chunk-426QAEUC-CsVoBkfR.js +1 -0
- package/web/dist/assets/chunk-4BX2VUAB-0Z13aFAn.js +1 -0
- package/web/dist/assets/chunk-4TB4RGXK-DqC0Zwm7.js +206 -0
- package/web/dist/assets/chunk-55IACEB6-CWE_u-IY.js +1 -0
- package/web/dist/assets/chunk-5FUZZQ4R-CApli0xX.js +62 -0
- package/web/dist/assets/chunk-5PVQY5BW-Cbzhfhln.js +2 -0
- package/web/dist/assets/chunk-67CJDMHE-Cx7uJS4d.js +1 -0
- package/web/dist/assets/chunk-7N4EOEYR-CYPNsFus.js +1 -0
- package/web/dist/assets/chunk-AA7GKIK3-rU0uhR_u.js +1 -0
- package/web/dist/assets/chunk-BSJP7CBP-5VmcfR4-.js +1 -0
- package/web/dist/assets/chunk-Bj-mKKzh.js +1 -0
- package/web/dist/assets/chunk-CIAEETIT-CHJ-L8H1.js +1 -0
- package/web/dist/assets/chunk-EDXVE4YY-DZHAJjMI.js +1 -0
- package/web/dist/assets/chunk-ENJZ2VHE-DbUDFa7w.js +10 -0
- package/web/dist/assets/chunk-FMBD7UC4-BsYE5e_h.js +15 -0
- package/web/dist/assets/chunk-FOC6F5B3-Cm6aoTv7.js +1 -0
- package/web/dist/assets/chunk-ICPOFSXX-C5eNZ4L6.js +123 -0
- package/web/dist/assets/chunk-K5T4RW27-R7dAJ4rq.js +94 -0
- package/web/dist/assets/chunk-KGLVRYIC-MO99YZXL.js +1 -0
- package/web/dist/assets/chunk-LIHQZDEY-DUJ656sT.js +1 -0
- package/web/dist/assets/chunk-ORNJ4GCN-DXuuEC1n.js +1 -0
- package/web/dist/assets/chunk-OYMX7WX6-pJlEprWq.js +231 -0
- package/web/dist/assets/chunk-QZHKN3VN-_pQxbbiW.js +1 -0
- package/web/dist/assets/chunk-U2HBQHQK-Mh_l9PLe.js +70 -0
- package/web/dist/assets/chunk-X2U36JSP-BOeiJW0w.js +1 -0
- package/web/dist/assets/chunk-XPW4576I-fQ9SDvr_.js +32 -0
- package/web/dist/assets/chunk-YZCP3GAM-eboO4P5S.js +1 -0
- package/web/dist/assets/chunk-ZZ45TVLE-Cky0eqlr.js +1 -0
- package/web/dist/assets/classDiagram-6PBFFD2Q-DEPsZSU3.js +1 -0
- package/web/dist/assets/classDiagram-v2-HSJHXN6E-DhmIOEpX.js +1 -0
- package/web/dist/assets/clone-DeTzYqo8.js +1 -0
- package/web/dist/assets/cose-bilkent-S5V4N54A-N4zWUJ7C.js +1 -0
- package/web/dist/assets/cytoscape.esm-BBMd0vGm.js +321 -0
- package/web/dist/assets/dagre-IpK1aoMm.js +1 -0
- package/web/dist/assets/dagre-KV5264BT-DCytJuju.js +4 -0
- package/web/dist/assets/defaultLocale-5eAKkKJC.js +1 -0
- package/web/dist/assets/diagram-5BDNPKRD-Cv4miBae.js +10 -0
- package/web/dist/assets/diagram-G4DWMVQ6-CtICKUFi.js +24 -0
- package/web/dist/assets/diagram-MMDJMWI5-Cn7aGorh.js +43 -0
- package/web/dist/assets/diagram-TYMM5635-CCUWDPsC.js +24 -0
- package/web/dist/assets/dist-Dm98VvTW.js +1 -0
- package/web/dist/assets/env-api-keys-BNlMKqxw.js +1 -0
- package/web/dist/assets/erDiagram-SMLLAGMA-uT88sBlT.js +85 -0
- package/web/dist/assets/event-stream-D33K9rpL.js +1 -0
- package/web/dist/assets/flatten-C-u5nd5-.js +1 -0
- package/web/dist/assets/flowDiagram-DWJPFMVM-Bl3O7S1m.js +162 -0
- package/web/dist/assets/ganttDiagram-T4ZO3ILL-B1FhwV45.js +292 -0
- package/web/dist/assets/gitGraph-7Q5UKJZL-Bc_7vzer.js +1 -0
- package/web/dist/assets/gitGraphDiagram-UUTBAWPF-DfW6svMS.js +106 -0
- package/web/dist/assets/github-copilot-headers-L39QqneT.js +1 -0
- package/web/dist/assets/google-BdYNeCP_.js +1 -0
- package/web/dist/assets/google-gemini-cli-DpxAL3K4.js +2 -0
- package/web/dist/assets/google-shared-DyQdgtsI.js +2 -0
- package/web/dist/assets/google-vertex-CKRybaXj.js +1 -0
- package/web/dist/assets/graphlib-CMTVFyOZ.js +1 -0
- package/web/dist/assets/hash-kZ2KD_no.js +1 -0
- package/web/dist/assets/index-Bdb7P7gx.css +2 -0
- package/web/dist/assets/index-DNxepp8B.js +2891 -0
- package/web/dist/assets/info-OMHHGYJF-BGcxeaZt.js +1 -0
- package/web/dist/assets/infoDiagram-42DDH7IO-BbES7X_c.js +2 -0
- package/web/dist/assets/init-DlZdxViB.js +1 -0
- package/web/dist/assets/isEmpty-DssUW35f.js +1 -0
- package/web/dist/assets/ishikawaDiagram-UXIWVN3A-DxQ28rho.js +70 -0
- package/web/dist/assets/journeyDiagram-VCZTEJTY-D0X8qQ0P.js +139 -0
- package/web/dist/assets/json-parse-C6tSeIxX.js +2 -0
- package/web/dist/assets/kanban-definition-6JOO6SKY-DWYfSlpl.js +89 -0
- package/web/dist/assets/katex-CyM-5LlM.js +265 -0
- package/web/dist/assets/line-CuHce5JG.js +1 -0
- package/web/dist/assets/linear-Ca0Vkwuj.js +1 -0
- package/web/dist/assets/mermaid-parser.core-Cy4iY_Dy.js +4 -0
- package/web/dist/assets/mermaid.core-6PGkQdYc.js +11 -0
- package/web/dist/assets/mindmap-definition-QFDTVHPH-BBnKdtQh.js +96 -0
- package/web/dist/assets/mistral-BWaUMIgd.js +7 -0
- package/web/dist/assets/openai-D4NSaQIs.js +16 -0
- package/web/dist/assets/openai-codex-responses-CHBgKhmb.js +7 -0
- package/web/dist/assets/openai-completions-kcXmmaHI.js +5 -0
- package/web/dist/assets/openai-responses-Cqq3H3p3.js +1 -0
- package/web/dist/assets/openai-responses-shared-CTNuo9ci.js +10 -0
- package/web/dist/assets/ordinal-_K3x1fkz.js +1 -0
- package/web/dist/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm +0 -0
- package/web/dist/assets/packet-4T2RLAQJ-D35ZLSBH.js +1 -0
- package/web/dist/assets/path-6uRLdFF7.js +1 -0
- package/web/dist/assets/pdf.worker.min-Cpi8b8z3.mjs +28 -0
- package/web/dist/assets/pie-ZZUOXDRM-DRoETpJX.js +1 -0
- package/web/dist/assets/pieDiagram-DEJITSTG-DfMjfTQz.js +30 -0
- package/web/dist/assets/preload-helper-DSXbuxSR.js +1 -0
- package/web/dist/assets/quadrantDiagram-34T5L4WZ-DfBSEept.js +7 -0
- package/web/dist/assets/radar-PYXPWWZC-DLKxRJ0V.js +1 -0
- package/web/dist/assets/reduce-836A2NiQ.js +1 -0
- package/web/dist/assets/requirementDiagram-MS252O5E-BPkxJQkz.js +84 -0
- package/web/dist/assets/rough.esm-Djo4Abte.js +1 -0
- package/web/dist/assets/sankeyDiagram-XADWPNL6-He3x9tNT.js +10 -0
- package/web/dist/assets/sequenceDiagram-FGHM5R23-DfCDpvrT.js +157 -0
- package/web/dist/assets/src-DdOdIreR.js +1 -0
- package/web/dist/assets/stateDiagram-FHFEXIEX-fuww6347.js +1 -0
- package/web/dist/assets/stateDiagram-v2-QKLJ7IA2-U6voafO3.js +1 -0
- package/web/dist/assets/timeline-definition-GMOUNBTQ-BWunHgBC.js +120 -0
- package/web/dist/assets/transform-messages-CqKEdRVp.js +1 -0
- package/web/dist/assets/transformers.web-DKUtmSAi.js +2818 -0
- package/web/dist/assets/treeView-SZITEDCU-BCx0xSAm.js +1 -0
- package/web/dist/assets/treemap-W4RFUUIX-2CvghWJK.js +1 -0
- package/web/dist/assets/vennDiagram-DHZGUBPP-CBXRutSP.js +34 -0
- package/web/dist/assets/wardley-RL74JXVD-BkPL_mhd.js +1 -0
- package/web/dist/assets/wardleyDiagram-NUSXRM2D-DTcVscPH.js +20 -0
- package/web/dist/assets/web-CMKYLKbT.js +10 -0
- package/web/dist/assets/xychartDiagram-5P7HB3ND-CZLgX9Fe.js +7 -0
- package/web/dist/favicon.svg +10 -0
- package/web/dist/index.html +104 -0
- package/web/dist/keating-metaharness.pdf +10557 -3
- package/web/dist/llms.txt +44 -0
- package/web/dist/logo.png +0 -0
- package/web/dist/manifest.webmanifest +1 -0
- package/web/dist/og-image.png +0 -0
- package/web/dist/pwa-192x192.svg +10 -0
- package/web/dist/pwa-512x512.svg +10 -0
- package/web/dist/registerSW.js +1 -0
- package/web/dist/robots.txt +8 -0
- package/web/dist/sitemap.xml +39 -0
- package/web/dist/sw.js +1 -0
- package/web/dist/tapes/doctor.mp4 +0 -0
- package/web/dist/tapes/feedback-flow.mp4 +0 -0
- package/web/dist/tapes/improve-flow.mp4 +0 -0
- package/web/dist/tapes/intro.mp4 +0 -0
- package/web/dist/tapes/learning-flow.mp4 +0 -0
- package/web/dist/tapes/session-flow.mp4 +0 -0
- package/web/dist/tapes/teacher-flow.mp4 +0 -0
- package/web/dist/tapes/tests.mp4 +0 -0
- package/web/dist/workbox-66610c77.js +1 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Prng } from "./random.js";
|
|
2
|
+
import { benchmarkTopics } from "./topics.js";
|
|
3
|
+
import { clamp, mean } from "./util.js";
|
|
4
|
+
import { DEFAULT_WEIGHTS, clampWeights } from "./policy.js";
|
|
5
|
+
function buildLearnerPopulation(seed, count) {
|
|
6
|
+
const prng = new Prng(seed);
|
|
7
|
+
const learners = [];
|
|
8
|
+
for (let index = 0; index < count; index += 1) {
|
|
9
|
+
learners.push({
|
|
10
|
+
id: `learner-${seed}-${index}`,
|
|
11
|
+
priorKnowledge: prng.next(),
|
|
12
|
+
abstractionComfort: prng.next(),
|
|
13
|
+
analogyNeed: prng.next(),
|
|
14
|
+
dialoguePreference: prng.next(),
|
|
15
|
+
diagramAffinity: prng.next(),
|
|
16
|
+
persistence: prng.next(),
|
|
17
|
+
transferDesire: prng.next(),
|
|
18
|
+
anxiety: prng.next()
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return learners;
|
|
22
|
+
}
|
|
23
|
+
import { piCompleteJson } from "./pi-agent.js";
|
|
24
|
+
export async function simulateTeaching(cwd, policy, topic, learner, weights = DEFAULT_WEIGHTS) {
|
|
25
|
+
// Use pure math to calculate initial constraints as baseline context
|
|
26
|
+
const intuitionFit = 1 - Math.abs(policy.analogyDensity - learner.analogyNeed);
|
|
27
|
+
const rigorTarget = clamp((topic.formalism + learner.abstractionComfort) / 2);
|
|
28
|
+
const rigorFit = 1 - Math.abs(policy.formalism - rigorTarget);
|
|
29
|
+
const dialogueFit = 1 - Math.abs(policy.socraticRatio - learner.dialoguePreference);
|
|
30
|
+
const diagramTarget = topic.visualizable ? learner.diagramAffinity : 0.2;
|
|
31
|
+
const diagramFit = 1 - Math.abs(policy.diagramBias - diagramTarget);
|
|
32
|
+
const practiceNeed = clamp(1 - learner.priorKnowledge + learner.anxiety * 0.2);
|
|
33
|
+
const practiceFit = 1 - Math.abs(policy.exerciseCount / 5 - practiceNeed);
|
|
34
|
+
const reflectionFit = 1 - Math.abs(policy.reflectionBias - learner.transferDesire);
|
|
35
|
+
const overload = clamp(policy.formalism * 0.35 +
|
|
36
|
+
(policy.exerciseCount / 5) * 0.15 +
|
|
37
|
+
policy.challengeRate * 0.3 -
|
|
38
|
+
learner.persistence * 0.2 +
|
|
39
|
+
learner.anxiety * 0.25 -
|
|
40
|
+
learner.priorKnowledge * 0.15);
|
|
41
|
+
const prompt = `Simulate an educational interaction based on the following context.
|
|
42
|
+
Teacher Policy: ${JSON.stringify(policy, null, 2)}
|
|
43
|
+
Topic: ${topic.title} (${topic.domain}) - ${topic.summary}
|
|
44
|
+
Learner Traits: ${JSON.stringify(learner, null, 2)}
|
|
45
|
+
|
|
46
|
+
Evaluate the teaching outcomes from 0.0 to 1.0 (masteryGain, retention, engagement, transfer, confusion). Also provide an overall 'score' (0.0=failure to 1.0=mastery) computed as: score = masteryGain*${weights.masteryGain.toFixed(2)} + retention*${weights.retention.toFixed(2)} + engagement*${weights.engagement.toFixed(2)} + transfer*${weights.transfer.toFixed(2)} - confusion*${weights.confusion.toFixed(2)}
|
|
47
|
+
Provide 1 to 3 string sentences explaining the outcome in the 'explanation' array.
|
|
48
|
+
Respond ONLY as a JSON matching:
|
|
49
|
+
{
|
|
50
|
+
"masteryGain": number,
|
|
51
|
+
"retention": number,
|
|
52
|
+
"engagement": number,
|
|
53
|
+
"transfer": number,
|
|
54
|
+
"confusion": number,
|
|
55
|
+
"score": number,
|
|
56
|
+
"explanation": string[]
|
|
57
|
+
}`;
|
|
58
|
+
try {
|
|
59
|
+
const evaluation = await piCompleteJson(cwd, prompt, { thinking: "low" });
|
|
60
|
+
if (typeof evaluation.masteryGain !== "number" || Number.isNaN(evaluation.masteryGain)) {
|
|
61
|
+
throw new Error("LLM returned invalid evaluation metrics");
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
learner,
|
|
65
|
+
topic,
|
|
66
|
+
masteryGain: clamp(evaluation.masteryGain, 0, 1),
|
|
67
|
+
retention: clamp(evaluation.retention, 0, 1),
|
|
68
|
+
engagement: clamp(evaluation.engagement, 0, 1),
|
|
69
|
+
transfer: clamp(evaluation.transfer, 0, 1),
|
|
70
|
+
confusion: clamp(evaluation.confusion, 0, 1),
|
|
71
|
+
score: clamp(evaluation.score, 0, 1),
|
|
72
|
+
breakdown: {
|
|
73
|
+
intuitionFit,
|
|
74
|
+
rigorFit,
|
|
75
|
+
dialogueFit,
|
|
76
|
+
diagramFit,
|
|
77
|
+
practiceFit,
|
|
78
|
+
reflectionFit,
|
|
79
|
+
overload
|
|
80
|
+
},
|
|
81
|
+
explanation: evaluation.explanation
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error("LLM simulation failed, falling back to algebraic baseline", error);
|
|
86
|
+
const masteryGain = clamp(0.14 + intuitionFit * 0.18 + rigorFit * 0.2 + dialogueFit * 0.12 + diagramFit * 0.09 + practiceFit * 0.12 + (1 - overload) * 0.18);
|
|
87
|
+
const retention = clamp(masteryGain * (0.55 + policy.retrievalPractice * 0.45));
|
|
88
|
+
const engagement = clamp(0.12 + intuitionFit * 0.16 + dialogueFit * 0.16 + diagramFit * 0.1 + reflectionFit * 0.14 + (1 - overload) * 0.18);
|
|
89
|
+
const transfer = clamp(masteryGain * (0.55 + policy.interdisciplinaryBias * 0.25 + learner.transferDesire * 0.2));
|
|
90
|
+
const confusion = clamp(0.04 + overload * 0.55 + Math.abs(policy.formalism - learner.abstractionComfort) * 0.18 + Math.abs(policy.challengeRate - learner.persistence) * 0.12);
|
|
91
|
+
const score = clamp(masteryGain * weights.masteryGain +
|
|
92
|
+
retention * weights.retention +
|
|
93
|
+
engagement * weights.engagement +
|
|
94
|
+
transfer * weights.transfer -
|
|
95
|
+
confusion * weights.confusion, 0, 1);
|
|
96
|
+
return {
|
|
97
|
+
learner,
|
|
98
|
+
topic,
|
|
99
|
+
masteryGain,
|
|
100
|
+
retention,
|
|
101
|
+
engagement,
|
|
102
|
+
transfer,
|
|
103
|
+
confusion,
|
|
104
|
+
score,
|
|
105
|
+
breakdown: {
|
|
106
|
+
intuitionFit,
|
|
107
|
+
rigorFit,
|
|
108
|
+
dialogueFit,
|
|
109
|
+
diagramFit,
|
|
110
|
+
practiceFit,
|
|
111
|
+
reflectionFit,
|
|
112
|
+
overload
|
|
113
|
+
},
|
|
114
|
+
explanation: ["Fallback deterministic explanation."]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function classifyDominantSignal(simulations, kind) {
|
|
119
|
+
const metrics = {
|
|
120
|
+
intuitionFit: mean(simulations.map((entry) => entry.breakdown.intuitionFit)),
|
|
121
|
+
rigorFit: mean(simulations.map((entry) => entry.breakdown.rigorFit)),
|
|
122
|
+
dialogueFit: mean(simulations.map((entry) => entry.breakdown.dialogueFit)),
|
|
123
|
+
diagramFit: mean(simulations.map((entry) => entry.breakdown.diagramFit)),
|
|
124
|
+
practiceFit: mean(simulations.map((entry) => entry.breakdown.practiceFit)),
|
|
125
|
+
reflectionFit: mean(simulations.map((entry) => entry.breakdown.reflectionFit)),
|
|
126
|
+
overload: mean(simulations.map((entry) => entry.breakdown.overload))
|
|
127
|
+
};
|
|
128
|
+
const ordered = Object.entries(metrics).sort((left, right) => kind === "strength" ? right[1] - left[1] : left[1] - right[1]);
|
|
129
|
+
const [name] = ordered[0] ?? ["unknown"];
|
|
130
|
+
return name;
|
|
131
|
+
}
|
|
132
|
+
export function summarizeTopic(topic, simulations, traceLimit) {
|
|
133
|
+
const ranked = [...simulations].sort((left, right) => right.score - left.score);
|
|
134
|
+
const scores = simulations.map((entry) => entry.score).filter(s => !Number.isNaN(s));
|
|
135
|
+
const meanScore = mean(scores) * 100;
|
|
136
|
+
return {
|
|
137
|
+
topic,
|
|
138
|
+
learnerCount: simulations.length,
|
|
139
|
+
meanScore,
|
|
140
|
+
meanMasteryGain: mean(simulations.map((entry) => entry.masteryGain).filter(m => !Number.isNaN(m))),
|
|
141
|
+
meanRetention: mean(simulations.map((entry) => entry.retention).filter(r => !Number.isNaN(r))),
|
|
142
|
+
meanEngagement: mean(simulations.map((entry) => entry.engagement).filter(e => !Number.isNaN(e))),
|
|
143
|
+
meanTransfer: mean(simulations.map((entry) => entry.transfer).filter(t => !Number.isNaN(t))),
|
|
144
|
+
meanConfusion: mean(simulations.map((entry) => entry.confusion).filter(c => !Number.isNaN(c))),
|
|
145
|
+
topLearners: ranked.slice(0, traceLimit),
|
|
146
|
+
strugglingLearners: ranked.slice(-traceLimit).reverse(),
|
|
147
|
+
dominantStrength: classifyDominantSignal(simulations, "strength"),
|
|
148
|
+
dominantWeakness: classifyDominantSignal(simulations, "weakness")
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export async function runBenchmarkSuite(cwd, policy, focusTopic, seed = 20260401, traceLimit = 3, weights = DEFAULT_WEIGHTS) {
|
|
152
|
+
const topics = benchmarkTopics(focusTopic);
|
|
153
|
+
const topicTraces = [];
|
|
154
|
+
// Reduce learner count to 3 instead of 18 to save LLM tokens and time
|
|
155
|
+
const NUM_LEARNERS = 3;
|
|
156
|
+
const topicBenchmarks = await Promise.all(topics.map(async (topic, index) => {
|
|
157
|
+
const learners = buildLearnerPopulation(seed + index * 97, NUM_LEARNERS);
|
|
158
|
+
const simulations = await Promise.all(learners.map((learner) => simulateTeaching(cwd, policy, topic, learner, weights)));
|
|
159
|
+
const summary = summarizeTopic(topic, simulations, traceLimit);
|
|
160
|
+
topicTraces.push({
|
|
161
|
+
topic: topic.title,
|
|
162
|
+
topLearners: summary.topLearners.map((entry) => ({
|
|
163
|
+
learnerId: entry.learner.id,
|
|
164
|
+
score: entry.score,
|
|
165
|
+
explanation: entry.explanation || ["unknown"]
|
|
166
|
+
})),
|
|
167
|
+
strugglingLearners: summary.strugglingLearners.map((entry) => ({
|
|
168
|
+
learnerId: entry.learner.id,
|
|
169
|
+
score: entry.score,
|
|
170
|
+
explanation: entry.explanation || ["unknown"]
|
|
171
|
+
})),
|
|
172
|
+
metricMeans: {
|
|
173
|
+
masteryGain: summary.meanMasteryGain,
|
|
174
|
+
retention: summary.meanRetention,
|
|
175
|
+
engagement: summary.meanEngagement,
|
|
176
|
+
transfer: summary.meanTransfer,
|
|
177
|
+
confusion: summary.meanConfusion
|
|
178
|
+
},
|
|
179
|
+
dominantStrength: summary.dominantStrength,
|
|
180
|
+
dominantWeakness: summary.dominantWeakness
|
|
181
|
+
});
|
|
182
|
+
return summary;
|
|
183
|
+
}));
|
|
184
|
+
const weakest = [...topicBenchmarks].sort((left, right) => left.meanScore - right.meanScore)[0];
|
|
185
|
+
const overallScores = topicBenchmarks.map((entry) => entry.meanScore).filter(s => !Number.isNaN(s));
|
|
186
|
+
return {
|
|
187
|
+
policy,
|
|
188
|
+
suiteName: focusTopic ? `focused:${focusTopic}` : "core-suite",
|
|
189
|
+
topicBenchmarks,
|
|
190
|
+
overallScore: mean(overallScores),
|
|
191
|
+
weakestTopic: weakest?.topic.title ?? "n/a",
|
|
192
|
+
trace: {
|
|
193
|
+
seed,
|
|
194
|
+
learnerCountPerTopic: NUM_LEARNERS,
|
|
195
|
+
topicTraces
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
export function benchmarkToMarkdown(result) {
|
|
200
|
+
const lines = [
|
|
201
|
+
`# Benchmark Report: ${result.policy.name}`,
|
|
202
|
+
"",
|
|
203
|
+
`- Suite: ${result.suiteName}`,
|
|
204
|
+
`- Overall score: ${result.overallScore.toFixed(2)}`,
|
|
205
|
+
`- Weakest topic: ${result.weakestTopic}`,
|
|
206
|
+
"",
|
|
207
|
+
"| Topic | Score | Mastery | Retention | Engagement | Transfer | Confusion |",
|
|
208
|
+
"| --- | ---: | ---: | ---: | ---: | ---: | ---: |"
|
|
209
|
+
];
|
|
210
|
+
for (const benchmark of result.topicBenchmarks) {
|
|
211
|
+
lines.push(`| ${benchmark.topic.title} | ${benchmark.meanScore.toFixed(2)} | ${benchmark.meanMasteryGain.toFixed(2)} | ${benchmark.meanRetention.toFixed(2)} | ${benchmark.meanEngagement.toFixed(2)} | ${benchmark.meanTransfer.toFixed(2)} | ${benchmark.meanConfusion.toFixed(2)} |`);
|
|
212
|
+
}
|
|
213
|
+
lines.push("");
|
|
214
|
+
lines.push("## Interpretation");
|
|
215
|
+
lines.push("");
|
|
216
|
+
lines.push(`- The policy currently underperforms most on ${result.weakestTopic}, which is a useful anchor for mutation and curriculum repair.`);
|
|
217
|
+
lines.push("- Invariants tracked here favor durable learning signals: mastery, retention, engagement, transfer, and bounded confusion.");
|
|
218
|
+
lines.push("- Debug traces below explain which learners the policy helped most, where it struggled, and which signal dominated.");
|
|
219
|
+
lines.push("");
|
|
220
|
+
lines.push("## Debug Trace");
|
|
221
|
+
lines.push("");
|
|
222
|
+
for (const benchmark of result.topicBenchmarks) {
|
|
223
|
+
lines.push(`### ${benchmark.topic.title}`);
|
|
224
|
+
lines.push(`- Dominant strength: ${benchmark.dominantStrength}`);
|
|
225
|
+
lines.push(`- Dominant weakness: ${benchmark.dominantWeakness}`);
|
|
226
|
+
lines.push("- Top learners:");
|
|
227
|
+
for (const learner of benchmark.topLearners) {
|
|
228
|
+
lines.push(` - ${learner.learner.id}: ${(learner.score * 100).toFixed(1)} because ${(learner.explanation || ["unknown"]).join("; ")}`);
|
|
229
|
+
}
|
|
230
|
+
lines.push("- Struggling learners:");
|
|
231
|
+
for (const learner of benchmark.strugglingLearners) {
|
|
232
|
+
lines.push(` - ${learner.learner.id}: ${(learner.score * 100).toFixed(1)} because ${(learner.explanation || ["unknown"]).join("; ")}`);
|
|
233
|
+
}
|
|
234
|
+
lines.push("");
|
|
235
|
+
}
|
|
236
|
+
lines.push("");
|
|
237
|
+
return `${lines.join("\n")}\n`;
|
|
238
|
+
}
|
|
239
|
+
export function applyFeedbackBias(feedback) {
|
|
240
|
+
if (feedback.sampleSize < 5)
|
|
241
|
+
return { ...DEFAULT_WEIGHTS };
|
|
242
|
+
const weights = { ...DEFAULT_WEIGHTS };
|
|
243
|
+
weights.confusion = clamp(weights.confusion + feedback.confusionRate * 0.08);
|
|
244
|
+
weights.engagement = clamp(weights.engagement + feedback.satisfactionRate * 0.04);
|
|
245
|
+
const positiveSum = weights.masteryGain + weights.retention + weights.engagement + weights.transfer;
|
|
246
|
+
const targetPositive = 1 - weights.confusion;
|
|
247
|
+
const scale = targetPositive / positiveSum;
|
|
248
|
+
weights.masteryGain *= scale;
|
|
249
|
+
weights.retention *= scale;
|
|
250
|
+
weights.engagement *= scale;
|
|
251
|
+
weights.transfer *= scale;
|
|
252
|
+
return clampWeights(weights);
|
|
253
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const extensionCommandSpecs = [
|
|
2
|
+
{ name: "plan", args: "<topic>", section: "Teaching", description: "Generate a deterministic lesson plan artifact." },
|
|
3
|
+
{ name: "map", args: "<topic>", section: "Teaching", description: "Generate a Mermaid concept map with oxdraw rendering." },
|
|
4
|
+
{ name: "animate", args: "<topic>", section: "Teaching", description: "Generate a manim-web animation bundle." },
|
|
5
|
+
{ name: "learn", args: "<topic>", section: "Teaching", description: "Start a Socratic teaching session.", shellOnly: true },
|
|
6
|
+
{ name: "diagnose", args: "<topic>", section: "Teaching", description: "Map prerequisites and knowledge gaps.", shellOnly: true },
|
|
7
|
+
{ name: "quiz", args: "<topic>", section: "Assessment", description: "Generate retrieval practice questions." },
|
|
8
|
+
{ name: "verify", args: "<topic>", section: "Assessment", description: "Generate a fact-checking checklist before teaching." },
|
|
9
|
+
{ name: "bench", args: "[topic]", section: "Optimization", description: "Benchmark the current teaching policy." },
|
|
10
|
+
{ name: "evolve", args: "[topic]", section: "Optimization", description: "Evolve teaching policies via MAP-Elites." },
|
|
11
|
+
{ name: "prompt-evolve", args: "[name]", section: "Optimization", description: "Evolve a prompt template with ACE." },
|
|
12
|
+
{ name: "prompt-eval", args: "<prompt>", section: "Optimization", description: "Evaluate a prompt template in a single pass." },
|
|
13
|
+
{ name: "improve", args: "", section: "Self-Improvement", description: "Generate a self-improvement proposal." },
|
|
14
|
+
{ name: "auto-improve", args: "[topic]", section: "Self-Improvement", description: "Run full self-improvement loop: bench ā evolve ā prompt-evolve ā bench." },
|
|
15
|
+
{ name: "feedback", args: "<up|down|confused> [topic] [--comment=text]", section: "Session", description: "Record session feedback with optional comment." },
|
|
16
|
+
{ name: "timeline", args: "", section: "Review", description: "Show engagement timeline for all topics." },
|
|
17
|
+
{ name: "learner-state", args: "", section: "Review", description: "Show learner profile and session history." },
|
|
18
|
+
{ name: "due", args: "", section: "Review", description: "Show topics due for spaced-review." },
|
|
19
|
+
{ name: "policy", args: "", section: "Session", description: "Show the active hyperteacher policy." },
|
|
20
|
+
{ name: "speech", args: "", section: "Session", description: "Show optional voice-tool status." },
|
|
21
|
+
{ name: "trace", args: "[query]", section: "Session", description: "Browse debug traces and artifacts." },
|
|
22
|
+
{ name: "outputs", args: "", section: "Session", description: "Browse all Keating artifacts.", shellOnly: true },
|
|
23
|
+
];
|
|
24
|
+
export const cliCommandSpecs = [
|
|
25
|
+
{ name: "shell", args: "[prompt]", section: "Core", description: "Launch the AI-powered hyperteacher shell." },
|
|
26
|
+
{ name: "web", args: "[port]", section: "Core", description: "Start the browser UI dev server." },
|
|
27
|
+
{ name: "doctor", args: "", section: "Core", description: "Inspect AI runtime and oxdraw availability." },
|
|
28
|
+
...extensionCommandSpecs.filter(s => !s.shellOnly),
|
|
29
|
+
];
|
|
30
|
+
export function shellCommandSections() {
|
|
31
|
+
const map = new Map();
|
|
32
|
+
for (const spec of extensionCommandSpecs) {
|
|
33
|
+
if (spec.cliOnly)
|
|
34
|
+
continue;
|
|
35
|
+
const usage = `/${spec.name}${spec.args ? ` ${spec.args}` : ""}`;
|
|
36
|
+
if (!map.has(spec.section))
|
|
37
|
+
map.set(spec.section, []);
|
|
38
|
+
map.get(spec.section).push({ usage, description: spec.description });
|
|
39
|
+
}
|
|
40
|
+
return Array.from(map.entries()).map(([title, commands]) => ({ title, commands }));
|
|
41
|
+
}
|
|
42
|
+
export function cliCommandSections() {
|
|
43
|
+
const map = new Map();
|
|
44
|
+
for (const spec of cliCommandSpecs) {
|
|
45
|
+
const usage = `keating ${spec.name}${spec.args ? ` ${spec.args}` : ""}`;
|
|
46
|
+
if (!map.has(spec.section))
|
|
47
|
+
map.set(spec.section, []);
|
|
48
|
+
map.get(spec.section).push({ usage, description: spec.description });
|
|
49
|
+
}
|
|
50
|
+
return Array.from(map.entries()).map(([title, commands]) => ({ title, commands }));
|
|
51
|
+
}
|
|
52
|
+
export function formatSlashUsage(spec) {
|
|
53
|
+
return `/${spec.name}${spec.args ? ` ${spec.args}` : ""}`;
|
|
54
|
+
}
|
|
55
|
+
export function formatCliUsage(spec) {
|
|
56
|
+
return `keating ${spec.name}${spec.args ? ` ${spec.args}` : ""}`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
export const DEFAULT_PI_PROVIDER = "google";
|
|
5
|
+
export const DEFAULT_PI_MODEL = "gemini-3.1-pro-preview";
|
|
6
|
+
export const FALLBACK_PI_MODELS = {
|
|
7
|
+
google: DEFAULT_PI_MODEL,
|
|
8
|
+
openai: "gpt-5.2",
|
|
9
|
+
anthropic: "claude-sonnet-4-5"
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_KEATING_CONFIG = {
|
|
12
|
+
pi: {
|
|
13
|
+
runtimePreference: "prefer-standalone",
|
|
14
|
+
defaultProvider: DEFAULT_PI_PROVIDER,
|
|
15
|
+
defaultModel: DEFAULT_PI_MODEL,
|
|
16
|
+
defaultThinking: "medium"
|
|
17
|
+
},
|
|
18
|
+
speech: {
|
|
19
|
+
enabled: false,
|
|
20
|
+
defaultVoice: "conversational",
|
|
21
|
+
fastModel: "gemini-3.1-flash-live-preview",
|
|
22
|
+
steeringModel: "default"
|
|
23
|
+
},
|
|
24
|
+
debug: {
|
|
25
|
+
persistTraces: true,
|
|
26
|
+
traceTopLearners: 3,
|
|
27
|
+
consoleSummary: false
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export function configPath(cwd) {
|
|
31
|
+
return resolve(cwd, "keating.config.json");
|
|
32
|
+
}
|
|
33
|
+
function sanitizeRuntimePreference(value) {
|
|
34
|
+
if (value === "standalone-only" || value === "prefer-standalone" || value === "embedded-only") {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return DEFAULT_KEATING_CONFIG.pi.runtimePreference;
|
|
38
|
+
}
|
|
39
|
+
function sanitizeOptionalString(value) {
|
|
40
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
41
|
+
}
|
|
42
|
+
function normalizeProvider(value) {
|
|
43
|
+
if (value === "google-gemini-cli")
|
|
44
|
+
return DEFAULT_PI_PROVIDER;
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
export async function loadKeatingConfig(cwd) {
|
|
48
|
+
const path = configPath(cwd);
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
51
|
+
return {
|
|
52
|
+
pi: {
|
|
53
|
+
runtimePreference: sanitizeRuntimePreference(parsed.pi?.runtimePreference),
|
|
54
|
+
defaultProvider: normalizeProvider(sanitizeOptionalString(parsed.pi?.defaultProvider)) ?? DEFAULT_KEATING_CONFIG.pi.defaultProvider,
|
|
55
|
+
defaultModel: sanitizeOptionalString(parsed.pi?.defaultModel) ?? DEFAULT_KEATING_CONFIG.pi.defaultModel,
|
|
56
|
+
defaultThinking: sanitizeOptionalString(parsed.pi?.defaultThinking) ?? DEFAULT_KEATING_CONFIG.pi.defaultThinking
|
|
57
|
+
},
|
|
58
|
+
speech: {
|
|
59
|
+
enabled: typeof parsed.speech?.enabled === "boolean"
|
|
60
|
+
? parsed.speech.enabled
|
|
61
|
+
: DEFAULT_KEATING_CONFIG.speech.enabled,
|
|
62
|
+
defaultVoice: sanitizeOptionalString(parsed.speech?.defaultVoice) ?? DEFAULT_KEATING_CONFIG.speech.defaultVoice,
|
|
63
|
+
fastModel: sanitizeOptionalString(parsed.speech?.fastModel) ?? DEFAULT_KEATING_CONFIG.speech.fastModel,
|
|
64
|
+
steeringModel: sanitizeOptionalString(parsed.speech?.steeringModel) ?? DEFAULT_KEATING_CONFIG.speech.steeringModel
|
|
65
|
+
},
|
|
66
|
+
debug: {
|
|
67
|
+
persistTraces: typeof parsed.debug?.persistTraces === "boolean"
|
|
68
|
+
? parsed.debug.persistTraces
|
|
69
|
+
: DEFAULT_KEATING_CONFIG.debug.persistTraces,
|
|
70
|
+
traceTopLearners: typeof parsed.debug?.traceTopLearners === "number" && parsed.debug.traceTopLearners > 0
|
|
71
|
+
? Math.round(parsed.debug.traceTopLearners)
|
|
72
|
+
: DEFAULT_KEATING_CONFIG.debug.traceTopLearners,
|
|
73
|
+
consoleSummary: typeof parsed.debug?.consoleSummary === "boolean"
|
|
74
|
+
? parsed.debug.consoleSummary
|
|
75
|
+
: DEFAULT_KEATING_CONFIG.debug.consoleSummary
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return DEFAULT_KEATING_CONFIG;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function writeKeatingConfig(cwd, config) {
|
|
84
|
+
await writeFile(configPath(cwd), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
85
|
+
}
|
|
86
|
+
export async function ensureConfig(cwd) {
|
|
87
|
+
const path = configPath(cwd);
|
|
88
|
+
if (!existsSync(path)) {
|
|
89
|
+
await writeKeatingConfig(cwd, DEFAULT_KEATING_CONFIG);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function mergePiDefaults(config, args) {
|
|
93
|
+
const merged = [...args];
|
|
94
|
+
const hasProvider = merged.includes("--provider");
|
|
95
|
+
const hasModel = merged.includes("--model");
|
|
96
|
+
const hasThinking = merged.includes("--thinking");
|
|
97
|
+
if (!hasProvider && config.pi.defaultProvider) {
|
|
98
|
+
merged.unshift(config.pi.defaultProvider);
|
|
99
|
+
merged.unshift("--provider");
|
|
100
|
+
}
|
|
101
|
+
if (!hasModel && config.pi.defaultModel) {
|
|
102
|
+
merged.unshift(config.pi.defaultModel);
|
|
103
|
+
merged.unshift("--model");
|
|
104
|
+
}
|
|
105
|
+
if (!hasThinking && config.pi.defaultThinking) {
|
|
106
|
+
merged.unshift(config.pi.defaultThinking);
|
|
107
|
+
merged.unshift("--thinking");
|
|
108
|
+
}
|
|
109
|
+
return merged;
|
|
110
|
+
}
|
|
111
|
+
export function mergePiDefaultsWithOverrides(config, args, overrides = {}) {
|
|
112
|
+
return mergePiDefaults({
|
|
113
|
+
...config,
|
|
114
|
+
pi: {
|
|
115
|
+
...config.pi,
|
|
116
|
+
defaultProvider: overrides.provider ?? config.pi.defaultProvider,
|
|
117
|
+
defaultModel: overrides.model ?? config.pi.defaultModel
|
|
118
|
+
}
|
|
119
|
+
}, args);
|
|
120
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mean } from "./util.js";
|
|
3
|
+
import { titleCase, slugify } from "./util.js";
|
|
4
|
+
export const DEFAULT_ENGAGEMENT_POLICY = {
|
|
5
|
+
name: "spaced-revisit-default",
|
|
6
|
+
retentionHalfLifeDays: 7,
|
|
7
|
+
dueThreshold: 0.5,
|
|
8
|
+
minReviewIntervalDays: 1,
|
|
9
|
+
urgencyTiers: [21, 14, 7, 3]
|
|
10
|
+
};
|
|
11
|
+
const MS_PER_DAY = 86_400_000;
|
|
12
|
+
function daysBetween(a, b) {
|
|
13
|
+
if (!a)
|
|
14
|
+
return 0;
|
|
15
|
+
const msA = typeof a === "string" ? new Date(a).getTime() : a.getTime();
|
|
16
|
+
const msB = typeof b === "string" ? new Date(b).getTime() : b.getTime();
|
|
17
|
+
if (!Number.isFinite(msA) || !Number.isFinite(msB))
|
|
18
|
+
return 0;
|
|
19
|
+
return Math.abs(msB - msA) / MS_PER_DAY;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Estimate retention using an exponential decay model inspired by Ebbinghaus.
|
|
23
|
+
* retention = mastery Ć e^(-t / (halfLife Ć masteryFactor))
|
|
24
|
+
*
|
|
25
|
+
* Higher mastery extends the half-life: a topic mastered at 0.9 decays
|
|
26
|
+
* more slowly than one mastered at 0.3.
|
|
27
|
+
*/
|
|
28
|
+
function estimateRetention(masteryEstimate, daysSinceLastSeen, halfLifeDays) {
|
|
29
|
+
const masteryFactor = 0.5 + masteryEstimate * 1.5; // range: 0.5ā2.0
|
|
30
|
+
const effectiveHalfLife = halfLifeDays * masteryFactor;
|
|
31
|
+
const decay = Math.exp((-daysSinceLastSeen * Math.LN2) / effectiveHalfLife);
|
|
32
|
+
return Math.max(0, Math.min(1, masteryEstimate * decay));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Compute urgency: 0 = not urgent, 1 = critically overdue.
|
|
36
|
+
* Based on how far retention has fallen below the due threshold.
|
|
37
|
+
*/
|
|
38
|
+
function computeUrgency(estimatedRetention, dueThreshold, daysSinceLastSeen, tiers) {
|
|
39
|
+
if (daysSinceLastSeen < 1) {
|
|
40
|
+
return { urgency: 0, label: "fresh" };
|
|
41
|
+
}
|
|
42
|
+
if (estimatedRetention >= dueThreshold) {
|
|
43
|
+
return { urgency: Math.max(0, 1 - estimatedRetention / dueThreshold) * 0.3, label: "low" };
|
|
44
|
+
}
|
|
45
|
+
// Retention is below threshold ā compute urgency based on how far below
|
|
46
|
+
const deficit = dueThreshold - estimatedRetention;
|
|
47
|
+
const rawUrgency = Math.min(1, deficit / dueThreshold + 0.3);
|
|
48
|
+
let label;
|
|
49
|
+
if (daysSinceLastSeen >= tiers[0]) {
|
|
50
|
+
label = "critical";
|
|
51
|
+
}
|
|
52
|
+
else if (daysSinceLastSeen >= tiers[1]) {
|
|
53
|
+
label = "high";
|
|
54
|
+
}
|
|
55
|
+
else if (daysSinceLastSeen >= tiers[2]) {
|
|
56
|
+
label = "moderate";
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
label = "low";
|
|
60
|
+
}
|
|
61
|
+
return { urgency: rawUrgency, label };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Estimate when the next review should happen based on current mastery
|
|
65
|
+
* and the retention decay model.
|
|
66
|
+
*/
|
|
67
|
+
function estimateNextReview(lastSeen, masteryEstimate, halfLifeDays, dueThreshold) {
|
|
68
|
+
if (!lastSeen)
|
|
69
|
+
return new Date().toISOString();
|
|
70
|
+
if (masteryEstimate <= dueThreshold) {
|
|
71
|
+
return new Date().toISOString();
|
|
72
|
+
}
|
|
73
|
+
const masteryFactor = 0.5 + masteryEstimate * 1.5;
|
|
74
|
+
const effectiveHalfLife = halfLifeDays * masteryFactor;
|
|
75
|
+
const daysUntilDue = (-effectiveHalfLife * Math.log(dueThreshold / masteryEstimate)) / Math.LN2;
|
|
76
|
+
const lastSeenMs = new Date(lastSeen).getTime();
|
|
77
|
+
if (!Number.isFinite(lastSeenMs))
|
|
78
|
+
return new Date().toISOString();
|
|
79
|
+
return new Date(lastSeenMs + daysUntilDue * MS_PER_DAY).toISOString();
|
|
80
|
+
}
|
|
81
|
+
export function computeTopicEngagement(topic, policy, now = new Date()) {
|
|
82
|
+
const daysSince = daysBetween(topic.lastSeen, now);
|
|
83
|
+
const retention = estimateRetention(topic.masteryEstimate, daysSince, policy.retentionHalfLifeDays);
|
|
84
|
+
const isDue = retention < policy.dueThreshold && daysSince >= policy.minReviewIntervalDays;
|
|
85
|
+
const { urgency, label } = computeUrgency(retention, policy.dueThreshold, daysSince, policy.urgencyTiers);
|
|
86
|
+
const nextReview = estimateNextReview(topic.lastSeen, topic.masteryEstimate, policy.retentionHalfLifeDays, policy.dueThreshold);
|
|
87
|
+
// Defensive: older learner.json files (or partial writes) can have a topic
|
|
88
|
+
// entry missing `slug`. Derive one from any available label so the timeline
|
|
89
|
+
// never crashes the extension at session_start.
|
|
90
|
+
const rawSlug = topic.slug ||
|
|
91
|
+
(topic.title ? slugify(topic.title) : null) ||
|
|
92
|
+
"unknown-topic";
|
|
93
|
+
return {
|
|
94
|
+
slug: rawSlug,
|
|
95
|
+
title: titleCase(rawSlug.replace(/-/g, " ")),
|
|
96
|
+
domain: topic.domain,
|
|
97
|
+
lastSeen: topic.lastSeen,
|
|
98
|
+
daysSinceLastSeen: daysSince,
|
|
99
|
+
masteryEstimate: topic.masteryEstimate,
|
|
100
|
+
estimatedRetention: retention,
|
|
101
|
+
isDue,
|
|
102
|
+
urgency,
|
|
103
|
+
urgencyLabel: label,
|
|
104
|
+
sessionCount: topic.sessionCount,
|
|
105
|
+
nextReviewAt: nextReview
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function buildEngagementTimeline(state, policy = DEFAULT_ENGAGEMENT_POLICY, now = new Date()) {
|
|
109
|
+
const topics = (state.coveredTopics ?? [])
|
|
110
|
+
.filter((t) => Boolean(t))
|
|
111
|
+
.map(t => computeTopicEngagement(t, policy, now));
|
|
112
|
+
// Sort by urgency descending (most urgent first)
|
|
113
|
+
topics.sort((a, b) => b.urgency - a.urgency);
|
|
114
|
+
const dueCount = topics.filter(t => t.isDue).length;
|
|
115
|
+
const criticalCount = topics.filter(t => t.urgencyLabel === "critical").length;
|
|
116
|
+
const averageRetention = topics.length > 0 ? mean(topics.map(t => t.estimatedRetention)) : 1;
|
|
117
|
+
const oldestUnreviewedDays = topics.length > 0
|
|
118
|
+
? Math.max(...topics.map(t => t.daysSinceLastSeen))
|
|
119
|
+
: 0;
|
|
120
|
+
return {
|
|
121
|
+
generatedAt: now.toISOString(),
|
|
122
|
+
policy,
|
|
123
|
+
topics,
|
|
124
|
+
summary: {
|
|
125
|
+
totalTopics: topics.length,
|
|
126
|
+
dueCount,
|
|
127
|
+
criticalCount,
|
|
128
|
+
averageRetention,
|
|
129
|
+
oldestUnreviewedDays
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function dueTopics(state, policy = DEFAULT_ENGAGEMENT_POLICY, now = new Date()) {
|
|
134
|
+
const timeline = buildEngagementTimeline(state, policy, now);
|
|
135
|
+
return timeline.topics.filter(t => t.isDue);
|
|
136
|
+
}
|
|
137
|
+
function retentionBar(retention) {
|
|
138
|
+
const filled = Math.round(retention * 10);
|
|
139
|
+
return "ā".repeat(filled) + "ā".repeat(10 - filled);
|
|
140
|
+
}
|
|
141
|
+
function urgencyEmoji(label) {
|
|
142
|
+
switch (label) {
|
|
143
|
+
case "critical": return "š“";
|
|
144
|
+
case "high": return "š ";
|
|
145
|
+
case "moderate": return "š”";
|
|
146
|
+
case "low": return "š¢";
|
|
147
|
+
case "fresh": return "āØ";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function formatDays(days) {
|
|
151
|
+
if (days < 1)
|
|
152
|
+
return "today";
|
|
153
|
+
if (days < 2)
|
|
154
|
+
return "1 day ago";
|
|
155
|
+
if (days < 7)
|
|
156
|
+
return `${Math.floor(days)} days ago`;
|
|
157
|
+
if (days < 14)
|
|
158
|
+
return "1 week ago";
|
|
159
|
+
if (days < 30)
|
|
160
|
+
return `${Math.floor(days / 7)} weeks ago`;
|
|
161
|
+
if (days < 60)
|
|
162
|
+
return "1 month ago";
|
|
163
|
+
return `${Math.floor(days / 30)} months ago`;
|
|
164
|
+
}
|
|
165
|
+
export function engagementTimelineToMarkdown(timeline) {
|
|
166
|
+
const lines = [
|
|
167
|
+
"# Engagement Timeline",
|
|
168
|
+
"",
|
|
169
|
+
`Generated: ${new Date(timeline.generatedAt).toLocaleString()}`,
|
|
170
|
+
"",
|
|
171
|
+
"## Summary",
|
|
172
|
+
"",
|
|
173
|
+
`- **Topics tracked:** ${timeline.summary.totalTopics}`,
|
|
174
|
+
`- **Due for review:** ${timeline.summary.dueCount}`,
|
|
175
|
+
`- **Critical:** ${timeline.summary.criticalCount}`,
|
|
176
|
+
`- **Average retention:** ${(timeline.summary.averageRetention * 100).toFixed(1)}%`,
|
|
177
|
+
`- **Oldest unreviewed:** ${formatDays(timeline.summary.oldestUnreviewedDays)}`,
|
|
178
|
+
""
|
|
179
|
+
];
|
|
180
|
+
if (timeline.topics.length === 0) {
|
|
181
|
+
lines.push("No topics covered yet. Start a lesson to begin tracking.");
|
|
182
|
+
return `${lines.join("\n")}\n`;
|
|
183
|
+
}
|
|
184
|
+
lines.push("## Topics");
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push("| Status | Topic | Last Seen | Retention | Mastery | Sessions | Next Review |");
|
|
187
|
+
lines.push("| :---: | --- | --- | --- | ---: | ---: | --- |");
|
|
188
|
+
for (const topic of timeline.topics) {
|
|
189
|
+
lines.push(`| ${urgencyEmoji(topic.urgencyLabel)} | **${topic.title}** (${topic.domain}) | ${formatDays(topic.daysSinceLastSeen)} | ${retentionBar(topic.estimatedRetention)} ${(topic.estimatedRetention * 100).toFixed(0)}% | ${(topic.masteryEstimate * 100).toFixed(0)}% | ${topic.sessionCount} | ${topic.isDue ? "**NOW**" : new Date(topic.nextReviewAt).toLocaleDateString()} |`);
|
|
190
|
+
}
|
|
191
|
+
lines.push("");
|
|
192
|
+
// Legend
|
|
193
|
+
lines.push("### Legend");
|
|
194
|
+
lines.push("");
|
|
195
|
+
lines.push("- š“ Critical ā severely overdue, retention critically low");
|
|
196
|
+
lines.push("- š High ā significantly overdue, review soon");
|
|
197
|
+
lines.push("- š” Moderate ā approaching review threshold");
|
|
198
|
+
lines.push("- š¢ Low ā retention adequate, no rush");
|
|
199
|
+
lines.push("- ⨠Fresh ā recently covered");
|
|
200
|
+
lines.push("");
|
|
201
|
+
return `${lines.join("\n")}\n`;
|
|
202
|
+
}
|
|
203
|
+
export function dueTopicsToMarkdown(topics) {
|
|
204
|
+
if (topics.length === 0) {
|
|
205
|
+
return "# Due Topics\n\nā
All topics are up to date! No reviews needed right now.\n";
|
|
206
|
+
}
|
|
207
|
+
const lines = [
|
|
208
|
+
"# Due Topics",
|
|
209
|
+
"",
|
|
210
|
+
`${topics.length} topic${topics.length === 1 ? "" : "s"} due for review:`,
|
|
211
|
+
""
|
|
212
|
+
];
|
|
213
|
+
for (const topic of topics) {
|
|
214
|
+
lines.push(`### ${urgencyEmoji(topic.urgencyLabel)} ${topic.title}`);
|
|
215
|
+
lines.push(`- Domain: ${topic.domain}`);
|
|
216
|
+
lines.push(`- Last seen: ${formatDays(topic.daysSinceLastSeen)}`);
|
|
217
|
+
lines.push(`- Retention: ${retentionBar(topic.estimatedRetention)} ${(topic.estimatedRetention * 100).toFixed(0)}%`);
|
|
218
|
+
lines.push(`- Mastery at last review: ${(topic.masteryEstimate * 100).toFixed(0)}%`);
|
|
219
|
+
lines.push(`- Sessions: ${topic.sessionCount}`);
|
|
220
|
+
lines.push("");
|
|
221
|
+
}
|
|
222
|
+
return `${lines.join("\n")}\n`;
|
|
223
|
+
}
|
|
224
|
+
export async function loadEngagementPolicy(filePath) {
|
|
225
|
+
try {
|
|
226
|
+
const raw = await readFile(filePath, "utf8");
|
|
227
|
+
return JSON.parse(raw);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return DEFAULT_ENGAGEMENT_POLICY;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export async function saveEngagementPolicy(filePath, policy) {
|
|
234
|
+
await writeFile(filePath, `${JSON.stringify(policy, null, 2)}\n`, "utf8");
|
|
235
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as dotenv from "dotenv";
|
|
2
|
+
const DEBUG_ENV_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
3
|
+
function envFlagEnabled(value) {
|
|
4
|
+
return value !== undefined && DEBUG_ENV_VALUES.has(value.toLowerCase());
|
|
5
|
+
}
|
|
6
|
+
export function loadEnv() {
|
|
7
|
+
const debug = envFlagEnabled(process.env.KEATING_DEBUG) || envFlagEnabled(process.env.DEBUG);
|
|
8
|
+
dotenv.config({ debug, quiet: !debug });
|
|
9
|
+
}
|