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.
Files changed (249) hide show
  1. package/README.md +334 -0
  2. package/SYSTEM.md +33 -0
  3. package/bin/keating.js +31 -0
  4. package/dist/src/cli/main.js +357 -0
  5. package/dist/src/cli/setup.js +197 -0
  6. package/dist/src/cli/web.js +84 -0
  7. package/dist/src/core/animation.js +304 -0
  8. package/dist/src/core/ax-optimizer.js +81 -0
  9. package/dist/src/core/ax-prompt-learner.js +59 -0
  10. package/dist/src/core/ax-trial.js +181 -0
  11. package/dist/src/core/benchmark.js +253 -0
  12. package/dist/src/core/commands.js +57 -0
  13. package/dist/src/core/config.js +120 -0
  14. package/dist/src/core/engagement.js +235 -0
  15. package/dist/src/core/env.js +9 -0
  16. package/dist/src/core/evolution.js +242 -0
  17. package/dist/src/core/flashcards.js +133 -0
  18. package/dist/src/core/learner-state.js +108 -0
  19. package/dist/src/core/lesson-plan.js +155 -0
  20. package/dist/src/core/map-elites.js +228 -0
  21. package/dist/src/core/map.js +89 -0
  22. package/dist/src/core/mastery.js +207 -0
  23. package/dist/src/core/paths.js +100 -0
  24. package/dist/src/core/pi-agent.js +82 -0
  25. package/dist/src/core/policy.js +79 -0
  26. package/dist/src/core/project.js +337 -0
  27. package/dist/src/core/projects.js +281 -0
  28. package/dist/src/core/prompt-evolution.js +344 -0
  29. package/dist/src/core/quiz.js +194 -0
  30. package/dist/src/core/random.js +19 -0
  31. package/dist/src/core/self-improve.js +425 -0
  32. package/dist/src/core/speech.js +54 -0
  33. package/dist/src/core/terminal.js +117 -0
  34. package/dist/src/core/theme.js +101 -0
  35. package/dist/src/core/topics.js +620 -0
  36. package/dist/src/core/types.js +1 -0
  37. package/dist/src/core/util.js +30 -0
  38. package/dist/src/core/verification.js +162 -0
  39. package/dist/src/pi/hyperteacher-extension.js +573 -0
  40. package/dist/src/runtime/pi.js +343 -0
  41. package/package.json +78 -0
  42. package/pi/prompts/bridge.md +14 -0
  43. package/pi/prompts/diagnose.md +15 -0
  44. package/pi/prompts/improve.md +39 -0
  45. package/pi/prompts/learn.md +21 -0
  46. package/pi/prompts/quiz.md +14 -0
  47. package/pi/skills/adaptive-teaching/SKILL.md +33 -0
  48. package/scripts/install/install.sh +308 -0
  49. package/web/dist/.well-known/llms.txt +44 -0
  50. package/web/dist/apple-touch-icon.svg +10 -0
  51. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  52. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  53. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  56. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  58. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  59. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  62. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  63. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  64. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  65. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  66. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  67. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  68. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  69. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  70. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  71. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  72. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  73. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  74. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  76. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  77. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  78. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  79. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  80. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  81. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  82. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  83. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  89. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  90. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  91. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  92. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  93. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  94. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  95. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  96. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  97. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  98. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  99. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  100. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  101. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  102. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  103. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  104. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  105. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  106. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  107. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  108. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  109. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  110. package/web/dist/assets/_baseFor-B_cjfMB6.js +1 -0
  111. package/web/dist/assets/anthropic-BT6Vfzb1.js +36 -0
  112. package/web/dist/assets/arc-x2nTilpc.js +1 -0
  113. package/web/dist/assets/architecture-YZFGNWBL-B1hlUWjX.js +1 -0
  114. package/web/dist/assets/architectureDiagram-Q4EWVU46-CMApWFyw.js +36 -0
  115. package/web/dist/assets/array-B9UHiPd-.js +1 -0
  116. package/web/dist/assets/azure-openai-responses-CommX3YJ.js +1 -0
  117. package/web/dist/assets/blockDiagram-DXYQGD6D-DOQbsNRY.js +132 -0
  118. package/web/dist/assets/c4Diagram-AHTNJAMY-VFfRZWWA.js +10 -0
  119. package/web/dist/assets/channel-KY2Tg8Ba.js +1 -0
  120. package/web/dist/assets/chunk-2KRD3SAO-B-AqvS0u.js +1 -0
  121. package/web/dist/assets/chunk-336JU56O-DlYgPyl6.js +2 -0
  122. package/web/dist/assets/chunk-426QAEUC-CsVoBkfR.js +1 -0
  123. package/web/dist/assets/chunk-4BX2VUAB-0Z13aFAn.js +1 -0
  124. package/web/dist/assets/chunk-4TB4RGXK-DqC0Zwm7.js +206 -0
  125. package/web/dist/assets/chunk-55IACEB6-CWE_u-IY.js +1 -0
  126. package/web/dist/assets/chunk-5FUZZQ4R-CApli0xX.js +62 -0
  127. package/web/dist/assets/chunk-5PVQY5BW-Cbzhfhln.js +2 -0
  128. package/web/dist/assets/chunk-67CJDMHE-Cx7uJS4d.js +1 -0
  129. package/web/dist/assets/chunk-7N4EOEYR-CYPNsFus.js +1 -0
  130. package/web/dist/assets/chunk-AA7GKIK3-rU0uhR_u.js +1 -0
  131. package/web/dist/assets/chunk-BSJP7CBP-5VmcfR4-.js +1 -0
  132. package/web/dist/assets/chunk-Bj-mKKzh.js +1 -0
  133. package/web/dist/assets/chunk-CIAEETIT-CHJ-L8H1.js +1 -0
  134. package/web/dist/assets/chunk-EDXVE4YY-DZHAJjMI.js +1 -0
  135. package/web/dist/assets/chunk-ENJZ2VHE-DbUDFa7w.js +10 -0
  136. package/web/dist/assets/chunk-FMBD7UC4-BsYE5e_h.js +15 -0
  137. package/web/dist/assets/chunk-FOC6F5B3-Cm6aoTv7.js +1 -0
  138. package/web/dist/assets/chunk-ICPOFSXX-C5eNZ4L6.js +123 -0
  139. package/web/dist/assets/chunk-K5T4RW27-R7dAJ4rq.js +94 -0
  140. package/web/dist/assets/chunk-KGLVRYIC-MO99YZXL.js +1 -0
  141. package/web/dist/assets/chunk-LIHQZDEY-DUJ656sT.js +1 -0
  142. package/web/dist/assets/chunk-ORNJ4GCN-DXuuEC1n.js +1 -0
  143. package/web/dist/assets/chunk-OYMX7WX6-pJlEprWq.js +231 -0
  144. package/web/dist/assets/chunk-QZHKN3VN-_pQxbbiW.js +1 -0
  145. package/web/dist/assets/chunk-U2HBQHQK-Mh_l9PLe.js +70 -0
  146. package/web/dist/assets/chunk-X2U36JSP-BOeiJW0w.js +1 -0
  147. package/web/dist/assets/chunk-XPW4576I-fQ9SDvr_.js +32 -0
  148. package/web/dist/assets/chunk-YZCP3GAM-eboO4P5S.js +1 -0
  149. package/web/dist/assets/chunk-ZZ45TVLE-Cky0eqlr.js +1 -0
  150. package/web/dist/assets/classDiagram-6PBFFD2Q-DEPsZSU3.js +1 -0
  151. package/web/dist/assets/classDiagram-v2-HSJHXN6E-DhmIOEpX.js +1 -0
  152. package/web/dist/assets/clone-DeTzYqo8.js +1 -0
  153. package/web/dist/assets/cose-bilkent-S5V4N54A-N4zWUJ7C.js +1 -0
  154. package/web/dist/assets/cytoscape.esm-BBMd0vGm.js +321 -0
  155. package/web/dist/assets/dagre-IpK1aoMm.js +1 -0
  156. package/web/dist/assets/dagre-KV5264BT-DCytJuju.js +4 -0
  157. package/web/dist/assets/defaultLocale-5eAKkKJC.js +1 -0
  158. package/web/dist/assets/diagram-5BDNPKRD-Cv4miBae.js +10 -0
  159. package/web/dist/assets/diagram-G4DWMVQ6-CtICKUFi.js +24 -0
  160. package/web/dist/assets/diagram-MMDJMWI5-Cn7aGorh.js +43 -0
  161. package/web/dist/assets/diagram-TYMM5635-CCUWDPsC.js +24 -0
  162. package/web/dist/assets/dist-Dm98VvTW.js +1 -0
  163. package/web/dist/assets/env-api-keys-BNlMKqxw.js +1 -0
  164. package/web/dist/assets/erDiagram-SMLLAGMA-uT88sBlT.js +85 -0
  165. package/web/dist/assets/event-stream-D33K9rpL.js +1 -0
  166. package/web/dist/assets/flatten-C-u5nd5-.js +1 -0
  167. package/web/dist/assets/flowDiagram-DWJPFMVM-Bl3O7S1m.js +162 -0
  168. package/web/dist/assets/ganttDiagram-T4ZO3ILL-B1FhwV45.js +292 -0
  169. package/web/dist/assets/gitGraph-7Q5UKJZL-Bc_7vzer.js +1 -0
  170. package/web/dist/assets/gitGraphDiagram-UUTBAWPF-DfW6svMS.js +106 -0
  171. package/web/dist/assets/github-copilot-headers-L39QqneT.js +1 -0
  172. package/web/dist/assets/google-BdYNeCP_.js +1 -0
  173. package/web/dist/assets/google-gemini-cli-DpxAL3K4.js +2 -0
  174. package/web/dist/assets/google-shared-DyQdgtsI.js +2 -0
  175. package/web/dist/assets/google-vertex-CKRybaXj.js +1 -0
  176. package/web/dist/assets/graphlib-CMTVFyOZ.js +1 -0
  177. package/web/dist/assets/hash-kZ2KD_no.js +1 -0
  178. package/web/dist/assets/index-Bdb7P7gx.css +2 -0
  179. package/web/dist/assets/index-DNxepp8B.js +2891 -0
  180. package/web/dist/assets/info-OMHHGYJF-BGcxeaZt.js +1 -0
  181. package/web/dist/assets/infoDiagram-42DDH7IO-BbES7X_c.js +2 -0
  182. package/web/dist/assets/init-DlZdxViB.js +1 -0
  183. package/web/dist/assets/isEmpty-DssUW35f.js +1 -0
  184. package/web/dist/assets/ishikawaDiagram-UXIWVN3A-DxQ28rho.js +70 -0
  185. package/web/dist/assets/journeyDiagram-VCZTEJTY-D0X8qQ0P.js +139 -0
  186. package/web/dist/assets/json-parse-C6tSeIxX.js +2 -0
  187. package/web/dist/assets/kanban-definition-6JOO6SKY-DWYfSlpl.js +89 -0
  188. package/web/dist/assets/katex-CyM-5LlM.js +265 -0
  189. package/web/dist/assets/line-CuHce5JG.js +1 -0
  190. package/web/dist/assets/linear-Ca0Vkwuj.js +1 -0
  191. package/web/dist/assets/mermaid-parser.core-Cy4iY_Dy.js +4 -0
  192. package/web/dist/assets/mermaid.core-6PGkQdYc.js +11 -0
  193. package/web/dist/assets/mindmap-definition-QFDTVHPH-BBnKdtQh.js +96 -0
  194. package/web/dist/assets/mistral-BWaUMIgd.js +7 -0
  195. package/web/dist/assets/openai-D4NSaQIs.js +16 -0
  196. package/web/dist/assets/openai-codex-responses-CHBgKhmb.js +7 -0
  197. package/web/dist/assets/openai-completions-kcXmmaHI.js +5 -0
  198. package/web/dist/assets/openai-responses-Cqq3H3p3.js +1 -0
  199. package/web/dist/assets/openai-responses-shared-CTNuo9ci.js +10 -0
  200. package/web/dist/assets/ordinal-_K3x1fkz.js +1 -0
  201. package/web/dist/assets/ort-wasm-simd-threaded.jsep-B0T3yYHD.wasm +0 -0
  202. package/web/dist/assets/packet-4T2RLAQJ-D35ZLSBH.js +1 -0
  203. package/web/dist/assets/path-6uRLdFF7.js +1 -0
  204. package/web/dist/assets/pdf.worker.min-Cpi8b8z3.mjs +28 -0
  205. package/web/dist/assets/pie-ZZUOXDRM-DRoETpJX.js +1 -0
  206. package/web/dist/assets/pieDiagram-DEJITSTG-DfMjfTQz.js +30 -0
  207. package/web/dist/assets/preload-helper-DSXbuxSR.js +1 -0
  208. package/web/dist/assets/quadrantDiagram-34T5L4WZ-DfBSEept.js +7 -0
  209. package/web/dist/assets/radar-PYXPWWZC-DLKxRJ0V.js +1 -0
  210. package/web/dist/assets/reduce-836A2NiQ.js +1 -0
  211. package/web/dist/assets/requirementDiagram-MS252O5E-BPkxJQkz.js +84 -0
  212. package/web/dist/assets/rough.esm-Djo4Abte.js +1 -0
  213. package/web/dist/assets/sankeyDiagram-XADWPNL6-He3x9tNT.js +10 -0
  214. package/web/dist/assets/sequenceDiagram-FGHM5R23-DfCDpvrT.js +157 -0
  215. package/web/dist/assets/src-DdOdIreR.js +1 -0
  216. package/web/dist/assets/stateDiagram-FHFEXIEX-fuww6347.js +1 -0
  217. package/web/dist/assets/stateDiagram-v2-QKLJ7IA2-U6voafO3.js +1 -0
  218. package/web/dist/assets/timeline-definition-GMOUNBTQ-BWunHgBC.js +120 -0
  219. package/web/dist/assets/transform-messages-CqKEdRVp.js +1 -0
  220. package/web/dist/assets/transformers.web-DKUtmSAi.js +2818 -0
  221. package/web/dist/assets/treeView-SZITEDCU-BCx0xSAm.js +1 -0
  222. package/web/dist/assets/treemap-W4RFUUIX-2CvghWJK.js +1 -0
  223. package/web/dist/assets/vennDiagram-DHZGUBPP-CBXRutSP.js +34 -0
  224. package/web/dist/assets/wardley-RL74JXVD-BkPL_mhd.js +1 -0
  225. package/web/dist/assets/wardleyDiagram-NUSXRM2D-DTcVscPH.js +20 -0
  226. package/web/dist/assets/web-CMKYLKbT.js +10 -0
  227. package/web/dist/assets/xychartDiagram-5P7HB3ND-CZLgX9Fe.js +7 -0
  228. package/web/dist/favicon.svg +10 -0
  229. package/web/dist/index.html +104 -0
  230. package/web/dist/keating-metaharness.pdf +10557 -3
  231. package/web/dist/llms.txt +44 -0
  232. package/web/dist/logo.png +0 -0
  233. package/web/dist/manifest.webmanifest +1 -0
  234. package/web/dist/og-image.png +0 -0
  235. package/web/dist/pwa-192x192.svg +10 -0
  236. package/web/dist/pwa-512x512.svg +10 -0
  237. package/web/dist/registerSW.js +1 -0
  238. package/web/dist/robots.txt +8 -0
  239. package/web/dist/sitemap.xml +39 -0
  240. package/web/dist/sw.js +1 -0
  241. package/web/dist/tapes/doctor.mp4 +0 -0
  242. package/web/dist/tapes/feedback-flow.mp4 +0 -0
  243. package/web/dist/tapes/improve-flow.mp4 +0 -0
  244. package/web/dist/tapes/intro.mp4 +0 -0
  245. package/web/dist/tapes/learning-flow.mp4 +0 -0
  246. package/web/dist/tapes/session-flow.mp4 +0 -0
  247. package/web/dist/tapes/teacher-flow.mp4 +0 -0
  248. package/web/dist/tapes/tests.mp4 +0 -0
  249. package/web/dist/workbox-66610c77.js +1 -0
@@ -0,0 +1,242 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { Prng } from "./random.js";
3
+ import { clamp } from "./util.js";
4
+ import { DEFAULT_POLICY, DEFAULT_WEIGHTS, clampPolicy, clampWeights } from "./policy.js";
5
+ import { benchmarkToMarkdown, runBenchmarkSuite } from "./benchmark.js";
6
+ function diffPolicy(before, after) {
7
+ const keys = [
8
+ "analogyDensity",
9
+ "socraticRatio",
10
+ "formalism",
11
+ "retrievalPractice",
12
+ "exerciseCount",
13
+ "diagramBias",
14
+ "reflectionBias",
15
+ "interdisciplinaryBias",
16
+ "challengeRate"
17
+ ];
18
+ return keys
19
+ .map((field) => {
20
+ const previous = before[field];
21
+ const next = after[field];
22
+ const delta = typeof previous === "number" && typeof next === "number" ? next - previous : 0;
23
+ return { field, before: previous, after: next, delta };
24
+ })
25
+ .filter((entry) => entry.delta !== 0);
26
+ }
27
+ function mutateScalar(prng, value, amplitude = 0.18) {
28
+ return clamp(value + (prng.next() * 2 - 1) * amplitude);
29
+ }
30
+ function mutateWeights(parent, prng, amplitude = 0.12) {
31
+ return clampWeights({
32
+ masteryGain: mutateScalar(prng, parent.masteryGain, amplitude),
33
+ retention: mutateScalar(prng, parent.retention, amplitude),
34
+ engagement: mutateScalar(prng, parent.engagement, amplitude),
35
+ transfer: mutateScalar(prng, parent.transfer, amplitude),
36
+ confusion: mutateScalar(prng, parent.confusion, amplitude)
37
+ });
38
+ }
39
+ function mutatePolicy(parent, prng, iteration) {
40
+ const mutated = clampPolicy({
41
+ ...parent,
42
+ name: `keating-candidate-${iteration}`,
43
+ analogyDensity: mutateScalar(prng, parent.analogyDensity),
44
+ socraticRatio: mutateScalar(prng, parent.socraticRatio),
45
+ formalism: mutateScalar(prng, parent.formalism),
46
+ retrievalPractice: mutateScalar(prng, parent.retrievalPractice),
47
+ exerciseCount: parent.exerciseCount + prng.int(-1, 1),
48
+ diagramBias: mutateScalar(prng, parent.diagramBias),
49
+ reflectionBias: mutateScalar(prng, parent.reflectionBias),
50
+ interdisciplinaryBias: mutateScalar(prng, parent.interdisciplinaryBias),
51
+ challengeRate: mutateScalar(prng, parent.challengeRate)
52
+ });
53
+ return mutated;
54
+ }
55
+ function policyVector(policy) {
56
+ return [
57
+ policy.analogyDensity,
58
+ policy.socraticRatio,
59
+ policy.formalism,
60
+ policy.retrievalPractice,
61
+ policy.exerciseCount / 5,
62
+ policy.diagramBias,
63
+ policy.reflectionBias,
64
+ policy.interdisciplinaryBias,
65
+ policy.challengeRate
66
+ ];
67
+ }
68
+ function euclideanDistance(a, b) {
69
+ let sum = 0;
70
+ for (let i = 0; i < a.length; i++) {
71
+ sum += (a[i] - b[i]) ** 2;
72
+ }
73
+ return Math.sqrt(sum / a.length);
74
+ }
75
+ export function noveltyScore(existingPolicies, candidate) {
76
+ if (existingPolicies.length === 0)
77
+ return 1;
78
+ const candidateVec = policyVector(candidate);
79
+ let minDist = Infinity;
80
+ for (const existing of existingPolicies) {
81
+ const dist = euclideanDistance(candidateVec, policyVector(existing));
82
+ minDist = Math.min(minDist, dist);
83
+ }
84
+ return minDist;
85
+ }
86
+ async function loadArchive(filePath) {
87
+ try {
88
+ const content = await readFile(filePath, "utf8");
89
+ return JSON.parse(content);
90
+ }
91
+ catch {
92
+ return {
93
+ currentPolicy: DEFAULT_POLICY,
94
+ bestScore: 0,
95
+ candidates: []
96
+ };
97
+ }
98
+ }
99
+ async function saveArchive(filePath, archive) {
100
+ await writeFile(filePath, `${JSON.stringify(archive, null, 2)}\n`, "utf8");
101
+ }
102
+ export async function evolvePolicy(archivePath, basePolicy, focusTopic, iterations = 24, seed = 20260401, baseWeights = DEFAULT_WEIGHTS) {
103
+ const archive = await loadArchive(archivePath);
104
+ const baseline = await runBenchmarkSuite(process.cwd(), basePolicy, focusTopic, seed, 3, baseWeights);
105
+ let best = baseline;
106
+ let bestWeights = baseWeights;
107
+ const acceptedCandidates = [];
108
+ const exploredCandidates = [];
109
+ const prng = new Prng(seed + 17);
110
+ const seen = [...archive.candidates.map((entry) => entry.policy), basePolicy];
111
+ for (let iteration = 1; iteration <= iterations; iteration += 1) {
112
+ const candidatePolicy = mutatePolicy(best.policy, prng, iteration);
113
+ const candidateWeights = mutateWeights(bestWeights, prng);
114
+ const novelty = noveltyScore(seen, candidatePolicy);
115
+ const candidateBenchmark = await runBenchmarkSuite(process.cwd(), candidatePolicy, focusTopic, seed + iteration * 11, 3, candidateWeights);
116
+ const parameterDelta = diffPolicy(best.policy, candidatePolicy);
117
+ const candidate = {
118
+ policy: candidatePolicy,
119
+ benchmark: candidateBenchmark,
120
+ parentName: best.policy.name,
121
+ iteration,
122
+ novelty,
123
+ accepted: false,
124
+ parameterDelta,
125
+ decision: {
126
+ improves: false,
127
+ safe: false,
128
+ novelEnough: false,
129
+ scoreDelta: 0,
130
+ weakestTopicDelta: 0,
131
+ reasons: []
132
+ }
133
+ };
134
+ const bestWeakest = Math.min(...best.topicBenchmarks.map((entry) => entry.meanScore));
135
+ const candidateWeakest = Math.min(...candidateBenchmark.topicBenchmarks.map((entry) => entry.meanScore));
136
+ const improves = candidateBenchmark.overallScore > best.overallScore;
137
+ const safe = candidateWeakest >= bestWeakest - 1.5;
138
+ const novelEnough = novelty >= 0.05;
139
+ candidate.decision.improves = improves;
140
+ candidate.decision.safe = safe;
141
+ candidate.decision.novelEnough = novelEnough;
142
+ candidate.decision.scoreDelta = candidateBenchmark.overallScore - best.overallScore;
143
+ candidate.decision.weakestTopicDelta = candidateWeakest - bestWeakest;
144
+ if (improves) {
145
+ candidate.decision.reasons.push(`overall score improved by ${candidate.decision.scoreDelta.toFixed(2)}`);
146
+ }
147
+ else {
148
+ candidate.decision.reasons.push(`overall score regressed by ${Math.abs(candidate.decision.scoreDelta).toFixed(2)}`);
149
+ }
150
+ if (safe) {
151
+ candidate.decision.reasons.push(`weakest-topic score stayed within tolerance (${candidate.decision.weakestTopicDelta.toFixed(2)})`);
152
+ }
153
+ else {
154
+ candidate.decision.reasons.push(`weakest-topic score fell too far (${candidate.decision.weakestTopicDelta.toFixed(2)})`);
155
+ }
156
+ if (novelEnough) {
157
+ candidate.decision.reasons.push(`novelty ${novelty.toFixed(3)} cleared the 0.05 threshold`);
158
+ }
159
+ else {
160
+ candidate.decision.reasons.push(`novelty ${novelty.toFixed(3)} was too close to archived policies`);
161
+ }
162
+ if (improves && safe && novelEnough) {
163
+ candidate.accepted = true;
164
+ best = candidateBenchmark;
165
+ bestWeights = candidateWeights;
166
+ acceptedCandidates.push(candidate);
167
+ }
168
+ exploredCandidates.push(candidate);
169
+ seen.push(candidate.policy);
170
+ }
171
+ const nextArchive = {
172
+ currentPolicy: best.policy,
173
+ bestScore: best.overallScore,
174
+ candidates: [
175
+ ...archive.candidates,
176
+ ...exploredCandidates.map((entry) => ({
177
+ policy: entry.policy,
178
+ score: entry.benchmark.overallScore,
179
+ novelty: entry.novelty,
180
+ accepted: entry.accepted,
181
+ iteration: entry.iteration
182
+ }))
183
+ ]
184
+ };
185
+ await saveArchive(archivePath, nextArchive);
186
+ return {
187
+ baseline,
188
+ best,
189
+ acceptedCandidates,
190
+ exploredCandidates,
191
+ archive: nextArchive
192
+ };
193
+ }
194
+ export function evolutionToMarkdown(run) {
195
+ const lines = [
196
+ `# Evolution Report: ${run.best.policy.name}`,
197
+ "",
198
+ `- Baseline score: ${run.baseline.overallScore.toFixed(2)}`,
199
+ `- Best score: ${run.best.overallScore.toFixed(2)}`,
200
+ `- Accepted candidates: ${run.acceptedCandidates.length}`,
201
+ `- Explored candidates: ${run.exploredCandidates.length}`,
202
+ ""
203
+ ];
204
+ lines.push("## Accepted Candidates");
205
+ lines.push("");
206
+ if (run.acceptedCandidates.length === 0) {
207
+ lines.push("- No candidate cleared both the novelty and safety gates in this run.");
208
+ }
209
+ else {
210
+ for (const candidate of run.acceptedCandidates) {
211
+ lines.push(`- Iteration ${candidate.iteration}: ${candidate.policy.name} scored ${candidate.benchmark.overallScore.toFixed(2)} with novelty ${candidate.novelty.toFixed(3)}.`);
212
+ }
213
+ }
214
+ lines.push("");
215
+ lines.push("## Decision Ledger");
216
+ lines.push("");
217
+ for (const candidate of run.exploredCandidates) {
218
+ lines.push(`- Iteration ${candidate.iteration} ${candidate.policy.name}: ${candidate.accepted ? "accepted" : "rejected"}`);
219
+ lines.push(` - score delta: ${candidate.decision.scoreDelta.toFixed(2)}`);
220
+ lines.push(` - weakest-topic delta: ${candidate.decision.weakestTopicDelta.toFixed(2)}`);
221
+ lines.push(` - novelty: ${candidate.novelty.toFixed(3)}`);
222
+ lines.push(` - gates: improves=${candidate.decision.improves}, safe=${candidate.decision.safe}, novelEnough=${candidate.decision.novelEnough}`);
223
+ lines.push(` - reasons: ${candidate.decision.reasons.join("; ")}`);
224
+ if (candidate.parameterDelta.length > 0) {
225
+ lines.push(` - parameter delta: ${candidate.parameterDelta
226
+ .map((entry) => `${entry.field}:${entry.delta >= 0 ? "+" : ""}${entry.delta.toFixed(2)}`)
227
+ .join(", ")}`);
228
+ }
229
+ }
230
+ lines.push("");
231
+ lines.push("## Best Benchmark Snapshot");
232
+ lines.push("");
233
+ lines.push(benchmarkToMarkdown(run.best).trim());
234
+ lines.push("");
235
+ return `${lines.join("\n")}\n`;
236
+ }
237
+ import { optimizePolicy } from "./ax-optimizer.js";
238
+ import { mapElitesEvolve, mapElitesToEvolutionRun, mapElitesToMarkdown } from "./map-elites.js";
239
+ export async function evolveWithGEPA(cwd, basePolicy, options) {
240
+ return optimizePolicy(cwd, basePolicy, options);
241
+ }
242
+ export { mapElitesEvolve, mapElitesToEvolutionRun, mapElitesToMarkdown };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Flash Cards Engine — spaced-repetition flash cards with front/back and mnemonics.
3
+ */
4
+ import { resolveTopic } from "./topics.js";
5
+ export function generateFlashCards(topicName) {
6
+ const topic = resolveTopic(topicName);
7
+ const cards = [];
8
+ // Definition card
9
+ cards.push({
10
+ id: `${topic.slug}-fc-def`,
11
+ front: `What is ${topic.title}?`,
12
+ back: topic.summary,
13
+ tags: ["definition", topic.domain],
14
+ difficulty: "easy",
15
+ source: "definition"
16
+ });
17
+ // Formal core cards
18
+ topic.formalCore.forEach((fc, i) => {
19
+ cards.push({
20
+ id: `${topic.slug}-fc-formal-${i}`,
21
+ front: `Formal property of ${topic.title} (#${i + 1})`,
22
+ back: fc,
23
+ tags: ["formal", topic.domain],
24
+ difficulty: "hard",
25
+ source: "definition"
26
+ });
27
+ });
28
+ // Intuition cards
29
+ topic.intuition.forEach((inText, i) => {
30
+ cards.push({
31
+ id: `${topic.slug}-fc-intuition-${i}`,
32
+ front: `Intuition check: What does "${inText}" mean for ${topic.title}?`,
33
+ back: `This is a concrete way to think about ${topic.title} before formal notation.`,
34
+ mnemonic: inText,
35
+ tags: ["intuition", topic.domain],
36
+ difficulty: "medium",
37
+ source: "intuition"
38
+ });
39
+ });
40
+ // Misconception cards (crucial for learning)
41
+ topic.misconceptions.forEach((mis, i) => {
42
+ cards.push({
43
+ id: `${topic.slug}-fc-misconception-${i}`,
44
+ front: `True or False: "${mis}"`,
45
+ back: `FALSE. ${mis} is a common misconception.`,
46
+ tags: ["misconception", topic.domain],
47
+ difficulty: "hard",
48
+ source: "misconception"
49
+ });
50
+ });
51
+ // Example cards
52
+ topic.examples.forEach((ex, i) => {
53
+ cards.push({
54
+ id: `${topic.slug}-fc-example-${i}`,
55
+ front: `Worked example: ${ex}`,
56
+ back: `This example illustrates ${topic.title} in practice.`,
57
+ tags: ["example", topic.domain],
58
+ difficulty: "medium",
59
+ source: "example"
60
+ });
61
+ });
62
+ // Reflection cards
63
+ topic.reflections.forEach((ref, i) => {
64
+ cards.push({
65
+ id: `${topic.slug}-fc-reflection-${i}`,
66
+ front: ref,
67
+ back: `Consider ${topic.title} from a broader perspective. What changed in your understanding?`,
68
+ tags: ["reflection", topic.domain],
69
+ difficulty: "hard",
70
+ source: "transfer"
71
+ });
72
+ });
73
+ // Transfer card
74
+ if (topic.interdisciplinaryHooks.length > 0) {
75
+ cards.push({
76
+ id: `${topic.slug}-fc-transfer`,
77
+ front: `How does ${topic.title} connect to ${topic.interdisciplinaryHooks[0]}?`,
78
+ back: `Build an analogy that preserves structural relationships, not surface features.`,
79
+ tags: ["transfer", topic.domain],
80
+ difficulty: "hard",
81
+ source: "transfer"
82
+ });
83
+ }
84
+ return {
85
+ topic: topic.title,
86
+ slug: topic.slug,
87
+ generatedAt: new Date().toISOString(),
88
+ cards
89
+ };
90
+ }
91
+ export function flashcardsToMarkdown(deck) {
92
+ const lines = [
93
+ `# Flash Cards: ${deck.topic}`,
94
+ `> ${deck.cards.length} cards | Generated: ${deck.generatedAt}`,
95
+ ""
96
+ ];
97
+ for (const card of deck.cards) {
98
+ lines.push(`---`);
99
+ lines.push(`## ${card.id} [${card.difficulty}]`);
100
+ lines.push(`**Front:** ${card.front}`);
101
+ lines.push("");
102
+ lines.push(`**Back:** ${card.back}`);
103
+ if (card.mnemonic) {
104
+ lines.push(`**Mnemonic:** ${card.mnemonic}`);
105
+ }
106
+ lines.push(`*Tags:* ${card.tags.join(", ")}`);
107
+ lines.push("");
108
+ }
109
+ return lines.join("\n") + "\n";
110
+ }
111
+ /**
112
+ * Anki-compatible TSV export (front \t back).
113
+ */
114
+ export function flashcardsToAnki(deck) {
115
+ const rows = deck.cards.map(c => {
116
+ const front = c.front.replace(/\t/g, " ");
117
+ const back = c.back.replace(/\t/g, " ");
118
+ return `${front}\t${back}`;
119
+ });
120
+ return rows.join("\n");
121
+ }
122
+ /**
123
+ * Mnemonics for selected topics.
124
+ */
125
+ export function getMnemonic(topicName) {
126
+ const mnemonics = {
127
+ derivative: "DERIV-ative: Differential Equation Reveals Instant Velocity",
128
+ entropy: "ENTROPY: Energy Not Totally Recovered, Order Partially Yielded",
129
+ bayes: "BAYES: Beliefs Are Updated, Evidence Subtracts",
130
+ recursion: "RECURSION: Repeating Every Call Until Reaching Simple Initial-Case Output Now"
131
+ };
132
+ return mnemonics[topicName.toLowerCase()] ?? "";
133
+ }
@@ -0,0 +1,108 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { clamp } from "./util.js";
3
+ const DEFAULT_LEARNER_STATE = {
4
+ coveredTopics: [],
5
+ identifiedMisconceptions: [],
6
+ feedback: [],
7
+ sessions: [],
8
+ profile: {
9
+ id: "default",
10
+ priorKnowledge: 0.5,
11
+ abstractionComfort: 0.5,
12
+ analogyNeed: 0.5,
13
+ dialoguePreference: 0.5,
14
+ diagramAffinity: 0.5,
15
+ persistence: 0.5,
16
+ transferDesire: 0.5,
17
+ anxiety: 0.3
18
+ }
19
+ };
20
+ export async function loadLearnerState(filePath) {
21
+ // Read JSON from filePath. If file doesn't exist or is invalid, return default with id "learner-1"
22
+ try {
23
+ const raw = await readFile(filePath, "utf8");
24
+ return JSON.parse(raw);
25
+ }
26
+ catch {
27
+ return { id: "learner-1", ...DEFAULT_LEARNER_STATE };
28
+ }
29
+ }
30
+ export async function saveLearnerState(filePath, state) {
31
+ await writeFile(filePath, JSON.stringify(state, null, 2), "utf8");
32
+ }
33
+ export function recordTopicCoverage(state, topic, masteryEstimate) {
34
+ const existing = state.coveredTopics.find(t => t.slug === topic.slug);
35
+ if (existing) {
36
+ existing.lastSeen = new Date().toISOString();
37
+ existing.masteryEstimate = clamp(masteryEstimate);
38
+ existing.sessionCount += 1;
39
+ }
40
+ else {
41
+ state.coveredTopics.push({
42
+ slug: topic.slug,
43
+ domain: topic.domain,
44
+ lastSeen: new Date().toISOString(),
45
+ masteryEstimate: clamp(masteryEstimate),
46
+ sessionCount: 1
47
+ });
48
+ }
49
+ return state;
50
+ }
51
+ export function recordMisconception(state, topic, misconception) {
52
+ const existing = state.identifiedMisconceptions.find(m => m.topic === topic && m.misconception === misconception);
53
+ if (!existing) {
54
+ state.identifiedMisconceptions.push({ topic, misconception, addressed: false });
55
+ }
56
+ return state;
57
+ }
58
+ export function recordFeedback(state, topic, signal, comment) {
59
+ state.feedback.push({
60
+ topic,
61
+ timestamp: new Date().toISOString(),
62
+ signal,
63
+ comment
64
+ });
65
+ return state;
66
+ }
67
+ export function recordSessionStart(state) {
68
+ if (!state.sessions)
69
+ state.sessions = [];
70
+ state.sessions.push({
71
+ startedAt: new Date().toISOString(),
72
+ topicsCovered: []
73
+ });
74
+ return state;
75
+ }
76
+ export function recordSessionEnd(state, topicsCovered) {
77
+ if (!state.sessions)
78
+ state.sessions = [];
79
+ const current = state.sessions[state.sessions.length - 1];
80
+ if (current && !current.endedAt) {
81
+ current.endedAt = new Date().toISOString();
82
+ current.topicsCovered = topicsCovered;
83
+ }
84
+ return state;
85
+ }
86
+ import { piCompleteJson } from "./pi-agent.js";
87
+ export async function buildProfileFromFeedback(cwd, state) {
88
+ if (state.feedback.length === 0)
89
+ return { ...state.profile };
90
+ const prompt = `You are updating a learner's pedagogical profile based on their history.
91
+ Current Profile: ${JSON.stringify(state.profile, null, 2)}
92
+ Recent Feedback: ${JSON.stringify(state.feedback, null, 2)}
93
+ Covered Topics: ${JSON.stringify(state.coveredTopics, null, 2)}
94
+
95
+ Given this feedback, how should the learner's traits (priorKnowledge, abstractionComfort, analogyNeed, dialoguePreference, diagramAffinity, persistence, transferDesire, anxiety) be updated? Each trait is a scalar from 0.0 to 1.0.
96
+ Respond ONLY with a JSON object exactly matching the LearnerProfile schema (include all fields, even 'id'). Use thoughtful inferences. For example, if they are confused frequently, they may need more analogies and less abstraction. If they give thumbs-up on highly formal topics, they have high abstraction comfort.`;
97
+ try {
98
+ const updated = await piCompleteJson(cwd, prompt, { thinking: "low" });
99
+ return {
100
+ ...updated,
101
+ id: state.profile.id // Preserve ID
102
+ };
103
+ }
104
+ catch (error) {
105
+ console.error("Failed to dynamically update profile, falling back to current:", error);
106
+ return { ...state.profile };
107
+ }
108
+ }
@@ -0,0 +1,155 @@
1
+ import { resolveTopic } from "./topics.js";
2
+ function prerequisiteBullets(topic) {
3
+ return topic.prerequisites.map((item) => `Recall ${item} and connect it to ${topic.title}.`);
4
+ }
5
+ function misconceptionBullets(topic) {
6
+ return topic.misconceptions.map((item) => `Address misconception: ${item}`);
7
+ }
8
+ function practiceBullets(topic, exerciseCount) {
9
+ const bullets = [...topic.exercises];
10
+ while (bullets.length < exerciseCount) {
11
+ bullets.push(`Invent a new example that makes ${topic.title} easier to explain.`);
12
+ }
13
+ return bullets.slice(0, exerciseCount).map((item) => `Practice: ${item}`);
14
+ }
15
+ export function buildLessonPlan(topicName, policy) {
16
+ const topic = resolveTopic(topicName);
17
+ const phases = [
18
+ {
19
+ id: "orient",
20
+ title: "Orientation",
21
+ purpose: "Assess prerequisites and frame the core question.",
22
+ bullets: [
23
+ `State the big question: ${topic.summary}`,
24
+ ...prerequisiteBullets(topic)
25
+ ]
26
+ },
27
+ {
28
+ id: "intuition",
29
+ title: "Intuition",
30
+ purpose: "Teach the concept concretely before pushing notation or abstract framing.",
31
+ bullets: topic.intuition.map((item) => `Intuition: ${item}`)
32
+ },
33
+ {
34
+ id: "formal-core",
35
+ title: "Formal Core",
36
+ purpose: "Escalate into rigorous structure once intuition has traction.",
37
+ bullets: topic.formalCore.map((item) => `Formal: ${item}`)
38
+ },
39
+ {
40
+ id: "misconceptions",
41
+ title: "Misconception Repair",
42
+ purpose: "Anticipate predictable mistakes before they calcify.",
43
+ bullets: misconceptionBullets(topic)
44
+ },
45
+ {
46
+ id: "examples",
47
+ title: "Worked Examples",
48
+ purpose: "Move between examples so the learner sees the invariant structure.",
49
+ bullets: topic.examples.map((item) => `Example: ${item}`)
50
+ },
51
+ {
52
+ id: "practice",
53
+ title: "Guided Practice",
54
+ purpose: "Force retrieval and re-expression, not passive agreement.",
55
+ bullets: practiceBullets(topic, policy.exerciseCount)
56
+ },
57
+ {
58
+ id: "transfer",
59
+ title: "Transfer and Reflection",
60
+ purpose: "Bridge the concept across domains and make the learner summarize what changed.",
61
+ bullets: [
62
+ ...topic.reflections.map((item) => `Reflect: ${item}`),
63
+ `Bridge ${topic.title} into: ${topic.interdisciplinaryHooks.join(", ")}.`
64
+ ]
65
+ }
66
+ ];
67
+ if (policy.diagramBias >= 0.55) {
68
+ phases.splice(4, 0, {
69
+ id: "diagram",
70
+ title: "Diagram",
71
+ purpose: "Compress the concept into a visual structure before free recall.",
72
+ bullets: [
73
+ `Map the concept using nodes: ${topic.diagramNodes.join(" -> ")}.`,
74
+ `Ask the learner to narrate the diagram without reading from it.`
75
+ ]
76
+ });
77
+ }
78
+ if (policy.socraticRatio >= 0.6) {
79
+ phases[0].bullets.unshift(`Open with a diagnostic question instead of a lecture on ${topic.title}.`);
80
+ phases[5].bullets.unshift(`Pause after each practice step and ask the learner to predict the next move.`);
81
+ }
82
+ // Domain-specific phase customizations
83
+ if (topic.domain === "code") {
84
+ const exIdx = phases.findIndex((p) => p.id === "examples");
85
+ if (exIdx !== -1) {
86
+ phases.splice(exIdx + 1, 0, {
87
+ id: "live-code",
88
+ title: "Live Code",
89
+ purpose: "Write and trace runnable code so the learner sees the concept execute.",
90
+ bullets: [
91
+ `Write a minimal runnable example demonstrating ${topic.title}.`,
92
+ `Step through execution line by line, narrating state changes.`,
93
+ `Ask the learner to predict output before running.`
94
+ ]
95
+ });
96
+ }
97
+ }
98
+ if (topic.domain === "law") {
99
+ const examples = phases.find((p) => p.id === "examples");
100
+ if (examples) {
101
+ examples.bullets.push(`Cite at least one leading case or statute relevant to ${topic.title}.`, `Distinguish the ratio decidendi from obiter dicta.`);
102
+ }
103
+ }
104
+ if (topic.domain === "medicine") {
105
+ const formalCore = phases.find((p) => p.id === "formal-core");
106
+ if (formalCore) {
107
+ formalCore.bullets.push(`Reference the level of evidence (RCT, meta-analysis, observational) for key claims about ${topic.title}.`, `Distinguish mechanism-based reasoning from evidence-based conclusions.`);
108
+ }
109
+ }
110
+ if (topic.domain === "history") {
111
+ const examples = phases.find((p) => p.id === "examples");
112
+ if (examples) {
113
+ examples.bullets.push(`Place ${topic.title} on a timeline with at least two contextual events.`, `Distinguish primary sources from secondary interpretation.`);
114
+ }
115
+ }
116
+ if (topic.domain === "psychology") {
117
+ const misconceptions = phases.find((p) => p.id === "misconceptions");
118
+ if (misconceptions) {
119
+ misconceptions.bullets.push(`Flag the replication status of key studies related to ${topic.title}.`, `Distinguish folk-psychology usage from empirical findings.`);
120
+ }
121
+ }
122
+ if (topic.domain === "politics") {
123
+ const transfer = phases.find((p) => p.id === "transfer");
124
+ if (transfer) {
125
+ transfer.bullets.push(`Present at least two competing analytical frameworks for ${topic.title}.`, `Distinguish normative claims from descriptive ones.`);
126
+ }
127
+ }
128
+ if (topic.domain === "arts") {
129
+ const examples = phases.find((p) => p.id === "examples");
130
+ if (examples) {
131
+ examples.bullets.push(`Ground analysis in at least one specific work that exemplifies ${topic.title}.`, `Connect formal technique to expressive effect.`);
132
+ }
133
+ }
134
+ return { topic, policy, phases };
135
+ }
136
+ export function lessonPlanToMarkdown(plan) {
137
+ const lines = [
138
+ `# Lesson Plan: ${plan.topic.title}`,
139
+ "",
140
+ `- Domain: ${plan.topic.domain}`,
141
+ `- Policy: ${plan.policy.name}`,
142
+ `- Summary: ${plan.topic.summary}`,
143
+ ""
144
+ ];
145
+ for (const phase of plan.phases) {
146
+ lines.push(`## ${phase.title}`);
147
+ lines.push(phase.purpose);
148
+ lines.push("");
149
+ for (const bullet of phase.bullets) {
150
+ lines.push(`- ${bullet}`);
151
+ }
152
+ lines.push("");
153
+ }
154
+ return `${lines.join("\n").trim()}\n`;
155
+ }