diff --git a/adk-rust/crates/detmir-portal/src/contracts/openapi.json b/adk-rust/crates/detmir-portal/src/contracts/openapi.json index 40ee54b..cdd2aac 100644 --- a/adk-rust/crates/detmir-portal/src/contracts/openapi.json +++ b/adk-rust/crates/detmir-portal/src/contracts/openapi.json @@ -9,28 +9,50 @@ { "url": "/api", "description": "Gateway-relative API base" + }, + { + "url": "/dpd/api", + "description": "DPD mirror API base" } ], "tags": [ - { "name": "contracts" }, - { "name": "portal" }, - { "name": "reports" }, - { "name": "incidents" }, - { "name": "cases" }, - { "name": "readiness" }, - { "name": "telemetry" } + { + "name": "contracts" + }, + { + "name": "portal" + }, + { + "name": "reports" + }, + { + "name": "incidents" + }, + { + "name": "cases" + }, + { + "name": "readiness" + }, + { + "name": "telemetry" + } ], "paths": { "/contracts": { "get": { - "tags": ["contracts"], + "tags": [ + "contracts" + ], "summary": "Contract index", "responses": { "200": { "description": "Contract index", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/ContractIndex" } + "schema": { + "$ref": "#/components/schemas/ContractIndex" + } } } } @@ -39,14 +61,19 @@ }, "/contracts/openapi.json": { "get": { - "tags": ["contracts"], + "tags": [ + "contracts" + ], "summary": "OpenAPI document", "responses": { "200": { "description": "OpenAPI 3.1 document", "content": { "application/json": { - "schema": { "type": "object", "additionalProperties": true } + "schema": { + "type": "object", + "additionalProperties": true + } } } } @@ -55,14 +82,18 @@ }, "/contracts/typescript.d.ts": { "get": { - "tags": ["contracts"], + "tags": [ + "contracts" + ], "summary": "TypeScript declarations", "responses": { "200": { "description": "TypeScript declaration file", "content": { "text/plain": { - "schema": { "type": "string" } + "schema": { + "type": "string" + } } } } @@ -71,14 +102,18 @@ }, "/health": { "get": { - "tags": ["portal"], + "tags": [ + "portal" + ], "summary": "Light service health", "responses": { "200": { "description": "Health payload", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -87,14 +122,18 @@ }, "/operator": { "get": { - "tags": ["portal"], + "tags": [ + "portal" + ], "summary": "Operator overview payload", "responses": { "200": { "description": "Operator payload used by the HTML portal", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -103,14 +142,18 @@ }, "/manager": { "get": { - "tags": ["portal"], + "tags": [ + "portal" + ], "summary": "Manager payload", "responses": { "200": { "description": "Manager payload", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -119,14 +162,18 @@ }, "/owner": { "get": { - "tags": ["portal"], + "tags": [ + "portal" + ], "summary": "Owner/security payload", "responses": { "200": { "description": "Owner payload", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -135,14 +182,18 @@ }, "/reports": { "get": { - "tags": ["reports"], + "tags": [ + "reports" + ], "summary": "Management report payload", "parameters": [ { "name": "anonymize", "in": "query", "required": false, - "schema": { "type": "boolean" }, + "schema": { + "type": "boolean" + }, "description": "Return anonymized values when supported" } ], @@ -151,7 +202,9 @@ "description": "Report payload", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/ReportPayload" } + "schema": { + "$ref": "#/components/schemas/ReportPayload" + } } } } @@ -160,14 +213,18 @@ }, "/workforce/policy/explain": { "get": { - "tags": ["reports"], + "tags": [ + "reports" + ], "summary": "Workforce scoring policy explanation", "parameters": [ { "name": "anonymize", "in": "query", "required": false, - "schema": { "type": "boolean" } + "schema": { + "type": "boolean" + } } ], "responses": { @@ -175,7 +232,9 @@ "description": "Policy explanation", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -184,14 +243,18 @@ }, "/incidents": { "get": { - "tags": ["incidents"], + "tags": [ + "incidents" + ], "summary": "Incident list and current state", "responses": { "200": { "description": "Incident payload", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -200,13 +263,17 @@ }, "/incident-review": { "post": { - "tags": ["incidents"], + "tags": [ + "incidents" + ], "summary": "Set manual review status for an incident candidate", "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/IncidentReviewRequest" } + "schema": { + "$ref": "#/components/schemas/IncidentReviewRequest" + } } } }, @@ -215,7 +282,9 @@ "description": "Updated review state", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -224,20 +293,30 @@ }, "/investigation-pack/{candidate_id}": { "get": { - "tags": ["incidents"], + "tags": [ + "incidents" + ], "summary": "Export investigation pack for a candidate", "parameters": [ { "name": "candidate_id", "in": "path", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "format", "in": "query", "required": false, - "schema": { "type": "string", "enum": ["json", "markdown"] } + "schema": { + "type": "string", + "enum": [ + "json", + "markdown" + ] + } } ], "responses": { @@ -245,10 +324,14 @@ "description": "Investigation pack", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } }, "text/markdown": { - "schema": { "type": "string" } + "schema": { + "type": "string" + } } } } @@ -257,27 +340,35 @@ }, "/cases": { "get": { - "tags": ["cases"], + "tags": [ + "cases" + ], "summary": "Case list", "responses": { "200": { "description": "Case list", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaseListResponse" } + "schema": { + "$ref": "#/components/schemas/CaseListResponse" + } } } } } }, "post": { - "tags": ["cases"], + "tags": [ + "cases" + ], "summary": "Create a manual case from a confirmed candidate", "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CreateCaseRequest" } + "schema": { + "$ref": "#/components/schemas/CreateCaseRequest" + } } } }, @@ -286,7 +377,9 @@ "description": "Created case", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -295,20 +388,30 @@ }, "/cases/{case_id}": { "get": { - "tags": ["cases"], + "tags": [ + "cases" + ], "summary": "Case details", "parameters": [ { "name": "case_id", "in": "path", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } }, { "name": "format", "in": "query", "required": false, - "schema": { "type": "string", "enum": ["json", "markdown"] } + "schema": { + "type": "string", + "enum": [ + "json", + "markdown" + ] + } } ], "responses": { @@ -316,10 +419,14 @@ "description": "Case details", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } }, "text/markdown": { - "schema": { "type": "string" } + "schema": { + "type": "string" + } } } } @@ -328,21 +435,27 @@ }, "/cases/{case_id}/status": { "post": { - "tags": ["cases"], + "tags": [ + "cases" + ], "summary": "Set manual case status", "parameters": [ { "name": "case_id", "in": "path", "required": true, - "schema": { "type": "string" } + "schema": { + "type": "string" + } } ], "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/CaseStatusRequest" } + "schema": { + "$ref": "#/components/schemas/CaseStatusRequest" + } } } }, @@ -351,7 +464,9 @@ "description": "Updated case", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -360,14 +475,18 @@ }, "/dlp/evidence": { "get": { - "tags": ["incidents"], + "tags": [ + "incidents" + ], "summary": "DLP evidence list", "responses": { "200": { "description": "Evidence list", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -376,14 +495,18 @@ }, "/readiness/latest": { "get": { - "tags": ["readiness"], + "tags": [ + "readiness" + ], "summary": "Latest readiness bundle status", "responses": { "200": { "description": "Readiness status", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -392,14 +515,18 @@ }, "/readiness/bundle": { "get": { - "tags": ["readiness"], + "tags": [ + "readiness" + ], "summary": "Readiness bundle artifact list", "responses": { "200": { "description": "Readiness bundle", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -408,14 +535,18 @@ }, "/readiness/verify": { "get": { - "tags": ["readiness"], + "tags": [ + "readiness" + ], "summary": "Verify readiness checksums and signature", "responses": { "200": { "description": "Verification result", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -424,14 +555,18 @@ }, "/links": { "get": { - "tags": ["portal"], + "tags": [ + "portal" + ], "summary": "Gateway-relative portal links", "responses": { "200": { "description": "Link map", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } } @@ -440,13 +575,17 @@ }, "/telemetry": { "post": { - "tags": ["telemetry"], + "tags": [ + "telemetry" + ], "summary": "Agent telemetry ingest", "requestBody": { "required": true, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } }, @@ -455,7 +594,9 @@ "description": "Accepted telemetry", "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/JsonObject" } + "schema": { + "$ref": "#/components/schemas/JsonObject" + } } } }, @@ -474,107 +615,207 @@ }, "ContractIndex": { "type": "object", - "required": ["ok", "contract_version", "api_base", "artifacts", "stable_endpoints"], + "required": [ + "ok", + "contract_version", + "api_base", + "artifacts", + "stable_endpoints" + ], "properties": { - "ok": { "type": "boolean" }, - "contract_version": { "type": "string" }, - "generated_by": { "type": "string" }, - "api_base": { "type": "string" }, - "compatibility": { "$ref": "#/components/schemas/JsonObject" }, - "targets": { "type": "array", "items": { "type": "string" } }, - "artifacts": { "$ref": "#/components/schemas/JsonObject" }, + "ok": { + "type": "boolean" + }, + "contract_version": { + "type": "string" + }, + "generated_by": { + "type": "string" + }, + "api_base": { + "type": "string" + }, + "compatibility": { + "$ref": "#/components/schemas/JsonObject" + }, + "targets": { + "type": "array", + "items": { + "type": "string" + } + }, + "artifacts": { + "$ref": "#/components/schemas/JsonObject" + }, "stable_endpoints": { "type": "array", - "items": { "$ref": "#/components/schemas/EndpointDescriptor" } + "items": { + "$ref": "#/components/schemas/EndpointDescriptor" + } } }, "additionalProperties": true }, "EndpointDescriptor": { "type": "object", - "required": ["method", "path"], + "required": [ + "method", + "path" + ], "properties": { - "method": { "type": "string" }, - "path": { "type": "string" }, - "purpose": { "type": "string" } + "method": { + "type": "string" + }, + "path": { + "type": "string" + }, + "purpose": { + "type": "string" + } }, "additionalProperties": true }, "ReportPayload": { "type": "object", - "required": ["ok"], + "required": [ + "ok" + ], "properties": { - "ok": { "type": "boolean" }, - "generated_at_utc": { "type": "string" }, + "ok": { + "type": "boolean" + }, + "generated_at_utc": { + "type": "string" + }, "executive_points": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } + }, + "executive_dashboard": { + "$ref": "#/components/schemas/JsonObject" + }, + "risk_narrative": { + "$ref": "#/components/schemas/JsonObject" + }, + "agent_quality": { + "$ref": "#/components/schemas/JsonObject" + }, + "agent_coverage_sla": { + "$ref": "#/components/schemas/JsonObject" }, - "executive_dashboard": { "$ref": "#/components/schemas/JsonObject" }, - "risk_narrative": { "$ref": "#/components/schemas/JsonObject" }, - "agent_quality": { "$ref": "#/components/schemas/JsonObject" }, - "agent_coverage_sla": { "$ref": "#/components/schemas/JsonObject" }, "business_risk": { "type": "array", - "items": { "$ref": "#/components/schemas/JsonObject" } + "items": { + "$ref": "#/components/schemas/JsonObject" + } }, "risk_incident_candidates": { "type": "array", - "items": { "$ref": "#/components/schemas/JsonObject" } + "items": { + "$ref": "#/components/schemas/JsonObject" + } }, "cases": { "type": "array", - "items": { "$ref": "#/components/schemas/JsonObject" } + "items": { + "$ref": "#/components/schemas/JsonObject" + } } }, "additionalProperties": true }, "IncidentReviewRequest": { "type": "object", - "required": ["candidate_id", "status"], + "required": [ + "candidate_id", + "status" + ], "properties": { - "candidate_id": { "type": "string" }, + "candidate_id": { + "type": "string" + }, "status": { "type": "string", - "enum": ["NEW", "IN_REVIEW", "CONFIRMED", "FALSE_POSITIVE", "POSTPONED"] + "enum": [ + "NEW", + "IN_REVIEW", + "CONFIRMED", + "FALSE_POSITIVE", + "POSTPONED" + ] }, - "reviewer": { "type": "string" }, - "comment": { "type": "string" } + "reviewer": { + "type": "string" + }, + "comment": { + "type": "string" + } }, "additionalProperties": true }, "CreateCaseRequest": { "type": "object", - "required": ["candidate_id"], + "required": [ + "candidate_id" + ], "properties": { - "candidate_id": { "type": "string" }, - "title": { "type": "string" }, - "owner": { "type": "string" }, - "summary": { "type": "string" }, - "decision": { "type": "string" } + "candidate_id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "summary": { + "type": "string" + }, + "decision": { + "type": "string" + } }, "additionalProperties": true }, "CaseStatusRequest": { "type": "object", - "required": ["status"], + "required": [ + "status" + ], "properties": { "status": { "type": "string", - "enum": ["OPEN", "IN_PROGRESS", "RESOLVED", "REJECTED", "ARCHIVED"] + "enum": [ + "OPEN", + "IN_PROGRESS", + "RESOLVED", + "REJECTED", + "ARCHIVED" + ] }, - "decision": { "type": "string" } + "decision": { + "type": "string" + } }, "additionalProperties": true }, "CaseListResponse": { "type": "object", - "required": ["ok", "cases"], + "required": [ + "ok", + "cases" + ], "properties": { - "ok": { "type": "boolean" }, + "ok": { + "type": "boolean" + }, "cases": { "type": "array", - "items": { "$ref": "#/components/schemas/JsonObject" } + "items": { + "$ref": "#/components/schemas/JsonObject" + } } }, "additionalProperties": true diff --git a/adk-rust/crates/detmir-portal/src/contracts/typescript.d.ts b/adk-rust/crates/detmir-portal/src/contracts/typescript.d.ts index 4adfd52..d9f36e4 100644 --- a/adk-rust/crates/detmir-portal/src/contracts/typescript.d.ts +++ b/adk-rust/crates/detmir-portal/src/contracts/typescript.d.ts @@ -1,7 +1,12 @@ export type ISODateTime = string; export type RiskLevel = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL" | "UNKNOWN"; -export type ReviewStatus = "NEW" | "IN_REVIEW" | "CONFIRMED" | "FALSE_POSITIVE" | "POSTPONED"; +export type ReviewStatus = + | "NEW" + | "IN_REVIEW" + | "CONFIRMED" + | "FALSE_POSITIVE" + | "POSTPONED"; export type CaseStatus = "OPEN" | "IN_PROGRESS" | "RESOLVED" | "REJECTED" | "ARCHIVED"; export interface JsonObject { @@ -179,7 +184,10 @@ export interface DetMirPortalApi { createCase(request: CreateCaseRequest): Promise; setCaseStatus(caseId: string, request: CaseStatusRequest): Promise; setIncidentReview(request: IncidentReviewRequest): Promise; - getInvestigationPack(candidateId: string, options?: { format?: "json" | "markdown" }): Promise; + getInvestigationPack( + candidateId: string, + options?: { format?: "json" | "markdown" }, + ): Promise; getDlpEvidence(): Promise; getReadinessLatest(): Promise; getReadinessBundle(): Promise; diff --git a/adk-rust/crates/detmir-portal/src/main.rs b/adk-rust/crates/detmir-portal/src/main.rs index 79b91dd..23b6541 100644 --- a/adk-rust/crates/detmir-portal/src/main.rs +++ b/adk-rust/crates/detmir-portal/src/main.rs @@ -9213,12 +9213,45 @@ mod tests { let openapi: Value = serde_json::from_str(API_CONTRACT_OPENAPI).expect("OpenAPI contract must be JSON"); assert_eq!(openapi["openapi"], "3.1.0"); - assert!(openapi["paths"]["/reports"].is_object()); + assert!( + openapi["info"]["version"] + .as_str() + .is_some_and(|value| !value.trim().is_empty()) + ); + for path in [ + "/contracts", + "/reports", + "/incidents", + "/cases", + "/readiness/latest", + ] { + assert!( + openapi["paths"][path].is_object(), + "OpenAPI path missing: {path}" + ); + } assert!(openapi["paths"]["/incident-review"].is_object()); - assert!(openapi["paths"]["/cases"].is_object()); - assert!(API_CONTRACT_TYPESCRIPT.contains("export interface DetMirPortalApi")); - assert!(API_CONTRACT_TYPESCRIPT.contains("ReportsResponse")); - assert!(API_CONTRACT_TYPESCRIPT.contains("IncidentReviewRequest")); + for required_type in [ + "ContractIndex", + "ReportsResponse", + "CaseListResponse", + "IncidentReviewRequest", + "export interface DetMirPortalApi", + ] { + assert!( + API_CONTRACT_TYPESCRIPT.contains(required_type), + "TypeScript declaration missing {required_type}" + ); + } + for (idx, _) in API_CONTRACT_TYPESCRIPT.match_indices("Promise") { + assert_eq!( + API_CONTRACT_TYPESCRIPT[idx + "Promise".len()..] + .chars() + .next(), + Some('<'), + "TypeScript declarations must not contain bare Promise return types" + ); + } let summary = api_contract_summary(); assert_eq!(summary["ok"], true); diff --git a/docs/DPD_PORTAL_RU.md b/docs/DPD_PORTAL_RU.md index 8fc6a9a..0d363ba 100644 --- a/docs/DPD_PORTAL_RU.md +++ b/docs/DPD_PORTAL_RU.md @@ -12,6 +12,7 @@ DPD нужен для безопасной эволюции интерфейса - `/dpd/` работает как полный mirror текущего портала; - будущий React/Tauri UI сможет использовать те же API-контракты; - новые UI-решения проверяются без cutover и без дублирования бизнес-логики. +- будущий UI не парсит HTML текущего портала как источник данных. DPD проксирует: @@ -77,9 +78,15 @@ detmir-dpd-portal \ - `/dpd/api/contracts/openapi.json`; - `/dpd/api/contracts/typescript.d.ts`. -Правило совместимости: изменения API должны быть additive. Клиенты React/Tauri -обязаны игнорировать неизвестные поля и корректно обрабатывать отсутствующие -optional-поля. +Правила совместимости: + +- изменения API должны быть additive; +- клиенты React/Tauri обязаны игнорировать неизвестные поля; +- optional-поля могут отсутствовать; +- `null` не должен ломать UI; +- breaking changes требуют version bump контракта и отдельного architecture + decision; +- будущий React/Tauri UI не должен парсить HTML текущего портала. ## Проверка симметрии diff --git a/docs/PORTAL_API_CONTRACTS_RU.md b/docs/PORTAL_API_CONTRACTS_RU.md index b68e97c..d501f96 100644 --- a/docs/PORTAL_API_CONTRACTS_RU.md +++ b/docs/PORTAL_API_CONTRACTS_RU.md @@ -6,7 +6,7 @@ React/Tauri-интерфейса без переписывания backend-логики. Текущий HTML-портал остаётся рабочим и основным. Новый UI должен подключаться к -тем же endpoint-ам через documented contract. +тем же endpoint-ам через documented contract и не должен парсить HTML. ## Контрактные endpoint-ы @@ -30,6 +30,12 @@ DPD mirror: - Клиенты обязаны корректно обрабатывать отсутствующие optional-поля. - Поля с `null` не должны ломать UI. - Ошибки API должны отображаться пользователю на русском языке. +- Breaking changes требуют отдельного version bump контракта и architecture + decision. +- Будущий React/Tauri UI не должен парсить HTML-страницы, CSS или встроенный + JavaScript текущего портала как источник данных. +- Бизнес-логика остаётся на стороне Rust backend/API; frontend только + отображает и отправляет пользовательские действия через контрактные endpoint-ы. ## Минимальный набор для будущего React/Tauri UI @@ -52,6 +58,7 @@ DPD mirror: - Маршрут `/portal/` остаётся стабильным. - DPD `/dpd/` остаётся параллельным mirror. - Backend-расчёты, JSON-хранилища и workflow не дублируются во frontend. +- Публичные JSON-поля не переименовываются без новой версии контракта. ## Проверка diff --git a/docs/UI_ARCHITECTURE_BASELINE_RU.md b/docs/UI_ARCHITECTURE_BASELINE_RU.md index d6d8e83..4c9e8c4 100644 --- a/docs/UI_ARCHITECTURE_BASELINE_RU.md +++ b/docs/UI_ARCHITECTURE_BASELINE_RU.md @@ -7,7 +7,7 @@ ```text Backend/API: Rust Agent: Rust -Current Portal: Rust server-rendered HTML + HTMX-ready static UI +Current Portal: Rust server-rendered HTML + HTMX Future Enterprise UI: React + TypeScript Future Desktop Forensics: Tauri + React + Rust core Dioxus: out of scope / не рассматривается @@ -15,14 +15,15 @@ Dioxus: out of scope / не рассматривается ## Правила -- Текущий HTML/HTMX-ready портал остаётся основным pilot/production - интерфейсом. +- Текущий HTML/HTMX портал остаётся основным pilot/production интерфейсом. - JSON API является контрактным слоем для будущих UI. - Бизнес-логика не должна зависеть от HTML. - Будущий React/Tauri UI не должен ломать текущий портал. - Agent и backend остаются Rust-first. - Новые UI-фреймворки не добавлять без отдельного architecture decision. - Dioxus не добавлять и не рассматривать. +- Будущий React/Tauri UI не должен парсить HTML текущего портала как источник + данных. ## Практическое следствие @@ -32,6 +33,8 @@ Dioxus: out of scope / не рассматривается 2. Зафиксировать стабильные JSON API-контракты. 3. Поддерживать DPD `/dpd/` как параллельный mirror для проверки совместимости. 4. Готовить будущий React/Tauri UI только поверх опубликованных контрактов. +5. Любые breaking changes проводить только через version bump контракта и + отдельное architecture decision. ## Контрактный слой @@ -48,5 +51,5 @@ DPD mirror endpoints: - `/dpd/api/contracts/typescript.d.ts`. Правило совместимости: изменения API должны быть additive. Клиенты обязаны -игнорировать неизвестные поля и корректно обрабатывать отсутствующие optional -поля. +игнорировать неизвестные поля, корректно обрабатывать отсутствующие optional +поля и не падать на `null`.