@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,245 @@
1
+ /**
2
+ * The library's pure rules: roadmap shape validation, the unlock pass, the
3
+ * covers' evidence gate, and two small helpers. No store, no context — the
4
+ * service layers and the tools call them.
5
+ *
6
+ * @module @khorsheed/dsh-library/service/rules
7
+ */
8
+ import { isLibraryId, coverKind, coverTopic, isThickResource, parseLocator, } from "../types.js";
9
+ import { normalizeFragment } from "./core.js";
10
+ /** A node's title, for event rows. */
11
+ export function titleOf(nodes, nodeId) {
12
+ return nodes.find(node => node.id === nodeId)?.title ?? '';
13
+ }
14
+ /**
15
+ * The option an answer key names — a letter in range, a lettered option, the
16
+ * option text, else a one-way containment — or -1. The sidebar's judge
17
+ * (`client/judge.ts`'s choiceIndex) resolves the same spellings; a question
18
+ * this resolves to -1 is judged wrong no matter what the learner picks, so
19
+ * the write gate refuses it.
20
+ */
21
+ export function choiceOptionIndex(options, answer) {
22
+ const key = normalizeFragment(answer);
23
+ if (/^[a-z]$/.test(key)) {
24
+ const at = key.charCodeAt(0) - 97;
25
+ return at < options.length ? at : -1;
26
+ }
27
+ const lettered = /^([a-z])[.、))::]\s*/.exec(key);
28
+ if (lettered !== null) {
29
+ const at = lettered[1].charCodeAt(0) - 97;
30
+ return at < options.length ? at : -1;
31
+ }
32
+ const exact = options.findIndex(option => normalizeFragment(option) === key);
33
+ if (exact >= 0)
34
+ return exact;
35
+ return options.findIndex(option => normalizeFragment(option).includes(key) || key.includes(normalizeFragment(option)));
36
+ }
37
+ /** Move every locked node whose deps are all done to current. */
38
+ export function unlockReady(nodes) {
39
+ const doneIds = new Set(nodes.filter(node => node.state === 'done').map(node => node.id));
40
+ const unlocked = [];
41
+ const next = nodes.map(node => {
42
+ if (node.state === 'locked' && node.dependsOn.every(dep => doneIds.has(dep))) {
43
+ unlocked.push(node.id);
44
+ return { ...node, state: 'current' };
45
+ }
46
+ return node;
47
+ });
48
+ return { nodes: next, unlocked };
49
+ }
50
+ /** A lesson's size in knowledge points (a quiz station's points). */
51
+ export const STATION_MIN_POINTS = 3;
52
+ export const STATION_MAX_POINTS = 5;
53
+ /**
54
+ * Structural validation of a roadmap's nodes; returns the teaching error or
55
+ * undefined. `scope.resourceId` marks a resource-level draft: the special case
56
+ * whose every node covers that one resource only. Every per-node problem is
57
+ * collected and reported at once — a draft with five mistakes earns ONE
58
+ * refusal listing all five, not five fail-fast round trips (each retry
59
+ * resends the whole nodes payload).
60
+ */
61
+ export function validateRoadmapShape(nodes, scope) {
62
+ const errors = [];
63
+ const seen = new Set();
64
+ for (const [index, node] of nodes.entries()) {
65
+ if (typeof node.id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(node.id) || seen.has(node.id)) {
66
+ errors.push(`nodes[${index}] needs a unique id made of letters, digits, '.', '_', '-' (n1, n2, … is plenty)`);
67
+ }
68
+ else {
69
+ seen.add(node.id);
70
+ }
71
+ if (typeof node.title !== 'string' || node.title.trim() === '')
72
+ errors.push(`nodes[${index}] (${node.id}) needs a title — what the learner masters here`);
73
+ const gateKnown = node.gate === 'quiz' || node.gate === 'read' || node.gate === 'acceptance';
74
+ if (!gateKnown) {
75
+ errors.push(`nodes[${index}] (${node.id}) needs gate: "quiz" | "acceptance" | "read" — quiz nodes gate on a quiz; acceptance nodes gate on acceptance (a hands-on deliverable); read nodes are optional side stops (读完 completes them, they never gate)`);
76
+ }
77
+ else {
78
+ if (node.gate === 'acceptance' && (node.acceptance === undefined || node.acceptance.length === 0)) {
79
+ errors.push(`nodes[${index}] (${node.id}) is an acceptance station — it needs acceptance: ["...", ...], the lines the coach judges one verdict each at assess time`);
80
+ }
81
+ if (node.gate !== 'acceptance' && node.acceptance !== undefined && node.acceptance.length > 0) {
82
+ errors.push(`nodes[${index}] (${node.id}) carries acceptance but its gate is "${node.gate}" — if these lines are judged one verdict each at completion, set gate: "acceptance"; if they are descriptive expectations while a quiz gates the station, move them into goals: [...]`);
83
+ }
84
+ }
85
+ for (const [lineIndex, entry] of (Array.isArray(node.acceptance) ? node.acceptance : []).entries()) {
86
+ if (typeof entry === 'string')
87
+ continue;
88
+ if (entry === null || typeof entry !== 'object' || typeof entry.line !== 'string' || entry.line.trim() === '') {
89
+ errors.push(`nodes[${index}] (${node.id}) acceptance line ${lineIndex + 1} needs its line text`);
90
+ continue;
91
+ }
92
+ if (entry.ref !== undefined && !scope.hasAssessment(entry.ref)) {
93
+ errors.push(`nodes[${index}] (${node.id}) acceptance line ${lineIndex + 1} refs assessment ${JSON.stringify(entry.ref)}, which does not exist — compose it first (library_quiz), then draft`);
94
+ }
95
+ }
96
+ // One station is one lesson: 3–5 knowledge points, never a chapter's whole glossary.
97
+ const pointCount = Array.isArray(node.points) ? node.points.length : 0;
98
+ if (node.gate === 'quiz' && pointCount > STATION_MAX_POINTS) {
99
+ errors.push(`nodes[${index}] (${node.id}) has ${pointCount} points — one station is one lesson of ${STATION_MIN_POINTS}–${STATION_MAX_POINTS} points: split it into consecutive stations, simple before hard`);
100
+ }
101
+ else if (node.gate === 'quiz' && pointCount > 0 && pointCount < STATION_MIN_POINTS) {
102
+ errors.push(`nodes[${index}] (${node.id}) has ${pointCount} point${pointCount === 1 ? '' : 's'} — a lesson carries ${STATION_MIN_POINTS}–${STATION_MAX_POINTS}: merge it with a neighbour or name what else it teaches`);
103
+ }
104
+ if (!Array.isArray(node.covers) || node.covers.length === 0) {
105
+ errors.push(`nodes[${index}] (${node.id}) needs at least one cover — what the station draws on: "r_…#1-5" (a shelf resource + contents rows) | "https://…" (a web text) | "topic: …" (plain coached teaching)`);
106
+ }
107
+ else {
108
+ for (const cover of node.covers) {
109
+ if (cover === null || typeof cover !== 'object' || typeof cover.ref !== 'string' || cover.ref.trim() === '') {
110
+ errors.push(`nodes[${index}] (${node.id}) has a cover with no ref — "r_…#1-5" | "https://…" | "topic: …"`);
111
+ continue;
112
+ }
113
+ const kind = coverKind(cover);
114
+ if (kind === 'resource') {
115
+ // A ref that is no id at all (a scheme-less url, say) must not reach
116
+ // the store's path-segment assert — teach the shape instead.
117
+ if (!isLibraryId(cover.ref)) {
118
+ errors.push(`nodes[${index}] (${node.id}) covers ${JSON.stringify(cover.ref)}, which is neither a shelf resource id (r_…) nor a usable ref — "r_…#1-5" | "https://…" | "topic: …"`);
119
+ continue;
120
+ }
121
+ const covered = scope.resource(cover.ref);
122
+ if (covered === undefined) {
123
+ errors.push(`nodes[${index}] (${node.id}) covers resource ${JSON.stringify(cover.ref)}, which is not on the project's shelf — library_read · resources lists them; register it first (library_textbook)`);
124
+ continue;
125
+ }
126
+ if (cover.locator !== undefined) {
127
+ if (!isThickResource(covered)) {
128
+ errors.push(`nodes[${index}] (${node.id}) puts a locator on 《${covered.title}》, which is a thin link (no contents) — a thin link is covered whole: drop the locator`);
129
+ continue;
130
+ }
131
+ const ranges = parseLocator(cover.locator);
132
+ if (ranges.length === 0) {
133
+ errors.push(`nodes[${index}] (${node.id}) has an unreadable locator ${JSON.stringify(cover.locator)} — locators count contents rows, 1-based and inclusive: "1-5,7"`);
134
+ continue;
135
+ }
136
+ // An authored textbook's chapters are lessons you wrote: one station teaches one.
137
+ if (covered.id === scope.resourceId && covered.origin === 'authored' && ranges.reduce((rows, range) => rows + range.to - range.from + 1, 0) > 1) {
138
+ errors.push(`nodes[${index}] (${node.id}) spans ${cover.locator} of 《${covered.title}》, a textbook you authored — each of its chapters is one lesson and one station: one row per station (too much for one lesson → split the chapter in the contents first)`);
139
+ }
140
+ for (const range of ranges) {
141
+ if (range.to > (covered.toc?.length ?? 0)) {
142
+ const rows = covered.toc?.length ?? 0;
143
+ errors.push(`nodes[${index}] (${node.id}) covers up to row ${range.to}, but 《${covered.title}》's contents has ${rows} rows — library_read · toc lists them${rows < 3 ? `; a book with ${rows} row(s) was registered with its chapters joined — fix the contents first (library_textbook op update, resourceId ${covered.id}, toc one chapter per line), never drop the chapters` : ''}`);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ else if (kind === 'topic') {
149
+ if ((coverTopic(cover) ?? '').trim() === '')
150
+ errors.push(`nodes[${index}] (${node.id}) covers a topic with no text — what the coach teaches here`);
151
+ }
152
+ // url covers: the ref IS the url; its http(s) form is what coverKind read.
153
+ }
154
+ }
155
+ for (const dep of Array.isArray(node.dependsOn) ? node.dependsOn : []) {
156
+ const depIndex = nodes.findIndex(candidate => candidate.id === dep);
157
+ if (depIndex === -1) {
158
+ errors.push(`nodes[${index}] (${node.id}) depends on ${JSON.stringify(dep)}, which the roadmap does not declare`);
159
+ continue;
160
+ }
161
+ if (gateKnown && node.gate !== 'read' && nodes[depIndex]?.gate === 'read') {
162
+ errors.push(`nodes[${index}] (${node.id}) depends on read node ${JSON.stringify(dep)} — a read node is a side stop and never gates a level; depend on the previous quiz or acceptance node instead. How-to-use pages, answer keys, transcripts and appendices are not nodes at all: put them in the resource's extras`);
163
+ continue;
164
+ }
165
+ if (depIndex >= index)
166
+ errors.push(`nodes[${index}] (${node.id}) depends on ${JSON.stringify(dep)}, which comes AFTER it — list a node after everything it depends on`);
167
+ }
168
+ }
169
+ if (errors.length === 0)
170
+ return undefined;
171
+ if (errors.length === 1)
172
+ return errors[0];
173
+ return `${errors.length} shape errors — fix them all, then resend:\n${errors.map(error => `- ${error}`).join('\n')}`;
174
+ }
175
+ /**
176
+ * Why a node's covers forbid a memory-backed content question, or undefined
177
+ * when they allow one: only knowledge books and plain coached teaching (topic)
178
+ * may be quizzed from domain knowledge; a material book, a thin link or a
179
+ * bare url demands a verbatim quote. When the station covers its own
180
+ * textbook (`primaryId`), that cover alone decides — a side reference (a
181
+ * video, an article) never turns a knowledge chapter into a quote-only one.
182
+ * The evidence-gate's node-level half (a resource-scoped quiz without a node
183
+ * keeps the resource rule).
184
+ */
185
+ export function coversVerbatimBlocker(covers, resource, primaryId) {
186
+ const main = primaryId === undefined ? [] : covers.filter(cover => coverKind(cover) === 'resource' && cover.ref === primaryId);
187
+ for (const cover of main.length > 0 ? main : covers) {
188
+ const kind = coverKind(cover);
189
+ if (kind === 'resource') {
190
+ const covered = resource(cover.ref);
191
+ if (covered === undefined)
192
+ continue;
193
+ if (isThickResource(covered)) {
194
+ if (covered.meta?.bookKind === 'material')
195
+ return `《${covered.title}》 is a material book (its exact text is what is tested)`;
196
+ }
197
+ else {
198
+ return `the station draws on 《${covered.title}》, a thin link (a linked text you cannot recall reliably)`;
199
+ }
200
+ }
201
+ else if (kind === 'url') {
202
+ return `the station covers ${cover.ref} (a web text you cannot recall reliably)`;
203
+ }
204
+ }
205
+ return undefined;
206
+ }
207
+ /** The least a point's 讲义 section says (characters, markup and blanks aside). */
208
+ export const LESSON_SECTION_MIN = 120;
209
+ /**
210
+ * What keeps a 讲义 from reading as a lesson, or undefined: change logs and
211
+ * internal words never reach the learner; on a textbook you authored (the 讲义
212
+ * ARE its text) every knowledge point gets its own section — a heading naming
213
+ * the point, then enough to learn it from.
214
+ */
215
+ export function lessonProblems(text, node, authored) {
216
+ const problems = [];
217
+ if (/第\s*\d+\s*次更新|修正说明|更新说明|\bacceptance\b/i.test(text)) {
218
+ problems.push('the 讲义 are for the learner — drop change logs (第 N 次更新, 修正说明; the sidebar already counts revisions) and internal words (acceptance)');
219
+ }
220
+ const points = node.points ?? [];
221
+ if (authored && points.length > 0) {
222
+ const squash = (value) => value.replace(/[\s*_`]/g, '').toLowerCase();
223
+ const headings = [...text.matchAll(/^#{1,4}\s+(.+)$/gm)].map(match => ({ title: squash(match[1] ?? ''), at: match.index ?? 0, end: (match.index ?? 0) + match[0].length }));
224
+ const pointHeadings = points.map(point => headings.find(heading => heading.title.includes(squash(point.title))));
225
+ const starts = pointHeadings.flatMap(heading => heading === undefined ? [] : [heading.at]).sort((a, b) => a - b);
226
+ const missing = [];
227
+ const thin = [];
228
+ for (const [index, point] of points.entries()) {
229
+ const heading = pointHeadings[index];
230
+ if (heading === undefined) {
231
+ missing.push(point.title);
232
+ continue;
233
+ }
234
+ const next = starts.find(start => start > heading.at) ?? text.length;
235
+ const body = text.slice(heading.end, next).replace(/[\s#>*_`|-]/g, '');
236
+ if (body.length < LESSON_SECTION_MIN)
237
+ thin.push(point.title);
238
+ }
239
+ if (missing.length > 0)
240
+ problems.push(`the 讲义 are the lesson itself — the learner learns from them, not from a reading list: give each knowledge point its own section headed by its title (## ${points[0].title}) — missing: ${missing.join('、')}`);
241
+ if (thin.length > 0)
242
+ problems.push(`too thin to learn from (each point: explain it plainly → an example or the steps → a common mistake → a 思考题; outside links belong in a final ## 参考): ${thin.join('、')}`);
243
+ }
244
+ return problems.length === 0 ? undefined : problems.join('; ');
245
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Projects and the shelf: create / edit / delete projects, register, place (seq), move and remove resources.
3
+ *
4
+ * One layer of the library service (see `service/core.ts` for the layering):
5
+ * the methods are the service's own, moved here verbatim by domain.
6
+ *
7
+ * @module @khorsheed/dsh-library/service/shelf
8
+ */
9
+ import { type LibraryBookKind, type LibraryColor, type LibraryProjectPatch, type LibraryIcon, type LibraryOutcome, type LibraryProject, type LibraryProjectSummary, type LibraryResource, type LibraryResourceKind, type LibraryResourceOrigin, type LibraryResourceProgress, type LibraryTemplate, type LibraryTocSource } from '../types.ts';
10
+ import { LibraryServiceCore } from './core.ts';
11
+ export declare class LibraryServiceShelf extends LibraryServiceCore {
12
+ /** One resource's progress row (a thick resource's roadmap; a thin link's bare row). */
13
+ protected resourceProgress(projectId: string, resource: LibraryResource): LibraryResourceProgress;
14
+ protected summarize(project: LibraryProject): LibraryProjectSummary;
15
+ /** Every project with its resources' progress and card tallies. */
16
+ listProjects(): Promise<{
17
+ projects: LibraryProjectSummary[];
18
+ }>;
19
+ /** One project's summary plus its resources' full records. */
20
+ getProject(request: {
21
+ id: string;
22
+ }): Promise<LibraryOutcome<{
23
+ project: LibraryProjectSummary;
24
+ resources: LibraryResource[];
25
+ }>>;
26
+ /** Create a project. */
27
+ createProject(request: {
28
+ name: string;
29
+ template?: LibraryTemplate;
30
+ templateNote?: string;
31
+ goal?: string;
32
+ color?: LibraryColor;
33
+ icon?: LibraryIcon;
34
+ }): Promise<LibraryOutcome<{
35
+ project: LibraryProject;
36
+ }>>;
37
+ /** Change a project's look or goal (the sidebar's 「样式」). */
38
+ updateProject(request: {
39
+ id: string;
40
+ patch: LibraryProjectPatch;
41
+ }): Promise<LibraryOutcome<{
42
+ project: LibraryProject;
43
+ }>>;
44
+ /** Delete a project and everything it holds. */
45
+ deleteProject(request: {
46
+ id: string;
47
+ }): Promise<LibraryOutcome<{
48
+ id: string;
49
+ }>>;
50
+ /**
51
+ * Put one entry on the project's shelf — the v5 unified write. A THICK
52
+ * entry (kind "book", or any kind carrying `toc`) registers its contents:
53
+ * the coach's table of contents, verified on the web whenever it can be,
54
+ * and cover locators count its rows. A THIN link is title + url + the
55
+ * coach's one-line why. Pass `resourceId` to correct an existing entry.
56
+ */
57
+ putResource(request: {
58
+ projectId: string;
59
+ resourceId?: string;
60
+ title: string;
61
+ kind: LibraryResourceKind;
62
+ url?: string;
63
+ note?: string;
64
+ origin?: LibraryResourceOrigin;
65
+ /** A textbook's place in the study order: inserts there (the rest shift down); omitted, a new textbook queues last. */
66
+ seq?: number;
67
+ toc?: readonly {
68
+ title: string;
69
+ part?: string;
70
+ }[];
71
+ tocSource?: LibraryTocSource;
72
+ bookKind?: LibraryBookKind;
73
+ short?: string;
74
+ extras?: readonly {
75
+ title: string;
76
+ note?: string;
77
+ where?: string;
78
+ when?: 'before' | 'reference';
79
+ }[];
80
+ author?: string;
81
+ edition?: string;
82
+ isbn?: string;
83
+ /** Register a clone of another project's resource (its toc and verification come along). */
84
+ from?: {
85
+ projectId: string;
86
+ resourceId: string;
87
+ };
88
+ }): Promise<LibraryOutcome<{
89
+ resource: LibraryResource;
90
+ }>>;
91
+ /**
92
+ * Renumber a project's textbooks gapless from 1 with `resourceId` at
93
+ * `seq` (clamped; omitted = keep its place, or last when it has none).
94
+ * Call inside the project lock. Returns the placed resource as stored.
95
+ */
96
+ protected placeTextbook(projectId: string, resourceId: string, seq: number | undefined): LibraryResource;
97
+ /** Switch a textbook between 知识型 (knowledge) and 材料型 (material) — the learner's call on the textbook page. */
98
+ setBookKind(request: {
99
+ projectId: string;
100
+ resourceId: string;
101
+ bookKind: LibraryBookKind;
102
+ }): Promise<LibraryOutcome<{
103
+ resource: LibraryResource;
104
+ }>>;
105
+ /** Move a textbook to another place in the study order (the rest shift). */
106
+ moveResource(request: {
107
+ projectId: string;
108
+ resourceId: string;
109
+ seq: number;
110
+ }): Promise<LibraryOutcome<{
111
+ resources: LibraryResource[];
112
+ }>>;
113
+ /** Remove a shelf entry: its record, roadmaps and pages (cards stay, unhung); the textbooks behind it move up. */
114
+ removeResource(request: {
115
+ projectId: string;
116
+ resourceId: string;
117
+ }): Promise<LibraryOutcome<{
118
+ id: string;
119
+ }>>;
120
+ /** One shelf entry, whole. */
121
+ getResource(request: {
122
+ projectId: string;
123
+ resourceId: string;
124
+ }): Promise<LibraryOutcome<{
125
+ resource: LibraryResource;
126
+ }>>;
127
+ /** A project's shelf: textbooks in study order, then links oldest first. */
128
+ listResources(request: {
129
+ projectId: string;
130
+ }): Promise<{
131
+ resources: LibraryResource[];
132
+ }>;
133
+ /** The project holding a resource, when an id is all the caller has (the tools' project fallback). */
134
+ findResourceOwner(resourceId: string): string | undefined;
135
+ }
136
+ //# sourceMappingURL=shelf.d.ts.map