test(portal): harden api contract validation

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