Перейти к основному содержимому

API-контракты

Endpoint coverage

api-map.md является endpoint registry. Этот файл не дублирует полный DTO для каждой CRUD-строки; все endpoints из api-map.md покрываются platform envelope, pagination, error и idempotency rules плюс DTO/feature docs из таблицы.

API map sectionEndpoint rows coveredContract source
Problems and versions/problems*, /problem-versions*, /problem-variants*Problem, Problem version, Problem variant, Answer schema, Checking rule; features/problems.md, features/variants.md, features/solutions.md
Taxonomy, sets, usage/taxonomy/nodes, /problems/{problemId}/taxonomy-links, /problem-sets*, /usages*Problem set and usage; features/taxonomy.md, features/usage.md
Templates and snapshots/activity-templates*, /program-templates*, /content-export-snapshots*Activity template, Program template, Content export snapshot; features/activity-templates.md, features/program-templates.md, features/usage.md
Relations and public catalog/problem-relations*, /public/*, /publication-profiles*Problem relation, Public task projection; features/relations.md, features/public-catalog.md
Attempts and checking/attempts*, /answer-checks*, /manual-reviews*, /evidenceAttempt, Answer check, Evidence; features/answers-checking.md
Admin/imports*, /audit-logs, /exports, /settingscommon ApiEnvelope, SourceRef, idempotency/audit rules; features/admin.md

Общие DTO

type PaginationQuery = {
page?: number;
perPage?: number;
sort?: string;
};

type SourceRef = {
domain: 'lms' | 'competitions' | 'management' | 'storefront' | 'manual' | 'migration';
type: string;
id: string;
};

type ApiEnvelope<TData, TMeta = Record<string, unknown>> = {
data: TData | null;
meta?: TMeta;
error?: {
code: string;
message: string;
details?: Record<string, unknown>;
};
};

type Visibility = 'private' | 'organization' | 'public';

Problem

type ProblemDto = {
id: string;
code: string;
subjectKey: string;
status: 'draft' | 'review' | 'published' | 'archived' | 'retired';
defaultDifficulty?: string;
sourceRef?: SourceRef;
};

type CreateProblemRequest = {
code?: string;
subjectKey: string;
defaultDifficulty?: string;
sourceRef?: SourceRef;
};

Problem version

type ProblemVersionDto = {
id: string;
problemId: string;
version: number;
status: 'draft' | 'review' | 'published' | 'retired';
statement: ProblemStatementDto;
answerSchema: AnswerSchemaDto;
metadata: Record<string, unknown>;
visibility: Record<string, unknown>;
};

type ProblemStatementDto = {
format: 'rich_text' | 'markdown' | 'structured';
body: Record<string, unknown>;
assets?: Array<{
type: 'image' | 'pdf' | 'video' | 'interactive' | 'source_file' | 'diagram';
url: string;
metadata?: Record<string, unknown>;
}>;
};

type CreateProblemVersionRequest = {
statement: ProblemStatementDto;
answerSchema: AnswerSchemaDto;
metadata?: Record<string, unknown>;
visibility?: Record<string, unknown>;
};

Validation:

  • statement обязателен;
  • answer schema обязателен;
  • published requires checking rule;
  • изменение published version запрещено.

Problem variant

type ProblemVariantDto = {
id: string;
problemId: string;
key: string;
problemVersionId: string;
settings: Record<string, unknown>;
};

type CreateProblemVariantRequest = {
key: string;
problemVersionId: string;
settings?: Record<string, unknown>;
};

Validation:

  • key уникален в рамках problem;
  • generation/equivalence commands не меняют published problem version;
  • generated variant stores reproducibility settings.

Answer schema

type AnswerSchemaDto =
| { type: 'exact_value'; valueType: 'string' | 'number'; normalization?: string[] }
| { type: 'numeric_with_tolerance'; tolerance?: number; unit?: string }
| { type: 'unit_value'; valueType: 'number'; allowedUnits: string[]; tolerance?: number }
| { type: 'single_choice'; options: Array<{ key: string; label: string }> }
| { type: 'multiple_select'; options: Array<{ key: string; label: string }> }
| { type: 'free_text'; normalization?: string[]; maxLength?: number; manualReview?: boolean }
| { type: 'expression'; syntax: 'latex' | 'plain' }
| { type: 'file_upload'; allowedTypes: string[]; maxSizeMb: number }
| { type: 'proof'; rubricRequired: true }
| { type: 'composite_answer'; parts: AnswerSchemaDto[] }
| { type: 'manual_review_required'; rubricRequired: true };

Checking rule

type CheckingRuleDto = {
id: string;
problemVersionId: string;
method: 'auto' | 'manual' | 'hybrid' | 'external';
rule: Record<string, unknown>;
scoringRule: {
maxScore: number;
partialCredit?: boolean;
hintPenalty?: Record<string, number>;
};
rubric?: Record<string, unknown>;
version: number;
};

Problem set and usage

type ProblemSetDto = {
id: string;
slug: string;
title: string;
purpose: 'homework' | 'lesson' | 'competition' | 'diagnostic' | 'training' | 'manual';
status: 'draft' | 'review' | 'published' | 'archived';
};

type CreateProblemUsageRequest = {
problemVersionId: string;
contextDomain: 'lms' | 'competitions' | 'management' | 'storefront' | 'manual';
contextType: string;
contextId: string;
position?: number;
scoringRuleSnapshot?: Record<string, unknown>;
visibilityPolicy?: Record<string, unknown>;
};

Problem relation

type ProblemRelationType =
| 'duplicate'
| 'analog'
| 'parameterized_variant'
| 'set_variant'
| 'similar'
| 'prerequisite'
| 'same_method';

type ProblemRelationDto = {
id: string;
fromProblemId: string;
toProblemId: string;
relationType: ProblemRelationType;
status: 'suggested' | 'confirmed' | 'rejected' | 'hidden';
weight?: number;
metadata?: Record<string, unknown>;
};

type CreateProblemRelationRequest = {
toProblemId: string;
relationType: ProblemRelationType;
weight?: number;
metadata?: Record<string, unknown>;
};

Validation:

  • self-link запрещён;
  • prerequisite directed and acyclic;
  • similar не даёт editorial identity без moderation.

Activity template

type ActivityTemplateDto = {
id: string;
slug: string;
subjectKey: string;
title: string;
activityType: 'lesson' | 'homework' | 'trainer' | 'diagnostic' | 'competition_round' | 'review' | 'intensive';
status: 'draft' | 'review' | 'approved' | 'published' | 'archived' | 'retired';
version: number;
ageGroup?: Record<string, unknown>;
difficultyRange?: Record<string, unknown>;
durationMinutes?: number;
primaryTopicId?: string;
objective?: string;
teacherNotes?: Record<string, unknown>;
studentIntro?: Record<string, unknown>;
visibility: Visibility;
sections: ActivityTemplateSectionDto[];
};

type ActivityTemplateSectionDto = {
id: string;
title: string;
sectionRole: 'warmup' | 'theory_bridge' | 'main' | 'practice' | 'homework' | 'challenge' | 'reserve' | 'reflection' | 'diagnostic';
order: number;
durationMinutes?: number;
teacherNotes?: Record<string, unknown>;
studentInstructions?: Record<string, unknown>;
items: ActivityTemplateItemDto[];
};

type ActivityTemplateItemDto = {
id: string;
problemId: string;
problemVersionId?: string;
role: 'main' | 'warmup' | 'reserve' | 'homework' | 'challenge' | 'diagnostic' | 'example';
order: number;
required: boolean;
recommendedTimeMinutes?: number;
recommendedPoints?: number;
checkingMode: 'auto' | 'manual' | 'hybrid' | 'none';
solutionVisibilityPolicy?: Record<string, unknown>;
hintPolicy?: Record<string, unknown>;
};

Validation:

  • template не принимает roster, schedule, attendance, assignment или LMS group;
  • published version immutable;
  • external use требует content_export_snapshot.

Program template

type ProgramTemplateDto = {
id: string;
slug: string;
subjectKey: string;
title: string;
programType: 'circle' | 'course_content' | 'intensive' | 'competition_prep' | 'trainer_path';
status: 'draft' | 'review' | 'approved' | 'published' | 'archived' | 'retired';
version: number;
ageGroup?: Record<string, unknown>;
durationUnits?: number;
objective?: string;
visibility: Visibility;
tracks: ProgramTrackDto[];
};

type ProgramTrackDto = {
id: string;
title: string;
level?: string;
order: number;
activities: ProgramActivityLinkDto[];
};

type ProgramActivityLinkDto = {
id: string;
activityTemplateId: string;
order: number;
moduleLabel?: string;
required: boolean;
replacementGroupId?: string;
prerequisiteActivityId?: string;
};

Validation:

  • program template не принимает enrollment, calendar, teacher assignment или progress;
  • sequence может содержать 32 и более activity templates;
  • export в LMS создаёт snapshot, а не LMS course напрямую.

Content export snapshot

type ContentExportSnapshotDto = {
id: string;
sourceType: 'problem_set' | 'activity_template' | 'program_template';
sourceId: string;
sourceVersion: number;
targetDomain: 'lms' | 'competitions' | 'storefront' | 'management' | 'manual';
targetType: string;
targetId?: string;
status: 'draft' | 'exported' | 'locked' | 'active' | 'retired' | 'failed';
payloadHash: string;
payload: Record<string, unknown>;
lockedAt?: string;
metadata?: Record<string, unknown>;
};

Validation:

  • locked snapshot immutable;
  • payload contains problem ids, problem version ids, order, roles, policies and lineage;
  • payload excludes answer keys, hidden solutions, rubrics and teacher notes unless target scope explicitly allows them.

Public task projection

type PublicTaskProjectionDto = {
taskId: string;
canonicalUrl: `/tasks/${string}`;
statement: ProblemStatementDto;
subjectKey: string;
difficulty?: string;
ageGroup?: Record<string, unknown>;
primaryTopic: { id: string; path: string; title: string };
secondaryTopics?: Array<{ id: string; title: string }>;
source?: { path: string; title: string; licenseStatus: string };
tags?: string[];
solutionVisibility: 'show' | 'show_after_attempt' | 'show_after_date' | 'hide' | 'teacher_only';
seo?: { title?: string; description?: string };
relatedTasks?: Array<{ taskId: string; relationType: string }>;
};

Validation:

  • public projection never returns hidden answer key, moderation notes, teacher-only notes, raw attempts or embargoed competition content;
  • /tasks/<taskId> is canonical; topic/source path is never part of canonical task URL.

Attempt

type StartAttemptRequest = {
problemUsageId?: string;
problemVersionId: string;
userId: string;
};

type SubmitAttemptRequest = {
answer: Record<string, unknown>;
};

type ProblemAttemptDto = {
id: string;
problemUsageId?: string;
problemVersionId: string;
userId: string;
status: 'started' | 'submitted' | 'checking' | 'checked' | 'cancelled' | 'void';
startedAt: string;
submittedAt?: string;
checkedAt?: string;
};

Validation:

  • attempt references published or explicitly allowed version;
  • submitted answer matches answer schema;
  • duplicate Idempotency-Key returns existing attempt/result.

Answer check

type CheckAttemptRequest = {
checkingRuleId?: string;
mode?: 'auto' | 'manual' | 'hybrid' | 'external';
};

type AnswerCheckDto = {
id: string;
attemptId: string;
status: 'pending' | 'checked' | 'needs_manual_review' | 'failed' | 'overridden';
method: 'auto' | 'manual' | 'hybrid' | 'external';
isCorrect?: boolean;
score?: number;
maxScore?: number;
feedback?: Record<string, unknown>;
};

type OverrideAnswerCheckRequest = {
isCorrect?: boolean;
score: number;
feedback?: Record<string, unknown>;
reason: string;
};

Evidence

type ProblemEvidenceDto = {
id: string;
attemptId: string;
problemId: string;
userId: string;
signal: 'attempted' | 'solved' | 'partially_solved' | 'failed' | 'solved_with_hint' | 'needs_review' | 'repeated_difficulty';
taxonomySnapshot: Record<string, unknown>;
score?: number;
createdAt: string;
};