Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
All notable changes to `learn`. Versions follow semantic versioning; each minor release was built
behind the `feat/learning-loop` branch and reviewed before merge.

## Unreleased

- `tutor/reverify.mjs`: tutor-receipt re-verification with typed failure codes. `reverifyReceipt`
recomputes a receipt's own evidence instead of trusting its stored booleans (`verified` /
`ledgerVerified` are author-controlled and deliberately ignored): the hash chain over the
witnessed practice entries must recompute (a break is typed `CHAIN_BROKEN` with the offending
entry's seq and hash; a hash-consistent truncation is caught by attempt accounting) and the
stored mastery verdict must re-derive from the recorded attempts under the recorded policy (a
divergence is typed `VERDICT_MISMATCH` with both projections). A chainless receipt re-verifies
as `UNVERIFIED`, never as verified. A clean re-check carries a witnessed summary digest.
- CLI: `learn tutor reverify <id> [--file <receipt.json>]` exits 0 only when every checked receipt
re-verifies as VERIFIED; any typed failure or UNVERIFIED receipt exits 1.
- MCP: `learn_tutor_reverify` (advisory, read-only).
- `doctor` gains `tutor.reverify_rejects_known_bad`: the re-verifier must pass a clean receipt and
reject each known-bad fixture (tampered chain entry, hand-edited verdict, chainless receipt).
A verifier that cannot fail on a known-bad input is not a verifier.

## 1.5.0

The learning loop reaches its first complete shape: every planned teach-you capability is shipped
Expand Down
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
## Try it

```bash
node --test # 167 tests
node --test # 206 tests
node src/cli.mjs status
node src/cli.mjs doctor
```
Expand Down Expand Up @@ -68,6 +68,12 @@ capability matches where you are stuck:
- **`learn tutor study`** (`study.mjs`): the orchestrator. Composes due + misconceptions +
interleaved order + prerequisite readiness + the mastery-gate into one plan, and a witnessed,
hash-chained `study-receipt` you can keep.
- **Receipt re-verification** (`reverify.mjs`): `learn tutor reverify` recomputes an emitted
receipt's own evidence instead of trusting its stored booleans. The hash chain over the
witnessed practice entries must recompute (a break is typed `CHAIN_BROKEN` with the offending
entry) and the stored mastery verdict must re-derive from the recorded attempts under the
recorded policy (a divergence is typed `VERDICT_MISMATCH`). A chainless receipt is `UNVERIFIED`,
never verified; a clean re-check exits 0 with a witnessed summary digest.

Underneath the learning loop, the original credential-logistics engine still runs workflows
(`learn run` / `learn resume`), halts at every graded `assess` step for you to complete yourself,
Expand All @@ -89,13 +95,14 @@ and emits a provenance receipt separating automated logistics from your own grad
- **Release:** `1.5.0`; command `learn`; Node >= 20; zero external dependencies (ES modules,
`node:test`).
- **Operator surface:** `learn status`, `learn doctor`, `learn run/resume/verify/receipt`,
`learn assist`, `learn visualize`, and `learn tutor <plan|record|mastery|receipt|due|
`learn assist`, `learn visualize`, and `learn tutor <plan|record|mastery|receipt|reverify|due|
misconceptions|retrieval|explain|predict|score|path|study|study-receipt>`. The zero-dep MCP
server (`src/mcp.mjs`) exposes the advisory/read tools: `learn_doctor`, `learn_status`,
`learn_verify`, `learn_receipt`, `learn_dry_run`, `learn_tutor_plan`, `learn_tutor_record`,
`learn_tutor_mastery`, `learn_tutor_due`, `learn_tutor_studyplan`, `learn_tutor_misconceptions`,
and `learn_visualize_dry_run`. Actuation (real runs) stays operator-driven on the CLI.
- **Test floor:** 167 tests across the runtime, adapters, receipt, tutor/learning-loop, and telos
`learn_tutor_reverify`, and `learn_visualize_dry_run`. Actuation (real runs) stays
operator-driven on the CLI.
- **Test floor:** 206 tests across the runtime, adapters, receipt, tutor/learning-loop, and telos
interop, including a falsifiable test per integrity invariant.
- **Housekeeping:** see [CHANGELOG.md](CHANGELOG.md) for version history.

Expand Down Expand Up @@ -133,7 +140,8 @@ a render, a visualization, or a pending prediction as if it were a graded answer
`schedule.mjs` (spaced repetition), `misconception.mjs` (aggregation), `retrieval.mjs`
(cloze generation + interleaving), `explain.mjs` (self-explanation grading), `predict.mjs`
(predict-then-observe), `map.mjs` (concept map + prerequisite gating), `study.mjs` (the
orchestrator + witnessed study-receipt), `tutorstore.mjs` (session persistence).
orchestrator + witnessed study-receipt), `reverify.mjs` (receipt re-verification: recomputed
chain + re-derived verdict, typed failures), `tutorstore.mjs` (session persistence).
- `interop/telos.mjs`: the visualization bridge. `concept -> math_physics scene-spec -> witnessed
AID render`. Delegates rendering to the telos engine over `LEARN_TELOS_CMD` (fail-closed); learn
never imports telos internals or picks the renderer profile.
Expand All @@ -157,6 +165,10 @@ a render, a visualization, or a pending prediction as if it were a graded answer
study) generates practice, structures study, or checks the operator's own work, and the
`mastery()` verdict is a function of the operator's own scored practice attempts only, never
of a render, a visualization, or an unscored pending prediction.
8. A tutor receipt re-verifies from its own recorded evidence. `learn tutor reverify` recomputes
the hash chain and re-derives the mastery verdict; failures are typed (`CHAIN_BROKEN`,
`VERDICT_MISMATCH`), author-controlled booleans never gate, and a chainless receipt is
`UNVERIFIED`, never verified.

## Interop

Expand Down
10 changes: 9 additions & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,20 @@ node src/cli.mjs tutor misconceptions mysession
node src/cli.mjs tutor mastery mysession
node src/cli.mjs tutor study mysession --now 2026-06-30T00:00:00Z
node src/cli.mjs tutor study-receipt mysession --now 2026-06-30T00:00:00Z
node src/cli.mjs tutor receipt mysession
node src/cli.mjs tutor reverify mysession
```

`learn tutor study` is the one command to run first on an existing session: it composes what is
due, what you keep getting wrong, a mixed practice order, and the mastery-gate verdict, all from
your own recorded attempts.

`learn tutor reverify` recomputes an emitted receipt's own evidence (hash chain + verdict
re-derivation) instead of trusting its stored booleans. It exits 0 with a witnessed summary only
when every checked receipt re-verifies clean; a tampered chain is typed `CHAIN_BROKEN`, an edited
verdict is typed `VERDICT_MISMATCH`, and a chainless receipt is `UNVERIFIED`, never verified
(all exit 1). Use `--file <receipt.json>` to check a single receipt file directly.

## Basic usage: the credential/coursework engine

```bash
Expand All @@ -61,7 +69,7 @@ node src/mcp.mjs

Exposes the advisory/read tools (`learn_doctor`, `learn_status`, `learn_verify`, `learn_receipt`,
`learn_dry_run`, `learn_tutor_plan`, `learn_tutor_record`, `learn_tutor_mastery`,
`learn_tutor_due`, `learn_tutor_studyplan`, `learn_tutor_misconceptions`,
`learn_tutor_due`, `learn_tutor_studyplan`, `learn_tutor_misconceptions`, `learn_tutor_reverify`,
`learn_visualize_dry_run`) over stdio JSON-RPC. Actuation (real workflow runs) stays
operator-driven on the CLI; the MCP surface never performs a real course action or answers a
graded step.
Expand Down
3 changes: 2 additions & 1 deletion docs/ENTERPRISE-READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ what exact material it relied on, what it changed, what verified, and what remai
- `node src/cli.mjs verify ID` and `node src/cli.mjs receipt ID` for ledger replay and receipt
emission (JSON, Markdown, HTML).
- `node src/cli.mjs tutor plan|record|due|misconceptions|retrieval|explain|predict|score|path|
study|study-receipt|mastery` for the learning loop.
study|study-receipt|mastery` for the learning loop, and `tutor reverify` to recompute an emitted
receipt's evidence (typed `CHAIN_BROKEN` / `VERDICT_MISMATCH` failures; exit 0 only when clean).
- `node src/mcp.mjs` for a stdio MCP host exposing the advisory/read surface only.

## Context Envelope Contribution
Expand Down
14 changes: 14 additions & 0 deletions docs/HOW-IT-WORKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ record. The receipt is a quiet floor: it proves the study happened as recorded.
grade or unlock anything beyond what `studyPlan` already computed, and it never contains an answer
to a certified assessment, only the operator's own recorded practice.

### 9. Receipt re-verification

```bash
node src/cli.mjs tutor receipt mysession
node src/cli.mjs tutor reverify mysession
```

`tutor/reverify.mjs` recomputes an emitted receipt's own evidence instead of trusting its stored
booleans. The hash chain over the witnessed practice entries must recompute (a break is typed
`CHAIN_BROKEN` with the offending entry's seq and hash) and the stored mastery verdict must
re-derive from the recorded attempts under the recorded policy (a divergence is typed
`VERDICT_MISMATCH`). A receipt without chain evidence is `UNVERIFIED`, never verified. A clean
re-check exits 0 with a witnessed summary digest; any failure exits 1.

---

## Fail-closed, the same way the rest of Project Telos is
Expand Down
8 changes: 7 additions & 1 deletion src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ export async function main(argv, { dir = process.cwd() } = {}) {
` order: ${plan.order.join(", ")}\n` +
` readiness: ${plan.readiness.map((r) => `${r.objective}:${r.unlocked ? "unlocked" : "locked"}`).join(", ")}` };
}
if (sub === "reverify") {
const { reverifyFiles, formatReverify } = await import("./tutor/reverify.mjs");
const file = arg(argv, "--file");
const r = reverifyFiles(dir, id, { file });
return { code: r.ok ? 0 : 1, out: formatReverify(r, file || id) };
}
if (sub === "study-receipt") {
const s = loadSession(dir, id); if (!s) return { code: 1, out: `no tutor session: ${id}` };
const { studyReceipt } = await import("./tutor/study.mjs");
Expand All @@ -159,7 +165,7 @@ export async function main(argv, { dir = process.cwd() } = {}) {
writeFileSync(join(dir, "tutor", id + ".study-receipt.json"), JSON.stringify(r, null, 2));
return { code: 0, out: `tutor study-receipt ${id}: verified ${r.verified}, mastery ${r.mastery.ready ? "READY" : "not yet"} -> tutor/${id}.study-receipt.json` };
}
return { code: 1, out: "usage: learn tutor <plan|record|mastery|receipt|due|misconceptions|retrieval|explain|predict|score|path|study|study-receipt> <id> ..." };
return { code: 1, out: "usage: learn tutor <plan|record|mastery|receipt|reverify|due|misconceptions|retrieval|explain|predict|score|path|study|study-receipt> <id> ..." };
}
if (cmd === "assist") {
const { assistArtifacts } = await import("./assist/assist.mjs");
Expand Down
6 changes: 6 additions & 0 deletions src/doctor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { telosRender, toAidLedgerEntry } from "./interop/telos.mjs";
import { buildReceipt } from "./receipt/receipt.mjs";
import { newSession, recordAttempt, mastery, recordVisualization } from "./tutor/tutor.mjs";
import { recordPrediction } from "./tutor/predict.mjs";
import { reverifySelfCheck } from "./tutor/reverify.mjs";

export async function doctor() {
const checks = [];
Expand Down Expand Up @@ -69,6 +70,11 @@ export async function doctor() {
masteryAfterPrediction.perObjective[0].attempts === masteryBefore.perObjective[0].attempts + 1 &&
masteryAfterPrediction.perObjective[0].correct === masteryBefore.perObjective[0].correct);

// 8. the tutor-receipt re-verifier can FAIL: it must pass a clean receipt and reject each
// known-bad fixture (tampered chain entry, hand-edited verdict, chainless receipt). A verifier
// that cannot fail on a known-bad input is not a verifier.
add("tutor.reverify_rejects_known_bad", reverifySelfCheck());

const ok = checks.every((c) => c.status === "MATCH");
return { tool: "learn", version, status: ok ? "MATCH" : "DEGRADED", checks };
}
5 changes: 5 additions & 0 deletions src/mcp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { saveSession, loadSession } from "./tutor/tutorstore.mjs";
import { due } from "./tutor/schedule.mjs";
import { misconceptions } from "./tutor/misconception.mjs";
import { studyPlan } from "./tutor/study.mjs";
import { reverifyFiles } from "./tutor/reverify.mjs";
import "./adapters/fake.mjs";
import "./adapters/generic.mjs";
import "./adapters/lms.mjs";
Expand All @@ -31,6 +32,7 @@ export const TOOLS = [
{ name: "learn_tutor_due", description: "Advisory, read-only: list objectives due for spaced-repetition review in a saved tutor session, most-overdue first.", inputSchema: { type: "object", properties: { sessionId: { type: "string" }, now: { type: ["string", "number"] }, asOf: { type: ["string", "number"] } }, required: ["sessionId", "now"] } },
{ name: "learn_tutor_studyplan", description: "Advisory, read-only: return the composed study plan for a saved tutor session (due list, ranked misconceptions, interleaved order, prerequisite readiness, mastery-gate verdict).", inputSchema: { type: "object", properties: { sessionId: { type: "string" }, now: { type: ["string", "number"] }, seed: { type: ["string", "number"] } }, required: ["sessionId", "now"] } },
{ name: "learn_tutor_misconceptions", description: "Advisory, read-only: return the ranked misconception aggregation (wrong attempts + the operator's own feedback) for a saved tutor session.", inputSchema: { type: "object", properties: { sessionId: { type: "string" } }, required: ["sessionId"] } },
{ name: "learn_tutor_reverify", description: "Advisory, read-only: re-verify a session's emitted tutor receipts from their own recorded evidence (recomputed hash chain + re-derived mastery verdict). Failures are typed CHAIN_BROKEN / VERDICT_MISMATCH; a chainless receipt is UNVERIFIED, never verified.", inputSchema: { type: "object", properties: { sessionId: { type: "string" }, file: { type: "string" } }, required: ["sessionId"] } },
];

export async function dispatch(name, args = {}, { dir = process.cwd() } = {}) {
Expand Down Expand Up @@ -76,6 +78,9 @@ export async function dispatch(name, args = {}, { dir = process.cwd() } = {}) {
const s = loadSession(dir, args.sessionId); if (!s) throw new Error("no tutor session: " + args.sessionId);
return { sessionId: args.sessionId, misconceptions: misconceptions(s) };
}
case "learn_tutor_reverify": {
return { sessionId: args.sessionId, ...reverifyFiles(dir, args.sessionId, { file: args.file }) };
}
default: throw new Error(`unknown tool: ${name}`);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/status.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function status() {
predict: "predict-then-observe: records the operator's OWN prediction as a pending attempt, scored only after they compare it to a rendered aid observation",
map: "normalizes objectives (string or {id,text,requires}), computes a topological learningPath, and gates readiness on prerequisite mastery",
study: "orchestrator composing due + misconceptions + interleaved order + readiness + mastery into one studyPlan, and a witnessed hash-chained studyReceipt",
reverify: "re-verifies an emitted tutor receipt from its own recorded evidence: the hash chain is recomputed and the mastery verdict re-derived under the recorded policy; failures are typed CHAIN_BROKEN / VERDICT_MISMATCH, and a chainless receipt is UNVERIFIED, never verified",
boundary: "every learning-loop capability generates practice, structures study, or checks the operator's OWN work — never produces, hints, or auto-fills an answer to a certified/graded assessment",
},
integrityInvariants: [
Expand All @@ -35,6 +36,7 @@ export function status() {
"the receipt separates automated logistics from human assessment",
"credentials, payment, CAPTCHA, and account creation halt for the operator",
"aid visualizations are learning aids only — they are witnessed but never satisfy an assess step or enter the graded receipt channels",
"tutor receipts re-verify from their own recorded evidence, never from author-controlled booleans (CHAIN_BROKEN / VERDICT_MISMATCH; chainless receipts are never verified)",
],
boundary: "The engine never produces graded work. It is a learning aid, not a bypass.",
};
Expand Down
Loading
Loading