dsh-diagnostic-tutor 0.1.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.
- package/LICENSE +21 -0
- package/README.md +658 -0
- package/cordis.patch.yml +23 -0
- package/lib/api.js +413 -0
- package/lib/api.js.map +1 -0
- package/lib/client.js +2029 -0
- package/lib/client.js.map +1 -0
- package/lib/contract.js +14 -0
- package/lib/contract.js.map +1 -0
- package/lib/diagnosis.js +224 -0
- package/lib/diagnosis.js.map +1 -0
- package/lib/handoff.js +194 -0
- package/lib/handoff.js.map +1 -0
- package/lib/index.js +186 -0
- package/lib/index.js.map +1 -0
- package/lib/lesson.js +285 -0
- package/lib/lesson.js.map +1 -0
- package/lib/prompt.js +96 -0
- package/lib/prompt.js.map +1 -0
- package/lib/state.js +500 -0
- package/lib/state.js.map +1 -0
- package/lib/tools.js +994 -0
- package/lib/tools.js.map +1 -0
- package/lib/trust-fence.js +101 -0
- package/lib/trust-fence.js.map +1 -0
- package/lib/types/api.d.ts +62 -0
- package/lib/types/api.d.ts.map +1 -0
- package/lib/types/contract.d.ts +147 -0
- package/lib/types/contract.d.ts.map +1 -0
- package/lib/types/diagnosis.d.ts +116 -0
- package/lib/types/diagnosis.d.ts.map +1 -0
- package/lib/types/handoff.d.ts +141 -0
- package/lib/types/handoff.d.ts.map +1 -0
- package/lib/types/index.d.ts +71 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/lesson.d.ts +295 -0
- package/lib/types/lesson.d.ts.map +1 -0
- package/lib/types/prompt.d.ts +85 -0
- package/lib/types/prompt.d.ts.map +1 -0
- package/lib/types/state.d.ts +627 -0
- package/lib/types/state.d.ts.map +1 -0
- package/lib/types/tools.d.ts +38 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/trust-fence.d.ts +53 -0
- package/lib/types/trust-fence.d.ts.map +1 -0
- package/lib/types/udt.d.ts +95 -0
- package/lib/types/udt.d.ts.map +1 -0
- package/lib/types/vocabulary.d.ts +162 -0
- package/lib/types/vocabulary.d.ts.map +1 -0
- package/lib/udt.js +141 -0
- package/lib/udt.js.map +1 -0
- package/lib/vocabulary.js +182 -0
- package/lib/vocabulary.js.map +1 -0
- package/package.json +104 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-diagnostic-tutor contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
# Universal Diagnostic Tutor for DeepSeek Harness
|
|
2
|
+
|
|
3
|
+
> **From a Tutor Skill to a Learning Runtime.**
|
|
4
|
+
|
|
5
|
+
Diagnosis-first AI learning runtime for DeepSeek Harness — map what you need,
|
|
6
|
+
learn interactively, and move to the next best step.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
<sub>The learning surface docked in the right sidebar, beside the conversation. The map, the lesson and the chat are on screen at once, so answering a check never means leaving the lesson.</sub>
|
|
11
|
+
|
|
12
|
+
## Status
|
|
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.
|
|
17
|
+
|
|
18
|
+
The learning loop works end to end: a real goal in your own words, a diagnosis
|
|
19
|
+
map grown one node at a time, a tutor-written lesson, a check answered in the
|
|
20
|
+
chat, recorded evidence, and a next step the tutor chose — which the learner
|
|
21
|
+
presses Continue to take.
|
|
22
|
+
|
|
23
|
+
It is also early. The [limitations](#known-limitations) are listed rather than
|
|
24
|
+
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.
|
|
27
|
+
|
|
28
|
+
## Two halves, one system
|
|
29
|
+
|
|
30
|
+
The unusual thing about this project is that **the teaching and the runtime are
|
|
31
|
+
separate programs**, and only one of them makes decisions.
|
|
32
|
+
|
|
33
|
+
| | Owns | Where |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| **[Universal Diagnostic Tutor](https://github.com/SenmuuuuW/universal-diagnostic-tutor-skill) skill**<br>= **the teaching brain** | What to teach next: diagnosis, teaching moves, pacing, when a check is passed, what the next step is | that repository (v2.1, MIT) |
|
|
36
|
+
| **`dsh-diagnostic-tutor`**<br>= **the learning runtime** | Where it is kept: persistent learner state, the diagnosis map, structured lessons, the UI | here |
|
|
37
|
+
|
|
38
|
+
That split is not a packaging detail. This repository contains **no teaching
|
|
39
|
+
logic**: no rule that says "if blocked, explain the prerequisite", no rule that
|
|
40
|
+
says "if wrong, give a simpler example". Those live in the skill. The runtime
|
|
41
|
+
stores what the tutor decided, shows it, and never decides it.
|
|
42
|
+
|
|
43
|
+
## The loop
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
Goal → Diagnose → Map → Learn → Check → Decide → Next lesson
|
|
47
|
+
│ │ │ │ │ │
|
|
48
|
+
│ │ │ │ │ └ the tutor names
|
|
49
|
+
│ │ │ │ │ the next node, with a reason
|
|
50
|
+
│ │ │ │ └ the learner answers in the chat
|
|
51
|
+
│ │ │ └ the tutor writes the lesson into the side panel
|
|
52
|
+
│ │ └ nodes appear only as diagnosis reveals them
|
|
53
|
+
│ └ the tutor asks what you actually know
|
|
54
|
+
└ stated in your own words, in the chat
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Read left to right, that is also the guarantee: **the runtime never advances on
|
|
58
|
+
its own.** A decision is stored with the tutor's reason, shown to the learner,
|
|
59
|
+
and waits to be pressed.
|
|
60
|
+
|
|
61
|
+
### The demo scenario
|
|
62
|
+
|
|
63
|
+
The screenshots below come from one scenario, run against real DeepSeek Harness
|
|
64
|
+
with the real skill:
|
|
65
|
+
|
|
66
|
+
> **I want to learn machine learning. I know some Python, but my math is weak.**
|
|
67
|
+
|
|
68
|
+
The tutor asks what the goal is *for* before it teaches anything, then grows a
|
|
69
|
+
map one diagnosis at a time.
|
|
70
|
+
|
|
71
|
+
Nothing in these images is a fixture or a mock — they are screenshots of the
|
|
72
|
+
running app. `demo-1` is a cold start from an empty store; the rest are the same
|
|
73
|
+
scenario resumed, because the clarify-then-diagnose phase costs several model
|
|
74
|
+
turns and a cold start to a full lesson runs to roughly fifteen minutes.
|
|
75
|
+
|
|
76
|
+
| | |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
|  | **First use.** No goal yet. One question, and the sentence that answers it — no wizard, no empty dashboard. |
|
|
79
|
+
|  | **The diagnosis map.** Six nodes, each traceable to evidence, nested by depth. `blocked` and `checked` read at a glance; nothing is `confirmed` without a check behind it. |
|
|
80
|
+
|  | **A lesson.** Real headings and lists, a worked example set apart, a diagram in its own frame, and a check that hands the turn back to the learner. |
|
|
81
|
+
|  | **The docked surface.** The same loop in the right sidebar, with the handoff progress line (`Lesson ready 7s`) so a slow model turn reads as *working* rather than *broken*. |
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Why this is not just another AI tutor
|
|
86
|
+
|
|
87
|
+
Most "AI tutor" projects are a prompt wrapped around a chat box: the model
|
|
88
|
+
teaches, and nothing about the learner survives the conversation. Three things
|
|
89
|
+
are different here.
|
|
90
|
+
|
|
91
|
+
**The teaching brain is a real, separate artifact.** Diagnosis, teaching moves,
|
|
92
|
+
pacing and mastery judgement live in a written skill with its own protocols —
|
|
93
|
+
not in a system prompt this repository invented. This plugin depends on it and
|
|
94
|
+
refuses to duplicate it.
|
|
95
|
+
|
|
96
|
+
**State is real, and it is the learner's.** A goal, a map of what you actually
|
|
97
|
+
know, and a record of the evidence behind each status. Nothing is `confirmed`
|
|
98
|
+
without a check to back it, nothing is scored, and it is all visible and
|
|
99
|
+
exportable.
|
|
100
|
+
|
|
101
|
+
**The runtime does not decide.** The tutor names the next step and says why; the
|
|
102
|
+
runtime stores that and shows it; the learner presses Continue. There is no
|
|
103
|
+
path by which progress advances on its own.
|
|
104
|
+
|
|
105
|
+
Its state vocabulary is not invented here either: nodes carry the skill's own
|
|
106
|
+
seven status terms, and checks carry its six readiness outcomes.
|
|
107
|
+
|
|
108
|
+
### Why a map does not contradict diagnosis-first
|
|
109
|
+
|
|
110
|
+
The skill is explicit that a broad goal must **never** become a pre-expanded
|
|
111
|
+
curriculum or a course outline. This plugin does not produce one. Nodes appear
|
|
112
|
+
only as diagnosis reveals them, every node is `unconfirmed` until a check
|
|
113
|
+
produces evidence, and the map is reversible — new evidence moves it. What you
|
|
114
|
+
see is a *diagnosis map*, not a syllabus.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Known limitations
|
|
119
|
+
|
|
120
|
+
Stated plainly, because each one is a real edge a user can reach.
|
|
121
|
+
|
|
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.
|
|
136
|
+
|
|
137
|
+
**Storage is one document.** Every record lives in
|
|
138
|
+
`<dsh-home>/storages/udt.json`. It is readable, copyable and easy to export, and
|
|
139
|
+
it is also a single point of failure: a record that no longer matches its schema
|
|
140
|
+
stops the plugin from loading. Nothing is deleted when that happens, the error
|
|
141
|
+
names the table and the key, and removing the record by hand restores the rest —
|
|
142
|
+
but **export before you edit it**.
|
|
143
|
+
|
|
144
|
+
**The tutor does not always converge.** It can judge `more-practice` on the same
|
|
145
|
+
node several turns running, each time writing a fresh check. That is its pacing
|
|
146
|
+
rather than the runtime's, but it decides whether a node ever finishes and the
|
|
147
|
+
loop moves on.
|
|
148
|
+
|
|
149
|
+
## Compatibility
|
|
150
|
+
|
|
151
|
+
A DSH profile can resolve **more than one harness version at once** (the running
|
|
152
|
+
host, the shared profile fallback, and each plugin's own store). Treat this
|
|
153
|
+
matrix as load-bearing, not decoration.
|
|
154
|
+
|
|
155
|
+
| This plugin | Verified against DSH | Node |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| `0.1.0` | `0.1.7-alpha.2` (also composed under `0.1.5-rc.1`) | `^22.19.0 \|\| >=24.0.0` |
|
|
158
|
+
|
|
159
|
+
Rules this repository enforces mechanically:
|
|
160
|
+
|
|
161
|
+
- **Named exports only.** A `export default` plugin makes Cordis' loader prefer
|
|
162
|
+
`.default` and **silently drop `inject`** — a documented DSH outage. Pinned by
|
|
163
|
+
a test that fails if a default export is ever introduced.
|
|
164
|
+
- **`@deepseek-ai/*` is imported `type`-only**, so the compiler erases it and no
|
|
165
|
+
second runtime copy can be resolved. Enforced by `verbatimModuleSyntax`, and
|
|
166
|
+
verified on the built artifact (it contains zero imports).
|
|
167
|
+
- **No `instanceof` across package boundaries.** Take runtime objects from `ctx`.
|
|
168
|
+
- Peer ranges are wide (`<0.2.0`); dev dependencies pin one exact cohort.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Install
|
|
173
|
+
|
|
174
|
+
**Requirements**
|
|
175
|
+
|
|
176
|
+
| | |
|
|
177
|
+
| --- | --- |
|
|
178
|
+
| [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) | verified against `0.1.7-alpha.2` |
|
|
179
|
+
| 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
|
+
| Node | `^22.19.0 \|\| >=24.0.0` |
|
|
181
|
+
|
|
182
|
+
**Install the plugin**
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
# `web` is a shipped profile template: it is created on first use with the
|
|
186
|
+
# base and web-app bundles, so this works on a machine that has never run DSH.
|
|
187
|
+
dsh plugin --profile web add dsh-diagnostic-tutor
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Use your own profile name in place of `web` to install into an existing one.
|
|
191
|
+
`--profile` is **mandatory** — without it `dsh plugin` exits non-zero, because it
|
|
192
|
+
is a thin pnpm forwarder that needs a profile to forward into.
|
|
193
|
+
|
|
194
|
+
For a new profile that is not one of the shipped names, create it from a
|
|
195
|
+
template first:
|
|
196
|
+
|
|
197
|
+
```sh
|
|
198
|
+
dsh --profile mine --from-default-profile web --dump-config >/dev/null
|
|
199
|
+
dsh plugin --profile mine add dsh-diagnostic-tutor
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Install the skill**
|
|
203
|
+
|
|
204
|
+
The skill is a separate project and is not bundled here — install it where your
|
|
205
|
+
DSH agent looks for skills (see its README). The runtime works with or without
|
|
206
|
+
it; without it, nothing writes a lesson. Note that the runtime cannot always
|
|
207
|
+
*detect* it: see [Known limitations](#known-limitations).
|
|
208
|
+
|
|
209
|
+
**Verify**
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
# The plugin should appear in the merged tree, as an insert row.
|
|
213
|
+
dsh --profile web --dump-config | grep -A2 dsh-diagnostic-tutor
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Then start the profile and look for **Learn** in the sidebar:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
dsh web --port 8399 --no-open
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
A `--dump-config` entry only proves a loader row exists — it is not proof the
|
|
223
|
+
plugin runs. Opening the panel is: a first run shows *What do you want to
|
|
224
|
+
learn?*.
|
|
225
|
+
|
|
226
|
+
**Install from a tarball** — the same command with a path:
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
npm pack
|
|
230
|
+
dsh plugin --profile web add "$PWD/dsh-diagnostic-tutor-0.1.0-rc.1.tgz"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**What gets installed is prebuilt.** The package ships `lib/` — the host half
|
|
234
|
+
and the browser bundle — plus `cordis.patch.yml`. No build step, no clone, no
|
|
235
|
+
`pnpm install`, and no `link:` dependency. The only runtime dependency is `zod`;
|
|
236
|
+
every `@deepseek-ai/*` package is a **peer**, resolved from the harness itself,
|
|
237
|
+
so a second copy of the DSH runtime is never pulled in.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Learner state and the diagnosis map
|
|
242
|
+
|
|
243
|
+
State lives in one Cordis **storage domain** (`udt`, version 1) over the
|
|
244
|
+
official `storageDomain` seam. The profile chooses the medium — the standard
|
|
245
|
+
profiles route it through `dsh-storage-json` under `dshHomePath('storages')` —
|
|
246
|
+
so this plugin never hardcodes a path.
|
|
247
|
+
|
|
248
|
+
With the json backend you get exactly one document, `<storage root>/udt.json`
|
|
249
|
+
(`~/.dsh/storages/udt.json` for a default install):
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"unit": { "name": "udt", "version": 1 },
|
|
254
|
+
"global": { "initializedAt": "…", "updatedAt": "…", "activeCourseId": "…" },
|
|
255
|
+
"tables": { "courses": { "…": {} }, "nodes": { "…": {} } }
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
| Slot | Holds |
|
|
260
|
+
| --- | --- |
|
|
261
|
+
| `global` | the learner singleton — preferences, active goal, `initializedAt` |
|
|
262
|
+
| `tables.courses` | one record per learning goal, in the learner's own words |
|
|
263
|
+
| `tables.nodes` | the diagnosis map: `id`, `courseId`, `title`, `parentId?`, `relation`, `state`, `evidence[]` |
|
|
264
|
+
|
|
265
|
+
Two deliberate rules:
|
|
266
|
+
|
|
267
|
+
- **zod is the contract.** Every record is validated at the durable boundary, so
|
|
268
|
+
a hand-edited or corrupt document fails loudly instead of entering memory.
|
|
269
|
+
- **No scores, ever.** There is no field for points, grades or percentages —
|
|
270
|
+
the skill forbids turning mastery into a score, and a test asserts that no
|
|
271
|
+
such key exists anywhere in the persisted document.
|
|
272
|
+
|
|
273
|
+
Records are never mutated in place; writes go through `put`/`set` on one
|
|
274
|
+
per-domain write chain, so concurrent writers cannot interleave.
|
|
275
|
+
|
|
276
|
+
### How the map is kept from becoming a syllabus
|
|
277
|
+
|
|
278
|
+
The rules are enforced in `src/diagnosis.ts` as pure functions, so no tool can
|
|
279
|
+
route around them:
|
|
280
|
+
|
|
281
|
+
- a node is **born `unconfirmed`**, and stays there without evidence;
|
|
282
|
+
- `confirmed` requires a **`check` or `transfer`** evidence entry. Explanation
|
|
283
|
+
or practice alone never confirms — the skill is explicit that "explanation
|
|
284
|
+
alone and one lucky answer never confirm readiness";
|
|
285
|
+
- a **goal node can never be confirmed**: it is the frame of the map, not a
|
|
286
|
+
claim about the learner;
|
|
287
|
+
- every non-goal node must attach to a **parent that already exists** in the
|
|
288
|
+
same course, and there are no cycles — so the map grows outward from what has
|
|
289
|
+
been diagnosed, one step at a time;
|
|
290
|
+
- **at most 8 nodes per call and 40 per course.** A single call cannot plant a
|
|
291
|
+
term's worth of material;
|
|
292
|
+
- `relation` is `goal | part-of | prerequisite | related`. There is deliberately
|
|
293
|
+
no `next-in-course`, because nothing in this runtime knows a teaching order.
|
|
294
|
+
|
|
295
|
+
### Tools
|
|
296
|
+
|
|
297
|
+
Five, and none of them decides anything about teaching.
|
|
298
|
+
|
|
299
|
+
| Tool | Does |
|
|
300
|
+
| --- | --- |
|
|
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 |
|
|
304
|
+
| `udt_map_update` | `add-nodes` · `set-state` · `add-evidence` |
|
|
305
|
+
| `udt_lesson_update` | writes teaching into the learning surface as blocks |
|
|
306
|
+
| `udt_decide_next` | records where the learner should go next, and why |
|
|
307
|
+
|
|
308
|
+
The division is the architecture: the tutor decides **what** to teach, when to
|
|
309
|
+
check, and what an answer showed; the runtime decides **what may be stored** and
|
|
310
|
+
renders it. There is no branch anywhere in this repository that says "if blocked
|
|
311
|
+
then explain the prerequisite" — that is the skill's call, made in the chat.
|
|
312
|
+
|
|
313
|
+
### The loop
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
press Start learning
|
|
317
|
+
→ focus recorded (courseId, nodeId, startedAt, status)
|
|
318
|
+
→ the tutor is woken in that conversation with the node named
|
|
319
|
+
→ the tutor teaches into the surface via udt_lesson_update
|
|
320
|
+
→ the learner answers the check in the chat
|
|
321
|
+
→ the tutor judges, records evidence via udt_map_update, decides the next move
|
|
322
|
+
→ the panel follows
|
|
323
|
+
→ the tutor decides the next step with udt_decide_next
|
|
324
|
+
→ the panel shows the recommendation and its reason
|
|
325
|
+
→ the learner presses Continue, and the next node begins
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### The decision
|
|
329
|
+
|
|
330
|
+
`action` is the skill's six readiness outcomes, reused rather than re-invented:
|
|
331
|
+
the words for "what this concept showed" and "where that sends the learner" are
|
|
332
|
+
the same words. The runtime only needs one structural fact about each — whether
|
|
333
|
+
it names a target:
|
|
334
|
+
|
|
335
|
+
| outcome | target | means |
|
|
336
|
+
| --- | --- | --- |
|
|
337
|
+
| `advance` / `advance-with-caution` | required | move there |
|
|
338
|
+
| `step-down` | required | the blocker; usually a prerequisite |
|
|
339
|
+
| `review-first` | optional | go back, or review here |
|
|
340
|
+
| `more-practice` / `diagnose-again` | forbidden | stay here |
|
|
341
|
+
|
|
342
|
+
A move ends the focus and stamps `endedAt`; staying leaves it open. Nothing
|
|
343
|
+
moves on its own — the learner reads the reason and presses Continue.
|
|
344
|
+
|
|
345
|
+
### The wait, measured
|
|
346
|
+
|
|
347
|
+
Moving to a node is not instant, so the wait is a **persisted record** keyed by
|
|
348
|
+
target node — which makes it idempotent, refresh-proof, restart-proof and
|
|
349
|
+
retryable — and it carries a timestamp per stage:
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
requestedAt → focusRecordedAt → promptedAt → firstActivityAt → lessonAt → observedAt
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
The same record is the progress line (`focus recorded` → `tutor requested` →
|
|
356
|
+
`tutor working` → `lesson ready`, with elapsed seconds) and the measurement. A
|
|
357
|
+
real run against DSH 0.1.6-alpha.2 and the real skill:
|
|
358
|
+
|
|
359
|
+
| stage | when |
|
|
360
|
+
| --- | --- |
|
|
361
|
+
| focus persisted | 0.0s |
|
|
362
|
+
| followup accepted | 0.0s |
|
|
363
|
+
| first tutor activity | 1.0s |
|
|
364
|
+
| lesson written | 26.1s |
|
|
365
|
+
| UI observed | 26.1s |
|
|
366
|
+
|
|
367
|
+
**The plugin costs about a second; the rest is the model writing.** A stall is
|
|
368
|
+
derived from the record rather than stored, and retrying never touches the
|
|
369
|
+
focus — a timeout is a statement about the wait, not about where the learner is.
|
|
370
|
+
|
|
371
|
+
`Start learning` is a **user-role turn attributed to this plugin**, not injected
|
|
372
|
+
context: `agent.inject()` would add model-visible context without waking an idle
|
|
373
|
+
agent, so nothing would happen until the learner typed. Opening a turn is what
|
|
374
|
+
the button means.
|
|
375
|
+
|
|
376
|
+
### Two surfaces, one state
|
|
377
|
+
|
|
378
|
+
| Surface | Where | For |
|
|
379
|
+
| --- | --- | --- |
|
|
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.
|
|
386
|
+
|
|
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.
|
|
390
|
+
|
|
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.
|
|
394
|
+
|
|
395
|
+
### Teaching-brain detection
|
|
396
|
+
|
|
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.
|
|
401
|
+
|
|
402
|
+
Compatibility is probed by **capability**, not by a version string: the skill's
|
|
403
|
+
maintenance contract permits only `name` and `description` in frontmatter, so
|
|
404
|
+
it cannot declare a version. The result carries a short content digest as a
|
|
405
|
+
version hint.
|
|
406
|
+
|
|
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.
|
|
410
|
+
|
|
411
|
+
### How the two halves agree
|
|
412
|
+
|
|
413
|
+
The skill's guardrails say mastery tracking must never become "scores,
|
|
414
|
+
databases, hidden memory, or a curriculum roadmap", while this runtime
|
|
415
|
+
deliberately persists state and renders a map.
|
|
416
|
+
|
|
417
|
+
Until `v0.0.8` that tension was bridged from this side: a short system-prompt
|
|
418
|
+
section explained the runtime's storage semantics to the teaching brain. UDT
|
|
419
|
+
v2.1's `learning_runtime_contract.md` now states all of it in the skill's own
|
|
420
|
+
words — what a runtime may hold, when a decision is recorded, and that a turn
|
|
421
|
+
which judged an answer is *not finished* until the next step is recorded — so
|
|
422
|
+
the bridge was **deleted rather than kept as a second voice**. The plugin got
|
|
423
|
+
better at it, which is the evidence it belonged upstream.
|
|
424
|
+
|
|
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:
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
[ Course + Diagnosis Map ] → [ Selected node ] → [ Learning surface ]
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
There is no dashboard: three panes, and the map is the navigation.
|
|
439
|
+
|
|
440
|
+
### Learning Blocks
|
|
441
|
+
|
|
442
|
+
A block is `{ id, type, content, metadata? }`. The **schema is host-side**
|
|
443
|
+
(zod-validated at the durable boundary) and the **renderers are browser-side**,
|
|
444
|
+
keyed by `type`:
|
|
445
|
+
|
|
446
|
+
| Type | Content |
|
|
447
|
+
| --- | --- |
|
|
448
|
+
| `text` | `md` — markdown, with the skill's `\(...\)` math convention |
|
|
449
|
+
| `example` | `title`, `steps[]`, `takeaway?` |
|
|
450
|
+
| `diagram` | `format` (`ascii` \| `mermaid`), `spec`, `caption?` |
|
|
451
|
+
| `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.
|
|
457
|
+
|
|
458
|
+
### Browser API
|
|
459
|
+
|
|
460
|
+
Three calls, at `/diagnostic-tutor/api`:
|
|
461
|
+
|
|
462
|
+
| Route | Returns |
|
|
463
|
+
| --- | --- |
|
|
464
|
+
| `GET /overview` | the current course and its whole map (`course: null` on a first run) |
|
|
465
|
+
| `GET /node?id=` | one node with its evidence, parent and children |
|
|
466
|
+
| `POST /lesson {nodeId}` | the prototype lesson, built once and reused after that |
|
|
467
|
+
|
|
468
|
+
Every request passes a **trust fence**: a bare `ctx.webServer.register()` route
|
|
469
|
+
inherits no authentication, so the route checks that the request arrived at a
|
|
470
|
+
loopback `Host`, from a loopback `Origin`, and is not marked cross-site. Anything
|
|
471
|
+
else gets `403` and no body. The browser receives **views only** — no storage
|
|
472
|
+
path, no domain handle, no raw record.
|
|
473
|
+
|
|
474
|
+
## Preview
|
|
475
|
+
|
|
476
|
+
The panel takes its API as a prop, so the UI runs with no DSH and no agent:
|
|
477
|
+
|
|
478
|
+
```sh
|
|
479
|
+
pnpm build && pnpm preview # then open the printed URL
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
`preview/index.html` loads the **real built bundle** through a
|
|
483
|
+
`__ModuleLoader__` shim over `preview/fixture.js`, which models a learner who
|
|
484
|
+
said "I want to learn machine learning" with shaky maths:
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
Machine Learning [goal, unconfirmed]
|
|
488
|
+
├─ Math Foundations [prerequisite, blocked]
|
|
489
|
+
│ ├─ Linear Algebra [part-of, unconfirmed]
|
|
490
|
+
│ ├─ Calculus [part-of, unconfirmed]
|
|
491
|
+
│ └─ Probability [part-of, unconfirmed]
|
|
492
|
+
└─ Python [prerequisite, unconfirmed]
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
To capture the panel from a *live* profile instead:
|
|
496
|
+
|
|
497
|
+
```sh
|
|
498
|
+
pnpm screenshot "<dsh-url-with-token>" preview/dsh-ui.png
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## Development
|
|
502
|
+
|
|
503
|
+
```sh
|
|
504
|
+
pnpm install
|
|
505
|
+
pnpm typecheck # tsc --noEmit (host and client)
|
|
506
|
+
pnpm test # 253 tests: unit, guard, DOM, render, real composition
|
|
507
|
+
pnpm build # tsc -> lib/ (host) + tsdown -> lib/client.js
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Installing a checkout instead of the package
|
|
511
|
+
|
|
512
|
+
Working on the plugin itself, rather than using it:
|
|
513
|
+
|
|
514
|
+
```sh
|
|
515
|
+
pnpm install && pnpm build
|
|
516
|
+
|
|
517
|
+
# Use an absolute path. `dsh plugin` runs pnpm inside the profile directory, so
|
|
518
|
+
# a relative path would resolve against the profile, not your checkout.
|
|
519
|
+
dsh plugin --profile <profile> add "$PWD"
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
The profile links the directory, so `pnpm build` is enough to pick up a change.
|
|
523
|
+
This is the only path that needs a clone and a build — the published package
|
|
524
|
+
ships prebuilt `lib/`.
|
|
525
|
+
|
|
526
|
+
`tests/harness.ts` mounts the **same storage stack the standard profiles use**
|
|
527
|
+
(`systemPrompt` → `tools`, and `storage` → `storage-json` → `storage-domain`)
|
|
528
|
+
over a temporary root. Persistence tests therefore exercise a real
|
|
529
|
+
serialize → file → reparse → validate round trip rather than a fake, and
|
|
530
|
+
`--dump-config` is never mistaken for proof that a plugin loads: that only
|
|
531
|
+
shows a loader row exists.
|
|
532
|
+
|
|
533
|
+
## Roadmap
|
|
534
|
+
|
|
535
|
+
| Version | Ships |
|
|
536
|
+
| --- | --- |
|
|
537
|
+
| `v0.0.1` | installable bundle, plugin loads, guard + composition tests |
|
|
538
|
+
| `v0.0.2` | `udt` storage domain, learner round-trip, `udt_status` tool |
|
|
539
|
+
| `v0.0.3` | teaching-brain detection, `udt_goal_create`, the diagnosis map (`udt_map_get` / `udt_map_update`) |
|
|
540
|
+
| `v0.0.4` | the client half: slot-mounted panel, clickable map, node detail, Learning Blocks, browser API |
|
|
541
|
+
| `v0.0.5` | the loop: learning focus, tutor-written lessons, check → evidence → state, live panel |
|
|
542
|
+
| `v0.0.6` | the learning surface docks beside the chat; both surfaces share one state |
|
|
543
|
+
| `v0.0.7` | the tutor decides the next step; focus lifecycle; NEXT BEST STEP card |
|
|
544
|
+
| `v0.0.8` | handoff record, progress line, retry, and the latency measured |
|
|
545
|
+
| `v0.0.9` | DSH 0.1.7 compatibility, the first real A → B, and product polish |
|
|
546
|
+
| `v0.0.10` | export and delete your data |
|
|
547
|
+
| `v0.0.11` | storage layout tested; `single` kept, with the reason |
|
|
548
|
+
| `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 |
|
|
551
|
+
|
|
552
|
+
## Trust
|
|
553
|
+
|
|
554
|
+
DSH does not sandbox plugin code: an installed plugin runs in-process with your
|
|
555
|
+
privileges, and a bare `ctx.webServer.register()` route inherits no
|
|
556
|
+
authentication. This plugin's commitments:
|
|
557
|
+
|
|
558
|
+
- reads and writes only its own storage domain;
|
|
559
|
+
- serves its browser half only from loopback-fenced routes it checks itself;
|
|
560
|
+
- makes no outbound network requests;
|
|
561
|
+
- writes no files outside the harness's own storage;
|
|
562
|
+
- keeps learner state visible, exportable and deletable — never hidden memory.
|
|
563
|
+
**Export my data** writes one self-describing JSON file; **Delete everything**
|
|
564
|
+
removes it, behind a second click, irreversibly. An undo would mean keeping a
|
|
565
|
+
copy of exactly what was asked to be deleted.
|
|
566
|
+
|
|
567
|
+
### Without the teaching brain
|
|
568
|
+
|
|
569
|
+
Install the plugin without the skill and nothing breaks: the plugin loads, the
|
|
570
|
+
panel opens, the map and the lesson surface render, and `GET /export` and
|
|
571
|
+
`POST /reset` work. What does not happen is teaching — nothing writes a lesson,
|
|
572
|
+
because nothing is making teaching decisions.
|
|
573
|
+
|
|
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:
|
|
580
|
+
|
|
581
|
+
| `teachingBrain` | meaning | what the panel does |
|
|
582
|
+
| --- | --- | --- |
|
|
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 |
|
|
586
|
+
|
|
587
|
+
An earlier revision treated "not in my catalog" as "not installed" and showed a
|
|
588
|
+
notice on that basis. In a web profile that was a confident, wrong answer — the
|
|
589
|
+
notice would have appeared with the skill installed and in use. It only fires on
|
|
590
|
+
`false` now.
|
|
591
|
+
|
|
592
|
+
The lesson in the meantime is the honest one: if lessons never appear, check
|
|
593
|
+
that the skill is installed for your agent before suspecting the runtime.
|
|
594
|
+
|
|
595
|
+
### Where your data lives, and what happens when it breaks
|
|
596
|
+
|
|
597
|
+
Everything is stored **locally**, in one JSON document:
|
|
598
|
+
`<dsh-home>/storages/udt.json`. Nothing is sent anywhere, there is no account,
|
|
599
|
+
and the file is plain enough to read.
|
|
600
|
+
|
|
601
|
+
**Export before you edit it.** Hand-editing is not blocked, but every record is
|
|
602
|
+
validated against its schema when the store opens, so one record that no longer
|
|
603
|
+
matches stops the plugin from loading.
|
|
604
|
+
|
|
605
|
+
**What is true today.** A damaged record fails the open, is reported with the
|
|
606
|
+
table and key that failed, and is **not deleted**. The plugin loads inert rather
|
|
607
|
+
than failing the rest of the profile, and removing the offending record restores
|
|
608
|
+
everything else. `GET /export` on a healthy store is the way to make sure you
|
|
609
|
+
still have your data.
|
|
610
|
+
|
|
611
|
+
**What is not true, and is not claimed.** The platform has a record-recovery
|
|
612
|
+
option, `invalidRecords: 'backup-and-skip'`, which moves a bad record aside and
|
|
613
|
+
opens without it. This plugin **does not declare it**, because under the
|
|
614
|
+
`single` layout the platform would ignore it: the option only runs when the
|
|
615
|
+
store can move a *per-record* document aside, and here one document holds
|
|
616
|
+
everything. Declaring it would read like a recovery guarantee while doing
|
|
617
|
+
nothing, which is worse than not having it. So a bad record is **not** backed
|
|
618
|
+
up and skipped, and this README says so rather than implying a recovery ability
|
|
619
|
+
that does not run.
|
|
620
|
+
|
|
621
|
+
#### Storage architecture is frozen for v0.1.0
|
|
622
|
+
|
|
623
|
+
Decided, and not revisited before the first release: `single` layout, no
|
|
624
|
+
per-record, no id migration. The recovery strategy is the one described above —
|
|
625
|
+
precise errors, nothing destroyed, export/reset, hand repair.
|
|
626
|
+
|
|
627
|
+
#### Why not `per-record`
|
|
628
|
+
|
|
629
|
+
Switching the layout would make that option live, so it was tested against the
|
|
630
|
+
platform rather than assumed. Migration is fine: a real store's 1 course,
|
|
631
|
+
6 nodes, 3 lessons, 1 focus and 2 decisions were all seeded into per-record
|
|
632
|
+
documents, with the original file left untouched.
|
|
633
|
+
|
|
634
|
+
Writing is not. In `per-record`, each record key becomes a path segment and must
|
|
635
|
+
match `[a-zA-Z0-9_-]+`:
|
|
636
|
+
|
|
637
|
+
```
|
|
638
|
+
unit 'udt': per-record key '机器学习' is not path-safe (must match /^[a-zA-Z0-9_-]+$/)
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
The `:` alone is enough — `ml:math` is rejected too. Every node, lesson and
|
|
642
|
+
decision id in this plugin is `<courseId>:<slug>`, so the layout would leave the
|
|
643
|
+
runtime able to read and unable to write. The same applies to course ids, which
|
|
644
|
+
come from the learner's own words and are frequently not ASCII.
|
|
645
|
+
|
|
646
|
+
Making the ids path-safe means a referential migration across courses, nodes,
|
|
647
|
+
lessons, focus and decisions, plus changing the ids the tutor passes back to the
|
|
648
|
+
tools. That is worth doing deliberately — not as a side effect of flipping a
|
|
649
|
+
layout flag. Until then, `single` is the honest choice, and its limit is the one
|
|
650
|
+
stated above.
|
|
651
|
+
|
|
652
|
+
## License
|
|
653
|
+
|
|
654
|
+
MIT. The Universal Diagnostic Tutor skill is a separate MIT project by the same
|
|
655
|
+
author and is not vendored here.
|
|
656
|
+
|
|
657
|
+
Planning and architecture research for this project live in
|
|
658
|
+
[`docs/planning/`](docs/planning/).
|