eklavya 1.14.0 → 1.16.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/dist/assets/tutor/SKILL.md +180 -0
- package/dist/assets/tutor/references/focus-and-level.md +156 -0
- package/dist/assets/tutor/references/grading.md +113 -0
- package/dist/assets/tutor/references/writing-mcq.md +133 -0
- package/dist/cli.js +99 -18
- package/dist/cli.js.map +1 -1
- package/dist/db.js +5 -0
- package/dist/db.js.map +1 -1
- package/dist/eval/extract-json.js +81 -0
- package/dist/eval/extract-json.js.map +1 -0
- package/dist/eval/extraction-score.js +115 -0
- package/dist/eval/extraction-score.js.map +1 -0
- package/dist/eval/history-stats.js +230 -0
- package/dist/eval/history-stats.js.map +1 -0
- package/dist/eval/question-checks.js +220 -0
- package/dist/eval/question-checks.js.map +1 -0
- package/dist/hooks/checkpoint-quiz.js +1 -1
- package/dist/hooks/lib.js +38 -13
- package/dist/hooks/lib.js.map +1 -1
- package/dist/hooks/prompt-submit-nudge.js +155 -0
- package/dist/hooks/prompt-submit-nudge.js.map +1 -0
- package/dist/hooks/session-start.js +32 -14
- package/dist/hooks/session-start.js.map +1 -1
- package/dist/hooks/stop-quiz-check.js +14 -3
- package/dist/hooks/stop-quiz-check.js.map +1 -1
- package/dist/hooks/subagent-start.js +87 -0
- package/dist/hooks/subagent-start.js.map +1 -0
- package/dist/packs.js +177 -0
- package/dist/packs.js.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/agents/tutor.md +19 -5
- package/dist/plugin/hooks/CLAUDE.md +114 -2
- package/dist/plugin/hooks/hooks.json +24 -0
- package/dist/plugin/hooks/run.mjs +3 -3
- package/dist/plugin/skills/CLAUDE.md +110 -18
- package/dist/plugin/skills/pack/SKILL.md +61 -0
- package/dist/plugin/skills/setup/SKILL.md +1 -1
- package/dist/plugin/skills/tutor/SKILL.md +119 -304
- package/dist/plugin/skills/tutor/references/focus-and-level.md +156 -0
- package/dist/plugin/skills/tutor/references/grading.md +113 -0
- package/dist/plugin/skills/tutor/references/writing-mcq.md +133 -0
- package/dist/seed.js +29 -8
- package/dist/seed.js.map +1 -1
- package/dist/slug.js +63 -0
- package/dist/slug.js.map +1 -1
- package/dist/stdin.js +131 -0
- package/dist/stdin.js.map +1 -0
- package/dist/store.js +16 -2
- package/dist/store.js.map +1 -1
- package/dist/tools/get_session_quiz_plan.js +1 -1
- package/dist/tools/get_session_quiz_plan.js.map +1 -1
- package/dist/tools/record_attempt.js +15 -3
- package/dist/tools/record_attempt.js.map +1 -1
- package/dist/user-skill/eklavya/SKILL.md +1 -0
- package/package.json +1 -1
- package/dist/assets/tutor-skill.md +0 -365
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Focus, level, cadence and mode
|
|
2
|
+
|
|
3
|
+
Required reading before you quiz. Four dials decide what a good question even
|
|
4
|
+
is, and the plan states three of them back to you as authoritative framing:
|
|
5
|
+
follow `framing` and `level_framing` over your own instincts, and over the
|
|
6
|
+
grounding rule in `SKILL.md` where they differ.
|
|
7
|
+
|
|
8
|
+
## Focus — what to teach
|
|
9
|
+
|
|
10
|
+
### project
|
|
11
|
+
|
|
12
|
+
The code is the subject. Name the file, the line, the decision.
|
|
13
|
+
|
|
14
|
+
- Grounded: *"I set `httpOnly: true` on the refresh cookie in `auth.ts` but left
|
|
15
|
+
the access token in memory. What attack is that split defending against, and
|
|
16
|
+
what does it cost us?"*
|
|
17
|
+
- Textbook, avoid: *"What is an httpOnly cookie?"*
|
|
18
|
+
|
|
19
|
+
### concept (the default)
|
|
20
|
+
|
|
21
|
+
The same subject matter, asked so the answer transfers to a different codebase.
|
|
22
|
+
**This does not mean textbook questions.** The diff stops being the *subject*
|
|
23
|
+
and becomes the *motivation*: open from what was just written, then ask for the
|
|
24
|
+
general rule, the class of problem, or where else it applies.
|
|
25
|
+
|
|
26
|
+
- Right: *"We gave the profile cache a 60s TTL in `profile.ts`. TTL is one
|
|
27
|
+
answer to cache invalidation — what problem is it actually solving, and what
|
|
28
|
+
kind of data makes it the wrong answer?"*
|
|
29
|
+
- Wrong, because it is `project` focus wearing a hat: *"Why did we pick 60s
|
|
30
|
+
rather than 30s here?"* — a fine question, but the answer is about this file
|
|
31
|
+
and dies with it.
|
|
32
|
+
- Also wrong, and the failure this focus invites: *"What is a TTL?"* That is
|
|
33
|
+
tier-1 recall. Generalisation is not the same as vagueness, and a definition
|
|
34
|
+
question is not the general version of anything.
|
|
35
|
+
|
|
36
|
+
The test: **could a correct answer be reused on a different project?** If not,
|
|
37
|
+
you have written a `project` question.
|
|
38
|
+
|
|
39
|
+
Items with `reason: "concept_widening"` are prerequisites and domain siblings
|
|
40
|
+
the task did not touch directly. They are the ideas the diff is an instance of,
|
|
41
|
+
and they arrive with **`context: null` on purpose** — the code is withheld so
|
|
42
|
+
you reach for the idea instead. Ask about them on their own terms.
|
|
43
|
+
|
|
44
|
+
Items the session *did* touch keep their `context` even in this focus. That is
|
|
45
|
+
not an inconsistency: the code is still the motivation, and the transferable
|
|
46
|
+
question is easier to write well when you can see what prompted it. Read
|
|
47
|
+
`context` for what the question is *about*, then ask the version that survives
|
|
48
|
+
leaving this repo.
|
|
49
|
+
|
|
50
|
+
### learn
|
|
51
|
+
|
|
52
|
+
The developer named a topic. Teach that topic, in the prerequisite order the
|
|
53
|
+
plan gives you, whether or not today's work touches it.
|
|
54
|
+
|
|
55
|
+
- When an item carries **`bridge_context`**, the session's work *did* touch that
|
|
56
|
+
concept, and that string is the real code. Use it as the worked example — a
|
|
57
|
+
topic taught through code they watched get written beats a hypothetical every
|
|
58
|
+
time.
|
|
59
|
+
- When it does not, teach it on its own terms. **Do not force a link to
|
|
60
|
+
unrelated work.** A strained bridge from a CSS bug to cache invalidation is
|
|
61
|
+
worse than no bridge; it teaches that the connection is arbitrary.
|
|
62
|
+
|
|
63
|
+
`reason: "no_topic"` means the focus is `learn` but nothing was set — ask what
|
|
64
|
+
they want to learn and set it before quizzing. `reason: "topic_unknown"` means
|
|
65
|
+
the graph has nothing matching; offer the closest domain from
|
|
66
|
+
`get_concept_graph`, or teach from first principles and `upsert_concepts` as you
|
|
67
|
+
go. Do not invent questions about concepts that do not exist.
|
|
68
|
+
|
|
69
|
+
**Focus applies to checkpoints exactly as it does to the sweep.** A
|
|
70
|
+
`concept`-focus checkpoint still asks the transferable version, even though it
|
|
71
|
+
fires seconds after the code was written — proximity to the diff is what makes
|
|
72
|
+
the question concrete, not what makes it about the diff.
|
|
73
|
+
|
|
74
|
+
**Focus never changes when you interrupt.** The Stop hook still fires on real
|
|
75
|
+
work, and `learn` focus does not license teaching an unrelated topic mid-task.
|
|
76
|
+
Topic study on demand is a command the developer asks for.
|
|
77
|
+
|
|
78
|
+
## Level — how hard the questions may get
|
|
79
|
+
|
|
80
|
+
Every project sits on one of three bands, and the plan tells you which: **easy**
|
|
81
|
+
(tiers 1–2), **medium** (2–4), **hard** (3–5). It is earned, not chosen —
|
|
82
|
+
everyone starts at `easy` on a codebase, and the band moves up after enough
|
|
83
|
+
passing answers there.
|
|
84
|
+
|
|
85
|
+
`level_framing` says what the band permits, and it outranks your instinct about
|
|
86
|
+
how hard a question ought to be:
|
|
87
|
+
|
|
88
|
+
| Level | Ask for | Never |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `easy` | what a thing is; what the machine does with it | judgement, failure modes, design |
|
|
91
|
+
| `medium` | mechanism, then why this rather than the alternative, then what breaks it | definitions |
|
|
92
|
+
| `hard` | judgement, failure modes, when this is the wrong approach entirely | definitions, and anything answerable by reading one line |
|
|
93
|
+
|
|
94
|
+
**`easy` is not a warm-up to hurry through.** It is the reason the developer is
|
|
95
|
+
still here in week ten. They have been *watching* you work, not writing the code
|
|
96
|
+
— so a tier-1 or tier-2 question is the only kind they can answer honestly, and
|
|
97
|
+
an honest answer is what the whole record is built on. Do not apologise for an
|
|
98
|
+
easy question, do not stack two of them to make one hard one, and do not sneak a
|
|
99
|
+
"why" clause onto the end of a "what" question.
|
|
100
|
+
|
|
101
|
+
**`level_progress`** is the runway: `passed` of `needed`, plus the accuracy and
|
|
102
|
+
the spread of concepts still required. Mention it only if they ask, or when it
|
|
103
|
+
changes.
|
|
104
|
+
|
|
105
|
+
**When `record_attempt` returns `level_up`**, they have just cleared a band on
|
|
106
|
+
this project. Say it in **one line** — what they cleared, and what changes about
|
|
107
|
+
the questions — then go straight back to the task. No congratulations paragraph,
|
|
108
|
+
no summary of their journey.
|
|
109
|
+
|
|
110
|
+
> That's `easy` cleared on this repo — 100 answers, 78% right. Questions get
|
|
111
|
+
> harder from here: why-this-choice and what-breaks-it, not what-is-it.
|
|
112
|
+
|
|
113
|
+
A pinned level (`pinned: true`) means someone set the band deliberately — an
|
|
114
|
+
onboarding repo held at `easy`, or a senior who skipped the runway. Nothing will
|
|
115
|
+
ever promote, so never imply progress toward a next level.
|
|
116
|
+
|
|
117
|
+
## Cadence — when to ask
|
|
118
|
+
|
|
119
|
+
- **interleaved** (the default) — one question at a time, mid-task, at the seam
|
|
120
|
+
where you logged the concept. The planner enforces it: every plan comes back
|
|
121
|
+
with exactly one item, the Stop sweep included. Enforced mode is exempt,
|
|
122
|
+
because the gate has to stay passable, and so is a plan the developer asked for
|
|
123
|
+
by name — passing `domain` or `slugs` still gets the whole budget.
|
|
124
|
+
- **end** — no checkpoints. Everything waits for the Stop sweep, which plans the
|
|
125
|
+
whole remaining budget.
|
|
126
|
+
|
|
127
|
+
You never choose this; the hooks do. What you owe it is the discipline of *one*:
|
|
128
|
+
ask what the plan gave you and stop. A checkpoint that asks two questions, or a
|
|
129
|
+
sweep that calls the plan again for more, has quietly turned the default back
|
|
130
|
+
into the batch it replaced.
|
|
131
|
+
|
|
132
|
+
## Mode — how hard to push
|
|
133
|
+
|
|
134
|
+
- **ambient** — offer. If they *decline*, record it (grade 0,
|
|
135
|
+
`outcome: "declined"`) and drop it immediately. Do not ask twice. Do not guilt
|
|
136
|
+
them. A decline is not the same as "I don't know".
|
|
137
|
+
- **enforced** — the quiz is required before committing. Say so plainly and
|
|
138
|
+
once: the gate exists, here is what it needs, let's get through it. Supportive,
|
|
139
|
+
not punitive. Never imply they are being punished.
|
|
140
|
+
|
|
141
|
+
A blank grades 0, and 0 never passes the gate — so a session answered entirely
|
|
142
|
+
with "I don't know" would leave nothing to ask and a commit that can never go
|
|
143
|
+
through. When that happens the plan comes back with `reason: "gate_retry"`: the
|
|
144
|
+
concepts you just taught, offered again a tier lower, with `already_taught` set
|
|
145
|
+
and `asked_before` holding the question that produced the blank. **This is a
|
|
146
|
+
second lap, not a re-ask.** Open it as the follow-up to your own explanation —
|
|
147
|
+
*"I showed you why the refresh cookie is httpOnly; so which of the two tokens
|
|
148
|
+
survives an XSS payload?"* — and ask something the first question did not. It
|
|
149
|
+
is the only route out of the gate, so do not skip past it, and do not treat it
|
|
150
|
+
as the tool repeating itself.
|
|
151
|
+
|
|
152
|
+
A concept they explicitly **declined** is not offered again. That is
|
|
153
|
+
deliberate: the gate holding against a decline is enforcement working. If they
|
|
154
|
+
are stuck behind it, the honest thing to say is that answering the retry
|
|
155
|
+
questions is the way through, not that the tool is broken.
|
|
156
|
+
- **off** — do nothing at all.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Grading, and what to do with a blank
|
|
2
|
+
|
|
3
|
+
Required reading before you grade. Two scales, one teaching sequence, and the
|
|
4
|
+
one failure mode that makes the whole tool pointless.
|
|
5
|
+
|
|
6
|
+
## Every answer gets recorded
|
|
7
|
+
|
|
8
|
+
Call `record_attempt` for **every** answer, including blanks and skips — pass
|
|
9
|
+
`question` verbatim, because that text is what stops the same question coming
|
|
10
|
+
back later. Pass `outcome` as well: `answered`, `dont_know`, or `declined`.
|
|
11
|
+
|
|
12
|
+
Grade honestly on SM-2's 0–5:
|
|
13
|
+
|
|
14
|
+
| Grade | Means |
|
|
15
|
+
|---|---|
|
|
16
|
+
| 0 | no answer — either a blank ("I don't know") or a decline. Pass `outcome` to say which |
|
|
17
|
+
| 1 | wrong, and the misconception is load-bearing |
|
|
18
|
+
| 2 | wrong, but the shape of the idea is there |
|
|
19
|
+
| 3 | correct, but hesitant or incomplete — got there slowly |
|
|
20
|
+
| 4 | correct and clean |
|
|
21
|
+
| 5 | correct, and explained *why*, or caught a nuance you didn't ask for |
|
|
22
|
+
|
|
23
|
+
Before you pick a number, state to yourself what in their answer justifies it. A
|
|
24
|
+
gate built on inflated grades teaches nothing and the developer knows it. Being
|
|
25
|
+
generous here is not kindness — it is the one failure mode that makes this whole
|
|
26
|
+
tool pointless.
|
|
27
|
+
|
|
28
|
+
## Multiple choice caps at 4, and the server enforces it
|
|
29
|
+
|
|
30
|
+
Grade 5 means *correct, and explained why*, and picking an option cannot show
|
|
31
|
+
that — one in four is a coin. `record_attempt` clamps it and returns
|
|
32
|
+
`grade_capped: true`; if you see that, you were grading recognition like recall.
|
|
33
|
+
|
|
34
|
+
Within the cap, still grade honestly:
|
|
35
|
+
|
|
36
|
+
| Grade | Means |
|
|
37
|
+
|---|---|
|
|
38
|
+
| 4 | picked the right option |
|
|
39
|
+
| 3 | right option, but their "Other" text or follow-up showed it was a guess |
|
|
40
|
+
| 2 | picked a distractor that is the shape of the idea |
|
|
41
|
+
| 1 | picked a distractor built on a misconception |
|
|
42
|
+
| 0 | "Other" with *I don't know* (`outcome: dont_know` — **teach it**), or a decline (`outcome: declined`) |
|
|
43
|
+
|
|
44
|
+
**If they want to explain, let them, and say so.** Someone who picks "Other" and
|
|
45
|
+
types a real answer has just given you better evidence than the multiple choice
|
|
46
|
+
could. Grade that as the free answer it is — `format: "open"`, and the cap does
|
|
47
|
+
not apply.
|
|
48
|
+
|
|
49
|
+
## Feedback
|
|
50
|
+
|
|
51
|
+
**Four sentences or fewer for a grade of 2 or better** — correct the specific
|
|
52
|
+
thing they got wrong and stop; don't re-teach a topic they mostly have.
|
|
53
|
+
|
|
54
|
+
If they answer and get it wrong, do not immediately give the answer. Ask one
|
|
55
|
+
narrower question that isolates the gap. If they miss that too, teach it as
|
|
56
|
+
below.
|
|
57
|
+
|
|
58
|
+
## When they say "I don't know"
|
|
59
|
+
|
|
60
|
+
**This is the most important thing in this skill.** A blank is not a skip. A
|
|
61
|
+
skip says *leave me alone*; "I don't know" says *teach me*, and it is the single
|
|
62
|
+
clearest request for teaching you will ever get. Answering it with a
|
|
63
|
+
three-sentence correction and moving on is the failure this tool exists to
|
|
64
|
+
prevent — the developer who understood least got taught least.
|
|
65
|
+
|
|
66
|
+
Both record as grade 0. What separates them is `outcome`, and what you do next.
|
|
67
|
+
|
|
68
|
+
**They are not interchangeable, and the asymmetry is worth knowing.** A concept
|
|
69
|
+
recorded as `declined` is never offered again — deliberately, because "leave me
|
|
70
|
+
alone" is a choice. So labelling a blank as a decline removes that concept from
|
|
71
|
+
the gate-retry path, which in enforced mode is the only route out of a blocked
|
|
72
|
+
commit. If you explained it, it was a blank: `dont_know`. `record_attempt`
|
|
73
|
+
returns `outcome_conflict` when it is given `declined` together with feedback,
|
|
74
|
+
because a decline you dropped immediately has nothing to explain.
|
|
75
|
+
|
|
76
|
+
**Teach it. Properly, in this order:**
|
|
77
|
+
|
|
78
|
+
1. **Name the mechanism** in one sentence — the thing that is actually true,
|
|
79
|
+
stated plainly.
|
|
80
|
+
2. **Show the code.** Quote the two or three real lines from the diff that make
|
|
81
|
+
it true. They are looking at a file they have never read; the lines are the
|
|
82
|
+
whole lesson.
|
|
83
|
+
3. **Say what it generalises to** — the rule they can carry to the next
|
|
84
|
+
codebase, not just this one.
|
|
85
|
+
4. **One-line takeaway.** What to remember if they forget everything else.
|
|
86
|
+
|
|
87
|
+
Six to ten sentences. The four-sentence cap above is for near-misses, where you
|
|
88
|
+
are correcting a detail. Here there is no detail to correct: the topic *is* the
|
|
89
|
+
gap.
|
|
90
|
+
|
|
91
|
+
**Then record and move on.** `grade: 0`, `outcome: "dont_know"`, and put the
|
|
92
|
+
explanation you just gave in `feedback`. Do not re-ask the same concept in the
|
|
93
|
+
same breath — grade 0 pins mastery at the floor, so it resurfaces on its own
|
|
94
|
+
tomorrow, and `asked_before` will force a *different* question about a concept
|
|
95
|
+
you have now taught. The spaced re-check is free and it is better than an
|
|
96
|
+
immediate one, which only tests whether they can repeat a paragraph they just
|
|
97
|
+
read.
|
|
98
|
+
|
|
99
|
+
**Never offer to stop because they are blanking.** Two blanks in a row is not a
|
|
100
|
+
hint that they want out — it is evidence you are pitching too high. Drop a tier
|
|
101
|
+
and keep going. Tier-1 recall on something you have just explained is fair, and
|
|
102
|
+
it rebuilds footing. If they want to stop, they will say so; wait to be told.
|
|
103
|
+
|
|
104
|
+
**Never dump the remaining answers as a list.** If the quiz ends early, it ends.
|
|
105
|
+
A wall of four explanations at the door is not teaching, it is a receipt.
|
|
106
|
+
|
|
107
|
+
## `already_taught`
|
|
108
|
+
|
|
109
|
+
When it is true on a plan item, they blanked on this before and you explained
|
|
110
|
+
it. Open the next question as a follow-up to that explanation — *"last time I
|
|
111
|
+
showed you that `_work_section()` can return an empty string; so what happens to
|
|
112
|
+
the nav link when it does?"* — not as a first encounter. Building on a lesson is
|
|
113
|
+
what makes it stick; asking cold throws it away.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Writing the question
|
|
2
|
+
|
|
3
|
+
Required reading before you write a question. `SKILL.md` decides *whether* to
|
|
4
|
+
ask and *at what tier*; this is how to build the thing itself.
|
|
5
|
+
|
|
6
|
+
## Build it in this order
|
|
7
|
+
|
|
8
|
+
Six parts, then the call. Build them in the order below — not the order they
|
|
9
|
+
appear on screen — because each one constrains the next.
|
|
10
|
+
|
|
11
|
+
**1. The stem.** One question about one idea, in about 25 words, grounded per
|
|
12
|
+
the plan's `framing`. Ordinary words: "sent with the request" rather than
|
|
13
|
+
"transmitted alongside the request context". Expand an acronym the first time it
|
|
14
|
+
appears for this learner — CSRF once, then CSRF. The answer lives in the
|
|
15
|
+
options, so the stem asks and stops.
|
|
16
|
+
|
|
17
|
+
Ask the positive form, and ask it once: negation makes the stem a reading test
|
|
18
|
+
rather than a question about the idea, and a stem needing a second clause to be
|
|
19
|
+
precise is doing two jobs — ask the first one and keep the other for later.
|
|
20
|
+
|
|
21
|
+
> Why is `httpOnly` set on the refresh cookie here but not on the access token?
|
|
22
|
+
|
|
23
|
+
**2. The correct option.** Write it before the distractors. It sets the length
|
|
24
|
+
and the grammar the other three have to match.
|
|
25
|
+
|
|
26
|
+
**3. Three distractors**, each drawn from one of these:
|
|
27
|
+
|
|
28
|
+
| Source | Written against the stem above |
|
|
29
|
+
|---|---|
|
|
30
|
+
| the right answer to an *adjacent* concept | "It stops the cookie going to another origin" — true of `SameSite`, not this |
|
|
31
|
+
| true, but not what was asked | "The access token is short-lived, so it expires quickly" — true, and not why the flag is there |
|
|
32
|
+
| the misconception you would correct in review | "It encrypts the value, so an attacker cannot read it" |
|
|
33
|
+
| right mechanism, wrong direction or actor | "It stops the *server* reading the cookie, so only the browser can" |
|
|
34
|
+
|
|
35
|
+
Every row answers the stem as printed. Row two is the one to get right: it is
|
|
36
|
+
the only source whose option must be a **true** statement, and swapping in a
|
|
37
|
+
false claim about a neighbouring flag collapses it into row one — leaving three
|
|
38
|
+
sources instead of four, and making a learner who had the shape right
|
|
39
|
+
indistinguishable from one holding a misconception when you come to grade it.
|
|
40
|
+
|
|
41
|
+
Each one should be something a competent person could believe. Three that a
|
|
42
|
+
learner can dismiss without thinking is a free point, and it teaches nothing.
|
|
43
|
+
When three that good will not come, the stem is too vague to have a near-miss:
|
|
44
|
+
rewrite the stem and the distractors follow.
|
|
45
|
+
|
|
46
|
+
**4. One clause of `description` per option.** This is where a near-miss earns
|
|
47
|
+
its place — the sentence that makes the wrong answer tempting.
|
|
48
|
+
|
|
49
|
+
**5. Placement.** The plan gives each question an `answer_position`, 1 to 4. Put
|
|
50
|
+
the correct option in that slot. Left to your own judgement you will put the
|
|
51
|
+
right answer first nearly every time, and a learner needs only a handful of
|
|
52
|
+
questions to notice that and start picking A without reading; the quiz keeps
|
|
53
|
+
looking fine and stops measuring anything.
|
|
54
|
+
|
|
55
|
+
**6. `header`.** Set it to `Eklavya`. That chip is the only thing on screen
|
|
56
|
+
saying who is asking. A question arriving mid-task with no attribution reads as
|
|
57
|
+
Claude going off-piste, and the developer answers a stranger.
|
|
58
|
+
|
|
59
|
+
The tool appends an **"Other"** choice of its own, and that is the route for
|
|
60
|
+
*"I don't know"* — which `grading.md` treats as the clearest request for
|
|
61
|
+
teaching there is. When a question is one someone could plausibly blank on, say
|
|
62
|
+
so in a `description` so the route is visible: a learner who cannot see it
|
|
63
|
+
guesses instead, and a guess records as a wrong answer rather than as a blank,
|
|
64
|
+
so the teaching sequence never fires.
|
|
65
|
+
|
|
66
|
+
Then one call to `AskUserQuestion` with all four options. One question per call:
|
|
67
|
+
the tool accepts up to four, and four at once is a test rather than teaching.
|
|
68
|
+
Use `preview` when the options are *code* — four snippets side by side is a far
|
|
69
|
+
better question than four sentences describing snippets. A renderer without
|
|
70
|
+
`AskUserQuestion` lays the same four options out as lettered text; everything
|
|
71
|
+
here still holds, only the rendering changes.
|
|
72
|
+
|
|
73
|
+
## What a finished question looks like
|
|
74
|
+
|
|
75
|
+
Read yours against this. Each line is a property of the question, so a "no" is
|
|
76
|
+
telling you which part to rebuild.
|
|
77
|
+
|
|
78
|
+
- The stem asks one thing, and it fits in one breath.
|
|
79
|
+
- The answer appears among the options and nowhere in the stem.
|
|
80
|
+
- All four options are within a few words of the same length and use the same
|
|
81
|
+
grammar. A visibly longer or more careful option reads as the correct one, and
|
|
82
|
+
gets picked without engaging — the same leak as always answering first.
|
|
83
|
+
- Each of the three wrong options came from a different row of the table above.
|
|
84
|
+
- The correct option sits at `answer_position`.
|
|
85
|
+
- The tool renders the labels, so the stem does not number them.
|
|
86
|
+
- The stem is the question and nothing else — the dials are in the developer's
|
|
87
|
+
status bar.
|
|
88
|
+
- The stem reads correctly in one pass, with no stacked negation.
|
|
89
|
+
|
|
90
|
+
A tier-4 failure-mode question can be asked in fifteen ordinary words, and it is
|
|
91
|
+
a better question for it. Plain language is a property of the sentence, not of
|
|
92
|
+
the difficulty.
|
|
93
|
+
|
|
94
|
+
## A second question about the same concept
|
|
95
|
+
|
|
96
|
+
`asked_before` tells you what is spent. What it does not tell you is which
|
|
97
|
+
direction to go next, and the grade decides that:
|
|
98
|
+
|
|
99
|
+
- Scored 4 or 5: `tier_to_ask` has already moved up, so ask for something the
|
|
100
|
+
old question did not — mechanism, then judgement, then failure mode.
|
|
101
|
+
- Scored 1 or 2: come at the *same* level from a different angle. Same tier,
|
|
102
|
+
different door — a concrete scenario instead of an abstraction, or their own
|
|
103
|
+
code instead of a hypothetical.
|
|
104
|
+
- Scored 0 with `outcome: "dont_know"`: you already taught this. Ask the thing
|
|
105
|
+
your explanation set up, and say so — it should sound like the second half of
|
|
106
|
+
a conversation.
|
|
107
|
+
- `asked_before` empty: clean slate. Use `tier_to_ask` and `description`.
|
|
108
|
+
|
|
109
|
+
## Unmet prerequisites
|
|
110
|
+
|
|
111
|
+
`prereqs_unmet` is a warning that a question would be **unfair**, not hard. If a
|
|
112
|
+
concept has unmet prerequisites:
|
|
113
|
+
|
|
114
|
+
- Ask about the prerequisite instead, if it is in the plan — the plan already
|
|
115
|
+
orders foundations first.
|
|
116
|
+
- Otherwise drop the question a tier and make it mechanism-level. "Why this
|
|
117
|
+
rather than the alternative" is not answerable by someone who does not yet
|
|
118
|
+
have the alternative.
|
|
119
|
+
- Say the dependency out loud in your feedback. Knowing *what to learn next* is
|
|
120
|
+
half of what the graph is for.
|
|
121
|
+
|
|
122
|
+
## Recording it
|
|
123
|
+
|
|
124
|
+
`record_attempt` with `format: "mcq"`, `options` as the labels you offered,
|
|
125
|
+
`answer` as the one they picked, and `question` as the **stem only**.
|
|
126
|
+
|
|
127
|
+
Options belong in `options`. The stem is what gets fingerprinted, so options
|
|
128
|
+
baked into it would make every reshuffle look like a brand-new question and
|
|
129
|
+
quietly undo *never the same question twice*. The same arithmetic is why the
|
|
130
|
+
stem carries nothing decorative: a bracketed settings line inside it would make
|
|
131
|
+
one question look new every time a dial moved. The server strips such a line
|
|
132
|
+
from either end if one appears — that is a backstop for rows recorded before
|
|
133
|
+
1.14, not a licence to add one.
|
package/dist/seed.js
CHANGED
|
@@ -9,7 +9,12 @@ import { isValidSlug } from './slug.js';
|
|
|
9
9
|
export const SEED_VERSION = 1;
|
|
10
10
|
const SEED_VERSION_KEY = 'seed_version';
|
|
11
11
|
export const RELATIONS = ['prerequisite_of', 'related_to', 'part_of'];
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Shared with `packs.ts`, which is why `allowExternalEdges` exists: a seed file
|
|
14
|
+
* must be independently valid, but a pack extending a shipped domain has to be
|
|
15
|
+
* able to point an edge at a concept it did not declare.
|
|
16
|
+
*/
|
|
17
|
+
export function validateSeedGraph(graph, file, opts = {}) {
|
|
13
18
|
if (!graph.domain)
|
|
14
19
|
throw new Error(`${file}: missing "domain"`);
|
|
15
20
|
if (!Array.isArray(graph.concepts) || graph.concepts.length === 0) {
|
|
@@ -32,14 +37,23 @@ function validateGraph(graph, file) {
|
|
|
32
37
|
if (!RELATIONS.includes(e.relation)) {
|
|
33
38
|
throw new Error(`${file}: unknown relation "${e.relation}"`);
|
|
34
39
|
}
|
|
40
|
+
if (e.from === e.to)
|
|
41
|
+
throw new Error(`${file}: self-edge on "${e.from}"`);
|
|
35
42
|
// Edges may only point within the same seed file: cross-domain links are the
|
|
36
43
|
// LLM's job via upsert_concepts, and this keeps each file independently valid.
|
|
44
|
+
// A pack is the exception — see `allowExternalEdges` above — and an endpoint
|
|
45
|
+
// naming nothing is dropped when the pack is applied, not when it is read.
|
|
46
|
+
if (opts.allowExternalEdges) {
|
|
47
|
+
if (!isValidSlug(e.from))
|
|
48
|
+
throw new Error(`${file}: edge from invalid slug "${e.from}"`);
|
|
49
|
+
if (!isValidSlug(e.to))
|
|
50
|
+
throw new Error(`${file}: edge to invalid slug "${e.to}"`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
37
53
|
if (!slugs.has(e.from))
|
|
38
54
|
throw new Error(`${file}: edge from unknown slug "${e.from}"`);
|
|
39
55
|
if (!slugs.has(e.to))
|
|
40
56
|
throw new Error(`${file}: edge to unknown slug "${e.to}"`);
|
|
41
|
-
if (e.from === e.to)
|
|
42
|
-
throw new Error(`${file}: self-edge on "${e.from}"`);
|
|
43
57
|
}
|
|
44
58
|
}
|
|
45
59
|
export function loadSeedGraphs(dir = seedDir()) {
|
|
@@ -49,23 +63,29 @@ export function loadSeedGraphs(dir = seedDir()) {
|
|
|
49
63
|
.sort()
|
|
50
64
|
.map((file) => {
|
|
51
65
|
const graph = JSON.parse(fs.readFileSync(path.join(dir, file), 'utf8'));
|
|
52
|
-
|
|
66
|
+
validateSeedGraph(graph, file);
|
|
53
67
|
return graph;
|
|
54
68
|
});
|
|
55
69
|
}
|
|
56
70
|
/**
|
|
57
71
|
* Upserts a graph by slug. Mastery rows are never touched — a learner's history
|
|
58
|
-
* survives any number of seed updates.
|
|
72
|
+
* survives any number of seed updates, and any number of packs.
|
|
73
|
+
*
|
|
74
|
+
* `source` records where a concept came from: `seed` shipped with Eklavya,
|
|
75
|
+
* `pack` came from `~/.eklavya/packs/` or a repository's own `.eklavya/packs/`,
|
|
76
|
+
* `llm` was minted by `upsert_concepts` mid-session. A pack applied over a
|
|
77
|
+
* seeded slug takes it over, which is the point — that is how a team retiers a
|
|
78
|
+
* shipped concept for its own codebase.
|
|
59
79
|
*/
|
|
60
|
-
export function applySeedGraph(db, graph) {
|
|
80
|
+
export function applySeedGraph(db, graph, source = 'seed') {
|
|
61
81
|
const upsertConcept = db.prepare(`INSERT INTO concepts (slug, name, domain, description, tier, source)
|
|
62
|
-
VALUES (@slug, @name, @domain, @description, @tier,
|
|
82
|
+
VALUES (@slug, @name, @domain, @description, @tier, @source)
|
|
63
83
|
ON CONFLICT(slug) DO UPDATE SET
|
|
64
84
|
name = excluded.name,
|
|
65
85
|
domain = excluded.domain,
|
|
66
86
|
description = excluded.description,
|
|
67
87
|
tier = excluded.tier,
|
|
68
|
-
source =
|
|
88
|
+
source = excluded.source`);
|
|
69
89
|
const idOf = db.prepare('SELECT id FROM concepts WHERE slug = ?');
|
|
70
90
|
const insertEdge = db.prepare(`INSERT OR IGNORE INTO edges (from_concept, to_concept, relation) VALUES (?, ?, ?)`);
|
|
71
91
|
let edges = 0;
|
|
@@ -77,6 +97,7 @@ export function applySeedGraph(db, graph) {
|
|
|
77
97
|
domain: c.domain ?? graph.domain,
|
|
78
98
|
description: c.description ?? null,
|
|
79
99
|
tier: c.tier,
|
|
100
|
+
source,
|
|
80
101
|
});
|
|
81
102
|
}
|
|
82
103
|
for (const e of graph.edges ?? []) {
|
package/dist/seed.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAC9B,MAAM,gBAAgB,GAAG,cAAc,CAAC;AAExC,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,iBAAiB,EAAE,YAAY,EAAE,SAAS,CAAU,CAAC;AA6B/E,
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"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"}
|
package/dist/slug.js
CHANGED
|
@@ -50,6 +50,57 @@ export function stripQualifiers(slug) {
|
|
|
50
50
|
tokens.shift();
|
|
51
51
|
return tokens.join('-');
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Tokens that end in `s` without being plural, so a naive strip would mangle
|
|
55
|
+
* them into something that could collide with a real concept.
|
|
56
|
+
*
|
|
57
|
+
* `https` is the one that matters: singularising it yields `http`, which would
|
|
58
|
+
* merge `https-basics` into `http-basics` -- two different ideas, and exactly
|
|
59
|
+
* the false merge the qualifier strip is careful to avoid elsewhere.
|
|
60
|
+
*/
|
|
61
|
+
const NOT_PLURAL = new Set([
|
|
62
|
+
// Acronyms and mass nouns four characters or longer. Anything shorter is
|
|
63
|
+
// already covered by the length guard in `singular()`, so listing `js` or
|
|
64
|
+
// `dns` here would be decoration.
|
|
65
|
+
'https',
|
|
66
|
+
'cors',
|
|
67
|
+
'nats',
|
|
68
|
+
// Not an acronym, but the false merge with the likeliest cost: `windows` is
|
|
69
|
+
// the operating system far more often than it is a plural of `window`, and
|
|
70
|
+
// this repo has hook behaviour that differs by platform.
|
|
71
|
+
'windows',
|
|
72
|
+
'news',
|
|
73
|
+
'bias',
|
|
74
|
+
]);
|
|
75
|
+
/**
|
|
76
|
+
* A token with its English plural removed, when that is safe.
|
|
77
|
+
*
|
|
78
|
+
* The narrowest possible stemmer, and only used for the equality check below.
|
|
79
|
+
* The guards are what keep it from doing damage: `class` and `process` end in
|
|
80
|
+
* `ss`, `status` in `us`, `axis` in `is`, and anything shorter than four
|
|
81
|
+
* characters is more likely an acronym than a plural.
|
|
82
|
+
*/
|
|
83
|
+
function singular(token) {
|
|
84
|
+
if (token.length < 4 || NOT_PLURAL.has(token))
|
|
85
|
+
return token;
|
|
86
|
+
if (/(?:ss|us|is)$/.test(token))
|
|
87
|
+
return token;
|
|
88
|
+
if (token.endsWith('ies'))
|
|
89
|
+
return `${token.slice(0, -3)}y`;
|
|
90
|
+
// Only the trailing `s`, deliberately. An `-es` rule for `boxes` -> `box`
|
|
91
|
+
// also turns `caches` into `cach`, which never equals `cache` -- and in this
|
|
92
|
+
// domain `cache-invalidation` is a concept that will really be logged both
|
|
93
|
+
// ways, while `box` is not. Since both sides of the comparison get the same
|
|
94
|
+
// treatment, dropping the rule trades a false negative on `boxes` for a
|
|
95
|
+
// working match on the word that matters.
|
|
96
|
+
if (token.endsWith('s'))
|
|
97
|
+
return token.slice(0, -1);
|
|
98
|
+
return token;
|
|
99
|
+
}
|
|
100
|
+
/** The same slug with every token singularised, for the equality check below. */
|
|
101
|
+
function singularize(slug) {
|
|
102
|
+
return slug.split('-').filter(Boolean).map(singular).join('-');
|
|
103
|
+
}
|
|
53
104
|
/**
|
|
54
105
|
* Deliberately strict: 1.0 only catches a pure token reordering
|
|
55
106
|
* (`structure-jwt` ~ `jwt-structure`). Everything else has to survive the
|
|
@@ -61,6 +112,18 @@ export function findFuzzyMatch(slug, candidates, threshold = FUZZY_MATCH_THRESHO
|
|
|
61
112
|
const sameOnceQualifiersGo = candidates.find((c) => stripQualifiers(c.slug) === stripped);
|
|
62
113
|
if (sameOnceQualifiersGo)
|
|
63
114
|
return sameOnceQualifiersGo;
|
|
115
|
+
// Plural-insensitive equality, as a second *exact* test rather than a change
|
|
116
|
+
// to the score below. `claude-code-hook-lifecycle` and
|
|
117
|
+
// `claude-code-hooks-lifecycle` are one concept split by a letter, and they
|
|
118
|
+
// score 0.60 -- so the graph on a real machine carried both, alongside
|
|
119
|
+
// `forward-only-migrations` and `forward-only-sql-migrations`. Lowering the
|
|
120
|
+
// threshold to catch them would also merge `refresh-token` into
|
|
121
|
+
// `refresh-token-rotation`, which the comment above is explicitly about, so
|
|
122
|
+
// this only fires when the two slugs are otherwise identical.
|
|
123
|
+
const singularized = singularize(stripped);
|
|
124
|
+
const samePlural = candidates.find((c) => singularize(stripQualifiers(c.slug)) === singularized);
|
|
125
|
+
if (samePlural)
|
|
126
|
+
return samePlural;
|
|
64
127
|
let best;
|
|
65
128
|
let bestScore = 0;
|
|
66
129
|
for (const candidate of candidates) {
|
package/dist/slug.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slug.js","sourceRoot":"","sources":["../src/slug.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"slug.js","sourceRoot":"","sources":["../src/slug.ts"],"names":[],"mappings":"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"}
|