dsh-diagnostic-tutor 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +224 -59
  2. package/lib/api.js +178 -0
  3. package/lib/api.js.map +1 -1
  4. package/lib/block-types.js +25 -0
  5. package/lib/block-types.js.map +1 -0
  6. package/lib/client.js +9448 -1654
  7. package/lib/client.js.map +1 -1
  8. package/lib/diagnostics.js +170 -0
  9. package/lib/diagnostics.js.map +1 -0
  10. package/lib/index.js +75 -13
  11. package/lib/index.js.map +1 -1
  12. package/lib/insights.js +82 -0
  13. package/lib/insights.js.map +1 -0
  14. package/lib/interactions.js +70 -0
  15. package/lib/interactions.js.map +1 -0
  16. package/lib/journal.js +122 -0
  17. package/lib/journal.js.map +1 -0
  18. package/lib/lesson-markdown.js +72 -0
  19. package/lib/lesson-markdown.js.map +1 -0
  20. package/lib/lesson.js +152 -3
  21. package/lib/lesson.js.map +1 -1
  22. package/lib/math.js +126 -0
  23. package/lib/math.js.map +1 -0
  24. package/lib/path-api.js +140 -0
  25. package/lib/path-api.js.map +1 -0
  26. package/lib/path-runtime.js +361 -0
  27. package/lib/path-runtime.js.map +1 -0
  28. package/lib/path-tools.js +107 -0
  29. package/lib/path-tools.js.map +1 -0
  30. package/lib/path.js +179 -0
  31. package/lib/path.js.map +1 -0
  32. package/lib/plan-tools.js +89 -0
  33. package/lib/plan-tools.js.map +1 -0
  34. package/lib/plan.js +82 -0
  35. package/lib/plan.js.map +1 -0
  36. package/lib/project-session.js +48 -0
  37. package/lib/project-session.js.map +1 -0
  38. package/lib/prompt.js +4 -19
  39. package/lib/prompt.js.map +1 -1
  40. package/lib/review-sync.js +51 -0
  41. package/lib/review-sync.js.map +1 -0
  42. package/lib/review-tools.js +115 -0
  43. package/lib/review-tools.js.map +1 -0
  44. package/lib/review.js +103 -0
  45. package/lib/review.js.map +1 -0
  46. package/lib/source-schema.js +24 -0
  47. package/lib/source-schema.js.map +1 -0
  48. package/lib/source-tools.js +61 -0
  49. package/lib/source-tools.js.map +1 -0
  50. package/lib/sources.js +78 -0
  51. package/lib/sources.js.map +1 -0
  52. package/lib/state.js +148 -7
  53. package/lib/state.js.map +1 -1
  54. package/lib/tools.js +456 -40
  55. package/lib/tools.js.map +1 -1
  56. package/lib/types/api.d.ts +7 -0
  57. package/lib/types/api.d.ts.map +1 -1
  58. package/lib/types/block-types.d.ts +12 -0
  59. package/lib/types/block-types.d.ts.map +1 -0
  60. package/lib/types/contract.d.ts +33 -1
  61. package/lib/types/contract.d.ts.map +1 -1
  62. package/lib/types/diagnostics.d.ts +95 -0
  63. package/lib/types/diagnostics.d.ts.map +1 -0
  64. package/lib/types/handoff.d.ts +2 -2
  65. package/lib/types/index.d.ts +15 -9
  66. package/lib/types/index.d.ts.map +1 -1
  67. package/lib/types/insights.d.ts +57 -0
  68. package/lib/types/insights.d.ts.map +1 -0
  69. package/lib/types/interactions.d.ts +23 -0
  70. package/lib/types/interactions.d.ts.map +1 -0
  71. package/lib/types/journal.d.ts +57 -0
  72. package/lib/types/journal.d.ts.map +1 -0
  73. package/lib/types/lesson-markdown.d.ts +11 -0
  74. package/lib/types/lesson-markdown.d.ts.map +1 -0
  75. package/lib/types/lesson.d.ts +697 -5
  76. package/lib/types/lesson.d.ts.map +1 -1
  77. package/lib/types/math.d.ts +50 -0
  78. package/lib/types/math.d.ts.map +1 -0
  79. package/lib/types/path-api.d.ts +5 -0
  80. package/lib/types/path-api.d.ts.map +1 -0
  81. package/lib/types/path-runtime.d.ts +1558 -0
  82. package/lib/types/path-runtime.d.ts.map +1 -0
  83. package/lib/types/path-tools.d.ts +4 -0
  84. package/lib/types/path-tools.d.ts.map +1 -0
  85. package/lib/types/path.d.ts +744 -0
  86. package/lib/types/path.d.ts.map +1 -0
  87. package/lib/types/plan-tools.d.ts +4 -0
  88. package/lib/types/plan-tools.d.ts.map +1 -0
  89. package/lib/types/plan.d.ts +53 -0
  90. package/lib/types/plan.d.ts.map +1 -0
  91. package/lib/types/project-session.d.ts +33 -0
  92. package/lib/types/project-session.d.ts.map +1 -0
  93. package/lib/types/prompt.d.ts.map +1 -1
  94. package/lib/types/review-sync.d.ts +17 -0
  95. package/lib/types/review-sync.d.ts.map +1 -0
  96. package/lib/types/review-tools.d.ts +4 -0
  97. package/lib/types/review-tools.d.ts.map +1 -0
  98. package/lib/types/review.d.ts +65 -0
  99. package/lib/types/review.d.ts.map +1 -0
  100. package/lib/types/source-schema.d.ts +31 -0
  101. package/lib/types/source-schema.d.ts.map +1 -0
  102. package/lib/types/source-tools.d.ts +4 -0
  103. package/lib/types/source-tools.d.ts.map +1 -0
  104. package/lib/types/sources.d.ts +33 -0
  105. package/lib/types/sources.d.ts.map +1 -0
  106. package/lib/types/state.d.ts +516 -9
  107. package/lib/types/state.d.ts.map +1 -1
  108. package/lib/types/tools.d.ts +3 -1
  109. package/lib/types/tools.d.ts.map +1 -1
  110. package/lib/types/version.d.ts +11 -0
  111. package/lib/types/version.d.ts.map +1 -0
  112. package/lib/types/vocabulary.d.ts +2 -2
  113. package/lib/version.js +26 -0
  114. package/lib/version.js.map +1 -0
  115. package/package.json +20 -18
@@ -0,0 +1,115 @@
1
+ /** Model-facing review queue. Scheduling only; no mastery judgement. */
2
+ import { randomUUID } from 'node:crypto';
3
+ import { z } from 'zod';
4
+ import { defineTool } from '@deepseek-ai/dsh-tools';
5
+ import { MAX_REVIEW_CARDS, REVIEW_RATINGS, dueCards, scheduleReview, } from './review.js';
6
+ const ReviewInputSchema = z.object({
7
+ front: z.string().min(1).max(2000),
8
+ back: z.string().min(1).max(4000),
9
+ courseId: z.string().min(1).max(120).optional(),
10
+ nodeId: z.string().min(1).max(120).optional(),
11
+ blockId: z.string().min(1).max(120).optional(),
12
+ sourceRefs: z.array(z.object({ sourceId: z.string().min(1).max(120), locator: z.string().min(1).max(300) })).max(6).optional(),
13
+ }).strict();
14
+ function output() {
15
+ return { schema: { type: 'object', additionalProperties: true }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] };
16
+ }
17
+ function validate(state, raw, index) {
18
+ const input = ReviewInputSchema.parse(raw);
19
+ if (input.courseId !== undefined) {
20
+ if (state.readCourse(input.courseId) === undefined)
21
+ throw new Error(`card[${index}] references an unknown course`);
22
+ if (input.nodeId !== undefined && state.readNode(input.nodeId)?.courseId !== input.courseId)
23
+ throw new Error(`card[${index}] references a node outside the course`);
24
+ }
25
+ else if (input.nodeId !== undefined)
26
+ throw new Error(`card[${index}] cannot name a node without its course`);
27
+ for (const ref of input.sourceRefs ?? []) {
28
+ if (state.readSource(ref.sourceId) === undefined)
29
+ throw new Error(`card[${index}] references an unknown source`);
30
+ }
31
+ return input;
32
+ }
33
+ export function reviewTools(state) {
34
+ return [
35
+ defineTool({
36
+ name: 'udt_review_add',
37
+ description: 'Add review cards to the learner-owned spaced-repetition queue. Scheduling is runtime policy, not a mastery claim: no node state changes. ' +
38
+ 'Use a flashcard or a check as the source of a card; include sourceRefs when the card quotes a source. New cards are due immediately.',
39
+ parameters: {
40
+ cards: {
41
+ type: 'array', required: true,
42
+ items: {
43
+ type: 'object', additionalProperties: false,
44
+ properties: {
45
+ front: { type: 'string', required: true },
46
+ back: { type: 'string', required: true },
47
+ courseId: { type: 'string' },
48
+ nodeId: { type: 'string' },
49
+ blockId: { type: 'string' },
50
+ sourceRefs: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { sourceId: { type: 'string', required: true }, locator: { type: 'string', required: true } } } },
51
+ },
52
+ },
53
+ },
54
+ },
55
+ output: output(),
56
+ execute: async (args) => {
57
+ const input = args;
58
+ if (!Array.isArray(input.cards) || input.cards.length === 0)
59
+ throw new Error('review-add-needs-cards');
60
+ if (state.listReviewCards().length + input.cards.length > MAX_REVIEW_CARDS)
61
+ throw new Error('too-many-review-cards');
62
+ const now = new Date().toISOString();
63
+ const cards = input.cards.map((candidate, index) => {
64
+ const parsed = validate(state, candidate, index);
65
+ return {
66
+ id: randomUUID(), front: parsed.front, back: parsed.back,
67
+ dueAt: now, intervalDays: 0, ease: 2.5, reps: 0, lapses: 0, status: 'open', createdAt: now, updatedAt: now,
68
+ ...(parsed.courseId === undefined ? {} : { courseId: parsed.courseId }),
69
+ ...(parsed.nodeId === undefined ? {} : { nodeId: parsed.nodeId }),
70
+ ...(parsed.blockId === undefined ? {} : { blockId: parsed.blockId }),
71
+ ...(parsed.sourceRefs === undefined ? {} : { sourceRefs: parsed.sourceRefs }),
72
+ };
73
+ });
74
+ for (const card of cards)
75
+ await state.writeReviewCard(card);
76
+ return JSON.parse(JSON.stringify({ ok: true, cards }));
77
+ },
78
+ }),
79
+ defineTool({
80
+ name: 'udt_review_next',
81
+ description: 'Read the review cards that are due now (open, dueAt <= now), earliest first. This only reports the queue; it does not grade or change learning state.',
82
+ parameters: { limit: { type: 'integer', description: 'Maximum cards, 1-20; default 5.' } },
83
+ output: output(),
84
+ execute: async (args) => {
85
+ const raw = args.limit;
86
+ const limit = Number.isInteger(raw) ? Math.min(20, Math.max(1, raw)) : 5;
87
+ return JSON.parse(JSON.stringify({ now: new Date().toISOString(), cards: dueCards(state.listReviewCards()).slice(0, limit) }));
88
+ },
89
+ }),
90
+ defineTool({
91
+ name: 'udt_review_grade',
92
+ description: 'Record one review grade (again | hard | good | easy) and schedule the card. This is interval arithmetic only: it does not mark a node confirmed, does not complete a lesson and does not auto-advance.',
93
+ parameters: {
94
+ cardId: { type: 'string', required: true },
95
+ rating: { type: 'string', required: true, enum: [...REVIEW_RATINGS] },
96
+ },
97
+ output: output(),
98
+ execute: async (args) => {
99
+ const input = args;
100
+ if (input.cardId === undefined)
101
+ throw new Error('review-grade-needs-cardId');
102
+ if (input.rating === undefined || !REVIEW_RATINGS.includes(input.rating))
103
+ throw new Error('review-grade-needs-rating');
104
+ const card = state.readReviewCard(input.cardId);
105
+ if (card === undefined)
106
+ throw new Error('no-such-review-card: read udt_review_next');
107
+ const { card: updated, log } = scheduleReview(card, input.rating);
108
+ await state.writeReviewCard(updated);
109
+ await state.writeReviewLog(log);
110
+ return JSON.parse(JSON.stringify({ ok: true, card: updated, log }));
111
+ },
112
+ }),
113
+ ];
114
+ }
115
+ //# sourceMappingURL=review-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-tools.js","sourceRoot":"","sources":["../src/review-tools.ts"],"names":[],"mappings":"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"}
package/lib/review.js ADDED
@@ -0,0 +1,103 @@
1
+ /** Review cards and a dependency-free local interval scheduler. No mastery claims. */
2
+ import { z } from 'zod';
3
+ export const REVIEW_CARDS_TABLE = 'review_cards';
4
+ export const REVIEW_LOG_TABLE = 'review_log';
5
+ export const REVIEW_RATINGS = ['again', 'hard', 'good', 'easy'];
6
+ export const REVIEW_STATUSES = ['open', 'suspended'];
7
+ export const MAX_REVIEW_CARDS = 500;
8
+ export const MAX_REVIEW_FRONT_CHARS = 2000;
9
+ export const MAX_REVIEW_BACK_CHARS = 4000;
10
+ export const ReviewSourceRefSchema = z.object({
11
+ sourceId: z.string().min(1).max(120),
12
+ locator: z.string().min(1).max(300),
13
+ }).strict();
14
+ export const ReviewCardSchema = z.object({
15
+ id: z.string().min(1).max(120),
16
+ courseId: z.string().min(1).max(120).optional(),
17
+ nodeId: z.string().min(1).max(120).optional(),
18
+ blockId: z.string().min(1).max(120).optional(),
19
+ front: z.string().min(1).max(MAX_REVIEW_FRONT_CHARS),
20
+ back: z.string().min(1).max(MAX_REVIEW_BACK_CHARS),
21
+ sourceRefs: z.array(ReviewSourceRefSchema).max(6).optional(),
22
+ dueAt: z.string(),
23
+ /** Days; fractional values are allowed for the first "again" step. */
24
+ intervalDays: z.number().min(0).max(3650),
25
+ ease: z.number().min(1.3).max(3.2),
26
+ reps: z.number().int().min(0),
27
+ lapses: z.number().int().min(0),
28
+ status: z.enum(REVIEW_STATUSES),
29
+ createdAt: z.string(),
30
+ updatedAt: z.string(),
31
+ }).strict();
32
+ export const ReviewLogSchema = z.object({
33
+ id: z.string().min(1).max(120),
34
+ cardId: z.string().min(1).max(120),
35
+ rating: z.enum(REVIEW_RATINGS),
36
+ at: z.string(),
37
+ previousDue: z.string(),
38
+ nextDue: z.string(),
39
+ intervalDays: z.number().min(0),
40
+ ease: z.number().min(1.3).max(3.2),
41
+ /** Present on logs written by the current build; old logs may lack them. */
42
+ previousIntervalDays: z.number().min(0).optional(),
43
+ previousEase: z.number().min(1.3).max(3.2).optional(),
44
+ previousReps: z.number().int().min(0).optional(),
45
+ previousLapses: z.number().int().min(0).optional(),
46
+ }).strict();
47
+ const DAY_MS = 86_400_000;
48
+ /** Apply one grade. This is scheduling arithmetic, not a judgement about learning. */
49
+ export function scheduleReview(card, rating, now = new Date()) {
50
+ let intervalDays = card.intervalDays;
51
+ let ease = card.ease;
52
+ let lapses = card.lapses;
53
+ if (rating === 'again') {
54
+ intervalDays = 0;
55
+ ease = Math.max(1.3, ease - 0.2);
56
+ lapses += 1;
57
+ }
58
+ else if (rating === 'hard') {
59
+ intervalDays = Math.max(1, card.intervalDays * 1.2);
60
+ ease = Math.max(1.3, ease - 0.15);
61
+ }
62
+ else if (rating === 'good') {
63
+ intervalDays = Math.max(1, card.intervalDays * ease);
64
+ }
65
+ else {
66
+ intervalDays = Math.max(2, card.intervalDays * ease * 1.3);
67
+ ease = Math.min(3.2, ease + 0.15);
68
+ }
69
+ const dueAt = rating === 'again'
70
+ ? new Date(now.getTime() + 10 * 60_000)
71
+ : new Date(now.getTime() + intervalDays * DAY_MS);
72
+ const updated = {
73
+ ...card,
74
+ intervalDays,
75
+ ease,
76
+ lapses,
77
+ reps: card.reps + 1,
78
+ dueAt: dueAt.toISOString(),
79
+ updatedAt: now.toISOString(),
80
+ };
81
+ const log = {
82
+ id: `${card.id}:${now.getTime()}:${card.reps + 1}`,
83
+ cardId: card.id,
84
+ rating,
85
+ at: now.toISOString(),
86
+ previousDue: card.dueAt,
87
+ nextDue: updated.dueAt,
88
+ intervalDays,
89
+ ease,
90
+ previousIntervalDays: card.intervalDays,
91
+ previousEase: card.ease,
92
+ previousReps: card.reps,
93
+ previousLapses: card.lapses,
94
+ };
95
+ return { card: updated, log };
96
+ }
97
+ export function dueCards(cards, now = new Date()) {
98
+ const time = now.getTime();
99
+ return [...cards]
100
+ .filter((card) => card.status === 'open' && Date.parse(card.dueAt) <= time)
101
+ .sort((left, right) => Date.parse(left.dueAt) - Date.parse(right.dueAt));
102
+ }
103
+ //# sourceMappingURL=review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /** Learner-owned source material. Pure schema, no DSH imports. */
2
+ import { z } from 'zod';
3
+ export const SOURCE_KINDS = ['text', 'file', 'markdown'];
4
+ export const SOURCE_STATUSES = ['ready', 'error'];
5
+ export const MAX_SOURCE_BYTES = 131_072;
6
+ export const MAX_SOURCE_CHARS = MAX_SOURCE_BYTES;
7
+ export const MAX_SOURCES = 40;
8
+ export const MAX_SOURCE_TITLE_CHARS = 200;
9
+ export const MAX_SOURCE_LOCATOR_CHARS = 500;
10
+ export const SourceRecordSchema = z.object({
11
+ id: z.string().min(1).max(120),
12
+ kind: z.enum(SOURCE_KINDS),
13
+ title: z.string().min(1).max(MAX_SOURCE_TITLE_CHARS),
14
+ /** Original path, URL, or a human-readable locator. Never fetched by the plugin. */
15
+ locator: z.string().min(1).max(MAX_SOURCE_LOCATOR_CHARS),
16
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
17
+ bytes: z.number().int().min(0).max(MAX_SOURCE_BYTES),
18
+ createdAt: z.string(),
19
+ status: z.enum(SOURCE_STATUSES),
20
+ /** Extracted text. Optional so an errored future record can stay metadata-only. */
21
+ content: z.string().max(MAX_SOURCE_CHARS).optional(),
22
+ }).strict();
23
+ export const SOURCES_TABLE = 'sources';
24
+ //# sourceMappingURL=source-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-schema.js","sourceRoot":"","sources":["../src/source-schema.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAU,CAAA;AACjE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,OAAO,CAAU,CAAA;AAC1D,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAA;AACvC,MAAM,CAAC,MAAM,gBAAgB,GAAG,gBAAgB,CAAA;AAChD,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,CAAA;AAC7B,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAA;AACzC,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAA;AAE3C,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC9B,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC;IAC1B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC;IACpD,oFAAoF;IACpF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,wBAAwB,CAAC;IACxD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,gBAAgB,CAAC;IAC1C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACpD,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC;IAC/B,mFAAmF;IACnF,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,QAAQ,EAAE;CACrD,CAAC,CAAC,MAAM,EAAE,CAAA;AAIX,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAA"}
@@ -0,0 +1,61 @@
1
+ /** Model-facing source ingestion: zero network, explicit local file or pasted text. */
2
+ import { defineTool } from '@deepseek-ai/dsh-tools';
3
+ import { addSource, sourceDraftFromFile, sourceDraftFromText } from './sources.js';
4
+ function output() {
5
+ return { schema: { type: 'object', additionalProperties: true }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] };
6
+ }
7
+ function metadata(record) {
8
+ return { id: record.id, kind: record.kind, title: record.title, locator: record.locator, sha256: record.sha256, bytes: record.bytes, createdAt: record.createdAt, status: record.status };
9
+ }
10
+ export function sourceTools(state) {
11
+ return [
12
+ defineTool({
13
+ name: 'udt_source_add',
14
+ description: 'Add learner-owned source material before teaching from it. Use kind=file with an absolute path to one local .md/.markdown/.txt file, or kind=text/markdown with pasted text. ' +
15
+ 'The plugin reads only that explicit file and never scans a directory or fetches a URL; for a web page, use your own web tool and pass the extracted text with a locator. ' +
16
+ 'Content is stored locally in the udt storage domain. Identical content is deduplicated by SHA-256. ' +
17
+ 'After adding, cite passages on lesson blocks with sourceRefs; never invent a sourceId.',
18
+ parameters: {
19
+ kind: { type: 'string', required: true, enum: ['text', 'file', 'markdown'] },
20
+ path: { type: 'string', description: 'Absolute path for kind=file. Never a directory or URL.' },
21
+ text: { type: 'string', description: 'Text for kind=text/markdown.' },
22
+ title: { type: 'string' },
23
+ locator: { type: 'string', description: 'Original URL, path or human-readable location.' },
24
+ },
25
+ output: output(),
26
+ execute: async (args) => {
27
+ const input = args;
28
+ if (input.kind !== 'text' && input.kind !== 'markdown' && input.kind !== 'file')
29
+ throw new Error('source-kind-required: use text, markdown or file');
30
+ const draft = input.kind === 'file'
31
+ ? await sourceDraftFromFile(input.path ?? '')
32
+ : sourceDraftFromText({ text: input.text ?? '', kind: input.kind, ...(input.title === undefined ? {} : { title: input.title }), ...(input.locator === undefined ? {} : { locator: input.locator }) });
33
+ const { source, deduped } = await addSource(state, draft);
34
+ return { ...metadata(source), deduped };
35
+ },
36
+ }),
37
+ defineTool({
38
+ name: 'udt_source_list',
39
+ description: 'List learner-owned sources with metadata only. Never returns full content; use udt_source_get for a selected source.',
40
+ parameters: {},
41
+ output: output(),
42
+ execute: async () => {
43
+ const sources = state.listSources().map(metadata);
44
+ return { count: sources.length, bytes: state.listSources().reduce((sum, source) => sum + source.bytes, 0), sources };
45
+ },
46
+ }),
47
+ defineTool({
48
+ name: 'udt_source_get',
49
+ description: 'Read one learner-owned source by sourceId. This returns stored text for citation; it does not fetch anything.',
50
+ parameters: { sourceId: { type: 'string', required: true } },
51
+ output: output(),
52
+ execute: async (args) => {
53
+ const source = state.readSource(args.sourceId ?? '');
54
+ if (source === undefined)
55
+ throw new Error('no-such-source: read udt_source_list');
56
+ return JSON.parse(JSON.stringify(source));
57
+ },
58
+ }),
59
+ ];
60
+ }
61
+ //# sourceMappingURL=source-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-tools.js","sourceRoot":"","sources":["../src/source-tools.ts"],"names":[],"mappings":"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"}
package/lib/sources.js ADDED
@@ -0,0 +1,78 @@
1
+ /** Local source ingestion. No network access is ever made by this module. */
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { lstat, readFile } from 'node:fs/promises';
4
+ import { basename, extname, isAbsolute, resolve } from 'node:path';
5
+ import { TextDecoder } from 'node:util';
6
+ import { MAX_SOURCE_BYTES, MAX_SOURCE_TITLE_CHARS, MAX_SOURCES } from './source-schema.js';
7
+ const ALLOWED_EXTENSIONS = new Set(['.md', '.markdown', '.txt']);
8
+ const decoder = new TextDecoder('utf-8', { fatal: true });
9
+ export function sourceDraftFromText(input) {
10
+ const text = input.text;
11
+ if (text.length === 0)
12
+ throw new Error('source-empty: paste some text first');
13
+ if (Buffer.byteLength(text, 'utf8') > MAX_SOURCE_BYTES)
14
+ throw new Error(`source-too-large: keep one source under ${MAX_SOURCE_BYTES} bytes`);
15
+ const kind = input.kind ?? 'text';
16
+ const title = (input.title?.trim() || (kind === 'markdown' ? 'Pasted Markdown' : 'Pasted text')).slice(0, MAX_SOURCE_TITLE_CHARS);
17
+ return { kind, title, locator: (input.locator?.trim() || title).slice(0, 500), content: text };
18
+ }
19
+ /**
20
+ * Read one explicitly selected local markdown/text file.
21
+ *
22
+ * This is the only place the plugin reads outside its own storage domain. It
23
+ * refuses directories, symlinks, unsupported extensions and oversized files;
24
+ * it never scans a directory or follows a URL.
25
+ */
26
+ export async function sourceDraftFromFile(path) {
27
+ if (!isAbsolute(path))
28
+ throw new Error('source-path-must-be-absolute: pass the full path to one file');
29
+ const resolved = resolve(path);
30
+ const stats = await lstat(resolved);
31
+ if (stats.isSymbolicLink())
32
+ throw new Error('source-symlink-refused: pass the file itself');
33
+ if (!stats.isFile())
34
+ throw new Error('source-not-a-file: choose one markdown or text file');
35
+ const extension = extname(resolved).toLowerCase();
36
+ if (!ALLOWED_EXTENSIONS.has(extension))
37
+ throw new Error('source-unsupported-file: only .md, .markdown and .txt are accepted in this phase');
38
+ if (stats.size > MAX_SOURCE_BYTES)
39
+ throw new Error(`source-too-large: keep one source under ${MAX_SOURCE_BYTES} bytes`);
40
+ const buffer = await readFile(resolved);
41
+ if (buffer.byteLength > MAX_SOURCE_BYTES)
42
+ throw new Error(`source-too-large: keep one source under ${MAX_SOURCE_BYTES} bytes`);
43
+ let content;
44
+ try {
45
+ content = decoder.decode(buffer);
46
+ }
47
+ catch {
48
+ throw new Error('source-not-utf8: convert the file to UTF-8 first');
49
+ }
50
+ if (content.includes('\u0000'))
51
+ throw new Error('source-binary: the selected file is not text');
52
+ const title = basename(resolved).slice(0, MAX_SOURCE_TITLE_CHARS);
53
+ return { kind: 'file', title, locator: resolved, content };
54
+ }
55
+ function sha256(content) {
56
+ return createHash('sha256').update(content, 'utf8').digest('hex');
57
+ }
58
+ export function sourceRecord(draft, now = new Date().toISOString()) {
59
+ const bytes = Buffer.byteLength(draft.content, 'utf8');
60
+ if (bytes > MAX_SOURCE_BYTES)
61
+ throw new Error(`source-too-large: keep one source under ${MAX_SOURCE_BYTES} bytes`);
62
+ return {
63
+ id: randomUUID(), kind: draft.kind, title: draft.title, locator: draft.locator,
64
+ sha256: sha256(draft.content), bytes, createdAt: now, status: 'ready', content: draft.content,
65
+ };
66
+ }
67
+ /** Add one source, deduplicating identical content by SHA-256. */
68
+ export async function addSource(store, draft, now) {
69
+ const record = sourceRecord(draft, now);
70
+ const existing = store.listSources().find((source) => source.sha256 === record.sha256 && source.status === 'ready');
71
+ if (existing !== undefined)
72
+ return { source: existing, deduped: true };
73
+ if (store.listSources().length >= MAX_SOURCES)
74
+ throw new Error(`too-many-sources: this learner already has ${MAX_SOURCES} sources`);
75
+ await store.writeSource(record);
76
+ return { source: record, deduped: false };
77
+ }
78
+ //# sourceMappingURL=sources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sources.js","sourceRoot":"","sources":["../src/sources.ts"],"names":[],"mappings":"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"}
package/lib/state.js CHANGED
@@ -38,8 +38,12 @@ import { z } from 'zod';
38
38
  import { endsFocus } from './vocabulary.js';
39
39
  import { EvidenceKindSchema, INITIAL_NODE_STATE, NodeRelationSchema, NodeStateSchema, ReadinessSchema, TeachingModeSchema, } from './vocabulary.js';
40
40
  import { LessonSchema } from './lesson.js';
41
+ import { SOURCES_TABLE, SourceRecordSchema } from './source-schema.js';
42
+ import { TASKS_TABLE, TaskSchema } from './plan.js';
43
+ import { REVIEW_CARDS_TABLE, REVIEW_LOG_TABLE, ReviewCardSchema, ReviewLogSchema } from './review.js';
41
44
  import { HandoffSchema } from './handoff.js';
42
45
  import { focusKey, handoffKey, isLegacyExecutionKey, nextStepKey } from './execution-key.js';
46
+ import { CourseRunSchema, LearningPathSchema, emptyPath, emptyRun } from './path.js';
43
47
  /* -------------------------------------------------------------------------- */
44
48
  /* Records */
45
49
  /* -------------------------------------------------------------------------- */
@@ -91,6 +95,9 @@ export const EvidenceSchema = z.object({
91
95
  at: z.string(),
92
96
  note: z.string().min(1).optional(),
93
97
  readiness: ReadinessSchema.optional(),
98
+ /** Runtime-stamped provenance for observations made during a planned attempt. */
99
+ sessionId: z.string().min(1).optional(),
100
+ attemptId: z.string().min(1).optional(),
94
101
  });
95
102
  /**
96
103
  * One node of the diagnosis map.
@@ -295,6 +302,13 @@ export const udtDomain = defineDomain({
295
302
  // handoff rather than two.
296
303
  [HANDOFFS_TABLE]: domainTable(HandoffSchema),
297
304
  [PROJECTS_TABLE]: domainTable(ProjectSchema),
305
+ // Additive v1 tables: older v1 documents open with empty planning maps.
306
+ learning_paths: domainTable(LearningPathSchema),
307
+ course_runs: domainTable(CourseRunSchema),
308
+ [SOURCES_TABLE]: domainTable(SourceRecordSchema),
309
+ [TASKS_TABLE]: domainTable(TaskSchema),
310
+ [REVIEW_CARDS_TABLE]: domainTable(ReviewCardSchema),
311
+ [REVIEW_LOG_TABLE]: domainTable(ReviewLogSchema),
298
312
  },
299
313
  });
300
314
  /**
@@ -339,6 +353,14 @@ export async function openUdState(facility) {
339
353
  const nextSteps = domain.table(NEXT_STEPS_TABLE);
340
354
  const handoffs = domain.table(HANDOFFS_TABLE);
341
355
  const projects = domain.table(PROJECTS_TABLE);
356
+ const paths = domain.table('learning_paths');
357
+ const runs = domain.table('course_runs');
358
+ const sources = domain.table(SOURCES_TABLE);
359
+ const tasks = domain.table(TASKS_TABLE);
360
+ const reviewCards = domain.table(REVIEW_CARDS_TABLE);
361
+ const reviewLog = domain.table(REVIEW_LOG_TABLE);
362
+ let planningGeneration = 0;
363
+ const runKey = (courseId, sessionId) => JSON.stringify([sessionId, courseId]);
342
364
  const learner = domain.global;
343
365
  const listNodesFor = (courseId) => [...nodes.entries()].map(([, record]) => record).filter((record) => record.courseId === courseId);
344
366
  const projectRevisions = new Map();
@@ -451,6 +473,22 @@ export async function openUdState(facility) {
451
473
  await nodes.put(record.id, record);
452
474
  return record;
453
475
  },
476
+ mutateNode(courseId, nodeId, change) {
477
+ const generation = planningGeneration;
478
+ return withProjectWrite(async () => {
479
+ if (generation !== planningGeneration || !courses.get(courseId))
480
+ throw new Error('project-unavailable: refresh the selected project');
481
+ const current = nodes.get(nodeId);
482
+ if (!current || current.courseId !== courseId)
483
+ throw new Error('node-unavailable: use an existing node in this project');
484
+ const proposed = change(structuredClone(current));
485
+ if (proposed.id !== nodeId || proposed.courseId !== courseId)
486
+ throw new Error('node identity cannot change');
487
+ const next = NodeSchema.parse(proposed);
488
+ await nodes.put(nodeId, next);
489
+ return structuredClone(next);
490
+ });
491
+ },
454
492
  nodeCount: () => nodes.size,
455
493
  readMap(courseId) {
456
494
  const course = courses.get(courseId);
@@ -594,16 +632,25 @@ export async function openUdState(facility) {
594
632
  // owns: it is a choice they made, and an export that dropped it would
595
633
  // restore them to a library with nothing selected.
596
634
  projects: [...projects.entries()].map(([, record]) => record),
635
+ learningPaths: [...paths.entries()].map(([, record]) => structuredClone(record)),
636
+ courseRuns: [...runs.entries()].map(([, record]) => structuredClone(record)),
637
+ sources: [...sources.entries()].map(([, record]) => structuredClone(record)),
638
+ tasks: [...tasks.entries()].map(([, record]) => structuredClone(record)),
639
+ reviewCards: [...reviewCards.entries()].map(([, record]) => structuredClone(record)),
640
+ reviewLog: [...reviewLog.entries()].map(([, record]) => structuredClone(record)),
597
641
  };
598
642
  },
599
643
  async resetState(now) {
600
- // Table by table, on the domain's own write chain, so a reset cannot
601
- // interleave with a write that is already queued.
602
- for (const table of [courses, nodes, lessons, focus, nextSteps, handoffs, projects]) {
603
- for (const key of [...table.keys()])
604
- await table.delete(key);
605
- }
606
- await learner.set({ initializedAt: UNINITIALIZED, updatedAt: now });
644
+ // Invalidate planning work captured before reset; serialize with planning
645
+ // and project selections so late writes cannot resurrect deleted plans.
646
+ ++planningGeneration;
647
+ await withProjectWrite(async () => {
648
+ for (const table of [courses, nodes, lessons, focus, nextSteps, handoffs, projects, paths, runs, sources, tasks, reviewCards, reviewLog]) {
649
+ for (const key of [...table.keys()])
650
+ await table.delete(key);
651
+ }
652
+ await learner.set({ initializedAt: UNINITIALIZED, updatedAt: now });
653
+ });
607
654
  },
608
655
  readHandoff: (sessionId, targetNodeId) => {
609
656
  const record = handoffs.get(handoffKey(sessionId, targetNodeId));
@@ -641,7 +688,101 @@ export async function openUdState(facility) {
641
688
  await lessons.put(record.id, record);
642
689
  return record;
643
690
  },
691
+ mutateLesson(courseId, nodeId, change) {
692
+ const generation = planningGeneration;
693
+ return withProjectWrite(async () => {
694
+ if (generation !== planningGeneration || !courses.get(courseId))
695
+ throw new Error('project-unavailable: refresh the selected project');
696
+ if (nodes.get(nodeId)?.courseId !== courseId)
697
+ throw new Error('node-unavailable: use an existing node in this project');
698
+ const current = [...lessons.entries()].map(([, record]) => record).find((record) => record.nodeId === nodeId);
699
+ const proposed = change(current === undefined ? undefined : structuredClone(current));
700
+ if (proposed.courseId !== courseId || proposed.nodeId !== nodeId || (current && proposed.id !== current.id))
701
+ throw new Error('lesson identity cannot change');
702
+ const next = LessonSchema.parse(proposed);
703
+ await lessons.put(next.id, next);
704
+ return structuredClone(next);
705
+ });
706
+ },
644
707
  lessonCount: () => lessons.size,
708
+ listSources: () => [...sources.entries()].map(([, record]) => structuredClone(record)),
709
+ readSource: (id) => {
710
+ const record = sources.get(id);
711
+ return record === undefined ? undefined : structuredClone(record);
712
+ },
713
+ async writeSource(record) {
714
+ await sources.put(record.id, record);
715
+ return record;
716
+ },
717
+ listTasks: () => [...tasks.entries()].map(([, record]) => structuredClone(record)),
718
+ readTask: (id) => {
719
+ const record = tasks.get(id);
720
+ return record === undefined ? undefined : structuredClone(record);
721
+ },
722
+ async writeTask(record) {
723
+ await tasks.put(record.id, record);
724
+ return record;
725
+ },
726
+ async deleteTask(id) {
727
+ await tasks.delete(id);
728
+ },
729
+ listReviewCards: () => [...reviewCards.entries()].map(([, record]) => structuredClone(record)),
730
+ readReviewCard: (id) => {
731
+ const record = reviewCards.get(id);
732
+ return record === undefined ? undefined : structuredClone(record);
733
+ },
734
+ async writeReviewCard(record) {
735
+ await reviewCards.put(record.id, record);
736
+ return record;
737
+ },
738
+ listReviewLog: () => [...reviewLog.entries()].map(([, record]) => structuredClone(record)),
739
+ async writeReviewLog(record) {
740
+ await reviewLog.put(record.id, record);
741
+ return record;
742
+ },
743
+ async deleteReviewLog(id) {
744
+ await reviewLog.delete(id);
745
+ },
746
+ readPath: (courseId) => {
747
+ const record = paths.get(courseId);
748
+ return record === undefined ? undefined : structuredClone(record);
749
+ },
750
+ mutatePath(courseId, expectedRevision, change) {
751
+ const generation = planningGeneration;
752
+ return withProjectWrite(async () => {
753
+ if (generation !== planningGeneration || !courses.get(courseId))
754
+ throw new Error('project-unavailable: refresh the selected project');
755
+ const current = structuredClone(paths.get(courseId) ?? emptyPath(courseId, new Date().toISOString()));
756
+ if (current.revision !== expectedRevision)
757
+ throw new Error('revision-conflict: learning plan changed; refresh before writing');
758
+ const proposed = change(current);
759
+ if (proposed.courseId !== courseId)
760
+ throw new Error('planning project identity cannot change');
761
+ const next = LearningPathSchema.parse({ ...proposed, revision: current.revision + 1 });
762
+ await paths.put(courseId, next);
763
+ return structuredClone(next);
764
+ });
765
+ },
766
+ readCourseRun: (courseId, sessionId) => {
767
+ const record = runs.get(runKey(courseId, sessionId));
768
+ return record === undefined ? undefined : structuredClone(record);
769
+ },
770
+ listCourseRuns: () => [...runs.entries()].map(([, record]) => structuredClone(record)),
771
+ mutateCourseRun(courseId, sessionId, change) {
772
+ const generation = planningGeneration;
773
+ return withProjectWrite(async () => {
774
+ if (!sessionId || generation !== planningGeneration || !courses.get(courseId))
775
+ throw new Error('project-unavailable: refresh the selected project');
776
+ const key = runKey(courseId, sessionId);
777
+ const current = structuredClone(runs.get(key) ?? emptyRun(courseId, sessionId, new Date().toISOString()));
778
+ const proposed = change(current);
779
+ if (proposed.courseId !== courseId || proposed.sessionId !== sessionId)
780
+ throw new Error('course run identity cannot change');
781
+ const next = CourseRunSchema.parse({ ...proposed, revision: current.revision + 1 });
782
+ await runs.put(key, next);
783
+ return structuredClone(next);
784
+ });
785
+ },
645
786
  snapshot() {
646
787
  const profile = learner.get();
647
788
  return {