@khorsheed/dsh-library 0.0.0-stage → 0.1.1

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 (130) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.en.md +49 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +48 -2
  5. package/cordis.patch.yml +17 -0
  6. package/lib/client.js +66334 -0
  7. package/lib/index.js +3318 -0
  8. package/lib/profiles-CFrPQdfO.js +1692 -0
  9. package/lib/tool.js +1597 -0
  10. package/lib/typert.host.d.ts +3 -0
  11. package/lib/typert.host.js +3811 -0
  12. package/lib/typert.remote-client.d.ts +137 -0
  13. package/lib/typert.remote-client.js +3077 -0
  14. package/lib/types/cards.d.ts +41 -0
  15. package/lib/types/cards.js +111 -0
  16. package/lib/types/client/LibraryPane.d.ts +26 -0
  17. package/lib/types/client/LibraryPane.js +129 -0
  18. package/lib/types/client/bind/BindChip.d.ts +33 -0
  19. package/lib/types/client/bind/BindChip.js +102 -0
  20. package/lib/types/client/contract.d.ts +167 -0
  21. package/lib/types/client/contract.js +1 -0
  22. package/lib/types/client/covers.d.ts +36 -0
  23. package/lib/types/client/covers.js +66 -0
  24. package/lib/types/client/definition.d.ts +27 -0
  25. package/lib/types/client/definition.js +32 -0
  26. package/lib/types/client/icons-local.d.ts +41 -0
  27. package/lib/types/client/icons-local.js +25 -0
  28. package/lib/types/client/icons.d.ts +27 -0
  29. package/lib/types/client/icons.js +11 -0
  30. package/lib/types/client/index.d.ts +35 -0
  31. package/lib/types/client/index.js +231 -0
  32. package/lib/types/client/judge.d.ts +70 -0
  33. package/lib/types/client/judge.js +262 -0
  34. package/lib/types/client/locales.d.ts +553 -0
  35. package/lib/types/client/locales.js +1076 -0
  36. package/lib/types/client/look.d.ts +63 -0
  37. package/lib/types/client/look.js +64 -0
  38. package/lib/types/client/math.d.ts +24 -0
  39. package/lib/types/client/math.js +47 -0
  40. package/lib/types/client/mistakes.d.ts +21 -0
  41. package/lib/types/client/mistakes.js +28 -0
  42. package/lib/types/client/preset-visibility.d.ts +109 -0
  43. package/lib/types/client/preset-visibility.js +123 -0
  44. package/lib/types/client/quiz-drafts.d.ts +29 -0
  45. package/lib/types/client/quiz-drafts.js +50 -0
  46. package/lib/types/client/reveal.d.ts +21 -0
  47. package/lib/types/client/reveal.js +31 -0
  48. package/lib/types/client/store.d.ts +111 -0
  49. package/lib/types/client/store.js +48 -0
  50. package/lib/types/client/titles.d.ts +17 -0
  51. package/lib/types/client/titles.js +23 -0
  52. package/lib/types/client/ui.d.ts +96 -0
  53. package/lib/types/client/ui.js +124 -0
  54. package/lib/types/client/views/book.d.ts +57 -0
  55. package/lib/types/client/views/book.js +164 -0
  56. package/lib/types/client/views/cards.d.ts +17 -0
  57. package/lib/types/client/views/cards.js +53 -0
  58. package/lib/types/client/views/home.d.ts +26 -0
  59. package/lib/types/client/views/home.js +263 -0
  60. package/lib/types/client/views/lesson.d.ts +42 -0
  61. package/lib/types/client/views/lesson.js +146 -0
  62. package/lib/types/client/views/path.d.ts +32 -0
  63. package/lib/types/client/views/path.js +64 -0
  64. package/lib/types/client/views/quiz.d.ts +42 -0
  65. package/lib/types/client/views/quiz.js +366 -0
  66. package/lib/types/client/views/station-quiz.d.ts +43 -0
  67. package/lib/types/client/views/station-quiz.js +203 -0
  68. package/lib/types/client/views/station.d.ts +18 -0
  69. package/lib/types/client/views/station.js +142 -0
  70. package/lib/types/image-refs.d.ts +35 -0
  71. package/lib/types/image-refs.js +71 -0
  72. package/lib/types/index.d.ts +51 -0
  73. package/lib/types/index.js +31 -0
  74. package/lib/types/mastery.d.ts +81 -0
  75. package/lib/types/mastery.js +110 -0
  76. package/lib/types/profiles.d.ts +31 -0
  77. package/lib/types/profiles.js +25 -0
  78. package/lib/types/remote.d.ts +355 -0
  79. package/lib/types/remote.js +332 -0
  80. package/lib/types/service/assess.d.ts +144 -0
  81. package/lib/types/service/assess.js +773 -0
  82. package/lib/types/service/bindings.d.ts +92 -0
  83. package/lib/types/service/bindings.js +198 -0
  84. package/lib/types/service/cards.d.ts +85 -0
  85. package/lib/types/service/cards.js +320 -0
  86. package/lib/types/service/core.d.ts +124 -0
  87. package/lib/types/service/core.js +169 -0
  88. package/lib/types/service/pages.d.ts +60 -0
  89. package/lib/types/service/pages.js +125 -0
  90. package/lib/types/service/roadmap.d.ts +99 -0
  91. package/lib/types/service/roadmap.js +284 -0
  92. package/lib/types/service/rules.d.ts +63 -0
  93. package/lib/types/service/rules.js +245 -0
  94. package/lib/types/service/shelf.d.ts +136 -0
  95. package/lib/types/service/shelf.js +422 -0
  96. package/lib/types/service.d.ts +42 -0
  97. package/lib/types/service.js +41 -0
  98. package/lib/types/store.d.ts +257 -0
  99. package/lib/types/store.js +1376 -0
  100. package/lib/types/tool-definition.d.ts +49 -0
  101. package/lib/types/tool-definition.js +49 -0
  102. package/lib/types/tool.d.ts +71 -0
  103. package/lib/types/tool.js +174 -0
  104. package/lib/types/tools/assess.d.ts +11 -0
  105. package/lib/types/tools/assess.js +82 -0
  106. package/lib/types/tools/binding.d.ts +11 -0
  107. package/lib/types/tools/binding.js +46 -0
  108. package/lib/types/tools/cards.d.ts +11 -0
  109. package/lib/types/tools/cards.js +100 -0
  110. package/lib/types/tools/kit.d.ts +215 -0
  111. package/lib/types/tools/kit.js +281 -0
  112. package/lib/types/tools/notes.d.ts +11 -0
  113. package/lib/types/tools/notes.js +40 -0
  114. package/lib/types/tools/page.d.ts +11 -0
  115. package/lib/types/tools/page.js +59 -0
  116. package/lib/types/tools/project.d.ts +11 -0
  117. package/lib/types/tools/project.js +67 -0
  118. package/lib/types/tools/quiz.d.ts +11 -0
  119. package/lib/types/tools/quiz.js +74 -0
  120. package/lib/types/tools/read.d.ts +11 -0
  121. package/lib/types/tools/read.js +127 -0
  122. package/lib/types/tools/resource.d.ts +11 -0
  123. package/lib/types/tools/resource.js +147 -0
  124. package/lib/types/tools/roadmap.d.ts +11 -0
  125. package/lib/types/tools/roadmap.js +102 -0
  126. package/lib/types/types.d.ts +752 -0
  127. package/lib/types/types.js +159 -0
  128. package/locale/en.json +6 -0
  129. package/locale/zh.json +6 -0
  130. package/package.json +135 -3
package/lib/tool.js ADDED
@@ -0,0 +1,1597 @@
1
+ import { U as isThickResource, V as formatLocator, j as LIBRARY_RESOURCE_KINDS, r as masteryReason, t as profileOf, x as normalizeQuizOptions } from "./profiles-CFrPQdfO.js";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { defineTool } from "@deepseek-ai/dsh-tools";
4
+ //#region lib/types/image-refs.js
5
+ /** How many sessions are remembered. */
6
+ const SESSIONS = 200;
7
+ /** The id without its digest prefix, for matching what the model wrote. */
8
+ function bareId(id) {
9
+ return id.replace(/^sha256:/, "");
10
+ }
11
+ /** A whole reference, when a value carries every field. */
12
+ function asAttachmentRef(value) {
13
+ if (typeof value !== "object" || value === null) return void 0;
14
+ const ref = value;
15
+ if (typeof ref.attachmentId !== "string" || typeof ref.mediaType !== "string") return void 0;
16
+ if (typeof ref.bytes !== "number" || ref.bytes <= 0) return void 0;
17
+ if (typeof ref.width !== "number" || typeof ref.height !== "number") return void 0;
18
+ return {
19
+ attachmentId: ref.attachmentId,
20
+ mediaType: ref.mediaType,
21
+ bytes: ref.bytes,
22
+ width: ref.width,
23
+ height: ref.height
24
+ };
25
+ }
26
+ /** The image references each session's user messages carried, newest last. */
27
+ var ImageRefCache = class {
28
+ bySession = /* @__PURE__ */ new Map();
29
+ /** Remember the images one committed event carries (non-image events are ignored). */
30
+ observe(sessionId, event) {
31
+ if (event.type !== "user/message") return;
32
+ const content = event.data?.content;
33
+ if (!Array.isArray(content)) return;
34
+ for (const block of content) {
35
+ if (block?.type !== "image") continue;
36
+ const ref = asAttachmentRef(block.attachment);
37
+ if (ref !== void 0) this.remember(sessionId, ref);
38
+ }
39
+ }
40
+ remember(sessionId, ref) {
41
+ const kept = (this.bySession.get(sessionId) ?? []).filter((entry) => entry.attachmentId !== ref.attachmentId);
42
+ kept.push(ref);
43
+ this.bySession.delete(sessionId);
44
+ this.bySession.set(sessionId, kept.slice(-20));
45
+ while (this.bySession.size > SESSIONS) this.bySession.delete(this.bySession.keys().next().value);
46
+ }
47
+ /**
48
+ * The whole reference for what the model wrote: a complete reference
49
+ * passes through; an id (with or without its prefix) is looked up; nothing
50
+ * at all means the session's latest photo.
51
+ */
52
+ resolve(sessionId, written) {
53
+ const whole = asAttachmentRef(written);
54
+ const known = sessionId === void 0 ? [] : this.bySession.get(sessionId) ?? [];
55
+ const id = typeof written === "string" ? written : typeof written?.attachmentId === "string" ? written.attachmentId : void 0;
56
+ if (id !== void 0) {
57
+ const match = known.find((ref) => bareId(ref.attachmentId) === bareId(id));
58
+ if (match !== void 0) return match;
59
+ }
60
+ if (whole !== void 0) return whole;
61
+ return id === void 0 ? known.at(-1) : void 0;
62
+ }
63
+ };
64
+ //#endregion
65
+ //#region lib/types/tools/kit.js
66
+ /** JSON pass-through output — what the render emits IS what the model reads, so it goes compact (indentation doubled every result). */
67
+ function jsonOutput() {
68
+ return {
69
+ schema: { type: "json" },
70
+ render: (_args, value) => [{
71
+ type: "text",
72
+ text: JSON.stringify(value)
73
+ }]
74
+ };
75
+ }
76
+ /** Unwrap a service outcome: the domain value, or a thrown Error whose message IS the lesson. */
77
+ function unwrap(outcome) {
78
+ if (outcome.ok) return outcome;
79
+ throw new Error(outcome.message);
80
+ }
81
+ const READ_WHATS = [
82
+ "projects",
83
+ "resources",
84
+ "toc",
85
+ "roadmap",
86
+ "pages",
87
+ "cards",
88
+ "quiz"
89
+ ];
90
+ /** The calling session's id, when the call came from a session. */
91
+ function sessionIdOf(exec) {
92
+ const id = exec?.agent?.session?.id;
93
+ return id === void 0 ? void 0 : String(id);
94
+ }
95
+ /**
96
+ * Payloads arrive as objects — or, from models that stringify deep parameters
97
+ * (common with big nested ones), as their JSON text; both parse. A refusal
98
+ * names what actually arrived, so the model fixes the transport instead of
99
+ * guessing at the shape.
100
+ */
101
+ function payload(value) {
102
+ if (typeof value === "string") try {
103
+ value = JSON.parse(value);
104
+ } catch {
105
+ return;
106
+ }
107
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
108
+ }
109
+ function payloadList(value) {
110
+ if (typeof value === "string") try {
111
+ value = JSON.parse(value);
112
+ } catch {
113
+ return;
114
+ }
115
+ if (Array.isArray(value)) return value;
116
+ const items = payload(value)?.items;
117
+ return Array.isArray(items) ? items : void 0;
118
+ }
119
+ function arrival(field, value) {
120
+ if (value === void 0) return `no ${field} field arrived — set the ${field} field itself; its value may be the object or its JSON text`;
121
+ if (typeof value === "string") try {
122
+ JSON.parse(value);
123
+ return `${field} arrived as JSON text that is not the expected shape`;
124
+ } catch {
125
+ return `${field} arrived as text that is not valid JSON — send the object itself (or its JSON text)`;
126
+ }
127
+ return `${field} arrived as ${Array.isArray(value) ? "an array" : typeof value} with an unexpected shape`;
128
+ }
129
+ /**
130
+ * A contents sent as one text: one row per line. Models also join the rows
131
+ * with ;/; on a single line, or send literal "\\n" escapes — a single line
132
+ * holding several separators splits on them (a 30-chapter book once landed
133
+ * as ONE row, and every station's chapters then fell out of range).
134
+ */
135
+ function splitTocText(text) {
136
+ const lines = text.replace(/\\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line !== "");
137
+ if (lines.length !== 1) return lines;
138
+ const parts = lines[0].split(/\s*[;;]\s*/).map((part) => part.trim()).filter((part) => part !== "");
139
+ return parts.length >= 3 ? parts : lines;
140
+ }
141
+ /**
142
+ * A station's chapters as a locator: row numbers and ranges, whatever the
143
+ * dash or separator ("15–16", "3-4、7", "第3-4章"); words around them (an
144
+ * appendix, a part name) come back as `rest` for a topic side reference.
145
+ */
146
+ function chaptersLocator(chapters) {
147
+ const normalized = chapters.replace(/[–—~~至到]/g, "-").replace(/[,、;;/]/g, ",");
148
+ const ranges = [...normalized.matchAll(/(\d+)\s*(?:-\s*(\d+))?/g)].map((match) => match[2] === void 0 ? match[1] : `${match[1]}-${match[2]}`);
149
+ const rest = normalized.replace(/(\d+)\s*(?:-\s*(\d+))?/g, "").replace(/[第章节,+\s-]+/g, " ").trim();
150
+ return {
151
+ locator: ranges.join(","),
152
+ ...rest === "" ? {} : { rest }
153
+ };
154
+ }
155
+ /** The first non-blank string of the candidates (the resource/book alias pairs). */
156
+ function pickString(...values) {
157
+ return values.find((value) => typeof value === "string" && value.trim() !== "")?.trim();
158
+ }
159
+ /**
160
+ * One cover argument → the stored `{ ref, locator?, … }`. Accepts the string
161
+ * shorthand ("r_…#1-5" | "https://…" | "topic: …"), the v5 object, and every
162
+ * legacy union member ({ kind: "book", bookId|book, ranges } |
163
+ * { kind: "resource", resourceId|resource } | { kind: "url", url, title? } |
164
+ * { kind: "topic", text }). Undefined when nothing usable arrived.
165
+ */
166
+ function normalizeCoverArg(value) {
167
+ if (typeof value === "string") {
168
+ const shorthand = value.trim();
169
+ if (shorthand === "") return void 0;
170
+ if (shorthand.startsWith("topic:") || /^https?:\/\//.test(shorthand)) return { ref: shorthand };
171
+ const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:#\s*(.+))?$/.exec(shorthand);
172
+ return {
173
+ ref: match?.[1] ?? shorthand,
174
+ ...match?.[2] === void 0 ? {} : { locator: match[2].trim() }
175
+ };
176
+ }
177
+ const record = payload(value);
178
+ if (record === void 0) return void 0;
179
+ const provenance = {
180
+ ...typeof record.by === "string" ? { by: record.by } : {},
181
+ ...typeof record.verifiedAt === "string" ? { verifiedAt: record.verifiedAt } : {}
182
+ };
183
+ if (typeof record.ref === "string" && record.ref.trim() !== "") return {
184
+ ref: record.ref.trim(),
185
+ ...typeof record.locator === "string" && record.locator.trim() !== "" ? { locator: record.locator.trim() } : {},
186
+ ...typeof record.title === "string" && record.title.trim() !== "" ? { title: record.title.trim() } : {},
187
+ ...provenance
188
+ };
189
+ const bookId = pickString(record.bookId, record.book);
190
+ if ((record.kind === "book" || record.kind === void 0) && bookId !== void 0) {
191
+ const ranges = Array.isArray(record.ranges) ? record.ranges.flatMap((range) => {
192
+ const row = payload(range);
193
+ const from = typeof row?.from === "number" ? row.from : void 0;
194
+ const to = typeof row?.to === "number" ? row.to : void 0;
195
+ return from === void 0 || to === void 0 ? [] : [{
196
+ from,
197
+ to
198
+ }];
199
+ }) : [];
200
+ return {
201
+ ref: bookId,
202
+ ...ranges.length === 0 ? {} : { locator: formatLocator(ranges) },
203
+ ...provenance
204
+ };
205
+ }
206
+ const resourceId = pickString(record.resourceId, record.resource);
207
+ if (record.kind === "resource" && resourceId !== void 0) return {
208
+ ref: resourceId,
209
+ ...provenance
210
+ };
211
+ if (record.kind === "url" && typeof record.url === "string") return {
212
+ ref: record.url,
213
+ ...typeof record.title === "string" ? { title: record.title } : {},
214
+ ...provenance
215
+ };
216
+ if (record.kind === "topic" && typeof record.text === "string") return {
217
+ ref: `topic:${record.text}`,
218
+ ...provenance
219
+ };
220
+ }
221
+ /**
222
+ * A question's two axes are optional on the wire: target defaults to the
223
+ * station's topic, evidence to domain knowledge. Flat shorthands fill them in —
224
+ * cardIds imply a marked target; pageId / url imply photo / web evidence; a
225
+ * bare string sets just the kind. The service's two-axis gate is untouched: it
226
+ * judges the normalized shape.
227
+ */
228
+ function normalizeQuestionAxes(question) {
229
+ const { cardIds, quote, url, pageId, ...rest } = question;
230
+ const flatIds = Array.isArray(cardIds) ? cardIds.filter((id) => typeof id === "string" && id !== "") : void 0;
231
+ const rawTarget = typeof rest.target === "string" ? { kind: rest.target } : rest.target;
232
+ let target = (rawTarget !== null && typeof rawTarget === "object" && !Array.isArray(rawTarget) ? rawTarget : void 0) ?? (flatIds !== void 0 && flatIds.length > 0 ? { kind: "marked" } : { kind: "topic" });
233
+ if (flatIds !== void 0 && flatIds.length > 0 && target.cardIds === void 0) target = {
234
+ ...target,
235
+ cardIds: flatIds
236
+ };
237
+ const rawEvidence = typeof rest.evidence === "string" ? { kind: rest.evidence } : rest.evidence;
238
+ let evidence = (rawEvidence !== null && typeof rawEvidence === "object" && !Array.isArray(rawEvidence) ? rawEvidence : void 0) ?? { kind: typeof pageId === "string" && pageId !== "" ? "photo" : typeof url === "string" && url !== "" ? "web" : "knowledge" };
239
+ for (const [key, value] of [
240
+ ["quote", quote],
241
+ ["url", url],
242
+ ["pageId", pageId]
243
+ ]) if (typeof value === "string" && value !== "" && evidence[key] === void 0) evidence = {
244
+ ...evidence,
245
+ [key]: value
246
+ };
247
+ return {
248
+ ...rest,
249
+ target,
250
+ evidence
251
+ };
252
+ }
253
+ /**
254
+ * The roadmap nodes schema. Only the canonical shape is advertised; execute
255
+ * still takes the older spellings (also, dependsOn, read stations, acceptance
256
+ * refs, chapterRanges, covers) — the schema items allow extra keys, and the
257
+ * JSON-text branch lets a stringified list through to execute's parser.
258
+ */
259
+ const NODES_SCHEMA = { oneOf: [{
260
+ type: "array",
261
+ items: {
262
+ type: "object",
263
+ additionalProperties: true,
264
+ properties: {
265
+ id: {
266
+ type: "string",
267
+ description: "Unique: s1, s2…"
268
+ },
269
+ title: { type: "string" },
270
+ chapters: {
271
+ oneOf: [{ type: "string" }, { type: "integer" }],
272
+ description: "Contents rows it covers: \"4\" | \"3-4\"."
273
+ },
274
+ points: {
275
+ type: "array",
276
+ description: "Required: its 3–5 knowledge points — the chapter quiz asks them.",
277
+ items: {
278
+ type: "object",
279
+ additionalProperties: true,
280
+ properties: {
281
+ id: {
282
+ type: "string",
283
+ description: "A stable slug."
284
+ },
285
+ title: { type: "string" }
286
+ }
287
+ }
288
+ },
289
+ goals: {
290
+ type: "json",
291
+ description: "What the learner can do after it: [\"…\"]."
292
+ }
293
+ }
294
+ }
295
+ }, {
296
+ type: "string",
297
+ description: "The same list as JSON text."
298
+ }] };
299
+ /**
300
+ * The quiz questions schema. ids are numbered when missing; target and
301
+ * evidence follow from cardIds / quote / pageId / url and the textbook (the
302
+ * full two-axis objects still parse).
303
+ */
304
+ const QUESTIONS_SCHEMA = { oneOf: [{
305
+ type: "array",
306
+ items: {
307
+ type: "object",
308
+ additionalProperties: true,
309
+ properties: {
310
+ point: {
311
+ type: "string",
312
+ description: "The station point it probes."
313
+ },
314
+ kind: {
315
+ type: "string",
316
+ enum: [
317
+ "choice",
318
+ "fill",
319
+ "free"
320
+ ]
321
+ },
322
+ prompt: { type: "string" },
323
+ options: {
324
+ type: "json",
325
+ description: "choice only: [\"…\", \"…\"]."
326
+ },
327
+ answer: {
328
+ type: "string",
329
+ description: "choice: the right option, verbatim."
330
+ },
331
+ explanation: { type: "string" },
332
+ quote: {
333
+ type: "string",
334
+ description: "The page text it rests on (required for a material textbook)."
335
+ }
336
+ }
337
+ }
338
+ }, {
339
+ type: "string",
340
+ description: "The same list as JSON text."
341
+ }] };
342
+ /** The per-call helpers every tool closes over, built once per tool set. */
343
+ function createToolKit(service, images) {
344
+ /** The session's binding, when it has one. */
345
+ const bindingOf = (exec) => {
346
+ const sessionId = sessionIdOf(exec);
347
+ return sessionId === void 0 ? void 0 : service.getBinding(sessionId);
348
+ };
349
+ /** The project: the argument, else the binding, else the resource's owner, else a lesson. */
350
+ const projectOf = (value, exec, resource) => {
351
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
352
+ const bound = bindingOf(exec)?.projectId;
353
+ if (bound !== void 0) return bound;
354
+ if (resource !== void 0) {
355
+ const owner = service.findResourceOwner(resource);
356
+ if (owner !== void 0) return owner;
357
+ }
358
+ throw new Error("no project: this session is not bound to one and no resource resolves one — library_read · projects lists them; ask the learner which one (ask_user_question), then library_binding { project, resource? }");
359
+ };
360
+ /** A successful write knows its work context: an unbound session binds to it, and a project-bound session picks up the resource it just registered — create the project, register the book, draft the path, and the chain has strung itself. Never steals another project's session, never yanks a resource off one. */
361
+ const autoBind = async (exec, binding) => {
362
+ const sessionId = sessionIdOf(exec);
363
+ if (sessionId === void 0) return;
364
+ service.noteSessionWrite(sessionId);
365
+ const current = bindingOf(exec);
366
+ if (current !== void 0 && (current.projectId !== binding.projectId || current.resourceId !== void 0)) return;
367
+ if (current !== void 0 && binding.resourceId === void 0) return;
368
+ await service.setBinding(sessionId, binding);
369
+ };
370
+ /** The textbooks of the bound project, as one teaching line for a refusal. */
371
+ const textbookList = (exec) => {
372
+ const textbooks = (sessionIdOf(exec) === void 0 ? void 0 : service.bindingSummary(sessionIdOf(exec), 0))?.textbooks ?? [];
373
+ return textbooks.length === 0 ? "" : ` — this project's textbooks: ${textbooks.map((textbook) => `${textbook.seq ?? "-"}. ${textbook.id} 《${textbook.title}》`).join("; ")}`;
374
+ };
375
+ /** The resource: the argument (either spelling), else the binding's resource, else a lesson. */
376
+ const resourceOf = (args, exec) => {
377
+ const named = pickString(args.resource, args.book);
378
+ if (named !== void 0) return named;
379
+ const bound = bindingOf(exec)?.resourceId;
380
+ if (bound !== void 0) return bound;
381
+ throw new Error(`no textbook named: pass resource${textbookList(exec) || " (library_read · resources lists them)"}, or bind this session to one (library_binding)`);
382
+ };
383
+ /** The shared scope parameters (write tools). */
384
+ const scopeParams = { project: {
385
+ type: "string",
386
+ description: "Defaults to the binding."
387
+ } };
388
+ return {
389
+ service,
390
+ images,
391
+ bindingOf,
392
+ projectOf,
393
+ autoBind,
394
+ textbookList,
395
+ resourceOf,
396
+ scopeParams,
397
+ textbookParams: {
398
+ ...scopeParams,
399
+ resource: {
400
+ type: "string",
401
+ description: "Defaults to the binding."
402
+ }
403
+ }
404
+ };
405
+ }
406
+ //#endregion
407
+ //#region lib/types/tools/read.js
408
+ /**
409
+ * The `library_read` tool (moved verbatim from tool-definition.ts; the shared
410
+ * helpers and schemas are in `tools/kit.ts`).
411
+ *
412
+ * @module @khorsheed/dsh-library/tools/read
413
+ */
414
+ /**
415
+ * A path as the coach needs it: each station's id, title, state, rows and
416
+ * points, and whether its 讲义 exist — never the 讲义 text itself (a read of
417
+ * an 11-station path once returned 7,300 characters).
418
+ */
419
+ function compactRoadmap(read) {
420
+ const stations = (roadmap) => roadmap.nodes.map((node) => ({
421
+ id: node.id,
422
+ title: node.title,
423
+ state: node.state,
424
+ ...node.gate === "quiz" ? {} : { gate: node.gate },
425
+ rows: node.covers.flatMap((cover) => cover.locator === void 0 ? [] : [cover.locator]).join(",") || void 0,
426
+ points: (node.points ?? []).map((point) => `${point.id}: ${point.title}`),
427
+ ...node.goals.length === 0 ? {} : { goals: node.goals },
428
+ ...node.acceptance === void 0 || node.acceptance.length === 0 ? {} : { acceptance: node.acceptance },
429
+ ...node.notes === void 0 ? {} : { notes: `${node.notes.text.length} chars, revision ${node.notes.revisions}` },
430
+ ...node.quizStats === void 0 ? {} : { best: node.quizStats.bestRatio }
431
+ }));
432
+ return {
433
+ ...read.roadmap === void 0 ? { adopted: null } : { adopted: { stations: stations(read.roadmap) } },
434
+ ...read.draft === void 0 ? {} : { draft: {
435
+ stations: stations(read.draft),
436
+ note: read.draft.note
437
+ } }
438
+ };
439
+ }
440
+ /** Build the `library_read` tool over the shared kit. */
441
+ function libraryReadTool(kit) {
442
+ const { service, bindingOf, projectOf, resourceOf } = kit;
443
+ return defineTool({
444
+ name: "library_read",
445
+ description: "Read the library: projects (all, with this session's binding) | resources (one project's textbooks in study order) | toc (a textbook's contents rows) | roadmap (a textbook's path and pending draft) | pages (photographed pages) | cards (the learner's cards, weakest first) | quiz (one quiz, or the list). project and resource default to the binding.",
446
+ parameters: {
447
+ what: {
448
+ type: "string",
449
+ description: "One of the reads above."
450
+ },
451
+ project: { type: "string" },
452
+ resource: { type: "string" },
453
+ node: {
454
+ type: "string",
455
+ description: "A station (pages / cards)."
456
+ },
457
+ quiz: {
458
+ type: "string",
459
+ description: "A quiz id."
460
+ },
461
+ query: {
462
+ type: "string",
463
+ description: "cards: search term, gloss, sentences."
464
+ },
465
+ due: {
466
+ type: "boolean",
467
+ description: "cards: only those due for review."
468
+ }
469
+ },
470
+ output: jsonOutput(),
471
+ isConcurrencySafe: () => true,
472
+ async execute(args, exec) {
473
+ const argsAny = args;
474
+ switch (typeof args.what === "string" && args.what !== "" ? args.what : args.quiz !== void 0 ? "quiz" : args.resource !== void 0 || args.node !== void 0 ? "roadmap" : "projects") {
475
+ case "projects": return {
476
+ ...await service.listProjects(),
477
+ binding: bindingOf(exec) ?? null
478
+ };
479
+ case "resources":
480
+ case "books": {
481
+ const detail = unwrap(await service.getProject({ id: projectOf(args.project, exec) }));
482
+ return {
483
+ project: detail.project,
484
+ resources: detail.resources
485
+ };
486
+ }
487
+ case "toc": {
488
+ const resource = resourceOf(args, exec);
489
+ return unwrap(await service.getResource({
490
+ projectId: projectOf(args.project, exec, resource),
491
+ resourceId: resource
492
+ })).resource;
493
+ }
494
+ case "roadmap": {
495
+ const resource = resourceOf(args, exec);
496
+ return compactRoadmap(await service.getRoadmap({
497
+ projectId: projectOf(args.project, exec, resource),
498
+ resourceId: resource
499
+ }));
500
+ }
501
+ case "pages": {
502
+ const resource = resourceOf(args, exec);
503
+ return await service.listPages({
504
+ projectId: projectOf(args.project, exec, resource),
505
+ resourceId: resource,
506
+ ...args.node === void 0 ? {} : { nodeId: args.node }
507
+ });
508
+ }
509
+ case "cards": {
510
+ const resource = pickString(args.resource, args.book);
511
+ const result = await service.listCards({
512
+ projectId: projectOf(args.project, exec, resource),
513
+ ...resource === void 0 ? {} : { resourceId: resource },
514
+ ...args.node === void 0 ? {} : { nodeId: args.node },
515
+ ...args.query === void 0 ? {} : { query: args.query },
516
+ ...argsAny.kind === "word" || argsAny.kind === "concept" ? { kind: argsAny.kind } : {},
517
+ ...[
518
+ "photo",
519
+ "chat",
520
+ "quiz",
521
+ "manual"
522
+ ].includes(String(argsAny.source ?? "")) ? { source: argsAny.source } : {},
523
+ ...typeof argsAny.weakMin === "number" ? { weakMin: argsAny.weakMin } : {},
524
+ ...args.due === true ? { due: true } : {},
525
+ limit: typeof argsAny.limit === "number" ? argsAny.limit : 30
526
+ });
527
+ return {
528
+ total: result.total,
529
+ cards: result.cards.map((card) => ({
530
+ id: card.id,
531
+ kind: card.kind,
532
+ term: card.term,
533
+ ...card.aliases.length === 0 ? {} : { aliases: card.aliases },
534
+ gloss: card.gloss,
535
+ ...card.userGloss === void 0 ? {} : { userGloss: card.userGloss },
536
+ ...card.resourceId === void 0 ? {} : { resource: card.resourceId },
537
+ contexts: card.contexts.slice(0, 2).map((context) => context.sentence),
538
+ weakness: card.weakness,
539
+ box: card.mastery.box,
540
+ dueAt: card.mastery.dueAt,
541
+ sources: card.sources.map((source) => source.note === void 0 ? source.type : `${source.type}: ${source.note}`).slice(-4)
542
+ }))
543
+ };
544
+ }
545
+ case "quiz": {
546
+ const resource = pickString(args.resource, args.book) ?? bindingOf(exec)?.resourceId;
547
+ const project = projectOf(args.project, exec, resource);
548
+ if (args.quiz === void 0 || args.quiz.trim() === "") return await service.listAssessments({
549
+ projectId: project,
550
+ type: "quiz",
551
+ ...resource === void 0 ? {} : { resourceId: resource }
552
+ });
553
+ return unwrap(await service.getAssessment({
554
+ projectId: project,
555
+ assessmentId: args.quiz.trim()
556
+ })).assessment;
557
+ }
558
+ case "stats": return await service.stats();
559
+ default: throw new Error(`what is one of ${READ_WHATS.join(" | ")} — got ${JSON.stringify(args.what)}`);
560
+ }
561
+ }
562
+ });
563
+ }
564
+ //#endregion
565
+ //#region lib/types/tools/project.js
566
+ /**
567
+ * The `library_project` tool (moved verbatim from tool-definition.ts; the shared
568
+ * helpers and schemas are in `tools/kit.ts`).
569
+ *
570
+ * @module @khorsheed/dsh-library/tools/project
571
+ */
572
+ /** Build the `library_project` tool over the shared kit. */
573
+ function libraryProjectTool(kit) {
574
+ const { service, autoBind } = kit;
575
+ return defineTool({
576
+ name: "library_project",
577
+ description: "Create a learning project (one goal), or edit one (projectId + what changes; goal \"\" clears). template: subject | language (marks are words) | custom. Next: shelve its textbooks (library_textbook).",
578
+ parameters: {
579
+ projectId: {
580
+ type: "string",
581
+ description: "Edit this project; omit to create (ids are minted)."
582
+ },
583
+ name: { type: "string" },
584
+ template: {
585
+ type: "string",
586
+ enum: [
587
+ "subject",
588
+ "language",
589
+ "custom"
590
+ ]
591
+ },
592
+ goal: {
593
+ type: "string",
594
+ description: "One line."
595
+ }
596
+ },
597
+ output: jsonOutput(),
598
+ isConcurrencySafe: () => false,
599
+ async execute(args, exec) {
600
+ const legacyId = args.id;
601
+ const editId = pickString(args.projectId, legacyId);
602
+ if (editId !== void 0) {
603
+ const patch = {};
604
+ if (typeof args.name === "string") patch.name = args.name;
605
+ if (typeof args.template === "string") patch.template = args.template;
606
+ if (typeof args.goal === "string") patch.goal = args.goal;
607
+ if (Object.keys(patch).length === 0) throw new Error("editing a project takes projectId plus the fields to change (name? / goal? / template?) — omit projectId to CREATE one (name + template)");
608
+ const edited = await service.updateProject({
609
+ id: editId,
610
+ patch
611
+ });
612
+ if (edited.ok) {
613
+ await autoBind(exec, { projectId: edited.project.id });
614
+ return edited;
615
+ }
616
+ if (typeof args.name !== "string" || args.name.trim() === "") throw new Error(edited.message);
617
+ const created = unwrap(await service.createProject({
618
+ name: args.name,
619
+ ...typeof args.template === "string" ? { template: args.template } : {},
620
+ ...typeof args.goal === "string" ? { goal: args.goal } : {}
621
+ }));
622
+ await autoBind(exec, { projectId: created.project.id });
623
+ return {
624
+ ...created,
625
+ note: `no project ${JSON.stringify(editId)} existed — created a new one instead (ids are minted (p_…), never chosen by the caller; omit projectId to create)`
626
+ };
627
+ }
628
+ if (typeof args.name !== "string" || args.name.trim() === "") throw new Error("creating a project takes name (+ template: \"language\" | \"subject\" | \"custom\", goal?) — e.g. { name: \"数学\", template: \"subject\" }");
629
+ const created = unwrap(await service.createProject({
630
+ name: args.name,
631
+ ...typeof args.template === "string" ? { template: args.template } : {},
632
+ ...typeof args.goal === "string" ? { goal: args.goal } : {}
633
+ }));
634
+ await autoBind(exec, { projectId: created.project.id });
635
+ return created;
636
+ }
637
+ });
638
+ }
639
+ //#endregion
640
+ //#region lib/types/tools/resource.js
641
+ /**
642
+ * The `library_textbook` tool (moved verbatim from tool-definition.ts; the shared
643
+ * helpers and schemas are in `tools/kit.ts`).
644
+ *
645
+ * @module @khorsheed/dsh-library/tools/resource
646
+ */
647
+ /** Build the `library_textbook` tool over the shared kit (the file keeps its v5 name). */
648
+ function libraryResourceTool(kit) {
649
+ const { service, projectOf, autoBind, scopeParams } = kit;
650
+ return defineTool({
651
+ name: "library_textbook",
652
+ description: "Shelve, correct, reorder or remove a textbook (anything with a contents) — in study order. origin: learner (their own) | picked (a book or course you chose; its path takes only the chapters the goal needs) | authored (you write it: one chapter per lesson, your 讲义 are its text). A real book: verify the edition and contents online and cite the page (tocSource { by: \"web\", url }) — search the title with its author or ISBN, narrowed with site: to catalog or publisher pages (site:book.douban.com, site:worldcat.org, the publisher's domain), one query per edition to tell apart, never a \" in a query; never ask for an upload. A correction (resourceId) sends only what changes. A web page a station cites is no textbook — the path names its url.",
653
+ parameters: {
654
+ ...scopeParams,
655
+ op: {
656
+ type: "string",
657
+ required: true,
658
+ description: "add | update | move | remove."
659
+ },
660
+ resourceId: {
661
+ type: "string",
662
+ description: "The textbook to update / move / remove."
663
+ },
664
+ title: {
665
+ type: "string",
666
+ description: "The exact edition's title."
667
+ },
668
+ origin: {
669
+ type: "string",
670
+ enum: [
671
+ "learner",
672
+ "picked",
673
+ "authored"
674
+ ]
675
+ },
676
+ toc: {
677
+ type: "json",
678
+ description: "The contents: one chapter per line in one string, or [\"…\", \"…\"]."
679
+ },
680
+ tocSource: {
681
+ type: "json",
682
+ description: "{ by: \"web\" | \"model\" | \"photo\", url? } — web needs the url."
683
+ },
684
+ seq: {
685
+ type: "integer",
686
+ description: "Its place in the study order, from 1."
687
+ },
688
+ bookKind: {
689
+ type: "string",
690
+ enum: ["knowledge", "material"],
691
+ description: "material only when the exact text is what is tested (exam papers, literature)."
692
+ },
693
+ edition: { type: "string" },
694
+ author: { type: "string" },
695
+ url: {
696
+ type: "string",
697
+ description: "A digital textbook's home page."
698
+ },
699
+ note: {
700
+ type: "string",
701
+ description: "One line on why it is here."
702
+ }
703
+ },
704
+ output: jsonOutput(),
705
+ isConcurrencySafe: () => false,
706
+ async execute(args, exec) {
707
+ const argsAny = args;
708
+ const namedId = pickString(args.resourceId, argsAny.bookId);
709
+ const op = args.op === "add" || args.op === "update" ? "create" : args.op;
710
+ if (op === "remove") {
711
+ if (namedId === void 0) throw new Error("library_textbook · op=remove takes the resourceId field — library_read · resources lists the ids");
712
+ return unwrap(await service.removeResource({
713
+ projectId: projectOf(args.project, exec, namedId),
714
+ resourceId: namedId
715
+ }));
716
+ }
717
+ if (op === "move") {
718
+ if (namedId === void 0 || typeof args.seq !== "number") throw new Error("library_textbook · op=move takes resourceId and seq (the textbook's new place, from 1)");
719
+ return { order: unwrap(await service.moveResource({
720
+ projectId: projectOf(args.project, exec, namedId),
721
+ resourceId: namedId,
722
+ seq: args.seq
723
+ })).resources.filter((resource) => resource.seq !== void 0).map((resource) => `${resource.seq}. ${resource.id} 《${resource.title}》`) };
724
+ }
725
+ if (op !== "create") throw new Error(`library_textbook needs op: add | update | move | remove — got ${JSON.stringify(args.op)}`);
726
+ const originArg = argsAny.origin ?? (argsAny.addedBy === "learner" ? "learner" : argsAny.addedBy === "coach" ? "picked" : void 0);
727
+ const origin = originArg === "learner" || originArg === "picked" || originArg === "authored" ? { origin: originArg } : {};
728
+ const projectId = projectOf(args.project, exec, namedId);
729
+ const from = payload(argsAny.from);
730
+ if (from !== void 0) {
731
+ const fromProject = pickString(from.project, from.projectId);
732
+ const fromResource = pickString(from.resource, from.resourceId, from.book, from.bookId);
733
+ if (fromProject === void 0 || fromResource === void 0) throw new Error("from is { project, resource } — the OTHER project's id and the entry's id there");
734
+ const cloned = unwrap(await service.putResource({
735
+ projectId,
736
+ title: "",
737
+ kind: "article",
738
+ ...namedId === void 0 ? {} : { resourceId: namedId },
739
+ from: {
740
+ projectId: fromProject,
741
+ resourceId: fromResource
742
+ }
743
+ }));
744
+ await autoBind(exec, {
745
+ projectId,
746
+ resourceId: cloned.resource.id
747
+ });
748
+ return cloned;
749
+ }
750
+ const tocList = payloadList(args.toc);
751
+ const sentRows = typeof args.toc === "string" && tocList === void 0 ? splitTocText(args.toc) : tocList ?? [];
752
+ const shelved = namedId !== void 0 && sentRows.length === 0 && pickString(args.url) === void 0 ? await service.getResource({
753
+ projectId,
754
+ resourceId: namedId
755
+ }) : void 0;
756
+ const kept = shelved?.ok === true && isThickResource(shelved.resource) ? shelved.resource : void 0;
757
+ const tocRows = sentRows.length > 0 ? sentRows : (kept?.toc ?? []).map((entry) => ({ ...entry }));
758
+ if (kept !== void 0) {
759
+ if (typeof args.title !== "string" || args.title.trim() === "") argsAny.title = kept.title;
760
+ if (argsAny.kind === void 0) argsAny.kind = kept.kind;
761
+ }
762
+ const linkKind = typeof argsAny.kind === "string" && argsAny.kind !== "book" && argsAny.kind !== "course" && LIBRARY_RESOURCE_KINDS.includes(argsAny.kind);
763
+ if (tocRows.length > 0 || !linkKind) {
764
+ if (typeof args.title !== "string" || args.title.trim() === "" || tocRows.length === 0) throw new Error(`a textbook takes: title, toc (one chapter per line), tocSource { by, url? }, origin (+ seq?, bookKind?, edition?, author?; resourceId to update one) — ${arrival("toc", args.toc)}`);
765
+ const toc = tocRows.map((row) => typeof row === "string" ? { title: row } : row);
766
+ const written = unwrap(await service.putResource({
767
+ projectId,
768
+ ...namedId === void 0 ? {} : { resourceId: namedId },
769
+ title: args.title,
770
+ kind: argsAny.kind === "course" ? "course" : "book",
771
+ ...typeof args.url === "string" ? { url: args.url } : {},
772
+ ...typeof args.note === "string" ? { note: args.note } : {},
773
+ ...origin,
774
+ ...typeof args.seq === "number" ? { seq: args.seq } : {},
775
+ toc,
776
+ tocSource: payload(args.tocSource) ?? kept?.tocSource ?? { by: "model" },
777
+ ...args.bookKind === "knowledge" || args.bookKind === "material" ? { bookKind: args.bookKind } : {},
778
+ ...typeof argsAny.short === "string" ? { short: argsAny.short } : {},
779
+ ...typeof args.author === "string" ? { author: args.author } : {},
780
+ ...typeof args.edition === "string" ? { edition: args.edition } : {},
781
+ ...typeof argsAny.isbn === "string" ? { isbn: argsAny.isbn } : {},
782
+ ...Array.isArray(payloadList(argsAny.extras)) ? { extras: payloadList(argsAny.extras) } : {}
783
+ }));
784
+ await autoBind(exec, {
785
+ projectId,
786
+ resourceId: written.resource.id
787
+ });
788
+ const rows = written.resource.toc?.length ?? 0;
789
+ return {
790
+ ...written,
791
+ contentsRows: rows,
792
+ ...rows < 3 ? { warning: `the contents has only ${rows} row(s) — one row per chapter is expected (station chapters count these rows). If the chapters got joined, resend toc with one chapter per line and resourceId ${written.resource.id}` } : {}
793
+ };
794
+ }
795
+ const title = pickString(args.title);
796
+ const url = pickString(args.url);
797
+ if (title === void 0 || url === void 0) throw new Error(`a link takes: title, url, kind ("article"|"video"|"repo"|"tool"|"hardware"), note? — got title ${title === void 0 ? "✗ MISSING" : "✓"} · url ${url === void 0 ? "✗ MISSING" : "✓"} (a textbook takes kind "book" | "course" + toc)`);
798
+ const added = unwrap(await service.putResource({
799
+ projectId,
800
+ ...namedId === void 0 ? {} : { resourceId: namedId },
801
+ title,
802
+ kind: linkKind ? argsAny.kind : "article",
803
+ url,
804
+ ...typeof args.note === "string" ? { note: args.note } : {},
805
+ ...origin
806
+ }));
807
+ await autoBind(exec, { projectId });
808
+ return added;
809
+ }
810
+ });
811
+ }
812
+ //#endregion
813
+ //#region lib/types/tools/roadmap.js
814
+ /**
815
+ * The `library_roadmap` tool (moved verbatim from tool-definition.ts; the shared
816
+ * helpers and schemas are in `tools/kit.ts`).
817
+ *
818
+ * @module @khorsheed/dsh-library/tools/roadmap
819
+ */
820
+ /** Build the `library_roadmap` tool over the shared kit. */
821
+ function libraryRoadmapTool(kit) {
822
+ const { service, projectOf, autoBind, textbookList } = kit;
823
+ return defineTool({
824
+ name: "library_roadmap",
825
+ description: "Draft one textbook's path (replaces its previous draft); the learner adopts it in the sidebar. One station = one lesson: 3–5 knowledge points, simple → hard. A real book's long chapter splits into several stations; a textbook you authored teaches one chapter per station.",
826
+ parameters: {
827
+ resource: {
828
+ type: "string",
829
+ required: true,
830
+ description: "The textbook."
831
+ },
832
+ nodes: {
833
+ ...NODES_SCHEMA,
834
+ required: true,
835
+ description: "The stations, in study order."
836
+ },
837
+ note: {
838
+ type: "string",
839
+ description: "One line of pacing for the draft card."
840
+ }
841
+ },
842
+ output: jsonOutput(),
843
+ isConcurrencySafe: () => false,
844
+ async execute(args, exec) {
845
+ const list = payloadList(args.nodes);
846
+ if (list === void 0) throw new Error(`library_roadmap takes resource and nodes: [{ id, title, chapters: "3-4", points: [{ id, title }] (3–5), goals?, acceptance? }] — ${arrival("nodes", args.nodes)}`);
847
+ const argsAny = args;
848
+ const resourceId = pickString(args.resource, argsAny.book);
849
+ if (resourceId === void 0) throw new Error(`a path belongs to one textbook — pass resource${textbookList(exec) || " (shelve the textbook first with library_textbook)"}`);
850
+ const adjusted = [];
851
+ const missingPoints = [];
852
+ const nodes = list.map((node) => {
853
+ const { chapters, also, chapterRanges, covers, kind, gate, points, ...rest } = node;
854
+ const gateValue = typeof gate === "string" ? gate : kind === "practice" ? "acceptance" : typeof kind === "string" ? kind : "quiz";
855
+ if (gateValue === "quiz" && Array.isArray(rest.acceptance) && rest.acceptance.length > 0) {
856
+ const lines = rest.acceptance.flatMap((line) => typeof line === "string" ? [line] : typeof payload(line)?.line === "string" ? [String(payload(line).line)] : []);
857
+ rest.goals = [...Array.isArray(rest.goals) ? rest.goals : typeof rest.goals === "string" ? [rest.goals] : [], ...lines];
858
+ delete rest.acceptance;
859
+ }
860
+ if (gateValue === "quiz" && (!Array.isArray(points) || points.length === 0)) missingPoints.push(String(rest.id ?? rest.title ?? "?"));
861
+ const keepPoints = gateValue === "quiz" || points === void 0 || Array.isArray(points) && points.length === 0;
862
+ if (!keepPoints) adjusted.push(`points dropped on ${String(rest.title ?? rest.id)} — a ${String(gateValue)} station has no quiz; what it checks belongs in ${gateValue === "acceptance" ? "acceptance lines" : "goals"}`);
863
+ const main = [];
864
+ const spelled = typeof chapters === "string" && chapters.trim() !== "" ? chaptersLocator(chapters) : void 0;
865
+ if (spelled?.rest !== void 0) adjusted.push(`${String(rest.title ?? rest.id)}: "${spelled.rest}" in chapters is not a row number — kept as a side reference (topic: ${spelled.rest})`);
866
+ const locator = typeof chapters === "number" ? String(chapters) : spelled !== void 0 ? spelled.locator : Array.isArray(chapters) ? chapters.filter((entry) => typeof entry === "number" || typeof entry === "string").join(",") : Array.isArray(chapterRanges) ? formatLocator(chapterRanges.flatMap((range) => {
867
+ const row = payload(range);
868
+ return typeof row?.from === "number" && typeof row.to === "number" ? [{
869
+ from: row.from,
870
+ to: row.to
871
+ }] : [];
872
+ })) : void 0;
873
+ if (locator !== void 0 && locator !== "") main.push({
874
+ ref: resourceId,
875
+ locator
876
+ });
877
+ const side = [
878
+ ...Array.isArray(covers) ? covers : [],
879
+ ...Array.isArray(also) ? also : typeof also === "string" ? [also] : [],
880
+ ...spelled?.rest === void 0 ? [] : [`topic: ${spelled.rest}`]
881
+ ].map((cover) => normalizeCoverArg(cover) ?? cover);
882
+ const all = main.length === 0 && side.length === 0 ? [{ ref: resourceId }] : [...main, ...side];
883
+ return {
884
+ ...rest,
885
+ ...gateValue === void 0 ? {} : { gate: gateValue },
886
+ ...keepPoints && points !== void 0 ? { points } : {},
887
+ covers: all
888
+ };
889
+ });
890
+ if (missingPoints.length > 0) throw new Error(`every station needs its 3–5 knowledge points (points: [{ id, title }]) — the chapter quiz asks them: ${missingPoints.join(", ")}`);
891
+ const projectId = projectOf(argsAny.project, exec, resourceId);
892
+ const replaced = unwrap(await service.getProject({ id: projectId })).project.resources.find((row) => row.id === resourceId)?.hasDraft === true;
893
+ const drafted = unwrap(await service.draftRoadmap({
894
+ projectId,
895
+ resourceId,
896
+ nodes,
897
+ ...typeof argsAny.passRatio === "number" ? { passRatio: argsAny.passRatio } : {},
898
+ ...typeof args.note === "string" ? { note: args.note } : {}
899
+ }));
900
+ await autoBind(exec, {
901
+ projectId,
902
+ resourceId
903
+ });
904
+ const after = unwrap(await service.getProject({ id: projectId }));
905
+ const textbook = after.project.resources.find((row) => row.id === resourceId);
906
+ return {
907
+ ok: true,
908
+ placedOn: `${textbook?.seq ?? "-"}. 《${textbook?.title ?? resourceId}》 (${resourceId})`,
909
+ stations: drafted.draft.nodes.length,
910
+ ...replaced ? { replacedDraft: true } : {},
911
+ waitingForAdoption: after.project.resources.filter((row) => row.hasDraft).map((row) => `${row.seq ?? "-"}. 《${row.title}》 ${row.draftTotal ?? 0} stations`),
912
+ ...adjusted.length === 0 ? {} : { adjusted },
913
+ next: "Describe the path to the learner in two lines and ask them to review and adopt it in the sidebar (采用). Then stop: before they adopt it, write no 讲义, teach nothing, set no quiz."
914
+ };
915
+ }
916
+ });
917
+ }
918
+ //#endregion
919
+ //#region lib/types/tools/notes.js
920
+ /**
921
+ * The `library_notes` tool (moved verbatim from tool-definition.ts; the shared
922
+ * helpers and schemas are in `tools/kit.ts`).
923
+ *
924
+ * @module @khorsheed/dsh-library/tools/notes
925
+ */
926
+ /** Build the `library_notes` tool over the shared kit. */
927
+ function libraryNotesTool(kit) {
928
+ const { service, projectOf, resourceOf, textbookParams } = kit;
929
+ return defineTool({
930
+ name: "library_notes",
931
+ description: "Write or rewrite a station's 讲义 (markdown, math in $…$), for THIS learner, when they start the station. The 讲义 are the lesson itself — the learner learns from them without leaving the page, whatever the textbook: open with the core question and 「你将学到」 (the points, numbered) and one or two warm-up questions; then one ## section per knowledge point, headed by its title — explain it plainly, give a concrete example or the steps to try, a common mistake, and a 思考题; then 小结. Outside material (a manual page, a video, the learner's book pages) goes only in a final ## 参考 section, never as the lesson. No change logs or internal words.",
932
+ parameters: {
933
+ ...textbookParams,
934
+ node: {
935
+ type: "string",
936
+ required: true,
937
+ description: "The station id."
938
+ },
939
+ text: {
940
+ type: "string",
941
+ required: true,
942
+ description: "The 讲义, markdown."
943
+ }
944
+ },
945
+ output: jsonOutput(),
946
+ isConcurrencySafe: () => false,
947
+ async execute(args, exec) {
948
+ const resourceId = resourceOf(args, exec);
949
+ const written = unwrap(await service.putNotes({
950
+ projectId: projectOf(args.project, exec, resourceId),
951
+ resourceId,
952
+ nodeId: String(args.node),
953
+ text: String(args.text)
954
+ }));
955
+ return {
956
+ ...written,
957
+ next: written.notes.revisions === 1 ? "Tell the learner in one line that the 讲义 are ready in the sidebar, then wait — they read first and ask." : "Tell the learner in one line what changed in the 讲义."
958
+ };
959
+ }
960
+ });
961
+ }
962
+ //#endregion
963
+ //#region lib/types/tools/page.js
964
+ /**
965
+ * The `library_page` tool (moved verbatim from tool-definition.ts; the shared
966
+ * helpers and schemas are in `tools/kit.ts`).
967
+ *
968
+ * @module @khorsheed/dsh-library/tools/page
969
+ */
970
+ /** Build the `library_page` tool over the shared kit. */
971
+ function libraryPageTool(kit) {
972
+ const { service, images, projectOf, resourceOf, textbookParams } = kit;
973
+ return defineTool({
974
+ name: "library_page",
975
+ description: "Record a photographed page in one call: its printed text and every mark the learner made (each certain mark becomes a card). Omit attachment for the latest photo. If you cannot see images, say so instead.",
976
+ parameters: {
977
+ ...textbookParams,
978
+ text: {
979
+ type: "string",
980
+ required: true,
981
+ description: "The printed text."
982
+ },
983
+ marks: {
984
+ oneOf: [{
985
+ type: "array",
986
+ items: {
987
+ type: "object",
988
+ additionalProperties: true,
989
+ properties: {
990
+ text: {
991
+ type: "string",
992
+ description: "As printed."
993
+ },
994
+ sentence: { type: "string" },
995
+ handNote: {
996
+ type: "string",
997
+ description: "The learner's handwriting beside it."
998
+ },
999
+ gloss: {
1000
+ type: "string",
1001
+ description: "In-context meaning."
1002
+ },
1003
+ uncertain: {
1004
+ type: "boolean",
1005
+ description: "You could not tell (kept, not carded)."
1006
+ }
1007
+ }
1008
+ }
1009
+ }, {
1010
+ type: "string",
1011
+ description: "The same list as JSON text."
1012
+ }],
1013
+ required: true,
1014
+ description: "Every mark on the page."
1015
+ },
1016
+ attachment: {
1017
+ type: "string",
1018
+ description: "The image id (omit for the latest photo in this conversation)."
1019
+ },
1020
+ label: {
1021
+ type: "string",
1022
+ description: "What the learner calls the page (「p.16」)."
1023
+ },
1024
+ node: {
1025
+ type: "string",
1026
+ description: "The station id, when the page belongs to one."
1027
+ }
1028
+ },
1029
+ output: jsonOutput(),
1030
+ isConcurrencySafe: () => false,
1031
+ async execute(args, exec) {
1032
+ const marks = payloadList(args.marks);
1033
+ if (marks === void 0) throw new Error(`library_page takes: text (the page's printed text), marks: [{ text, sentence?, handNote?, gloss?, uncertain? }] (+ attachment?, label?, node?) — ${arrival("marks", args.marks)}`);
1034
+ const attachment = images.resolve(sessionIdOf(exec), typeof args.attachment === "string" ? args.attachment : void 0);
1035
+ if (attachment === void 0) throw new Error("no photo to record: this conversation has no image the library saw arrive (or the id names none) — ask the learner to send the page photo here, then record it");
1036
+ const pageResource = resourceOf(args, exec);
1037
+ return unwrap(await service.recordPage({
1038
+ projectId: projectOf(args.project, exec, pageResource),
1039
+ resourceId: pageResource,
1040
+ ...typeof args.node === "string" && args.node !== "" ? { nodeId: args.node } : {},
1041
+ ...typeof args.label === "string" ? { label: args.label } : {},
1042
+ attachment,
1043
+ text: args.text,
1044
+ marks
1045
+ }));
1046
+ }
1047
+ });
1048
+ }
1049
+ //#endregion
1050
+ //#region lib/types/tools/cards.js
1051
+ /**
1052
+ * The `library_cards` tool (moved verbatim from tool-definition.ts; the shared
1053
+ * helpers and schemas are in `tools/kit.ts`).
1054
+ *
1055
+ * @module @khorsheed/dsh-library/tools/cards
1056
+ */
1057
+ /** Build the `library_cards` tool over the shared kit. */
1058
+ function libraryCardsTool(kit) {
1059
+ const { service, bindingOf, projectOf, scopeParams } = kit;
1060
+ return defineTool({
1061
+ name: "library_cards",
1062
+ description: "The learner is stuck on the material (mixes it up, asks the same thing again) → note it now, then say in one line what you noted. Subject projects: { point, gloss } — the note lands on the current station's knowledge point. Language projects: { term, gloss, sentence? } becomes a word card; a near-duplicate comes back pending — resend it with mergeInto or asNew. Not a weak point: curiosity, your own slip (fix the quiz), or feedback on a question or the tools.",
1063
+ parameters: {
1064
+ ...scopeParams,
1065
+ resource: {
1066
+ type: "string",
1067
+ description: "The textbook (defaults to the binding's)."
1068
+ },
1069
+ cards: {
1070
+ oneOf: [{
1071
+ type: "array",
1072
+ items: {
1073
+ type: "object",
1074
+ additionalProperties: true,
1075
+ properties: {
1076
+ point: {
1077
+ type: "string",
1078
+ description: "Subject: the knowledge point id."
1079
+ },
1080
+ term: {
1081
+ type: "string",
1082
+ description: "What they stuck on, in a few words."
1083
+ },
1084
+ gloss: {
1085
+ type: "string",
1086
+ description: "What they got wrong, or the meaning."
1087
+ },
1088
+ sentence: {
1089
+ type: "string",
1090
+ description: "Language: the sentence it came from."
1091
+ },
1092
+ mergeInto: { type: "string" },
1093
+ asNew: { type: "boolean" }
1094
+ }
1095
+ }
1096
+ }, {
1097
+ type: "string",
1098
+ description: "The same list as JSON text."
1099
+ }],
1100
+ required: true
1101
+ }
1102
+ },
1103
+ output: jsonOutput(),
1104
+ isConcurrencySafe: () => false,
1105
+ async execute(args, exec) {
1106
+ const list = payloadList(args.cards);
1107
+ if (list === void 0) throw new Error(`library_cards takes cards: [{ point, gloss }] (subject) or [{ term, gloss, sentence? }] (language) — ${arrival("cards", args.cards)}`);
1108
+ const sessionId = sessionIdOf(exec);
1109
+ const resource = pickString(args.resource, args.book) ?? bindingOf(exec)?.resourceId;
1110
+ const projectId = projectOf(args.project, exec, resource);
1111
+ const project = profileOf(unwrap(await service.getProject({ id: projectId })).project.template);
1112
+ const cards = list.map((card) => {
1113
+ const source = payload(card.source) ?? { type: "chat" };
1114
+ const context = payload(card.context) ?? (pickString(card.sentence) === void 0 ? void 0 : { sentence: card.sentence });
1115
+ return {
1116
+ ...card,
1117
+ kind: card.kind === "word" || card.kind === "concept" ? card.kind : project.markKind,
1118
+ ...card.weaken === void 0 && source.type === "chat" ? { weaken: true } : {},
1119
+ source: {
1120
+ ...source,
1121
+ ...source.type === "chat" && source.ref === void 0 && sessionId !== void 0 ? { ref: sessionId } : {}
1122
+ },
1123
+ ...context === void 0 ? {} : { context: {
1124
+ sentence: String(context.sentence ?? ""),
1125
+ ...typeof context.node === "string" ? { nodeId: context.node } : {}
1126
+ } }
1127
+ };
1128
+ });
1129
+ const pointed = project.pointsDriven ? list.filter((card) => pickString(card.point) !== void 0) : [];
1130
+ const noted = [];
1131
+ for (const card of pointed) {
1132
+ const station = pickString(card.node, payload(card.context)?.node) ?? (sessionId === void 0 ? void 0 : service.bindingSummary(sessionId, 0).station?.id);
1133
+ if (station === void 0) throw new Error("a point note needs its station: node (the per-turn context names the current station and its points)");
1134
+ const term = pickString(card.term);
1135
+ const text = [term, pickString(card.gloss, card.userGloss)].filter(Boolean).join(" — ");
1136
+ const written = unwrap(await service.notePoint({
1137
+ projectId,
1138
+ ...resource === void 0 ? {} : { resourceId: resource },
1139
+ nodeId: station,
1140
+ pointId: pickString(card.point),
1141
+ text: text === "" ? String(card.gloss ?? "") : text,
1142
+ ...term === void 0 ? {} : { term },
1143
+ ...sessionId === void 0 ? {} : { session: sessionId }
1144
+ }));
1145
+ noted.push({
1146
+ point: written.point.title,
1147
+ state: written.point.state,
1148
+ ...written.changes.length === 0 ? {} : { changed: written.changes.map((change) => `${change.title}: ${change.from} → ${change.to}`) }
1149
+ });
1150
+ }
1151
+ const rest = cards.filter((_card, index) => !pointed.includes(list[index]));
1152
+ if (project.pointsDriven) {
1153
+ const loose = rest.filter((card) => card.source?.type === "chat");
1154
+ if (loose.length > 0) {
1155
+ const points = sessionId === void 0 ? [] : service.bindingSummary(sessionId, 0).station?.mastery.points ?? [];
1156
+ throw new Error(`in a subject project a weak point from the conversation lands on the station's knowledge point — add point (+ node) to: ${loose.map((card) => card.term ?? "?").join(", ")}${points.length === 0 ? "" : `; this station's points: ${points.map((point) => `${point.id} (${point.title})`).join(", ")}`}. Record only what the LEARNER got wrong about the material — never your own slip (a wrong answer key: fix the quiz) and never feedback on a question or the tools. If none of that applies, record nothing.`);
1157
+ }
1158
+ }
1159
+ return {
1160
+ ...(rest.length === 0 ? void 0 : unwrap(await service.writeCards({
1161
+ projectId,
1162
+ ...resource === void 0 ? {} : { resourceId: resource },
1163
+ cards: rest
1164
+ }))) ?? { ok: true },
1165
+ ...noted.length === 0 ? {} : { pointNotes: noted }
1166
+ };
1167
+ }
1168
+ });
1169
+ }
1170
+ //#endregion
1171
+ //#region lib/types/tools/quiz.js
1172
+ /**
1173
+ * The `library_quiz` tool (moved verbatim from tool-definition.ts; the shared
1174
+ * helpers and schemas are in `tools/kit.ts`).
1175
+ *
1176
+ * @module @khorsheed/dsh-library/tools/quiz
1177
+ */
1178
+ /** Build the `library_quiz` tool over the shared kit. */
1179
+ function libraryQuizTool(kit) {
1180
+ const { service, bindingOf, projectOf, autoBind, scopeParams } = kit;
1181
+ return defineTool({
1182
+ name: "library_quiz",
1183
+ description: "Store a quiz, then reply with one line — the learner answers in the sidebar. node = the station it tests (omit: a whole-textbook quiz; resource null: a free quiz). focus is the learner's choice: mine = only their cards; book = the chapter only; mixed = their cards, then the chapter. Every question names its point; quiz weak and untested points first, then practicing ones. Solve each question again before storing; fix any disagreement. choice: answer = the right option verbatim. fill: a short value (1/2), several blanks comma-separated, alternatives \" / \". free: say what a good answer covers. Math only in $…$. The prompt carries no kind label or chapter reference. A material textbook needs quote (the page text). Replace voided (「这题有问题」) questions on the same points.",
1184
+ parameters: {
1185
+ ...scopeParams,
1186
+ resource: {
1187
+ type: "json",
1188
+ description: "The textbook (defaults to the binding's); null = a free quiz."
1189
+ },
1190
+ node: {
1191
+ type: "string",
1192
+ description: "The quiz-gated station it gates."
1193
+ },
1194
+ focus: {
1195
+ type: "string",
1196
+ enum: [
1197
+ "mine",
1198
+ "book",
1199
+ "mixed"
1200
+ ]
1201
+ },
1202
+ questions: {
1203
+ ...QUESTIONS_SCHEMA,
1204
+ required: true,
1205
+ description: "The questions."
1206
+ }
1207
+ },
1208
+ output: jsonOutput(),
1209
+ isConcurrencySafe: () => false,
1210
+ async execute(args, exec) {
1211
+ const questions = payloadList(args.questions);
1212
+ if (questions === void 0) throw new Error(`library_quiz takes questions: [{ point, kind: "choice"|"fill"|"free", prompt, options?, answer, explanation?, quote? }] (+ node?, focus?) — ${arrival("questions", args.questions)}`);
1213
+ const node = typeof args.node === "string" && args.node !== "" ? args.node : void 0;
1214
+ const argsAny = args;
1215
+ const resourceId = args.resource === null || argsAny.book === null ? void 0 : pickString(args.resource, argsAny.book) ?? bindingOf(exec)?.resourceId;
1216
+ const cleaned = questions.map((raw, index) => {
1217
+ const text = (value) => typeof value === "string" ? value : typeof value === "number" ? String(value) : "";
1218
+ let next = {
1219
+ ...raw,
1220
+ id: text(raw.id) === "" ? `q${index + 1}` : text(raw.id),
1221
+ kind: text(raw.kind),
1222
+ prompt: text(raw.prompt),
1223
+ answer: text(raw.answer)
1224
+ };
1225
+ if (next.options !== void 0) {
1226
+ const options = normalizeQuizOptions(next.options);
1227
+ if (options !== void 0) next = {
1228
+ ...next,
1229
+ options
1230
+ };
1231
+ }
1232
+ return normalizeQuestionAxes(next);
1233
+ });
1234
+ const projectId = projectOf(args.project, exec, resourceId);
1235
+ const written = unwrap(await service.putQuiz({
1236
+ projectId,
1237
+ quiz: {
1238
+ ...args.focus === "mine" || args.focus === "book" || args.focus === "mixed" ? { focus: args.focus } : {},
1239
+ scope: {
1240
+ ...resourceId === void 0 ? {} : { resourceId },
1241
+ ...node === void 0 ? {} : { nodeId: node }
1242
+ },
1243
+ questions: cleaned,
1244
+ ...Array.isArray(argsAny.addPoints) ? { addPoints: argsAny.addPoints } : {}
1245
+ }
1246
+ }));
1247
+ await autoBind(exec, {
1248
+ projectId,
1249
+ ...resourceId === void 0 ? {} : { resourceId }
1250
+ });
1251
+ return written;
1252
+ }
1253
+ });
1254
+ }
1255
+ //#endregion
1256
+ //#region lib/types/tools/binding.js
1257
+ /**
1258
+ * The `library_binding` tool (moved verbatim from tool-definition.ts; the shared
1259
+ * helpers and schemas are in `tools/kit.ts`).
1260
+ *
1261
+ * @module @khorsheed/dsh-library/tools/binding
1262
+ */
1263
+ /** Build the `library_binding` tool over the shared kit. */
1264
+ function libraryBindingTool(kit) {
1265
+ const { service, textbookList } = kit;
1266
+ return defineTool({
1267
+ name: "library_binding",
1268
+ description: "Bind this session to a project (and optionally a textbook) — every tool's default. Writes bind an unbound session by themselves; use this to switch, or unbind: true.",
1269
+ parameters: {
1270
+ project: { type: "string" },
1271
+ resource: {
1272
+ type: "string",
1273
+ description: "A textbook in it."
1274
+ },
1275
+ unbind: { type: "boolean" }
1276
+ },
1277
+ output: jsonOutput(),
1278
+ isConcurrencySafe: () => false,
1279
+ async execute(args, exec) {
1280
+ const sessionId = sessionIdOf(exec);
1281
+ if (sessionId === void 0) throw new Error("binding needs a session — this call came from none");
1282
+ const argsAny = args;
1283
+ if (args.unbind === true || argsAny.binding === null) return unwrap(await service.setBinding(sessionId, null));
1284
+ const body = payload(argsAny.binding) ?? {};
1285
+ const projectId = pickString(body.project, body.projectId, args.project);
1286
+ const resourceId = pickString(body.resource, body.resourceId, body.book, body.bookId, args.resource, args.book);
1287
+ if (projectId === void 0) throw new Error("binding needs the project: { project, resource? } (library_read · projects lists them); unbind: true to unbind");
1288
+ if (resourceId !== void 0 && !(await service.getResource({
1289
+ projectId,
1290
+ resourceId
1291
+ })).ok) return {
1292
+ ...unwrap(await service.setBinding(sessionId, { projectId })),
1293
+ note: `no resource ${JSON.stringify(resourceId)} in this project — bound the project only. Resource ids are minted (r_…); registering the textbook (library_textbook) binds this session to it by itself${textbookList(exec)}`
1294
+ };
1295
+ return unwrap(await service.setBinding(sessionId, {
1296
+ projectId,
1297
+ ...resourceId === void 0 ? {} : { resourceId }
1298
+ }));
1299
+ }
1300
+ });
1301
+ }
1302
+ //#endregion
1303
+ //#region lib/types/tools/assess.js
1304
+ /**
1305
+ * The `library_assess` tool (moved verbatim from tool-definition.ts; the shared
1306
+ * helpers and schemas are in `tools/kit.ts`).
1307
+ *
1308
+ * @module @khorsheed/dsh-library/tools/assess
1309
+ */
1310
+ /** A passed station's result carries the next step: name what follows and ask (the learner starts it). */
1311
+ function withNext(result) {
1312
+ if (result.completed === void 0) return result;
1313
+ return {
1314
+ ...result,
1315
+ next: `Congratulate the learner, name the next station${result.unlocked !== void 0 && result.unlocked.length > 0 ? ` (${result.unlocked.join(", ")})` : ""} and ask whether to start it — start only when they say so.`
1316
+ };
1317
+ }
1318
+ /** Build the `library_assess` tool over the shared kit. */
1319
+ function libraryAssessTool(kit) {
1320
+ const { service, projectOf } = kit;
1321
+ return defineTool({
1322
+ name: "library_assess",
1323
+ description: "Record a graded sitting after judging it in the chat: one verdict per question. The stored answer can be wrong — judge by the question, and say so when it was. A hands-on station: pass node instead of quiz; each task is a line id (L1, L2…). Then tell the learner what moved and what unlocked.",
1324
+ parameters: {
1325
+ project: {
1326
+ type: "string",
1327
+ description: "Project id (defaults to the binding)."
1328
+ },
1329
+ quiz: {
1330
+ type: "string",
1331
+ description: "The quiz id."
1332
+ },
1333
+ node: {
1334
+ type: "string",
1335
+ description: "A hands-on station, instead of quiz."
1336
+ },
1337
+ resource: {
1338
+ type: "string",
1339
+ description: "Its textbook (defaults to wherever the node is)."
1340
+ },
1341
+ verdicts: {
1342
+ type: "array",
1343
+ required: true,
1344
+ items: {
1345
+ type: "object",
1346
+ additionalProperties: true,
1347
+ properties: {
1348
+ task: {
1349
+ type: "string",
1350
+ description: "The question id, or the line id (L1…)."
1351
+ },
1352
+ pass: { type: "boolean" },
1353
+ ratio: {
1354
+ type: "number",
1355
+ description: "Partial credit 0..1."
1356
+ },
1357
+ why: {
1358
+ type: "string",
1359
+ description: "One line on the judgment."
1360
+ },
1361
+ void: {
1362
+ type: "boolean",
1363
+ description: "The learner flagged it broken."
1364
+ },
1365
+ missedCard: {
1366
+ type: "object",
1367
+ additionalProperties: false,
1368
+ description: "Language projects: the word they missed.",
1369
+ properties: {
1370
+ term: { type: "string" },
1371
+ gloss: { type: "string" }
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1377
+ },
1378
+ output: jsonOutput(),
1379
+ isConcurrencySafe: () => false,
1380
+ async execute(args, exec) {
1381
+ if (!Array.isArray(args.verdicts) || args.verdicts.length === 0) throw new Error("verdicts is required — one per question: [{ task: <question id>, pass, why, ratio?, void? }]");
1382
+ const verdicts = args.verdicts.map((verdict) => {
1383
+ const why = pickString(verdict.why, verdict.criterion, verdict.evidence) ?? "";
1384
+ return {
1385
+ ...verdict,
1386
+ criterion: pickString(verdict.criterion) ?? why,
1387
+ evidence: pickString(verdict.evidence) ?? why,
1388
+ by: verdict.by === "self" || verdict.by === "user" ? verdict.by : "llm"
1389
+ };
1390
+ });
1391
+ const node = pickString(args.node);
1392
+ if (node !== void 0) {
1393
+ const resource = pickString(args.resource, args.book);
1394
+ return withNext(unwrap(await service.putAttempt({
1395
+ projectId: projectOf(args.project, exec, resource),
1396
+ nodeId: node,
1397
+ ...resource === void 0 ? {} : { resourceId: resource },
1398
+ verdicts
1399
+ })));
1400
+ }
1401
+ const quiz = pickString(args.quiz);
1402
+ if (quiz === void 0) throw new Error("quiz is the id library_quiz returned — or pass node (an acceptance station's id) for acceptance");
1403
+ return withNext(unwrap(await service.putAttempt({
1404
+ projectId: projectOf(args.project, exec),
1405
+ assessmentId: quiz,
1406
+ verdicts
1407
+ })));
1408
+ }
1409
+ });
1410
+ }
1411
+ //#endregion
1412
+ //#region lib/types/tool-definition.js
1413
+ /** The names this module registers, in registration (workflow) order. */
1414
+ const LIBRARY_TOOL_NAMES = [
1415
+ "library_read",
1416
+ "library_project",
1417
+ "library_textbook",
1418
+ "library_roadmap",
1419
+ "library_notes",
1420
+ "library_page",
1421
+ "library_cards",
1422
+ "library_quiz",
1423
+ "library_binding",
1424
+ "library_assess"
1425
+ ];
1426
+ /**
1427
+ * Build the tools. Creating them is the package's `./tool` row, which applies
1428
+ * the origin tag; this module never registers.
1429
+ * @param service - the library service the adapters translate to.
1430
+ * @param images - the session image references the tool row remembers (a
1431
+ * page's photo is resolved from them, never trusted to the model's copy).
1432
+ * @returns the definitions, untagged and unregistered, in workflow order.
1433
+ */
1434
+ function libraryToolDefinitions(service, images = new ImageRefCache()) {
1435
+ const kit = createToolKit(service, images);
1436
+ return [
1437
+ libraryReadTool(kit),
1438
+ libraryProjectTool(kit),
1439
+ libraryResourceTool(kit),
1440
+ libraryRoadmapTool(kit),
1441
+ libraryNotesTool(kit),
1442
+ libraryPageTool(kit),
1443
+ libraryCardsTool(kit),
1444
+ libraryQuizTool(kit),
1445
+ libraryBindingTool(kit),
1446
+ libraryAssessTool(kit)
1447
+ ];
1448
+ }
1449
+ //#endregion
1450
+ //#region lib/types/tool.js
1451
+ const PACKAGE_NAME = "@khorsheed/dsh-library";
1452
+ /**
1453
+ * Tag the model-visible tools with their origin (AGENTS.md § Tool origin
1454
+ * tagging; seam S12): the capability catalog reads this `Symbol.for`-keyed
1455
+ * tag back through `ctx.tools.get()`, so the library tools attribute to THIS
1456
+ * package (the row the preset mounts). The tag is host-side only and never
1457
+ * travels on the model wire. Inlined (the eval precedent) — importing the
1458
+ * helper from capability-catalog would be a cross-package edge.
1459
+ */
1460
+ const definePluginTool = (def) => Object.assign(def, { [Symbol.for("dsh.tool.origin")]: {
1461
+ channel: "plugin",
1462
+ owner: PACKAGE_NAME
1463
+ } });
1464
+ const Config = z.object({
1465
+ tools: z.union([z.const("all"), z.const("none")]).default("all"),
1466
+ hideSections: z.array(z.string()).default([]),
1467
+ denyTools: z.array(z.string()).default([])
1468
+ });
1469
+ /** Cordis plugin name used by loader diagnostics. */
1470
+ const name = "library-tool";
1471
+ /**
1472
+ * The `library` core is a declared inject (the owning-family companion
1473
+ * exception to the community-service probe rule): a preset's standing scope
1474
+ * mounts at registry-activation time, BEFORE the profile's later bundle rows
1475
+ * provide the core, so a one-shot ctx.get probe at apply saw ABSENT there and
1476
+ * nothing re-ran the row (rc.1 boot order; 3080 production 2026-09-27). The
1477
+ * declared inject pends the row until the core provides, then the body
1478
+ * applies. The tools registry and the prompt assembly still join through
1479
+ * deferred injection so their mount order cannot strand the registrations.
1480
+ */
1481
+ const inject = ["library"];
1482
+ /**
1483
+ * The coach's guidance, registered beside the tools it describes (order 114,
1484
+ * the eval line). Per-`what` payloads are in the tool descriptions.
1485
+ */
1486
+ const LIBRARY_PROMPT = `The library holds the learner's projects, textbooks, paths and quizzes — record them there, never only in the chat. A refusal says what to fix: fix it and resend. Quote with 「」 inside any text you pass.
1487
+
1488
+ The learner leads; follow this order and stop where it says:
1489
+ 1. A goal → ask one or two questions (level, direction), then library_project.
1490
+ 2. Textbooks → library_textbook: the learner's book (verify edition and contents online), one you pick, or one you author.
1491
+ 3. A path → library_roadmap. Then describe it in two lines and STOP: the learner reviews and adopts it in the sidebar. Before that: no 讲义, no teaching, no quiz.
1492
+ 4. A station starts only when the learner says so (「开始学这一站」 or naming it). Then write its 讲义 for this learner (library_notes), say in one line that they are ready, and wait: the learner reads first and asks. Answer in the chat; if a question shows the 讲义 missed or got something wrong, fix that section.
1493
+ 5. Stuck on the material → library_cards on that point. A quiz when asked → library_quiz, then one line. Grade → library_assess.
1494
+ 6. A station passed → name the next one and ask whether to start it.
1495
+
1496
+ The per-turn "Library:" context is the current state; trust it over the chat.`;
1497
+ /** The bound-context line's order: right after the sandbox / approval snapshots. */
1498
+ const CONTEXT_ORDER = 650;
1499
+ /** How many weak cards the per-turn context names. */
1500
+ const CONTEXT_CARDS = 12;
1501
+ /**
1502
+ * Render the per-turn context for one session: its binding and the weakest
1503
+ * cards there. Kept short — it rides every turn.
1504
+ */
1505
+ function renderBindingContext(service, sessionId) {
1506
+ const summary = service.bindingSummary(sessionId, CONTEXT_CARDS);
1507
+ if (summary.binding === void 0) return "Library: this session is not bound to a project yet.";
1508
+ if (summary.project === void 0) return "Library: this session's bound project no longer exists — ask the learner which one to use.";
1509
+ const { project, resource, textbooks, weakCards } = summary;
1510
+ const lines = [`Library: bound to project 「${project.name}」 (${project.id}, template ${project.template})` + (resource === void 0 ? "." : `, textbook 《${resource.title}》 (${resource.id}).`)];
1511
+ if (summary.binding.previousProjectId !== void 0 && summary.binding.previousProjectId !== project.id) {
1512
+ const previous = service.projectName(summary.binding.previousProjectId);
1513
+ lines.push(`This session worked on another project${previous === void 0 ? "" : ` 「${previous}」`} before switching here: nothing said about it is 「${project.name}」's material — never quiz, teach or note from it here.`);
1514
+ }
1515
+ if (textbooks.length === 0) lines.push("Textbooks: none yet — shelve them first (library_textbook).");
1516
+ else {
1517
+ lines.push("Textbooks (study order):");
1518
+ for (const textbook of textbooks) {
1519
+ const state = textbook.roadmap === void 0 ? textbook.hasDraft ? `draft ${textbook.draftTotal ?? 0} stations, waiting for the learner to adopt` : "no path yet" : `path adopted, ${textbook.roadmap.done}/${textbook.roadmap.total} done` + (textbook.current === void 0 ? "" : `, current: ${textbook.current.title} (${textbook.current.id})`) + (textbook.hasDraft ? `; a new draft (${textbook.draftTotal ?? 0} stations) waits to replace it` : "");
1520
+ lines.push(` ${textbook.seq ?? "-"}. 《${textbook.title}》 (${textbook.id}) ${state}`);
1521
+ }
1522
+ }
1523
+ const profile = profileOf(project.template);
1524
+ if (profile.pointsDriven && summary.station !== void 0) {
1525
+ const { station } = summary;
1526
+ lines.push(`Current station ${station.title} (${station.id}) — its points:`);
1527
+ for (const point of station.mastery.points) {
1528
+ const reason = masteryReason(point);
1529
+ lines.push(` - ${point.title} (${point.id}): ${point.state}${reason === "" ? "" : ` — ${reason}`}`);
1530
+ }
1531
+ if (station.mastery.open.length > 0) lines.push(`Open misses here: ${station.mastery.open.length} (the learner retakes them in the sidebar; they count for points, not for passing).`);
1532
+ }
1533
+ if (weakCards.length > 0) lines.push(`Weak cards: ${weakCards.map((card) => `${card.term} [${card.weakness}]`).join("; ")}.`);
1534
+ lines.push(`Subject: ${profile.coachHint}`);
1535
+ return lines.join("\n");
1536
+ }
1537
+ /**
1538
+ * Plugin body: register the three tools, their guidance, and the per-turn
1539
+ * bound context; quiet the scope when configured. The declared `library`
1540
+ * inject pends the row until the core provides; the in-body guard stays as
1541
+ * the defensive direct-call path.
1542
+ * @param ctx - Cordis context (the preset's agent-plane mount).
1543
+ * @param config - validated plugin config.
1544
+ */
1545
+ function apply(ctx, config = {}) {
1546
+ const service = ctx.get("library");
1547
+ if (service === void 0) {
1548
+ ctx.logger.info(`${PACKAGE_NAME}: the global library service is absent — the library tools are not registered`);
1549
+ return;
1550
+ }
1551
+ if ((config.tools ?? "all") === "none") return;
1552
+ const images = new ImageRefCache();
1553
+ ctx.on("session/event", ((session, event) => {
1554
+ images.observe(String(session.id), event);
1555
+ }));
1556
+ ctx.inject(["tools"], (toolsCtx) => {
1557
+ for (const definition of libraryToolDefinitions(service, images)) toolsCtx.effect(() => toolsCtx.tools.register(definePluginTool(definition)), `${PACKAGE_NAME}: ${definition.name} tool`);
1558
+ for (const toolName of config.denyTools ?? []) try {
1559
+ toolsCtx.effect(() => toolsCtx.tools.restrict({ deny: [toolName] }), `${PACKAGE_NAME}: mask ${toolName}`);
1560
+ } catch (error) {
1561
+ ctx.logger.info(`${PACKAGE_NAME}: could not mask ${toolName} here — ${error instanceof Error ? error.message : String(error)}`);
1562
+ }
1563
+ });
1564
+ ctx.inject(["systemPrompt"], (promptCtx) => {
1565
+ const prompt = promptCtx.get("systemPrompt");
1566
+ if (prompt === void 0) return;
1567
+ prompt.section({
1568
+ name: "tool:library",
1569
+ order: 114,
1570
+ text: LIBRARY_PROMPT
1571
+ });
1572
+ for (const sectionName of config.hideSections ?? []) try {
1573
+ prompt.section({
1574
+ name: sectionName,
1575
+ order: 0,
1576
+ text: ""
1577
+ });
1578
+ } catch (error) {
1579
+ ctx.logger.info(`${PACKAGE_NAME}: could not blank ${sectionName} — ${error instanceof Error ? error.message : String(error)}`);
1580
+ }
1581
+ prompt.context({
1582
+ name: "library:binding",
1583
+ order: CONTEXT_ORDER,
1584
+ text: (context) => {
1585
+ const id = context.agent?.session?.id;
1586
+ if (id === void 0) return "";
1587
+ try {
1588
+ return renderBindingContext(service, String(id));
1589
+ } catch {
1590
+ return "";
1591
+ }
1592
+ }
1593
+ });
1594
+ });
1595
+ }
1596
+ //#endregion
1597
+ export { Config, LIBRARY_PROMPT, LIBRARY_TOOL_NAMES, apply, inject, libraryToolDefinitions, name, renderBindingContext };