dsh-diagnostic-tutor 0.1.0 → 0.2.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.
Files changed (126) hide show
  1. package/README.md +316 -79
  2. package/lib/api.js +364 -78
  3. package/lib/api.js.map +1 -1
  4. package/lib/block-types.js +25 -0
  5. package/lib/block-types.js.map +1 -0
  6. package/lib/client.js +9952 -821
  7. package/lib/client.js.map +1 -1
  8. package/lib/diagnostics.js +170 -0
  9. package/lib/diagnostics.js.map +1 -0
  10. package/lib/execution-key.js +91 -0
  11. package/lib/execution-key.js.map +1 -0
  12. package/lib/handoff.js +44 -1
  13. package/lib/handoff.js.map +1 -1
  14. package/lib/index.js +87 -51
  15. package/lib/index.js.map +1 -1
  16. package/lib/insights.js +82 -0
  17. package/lib/insights.js.map +1 -0
  18. package/lib/interactions.js +70 -0
  19. package/lib/interactions.js.map +1 -0
  20. package/lib/journal.js +122 -0
  21. package/lib/journal.js.map +1 -0
  22. package/lib/lesson-markdown.js +72 -0
  23. package/lib/lesson-markdown.js.map +1 -0
  24. package/lib/lesson.js +152 -3
  25. package/lib/lesson.js.map +1 -1
  26. package/lib/math.js +126 -0
  27. package/lib/math.js.map +1 -0
  28. package/lib/path-api.js +140 -0
  29. package/lib/path-api.js.map +1 -0
  30. package/lib/path-runtime.js +361 -0
  31. package/lib/path-runtime.js.map +1 -0
  32. package/lib/path-tools.js +107 -0
  33. package/lib/path-tools.js.map +1 -0
  34. package/lib/path.js +179 -0
  35. package/lib/path.js.map +1 -0
  36. package/lib/plan-tools.js +89 -0
  37. package/lib/plan-tools.js.map +1 -0
  38. package/lib/plan.js +82 -0
  39. package/lib/plan.js.map +1 -0
  40. package/lib/project-session.js +48 -0
  41. package/lib/project-session.js.map +1 -0
  42. package/lib/prompt.js +4 -19
  43. package/lib/prompt.js.map +1 -1
  44. package/lib/review-sync.js +51 -0
  45. package/lib/review-sync.js.map +1 -0
  46. package/lib/review-tools.js +115 -0
  47. package/lib/review-tools.js.map +1 -0
  48. package/lib/review.js +103 -0
  49. package/lib/review.js.map +1 -0
  50. package/lib/source-schema.js +24 -0
  51. package/lib/source-schema.js.map +1 -0
  52. package/lib/source-tools.js +61 -0
  53. package/lib/source-tools.js.map +1 -0
  54. package/lib/sources.js +78 -0
  55. package/lib/sources.js.map +1 -0
  56. package/lib/state.js +353 -47
  57. package/lib/state.js.map +1 -1
  58. package/lib/tools.js +620 -123
  59. package/lib/tools.js.map +1 -1
  60. package/lib/types/api.d.ts +10 -3
  61. package/lib/types/api.d.ts.map +1 -1
  62. package/lib/types/block-types.d.ts +12 -0
  63. package/lib/types/block-types.d.ts.map +1 -0
  64. package/lib/types/contract.d.ts +81 -7
  65. package/lib/types/contract.d.ts.map +1 -1
  66. package/lib/types/diagnostics.d.ts +95 -0
  67. package/lib/types/diagnostics.d.ts.map +1 -0
  68. package/lib/types/execution-key.d.ts +59 -0
  69. package/lib/types/execution-key.d.ts.map +1 -0
  70. package/lib/types/handoff.d.ts +2 -2
  71. package/lib/types/handoff.d.ts.map +1 -1
  72. package/lib/types/index.d.ts +19 -14
  73. package/lib/types/index.d.ts.map +1 -1
  74. package/lib/types/insights.d.ts +57 -0
  75. package/lib/types/insights.d.ts.map +1 -0
  76. package/lib/types/interactions.d.ts +23 -0
  77. package/lib/types/interactions.d.ts.map +1 -0
  78. package/lib/types/journal.d.ts +57 -0
  79. package/lib/types/journal.d.ts.map +1 -0
  80. package/lib/types/lesson-markdown.d.ts +11 -0
  81. package/lib/types/lesson-markdown.d.ts.map +1 -0
  82. package/lib/types/lesson.d.ts +697 -5
  83. package/lib/types/lesson.d.ts.map +1 -1
  84. package/lib/types/math.d.ts +50 -0
  85. package/lib/types/math.d.ts.map +1 -0
  86. package/lib/types/path-api.d.ts +5 -0
  87. package/lib/types/path-api.d.ts.map +1 -0
  88. package/lib/types/path-runtime.d.ts +1558 -0
  89. package/lib/types/path-runtime.d.ts.map +1 -0
  90. package/lib/types/path-tools.d.ts +4 -0
  91. package/lib/types/path-tools.d.ts.map +1 -0
  92. package/lib/types/path.d.ts +744 -0
  93. package/lib/types/path.d.ts.map +1 -0
  94. package/lib/types/plan-tools.d.ts +4 -0
  95. package/lib/types/plan-tools.d.ts.map +1 -0
  96. package/lib/types/plan.d.ts +53 -0
  97. package/lib/types/plan.d.ts.map +1 -0
  98. package/lib/types/project-session.d.ts +33 -0
  99. package/lib/types/project-session.d.ts.map +1 -0
  100. package/lib/types/prompt.d.ts.map +1 -1
  101. package/lib/types/review-sync.d.ts +17 -0
  102. package/lib/types/review-sync.d.ts.map +1 -0
  103. package/lib/types/review-tools.d.ts +4 -0
  104. package/lib/types/review-tools.d.ts.map +1 -0
  105. package/lib/types/review.d.ts +65 -0
  106. package/lib/types/review.d.ts.map +1 -0
  107. package/lib/types/source-schema.d.ts +31 -0
  108. package/lib/types/source-schema.d.ts.map +1 -0
  109. package/lib/types/source-tools.d.ts +4 -0
  110. package/lib/types/source-tools.d.ts.map +1 -0
  111. package/lib/types/sources.d.ts +33 -0
  112. package/lib/types/sources.d.ts.map +1 -0
  113. package/lib/types/state.d.ts +607 -23
  114. package/lib/types/state.d.ts.map +1 -1
  115. package/lib/types/tools.d.ts +3 -1
  116. package/lib/types/tools.d.ts.map +1 -1
  117. package/lib/types/udt.d.ts +20 -24
  118. package/lib/types/udt.d.ts.map +1 -1
  119. package/lib/types/version.d.ts +11 -0
  120. package/lib/types/version.d.ts.map +1 -0
  121. package/lib/types/vocabulary.d.ts +2 -2
  122. package/lib/udt.js +72 -19
  123. package/lib/udt.js.map +1 -1
  124. package/lib/version.js +26 -0
  125. package/lib/version.js.map +1 -0
  126. package/package.json +27 -22
package/README.md CHANGED
@@ -11,19 +11,35 @@ learn interactively, and move to the next best step.
11
11
 
12
12
  ## Status
13
13
 
14
- `v0.1.0` — the first stable release, and the first one meant to be **installed
15
- rather than built**: `dsh plugin --profile web add dsh-diagnostic-tutor`, with
16
- prebuilt output and no clone, no `pnpm install`, no build step.
14
+ `v0.2.0` is the published release. It includes the learning-path flow — saved
15
+ goal and time budget, evidence-referenced initial assessment, versioned course
16
+ routes with per-lesson Overviews, session-scoped attempts, and the
17
+ English/Simplified-Chinese interface — plus the content/input/review loop:
18
+
19
+ - typeset math, formula/code/quiz/flashcard/resource/comparison blocks;
20
+ - learner sources with `sourceRefs` citations;
21
+ - Today plan with ICS export and learner-controlled completion;
22
+ - spaced review with automatic flashcard cards, four ratings and undo;
23
+ - read-only Insights.
24
+
25
+ Install it with
26
+ `dsh plugin --profile web add dsh-diagnostic-tutor@0.2.0` — no clone, no
27
+ `pnpm install`, no build step. A checkout can be installed instead (see
28
+ [Installing a checkout](#installing-a-checkout-instead-of-the-package)).
17
29
 
18
30
  The learning loop works end to end: a real goal in your own words, a diagnosis
19
31
  map grown one node at a time, a tutor-written lesson, a check answered in the
20
32
  chat, recorded evidence, and a next step the tutor chose — which the learner
21
- presses Continue to take.
33
+ presses Continue to take. The route flow adds a full plan the learner reviews
34
+ and accepts, lesson by lesson; the content loop adds math, sources, Today and
35
+ review on top of it.
22
36
 
23
37
  It is also early. The [limitations](#known-limitations) are listed rather than
24
38
  left to be discovered, and nothing below claims a capability that is not there:
25
- no PDF or document ingestion, no RAG, no flashcards, no resource libraries, no
26
- analytics, no course generation.
39
+ no PDF extraction, no RAG, no automatic external resource fetching, no analytics.
40
+ Review scheduling is a local interval heuristic, not FSRS. Course routes are
41
+ proposed by the teaching model, never generated by the runtime, and a route never
42
+ marks knowledge as mastered.
27
43
 
28
44
  ## Two halves, one system
29
45
 
@@ -119,20 +135,19 @@ see is a *diagnosis map*, not a syllabus.
119
135
 
120
136
  Stated plainly, because each one is a real edge a user can reach.
121
137
 
122
- **Math is styled, not typeset.** The tutor writes LaTeX by convention — `\(...\)`
123
- inline, `\[...\]` display — and the runtime sets it apart in a monospace face
124
- with its own background. It does **not** render it. Real typesetting needs a
125
- library plus fonts and CSS, and the client bundle is a single JavaScript file
126
- with nowhere to serve those from. This is the most visible rough edge in any
127
- STEM lesson.
128
-
129
- **Skill presence cannot always be detected.** The runtime reports whether the
130
- Universal Diagnostic Tutor skill is available, but the skill registry reads the
131
- *global* layer unless it is given a viewing scope, and the standard web profile
132
- mounts skills per agent. From a plugin at the profile root the catalog is
133
- therefore empty whether the skill is installed or not. The runtime says
134
- `cannot tell` rather than guessing, and the panel stays quiet. If lessons never
135
- appear, check the skill first.
138
+ **Math is rendered as MathML on the host.** The tutor writes LaTeX by convention
139
+ — `\(...\)` inline, `\[...\]` display — and the host pre-renders it with
140
+ KaTeX's MathML output before storing the block. The browser renders native
141
+ MathML through a tag/attribute whitelist; no font or CSS is fetched. Invalid
142
+ TeX inside a text block falls back to the old styled source; an invalid
143
+ `formula` block is refused at write time. Browser MathML quality varies, and
144
+ this build ships no KaTeX CSS/fonts.
145
+
146
+ **Skill presence requires a live Agent.** The runtime checks the open
147
+ conversation's Agent scope on each overview, including its workspace. Before an
148
+ Agent exists, or while discovery is empty or incomplete, it reports `cannot tell`
149
+ and the panel stays quiet. Polling picks up later registration and Agent changes.
150
+ If lessons never appear, check the skill first.
136
151
 
137
152
  **Storage is one document.** Every record lives in
138
153
  `<dsh-home>/storages/udt.json`. It is readable, copyable and easy to export, and
@@ -154,8 +169,12 @@ matrix as load-bearing, not decoration.
154
169
 
155
170
  | This plugin | Verified against DSH | Node |
156
171
  | --- | --- | --- |
172
+ | `0.2.0` | `0.2.1-rc.3`, `0.2.1-rc.2`, `0.2.0-rc.2` (test suite also on `0.1.7-alpha.2`) | `^22.19.0 \|\| >=24.0.0` |
173
+ | `0.1.1` | `0.2.1-rc.3`, `0.2.1-rc.2`, `0.2.0-rc.2` (test suite also on `0.1.7-alpha.2`) | `^22.19.0 \|\| >=24.0.0` |
157
174
  | `0.1.0` | `0.1.7-alpha.2` (also composed under `0.1.5-rc.1`) | `^22.19.0 \|\| >=24.0.0` |
158
175
 
176
+ Phase 1–6 的本机 boot 矩阵(2026-10-29)覆盖 `0.2.1-rc.3`、`0.2.1-rc.2`、`0.2.0-rc.2`、`0.1.7-alpha.2` 的插件加载、本地路由、domain v1 与客户端启动;其中 rc3/alpha.2 还通过了 tarball 安装,并用真实模型完成 lesson → Review 评分/撤销 → Insights;rc2/rc20 的完整真实会话仍是补充项。详见 [`docs/planning/COMPATIBILITY.md`](docs/planning/COMPATIBILITY.md)。
177
+
159
178
  Rules this repository enforces mechanically:
160
179
 
161
180
  - **Named exports only.** A `export default` plugin makes Cordis' loader prefer
@@ -165,7 +184,42 @@ Rules this repository enforces mechanically:
165
184
  second runtime copy can be resolved. Enforced by `verbatimModuleSyntax`, and
166
185
  verified on the built artifact (it contains zero imports).
167
186
  - **No `instanceof` across package boundaries.** Take runtime objects from `ctx`.
168
- - Peer ranges are wide (`<0.2.0`); dev dependencies pin one exact cohort.
187
+ - Peer ranges name the release lines this plugin was verified on. DSH ships
188
+ prereleases, and npm's semver only admits a prerelease when its line is named
189
+ in the range — so the compatibility matrix above, not the range floor, is the
190
+ contract. Dev dependencies pin one exact cohort for the type baseline.
191
+
192
+ ## UI baseline for redesign
193
+
194
+ The current surfaces, state model and screenshots (generated from fixtures) live in
195
+ [`docs/design/ui-baseline/index.html`](docs/design/ui-baseline/index.html) with a
196
+ detailed handoff in
197
+ [`docs/design/ui-baseline/README.md`](docs/design/ui-baseline/README.md). It covers
198
+ the system structure, Learning map / Current lesson surfaces, every route-page state
199
+ and the design constraints a new UI must keep.
200
+
201
+ ## Diagnostics
202
+
203
+ Monitoring is local and content-free: no chat, goal, lesson or evidence text is
204
+ recorded, and nothing is sent anywhere.
205
+
206
+ ```sh
207
+ pnpm doctor # human-readable health report
208
+ node scripts/doctor.mjs --json # machine-readable
209
+ node scripts/doctor.mjs --strict # exit 1 if an error-severity issue exists
210
+ ```
211
+
212
+ It reads `<dsh-home>/storages/udt.json` plus the event journal and reports each
213
+ project's stage, pending/failed tutor requests (with age), stalled attempts and
214
+ route-length conflicts. The same report is available at
215
+ `GET /diagnostic-tutor/api/diagnostics` (loopback-only, like every plugin
216
+ route).
217
+
218
+ The host journal lives at `<dsh-home>/logs/diagnostic-tutor-events.jsonl`
219
+ (`DSH_DIAGNOSTIC_TUTOR_EVENTS` overrides the path, `off` disables it). It logs
220
+ lifecycle events only: tool names and operations, durations, request outcomes,
221
+ provider error messages (truncated). A single file is capped at 1 MB and
222
+ rotated once.
169
223
 
170
224
  ---
171
225
 
@@ -175,7 +229,7 @@ Rules this repository enforces mechanically:
175
229
 
176
230
  | | |
177
231
  | --- | --- |
178
- | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | verified against `0.1.7-alpha.2` |
232
+ | [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | verified against `0.2.1-rc.3` |
179
233
  | the [Universal Diagnostic Tutor](https://github.com/SenmuuuuW/universal-diagnostic-tutor-skill) skill | the teaching brain. Without it the runtime loads, records and displays state, but **no lesson is ever written** |
180
234
  | Node | `^22.19.0 \|\| >=24.0.0` |
181
235
 
@@ -227,7 +281,7 @@ learn?*.
227
281
 
228
282
  ```sh
229
283
  npm pack
230
- dsh plugin --profile web add "$PWD/dsh-diagnostic-tutor-0.1.0-rc.1.tgz"
284
+ dsh plugin --profile web add "$PWD/dsh-diagnostic-tutor-0.2.0.tgz"
231
285
  ```
232
286
 
233
287
  **What gets installed is prebuilt.** The package ships `lib/` — the host half
@@ -258,10 +312,30 @@ With the json backend you get exactly one document, `<storage root>/udt.json`
258
312
 
259
313
  | Slot | Holds |
260
314
  | --- | --- |
261
- | `global` | the learner singleton — preferences, active goal, `initializedAt` |
315
+ | `global` | the learner singleton — preferences, `initializedAt`, and the legacy shared `activeCourseId` marker |
262
316
  | `tables.courses` | one record per learning goal, in the learner's own words |
263
317
  | `tables.nodes` | the diagnosis map: `id`, `courseId`, `title`, `parentId?`, `relation`, `state`, `evidence[]` |
264
318
 
319
+ Courses, maps, lessons and evidence are shared by the learner's conversations.
320
+ Project selection, focus, next step and handoff belong to one conversation,
321
+ identified by its session ID. The legacy `activeCourseId` remains in storage for
322
+ compatibility; it does not select a project for a conversation. A fresh
323
+ conversation starts with the library, even when historical projects exist.
324
+
325
+ | Conversation selection | Project binding |
326
+ | --- | --- |
327
+ | `none` | A new goal, or a validated write explicitly naming a course or node in this conversation, may select its project. Reading shared data does not select it. |
328
+ | `new` | The learner pressed **Start something new**. Only a new goal created here may bind; historical map or lesson writes leave this choice intact. |
329
+ | `project` | The selected course is the default for tools. Creating another goal does not replace it; writes to another course require selecting that course first. |
330
+
331
+ `udt_status` reports this conversation's `selection` and, only for `project`,
332
+ its `courseId`. Goal, map and lesson writes return `projectAdopted` and
333
+ `projectBinding`: `adopted`, `already-selected`, `not-selected`, or
334
+ `missing-session-id`. A choice made while a write is pending takes precedence
335
+ over that write's adoption. Without a reliable session ID, status reports
336
+ `sessionAvailable: false`; explicit shared writes can still be stored, but their
337
+ result is `missing-session-id` and they claim no conversation binding.
338
+
265
339
  Two deliberate rules:
266
340
 
267
341
  - **zod is the contract.** Every record is validated at the durable boundary, so
@@ -294,16 +368,28 @@ route around them:
294
368
 
295
369
  ### Tools
296
370
 
297
- Five, and none of them decides anything about teaching.
371
+ Eighteen. None of them decides anything about teaching; they record or report.
298
372
 
299
373
  | Tool | Does |
300
374
  | --- | --- |
301
- | `udt_status` | reports the runtime: domain, version, goals, map, **and the current focus** |
302
- | `udt_goal_create` | records a goal and plants the map root — and nothing else |
303
- | `udt_map_get` | reads the map with each node's relation, state and evidence |
375
+ | `udt_status` | reports shared goals and counts, plus this conversation's selection, focus and pending next step |
376
+ | `udt_goal_create` | records a goal and plants only the map root; may also select it for this conversation |
377
+ | `udt_map_get` | reads the selected project's map, or an explicitly named shared course, with each node's relation, state and evidence |
304
378
  | `udt_map_update` | `add-nodes` · `set-state` · `add-evidence` |
305
- | `udt_lesson_update` | writes teaching into the learning surface as blocks |
379
+ | `udt_lesson_update` | writes teaching into the learning surface as blocks; pre-renders math; attaches source citations |
380
+ | `udt_quiz_update` | records a quiz/flashcard submission; never grades and never changes node state |
306
381
  | `udt_decide_next` | records where the learner should go next, and why |
382
+ | `udt_path_get` | reads the goal brief, assessment, routes and this conversation's active attempt |
383
+ | `udt_path_update` | saves a brief, assessment or route proposal |
384
+ | `udt_course_run_update` | binds a diagnosed node to an attempt, or completes with evidence |
385
+ | `udt_source_add` | stores pasted text or one explicitly named local `.md`/`.txt` file; deduplicates by SHA-256 |
386
+ | `udt_source_list` | lists source metadata without content |
387
+ | `udt_source_get` | returns one stored source for citation |
388
+ | `udt_plan_get` | reads the learner-owned Today plan |
389
+ | `udt_plan_update` | adds, completes or removes plan items; planning is policy, not teaching |
390
+ | `udt_review_add` | queues review cards; scheduling is policy, not mastery |
391
+ | `udt_review_next` | reads cards due now |
392
+ | `udt_review_grade` | records a review rating and reschedules; never confirms a node |
307
393
 
308
394
  The division is the architecture: the tutor decides **what** to teach, when to
309
395
  check, and what an answer showed; the runtime decides **what may be stored** and
@@ -377,36 +463,36 @@ the button means.
377
463
 
378
464
  | Surface | Where | For |
379
465
  | --- | --- | --- |
380
- | **Learning tab** | right sidebar, beside the chat | everyday work — map, node and lesson while you talk |
381
- | **Learning panel** | the main column (`main`) | focus mode — the whole runtime at once |
382
-
383
- The tab is the reason the loop is usable: the full panel fills the main column,
384
- which is also where the conversation lives, so with only that panel answering a
385
- check meant leaving the lesson. The right sidebar is a separate column.
466
+ | **Learning map** | the main column (`main`, key `diagnostic-tutor-map`) | browsing — course route, every lesson Overview, knowledge & evidence |
467
+ | **Current lesson** | the right sidebar tab, beside the chat | the lesson itself — teaching units, and checks answered in the chat |
386
468
 
387
- Both run on one shared `useLearning` state, so they cannot disagree about what
388
- is focused or what the tutor wrote. The panel's **Answer in the chat** button
389
- returns to the conversation and docks the tab in the same step.
469
+ One page for browsing, one surface for learning. The page never starts a lesson;
470
+ the tab never decides what to teach. Both read the same `useLearning` state and
471
+ the same path client, so they cannot disagree about what is focused, accepted or
472
+ written. **Open current lesson** on the map returns to the conversation with the
473
+ lesson tab open in the same step.
390
474
 
391
- The right sidebar hosts session-scoped tabs, so it can only accept one while a
392
- session surface is mounted — which is why docking happens on the way back to the
393
- conversation rather than at load.
475
+ The container for the main column is the map page. A full-page lesson panel used
476
+ to exist as a second main destination; it was removed so the same lesson is not
477
+ offered in two places. The standalone full-page component is still used by the
478
+ preview and the tests, but the plugin does not register it.
394
479
 
395
480
  ### Teaching-brain detection
396
481
 
397
- At load the plugin asks the platform's own skill registry whether the
398
- Universal Diagnostic Tutor skill is installed — no path is hardcoded, no skill
399
- root is assumed, and nothing is copied. A missing catalog, a missing skill and
400
- an unreadable body each degrade to a reported status rather than an error.
482
+ Each overview asks the platform's own Agent and Skill registries whether the
483
+ Universal Diagnostic Tutor skill is available in the requesting session's live
484
+ Agent scope and cwd. No path is hardcoded, no skill root is assumed, and nothing
485
+ is copied. Missing services and incomplete discovery return an inconclusive
486
+ status; complete scoped discovery can establish presence or absence.
401
487
 
402
488
  Compatibility is probed by **capability**, not by a version string: the skill's
403
489
  maintenance contract permits only `name` and `description` in frontmatter, so
404
490
  it cannot declare a version. The result carries a short content digest as a
405
491
  version hint.
406
492
 
407
- Detection results stay **internal** — logged at `debug`, absent from every tool
408
- output. The skill forbids naming its files, versions or repository in
409
- learner-facing text, and this runtime will not be what leaks them.
493
+ Paths, fingerprints and compatibility details stay **internal**, absent from
494
+ every tool output and overview. The overview exposes only availability. The skill
495
+ forbids naming its files, versions or repository in learner-facing text.
410
496
 
411
497
  ### How the two halves agree
412
498
 
@@ -422,20 +508,76 @@ which judged an answer is *not finished* until the next step is recorded — so
422
508
  the bridge was **deleted rather than kept as a second voice**. The plugin got
423
509
  better at it, which is the evidence it belonged upstream.
424
510
 
425
- ## The panel
426
-
427
- The browser half registers two things and nothing else: a sidebar icon
428
- (`sidebar.panellist`, a `list`) and the page it opens (`main`, a `keyed` slot).
429
- The sidebar `id` and the panel `key` come from one constant — a drift between
430
- them would leave the icon opening nothing.
431
-
432
- It reads as one sentence, left to right:
511
+ ## The page and the lesson surface
512
+
513
+ The browser half registers three things: the **Learning map** icon in the
514
+ sidebar (`sidebar.panellist`, a `list`), the page it opens (`main`, a `keyed`
515
+ slot, key `diagnostic-tutor-map`), and the **Current lesson** tab in the right
516
+ sidebar (a tab type plus its body and title). The map's sidebar `id` and its
517
+ `main` key come from one constant — a drift between them would leave the icon
518
+ opening nothing.
519
+
520
+ The page has two tabs: **Course route** (the versioned plan, route canvas,
521
+ ordered lesson list and every Overview) and **Knowledge & evidence** (the
522
+ diagnosis map and its recorded observations). Neither one teaches: they show
523
+ stored state.
524
+
525
+ The route tab shows one stage at a time and one next step. A progress bar names
526
+ the four stages — goal & time, initial diagnosis, route, learning — and a
527
+ **Next step** card always answers "what now?" with a single primary action
528
+ (set the goal, request or answer the diagnosis, request the route, accept the
529
+ version, start the first lesson, continue the current one) plus at most one
530
+ real alternative. Competing action rows live in the folded history, not beside
531
+ it. Before a goal is saved the wizard owns the page; while the tutor is asking
532
+ diagnostic questions the card points back to the conversation, shows how long it
533
+ has waited, says whether the tutor is still thinking, and on a failed turn
534
+ retries with the current model and opens the chat (the failure is recorded on
535
+ the path request, so it survives a restart; provider errors are translated into
536
+ billing / auth / rate-limit / network / context advice); route versions
537
+ appear only after the diagnosis is complete or skipped; once accepted, the page
538
+ leads with the route canvas. A proposed or accepted route shows its total study
539
+ time, the weeks it takes at the saved weekly budget, and any lessons longer than
540
+ the preferred session. Next step moves to the first unfinished lesson after a
541
+ completion, and says when the whole route is done. Completed diagnosis details
542
+ and route settings stay reachable in folded sections.
543
+
544
+ The plugin's own accent is **DeepSeek's brand blue** — `#4d6bfe` on the light
545
+ canvas, `#6a84ff` on the dark one — used sparingly for progress, the active tab,
546
+ the current/selected item, focus rings and action links. Primary buttons keep the
547
+ host's near-black/white fill: the blue marks, it does not act. All other colours
548
+ continue to come from the host theme.
549
+
550
+ The initial diagnosis is asked through DSH's **interactive question card**
551
+ (`ask_user_question`) when the profile provides it: options are tapped rather
552
+ than typed, multi-select works, and a free-text question covers explanations.
553
+ The tutor still owns the questions, evidence and assessment; profiles without
554
+ that tool fall back to chat text.
555
+
556
+ The lesson lives in the docked tab, where the conversation is still on screen.
557
+ That is what makes the check loop work without leaving the lesson. The sidebar
558
+ action **Learn** opens that tab and never takes over the main column; the map's
559
+ **Open current lesson** button returns to the conversation and opens the same
560
+ tab.
433
561
 
434
562
  ```
435
- [ Course + Diagnosis Map ] → [ Selected node ] → [ Learning surface ]
563
+ sidebar Learn ─────────────▶ Current lesson (right sidebar)
564
+ sidebar Learning map ──────▶ Learning map (main column: route | knowledge)
565
+
566
+ Open current lesson ◀────── returns to chat + lesson tab
436
567
  ```
437
568
 
438
- There is no dashboard: three panes, and the map is the navigation.
569
+ Entering learning follows the conversation, not the window. A project is
570
+ shared, but its focus and planned runs belong to a conversation, so the project
571
+ library and **Open current lesson** ask the host which conversation was last
572
+ learning the project (`GET /project/session`, read-only) and switch there with
573
+ the shell's session navigation when it differs. The order is: newest active
574
+ focus, then newest course run with an attempt, then newest project selection;
575
+ only a project no conversation has ever touched falls back to the conversation
576
+ on screen.
577
+
578
+ A full-page lesson panel used to exist as a second main destination. It was
579
+ removed: one page for browsing, one surface for learning, and the conversation
580
+ keeps the main column.
439
581
 
440
582
  ### Learning Blocks
441
583
 
@@ -445,25 +587,42 @@ keyed by `type`:
445
587
 
446
588
  | Type | Content |
447
589
  | --- | --- |
448
- | `text` | `md` — markdown, with the skill's `\(...\)` math convention |
590
+ | `text` | `md` — markdown, plus host `math[]` fragments for typeset formulas |
449
591
  | `example` | `title`, `steps[]`, `takeaway?` |
450
592
  | `diagram` | `format` (`ascii` \| `mermaid`), `spec`, `caption?` |
451
593
  | `check` | `prompt`, `expect?`, `hint?` — the stop-and-wait surface |
452
-
453
- Adding Formula, Code, Comparison, Practice or Resource later means adding one
454
- registry entry, never rewriting the lesson renderer. An **unknown type renders a
455
- readable placeholder** rather than throwing, so a lesson authored by a newer
456
- host still renders here.
594
+ | `formula` | `tex`, `display?`, host-rendered `mathml`, `caption?` |
595
+ | `code` | `language?`, `source`, `caption?` — displayed, never executed |
596
+ | `quiz` | `title?`, `questions[]` (`mcq`/`short`); answers are recorded, never graded by the runtime |
597
+ | `flashcard` | `front`, `back`, `tags?`; self-report is recorded, not scheduled as mastery |
598
+ | `resource` | `title`, optional `sourceId`/`locator`/`url`/`note` |
599
+ | `comparison` | `columns[]`, `rows[][]`, `caption?` |
600
+
601
+ Blocks may also carry `sourceRefs: [{sourceId, locator}]`; every reference is
602
+ checked against the `sources` table at write time. The stored schema accepts an
603
+ **unknown type** from a newer host and the client renders a readable placeholder;
604
+ the current model tool still writes only known types.
605
+ The Learning map also shows a compact **Today** card when `udt_plan_update` has created open plan items, with a done action and an **Export calendar** action for dated items. DSH's own `schedule` is session-local, so this is an explicit calendar export rather than a push notification. Review cards and their local interval scheduler are stored but have no separate review surface yet.
457
606
 
458
607
  ### Browser API
459
608
 
460
- Three calls, at `/diagnostic-tutor/api`:
609
+ The browser's core calls, at `/diagnostic-tutor/api` (the full route list is in
610
+ [the architecture notes](docs/planning/CURRENT-ARCHITECTURE.md)):
461
611
 
462
612
  | Route | Returns |
463
613
  | --- | --- |
464
614
  | `GET /overview` | the current course and its whole map (`course: null` on a first run) |
465
615
  | `GET /node?id=` | one node with its evidence, parent and children |
616
+ | `GET /project/session?courseId=` | the conversation that should resume a project (read-only) |
466
617
  | `POST /lesson {nodeId}` | the prototype lesson, built once and reused after that |
618
+ | `GET /source?id=` | one stored source and its text, for a citation preview |
619
+ | `POST /path/answer` | record a quiz/flashcard submission on the active attempt |
620
+ | `GET /plan/ics` | download open, dated Today plan items as an ICS calendar |
621
+ | `GET /review` | due review cards (read-only) |
622
+ | `POST /review/grade` | record a review rating and reschedule |
623
+ | `POST /review/undo` | restore the previous schedule after the latest grade |
624
+ | `GET /insights?courseId=` | read-only course insight counts |
625
+ | `POST /plan/task` | mark a Today plan item done (learner action; plan only) |
467
626
 
468
627
  Every request passes a **trust fence**: a bare `ctx.webServer.register()` route
469
628
  inherits no authentication, so the route checks that the request arrived at a
@@ -471,6 +630,27 @@ loopback `Host`, from a loopback `Origin`, and is not marked cross-site. Anythin
471
630
  else gets `403` and no body. The browser receives **views only** — no storage
472
631
  path, no domain handle, no raw record.
473
632
 
633
+ ### Interface language
634
+
635
+ Every plugin surface ships copy in **English and Simplified Chinese**: the
636
+ Learning map page, the course route, the docked Current-lesson tab, the
637
+ standalone full-page panel used by the preview and tests, and the shared
638
+ next-step card, handoff line and welcome row. The sidebar Learn action and its
639
+ missing-conversation explanation switch too. A toggle in the map header (and in
640
+ the lesson surfaces) changes the copy in place; the language is saved on the
641
+ learner profile (`preferredLanguage`, via `GET /preferences` and
642
+ `POST /preferences/language`), so it survives a restart and applies to every
643
+ conversation. The choice applies immediately and is saved in the background: a
644
+ failed request keeps the local choice rather than flipping the UI back.
645
+
646
+ It is copy, not content: the tutor's teaching, the learner's goal text, lesson
647
+ overviews, node titles and recorded evidence are never machine-translated, and
648
+ the model is not instructed to change language by the toggle. Three things stay
649
+ outside the switch: the **Learning map** sidebar label (registered once when the
650
+ plugin loads, so it cannot follow an in-place change), server/model error text
651
+ and dynamic handoff details as generated, and the development preview's own
652
+ toolbar.
653
+
474
654
  ## Preview
475
655
 
476
656
  The panel takes its API as a prop, so the UI runs with no DSH and no agent:
@@ -492,18 +672,67 @@ Machine Learning [goal, unconfirmed]
492
672
  └─ Python [prerequisite, unconfirmed]
493
673
  ```
494
674
 
675
+ Two query parameters reach the states that are otherwise awkward to produce on
676
+ demand — `?scenario=` (`first-use`, `map`, `handoff`, `next`, `stay`, `noskill`,
677
+ `long`, `scroll`, `library`, `new`, `showcase`, `curriculum`, `path-draft`,
678
+ `path-setup`, `path-assessment`) and `?surface=tab` / `?surface=map` for the
679
+ docked right-sidebar tab and the standalone map page. The scenario switch is in
680
+ the page's toolbar, and the theme toggle sets the same
681
+ `body[data-ds-dark-theme]` attribute DSH does. The page carries the real DSH
682
+ design tokens for the same reason: with hardcoded fallbacks in their place, a
683
+ token that resolves to near-white in dark mode looks perfectly fine locally.
684
+
495
685
  To capture the panel from a *live* profile instead:
496
686
 
497
687
  ```sh
498
688
  pnpm screenshot "<dsh-url-with-token>" preview/dsh-ui.png
499
689
  ```
500
690
 
691
+ ### Auditing the presentation
692
+
693
+ ```sh
694
+ pnpm preview # in one shell
695
+ pnpm audit:ui http://127.0.0.1:8391/?scenario=next # in another
696
+ pnpm audit:ui "<dsh-url-with-token>" /tmp/audit --surface=tab
697
+ pnpm check:scroll http://127.0.0.1:8391/ /tmp/udt-scroll-check
698
+ ```
699
+
700
+ The scroll regression requires local Chrome and runs against the built bundle.
701
+ The preview uses a clipped, fixed-height slot like DSH, so the whole document
702
+ cannot scroll to mask an unscrollable plugin. The check sends real wheel events
703
+ and verifies bottom content remains visible in the tab, expanded tab, stacked
704
+ panel, all three wide-panel panes, and long library/welcome views, in both themes.
705
+ Only fixture data is used; no live learner state is changed. The ordinary visual
706
+ audit alone does **not** verify scrolling.
707
+
708
+ The visual audit measures, at each width this project targets and in both themes: the contrast
709
+ of every control's own text against its own fill, class names in the DOM that no
710
+ rule matches, how many controls carry primary weight in one view, tap targets
711
+ under 28px, type under 10.5px, and anything spilling or clipped. It prints a
712
+ table, writes `findings.json`, and saves screenshots — it does not fail the run,
713
+ so a human can disagree with it.
714
+
715
+ That is not a substitute for looking. It is what makes looking affordable: the
716
+ docked tab's invisible primary button and the dark-mode 1.05:1 controls were
717
+ both found by it, in a live profile, in seconds.
718
+
719
+ What the surfaces look like now, captured from a live profile:
720
+
721
+ | | light | dark |
722
+ | --- | --- | --- |
723
+ | standalone panel (preview/tests only) | ![panel, light](preview/ui-polish/panel-light-1440.png) | ![panel, dark](preview/ui-polish/panel-dark-1440.png) |
724
+ | docked tab | ![tab, light](preview/ui-polish/tab-light-400.png) | ![tab, dark](preview/ui-polish/tab-dark-400.png) |
725
+
726
+ These captures predate the current layout: the standalone full-page panel is no
727
+ longer a product surface. Today the Learning map page owns the main column and
728
+ the Current lesson tab holds the teaching.
729
+
501
730
  ## Development
502
731
 
503
732
  ```sh
504
733
  pnpm install
505
734
  pnpm typecheck # tsc --noEmit (host and client)
506
- pnpm test # 253 tests: unit, guard, DOM, render, real composition
735
+ pnpm test # unit, guard, DOM, render, real composition
507
736
  pnpm build # tsc -> lib/ (host) + tsdown -> lib/client.js
508
737
  ```
509
738
 
@@ -546,8 +775,10 @@ shows a loader row exists.
546
775
  | `v0.0.10` | export and delete your data |
547
776
  | `v0.0.11` | storage layout tested; `single` kept, with the reason |
548
777
  | `v0.1.0-rc.1` | packaged install, no clone or build required |
549
- | `v0.1.0` | **current** — first stable release, installable from npm |
550
- | `v0.2.0` | settings, i18n, math typesetting |
778
+ | `v0.1.0` | first stable release, installable from npm |
779
+ | `v0.1.1` | packaged release with the docked learning surface |
780
+ | `v0.2.0` | **published** — Phase 1–6 content/input/review loop, learning-path flow, Today/Review/Insights |
781
+ | next | settings, automatic replan suggestions (the tutor already proposes revisions on request; no runtime auto-replan) |
551
782
 
552
783
  ## Trust
553
784
 
@@ -555,7 +786,10 @@ DSH does not sandbox plugin code: an installed plugin runs in-process with your
555
786
  privileges, and a bare `ctx.webServer.register()` route inherits no
556
787
  authentication. This plugin's commitments:
557
788
 
558
- - reads and writes only its own storage domain;
789
+ - reads and writes only its own storage domain, with one explicit exception:
790
+ `udt_source_add(path)` reads exactly one regular `.md`/`.markdown`/`.txt` file
791
+ the caller names, once, then stores its text in that domain. It never scans a
792
+ directory, follows a symlink, accepts a URL, or fetches anything;
559
793
  - serves its browser half only from loopback-fenced routes it checks itself;
560
794
  - makes no outbound network requests;
561
795
  - writes no files outside the harness's own storage;
@@ -571,18 +805,19 @@ panel opens, the map and the lesson surface render, and `GET /export` and
571
805
  `POST /reset` work. What does not happen is teaching — nothing writes a lesson,
572
806
  because nothing is making teaching decisions.
573
807
 
574
- **The plugin cannot always tell you that.** The skill registry reads the
575
- *global* layer unless it is given a viewing scope, and the standard web profile
576
- mounts the filesystem skill provider **inside a per-agent layer**. So a plugin
577
- at the profile root sees an empty skill catalog whether the skill is missing or
578
- merely mounted where it cannot look. The runtime therefore reports three states,
579
- not two:
808
+ **Availability is checked for the conversation you have open.** On every
809
+ `GET /overview?sessionId=…`, the plugin resolves that session's live Agent from
810
+ DSH's Agent registry and asks the Skill registry for a complete snapshot using
811
+ `{ scope: agent, cwd: agent.session.header.cwd }`. A root catalog containing
812
+ Office skills cannot establish whether that Agent has a tutor. Services are
813
+ resolved for each request, so delayed registration, removal and preset changes
814
+ are picked up by the panel's existing polling. The runtime reports three states:
580
815
 
581
816
  | `teachingBrain` | meaning | what the panel does |
582
817
  | --- | --- | --- |
583
- | `true` | found | nothing |
584
- | `false` | not found, **in a catalog that has entries** | says a tutor is needed |
585
- | `null` | cannot tell from this scope | says nothing |
818
+ | `true` | found and model-invocable in the live Agent scope | nothing |
819
+ | `false` | unavailable after complete scoped discovery with entries | says a tutor is needed |
820
+ | `null` | no live Agent, empty/incomplete discovery, or unsupported/unavailable service | says nothing |
586
821
 
587
822
  An earlier revision treated "not in my catalog" as "not installed" and showed a
588
823
  notice on that basis. In a web profile that was a confident, wrong answer — the
@@ -655,4 +890,6 @@ MIT. The Universal Diagnostic Tutor skill is a separate MIT project by the same
655
890
  author and is not vendored here.
656
891
 
657
892
  Planning and architecture research for this project live in
658
- [`docs/planning/`](docs/planning/).
893
+ [`docs/planning/`](docs/planning/). Phase 1–6 merge order, acceptance checks and the DSH compatibility checklist are in
894
+ [`docs/planning/PHASE-1-6-MERGE.md`](docs/planning/PHASE-1-6-MERGE.md). 0.2.0 发布清单与验证记录见
895
+ [`docs/planning/RELEASE-0.2.0.md`](docs/planning/RELEASE-0.2.0.md).