@khorsheed/dsh-library 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +14 -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 +66276 -0
  7. package/lib/index.js +3318 -0
  8. package/lib/profiles-BXM8xNUf.js +1608 -0
  9. package/lib/profiles-BrbeM-bi.js +1635 -0
  10. package/lib/profiles-CFrPQdfO.js +1692 -0
  11. package/lib/profiles-DxtzIUvd.js +1632 -0
  12. package/lib/profiles-O0ddEit3.js +1631 -0
  13. package/lib/profiles-Oj-zBAY9.js +1692 -0
  14. package/lib/profiles-gIEviEuO.js +1610 -0
  15. package/lib/store-CIcZgTsg.js +1456 -0
  16. package/lib/store-DfYOmS44.js +1418 -0
  17. package/lib/tool.js +1597 -0
  18. package/lib/typert.host.d.ts +3 -0
  19. package/lib/typert.host.js +3811 -0
  20. package/lib/typert.remote-client.d.ts +137 -0
  21. package/lib/typert.remote-client.js +3077 -0
  22. package/lib/types/cards.d.ts +41 -0
  23. package/lib/types/cards.js +111 -0
  24. package/lib/types/client/LibraryPane.d.ts +26 -0
  25. package/lib/types/client/LibraryPane.js +117 -0
  26. package/lib/types/client/bind/BindChip.d.ts +33 -0
  27. package/lib/types/client/bind/BindChip.js +102 -0
  28. package/lib/types/client/contract.d.ts +164 -0
  29. package/lib/types/client/contract.js +1 -0
  30. package/lib/types/client/covers.d.ts +36 -0
  31. package/lib/types/client/covers.js +66 -0
  32. package/lib/types/client/definition.d.ts +27 -0
  33. package/lib/types/client/definition.js +32 -0
  34. package/lib/types/client/icons-local.d.ts +41 -0
  35. package/lib/types/client/icons-local.js +25 -0
  36. package/lib/types/client/icons.d.ts +27 -0
  37. package/lib/types/client/icons.js +11 -0
  38. package/lib/types/client/index.d.ts +35 -0
  39. package/lib/types/client/index.js +219 -0
  40. package/lib/types/client/judge.d.ts +70 -0
  41. package/lib/types/client/judge.js +262 -0
  42. package/lib/types/client/locales.d.ts +553 -0
  43. package/lib/types/client/locales.js +1076 -0
  44. package/lib/types/client/look.d.ts +63 -0
  45. package/lib/types/client/look.js +64 -0
  46. package/lib/types/client/math.d.ts +24 -0
  47. package/lib/types/client/math.js +47 -0
  48. package/lib/types/client/mistakes.d.ts +21 -0
  49. package/lib/types/client/mistakes.js +28 -0
  50. package/lib/types/client/preset-visibility.d.ts +109 -0
  51. package/lib/types/client/preset-visibility.js +123 -0
  52. package/lib/types/client/quiz-drafts.d.ts +29 -0
  53. package/lib/types/client/quiz-drafts.js +50 -0
  54. package/lib/types/client/store.d.ts +109 -0
  55. package/lib/types/client/store.js +44 -0
  56. package/lib/types/client/titles.d.ts +17 -0
  57. package/lib/types/client/titles.js +23 -0
  58. package/lib/types/client/ui.d.ts +96 -0
  59. package/lib/types/client/ui.js +124 -0
  60. package/lib/types/client/views/book.d.ts +57 -0
  61. package/lib/types/client/views/book.js +164 -0
  62. package/lib/types/client/views/cards.d.ts +17 -0
  63. package/lib/types/client/views/cards.js +53 -0
  64. package/lib/types/client/views/home.d.ts +26 -0
  65. package/lib/types/client/views/home.js +263 -0
  66. package/lib/types/client/views/lesson.d.ts +42 -0
  67. package/lib/types/client/views/lesson.js +146 -0
  68. package/lib/types/client/views/path.d.ts +32 -0
  69. package/lib/types/client/views/path.js +64 -0
  70. package/lib/types/client/views/quiz.d.ts +42 -0
  71. package/lib/types/client/views/quiz.js +366 -0
  72. package/lib/types/client/views/station-quiz.d.ts +43 -0
  73. package/lib/types/client/views/station-quiz.js +203 -0
  74. package/lib/types/client/views/station.d.ts +18 -0
  75. package/lib/types/client/views/station.js +142 -0
  76. package/lib/types/image-refs.d.ts +35 -0
  77. package/lib/types/image-refs.js +71 -0
  78. package/lib/types/index.d.ts +51 -0
  79. package/lib/types/index.js +31 -0
  80. package/lib/types/mastery.d.ts +81 -0
  81. package/lib/types/mastery.js +110 -0
  82. package/lib/types/profiles.d.ts +31 -0
  83. package/lib/types/profiles.js +25 -0
  84. package/lib/types/remote.d.ts +355 -0
  85. package/lib/types/remote.js +332 -0
  86. package/lib/types/service/assess.d.ts +144 -0
  87. package/lib/types/service/assess.js +773 -0
  88. package/lib/types/service/bindings.d.ts +92 -0
  89. package/lib/types/service/bindings.js +198 -0
  90. package/lib/types/service/cards.d.ts +85 -0
  91. package/lib/types/service/cards.js +320 -0
  92. package/lib/types/service/core.d.ts +124 -0
  93. package/lib/types/service/core.js +169 -0
  94. package/lib/types/service/pages.d.ts +60 -0
  95. package/lib/types/service/pages.js +125 -0
  96. package/lib/types/service/roadmap.d.ts +99 -0
  97. package/lib/types/service/roadmap.js +284 -0
  98. package/lib/types/service/rules.d.ts +63 -0
  99. package/lib/types/service/rules.js +245 -0
  100. package/lib/types/service/shelf.d.ts +136 -0
  101. package/lib/types/service/shelf.js +422 -0
  102. package/lib/types/service.d.ts +42 -0
  103. package/lib/types/service.js +41 -0
  104. package/lib/types/store.d.ts +257 -0
  105. package/lib/types/store.js +1376 -0
  106. package/lib/types/tool-definition.d.ts +49 -0
  107. package/lib/types/tool-definition.js +49 -0
  108. package/lib/types/tool.d.ts +71 -0
  109. package/lib/types/tool.js +174 -0
  110. package/lib/types/tools/assess.d.ts +11 -0
  111. package/lib/types/tools/assess.js +82 -0
  112. package/lib/types/tools/binding.d.ts +11 -0
  113. package/lib/types/tools/binding.js +46 -0
  114. package/lib/types/tools/cards.d.ts +11 -0
  115. package/lib/types/tools/cards.js +100 -0
  116. package/lib/types/tools/kit.d.ts +215 -0
  117. package/lib/types/tools/kit.js +281 -0
  118. package/lib/types/tools/notes.d.ts +11 -0
  119. package/lib/types/tools/notes.js +40 -0
  120. package/lib/types/tools/page.d.ts +11 -0
  121. package/lib/types/tools/page.js +59 -0
  122. package/lib/types/tools/project.d.ts +11 -0
  123. package/lib/types/tools/project.js +67 -0
  124. package/lib/types/tools/quiz.d.ts +11 -0
  125. package/lib/types/tools/quiz.js +74 -0
  126. package/lib/types/tools/read.d.ts +11 -0
  127. package/lib/types/tools/read.js +127 -0
  128. package/lib/types/tools/resource.d.ts +11 -0
  129. package/lib/types/tools/resource.js +147 -0
  130. package/lib/types/tools/roadmap.d.ts +11 -0
  131. package/lib/types/tools/roadmap.js +102 -0
  132. package/lib/types/types.d.ts +752 -0
  133. package/lib/types/types.js +159 -0
  134. package/locale/en.json +6 -0
  135. package/locale/zh.json +6 -0
  136. package/package.json +135 -3
@@ -0,0 +1,215 @@
1
+ /**
2
+ * What every library tool shares: payload transport helpers, the parameter
3
+ * schemas, and the per-call context kit (binding defaults, project / resource
4
+ * resolution, auto-binding). Each tool lives in its own file under `tools/`
5
+ * and receives the kit; `tool-definition.ts` assembles them in workflow order.
6
+ *
7
+ * @module @khorsheed/dsh-library/tools/kit
8
+ */
9
+ import type { Agent } from '@deepseek-ai/dsh-agent';
10
+ import { type LibraryBinding, type LibraryOutcome } from '../types.ts';
11
+ import type { LibraryService } from '../service.ts';
12
+ import { ImageRefCache } from '../image-refs.ts';
13
+ /** JSON pass-through output — what the render emits IS what the model reads, so it goes compact (indentation doubled every result). */
14
+ export declare function jsonOutput(): {
15
+ schema: {
16
+ readonly type: "json";
17
+ };
18
+ render: (_args: unknown, value: unknown) => {
19
+ type: "text";
20
+ text: string;
21
+ }[];
22
+ };
23
+ /** Unwrap a service outcome: the domain value, or a thrown Error whose message IS the lesson. */
24
+ export declare function unwrap<T>(outcome: LibraryOutcome<T>): T;
25
+ export declare const READ_WHATS: readonly ["projects", "resources", "toc", "roadmap", "pages", "cards", "quiz"];
26
+ export type Exec = {
27
+ agent?: Agent;
28
+ };
29
+ /** The calling session's id, when the call came from a session. */
30
+ export declare function sessionIdOf(exec: Exec | undefined): string | undefined;
31
+ /**
32
+ * Payloads arrive as objects — or, from models that stringify deep parameters
33
+ * (common with big nested ones), as their JSON text; both parse. A refusal
34
+ * names what actually arrived, so the model fixes the transport instead of
35
+ * guessing at the shape.
36
+ */
37
+ export declare function payload(value: unknown): Record<string, unknown> | undefined;
38
+ export declare function payloadList(value: unknown): unknown[] | undefined;
39
+ export declare function arrival(field: string, value: unknown): string;
40
+ /**
41
+ * A contents sent as one text: one row per line. Models also join the rows
42
+ * with ;/; on a single line, or send literal "\\n" escapes — a single line
43
+ * holding several separators splits on them (a 30-chapter book once landed
44
+ * as ONE row, and every station's chapters then fell out of range).
45
+ */
46
+ export declare function splitTocText(text: string): string[];
47
+ /**
48
+ * A station's chapters as a locator: row numbers and ranges, whatever the
49
+ * dash or separator ("15–16", "3-4、7", "第3-4章"); words around them (an
50
+ * appendix, a part name) come back as `rest` for a topic side reference.
51
+ */
52
+ export declare function chaptersLocator(chapters: string): {
53
+ locator: string;
54
+ rest?: string;
55
+ };
56
+ /** The first non-blank string of the candidates (the resource/book alias pairs). */
57
+ export declare function pickString(...values: unknown[]): string | undefined;
58
+ /**
59
+ * One cover argument → the stored `{ ref, locator?, … }`. Accepts the string
60
+ * shorthand ("r_…#1-5" | "https://…" | "topic: …"), the v5 object, and every
61
+ * legacy union member ({ kind: "book", bookId|book, ranges } |
62
+ * { kind: "resource", resourceId|resource } | { kind: "url", url, title? } |
63
+ * { kind: "topic", text }). Undefined when nothing usable arrived.
64
+ */
65
+ export declare function normalizeCoverArg(value: unknown): Record<string, unknown> | undefined;
66
+ /**
67
+ * A question's two axes are optional on the wire: target defaults to the
68
+ * station's topic, evidence to domain knowledge. Flat shorthands fill them in —
69
+ * cardIds imply a marked target; pageId / url imply photo / web evidence; a
70
+ * bare string sets just the kind. The service's two-axis gate is untouched: it
71
+ * judges the normalized shape.
72
+ */
73
+ export declare function normalizeQuestionAxes(question: Record<string, unknown>): Record<string, unknown>;
74
+ /**
75
+ * Models sometimes wrap a list payload as `{ items: [...] }`. Declaring the
76
+ * wrapper as a oneOf branch lets it past schema validation into execute (which
77
+ * unwraps it in payloadList) — and a genuinely wrong object then earns the
78
+ * tool's own teaching error instead of the harness's opaque "must match
79
+ * exactly one oneOf branch" refusal.
80
+ */
81
+ export declare const LIST_WRAPPER_SCHEMA: {
82
+ readonly type: "object";
83
+ readonly additionalProperties: true;
84
+ readonly description: "The same list wrapped: { items: [...] }.";
85
+ };
86
+ /**
87
+ * The roadmap nodes schema. Only the canonical shape is advertised; execute
88
+ * still takes the older spellings (also, dependsOn, read stations, acceptance
89
+ * refs, chapterRanges, covers) — the schema items allow extra keys, and the
90
+ * JSON-text branch lets a stringified list through to execute's parser.
91
+ */
92
+ export declare const NODES_SCHEMA: {
93
+ readonly oneOf: readonly [{
94
+ readonly type: "array";
95
+ readonly items: {
96
+ readonly type: "object";
97
+ readonly additionalProperties: true;
98
+ readonly properties: {
99
+ readonly id: {
100
+ readonly type: "string";
101
+ readonly description: "Unique: s1, s2…";
102
+ };
103
+ readonly title: {
104
+ readonly type: "string";
105
+ };
106
+ readonly chapters: {
107
+ readonly oneOf: readonly [{
108
+ readonly type: "string";
109
+ }, {
110
+ readonly type: "integer";
111
+ }];
112
+ readonly description: "Contents rows it covers: \"4\" | \"3-4\".";
113
+ };
114
+ readonly points: {
115
+ readonly type: "array";
116
+ readonly description: "Required: its 3–5 knowledge points — the chapter quiz asks them.";
117
+ readonly items: {
118
+ readonly type: "object";
119
+ readonly additionalProperties: true;
120
+ readonly properties: {
121
+ readonly id: {
122
+ readonly type: "string";
123
+ readonly description: "A stable slug.";
124
+ };
125
+ readonly title: {
126
+ readonly type: "string";
127
+ };
128
+ };
129
+ };
130
+ };
131
+ readonly goals: {
132
+ readonly type: "json";
133
+ readonly description: "What the learner can do after it: [\"…\"].";
134
+ };
135
+ };
136
+ };
137
+ }, {
138
+ readonly type: "string";
139
+ readonly description: "The same list as JSON text.";
140
+ }];
141
+ };
142
+ /**
143
+ * The quiz questions schema. ids are numbered when missing; target and
144
+ * evidence follow from cardIds / quote / pageId / url and the textbook (the
145
+ * full two-axis objects still parse).
146
+ */
147
+ export declare const QUESTIONS_SCHEMA: {
148
+ readonly oneOf: readonly [{
149
+ readonly type: "array";
150
+ readonly items: {
151
+ readonly type: "object";
152
+ readonly additionalProperties: true;
153
+ readonly properties: {
154
+ readonly point: {
155
+ readonly type: "string";
156
+ readonly description: "The station point it probes.";
157
+ };
158
+ readonly kind: {
159
+ readonly type: "string";
160
+ readonly enum: readonly ["choice", "fill", "free"];
161
+ };
162
+ readonly prompt: {
163
+ readonly type: "string";
164
+ };
165
+ readonly options: {
166
+ readonly type: "json";
167
+ readonly description: "choice only: [\"…\", \"…\"].";
168
+ };
169
+ readonly answer: {
170
+ readonly type: "string";
171
+ readonly description: "choice: the right option, verbatim.";
172
+ };
173
+ readonly explanation: {
174
+ readonly type: "string";
175
+ };
176
+ readonly quote: {
177
+ readonly type: "string";
178
+ readonly description: "The page text it rests on (required for a material textbook).";
179
+ };
180
+ };
181
+ };
182
+ }, {
183
+ readonly type: "string";
184
+ readonly description: "The same list as JSON text.";
185
+ }];
186
+ };
187
+ /** The per-call helpers every tool closes over, built once per tool set. */
188
+ export declare function createToolKit(service: LibraryService, images: ImageRefCache): {
189
+ service: LibraryService;
190
+ images: ImageRefCache;
191
+ bindingOf: (exec: Exec | undefined) => LibraryBinding | undefined;
192
+ projectOf: (value: unknown, exec: Exec | undefined, resource?: string) => string;
193
+ autoBind: (exec: Exec | undefined, binding: LibraryBinding) => Promise<void>;
194
+ textbookList: (exec: Exec | undefined) => string;
195
+ resourceOf: (args: Record<string, unknown>, exec: Exec | undefined) => string;
196
+ scopeParams: {
197
+ readonly project: {
198
+ readonly type: "string";
199
+ readonly description: "Defaults to the binding.";
200
+ };
201
+ };
202
+ textbookParams: {
203
+ readonly resource: {
204
+ readonly type: "string";
205
+ readonly description: "Defaults to the binding.";
206
+ };
207
+ readonly project: {
208
+ readonly type: "string";
209
+ readonly description: "Defaults to the binding.";
210
+ };
211
+ };
212
+ };
213
+ /** The kit a tool factory receives. */
214
+ export type ToolKit = ReturnType<typeof createToolKit>;
215
+ //# sourceMappingURL=kit.d.ts.map
@@ -0,0 +1,281 @@
1
+ import { formatLocator } from "../types.js";
2
+ /** JSON pass-through output — what the render emits IS what the model reads, so it goes compact (indentation doubled every result). */
3
+ export function jsonOutput() {
4
+ return {
5
+ schema: { type: 'json' },
6
+ render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }],
7
+ };
8
+ }
9
+ /** Unwrap a service outcome: the domain value, or a thrown Error whose message IS the lesson. */
10
+ export function unwrap(outcome) {
11
+ if (outcome.ok)
12
+ return outcome;
13
+ throw new Error(outcome.message);
14
+ }
15
+ export const READ_WHATS = ['projects', 'resources', 'toc', 'roadmap', 'pages', 'cards', 'quiz'];
16
+ /** The calling session's id, when the call came from a session. */
17
+ export function sessionIdOf(exec) {
18
+ const id = exec?.agent?.session?.id;
19
+ return id === undefined ? undefined : String(id);
20
+ }
21
+ /**
22
+ * Payloads arrive as objects — or, from models that stringify deep parameters
23
+ * (common with big nested ones), as their JSON text; both parse. A refusal
24
+ * names what actually arrived, so the model fixes the transport instead of
25
+ * guessing at the shape.
26
+ */
27
+ export function payload(value) {
28
+ if (typeof value === 'string') {
29
+ try {
30
+ value = JSON.parse(value);
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined;
37
+ }
38
+ export function payloadList(value) {
39
+ if (typeof value === 'string') {
40
+ try {
41
+ value = JSON.parse(value);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ if (Array.isArray(value))
48
+ return value;
49
+ // A model as often wraps the list: { items: [...] } — unwrap it (the schema
50
+ // carries a matching object branch so this shape reaches execute at all).
51
+ const items = payload(value)?.items;
52
+ return Array.isArray(items) ? items : undefined;
53
+ }
54
+ export function arrival(field, value) {
55
+ if (value === undefined)
56
+ return `no ${field} field arrived — set the ${field} field itself; its value may be the object or its JSON text`;
57
+ if (typeof value === 'string') {
58
+ try {
59
+ JSON.parse(value);
60
+ return `${field} arrived as JSON text that is not the expected shape`;
61
+ }
62
+ catch {
63
+ return `${field} arrived as text that is not valid JSON — send the object itself (or its JSON text)`;
64
+ }
65
+ }
66
+ return `${field} arrived as ${Array.isArray(value) ? 'an array' : typeof value} with an unexpected shape`;
67
+ }
68
+ /**
69
+ * A contents sent as one text: one row per line. Models also join the rows
70
+ * with ;/; on a single line, or send literal "\\n" escapes — a single line
71
+ * holding several separators splits on them (a 30-chapter book once landed
72
+ * as ONE row, and every station's chapters then fell out of range).
73
+ */
74
+ export function splitTocText(text) {
75
+ const lines = text.replace(/\\n/g, '\n').split('\n').map(line => line.trim()).filter(line => line !== '');
76
+ if (lines.length !== 1)
77
+ return lines;
78
+ const parts = lines[0].split(/\s*[;;]\s*/).map(part => part.trim()).filter(part => part !== '');
79
+ return parts.length >= 3 ? parts : lines;
80
+ }
81
+ /**
82
+ * A station's chapters as a locator: row numbers and ranges, whatever the
83
+ * dash or separator ("15–16", "3-4、7", "第3-4章"); words around them (an
84
+ * appendix, a part name) come back as `rest` for a topic side reference.
85
+ */
86
+ export function chaptersLocator(chapters) {
87
+ const normalized = chapters.replace(/[–—~~至到]/g, '-').replace(/[,、;;/]/g, ',');
88
+ const ranges = [...normalized.matchAll(/(\d+)\s*(?:-\s*(\d+))?/g)].map(match => match[2] === undefined ? match[1] : `${match[1]}-${match[2]}`);
89
+ const rest = normalized.replace(/(\d+)\s*(?:-\s*(\d+))?/g, '').replace(/[第章节,+\s-]+/g, ' ').trim();
90
+ return { locator: ranges.join(','), ...(rest === '' ? {} : { rest }) };
91
+ }
92
+ /** The first non-blank string of the candidates (the resource/book alias pairs). */
93
+ export function pickString(...values) {
94
+ return values.find((value) => typeof value === 'string' && value.trim() !== '')?.trim();
95
+ }
96
+ /**
97
+ * One cover argument → the stored `{ ref, locator?, … }`. Accepts the string
98
+ * shorthand ("r_…#1-5" | "https://…" | "topic: …"), the v5 object, and every
99
+ * legacy union member ({ kind: "book", bookId|book, ranges } |
100
+ * { kind: "resource", resourceId|resource } | { kind: "url", url, title? } |
101
+ * { kind: "topic", text }). Undefined when nothing usable arrived.
102
+ */
103
+ export function normalizeCoverArg(value) {
104
+ if (typeof value === 'string') {
105
+ const shorthand = value.trim();
106
+ if (shorthand === '')
107
+ return undefined;
108
+ if (shorthand.startsWith('topic:') || /^https?:\/\//.test(shorthand))
109
+ return { ref: shorthand };
110
+ const match = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:#\s*(.+))?$/.exec(shorthand);
111
+ return { ref: match?.[1] ?? shorthand, ...(match?.[2] === undefined ? {} : { locator: match[2].trim() }) };
112
+ }
113
+ const record = payload(value);
114
+ if (record === undefined)
115
+ return undefined;
116
+ const provenance = {
117
+ ...(typeof record.by === 'string' ? { by: record.by } : {}),
118
+ ...(typeof record.verifiedAt === 'string' ? { verifiedAt: record.verifiedAt } : {}),
119
+ };
120
+ if (typeof record.ref === 'string' && record.ref.trim() !== '') {
121
+ return {
122
+ ref: record.ref.trim(),
123
+ ...(typeof record.locator === 'string' && record.locator.trim() !== '' ? { locator: record.locator.trim() } : {}),
124
+ ...(typeof record.title === 'string' && record.title.trim() !== '' ? { title: record.title.trim() } : {}),
125
+ ...provenance,
126
+ };
127
+ }
128
+ const bookId = pickString(record.bookId, record.book);
129
+ if ((record.kind === 'book' || record.kind === undefined) && bookId !== undefined) {
130
+ const ranges = Array.isArray(record.ranges)
131
+ ? record.ranges.flatMap(range => {
132
+ const row = payload(range);
133
+ const from = typeof row?.from === 'number' ? row.from : undefined;
134
+ const to = typeof row?.to === 'number' ? row.to : undefined;
135
+ return from === undefined || to === undefined ? [] : [{ from, to }];
136
+ })
137
+ : [];
138
+ return { ref: bookId, ...(ranges.length === 0 ? {} : { locator: formatLocator(ranges) }), ...provenance };
139
+ }
140
+ const resourceId = pickString(record.resourceId, record.resource);
141
+ if (record.kind === 'resource' && resourceId !== undefined)
142
+ return { ref: resourceId, ...provenance };
143
+ if (record.kind === 'url' && typeof record.url === 'string') {
144
+ return { ref: record.url, ...(typeof record.title === 'string' ? { title: record.title } : {}), ...provenance };
145
+ }
146
+ if (record.kind === 'topic' && typeof record.text === 'string')
147
+ return { ref: `topic:${record.text}`, ...provenance };
148
+ return undefined;
149
+ }
150
+ /**
151
+ * A question's two axes are optional on the wire: target defaults to the
152
+ * station's topic, evidence to domain knowledge. Flat shorthands fill them in —
153
+ * cardIds imply a marked target; pageId / url imply photo / web evidence; a
154
+ * bare string sets just the kind. The service's two-axis gate is untouched: it
155
+ * judges the normalized shape.
156
+ */
157
+ export function normalizeQuestionAxes(question) {
158
+ const { cardIds, quote, url, pageId, ...rest } = question;
159
+ const flatIds = Array.isArray(cardIds) ? cardIds.filter((id) => typeof id === 'string' && id !== '') : undefined;
160
+ const rawTarget = typeof rest.target === 'string' ? { kind: rest.target } : rest.target;
161
+ const targetIn = rawTarget !== null && typeof rawTarget === 'object' && !Array.isArray(rawTarget) ? rawTarget : undefined;
162
+ let target = targetIn ?? (flatIds !== undefined && flatIds.length > 0 ? { kind: 'marked' } : { kind: 'topic' });
163
+ if (flatIds !== undefined && flatIds.length > 0 && target.cardIds === undefined)
164
+ target = { ...target, cardIds: flatIds };
165
+ const rawEvidence = typeof rest.evidence === 'string' ? { kind: rest.evidence } : rest.evidence;
166
+ const evidenceIn = rawEvidence !== null && typeof rawEvidence === 'object' && !Array.isArray(rawEvidence) ? rawEvidence : undefined;
167
+ let evidence = evidenceIn ?? { kind: typeof pageId === 'string' && pageId !== '' ? 'photo' : typeof url === 'string' && url !== '' ? 'web' : 'knowledge' };
168
+ for (const [key, value] of [['quote', quote], ['url', url], ['pageId', pageId]]) {
169
+ if (typeof value === 'string' && value !== '' && evidence[key] === undefined)
170
+ evidence = { ...evidence, [key]: value };
171
+ }
172
+ return { ...rest, target, evidence };
173
+ }
174
+ /**
175
+ * Models sometimes wrap a list payload as `{ items: [...] }`. Declaring the
176
+ * wrapper as a oneOf branch lets it past schema validation into execute (which
177
+ * unwraps it in payloadList) — and a genuinely wrong object then earns the
178
+ * tool's own teaching error instead of the harness's opaque "must match
179
+ * exactly one oneOf branch" refusal.
180
+ */
181
+ export const LIST_WRAPPER_SCHEMA = { type: 'object', additionalProperties: true, description: 'The same list wrapped: { items: [...] }.' };
182
+ /**
183
+ * The roadmap nodes schema. Only the canonical shape is advertised; execute
184
+ * still takes the older spellings (also, dependsOn, read stations, acceptance
185
+ * refs, chapterRanges, covers) — the schema items allow extra keys, and the
186
+ * JSON-text branch lets a stringified list through to execute's parser.
187
+ */
188
+ export const NODES_SCHEMA = {
189
+ oneOf: [
190
+ { type: 'array', items: { type: 'object', additionalProperties: true, properties: {
191
+ id: { type: 'string', description: 'Unique: s1, s2…' },
192
+ title: { type: 'string' },
193
+ chapters: { oneOf: [{ type: 'string' }, { type: 'integer' }], description: 'Contents rows it covers: "4" | "3-4".' },
194
+ points: { type: 'array', description: 'Required: its 3–5 knowledge points — the chapter quiz asks them.', items: { type: 'object', additionalProperties: true, properties: {
195
+ id: { type: 'string', description: 'A stable slug.' },
196
+ title: { type: 'string' },
197
+ } } },
198
+ goals: { type: 'json', description: 'What the learner can do after it: ["…"].' },
199
+ } } },
200
+ { type: 'string', description: 'The same list as JSON text.' },
201
+ ],
202
+ };
203
+ /**
204
+ * The quiz questions schema. ids are numbered when missing; target and
205
+ * evidence follow from cardIds / quote / pageId / url and the textbook (the
206
+ * full two-axis objects still parse).
207
+ */
208
+ export const QUESTIONS_SCHEMA = {
209
+ oneOf: [
210
+ { type: 'array', items: { type: 'object', additionalProperties: true, properties: {
211
+ point: { type: 'string', description: 'The station point it probes.' },
212
+ kind: { type: 'string', enum: ['choice', 'fill', 'free'] },
213
+ prompt: { type: 'string' },
214
+ options: { type: 'json', description: 'choice only: ["…", "…"].' },
215
+ answer: { type: 'string', description: 'choice: the right option, verbatim.' },
216
+ explanation: { type: 'string' },
217
+ quote: { type: 'string', description: 'The page text it rests on (required for a material textbook).' },
218
+ } } },
219
+ { type: 'string', description: 'The same list as JSON text.' },
220
+ ],
221
+ };
222
+ /** The per-call helpers every tool closes over, built once per tool set. */
223
+ export function createToolKit(service, images) {
224
+ /** The session's binding, when it has one. */
225
+ const bindingOf = (exec) => {
226
+ const sessionId = sessionIdOf(exec);
227
+ return sessionId === undefined ? undefined : service.getBinding(sessionId);
228
+ };
229
+ /** The project: the argument, else the binding, else the resource's owner, else a lesson. */
230
+ const projectOf = (value, exec, resource) => {
231
+ if (typeof value === 'string' && value.trim() !== '')
232
+ return value.trim();
233
+ const bound = bindingOf(exec)?.projectId;
234
+ if (bound !== undefined)
235
+ return bound;
236
+ if (resource !== undefined) {
237
+ const owner = service.findResourceOwner(resource);
238
+ if (owner !== undefined)
239
+ return owner;
240
+ }
241
+ 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? }');
242
+ };
243
+ /** 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. */
244
+ const autoBind = async (exec, binding) => {
245
+ const sessionId = sessionIdOf(exec);
246
+ if (sessionId === undefined)
247
+ return;
248
+ service.noteSessionWrite(sessionId);
249
+ const current = bindingOf(exec);
250
+ if (current !== undefined && (current.projectId !== binding.projectId || current.resourceId !== undefined))
251
+ return;
252
+ if (current !== undefined && binding.resourceId === undefined)
253
+ return;
254
+ await service.setBinding(sessionId, binding);
255
+ };
256
+ /** The textbooks of the bound project, as one teaching line for a refusal. */
257
+ const textbookList = (exec) => {
258
+ const summary = sessionIdOf(exec) === undefined ? undefined : service.bindingSummary(sessionIdOf(exec), 0);
259
+ const textbooks = summary?.textbooks ?? [];
260
+ return textbooks.length === 0 ? '' : ` — this project's textbooks: ${textbooks.map(textbook => `${textbook.seq ?? '-'}. ${textbook.id} 《${textbook.title}》`).join('; ')}`;
261
+ };
262
+ /** The resource: the argument (either spelling), else the binding's resource, else a lesson. */
263
+ const resourceOf = (args, exec) => {
264
+ const named = pickString(args.resource, args.book);
265
+ if (named !== undefined)
266
+ return named;
267
+ const bound = bindingOf(exec)?.resourceId;
268
+ if (bound !== undefined)
269
+ return bound;
270
+ throw new Error(`no textbook named: pass resource${textbookList(exec) || ' (library_read · resources lists them)'}, or bind this session to one (library_binding)`);
271
+ };
272
+ /** The shared scope parameters (write tools). */
273
+ const scopeParams = {
274
+ project: { type: 'string', description: 'Defaults to the binding.' },
275
+ };
276
+ const textbookParams = {
277
+ ...scopeParams,
278
+ resource: { type: 'string', description: 'Defaults to the binding.' },
279
+ };
280
+ return { service, images, bindingOf, projectOf, autoBind, textbookList, resourceOf, scopeParams, textbookParams };
281
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The `library_notes` tool (moved verbatim from tool-definition.ts; the shared
3
+ * helpers and schemas are in `tools/kit.ts`).
4
+ *
5
+ * @module @khorsheed/dsh-library/tools/notes
6
+ */
7
+ import { type ToolDefinition } from '@deepseek-ai/dsh-tools';
8
+ import { type ToolKit } from './kit.ts';
9
+ /** Build the `library_notes` tool over the shared kit. */
10
+ export declare function libraryNotesTool(kit: ToolKit): ToolDefinition;
11
+ //# sourceMappingURL=notes.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The `library_notes` tool (moved verbatim from tool-definition.ts; the shared
3
+ * helpers and schemas are in `tools/kit.ts`).
4
+ *
5
+ * @module @khorsheed/dsh-library/tools/notes
6
+ */
7
+ import { defineTool } from '@deepseek-ai/dsh-tools';
8
+ import { jsonOutput, unwrap } from "./kit.js";
9
+ /** Build the `library_notes` tool over the shared kit. */
10
+ export function libraryNotesTool(kit) {
11
+ const { service, projectOf, resourceOf, textbookParams } = kit;
12
+ return defineTool({
13
+ name: 'library_notes',
14
+ 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: '
15
+ + '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 思考题; '
16
+ + '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.',
17
+ parameters: {
18
+ ...textbookParams,
19
+ node: { type: 'string', required: true, description: 'The station id.' },
20
+ text: { type: 'string', required: true, description: 'The 讲义, markdown.' },
21
+ },
22
+ output: jsonOutput(),
23
+ isConcurrencySafe: () => false,
24
+ async execute(args, exec) {
25
+ const resourceId = resourceOf(args, exec);
26
+ const written = unwrap(await service.putNotes({
27
+ projectId: projectOf(args.project, exec, resourceId),
28
+ resourceId,
29
+ nodeId: String(args.node),
30
+ text: String(args.text),
31
+ }));
32
+ return {
33
+ ...written,
34
+ next: written.notes.revisions === 1
35
+ ? 'Tell the learner in one line that the 讲义 are ready in the sidebar, then wait — they read first and ask.'
36
+ : 'Tell the learner in one line what changed in the 讲义.',
37
+ };
38
+ },
39
+ });
40
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The `library_page` tool (moved verbatim from tool-definition.ts; the shared
3
+ * helpers and schemas are in `tools/kit.ts`).
4
+ *
5
+ * @module @khorsheed/dsh-library/tools/page
6
+ */
7
+ import { type ToolDefinition } from '@deepseek-ai/dsh-tools';
8
+ import { type ToolKit } from './kit.ts';
9
+ /** Build the `library_page` tool over the shared kit. */
10
+ export declare function libraryPageTool(kit: ToolKit): ToolDefinition;
11
+ //# sourceMappingURL=page.d.ts.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The `library_page` tool (moved verbatim from tool-definition.ts; the shared
3
+ * helpers and schemas are in `tools/kit.ts`).
4
+ *
5
+ * @module @khorsheed/dsh-library/tools/page
6
+ */
7
+ import { defineTool } from '@deepseek-ai/dsh-tools';
8
+ import { arrival, jsonOutput, payloadList, sessionIdOf, unwrap, } from "./kit.js";
9
+ /** Build the `library_page` tool over the shared kit. */
10
+ export function libraryPageTool(kit) {
11
+ const { service, images, projectOf, resourceOf, textbookParams } = kit;
12
+ return defineTool({
13
+ name: 'library_page',
14
+ 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.',
15
+ parameters: {
16
+ ...textbookParams,
17
+ text: { type: 'string', required: true, description: 'The printed text.' },
18
+ marks: {
19
+ oneOf: [
20
+ { type: 'array', items: { type: 'object', additionalProperties: true, properties: {
21
+ text: { type: 'string', description: 'As printed.' },
22
+ sentence: { type: 'string' },
23
+ handNote: { type: 'string', description: 'The learner\'s handwriting beside it.' },
24
+ gloss: { type: 'string', description: 'In-context meaning.' },
25
+ uncertain: { type: 'boolean', description: 'You could not tell (kept, not carded).' },
26
+ } } },
27
+ { type: 'string', description: 'The same list as JSON text.' },
28
+ ],
29
+ required: true,
30
+ description: 'Every mark on the page.',
31
+ },
32
+ attachment: { type: 'string', description: 'The image id (omit for the latest photo in this conversation).' },
33
+ label: { type: 'string', description: 'What the learner calls the page (「p.16」).' },
34
+ node: { type: 'string', description: 'The station id, when the page belongs to one.' },
35
+ },
36
+ output: jsonOutput(),
37
+ isConcurrencySafe: () => false,
38
+ async execute(args, exec) {
39
+ const marks = payloadList(args.marks);
40
+ if (marks === undefined) {
41
+ 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)}`);
42
+ }
43
+ const attachment = images.resolve(sessionIdOf(exec), typeof args.attachment === 'string' ? args.attachment : undefined);
44
+ if (attachment === undefined) {
45
+ 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');
46
+ }
47
+ const pageResource = resourceOf(args, exec);
48
+ return unwrap(await service.recordPage({
49
+ projectId: projectOf(args.project, exec, pageResource),
50
+ resourceId: pageResource,
51
+ ...(typeof args.node === 'string' && args.node !== '' ? { nodeId: args.node } : {}),
52
+ ...(typeof args.label === 'string' ? { label: args.label } : {}),
53
+ attachment,
54
+ text: args.text,
55
+ marks: marks,
56
+ }));
57
+ },
58
+ });
59
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The `library_project` tool (moved verbatim from tool-definition.ts; the shared
3
+ * helpers and schemas are in `tools/kit.ts`).
4
+ *
5
+ * @module @khorsheed/dsh-library/tools/project
6
+ */
7
+ import { type ToolDefinition } from '@deepseek-ai/dsh-tools';
8
+ import { type ToolKit } from './kit.ts';
9
+ /** Build the `library_project` tool over the shared kit. */
10
+ export declare function libraryProjectTool(kit: ToolKit): ToolDefinition;
11
+ //# sourceMappingURL=project.d.ts.map