@dolphy-app/extension-sdk 0.4.0 → 0.5.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.
@@ -0,0 +1,1958 @@
1
+ //#region packages/engine-contract/src/index.d.ts
2
+ type UnitId = string;
3
+ type EpochMs = number;
4
+ type Grade = 1 | 2 | 3 | 4 | 5;
5
+ type UnitKind = 'course' | 'lesson' | 'exercise';
6
+ /** Любое значение JSON (хранилище и настройки расширений). */
7
+ type JsonValue = null | boolean | number | string | JsonValue[] | {
8
+ [key: string]: JsonValue;
9
+ };
10
+ interface PageRequest {
11
+ limit?: number;
12
+ cursor?: string;
13
+ }
14
+ interface Page<T> {
15
+ items: T[];
16
+ nextCursor?: string;
17
+ }
18
+ type EngineErrorCode = 'INVALID_ARGUMENT' | 'NOT_FOUND' | 'ENGINE_CLOSED' | 'INCOMPATIBLE_CONTRACT' | 'LIBRARY_NOT_LOADED' | 'LIBRARY_INVALID' | 'ASSET_OUTSIDE_LIBRARY' | 'ASSET_TOO_LARGE' | 'ATTEMPT_NOT_FOUND' | 'ATTEMPT_CLOSED' | 'EXERCISE_TYPE_UNAVAILABLE' | 'PLACEMENT_SESSION_NOT_FOUND' | 'PLACEMENT_SESSION_ACTIVE' | 'PLACEMENT_BUDGET_EXHAUSTED' | 'SYNC_DEVICE_ID_CLASH' | 'SYNC_CONFLICT_NOT_FOUND' | 'SYNC_FOLDER_NOT_CONFIGURED' | 'STORE_BUSY' | 'STORE_READONLY' | 'STORE_CORRUPT' | 'REPOSITORY_EXISTS' | 'REPOSITORY_REJECTED' | 'GIT_FETCH_FAILED' | 'CATALOG_UNAVAILABLE' | 'EXTENSION_INSTALL_FAILED' |
19
+ /** Запись в хранилище расширения превысила потолок; `details`: `extensionId`, `kind`, `limit`. */
20
+ 'EXTENSION_STORAGE_QUOTA' |
21
+ /** Системного хранилища ключей нет (или оно не расшифровало значение): секрет расширения не записан и не прочитан. */
22
+ 'SECRETS_UNAVAILABLE' |
23
+ /** Команда расширения не выполнена; `details`: `extensionId`, `commandId`, `reason` (`ExtensionCommandFailureReason`). */
24
+ 'EXTENSION_COMMAND_FAILED' |
25
+ /** Импорт или экспорт расширения не выполнен; `details`: `extensionId`, `id`, `kind` (`import` | `export`), `reason` (`ExtensionTransferFailureReason`). */
26
+ 'EXTENSION_TRANSFER_FAILED' |
27
+ /** An RPC call to the server part of an extension failed; `details`: `extensionId`, `name`, `reason` (`ExtensionRpcFailureReason`). The message of a failed handler reaches the caller as the error message. */
28
+ 'EXTENSION_RPC_FAILED' |
29
+ /** Хук «до» расширения отменил операцию; `details`: `ExtensionHookFailureDetails`. Сообщение ошибки содержит имя расширения и текст его исключения; журнал и сессия не меняются. */
30
+ 'EXTENSION_HOOK_FAILED' | 'INTERNAL';
31
+ interface EngineErrorDto {
32
+ code: EngineErrorCode;
33
+ message: string;
34
+ retryable: boolean;
35
+ details?: Record<string, unknown>;
36
+ }
37
+ type Severity = 'error' | 'warning' | 'info';
38
+ /** Каталог компилятора: 36 кодов + `W_GRANULARITY`, `E_REFERENCE_FAILS` (engine-ts.md §1.1, F2) + `W_ORPHAN_EVENTS` (его выдаёт движок при открытии, не компилятор). Префикс = серьёзность по умолчанию. */
39
+ type DiagnosticCode = 'E_IO' | 'E_JSON_PARSE' | 'E_SCHEMA' | 'W_UNKNOWN_KEY' | 'E_FRONTMATTER_UNTERMINATED' | 'E_FRONTMATTER_PARSE' | 'E_ENGINE_SCHEMA' | 'W_ENGINE_UNKNOWN_KEY' | 'E_ENGINE_DUPLICATE' | 'W_UNKNOWN_EXERCISE_TYPE' | 'E_EXERCISE_SPEC' | 'E_ID_EMPTY' | 'E_ID_DUPLICATE' | 'E_ID_MISMATCH' | 'E_DEP_MISSING' | 'E_DEP_SELF' | 'E_DEP_KIND' | 'E_CYCLE_DEPENDENCY' | 'E_CYCLE_SUPERSEDED' | 'E_CYCLE_ENCOMPASSED' | 'W_REDUNDANT_EDGE' | 'E_ENC_WEIGHT' | 'E_ENC_MISSING' | 'E_ENC_NOT_ANCESTOR' | 'E_SUP_MISSING' | 'W_ORPHAN_LESSON' | 'W_FAN_IN' | 'E_KEYPREREQ_MISSING' | 'E_KEYPREREQ_NOT_ANCESTOR' | 'E_NO_VERIFICATION' | 'I_NO_VERIFICATION' | 'W_UNSUPPORTED_GENERATOR' | 'E_ASSET_MISSING' | 'E_ASSET_ESCAPES_ROOT' | 'E_ASSET_TYPE' | 'W_ASSET_KIND_UNSUPPORTED' | 'W_KB_STRAY_FILE' | 'W_GRANULARITY' | 'E_REFERENCE_FAILS' | 'W_ORPHAN_EVENTS';
40
+ interface Diagnostic {
41
+ code: DiagnosticCode;
42
+ severity: Severity;
43
+ message: string;
44
+ unitId?: UnitId;
45
+ /** Путь файла относительно корня библиотеки. */
46
+ path?: string;
47
+ /** Строка в `path` (1-based), где она детерминирована. */
48
+ line?: number;
49
+ /** Связанные юниты: полный путь цикла (`E_CYCLE_*`), цель `encompassed`, обходной пререквизит `W_REDUNDANT_EDGE`. */
50
+ related?: UnitId[];
51
+ }
52
+ interface DiagnosticSummary {
53
+ errors: number;
54
+ warnings: number;
55
+ infos: number;
56
+ }
57
+ /** `fresh` — артефакт соответствует библиотеке (stat или content-`revision`); `stale` — `revision` отличается; `missing` — файла нет или он не читается; `compiling` — идёт фоновая компиляция. */
58
+ type ArtifactState = 'fresh' | 'stale' | 'missing' | 'compiling';
59
+ interface LibraryInfo {
60
+ contractVersion: number;
61
+ root: string;
62
+ /** content-`revision` загруженной библиотеки (sha256 по отсортированным `path\0len\0bytes`). */
63
+ revision: string;
64
+ state: 'ready' | 'invalid';
65
+ artifact: ArtifactState;
66
+ counts: {
67
+ courses: number;
68
+ lessons: number;
69
+ exercises: number;
70
+ dependencyEdges: number;
71
+ };
72
+ diagnostics: DiagnosticSummary;
73
+ loadedAt: EpochMs;
74
+ loadMs: number;
75
+ }
76
+ interface AssetRef {
77
+ unitId: UnitId;
78
+ path: string;
79
+ }
80
+ interface WeightedRef {
81
+ id: UnitId;
82
+ weight: number;
83
+ }
84
+ interface UnitCommon {
85
+ id: UnitId;
86
+ name: string;
87
+ description?: string;
88
+ metadata: Record<string, string[]>;
89
+ dependencies: UnitId[];
90
+ encompassed: WeightedRef[];
91
+ superseded: UnitId[];
92
+ }
93
+ interface CourseDto extends UnitCommon {
94
+ kind: 'course';
95
+ lessonCount: number;
96
+ authors?: string[];
97
+ material?: AssetRef;
98
+ instructions?: AssetRef;
99
+ }
100
+ interface LessonDto extends UnitCommon {
101
+ kind: 'lesson';
102
+ courseId: UnitId;
103
+ exerciseCount: number;
104
+ material?: AssetRef;
105
+ instructions?: AssetRef;
106
+ }
107
+ type ExerciseContentDto = {
108
+ type: 'flashcard';
109
+ front: AssetRef;
110
+ back?: AssetRef;
111
+ } | {
112
+ type: 'inlineFlashcard';
113
+ front: string;
114
+ back?: string;
115
+ } | {
116
+ type: 'markdown';
117
+ ref: AssetRef;
118
+ } | {
119
+ type: 'inlineMarkdown';
120
+ text: string;
121
+ };
122
+ /** Вид задания: окно ищет компонент ввода ответа в своём реестре по `type` (регистрация `addAnswerView`). */
123
+ interface ExerciseTaskDto {
124
+ /** Id вида задания. */
125
+ type: string;
126
+ timeoutMs: number;
127
+ /** Расширение, зарегистрировавшее вид. */
128
+ extensionId: string;
129
+ }
130
+ interface ExerciseDto {
131
+ kind: 'exercise';
132
+ id: UnitId;
133
+ lessonId: UnitId;
134
+ courseId: UnitId;
135
+ name: string;
136
+ description?: string;
137
+ exerciseType: 'declarative' | 'procedural';
138
+ content: ExerciseContentDto;
139
+ task?: ExerciseTaskDto;
140
+ keyPrerequisites: UnitId[];
141
+ }
142
+ type UnitDto = CourseDto | LessonDto | ExerciseDto;
143
+ interface GraphQuery {
144
+ rootIds?: UnitId[];
145
+ depth?: number;
146
+ kinds?: UnitKind[];
147
+ limit?: number;
148
+ }
149
+ interface GraphNodeDto {
150
+ id: UnitId;
151
+ kind: UnitKind;
152
+ name: string;
153
+ parentId?: UnitId;
154
+ }
155
+ /** `from` зависит от `to` (dependency), охватывает `to` (encompassed) или заменяет `to` (superseded). */
156
+ interface GraphEdgeDto {
157
+ from: UnitId;
158
+ to: UnitId;
159
+ type: 'dependency' | 'encompassed' | 'superseded';
160
+ weight?: number;
161
+ }
162
+ interface GraphDto {
163
+ nodes: GraphNodeDto[];
164
+ edges: GraphEdgeDto[];
165
+ truncated: boolean;
166
+ }
167
+ interface AssetContent {
168
+ ref: AssetRef;
169
+ mime: 'text/markdown' | 'text/plain';
170
+ text: string;
171
+ bytes: number;
172
+ }
173
+ interface ValidateRequest extends PageRequest {
174
+ minSeverity?: Severity;
175
+ /** Прогнать эталонные решения через раннер и выдать `E_REFERENCE_FAILS` (M5). */
176
+ runChecks?: boolean;
177
+ }
178
+ interface ValidateResult extends Page<Diagnostic> {
179
+ /** content-`revision` файлов на момент проверки; может отличаться от `LibraryInfo.revision`. */
180
+ revision: string;
181
+ summary: DiagnosticSummary;
182
+ checksRun: boolean;
183
+ }
184
+ interface CompileRequest {
185
+ runChecks?: boolean;
186
+ }
187
+ interface CompileResult {
188
+ revision: string;
189
+ diagnosticsSummary: DiagnosticSummary;
190
+ /** false: есть ошибки (артефакт не пишется) или свежий артефакт уже лежит на диске. */
191
+ artifactWritten: boolean;
192
+ }
193
+ interface LibraryService {
194
+ getInfo(): Promise<LibraryInfo>;
195
+ getDiagnostics(req?: PageRequest & {
196
+ minSeverity?: Severity;
197
+ }): Promise<Page<Diagnostic>>;
198
+ validate(req?: ValidateRequest): Promise<ValidateResult>;
199
+ compile(req?: CompileRequest): Promise<CompileResult>;
200
+ reload(): Promise<LibraryInfo>;
201
+ listCourses(req?: PageRequest): Promise<Page<CourseDto>>;
202
+ listLessons(courseId: UnitId, req?: PageRequest): Promise<Page<LessonDto>>;
203
+ listExercises(lessonId: UnitId, req?: PageRequest): Promise<Page<ExerciseDto>>;
204
+ getUnit(id: UnitId): Promise<UnitDto>;
205
+ matchPrefix(prefix: string, kind?: UnitKind, req?: PageRequest): Promise<Page<UnitId>>;
206
+ getGraph(query?: GraphQuery): Promise<GraphDto>;
207
+ readAsset(ref: AssetRef): Promise<AssetContent>;
208
+ }
209
+ type FilterOp = 'All' | 'Any';
210
+ type FilterType = 'Include' | 'Exclude';
211
+ type KeyValueFilterWire = {
212
+ CourseFilter: {
213
+ key: string;
214
+ value: string;
215
+ filter_type: FilterType;
216
+ };
217
+ } | {
218
+ LessonFilter: {
219
+ key: string;
220
+ value: string;
221
+ filter_type: FilterType;
222
+ };
223
+ } | {
224
+ CombinedFilter: {
225
+ op: FilterOp;
226
+ filters: KeyValueFilterWire[];
227
+ };
228
+ };
229
+ type UnitFilterWire = {
230
+ CourseFilter: {
231
+ course_ids: UnitId[];
232
+ };
233
+ } | {
234
+ LessonFilter: {
235
+ lesson_ids: UnitId[];
236
+ };
237
+ } | {
238
+ MetadataFilter: {
239
+ filter: KeyValueFilterWire;
240
+ };
241
+ } | 'ReviewListFilter' | {
242
+ Dependents: {
243
+ unit_ids: UnitId[];
244
+ };
245
+ } | {
246
+ Dependencies: {
247
+ unit_ids: UnitId[];
248
+ depth: number;
249
+ };
250
+ };
251
+ type SessionPartWire = {
252
+ UnitFilter: {
253
+ filter: UnitFilterWire;
254
+ duration: number;
255
+ };
256
+ } | {
257
+ SavedFilter: {
258
+ filter_id: string;
259
+ duration: number;
260
+ };
261
+ } | {
262
+ NoFilter: {
263
+ duration: number;
264
+ };
265
+ };
266
+ interface StudySessionWire {
267
+ id: string;
268
+ description?: string;
269
+ parts?: SessionPartWire[];
270
+ }
271
+ type ExerciseFilterDto = {
272
+ UnitFilter: UnitFilterWire;
273
+ } | {
274
+ StudySession: {
275
+ startTimeMs: EpochMs;
276
+ definition: StudySessionWire;
277
+ };
278
+ };
279
+ type MasteryWindowName = 'new' | 'target' | 'current' | 'easy' | 'mastered';
280
+ interface UnitScoreDto {
281
+ unitId: UnitId;
282
+ kind: UnitKind;
283
+ /** 0..5; null = нет валидной оценки. В `getBatch` (паритет Trane) зависимость без оценки считается выполненной; в `getFrontier` — закрытой (§4). */
284
+ score: number | null;
285
+ avgTrials: number | null;
286
+ window: MasteryWindowName | null;
287
+ }
288
+ type UnitStatus = 'locked' | 'ready' | 'in-progress' | 'mastered' | 'blacklisted' | 'superseded';
289
+ interface ProgressNodeDto {
290
+ id: UnitId;
291
+ kind: UnitKind;
292
+ status: UnitStatus;
293
+ score: number | null;
294
+ avgTrials: number | null;
295
+ attempts: number;
296
+ lastAttemptAt?: EpochMs;
297
+ dueExercises?: number;
298
+ }
299
+ interface ProgressQuery {
300
+ scope?: {
301
+ courseId: UnitId;
302
+ } | {
303
+ lessonId: UnitId;
304
+ } | {
305
+ unitIds: UnitId[];
306
+ };
307
+ includeExercises?: boolean;
308
+ }
309
+ type AttemptSource = 'self' | 'runner' | 'placement' | 'trane-import';
310
+ interface AttemptRecordDto {
311
+ eventId: string;
312
+ exerciseId: UnitId;
313
+ grade: Grade;
314
+ at: EpochMs;
315
+ source: AttemptSource;
316
+ }
317
+ interface BatchRequest {
318
+ filter?: ExerciseFilterDto;
319
+ }
320
+ /** `reasons[i]` — причина показа `exercises[i]`; массивы одной длины. */
321
+ interface BatchDto {
322
+ exercises: ExerciseDto[];
323
+ reasons: ItemReason[];
324
+ generatedAt: EpochMs;
325
+ sessionId: string;
326
+ }
327
+ /** Причина позиции в батче и плане дня: `new` — попыток нет; `review` — есть попытки; `remediation` — вставлено ремедиацией (§4.3). */
328
+ type ItemReason = 'review' | 'new' | 'remediation';
329
+ interface AttemptDto {
330
+ attemptId: string;
331
+ exercise: ExerciseDto;
332
+ startedAt: EpochMs;
333
+ verifiable: boolean;
334
+ /** Результат `project()` расширения; `null`, если упражнение не проверяемое. */
335
+ view: unknown;
336
+ }
337
+ interface SubmitAnswerRequest {
338
+ attemptId: string;
339
+ answer: unknown;
340
+ }
341
+ interface VerdictBase {
342
+ attemptId: string;
343
+ /** Число вердиктов `passed`/`failed` по попытке; `error` не считается. */
344
+ attemptsUsed: number;
345
+ durationMs: number;
346
+ feedback?: string;
347
+ /** Данные расширения, непрозрачны для движка (например, `{ rowCount }` у SQL). */
348
+ data?: unknown;
349
+ }
350
+ type VerdictDto = (VerdictBase & {
351
+ outcome: 'passed';
352
+ }) |
353
+ /** Вина ученика. `detail` (ожидаемые строки) — только при `EngineConfig.authorMode`. */
354
+ (VerdictBase & {
355
+ outcome: 'failed';
356
+ reason: string;
357
+ detail?: string;
358
+ }) |
359
+ /** Не вина ученика: журнал не затрагивается, повтор `submitAnswer` разрешён. */
360
+ (VerdictBase & {
361
+ outcome: 'error';
362
+ reason: string;
363
+ });
364
+ interface CompleteAttemptRequest {
365
+ attemptId: string;
366
+ grade?: Grade;
367
+ outcome?: 'gave-up';
368
+ }
369
+ interface RecordAttemptRequest {
370
+ requestId: string;
371
+ exerciseId: UnitId;
372
+ grade: Grade;
373
+ at?: EpochMs;
374
+ source?: AttemptSource;
375
+ }
376
+ interface RecordResultDto {
377
+ eventId: string;
378
+ exerciseId: UnitId;
379
+ grade: Grade;
380
+ at: EpochMs;
381
+ duplicate: boolean;
382
+ affected: UnitScoreDto[];
383
+ /** Только если порог ремедиации пересечён именно этой попыткой (§4.3). */
384
+ remediation?: RemediationDto;
385
+ }
386
+ interface FrontierRequest extends PageRequest {
387
+ courseId?: UnitId;
388
+ }
389
+ interface FrontierItemDto {
390
+ lessonId: UnitId;
391
+ courseId: UnitId;
392
+ exerciseCount: number;
393
+ }
394
+ interface DueRequest extends PageRequest {
395
+ minNeed?: number;
396
+ /** Область курсов (как `PlacementStartRequest.courseIds`): пусто или нет поля — все курсы; неизвестный курс — `NOT_FOUND`. */
397
+ courseIds?: UnitId[];
398
+ }
399
+ interface DueItemDto {
400
+ exerciseId: UnitId;
401
+ lessonId: UnitId;
402
+ /** `1 − retrievability`, 0..1; список отсортирован по убыванию. */
403
+ need: number;
404
+ /** R сейчас, 0..1. */
405
+ retrievability: number;
406
+ score: number;
407
+ lastAttemptAt: EpochMs;
408
+ }
409
+ interface PracticeService {
410
+ /**
411
+ * Хук `session.start`: `EXTENSION_HOOK_FAILED` — расширение отменило
412
+ * старт, сессия не создаётся и `session.started` не уходит.
413
+ */
414
+ startSession(): Promise<{
415
+ sessionId: string;
416
+ startedAt: EpochMs;
417
+ }>;
418
+ /**
419
+ * Окно сообщает, что сессия обучения закончилась: расширения с событием
420
+ * `session.finished` получают его один раз на `sessionId`. Идемпотентна:
421
+ * повтор и неизвестный `sessionId` ничего не отправляют (`emitted: false`).
422
+ * После неё следующий `getBatch` начинает новую сессию.
423
+ */
424
+ finishSession(req: {
425
+ sessionId: string;
426
+ }): Promise<{
427
+ emitted: boolean;
428
+ }>;
429
+ /**
430
+ * Хуки расширений: `session.start`, если батч открывает сессию, и
431
+ * `practice.batch` до возврата батча; `EXTENSION_HOOK_FAILED` — хук
432
+ * отменил операцию или ответил неверно, батч не возвращается, сессия и
433
+ * журнал не меняются.
434
+ */
435
+ getBatch(req?: BatchRequest): Promise<BatchDto>;
436
+ beginAttempt(req: {
437
+ exerciseId: UnitId;
438
+ }): Promise<AttemptDto>;
439
+ submitAnswer(req: SubmitAnswerRequest): Promise<VerdictDto>;
440
+ completeAttempt(req: CompleteAttemptRequest): Promise<RecordResultDto>;
441
+ recordAttempt(req: RecordAttemptRequest): Promise<RecordResultDto>;
442
+ getUnitScore(unitId: UnitId): Promise<UnitScoreDto>;
443
+ getAttempts(exerciseId: UnitId, req?: PageRequest): Promise<Page<AttemptRecordDto>>;
444
+ getProgress(query?: ProgressQuery, req?: PageRequest): Promise<Page<ProgressNodeDto>>;
445
+ getFrontier(req?: FrontierRequest): Promise<Page<FrontierItemDto>>;
446
+ getDue(req?: DueRequest): Promise<Page<DueItemDto>>;
447
+ resetProgress(req: {
448
+ unitId: UnitId;
449
+ requestId: string;
450
+ }): Promise<{
451
+ eventId: string;
452
+ duplicate: boolean;
453
+ }>;
454
+ /**
455
+ * Отменяет попытку: она перестаёт влиять на оценки, награды, фронтир,
456
+ * повторы, ремедиацию и статистику; в журнал пишется запись `retract`
457
+ * (`op: 'set'`), попытка остаётся в нём. `targetId` — `id` попытки
458
+ * (`eventId` результата записи) либо `requestId` завершённого входного
459
+ * теста: тогда отменяется вся его пачка. Повтор с тем же `requestId` ничего
460
+ * не пишет (`duplicate: true`); цель уже отменена — запись не пишется,
461
+ * `changed: false`. Неизвестная цель — `NOT_FOUND`.
462
+ */
463
+ undo(req: RetractRequest): Promise<RetractResult>;
464
+ /** Возвращает отменённую `undo` цель (`op: 'unset'`); семантика та же. */
465
+ redo(req: RetractRequest): Promise<RetractResult>;
466
+ }
467
+ interface RetractRequest {
468
+ targetId: string;
469
+ requestId: string;
470
+ }
471
+ interface RetractResult {
472
+ /** `id` записи отмены; `null`, если запись не понадобилась (`changed: false`). */
473
+ eventId: string | null;
474
+ /** Повтор `requestId`: прежний результат. */
475
+ duplicate: boolean;
476
+ /** Состояние цели изменилось. */
477
+ changed: boolean;
478
+ }
479
+ interface PlanRequest {
480
+ /** 1..200 (граница — самый большой замеренный размер плана, §10). */
481
+ maxItems: number;
482
+ /** uint32; при равных (состояние, seed) план одинаков. Без `seed` хост берёт его из `Rng` и возвращает в `DayPlanDto.seed`. */
483
+ seed?: number;
484
+ /** Область курсов: в план попадают упражнения (в том числе ремедиация) только этих курсов, порядок и чередование считаются внутри области. Пусто или нет поля — все курсы; неизвестный курс — `NOT_FOUND`. При равных (состояние, seed, область) план одинаков. */
485
+ courseIds?: UnitId[];
486
+ }
487
+ interface PlanCoverDto {
488
+ exerciseId: UnitId;
489
+ credit: number;
490
+ }
491
+ interface PlanItemDto {
492
+ exerciseId: UnitId;
493
+ reason: ItemReason;
494
+ /** Упражнения, чей повтор этот элемент сжимает неявным кредитом; только при `implicitCreditEnabled`. */
495
+ covers?: PlanCoverDto[];
496
+ }
497
+ interface DayPlanDto {
498
+ items: PlanItemDto[];
499
+ /** true, если интерливинг выполним и соблюдён (не более `plan.maxSameCourseRun` подряд из одного курса, общие теги разнесены на `plan.minTagDistance`). */
500
+ interleaveOk: boolean;
501
+ implicitCreditEnabled: boolean;
502
+ seed: number;
503
+ generatedAt: EpochMs;
504
+ }
505
+ interface PlanService {
506
+ /**
507
+ * Расширение с хуком `practice.batch` может переставить, убрать и добавить
508
+ * упражнения до возврата плана; `EXTENSION_HOOK_FAILED` — хук отменил или
509
+ * ответил неверно, план не возвращается.
510
+ */
511
+ getDay(req: PlanRequest): Promise<DayPlanDto>;
512
+ }
513
+ interface PlacementStartRequest {
514
+ /** Пусто или нет поля — все курсы библиотеки. */
515
+ courseIds?: UnitId[];
516
+ /** Максимум проб, целое 1..200 (граница — предположение [ВЫВОД]); иначе `INVALID_ARGUMENT`. */
517
+ budget: number;
518
+ seed?: number;
519
+ }
520
+ interface PlacementStartResult {
521
+ sessionId: string;
522
+ lessonCount: number;
523
+ budget: number;
524
+ seed: number;
525
+ }
526
+ interface PlacementProbeDto {
527
+ probeId: string;
528
+ lessonId: UnitId;
529
+ exerciseId: UnitId;
530
+ }
531
+ /** `grade` — самооценка («пройдено» ⇔ оценка ≥ 3, engine-ts.md §6a.3); `attempt` — итог открытой попытки с проверкой (последний `passed`/`failed`). */
532
+ type PlacementResult = {
533
+ kind: 'grade';
534
+ grade: Grade;
535
+ } | {
536
+ kind: 'attempt';
537
+ attemptId: string;
538
+ };
539
+ interface PlacementAnswerRequest {
540
+ probeId: string;
541
+ result: PlacementResult;
542
+ }
543
+ interface PlacementProgressDto {
544
+ asked: number;
545
+ budget: number;
546
+ /** Темы, ещё не решённые (`p` между порогами); 0 — тест можно завершать. */
547
+ unresolved: number;
548
+ }
549
+ interface PlacementFinishRequest {
550
+ sessionId: string;
551
+ requestId: string;
552
+ }
553
+ interface PlacementSummaryDto {
554
+ /** Идентификаторы уроков. */
555
+ known: UnitId[];
556
+ unknown: UnitId[];
557
+ uncertain: UnitId[];
558
+ /** Фронтир, вычисленный из `known`; завышен за счёт `uncertain`-границы. */
559
+ frontier: UnitId[];
560
+ /** Записано попыток `source: 'placement'`: по 2 на упражнение каждого `known`-урока. */
561
+ attemptsWritten: number;
562
+ duplicate: boolean;
563
+ }
564
+ interface PlacementStepResult {
565
+ /** Ответ снят или возвращён; `false` — снимать (возвращать) было нечего. */
566
+ changed: boolean;
567
+ progress: PlacementProgressDto;
568
+ }
569
+ interface PlacementService {
570
+ start(req: PlacementStartRequest): Promise<PlacementStartResult>;
571
+ /** `null` — проб больше нет (бюджет исчерпан или все темы решены). До ответа на выданную пробу возвращает ту же пробу. */
572
+ nextProbe(sessionId: string): Promise<PlacementProbeDto | null>;
573
+ answer(req: PlacementAnswerRequest): Promise<PlacementProgressDto>;
574
+ /**
575
+ * Снимает последний ответ открытой сессии: следующая `nextProbe` выдаёт ту
576
+ * же тему. Каждый вызов — один шаг назад. Завершённая, прерванная и
577
+ * неизвестная сессия — `PLACEMENT_SESSION_NOT_FOUND`.
578
+ */
579
+ undo(sessionId: string): Promise<PlacementStepResult>;
580
+ /** Возвращает последний снятый `undo` ответ; новый `answer` сбрасывает возврат. */
581
+ redo(sessionId: string): Promise<PlacementStepResult>;
582
+ finish(req: PlacementFinishRequest): Promise<PlacementSummaryDto>;
583
+ abort(req: {
584
+ sessionId: string;
585
+ }): Promise<void>;
586
+ }
587
+ interface RemediationStepDto {
588
+ /** Пререквизит-юнит (урок); в порядке `engine.keyPrerequisites`, иначе прямые зависимости урока с наименьшей R. */
589
+ unitId: UnitId;
590
+ source: 'key-prerequisite' | 'lesson-dependency';
591
+ /** Упражнения шага: наименьшая R, не начатые — по порядку id; всего по плану не больше `remediation.maxItems`. */
592
+ exerciseIds: UnitId[];
593
+ /** Успех на каждом упражнении шага после триггера. */
594
+ done: boolean;
595
+ }
596
+ interface RemediationDto {
597
+ exerciseId: UnitId;
598
+ /** true — триггер сработал и не снят. */
599
+ active: boolean;
600
+ triggeredAt?: EpochMs;
601
+ steps: RemediationStepDto[];
602
+ }
603
+ interface RemediationService {
604
+ getPlan(req: {
605
+ exerciseId: UnitId;
606
+ }): Promise<RemediationDto>;
607
+ }
608
+ interface FlagService {
609
+ list(req?: PageRequest): Promise<Page<UnitId>>;
610
+ has(unitId: UnitId): Promise<boolean>;
611
+ add(unitId: UnitId): Promise<void>;
612
+ remove(unitId: UnitId): Promise<void>;
613
+ /** Раскрывает префикс в id на момент вызова и пишет по записи на юнит. */
614
+ removePrefix(prefix: string): Promise<{
615
+ removed: UnitId[];
616
+ }>;
617
+ }
618
+ interface SavedFilterDto {
619
+ id: string;
620
+ description: string;
621
+ filter: UnitFilterWire;
622
+ }
623
+ interface FilterStoreService {
624
+ list(): Promise<Array<{
625
+ id: string;
626
+ description: string;
627
+ }>>;
628
+ get(id: string): Promise<SavedFilterDto>;
629
+ save(filter: SavedFilterDto): Promise<void>;
630
+ delete(id: string): Promise<void>;
631
+ }
632
+ interface SessionStoreService {
633
+ list(): Promise<Array<{
634
+ id: string;
635
+ description: string;
636
+ }>>;
637
+ get(id: string): Promise<StudySessionWire>;
638
+ save(session: StudySessionWire): Promise<void>;
639
+ delete(id: string): Promise<void>;
640
+ }
641
+ interface CurationService {
642
+ blacklist: FlagService;
643
+ reviewList: FlagService;
644
+ filters: FilterStoreService;
645
+ sessions: SessionStoreService;
646
+ }
647
+ interface MasteryWindowDto {
648
+ percentage: number;
649
+ range: [number, number];
650
+ }
651
+ interface ImplicitCreditOptionsDto {
652
+ /** По умолчанию false: выигрыш измерен только в круговой модели (engine-ts.md §6a.2). */
653
+ enabled: boolean;
654
+ /** Затухание по глубине охвата, 0 < λ ≤ 1 [диапазон — ВЫВОД]. */
655
+ lambda: number;
656
+ /** Кредит ниже порога отбрасывается, 0 < minCredit ≤ 1 [диапазон — ВЫВОД]. */
657
+ minCredit: number;
658
+ /** Множитель на кредит. Не измерен: подбирается A/B на реальных ответах [НЕ ПОДТВЕРЖДЕНО]. */
659
+ kappa: number;
660
+ }
661
+ interface RemediationOptionsDto {
662
+ failThreshold: number;
663
+ maxItems: number;
664
+ }
665
+ interface PlanOptionsDto {
666
+ targetRetention: number;
667
+ minNewFraction: number;
668
+ maxSameCourseRun: number;
669
+ minTagDistance: number;
670
+ }
671
+ interface SchedulerOptionsDto {
672
+ batchSize: number;
673
+ relearnFraction: number;
674
+ masteryWindows: {
675
+ new: MasteryWindowDto;
676
+ target: MasteryWindowDto;
677
+ current: MasteryWindowDto;
678
+ easy: MasteryWindowDto;
679
+ mastered: MasteryWindowDto;
680
+ };
681
+ passingScore: {
682
+ minScore: number;
683
+ minFraction: number;
684
+ minAvgTrials: number;
685
+ };
686
+ supersedingScore: number;
687
+ numTrials: number;
688
+ numRewards: number;
689
+ maxLessonsInProgress: number;
690
+ /** Неявный повтор (FIRe), по умолчанию `{ enabled: false, lambda: 0.9, minCredit: 0.2, kappa: 1 }`. */
691
+ implicitCredit: ImplicitCreditOptionsDto;
692
+ /** По умолчанию `{ failThreshold: 2, maxItems: 3 }` — предположения [НЕ ПОДТВЕРЖДЕНО]. */
693
+ remediation: RemediationOptionsDto;
694
+ /** По умолчанию `{ targetRetention: 0.9, minNewFraction: 0.25, maxSameCourseRun: 2, minTagDistance: 2 }`. */
695
+ plan: PlanOptionsDto;
696
+ }
697
+ type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]; };
698
+ interface ScorerInfoDto {
699
+ kind: 'fsrs-hybrid' | 'power-law';
700
+ memoryModelId: string;
701
+ ratingMap: 'runner' | 'anki';
702
+ numTrials: number;
703
+ parametersHash: string;
704
+ }
705
+ interface PreferencesDto {
706
+ ignoredPaths: string[];
707
+ schedulerBatchSize?: number;
708
+ }
709
+ /** `system` — язык системы; renderer сам выбирает из поддерживаемых. */
710
+ type LocaleMode = 'system' | 'ru' | 'en';
711
+ /** Настройки интерфейса; хранятся вместе с остальными настройками в `engine.db`. */
712
+ interface UiSettingsDto {
713
+ /** Встроенный режим или id темы расширения. */
714
+ theme: string;
715
+ locale: LocaleMode;
716
+ /** Курс в фокусе: клиент передаёт его в `courseIds` плана и повторений. Нет поля — все курсы. Движок не проверяет, что курс есть в библиотеке: курс могли убрать, клиент сверяет сам. */
717
+ activeCourseId?: UnitId;
718
+ /** Ширина панели теории в сессии и вход-тесте, px. Нет поля — умолчание клиента. */
719
+ materialWidth?: number;
720
+ /** Панель теории скрыта. Нет поля — показана. */
721
+ materialCollapsed?: true;
722
+ /** Исход обучающих туров по их id; нет записи — тур ещё не предлагали. */
723
+ tours?: Record<string, TourStatus>;
724
+ }
725
+ /** Как закончился тур: дошёл до конца или пропущен. */
726
+ type TourStatus = 'completed' | 'skipped';
727
+ /**
728
+ * `activeCourseId: null` снимает фокус, `materialWidth: null` возвращает умолчание,
729
+ * `materialCollapsed: false` показывает панель; `tours` меняет только перечисленные
730
+ * ключи, `null` удаляет запись о туре.
731
+ */
732
+ type UiSettingsPatch = Partial<Omit<UiSettingsDto, 'activeCourseId' | 'materialWidth' | 'materialCollapsed' | 'tours'>> & {
733
+ activeCourseId?: UnitId | null;
734
+ materialWidth?: number | null;
735
+ materialCollapsed?: boolean;
736
+ tours?: Record<string, TourStatus | null>;
737
+ };
738
+ /** Настройки обучения; хранятся вместе с остальными настройками в `engine.db`. */
739
+ interface LearningSettingsDto {
740
+ /** `passAtN` или id правила оценки расширения. */
741
+ gradePolicy: string;
742
+ }
743
+ interface SettingsService {
744
+ getScheduler(): Promise<SchedulerOptionsDto>;
745
+ /** Валидирует (`verify` как при открытии), применяет ко всем компонентам сразу. */
746
+ setScheduler(patch: DeepPartial<SchedulerOptionsDto>): Promise<SchedulerOptionsDto>;
747
+ resetScheduler(): Promise<SchedulerOptionsDto>;
748
+ getPreferences(): Promise<PreferencesDto>;
749
+ setPreferences(prefs: PreferencesDto): Promise<{
750
+ restartRequired: boolean;
751
+ }>;
752
+ getScorer(): Promise<ScorerInfoDto>;
753
+ getUi(): Promise<UiSettingsDto>;
754
+ /** Валидирует и сохраняет; возвращает итоговые настройки. */
755
+ setUi(patch: UiSettingsPatch): Promise<UiSettingsDto>;
756
+ getLearning(): Promise<LearningSettingsDto>;
757
+ /** Валидирует вид id (существование правила не проверяется) и сохраняет; возвращает итоговые настройки. */
758
+ setLearning(patch: Partial<LearningSettingsDto>): Promise<LearningSettingsDto>;
759
+ getKeybindings(): Promise<KeybindingsSettingsDto>;
760
+ /**
761
+ * Применяет патч целиком или не применяет: набор команды заменяется,
762
+ * `null` возвращает умолчания. Отклоняет (`INVALID_ARGUMENT`,
763
+ * `details.field`/`reason`/`command`/`other`) неверные клавиши и условия,
764
+ * превышение лимитов, повторы и пересечения пользовательских привязок
765
+ * разных команд. Возвращает итоговые привязки.
766
+ */
767
+ setKeybindings(patch: KeybindingsPatch): Promise<KeybindingsSettingsDto>;
768
+ }
769
+ /** Привязка пользователя: запись клавиш (`Mod+Shift+L`, `Mod+K Mod+S`) и условие `when` (`null` — без условия). */
770
+ interface KeybindingEntryDto {
771
+ key: string;
772
+ when: string | null;
773
+ }
774
+ /** Пользовательские привязки по ключам команд (`app:<id>`, `extension:<extensionId>:<id>`); набор заменяет привязки команды из кода и расширений целиком, пустой — «снято». Хранятся в `engine.db`. */
775
+ interface KeybindingsSettingsDto {
776
+ commands: Record<string, KeybindingEntryDto[]>;
777
+ }
778
+ /** Ключ команды → новый набор или `null` (сбросить к умолчаниям). */
779
+ type KeybindingsPatch = Record<string, KeybindingEntryDto[] | null>;
780
+ /** Вектор для дельта-экспорта: `{deviceId: contiguous}` — непрерывный префикс seq (1..contiguous без пропусков), не `maxSeq`. */
781
+ type StateVector = Record<string, number>;
782
+ /** Дыры за префиксом по устройствам: seq, записи которых уже есть, но предыдущих нет. */
783
+ type MissingSeqs = Record<string, number[]>;
784
+ /**
785
+ * `at` — время события по HLC-правилу (engine-ts.md §5.1): `max(min(now, now + 5 мин), maxAtУвиденный + 1, свойПрошлыйAt)`.
786
+ * Порядок везде `(at, deviceId, seq)`, при равных `(deviceId, seq)` — `id`. `recordedAt` — wall-clock записи, порядок не задаёт.
787
+ */
788
+ interface LogEntryBaseDto {
789
+ id: string;
790
+ deviceId: string;
791
+ seq: number;
792
+ at: EpochMs;
793
+ recordedAt: EpochMs;
794
+ }
795
+ interface AttemptEntryDto extends LogEntryBaseDto {
796
+ kind: 'attempt';
797
+ exerciseId: UnitId;
798
+ grade: Grade;
799
+ source: AttemptSource;
800
+ }
801
+ interface UnitFlagEntryDto extends LogEntryBaseDto {
802
+ kind: 'unit_flag';
803
+ unitId: UnitId;
804
+ flag: 'blacklist' | 'review';
805
+ op: 'set' | 'unset';
806
+ }
807
+ interface ProgressResetEntryDto extends LogEntryBaseDto {
808
+ kind: 'progress_reset';
809
+ unitId: UnitId;
810
+ /** `revision` библиотеки на момент записи: диагностика расхождения версий курса между устройствами. */
811
+ libraryRevision?: string;
812
+ }
813
+ interface RetractEntryDto extends LogEntryBaseDto {
814
+ kind: 'retract';
815
+ /** `id` попытки или общая часть `id` пачки `<targetId>#<i>`. */
816
+ targetId: string;
817
+ op: 'set' | 'unset';
818
+ }
819
+ type LogEntryDto = AttemptEntryDto | UnitFlagEntryDto | ProgressResetEntryDto | RetractEntryDto;
820
+ interface SyncStateDto {
821
+ deviceId: string;
822
+ vector: StateVector;
823
+ missing: MissingSeqs;
824
+ entryCount: number;
825
+ /** Неразрешённые конфликты. */
826
+ conflictCount: number;
827
+ }
828
+ interface ExportRequest {
829
+ since?: StateVector;
830
+ limit?: number;
831
+ }
832
+ interface ExportResult {
833
+ entries: LogEntryDto[];
834
+ next?: StateVector;
835
+ }
836
+ interface ImportResult {
837
+ inserted: number;
838
+ duplicates: number;
839
+ /** Структурно неверные записи. Конфликты (`id-content`, `seq-two-ids`, `clock-skew`) сюда не попадают. */
840
+ rejected: Array<{
841
+ id: string;
842
+ reason: string;
843
+ }>;
844
+ /** Новые конфликты, обнаруженные этим импортом. */
845
+ conflicts: number;
846
+ rebuilt: boolean;
847
+ }
848
+ interface RebuildResult {
849
+ entries: number;
850
+ ms: number;
851
+ }
852
+ interface TraneImportResult {
853
+ attempts: number;
854
+ flags: number;
855
+ skipped: number;
856
+ }
857
+ interface SyncConflictDto {
858
+ conflictId: string;
859
+ /**
860
+ * `id-content` — тот же `id`, другое содержимое; `seq-two-ids` — тот же `(deviceId, seq)`, другой `id`;
861
+ * `clock-skew` — запись с `at > recordedAt + 24 ч` (карантин по часам) [ВЫВОД, в спайке не реализовано].
862
+ */
863
+ reason: 'id-content' | 'seq-two-ids' | 'clock-skew';
864
+ /** Все стороны конфликта; скрыты от проекций, пока конфликт открыт. */
865
+ entries: LogEntryDto[];
866
+ /**
867
+ * `entryHash` (sha256 канонического JSON записи) каждой стороны — в том же порядке, что и `entries`.
868
+ * В `id-content` у сторон общий `id`; конкретную сторону выбирают по `entryHash` в `ResolveConflictRequest.keep`.
869
+ */
870
+ entryHashes: string[];
871
+ detectedAt: EpochMs;
872
+ }
873
+ interface ResolveConflictRequest {
874
+ conflictId: string;
875
+ /**
876
+ * `id` записи либо её `entryHash` (из `SyncConflictDto.entryHashes`), которую вернуть в проекции;
877
+ * `'none'` — оставить скрытыми все. В `id-content` у сторон общий `id` — по `id` берётся первая сторона
878
+ * в каноническом порядке, точную сторону выбирают по `entryHash`.
879
+ */
880
+ keep: string | 'none';
881
+ }
882
+ interface ResolveConflictResult {
883
+ conflictId: string;
884
+ kept: string | null;
885
+ rebuilt: boolean;
886
+ }
887
+ interface FolderSyncConfigureRequest {
888
+ dir: string;
889
+ }
890
+ interface FolderSyncReport {
891
+ inserted: number;
892
+ duplicates: number;
893
+ /** Число отвергнутых записей (у `sync.import` — массив с причинами). */
894
+ rejected: number;
895
+ /** Сегменты, которые не применены и повторятся в следующем раунде. */
896
+ pending: number;
897
+ headErrors: string[];
898
+ corruptSegments: string[];
899
+ pendingSegments: string[];
900
+ /** Применённые сегменты `device/name`. */
901
+ applied: string[];
902
+ /** Опубликованный хвост своего журнала. */
903
+ published: {
904
+ segments: number;
905
+ entries: number;
906
+ };
907
+ conflicts: number;
908
+ rebuilt: boolean;
909
+ }
910
+ interface FolderRestoreResult {
911
+ /** `catch-up` — свой хвост в папке длиннее локального: догнать и продолжить с `maxSeq + 1`; `fork` — данных нет нигде: новый `deviceId`. */
912
+ action: 'none' | 'catch-up' | 'fork';
913
+ newDeviceId?: string;
914
+ }
915
+ interface FolderSyncService {
916
+ /** Запоминает общую папку сегментов в `dataDir/settings/sync.json`; каталог должен существовать. */
917
+ configure(req: FolderSyncConfigureRequest): Promise<{
918
+ dir: string;
919
+ }>;
920
+ /** Один раунд: публикация своего хвоста, затем применение чужих сегментов. */
921
+ sync(): Promise<FolderSyncReport>;
922
+ /** Вызывать при старте и после восстановления из бэкапа. */
923
+ checkRestore(): Promise<FolderRestoreResult>;
924
+ }
925
+ interface SyncService {
926
+ getState(): Promise<SyncStateDto>;
927
+ exportSince(req?: ExportRequest): Promise<ExportResult>;
928
+ import(entries: LogEntryDto[]): Promise<ImportResult>;
929
+ rebuild(): Promise<RebuildResult>;
930
+ importFromTrane(req: {
931
+ traneDir: string;
932
+ }): Promise<TraneImportResult>;
933
+ getConflicts(req?: PageRequest): Promise<Page<SyncConflictDto>>;
934
+ resolveConflict(req: ResolveConflictRequest): Promise<ResolveConflictResult>;
935
+ readonly folder: FolderSyncService;
936
+ }
937
+ type EngineEvent = {
938
+ type: 'progress';
939
+ unitIds: UnitId[];
940
+ at: EpochMs;
941
+ } | {
942
+ type: 'library-reloaded';
943
+ revision: string;
944
+ errors: number;
945
+ warnings: number;
946
+ } | {
947
+ type: 'library-compiled';
948
+ revision: string;
949
+ artifactWritten: boolean;
950
+ errors: number;
951
+ warnings: number;
952
+ } | {
953
+ type: 'state-rebuilt';
954
+ entries: number;
955
+ ms: number;
956
+ } | {
957
+ type: 'sync-conflict';
958
+ conflictIds: string[];
959
+ unresolved: number;
960
+ } | {
961
+ type: 'remediation-triggered';
962
+ exerciseId: UnitId;
963
+ steps: number;
964
+ at: EpochMs;
965
+ } | {
966
+ type: 'settings-changed';
967
+ scope: 'scheduler' | 'preferences' | 'filters' | 'sessions' | 'blacklist' | 'reviewList' | 'ui' | 'learning' | 'extensions' | 'keybindings' |
968
+ /** Значения настроек или хранилище расширения; `extensionId` — чьи. */
969
+ 'extensionValues';
970
+ extensionId?: string;
971
+ } | {
972
+ type: 'extensions-changed';
973
+ } |
974
+ /**
975
+ * Здоровье расширений или состояние хоста расширений изменилось (сбой,
976
+ * активация, приостановка, перезапуск хоста): окно перечитывает
977
+ * `extensions.diagnostics()`.
978
+ */
979
+ {
980
+ type: 'extension-health-changed';
981
+ } |
982
+ /**
983
+ * Набор вкладов расширений изменился: движок и хост расширений закончили
984
+ * применять установку, удаление, включение, доверие или правку в режиме
985
+ * разработчика. Окно перечитывает `extensions.contributions()`.
986
+ */
987
+ {
988
+ type: 'contributions-changed';
989
+ generation: number;
990
+ } | {
991
+ type: 'repository-progress';
992
+ id: string;
993
+ phase: RepositoryPhase;
994
+ /** Байты или объекты — по фазе; `total` неизвестен, пока сервер его не сообщил. */
995
+ loaded?: number;
996
+ total?: number;
997
+ } |
998
+ /**
999
+ * Закончилась проверка обновлений репозиториев курсов (`repositories.checkUpdates`
1000
+ * или проверка при запуске). `available` — `id` репозиториев с `availableCommit`
1001
+ * в порядке `repositories.list()`; пусто, если обновлений нет. Окно перечитывает
1002
+ * `repositories.list()`. Не приходит, если не проверялся ни один репозиторий.
1003
+ */
1004
+ {
1005
+ type: 'repository-updates-checked';
1006
+ available: string[];
1007
+ };
1008
+ interface EngineDiagnosticsDto {
1009
+ contractVersion: number;
1010
+ engineVersion: string;
1011
+ uptimeMs: number;
1012
+ entryCount: number;
1013
+ dbBytes?: number;
1014
+ timings: {
1015
+ openLibraryMs: number;
1016
+ rebuildMs: number;
1017
+ batch: {
1018
+ count: number;
1019
+ p50Ms: number;
1020
+ p95Ms: number;
1021
+ };
1022
+ recordAttemptP95Ms: number;
1023
+ };
1024
+ cache: {
1025
+ exerciseHitRatio: number;
1026
+ entries: number;
1027
+ };
1028
+ dirty: boolean;
1029
+ }
1030
+ /** Этап `repositories.add` / `repositories.update` (событие `repository-progress`). */
1031
+ type RepositoryPhase = 'resolve' | 'fetch' | 'export' | 'validate' | 'reload';
1032
+ /** `updating` — идёт операция; `error` — последняя операция отклонена или снимок пропал (`lastError`). */
1033
+ type RepositoryStatus = 'ready' | 'updating' | 'error';
1034
+ /** Git-репозиторий с курсами: снимок коммита лежит в `<libraryRoot>/repositories/<id>`. */
1035
+ interface RepositoryDto {
1036
+ /** Стабильный slug нормализованного URL. */
1037
+ id: string;
1038
+ /** Нормализованный URL (`http(s)`, без учётных данных). */
1039
+ url: string;
1040
+ /** Ветка или тег; `null` — ветка по умолчанию удалённого репозитория. */
1041
+ ref: string | null;
1042
+ /** Полный SHA-1 загруженного коммита. */
1043
+ commit: string;
1044
+ fetchedAt: EpochMs;
1045
+ status: RepositoryStatus;
1046
+ /** Курсы, пришедшие из этого репозитория. */
1047
+ courseIds: UnitId[];
1048
+ /** Курсы загруженного коммита, которых нет в библиотеке из-за выбора ученика; у репозитория без выбора пусто. */
1049
+ skippedCourseIds: UnitId[];
1050
+ lastError?: EngineErrorDto;
1051
+ /**
1052
+ * Коммит на сервере, если он отличается от загруженного (последняя проверка
1053
+ * этого запуска движка, в `engine.db` не пишется); нет — обновления нет или
1054
+ * проверки ещё не было.
1055
+ */
1056
+ availableCommit?: string;
1057
+ /** Когда репозиторий в последний раз успешно сверен с сервером в этом запуске; нет — не сверялся. */
1058
+ checkedAt?: EpochMs;
1059
+ }
1060
+ /** Вход `repositories.preview`: как у `add`, без выбора курсов. */
1061
+ interface PreviewRepositoryRequest {
1062
+ url: string;
1063
+ ref?: string;
1064
+ }
1065
+ interface AddRepositoryRequest {
1066
+ url: string;
1067
+ ref?: string;
1068
+ /**
1069
+ * Курсы репозитория, которые нужно поставить (непустой список без повторов).
1070
+ * Нет поля — все курсы коммита, и новые курсы при `update` тоже ставятся.
1071
+ */
1072
+ courseIds?: UnitId[];
1073
+ /** `RepositoryPreviewDto.previewId` того же адреса и ветки: установка без загрузки. */
1074
+ previewId?: string;
1075
+ }
1076
+ interface UpdateRepositoryOptions {
1077
+ /** Новый выбор курсов (как `AddRepositoryRequest.courseIds`); нет поля — прежний. */
1078
+ courseIds?: UnitId[];
1079
+ /** `RepositoryPreviewDto.previewId` того же репозитория: установка без загрузки. */
1080
+ previewId?: string;
1081
+ }
1082
+ interface RemoveRepositoryOptions {
1083
+ /**
1084
+ * Сбросить прогресс курсов репозитория (`progress_reset` на каждый курс из
1085
+ * `RepositoryDto.courseIds`, на других устройствах — после синхронизации).
1086
+ * По умолчанию `false`: журнал не меняется, прогресс вернётся при повторном
1087
+ * добавлении.
1088
+ */
1089
+ removeProgress?: boolean;
1090
+ }
1091
+ /** Курс репозитория в предпросмотре (`repositories.preview`). */
1092
+ interface RepositoryCourseDto {
1093
+ id: UnitId;
1094
+ title: string;
1095
+ /** Каталог курса от корня репозитория. */
1096
+ path: string;
1097
+ lessonCount: number;
1098
+ /**
1099
+ * Курсы того же репозитория, без которых этот не загрузится: зависимости,
1100
+ * `superseded` и `encompassed` на юниты других курсов, курсы-предки по
1101
+ * вложенности каталогов.
1102
+ */
1103
+ requires: UnitId[];
1104
+ /** Диагностики сканера в каталоге курса. */
1105
+ errors: number;
1106
+ warnings: number;
1107
+ /** До пяти первых текстов ошибок; не переводятся. */
1108
+ messages: string[];
1109
+ /** Курс уже пришёл из этого репозитория (запись реестра с тем же URL). */
1110
+ installed: boolean;
1111
+ /** Курс с таким `id` уже есть в библиотеке из другого источника. */
1112
+ inLibrary: boolean;
1113
+ }
1114
+ interface RepositoryPreviewDto {
1115
+ /** Нормализованный URL. */
1116
+ url: string;
1117
+ ref: string | null;
1118
+ /** Полный SHA-1 просмотренного коммита. */
1119
+ commit: string;
1120
+ /** Все курсы коммита в порядке обхода каталогов. */
1121
+ courses: RepositoryCourseDto[];
1122
+ /**
1123
+ * Токен скачанного снимка: движок держит его до 5 минут (не больше двух
1124
+ * снимков) и ставит курсы из него, если передать токен в `add` или `update`.
1125
+ * Токен одноразовый; просроченный, израсходованный или чужой токен не
1126
+ * ошибка — репозиторий скачивается заново.
1127
+ */
1128
+ previewId: string;
1129
+ }
1130
+ interface UpdateRepositoryResult {
1131
+ /** `false` — коммит на сервере совпал с загруженным, ничего не скачивалось. */
1132
+ changed: boolean;
1133
+ repository: RepositoryDto;
1134
+ }
1135
+ interface RepositoriesService {
1136
+ list(): Promise<RepositoryDto[]>;
1137
+ /**
1138
+ * Скачивает репозиторий во временный каталог и возвращает его курсы для
1139
+ * выбора; библиотека, реестр и каталоги библиотеки не меняются.
1140
+ * `INVALID_ARGUMENT`, `GIT_FETCH_FAILED`, `REPOSITORY_REJECTED` (правила снимка).
1141
+ */
1142
+ preview(req: PreviewRepositoryRequest): Promise<RepositoryPreviewDto>;
1143
+ /**
1144
+ * `INVALID_ARGUMENT`, `REPOSITORY_EXISTS`, `GIT_FETCH_FAILED`,
1145
+ * `REPOSITORY_REJECTED` (в том числе `unknown-course` и `missing-requirement`
1146
+ * при `courseIds`).
1147
+ */
1148
+ add(req: AddRepositoryRequest): Promise<RepositoryDto>;
1149
+ /**
1150
+ * Обновляет репозиторий; `options.courseIds` заменяет выбор курсов.
1151
+ * `NOT_FOUND`, `INVALID_ARGUMENT`, `GIT_FETCH_FAILED`, `REPOSITORY_REJECTED`.
1152
+ */
1153
+ update(id: string, options?: UpdateRepositoryOptions): Promise<UpdateRepositoryResult>;
1154
+ /**
1155
+ * Снимок и запись удаляются, курсы пропадают из библиотеки. С
1156
+ * `options.removeProgress` прогресс курсов сбрасывается (журнал только
1157
+ * дополняется), иначе журнал не меняется. `NOT_FOUND`, `INVALID_ARGUMENT`.
1158
+ */
1159
+ remove(id: string, options?: RemoveRepositoryOptions): Promise<void>;
1160
+ /** `true`, если операция над репозиторием шла и прервана. */
1161
+ cancel(id: string): Promise<boolean>;
1162
+ /**
1163
+ * Сверяет коммиты репозиториев с сервером без скачивания объектов и
1164
+ * возвращает то же, что `list()`. Недоступный репозиторий пропускается
1165
+ * (его прежний результат остаётся), репозиторий с идущей операцией не
1166
+ * проверяется; вызов не падает из-за сети. Публикует `repository-updates-checked`.
1167
+ */
1168
+ checkUpdates(): Promise<RepositoryDto[]>;
1169
+ }
1170
+ type ExtensionOriginDto = 'bundled' | 'user' | 'dev';
1171
+ /**
1172
+ * Подпись расширения: строка без перевода (показывается как есть) или тексты
1173
+ * по языкам (`en` обязателен и служит запасным). Тот же тип, что `LocalizedText`
1174
+ * в `@dolphy-app/extension-api` (контракт от него не зависит).
1175
+ */
1176
+ type LocalizedTextDto = string | {
1177
+ readonly en: string;
1178
+ readonly ru?: string;
1179
+ };
1180
+ /**
1181
+ * Состояние расширения. `dependencies-unmet` — включено, но не загружено:
1182
+ * зависимость отсутствует, отключена, не загружена или не подходит по версии
1183
+ * (причины — в `diagnostics`), вкладов нет. Отключение пользователем и
1184
+ * безопасный режим важнее: такое расширение — `disabled`.
1185
+ */
1186
+ type ExtensionStateDto = 'loaded' | 'overridden' | 'invalid' | 'disabled' | 'dependencies-unmet';
1187
+ /** Закрытый список кодов диагностик расширения; интерфейс строит текст по коду и данным. */
1188
+ declare const EXTENSION_DIAGNOSTIC_CODES: readonly ["manifest-unreadable", "manifest-invalid", "id-mismatch", "requires-app", "unavailable-platform", "claim-clash", "load-failed", "overridden-by", "safe-mode", "dependency-missing", "dependency-disabled", "dependency-version", "dependency-unmet", "dependency-cycle"];
1189
+ type ExtensionDiagnosticCode = (typeof EXTENSION_DIAGNOSTIC_CODES)[number];
1190
+ /** Значения `data` диагностики: строки, числа и списки строк. */
1191
+ type ExtensionDiagnosticValue = string | number | string[];
1192
+ /**
1193
+ * Причина состояния расширения. Данные по кодам:
1194
+ * `manifest-unreadable` — `reason`; `manifest-invalid` — `issues` (`путь: сообщение`);
1195
+ * `id-mismatch` — `expected`, `actual`; `requires-app` — `minAppVersion`;
1196
+ * `unavailable-platform` — `platform`; `claim-clash` — `kind`, `name`, `by`;
1197
+ * `load-failed` — `reason` (код не загрузился или `server` не уложился в срок, регистрация расширения отвергнута целиком); `overridden-by` — `origin`, `version`; `safe-mode` — без данных;
1198
+ * `dependency-missing` — `id`, `range` (нет, если диапазон не задан): расширения с таким id нет;
1199
+ * `dependency-disabled` — `id`, `range`: зависимость отключена пользователем, отозвана или безопасным режимом;
1200
+ * `dependency-version` — `id`, `range`, `found`: установлена версия вне диапазона;
1201
+ * `dependency-unmet` — `id`, `range`: зависимость включена, но сама не загружена (её зависимости не выполнены);
1202
+ * `dependency-cycle` — `cycle` (id расширений цикла): расширения зависят друг от друга.
1203
+ */
1204
+ interface ExtensionDiagnosticDto {
1205
+ code: ExtensionDiagnosticCode;
1206
+ data: Record<string, ExtensionDiagnosticValue>;
1207
+ }
1208
+ interface ExtensionInfoDto {
1209
+ /** Id манифеста; у некорректного расширения — имя каталога. */
1210
+ id: string;
1211
+ /** `null`, если манифест не удалось прочитать. */
1212
+ version: string | null;
1213
+ origin: ExtensionOriginDto;
1214
+ state: ExtensionStateDto;
1215
+ /** Серверные вклады по точкам (id); пусто, если расширение не `loaded`/`overridden`. */
1216
+ contributes: ExtensionContributesDto;
1217
+ /** Почему некорректно, кем перекрыто; пусто у загруженного и отключённого пользователем. */
1218
+ diagnostics: ExtensionDiagnosticDto[];
1219
+ /** `false` у расширений из поставки, перекрытых и некорректных: переключатели недоступны. */
1220
+ toggleable: boolean;
1221
+ /** Название из манифеста; `null` — не задано. */
1222
+ name: string | null;
1223
+ description: string | null;
1224
+ /** GitHub-логин автора из манифеста. */
1225
+ author: string | null;
1226
+ /** Зависимости из манифеста; `[]` — нет или манифест не прочитан. */
1227
+ dependencies: ExtensionDependencyDto[];
1228
+ /** Значок из манифеста как `data:image/png|webp;base64,…`; `null` — значка нет или манифест не прочитан. */
1229
+ icon: string | null;
1230
+ /** Явные теги из манифеста; `[]` — нет или манифест не прочитан. */
1231
+ tags: string[];
1232
+ /** Установлено из каталога; `null` — скопировано вручную, из поставки или из режима разработчика. */
1233
+ installed: ExtensionInstallDto | null;
1234
+ /** `true` у расширений с origin `user`: их можно удалить. */
1235
+ removable: boolean;
1236
+ /** Причина отзыва установленной версии в каталоге; `null` — не отозвана. Отозванное расширение в состоянии `disabled`, включить его нельзя. */
1237
+ revoked: string | null;
1238
+ /**
1239
+ * Предупреждение об устаревании, действующее для установленной версии (по последнему известному
1240
+ * индексу); `null` — расширение не устарело, скопировано вручную или индекса нет. Накладывает сервис
1241
+ * `extensions.list`; это предупреждение, а не отзыв: состояние и политика не меняются.
1242
+ */
1243
+ deprecated: DeprecationDto | null;
1244
+ }
1245
+ /** Зависимость расширения (`dependencies` манифеста): `range` — диапазон версий, `null` — любая. */
1246
+ interface ExtensionDependencyDto {
1247
+ id: string;
1248
+ range: string | null;
1249
+ }
1250
+ /** Альтернатива устаревшему расширению; `name` берётся из индекса каталога. */
1251
+ interface DeprecationAlternativeDto {
1252
+ id: string;
1253
+ /** Название записи каталога; `null` — такой записи в индексе нет. */
1254
+ name: string | null;
1255
+ }
1256
+ /** Расширение помечено устаревшим в каталоге (`deprecated.json`). */
1257
+ interface DeprecationDto {
1258
+ /** Диапазон версий, на которые распространяется пометка; `null` — на все. */
1259
+ versions: string | null;
1260
+ /** Причина, 1–200 символов, на английском. */
1261
+ reason: string;
1262
+ /** До 3 альтернатив. */
1263
+ alternatives: DeprecationAlternativeDto[];
1264
+ }
1265
+ /** Метаданные установки из каталога (файл `.dolphy-install.json` в каталоге расширения). */
1266
+ interface ExtensionInstallDto {
1267
+ catalogUrl: string;
1268
+ version: string;
1269
+ /** ISO-время установки. */
1270
+ installedAt: string;
1271
+ }
1272
+ /** Id серверных вкладов расширения (то, что оно зарегистрировало вызовом `server`). */
1273
+ interface ExtensionContributesDto {
1274
+ exerciseTypes: string[];
1275
+ gradePolicies: string[];
1276
+ /** Id настроек (`server.registerSettings`). */
1277
+ settings: string[];
1278
+ /** Имена событий обучения (`server.on`). */
1279
+ events: string[];
1280
+ /** Id команд (`server.registerCommand`). */
1281
+ commands: string[];
1282
+ /** Id расписаний (`server.schedule`). */
1283
+ schedules: string[];
1284
+ /** Id импортёров (`server.registerImporter`). */
1285
+ importers: string[];
1286
+ /** Id экспортёров (`server.registerExporter`). */
1287
+ exporters: string[];
1288
+ }
1289
+ /**
1290
+ * Привязка команды расширения (`keybindings` регистрации команды). `mac`/`windows`/`linux`
1291
+ * заменяют `key` на своей платформе (`null` — `key`); `when` — условие
1292
+ * (`null` — без условия).
1293
+ */
1294
+ interface ExtensionKeybindingDto {
1295
+ key: string;
1296
+ mac: string | null;
1297
+ windows: string | null;
1298
+ linux: string | null;
1299
+ when: string | null;
1300
+ }
1301
+ /** Серверная команда расширения (`server.registerCommand`). */
1302
+ interface CommandContributionDto {
1303
+ /** Id в пространстве расширения. */
1304
+ id: string;
1305
+ extensionId: string;
1306
+ /** Название в палитре. */
1307
+ title: LocalizedTextDto;
1308
+ description: LocalizedTextDto | null;
1309
+ category: LocalizedTextDto | null;
1310
+ /** Привязки команды (до 4); `[]` — нет. Привязывают только эту команду. */
1311
+ keybindings: ExtensionKeybindingDto[];
1312
+ /** `false` скрывает команду из палитры: её вызывает только панель. */
1313
+ palette: boolean;
1314
+ /**
1315
+ * Условие видимости (`parseWhen` из `@dolphy-app/extension-api`), `null` — всегда.
1316
+ * Пока оно ложно, команды нет в палитре и она не выполняется сочетанием;
1317
+ * расширению она по-прежнему доступна.
1318
+ */
1319
+ when: string | null;
1320
+ /** Имя значка из закрытого списка `EXTENSION_ICONS` (умолчание `puzzle`); окно рисует свой символ, подпись декоративна. */
1321
+ icon: string;
1322
+ }
1323
+ /** Расписание расширения (`server.schedule`): когда приложение запускает обработчик. */
1324
+ interface ScheduleContributionDto {
1325
+ id: string;
1326
+ extensionId: string;
1327
+ /** `daily` — раз в сутки в `at`, `hourly` — в начале каждого часа; по местному времени. */
1328
+ every: 'daily' | 'hourly';
1329
+ /** `HH:MM` у `daily`; `null` у `hourly`. */
1330
+ at: string | null;
1331
+ }
1332
+ /** Импортёр расширения (`server.registerImporter`): файл пользователя → каталог курса. */
1333
+ interface ImporterContributionDto {
1334
+ id: string;
1335
+ extensionId: string;
1336
+ /** Название в палитре и карточке «Библиотеки». */
1337
+ title: LocalizedTextDto;
1338
+ /** Допустимые расширения файла в нижнем регистре (`.csv`), от 1 до 8; фильтр системного диалога. */
1339
+ accept: string[];
1340
+ /** `text` — обработчик получает файл строкой UTF-8, `bytes` — байтами. */
1341
+ input: 'text' | 'bytes';
1342
+ }
1343
+ /** Экспортёр расширения (`server.registerExporter`): курс или прогресс → файл пользователя. */
1344
+ interface ExporterContributionDto {
1345
+ id: string;
1346
+ extensionId: string;
1347
+ /** Название в палитре и карточке «Библиотеки». */
1348
+ title: LocalizedTextDto;
1349
+ /** `course` — снимок выбранного курса; `progress` — статистика через `server.stats`. */
1350
+ scope: 'course' | 'progress';
1351
+ }
1352
+ /** Что вернул обработчик команды; окно исполняет `notify` и `openPanel` само. */
1353
+ type CommandResultDto = {
1354
+ kind: 'none';
1355
+ } | {
1356
+ kind: 'notify';
1357
+ text: string;
1358
+ } | {
1359
+ kind: 'openPanel';
1360
+ panelId: string;
1361
+ props?: JsonValue;
1362
+ } | {
1363
+ kind: 'data';
1364
+ value: JsonValue;
1365
+ };
1366
+ /** Parameters of `extensions.invokeRpc`. */
1367
+ interface ExtensionRpcRequest {
1368
+ extensionId: string;
1369
+ /** The name of the contract (`RPC_NAME_PATTERN` of the extension API). */
1370
+ name: string;
1371
+ /** A JSON value; at most `MAX_ANSWER_CHARS` characters of `JSON.stringify(input)`. */
1372
+ input: unknown;
1373
+ }
1374
+ /** Файл, который пользователь выбрал для импортёра: имя без каталога и содержимое по `input` импортёра. */
1375
+ type ImportFileDto = {
1376
+ name: string;
1377
+ text: string;
1378
+ } | {
1379
+ name: string;
1380
+ bytes: Uint8Array;
1381
+ };
1382
+ /** Результат `extensions.runImporter`: сводка присланного дерева после проверки компилятором курсов. */
1383
+ interface ImportPreviewDto {
1384
+ /** Для `commitImport` и `discardImport`; `null` — в дереве есть ошибки или нет ни одного курса: ничего не ожидает, на диске ничего нет. */
1385
+ importId: string | null;
1386
+ extensionId: string;
1387
+ importerId: string;
1388
+ /** Каталог курса от корня библиотеки: `imported/<id расширения>-<имя файла латиницей>`. */
1389
+ path: string;
1390
+ /** Каталог уже есть (повторный импорт того же файла): `commitImport` заменит его. */
1391
+ replaces: boolean;
1392
+ /** Файлов в присланном дереве. */
1393
+ files: number;
1394
+ counts: {
1395
+ courses: number;
1396
+ lessons: number;
1397
+ exercises: number;
1398
+ };
1399
+ /** Все диагностики дерева, а не только вошедшие в `diagnostics`. */
1400
+ summary: DiagnosticSummary;
1401
+ /** Ошибки, затем предупреждения, не более 50, без `info`; пути — от каталога курса. */
1402
+ diagnostics: Diagnostic[];
1403
+ }
1404
+ /** Результат `extensions.commitImport`. */
1405
+ interface CommitImportResultDto {
1406
+ /** Каталог курса от корня библиотеки. */
1407
+ path: string;
1408
+ /** Прежний каталог был заменён. */
1409
+ replaced: boolean;
1410
+ /** Курсы каталога; после `commitImport` они в библиотеке. */
1411
+ courseIds: UnitId[];
1412
+ }
1413
+ /** Что экспортировать: курс (снимок собирает движок) или прогресс (обработчик читает `server.stats`). */
1414
+ type ExportRequestDto = {
1415
+ scope: 'course';
1416
+ courseId: UnitId;
1417
+ } | {
1418
+ scope: 'progress';
1419
+ };
1420
+ /** Файл, который вернул экспортёр; имя без разделителей пути, размер проверен. */
1421
+ type ExportFileDto = {
1422
+ filename: string;
1423
+ text: string;
1424
+ } | {
1425
+ filename: string;
1426
+ bytes: Uint8Array;
1427
+ };
1428
+ /** Вид задания расширения (`server.registerExerciseType`). */
1429
+ interface ExerciseTypeContributionDto {
1430
+ type: string;
1431
+ extensionId: string;
1432
+ /** Название для чипа вклада; `null` — показывается id. */
1433
+ title: LocalizedTextDto | null;
1434
+ }
1435
+ interface GradePolicyInfoDto {
1436
+ id: string;
1437
+ /** `null` у встроенного правила. */
1438
+ extensionId: string | null;
1439
+ /** `null` у встроенного правила: название переводит окно. */
1440
+ label: LocalizedTextDto | null;
1441
+ }
1442
+ /**
1443
+ * Клиентская часть включённого расширения: окно импортирует `url`, вызывает
1444
+ * `client(c)` и держит свой реестр вкладов (панели, места, виды ответа,
1445
+ * рендереры markdown, темы, клиентские команды); в движок они не попадают.
1446
+ */
1447
+ interface ExtensionClientDto {
1448
+ extensionId: string;
1449
+ /** `dolphy-ext://<extensionId>/<client>` — собранный `client.mjs`. */
1450
+ url: string;
1451
+ origin: ExtensionOriginDto;
1452
+ /** Отпечаток файлов расширения; у расширений из поставки — пустая строка. У `dev` меняется при правке: окно загружает клиентскую часть заново. */
1453
+ revision: string;
1454
+ }
1455
+ interface ContributionsDto {
1456
+ /**
1457
+ * Поколение набора вкладов: растёт при каждом применении расширений и равно
1458
+ * `generation` последнего события `contributions-changed`. Ответ с меньшим
1459
+ * поколением, чем уже виденное в событии, устарел. Отсчёт начинается заново
1460
+ * при каждом запуске движка.
1461
+ */
1462
+ generation: number;
1463
+ /** Включённые расширения с клиентской частью (`client.mjs`). */
1464
+ clients: ExtensionClientDto[];
1465
+ exerciseTypes: ExerciseTypeContributionDto[];
1466
+ gradePolicies: GradePolicyInfoDto[];
1467
+ /** Определения настроек включённых расширений. */
1468
+ settings: ExtensionSettingDefDto[];
1469
+ /** Серверные команды включённых расширений. */
1470
+ commands: CommandContributionDto[];
1471
+ /** Расписания включённых расширений. */
1472
+ schedules: ScheduleContributionDto[];
1473
+ /** Импортёры включённых расширений. */
1474
+ importers: ImporterContributionDto[];
1475
+ /** Экспортёры включённых расширений. */
1476
+ exporters: ExporterContributionDto[];
1477
+ }
1478
+ interface ExtensionSettingBaseDto {
1479
+ /** Равен id расширения или начинается с `<id расширения>.`. */
1480
+ id: string;
1481
+ extensionId: string;
1482
+ /** Подпись поля в диалоге настроек. */
1483
+ label: LocalizedTextDto;
1484
+ description: LocalizedTextDto | null;
1485
+ /** Заголовок раздела формы; `null` — настройка в первом разделе без заголовка. */
1486
+ group: LocalizedTextDto | null;
1487
+ /** Ключ сортировки формы, целое 0–1000; при равных — порядок объявления. */
1488
+ order: number;
1489
+ /** Поле скрыто, пока значение настройки `setting` (того же расширения, не `list`) не равно `equals`; скрытое значение сохраняется. `null` — поле видно всегда. */
1490
+ visibleWhen: SettingVisibleWhenDto | null;
1491
+ }
1492
+ /** Условие показа поля формы настроек. */
1493
+ interface SettingVisibleWhenDto {
1494
+ setting: string;
1495
+ equals: boolean | string | number;
1496
+ }
1497
+ interface BooleanSettingDefDto extends ExtensionSettingBaseDto {
1498
+ type: 'boolean';
1499
+ default: boolean;
1500
+ }
1501
+ interface StringSettingDefDto extends ExtensionSettingBaseDto {
1502
+ type: 'string';
1503
+ default: string;
1504
+ /** Длина в кодовых единицах UTF-16; `null` — без ограничения. */
1505
+ maxLength: number | null;
1506
+ }
1507
+ /** Многострочная строка. */
1508
+ interface TextSettingDefDto extends ExtensionSettingBaseDto {
1509
+ type: 'text';
1510
+ default: string;
1511
+ /** Длина в кодовых единицах UTF-16; `null` — до 10 000. */
1512
+ maxLength: number | null;
1513
+ }
1514
+ /** Цвет `#rrggbb`; значение хранится в нижнем регистре. */
1515
+ interface ColorSettingDefDto extends ExtensionSettingBaseDto {
1516
+ type: 'color';
1517
+ default: string;
1518
+ }
1519
+ /** Список строк. */
1520
+ interface ListSettingDefDto extends ExtensionSettingBaseDto {
1521
+ type: 'list';
1522
+ default: string[];
1523
+ /** Наибольшее число элементов, 1–50. */
1524
+ maxItems: number;
1525
+ /** Наибольшая длина элемента в кодовых единицах UTF-16, 1–200. */
1526
+ itemMaxLength: number;
1527
+ }
1528
+ interface NumberSettingDefDto extends ExtensionSettingBaseDto {
1529
+ type: 'number';
1530
+ default: number;
1531
+ min: number | null;
1532
+ max: number | null;
1533
+ integer: boolean;
1534
+ }
1535
+ interface EnumSettingOptionDto {
1536
+ value: string;
1537
+ label: LocalizedTextDto;
1538
+ }
1539
+ interface EnumSettingDefDto extends ExtensionSettingBaseDto {
1540
+ type: 'enum';
1541
+ default: string;
1542
+ options: EnumSettingOptionDto[];
1543
+ }
1544
+ /** Настройка расширения, которую пользователь меняет в «Настройки → Расширения». */
1545
+ type ExtensionSettingDefDto = BooleanSettingDefDto | StringSettingDefDto | TextSettingDefDto | ColorSettingDefDto | ListSettingDefDto | NumberSettingDefDto | EnumSettingDefDto;
1546
+ /** Действующие значения настроек расширения: по `id` каждого определения; сохранённое или `default`. */
1547
+ type ExtensionSettingValuesDto = Record<string, JsonValue>;
1548
+ /** Занятое место данных расширения (хранилище кода, значения настроек и секреты считаются отдельно; у секретов байты — шифртекст). */
1549
+ interface ExtensionDataUsageDto {
1550
+ storage: {
1551
+ keys: number;
1552
+ bytes: number;
1553
+ };
1554
+ settings: {
1555
+ keys: number;
1556
+ bytes: number;
1557
+ };
1558
+ secrets: {
1559
+ keys: number;
1560
+ bytes: number;
1561
+ };
1562
+ }
1563
+ /** Чем безопасный режим задан при запуске: флагом `--safe-mode` или переменной `DOLPHY_SAFE_MODE`. */
1564
+ type SafeModeSource = 'flag' | 'env';
1565
+ /** Настройки расширений; хранятся вместе с остальными настройками в `engine.db`. */
1566
+ interface ExtensionSettingsDto {
1567
+ /** Отключённые расширения (по id), отсортированы, без повторов. */
1568
+ disabled: string[];
1569
+ /** Проверять обновления расширений из каталога при запуске. По умолчанию включено. */
1570
+ checkUpdates: boolean;
1571
+ /**
1572
+ * Безопасный режим: расширения не из поставки отключены (диагностика
1573
+ * `safe-mode`), их код не запускается. Расширения из поставки работают.
1574
+ * По умолчанию выключено. Флаг запуска включает режим независимо от настройки.
1575
+ */
1576
+ safeMode: boolean;
1577
+ /**
1578
+ * Расширения с выключенными системными уведомлениями (по id), отсортированы,
1579
+ * без повторов: `server.notifications.show` у них даёт `false`. По умолчанию
1580
+ * пусто (уведомления включены).
1581
+ */
1582
+ notificationsOff: string[];
1583
+ /**
1584
+ * Свой адрес каталога расширений (канонический `URL.href`); `null` — адрес
1585
+ * по умолчанию (адрес, равный умолчанию, тоже хранится как `null`).
1586
+ * Нечитаемое сохранённое значение читается как `null`.
1587
+ */
1588
+ catalogUrl: string | null;
1589
+ /**
1590
+ * Расширения с выключенными расписаниями (по id), отсортированы, без
1591
+ * повторов: их `server.schedule` не срабатывает. По умолчанию пусто
1592
+ * (расписания включены).
1593
+ */
1594
+ schedulesOff: string[];
1595
+ }
1596
+ /** Откуда взят действующий адрес каталога: умолчание, настройка или `DOLPHY_EXTENSION_CATALOG_URL` (только несобранное приложение). */
1597
+ type CatalogSourceOrigin = 'default' | 'setting' | 'env';
1598
+ /** Действующий адрес каталога расширений (`extensions.catalogSource`). */
1599
+ interface CatalogSourceDto {
1600
+ /** Адрес, из которого читается каталог: идентичность каталога в `ExtensionInstallDto.catalogUrl`. */
1601
+ url: string;
1602
+ /** Адрес по умолчанию (официальный каталог). */
1603
+ default: string;
1604
+ origin: CatalogSourceOrigin;
1605
+ }
1606
+ /** Состояние процесса хоста расширений: `gave-up` — после повторных сбоев перезапуск прекращён до `restartHost()`. */
1607
+ type ExtensionHostStatusDto = 'running' | 'restarting' | 'gave-up';
1608
+ /** Сбой расширения; `reason` — причина (`handler-failed`, `timeout`, `invalid-result`, `activation-failed`), `message` — текст сбоя. */
1609
+ interface ExtensionFailureDto {
1610
+ at: EpochMs;
1611
+ reason: string;
1612
+ message: string;
1613
+ }
1614
+ /** Здоровье одного расширения с запуска приложения (в памяти, не сохраняется; сбрасывается при смене файлов расширения). */
1615
+ interface ExtensionHealthDto {
1616
+ id: string;
1617
+ /** Сбоев команд, событий и видов заданий. Убийства процесса и приостановка — состояние, а не сбой. */
1618
+ failures: number;
1619
+ lastFailure: ExtensionFailureDto | null;
1620
+ /** Длительность последней успешной активации; `null` — расширение не активировалось. */
1621
+ lastActivationMs: number | null;
1622
+ /** Ограниченный процесс приостановлен за цикл падений до этого времени; `null` — не приостановлен. */
1623
+ suppressedUntil: EpochMs | null;
1624
+ }
1625
+ /** Безопасный режим: `active` = `persisted` или `forcedBy !== null`. */
1626
+ interface SafeModeStatusDto {
1627
+ active: boolean;
1628
+ /** Значение настройки `safeMode`. */
1629
+ persisted: boolean;
1630
+ forcedBy: SafeModeSource | null;
1631
+ }
1632
+ /** Уровни записи журнала от подробного к важному. */
1633
+ declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error"];
1634
+ type LogLevelDto = (typeof LOG_LEVELS)[number];
1635
+ /** Запись файлового журнала. */
1636
+ interface ExtensionLogEntryDto {
1637
+ at: EpochMs;
1638
+ level: LogLevelDto;
1639
+ /** Кто написал: `main`, `engine` или `ext-host`. */
1640
+ source: string;
1641
+ message: string;
1642
+ /** Расширение, к которому относится запись; `null` — запись самого приложения. */
1643
+ extensionId: string | null;
1644
+ /** Остальные поля записи одной JSON-строкой (обрезаются до 4096 знаков); `null` — полей нет. */
1645
+ details: string | null;
1646
+ }
1647
+ /** Параметры `extensions.readLogs`. */
1648
+ interface ReadLogsOptions {
1649
+ /** Только записи этого расширения. */
1650
+ extensionId?: string;
1651
+ /** Записи не ниже этого уровня; по умолчанию все. */
1652
+ minLevel?: LogLevelDto;
1653
+ /** Сколько последних записей вернуть, 1…`MAX_LOG_ENTRIES`; по умолчанию `MAX_LOG_ENTRIES`. */
1654
+ limit?: number;
1655
+ }
1656
+ interface ExtensionsDiagnosticsDto {
1657
+ host: ExtensionHostStatusDto;
1658
+ safeMode: SafeModeStatusDto;
1659
+ /** Запись для каждого расширения из `list()` (у не сбоивших — нули). */
1660
+ extensions: ExtensionHealthDto[];
1661
+ }
1662
+ interface ExtensionsService {
1663
+ list(): Promise<ExtensionInfoDto[]>;
1664
+ getSettings(): Promise<ExtensionSettingsDto>;
1665
+ /** `NOT_FOUND` — нет такого расширения; `INVALID_ARGUMENT` `{reason:'bundled'}` — расширение из поставки. */
1666
+ setEnabled(id: string, enabled: boolean): Promise<ExtensionSettingsDto>;
1667
+ /**
1668
+ * Включает и выключает системные уведомления расширения (`notificationsOff`);
1669
+ * не перезапускает расширение. `NOT_FOUND` — нет такого расширения;
1670
+ * `INVALID_ARGUMENT` `{reason:'bundled'}` — расширение из поставки не
1671
+ * настраивается; не булево значение — `INVALID_ARGUMENT`.
1672
+ */
1673
+ setNotificationsEnabled(id: string, enabled: boolean): Promise<ExtensionSettingsDto>;
1674
+ /**
1675
+ * Включает и выключает расписания расширения (`schedulesOff`); не
1676
+ * перезапускает расширение. `NOT_FOUND` — нет такого расширения;
1677
+ * `INVALID_ARGUMENT` `{reason:'bundled'}` — расширение из поставки не
1678
+ * настраивается; не булево значение — `INVALID_ARGUMENT`.
1679
+ */
1680
+ setSchedulesEnabled(id: string, enabled: boolean): Promise<ExtensionSettingsDto>;
1681
+ /** Вклады загруженных расширений для окна (только чтение). */
1682
+ contributions(): Promise<ContributionsDto>;
1683
+ /**
1684
+ * Каталог расширений. `refresh` — запросить индекс у сервера (иначе — кэш,
1685
+ * если он свежий). Нет сети: последний кэш и `stale: true`; кэша нет —
1686
+ * `CATALOG_UNAVAILABLE`.
1687
+ */
1688
+ catalog(options?: {
1689
+ refresh?: boolean;
1690
+ }): Promise<CatalogDto>;
1691
+ /**
1692
+ * Устанавливает (или обновляет) расширение из каталога; `version` — точная
1693
+ * версия, иначе новейшая совместимая. `NOT_FOUND` — нет в каталоге;
1694
+ * `EXTENSION_INSTALL_FAILED` с `details.reason`:
1695
+ * `incompatible` | `network` | `integrity` | `limits` | `invalid` | `conflict`.
1696
+ * Установленное действует сразу: перед ответом движок и хост расширений
1697
+ * применили набор, окно получило событие `contributions-changed`.
1698
+ */
1699
+ install(id: string, version?: string): Promise<InstallResultDto>;
1700
+ /**
1701
+ * Удаляет расширение с origin `user`. `removeData` — удалить и данные
1702
+ * расширения (по умолчанию остаются). `NOT_FOUND`; `INVALID_ARGUMENT`
1703
+ * `{reason:'not-removable'}`.
1704
+ */
1705
+ uninstall(id: string, options?: {
1706
+ removeData?: boolean;
1707
+ }): Promise<void>;
1708
+ /** Доступные обновления установленных из каталога расширений (по последнему известному индексу). */
1709
+ updates(): Promise<ExtensionUpdateDto[]>;
1710
+ /**
1711
+ * README и журнал изменений. Без `version` — установленной версии (из каталога расширения, без сети),
1712
+ * у не установленного — новейшей показанной версии каталога; с `version` — этой версии (установленной
1713
+ * или из каталога; файлы каталога проверяются по размеру и `sha256` и кэшируются на диске).
1714
+ * `NOT_FOUND` — нет такого расширения или версии; `EXTENSION_INSTALL_FAILED`
1715
+ * (`details.reason` `network` | `integrity` | `limits`) — файл недоступен; `CATALOG_UNAVAILABLE` — индекса нет.
1716
+ */
1717
+ docs(id: string, options?: {
1718
+ version?: string;
1719
+ }): Promise<ExtensionDocsDto>;
1720
+ /**
1721
+ * Картинка README как `data:image/png|webp|jpeg;base64,…`: файл `png`/`webp`/`jpg`/`jpeg` до 256 КиБ из
1722
+ * файлов этой версии. `NOT_FOUND` — расширения, версии или файла нет; `INVALID_ARGUMENT` — путь,
1723
+ * тип или размер недопустимы.
1724
+ */
1725
+ docImage(id: string, version: string, path: string): Promise<string>;
1726
+ setCheckUpdates(enabled: boolean): Promise<ExtensionSettingsDto>;
1727
+ /**
1728
+ * Меняет адрес каталога расширений; `null` — вернуть умолчание. Адрес: `https:`
1729
+ * (или `http:` на loopback: `localhost`, `127.0.0.0/8`, `[::1]`), до 2048 знаков,
1730
+ * без логина и фрагмента, путь оканчивается на `.json`; сохраняется как `URL.href`,
1731
+ * равный умолчанию — как `null`. Действует сразу: установщик переключается,
1732
+ * набор расширений применяется заново (отзыв и устаревание берутся только из
1733
+ * нового каталога), метка проверки обновлений сбрасывается и проверка идёт заново,
1734
+ * окно получает `extensions-changed`. Установленное из прежнего каталога
1735
+ * остаётся (`.dolphy-install.json` не меняется). `INVALID_ARGUMENT` с
1736
+ * `details.reason` (`CatalogUrlRejection`); `env` — адрес задан
1737
+ * `DOLPHY_EXTENSION_CATALOG_URL`, настройка не меняется.
1738
+ */
1739
+ setCatalogUrl(url: string | null): Promise<ExtensionSettingsDto>;
1740
+ /** Действующий адрес каталога, умолчание и источник значения. */
1741
+ catalogSource(): Promise<CatalogSourceDto>;
1742
+ /**
1743
+ * Включает и выключает безопасный режим (настройка `safeMode`); действует
1744
+ * сразу, без перезапуска. Не булево значение — `INVALID_ARGUMENT`.
1745
+ */
1746
+ setSafeMode(enabled: boolean): Promise<ExtensionSettingsDto>;
1747
+ /** Здоровье расширений, состояние хоста расширений и безопасного режима. */
1748
+ diagnostics(): Promise<ExtensionsDiagnosticsDto>;
1749
+ /**
1750
+ * Запускает хост расширений заново, сбрасывает счётчик его падений (после
1751
+ * `gave-up` вернуть расширениям работу без перезапуска приложения).
1752
+ */
1753
+ restartHost(): Promise<void>;
1754
+ /**
1755
+ * Последние записи файлового журнала, самые новые последними. Читаются все
1756
+ * файлы журнала от новых к старым; нечитаемые строки пропускаются. Неверные
1757
+ * `limit`, `minLevel` или `extensionId` — `INVALID_ARGUMENT`.
1758
+ */
1759
+ readLogs(options?: ReadLogsOptions): Promise<ExtensionLogEntryDto[]>;
1760
+ /**
1761
+ * Действующие значения настроек расширения (определения — в
1762
+ * `contributions().settings`). `NOT_FOUND` — расширения нет;
1763
+ * `INVALID_ARGUMENT` `{reason:'disabled'}` — расширение отключено.
1764
+ */
1765
+ getSettingValues(id: string): Promise<ExtensionSettingValuesDto>;
1766
+ /**
1767
+ * Меняет одно значение; проверяет тип, границы, формат цвета, размер списка и `options` по определению; цвет сохраняется в нижнем регистре.
1768
+ * Неизвестный `settingId` и неверное значение — `INVALID_ARGUMENT`
1769
+ * (`details.reason`: `unknown-setting` | `type` | `range` | `integer` |
1770
+ * `max-length` | `option` | `format` | `max-items`). Расширение и окно узнают об изменении без перезапуска.
1771
+ */
1772
+ setSettingValue(id: string, settingId: string, value: JsonValue): Promise<ExtensionSettingValuesDto>;
1773
+ /** Возвращает значения по умолчанию (удаляет сохранённые). */
1774
+ resetSettingValues(id: string): Promise<ExtensionSettingValuesDto>;
1775
+ /** Сколько места занимают данные расширения; работает и для удалённого расширения, чьи данные остались. */
1776
+ dataUsage(id: string): Promise<ExtensionDataUsageDto>;
1777
+ /** Стирает хранилище и значения настроек; работающее расширение видит пустое хранилище и значения по умолчанию. */
1778
+ clearData(id: string): Promise<void>;
1779
+ /**
1780
+ * Выполняет объявленную команду расширения (код расширения; первый вызов
1781
+ * лениво его активирует). Вызов не занимает очередь команд движка.
1782
+ * `INVALID_ARGUMENT` — неверный `extensionId`/`commandId` или аргументы длиннее
1783
+ * `MAX_ANSWER_CHARS` (`details.reason`: `args-too-large`). Всё остальное —
1784
+ * `EXTENSION_COMMAND_FAILED` с `details` `{ extensionId, commandId, reason }`
1785
+ * (`ExtensionCommandFailureReason`): расширения или объявленной команды нет —
1786
+ * `unknown-command`, расширение отключено — `disabled`, `timeout` и
1787
+ * `host-down` допускают повтор.
1788
+ */
1789
+ invokeCommand(extensionId: string, commandId: string, args?: JsonValue): Promise<CommandResultDto>;
1790
+ /**
1791
+ * Calls a handler the server part of an extension registered with
1792
+ * `server.handle` (first call activates the extension lazily). The input is
1793
+ * validated against the input schema of the contract on the server, the
1794
+ * result against the output schema. The call does not occupy the engine
1795
+ * command queue. `INVALID_ARGUMENT` — a malformed `extensionId` or `name`,
1796
+ * or an input longer than `MAX_ANSWER_CHARS` (`details.reason`:
1797
+ * `args-too-large`). Everything else is `EXTENSION_RPC_FAILED` with
1798
+ * `details` `{ extensionId, name, reason }` (`ExtensionRpcFailureReason`):
1799
+ * a missing extension or handler — `unknown-rpc`, a disabled extension —
1800
+ * `disabled`, an exception of the handler — `handler-failed` with the
1801
+ * message of the exception; `timeout` and `host-down` allow a retry.
1802
+ */
1803
+ invokeRpc(params: ExtensionRpcRequest): Promise<unknown>;
1804
+ /**
1805
+ * Запускает объявленный импортёр на файле, который выбрал пользователь:
1806
+ * присланное расширением дерево курса проверяется компилятором курсов во
1807
+ * временном каталоге, на диск библиотеки ничего не попадает. Ожидающих
1808
+ * импортов не более 4, каждый живёт 10 минут и пропадает вместе с движком.
1809
+ * Вызов не занимает очередь команд. `INVALID_ARGUMENT` — неверные id, имя
1810
+ * файла (не имя, а путь) или форма файла не по `input` импортёра.
1811
+ * `EXTENSION_TRANSFER_FAILED` с `details` `{ extensionId, id, kind: 'import',
1812
+ * reason }` (`ExtensionTransferFailureReason`): `unknown-importer`,
1813
+ * `disabled`, `too-large` (файл больше `MAX_EXTENSION_TRANSFER_BYTES`),
1814
+ * остальное — как в хосте; `timeout` и `host-down` допускают повтор.
1815
+ */
1816
+ runImporter(extensionId: string, importerId: string, file: ImportFileDto): Promise<ImportPreviewDto>;
1817
+ /**
1818
+ * Кладёт ожидающий импорт в библиотеку: заменяет каталог `imported/<имя>` и
1819
+ * перезагружает библиотеку; курс виден без перезапуска. Библиотека отвергла
1820
+ * результат — каталог откатывается, прежняя библиотека остаётся, ошибка
1821
+ * `EXTENSION_TRANSFER_FAILED` с `reason: 'reload-rejected'` и `details`
1822
+ * `summary` и `diagnostics`. Нет такого ожидающего импорта (не было,
1823
+ * истёк, уже применён или отменён) — `NOT_FOUND`. Идёт в очереди команд.
1824
+ */
1825
+ commitImport(importId: string): Promise<CommitImportResultDto>;
1826
+ /** Отменяет ожидающий импорт и удаляет его временный каталог; `false`, если такого не было. Идемпотентна. */
1827
+ discardImport(importId: string): Promise<boolean>;
1828
+ /**
1829
+ * Запускает объявленный экспортёр. Для `scope: 'course'` движок читает
1830
+ * текстовые файлы каталога курса (до `MAX_EXTENSION_TRANSFER_BYTES`) и
1831
+ * передаёт их обработчику; для `progress` обработчик читает `server.stats`.
1832
+ * Область запроса должна совпасть с областью экспортёра (иначе
1833
+ * `INVALID_ARGUMENT`), курса нет — `NOT_FOUND`. Вызов не занимает очередь
1834
+ * команд. Ошибки — как у `runImporter` с `kind: 'export'` и
1835
+ * `unknown-exporter`; снимок курса больше потолка — `too-large`.
1836
+ */
1837
+ runExporter(extensionId: string, exporterId: string, request: ExportRequestDto): Promise<ExportFileDto>;
1838
+ }
1839
+ type CatalogStatusDto = 'available' | 'installed' | 'update' | 'incompatible';
1840
+ interface CatalogVersionDto {
1841
+ version: string;
1842
+ /** Зависимости версии; установка их не ставит и не блокируется. */
1843
+ dependencies: ExtensionDependencyDto[];
1844
+ /** ISO-время публикации. */
1845
+ publishedAt: string;
1846
+ /** Суммарный размер файлов, байты. */
1847
+ size: number;
1848
+ minAppVersion: string | null;
1849
+ }
1850
+ /** Версия в списке версий записи каталога (не больше 5, новейшие первыми). */
1851
+ interface CatalogListedVersionDto extends CatalogVersionDto {
1852
+ /** `true` — версию можно установить на этом приложении и платформе. */
1853
+ compatible: boolean;
1854
+ /** Почему нельзя установить; `null` у совместимой. */
1855
+ incompatible: {
1856
+ reason: CatalogIncompatibleDto['reason'];
1857
+ detail: string;
1858
+ } | null;
1859
+ /** В версии есть `CHANGELOG.md`. */
1860
+ hasChangelog: boolean;
1861
+ }
1862
+ interface CatalogIncompatibleDto {
1863
+ reason: 'platform' | 'api' | 'app' | 'revoked';
1864
+ /** Человекочитаемая причина на английском (`requires app >= 1.2.0`). */
1865
+ detail: string;
1866
+ /** Ближайшая более старая совместимая версия; `null` — нет. */
1867
+ fallback: CatalogVersionDto | null;
1868
+ }
1869
+ interface CatalogEntryDto {
1870
+ id: string;
1871
+ name: string;
1872
+ description: string;
1873
+ author: string;
1874
+ /** Адрес исходников (страница в репозитории каталога). */
1875
+ source: string;
1876
+ platforms: string[];
1877
+ /** Значок показанной версии как `data:image/png|webp;base64,…`; `null` — значка нет (или каталог старого формата). */
1878
+ icon: string | null;
1879
+ /** Теги показанной версии из записи индекса; `[]` — нет. */
1880
+ tags: string[];
1881
+ status: CatalogStatusDto;
1882
+ /** Версия, установленная из каталога; `null` — не установлено (или скопировано вручную). */
1883
+ installedVersion: string | null;
1884
+ /** Версия, которая будет установлена (новейшая совместимая); `null` у несовместимых. */
1885
+ latest: CatalogVersionDto | null;
1886
+ incompatible: CatalogIncompatibleDto | null;
1887
+ /** Версии записи индекса (до 5, новейшие первыми). */
1888
+ versions: CatalogListedVersionDto[];
1889
+ /** Пометка «устарело», действующая для показанной версии (`latest`, у несовместимых — новейшая); `null` — нет. */
1890
+ deprecated: DeprecationDto | null;
1891
+ /**
1892
+ * `true` — расширение с этим id уже есть, но установлено не из этого каталога (скопировано вручную,
1893
+ * из режима разработчика, из поставки или из другого каталога): установка невозможна без удаления прежнего.
1894
+ */
1895
+ elsewhere: boolean;
1896
+ }
1897
+ interface CatalogDto {
1898
+ entries: CatalogEntryDto[];
1899
+ /** ISO-время получения индекса; `null` — индекса нет. */
1900
+ fetchedAt: string | null;
1901
+ /** Показан кэш, потому что свежий индекс получить не удалось. */
1902
+ stale: boolean;
1903
+ /** Причина, по которой не удалось обновить индекс; `null` — без ошибок. */
1904
+ error: string | null;
1905
+ }
1906
+ interface ExtensionUpdateDto {
1907
+ id: string;
1908
+ name: string;
1909
+ installed: string;
1910
+ available: CatalogVersionDto;
1911
+ }
1912
+ /** Описание расширения: README и журнал изменений одной версии. */
1913
+ interface ExtensionDocsDto {
1914
+ /** Версия, к которой относятся тексты. */
1915
+ version: string;
1916
+ /** Содержимое `README.md` (первые 64 КиБ); `null` — файла нет. */
1917
+ readme: string | null;
1918
+ /** Содержимое `CHANGELOG.md` версии; `null` — файла нет. */
1919
+ changelog: string | null;
1920
+ /** Любой из текстов обрезан до 64 КиБ. */
1921
+ truncated: boolean;
1922
+ /**
1923
+ * Откуда тексты: `installed` — каталог установленного расширения; `catalog` — каталог (скачаны или
1924
+ * уже лежали в дисковом кэше); `cache` — дисковый кэш, потому что до каталога не дозвониться (индекс устарел).
1925
+ */
1926
+ source: 'installed' | 'catalog' | 'cache';
1927
+ }
1928
+ interface InstallResultDto {
1929
+ id: string;
1930
+ version: string;
1931
+ /** Прежняя версия из каталога; `null` — новая установка. */
1932
+ previousVersion: string | null;
1933
+ }
1934
+ interface LearningEngine {
1935
+ readonly library: LibraryService;
1936
+ readonly repositories: RepositoriesService;
1937
+ readonly practice: PracticeService;
1938
+ readonly curation: CurationService;
1939
+ readonly settings: SettingsService;
1940
+ readonly sync: SyncService;
1941
+ readonly plan: PlanService;
1942
+ readonly placement: PlacementService;
1943
+ readonly remediation: RemediationService;
1944
+ readonly extensions: ExtensionsService;
1945
+ diagnostics(): Promise<EngineDiagnosticsDto>;
1946
+ /** In-process. По RPC — сообщения `events.subscribe` / `events.unsubscribe` и push `EngineEvent`. */
1947
+ subscribe(listener: (event: EngineEvent) => void): () => void;
1948
+ close(): Promise<void>;
1949
+ }
1950
+ /**
1951
+ * What an extension gets as `ctx.engine` on the server and `useEngine()` in a
1952
+ * component: every method of `LearningEngine` including `subscribe`, but not
1953
+ * `close`. Over the wire `subscribe` is the `events.subscribe` /
1954
+ * `events.unsubscribe` messages of `RPC_CONTROL`.
1955
+ */
1956
+ type ExtensionEngine = Omit<LearningEngine, 'close'>;
1957
+ //#endregion
1958
+ export { ExtensionEngine as t };