feat(lab): add lazy E40 evidence review

This commit is contained in:
DCCONSTRUCTIONS
2026-07-30 21:53:04 +03:00
parent e9bbfb9a41
commit fdcaf57ae7
24 changed files with 3570 additions and 165 deletions
@@ -0,0 +1,243 @@
import {
AdvancedLaboratoryContractError,
fetchOne,
parseE31,
parseE32,
parseE33,
parseE37,
parseE38,
parseE39,
type AdvancedLaboratoryResults,
type LaboratoryFetch,
} from "./advancedResults";
import { fetchE34TemporalLayerResult } from "./e34TemporalLayer";
import { fetchE35DegradationRecoveryResult } from "./e35DegradationRecovery";
import { fetchE40ProductGateResult } from "./e40ProductGate";
export type AdvancedLaboratoryWorkId =
| "e31-source-binding"
| "e32-track-geometry"
| "e33-worker-shadow"
| "e34-temporal-layer"
| "e35-degradation-recovery"
| "e37-ravnoves-acceptance"
| "e38-perception-baseline"
| "e39-perception-refinement"
| "e40-perception-product-gate";
export interface AdvancedLaboratoryIndexItem {
workId: AdvancedLaboratoryWorkId;
resultId: string;
createdAtUtc: string;
}
const WORK_IDS: readonly AdvancedLaboratoryWorkId[] = [
"e31-source-binding",
"e32-track-geometry",
"e33-worker-shadow",
"e34-temporal-layer",
"e35-degradation-recovery",
"e37-ravnoves-acceptance",
"e38-perception-baseline",
"e39-perception-refinement",
"e40-perception-product-gate",
];
const RESULT_PREFIX: Readonly<Record<AdvancedLaboratoryWorkId, string>> = {
"e31-source-binding": "e31-source-qualification",
"e32-track-geometry": "e32-track-geometry",
"e33-worker-shadow": "e33-worker-shadow",
"e34-temporal-layer": "e34-temporal-occupied",
"e35-degradation-recovery": "e35-degradation-recovery",
"e37-ravnoves-acceptance": "e37-ravnoves-acceptance",
"e38-perception-baseline": "e38-perception-baseline",
"e39-perception-refinement": "e39-perception-refinement",
"e40-perception-product-gate": "e40-perception-product-gate",
};
export function isAdvancedLaboratoryWorkId(
value: string,
): value is AdvancedLaboratoryWorkId {
return WORK_IDS.includes(value as AdvancedLaboratoryWorkId);
}
export function emptyAdvancedLaboratoryResults(): AdvancedLaboratoryResults {
return {
e31: null,
e32: null,
e33: null,
e34: null,
e35: null,
e37: null,
e38: null,
e39: null,
e40: null,
};
}
function objectValue(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new AdvancedLaboratoryContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function exactString(value: unknown, expected: string, label: string): string {
if (value !== expected) {
throw new AdvancedLaboratoryContractError(`${label}: нарушен контракт.`);
}
return expected;
}
function parseIndexItem(value: unknown): AdvancedLaboratoryIndexItem {
const item = objectValue(value, "Индекс LAB.item");
if (
typeof item.work_id !== "string"
|| !isAdvancedLaboratoryWorkId(item.work_id)
) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.work_id: неизвестная лабораторная работа.",
);
}
const prefix = RESULT_PREFIX[item.work_id];
if (
typeof item.result_id !== "string"
|| !new RegExp(`^${prefix}-[a-f0-9]{64}$`).test(item.result_id)
) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.result_id: нарушена идентичность результата.",
);
}
if (typeof item.created_at_utc !== "string" || !item.created_at_utc.trim()) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.created_at_utc: ожидалась строка.",
);
}
exactString(item.access, "read-only", "Индекс LAB.item.access");
return {
workId: item.work_id,
resultId: item.result_id,
createdAtUtc: item.created_at_utc,
};
}
export async function fetchAdvancedLaboratoryIndex({
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {}): Promise<readonly AdvancedLaboratoryIndexItem[]> {
const response = await fetcher("/api/v1/laboratory/advanced-index", {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) {
throw new AdvancedLaboratoryContractError(
`Индекс LAB недоступен: HTTP ${response.status}.`,
);
}
const payload = objectValue(await response.json(), "Индекс LAB");
exactString(
payload.schema_version,
"missioncore.laboratory-advanced-index/v1",
"Индекс LAB.schema_version",
);
exactString(payload.access, "read-only", "Индекс LAB.access");
if (!Array.isArray(payload.items) || payload.items.length > WORK_IDS.length) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.items: нарушен размер каталога.",
);
}
const items = payload.items.map(parseIndexItem);
if (new Set(items.map((item) => item.workId)).size !== items.length) {
throw new AdvancedLaboratoryContractError(
"Индекс LAB.items: лабораторная работа продублирована.",
);
}
return items;
}
export function advancedLaboratoryResultAvailable(
workId: AdvancedLaboratoryWorkId,
results: AdvancedLaboratoryResults,
): boolean {
return workId === "e31-source-binding" ? results.e31 !== null
: workId === "e32-track-geometry" ? results.e32 !== null
: workId === "e33-worker-shadow" ? results.e33 !== null
: workId === "e34-temporal-layer" ? results.e34 !== null
: workId === "e35-degradation-recovery" ? results.e35 !== null
: workId === "e37-ravnoves-acceptance" ? results.e37 !== null
: workId === "e38-perception-baseline" ? results.e38 !== null
: workId === "e39-perception-refinement" ? results.e39 !== null
: results.e40 !== null;
}
export async function fetchAdvancedLaboratoryResult(
workId: AdvancedLaboratoryWorkId,
{
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {},
): Promise<AdvancedLaboratoryResults> {
const results = emptyAdvancedLaboratoryResults();
if (workId === "e31-source-binding") {
results.e31 = await fetchOne(
"/api/v1/laboratory/e31/results?limit=1",
parseE31,
fetcher,
signal,
);
} else if (workId === "e32-track-geometry") {
results.e32 = await fetchOne(
"/api/v1/laboratory/e32/results?limit=1",
parseE32,
fetcher,
signal,
);
} else if (workId === "e33-worker-shadow") {
results.e33 = await fetchOne(
"/api/v1/laboratory/e33/results?limit=1",
parseE33,
fetcher,
signal,
);
} else if (workId === "e34-temporal-layer") {
results.e34 = await fetchE34TemporalLayerResult({ fetcher, signal });
} else if (workId === "e35-degradation-recovery") {
results.e35 = await fetchE35DegradationRecoveryResult({ fetcher, signal });
} else if (workId === "e37-ravnoves-acceptance") {
results.e37 = await fetchOne(
"/api/v1/laboratory/e37/results?limit=1",
parseE37,
fetcher,
signal,
);
} else if (workId === "e38-perception-baseline") {
results.e38 = await fetchOne(
"/api/v1/laboratory/e38/results?limit=1",
parseE38,
fetcher,
signal,
);
} else if (workId === "e39-perception-refinement") {
results.e39 = await fetchOne(
"/api/v1/laboratory/e39/results?limit=1",
parseE39,
fetcher,
signal,
);
} else {
results.e40 = await fetchE40ProductGateResult({ fetcher, signal });
}
if (!advancedLaboratoryResultAvailable(workId, results)) {
throw new AdvancedLaboratoryContractError(
"Выбранная лабораторная работа не прошла серверную проверку.",
);
}
return results;
}
@@ -256,7 +256,7 @@ export class AdvancedLaboratoryContractError extends Error {
}
}
type LaboratoryFetch = (
export type LaboratoryFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
@@ -402,7 +402,7 @@ function parseCatalog<T>(
return value.items.length ? parseItem(value.items[0]) : null;
}
function parseE31(value: unknown): E31LaboratoryResult {
export function parseE31(value: unknown): E31LaboratoryResult {
const item = record(value, "E31");
const metrics = record(item.metrics, "E31.metrics");
diagnosticAuthority(item.authority, "E31.authority");
@@ -437,7 +437,7 @@ function parseE31(value: unknown): E31LaboratoryResult {
};
}
function parseE32(value: unknown): E32LaboratoryResult {
export function parseE32(value: unknown): E32LaboratoryResult {
const item = record(value, "E32");
const metrics = record(item.metrics, "E32.metrics");
diagnosticAuthority(item.authority, "E32.authority");
@@ -470,7 +470,7 @@ function parseE32(value: unknown): E32LaboratoryResult {
};
}
function parseE33(value: unknown): E33LaboratoryResult {
export function parseE33(value: unknown): E33LaboratoryResult {
const item = record(value, "E33");
const worker = record(item.worker, "E33.worker");
const metrics = record(item.metrics, "E33.metrics");
@@ -521,7 +521,7 @@ function parseE33(value: unknown): E33LaboratoryResult {
};
}
function parseE37(value: unknown): E37AcceptanceContractResult {
export function parseE37(value: unknown): E37AcceptanceContractResult {
const item = record(value, "E37");
const metrics = record(item.metrics, "E37.metrics");
const distributions = record(
@@ -718,7 +718,7 @@ function parseE38Dimension(
};
}
function parseE38(value: unknown): E38PerceptionBaselineResult {
export function parseE38(value: unknown): E38PerceptionBaselineResult {
const item = record(value, "E38");
const metrics = record(item.metrics, "E38.metrics");
const dimensions = record(metrics.dimensions, "E38.metrics.dimensions");
@@ -810,7 +810,7 @@ function parseDevelopmentDimension(
};
}
function parseE39(value: unknown): E39PerceptionRefinementResult {
export function parseE39(value: unknown): E39PerceptionRefinementResult {
const item = record(value, "E39");
const metrics = record(item.metrics, "E39.metrics");
const dimensions = record(metrics.dimensions, "E39.metrics.dimensions");
@@ -944,7 +944,7 @@ function parseE39(value: unknown): E39PerceptionRefinementResult {
};
}
async function fetchOne<T>(
export async function fetchOne<T>(
path: string,
parser: (value: unknown) => T,
fetcher: LaboratoryFetch,
@@ -59,6 +59,8 @@ export interface E30ReviewItem {
materialization: {
framePointCount: number;
projectedPointCount: number;
projectionWidth: number;
projectionHeight: number;
candidatePointCount: number;
selectedPointCount: number;
rejectedCandidatePointCount: number;
@@ -428,6 +430,14 @@ function parseItem(value: unknown): E30ReviewItem {
materialized.projected_point_count,
"E30 materialization.projected_point_count",
),
projectionWidth: integerValue(
materialized.projection_width,
"E30 materialization.projection_width",
),
projectionHeight: integerValue(
materialized.projection_height,
"E30 materialization.projection_height",
),
candidatePointCount: integerValue(
materialized.candidate_point_count,
"E30 materialization.candidate_point_count",
@@ -0,0 +1,538 @@
export const E40_CASE_DIMENSIONS = [
"presence",
"geometry_association",
"freshness",
] as const;
export type E40CaseDimension = (typeof E40_CASE_DIMENSIONS)[number];
export type E40CaseSeverity = "high" | "medium" | "standard";
export type E40CaseStratum =
| "agree"
| "camera-only"
| "conflict"
| "geometry-only"
| "unknown";
export type E40PredictionBasis =
| "camera-only-softmax"
| "fixed-stratum-policy";
export interface E40CaseState {
presence: "background-or-noise" | "object-present" | "occupied-environment";
geometryAssociation:
| "independent-occupied"
| "insufficient-support"
| "object-associated"
| "rejected-nonobject"
| "unknown";
freshness: "current" | "stale" | "unavailable";
}
export interface E40ErrorCase {
itemId: string;
sequence: number;
sourceFrameIndex: number;
sourceStratum: E40CaseStratum;
severity: E40CaseSeverity;
presenceConfidence: number;
predictionBasis: E40PredictionBasis;
reference: E40CaseState;
prediction: E40CaseState;
mismatchedDimensions: readonly E40CaseDimension[];
access: "read-only";
}
export interface E40CaseCatalog {
resultId: string;
materializationId: string;
items: readonly E40ErrorCase[];
total: number;
truncated: boolean;
access: "read-only";
}
export type E40OperatorVerdict = "confirmed-error" | "rejected-error";
export interface E40OperatorDecision {
itemId: string;
verdict: E40OperatorVerdict;
revision: number;
idempotencyKey: string;
decidedAtUtc: string;
eventId: string;
}
export interface E40OperatorReview {
reviewId: string;
resultId: string;
materializationId: string;
caseCatalogSha256: string;
itemSetSha256: string;
itemCount: number;
reviewerId: string;
revision: number;
decisions: readonly E40OperatorDecision[];
reviewedItemCount: number;
remainingItemCount: number;
updatedAtUtc: string | null;
access: "review-write";
}
export class E40CaseReviewContractError extends Error {
constructor(message: string) {
super(message);
this.name = "E40CaseReviewContractError";
}
}
type LaboratoryFetch = (
input: RequestInfo | URL,
init?: RequestInit,
) => Promise<Response>;
function record(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new E40CaseReviewContractError(`${label}: ожидался объект.`);
}
return value as Record<string, unknown>;
}
function stringValue(value: unknown, label: string): string {
if (typeof value !== "string" || !value.trim()) {
throw new E40CaseReviewContractError(`${label}: ожидалась строка.`);
}
return value;
}
function contentId(value: unknown, prefix: string, label: string): string {
const parsed = stringValue(value, label);
if (!new RegExp(`^${prefix}-[a-f0-9]{64}$`).test(parsed)) {
throw new E40CaseReviewContractError(`${label}: неверный content id.`);
}
return parsed;
}
function sha256Value(value: unknown, label: string): string {
const parsed = stringValue(value, label);
if (!/^[a-f0-9]{64}$/.test(parsed)) {
throw new E40CaseReviewContractError(`${label}: неверный SHA-256.`);
}
return parsed;
}
function integerValue(value: unknown, label: string): number {
if (
typeof value !== "number"
|| !Number.isSafeInteger(value)
|| value < 0
) {
throw new E40CaseReviewContractError(`${label}: ожидалось целое число.`);
}
return value;
}
function nullableString(value: unknown, label: string): string | null {
if (value === null) return null;
return stringValue(value, label);
}
function probability(value: unknown, label: string): number {
if (
typeof value !== "number"
|| !Number.isFinite(value)
|| value < 0
|| value > 1
) {
throw new E40CaseReviewContractError(`${label}: ожидалась вероятность.`);
}
return value;
}
function exactString<T extends string>(
value: unknown,
expected: T,
label: string,
): T {
if (value !== expected) {
throw new E40CaseReviewContractError(`${label}: неверное значение.`);
}
return expected;
}
function enumValue<T extends string>(
value: unknown,
allowed: readonly T[],
label: string,
): T {
if (typeof value !== "string" || !allowed.includes(value as T)) {
throw new E40CaseReviewContractError(`${label}: значение не поддерживается.`);
}
return value as T;
}
function parseState(value: unknown, label: string): E40CaseState {
const source = record(value, label);
return {
presence: enumValue(
source.presence,
[
"background-or-noise",
"object-present",
"occupied-environment",
],
`${label}.presence`,
),
geometryAssociation: enumValue(
source.geometry_association,
[
"independent-occupied",
"insufficient-support",
"object-associated",
"rejected-nonobject",
"unknown",
],
`${label}.geometry_association`,
),
freshness: enumValue(
source.freshness,
["current", "stale", "unavailable"],
`${label}.freshness`,
),
};
}
function parseCase(value: unknown, index: number): E40ErrorCase {
const label = `E40 case[${index}]`;
const source = record(value, label);
if (!Array.isArray(source.mismatched_dimensions)) {
throw new E40CaseReviewContractError(
`${label}.mismatched_dimensions: ожидался массив.`,
);
}
const mismatchedDimensions = source.mismatched_dimensions.map(
(dimension, dimensionIndex) => enumValue(
dimension,
E40_CASE_DIMENSIONS,
`${label}.mismatched_dimensions[${dimensionIndex}]`,
),
);
if (!mismatchedDimensions.length) {
throw new E40CaseReviewContractError(`${label}: кейс не содержит ошибки.`);
}
return {
itemId: contentId(source.item_id, "e30-review-item", `${label}.item_id`),
sequence: integerValue(source.sequence, `${label}.sequence`),
sourceFrameIndex: integerValue(
source.source_frame_index,
`${label}.source_frame_index`,
),
sourceStratum: enumValue(
source.source_stratum,
["agree", "camera-only", "conflict", "geometry-only", "unknown"],
`${label}.source_stratum`,
),
severity: enumValue(
source.severity,
["high", "medium", "standard"],
`${label}.severity`,
),
presenceConfidence: probability(
source.presence_confidence,
`${label}.presence_confidence`,
),
predictionBasis: enumValue(
source.prediction_basis,
["camera-only-softmax", "fixed-stratum-policy"],
`${label}.prediction_basis`,
),
reference: parseState(source.reference, `${label}.reference`),
prediction: parseState(source.prediction, `${label}.prediction`),
mismatchedDimensions,
access: exactString(source.access, "read-only", `${label}.access`),
};
}
export function parseE40CaseCatalog(payload: unknown): E40CaseCatalog {
const source = record(payload, "E40 case catalog");
exactString(
source.schema_version,
"missioncore.laboratory-e40-case-catalog/v1",
"E40 case catalog.schema_version",
);
if (!Array.isArray(source.items) || source.items.length > 64) {
throw new E40CaseReviewContractError(
"E40 case catalog.items: нарушена граница.",
);
}
return {
resultId: contentId(
source.result_id,
"e40-perception-product-gate",
"E40 case catalog.result_id",
),
materializationId: contentId(
source.materialization_id,
"e30-materialization",
"E40 case catalog.materialization_id",
),
items: source.items.map(parseCase),
total: integerValue(source.total, "E40 case catalog.total"),
truncated: typeof source.truncated === "boolean"
? source.truncated
: (() => {
throw new E40CaseReviewContractError(
"E40 case catalog.truncated: ожидался boolean.",
);
})(),
access: exactString(
source.access,
"read-only",
"E40 case catalog.access",
),
};
}
function parseOperatorDecision(
value: unknown,
index: number,
): E40OperatorDecision {
const label = `E40 operator decision[${index}]`;
const source = record(value, label);
return {
itemId: contentId(source.item_id, "e30-review-item", `${label}.item_id`),
verdict: enumValue(
source.verdict,
["confirmed-error", "rejected-error"],
`${label}.verdict`,
),
revision: integerValue(source.revision, `${label}.revision`),
idempotencyKey: stringValue(
source.idempotency_key,
`${label}.idempotency_key`,
),
decidedAtUtc: stringValue(
source.decided_at_utc,
`${label}.decided_at_utc`,
),
eventId: contentId(
source.event_id,
"e40-operator-verdict",
`${label}.event_id`,
),
};
}
export function parseE40OperatorReview(
payload: unknown,
): E40OperatorReview {
const source = record(payload, "E40 operator review");
exactString(
source.schema_version,
"missioncore.e40-operator-review/v1",
"E40 operator review.schema_version",
);
exactString(
source.protocol,
"sealed-error-adjudication/v1",
"E40 operator review.protocol",
);
const binding = record(source.source, "E40 operator review.source");
if (!Array.isArray(source.decisions) || source.decisions.length > 64) {
throw new E40CaseReviewContractError(
"E40 operator review.decisions: нарушена граница.",
);
}
const decisions = source.decisions.map(parseOperatorDecision);
const reviewedItemCount = integerValue(
source.reviewed_item_count,
"E40 operator review.reviewed_item_count",
);
const remainingItemCount = integerValue(
source.remaining_item_count,
"E40 operator review.remaining_item_count",
);
const itemCount = integerValue(
binding.item_count,
"E40 operator review.source.item_count",
);
if (
reviewedItemCount !== decisions.length
|| reviewedItemCount + remainingItemCount !== itemCount
|| new Set(decisions.map((decision) => decision.itemId)).size
!== decisions.length
) {
throw new E40CaseReviewContractError(
"E40 operator review: счётчики решений несовместимы.",
);
}
return {
reviewId: contentId(
source.review_id,
"e40-operator-review",
"E40 operator review.review_id",
),
resultId: contentId(
binding.result_id,
"e40-perception-product-gate",
"E40 operator review.source.result_id",
),
materializationId: contentId(
binding.materialization_id,
"e30-materialization",
"E40 operator review.source.materialization_id",
),
caseCatalogSha256: sha256Value(
binding.case_catalog_sha256,
"E40 operator review.source.case_catalog_sha256",
),
itemSetSha256: sha256Value(
binding.item_set_sha256,
"E40 operator review.source.item_set_sha256",
),
itemCount,
reviewerId: stringValue(
source.reviewer_id,
"E40 operator review.reviewer_id",
),
revision: integerValue(
source.revision,
"E40 operator review.revision",
),
decisions,
reviewedItemCount,
remainingItemCount,
updatedAtUtc: nullableString(
source.updated_at_utc,
"E40 operator review.updated_at_utc",
),
access: exactString(
source.access,
"review-write",
"E40 operator review.access",
),
};
}
export async function fetchE40CaseCatalog(
resultId: string,
{
fetcher = fetch,
signal,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {},
): Promise<E40CaseCatalog> {
const response = await fetcher(
`/api/v1/laboratory/e40/results/${encodeURIComponent(resultId)}/cases?limit=48`,
{
method: "GET",
headers: { Accept: "application/json" },
signal,
},
);
if (!response.ok) {
throw new E40CaseReviewContractError(
`Case-review E40 недоступен: HTTP ${response.status}.`,
);
}
const catalog = parseE40CaseCatalog(await response.json());
if (catalog.resultId !== resultId) {
throw new E40CaseReviewContractError(
"Case-review E40 вернул другой результат.",
);
}
return catalog;
}
export async function fetchE40OperatorReview(
resultId: string,
{
reviewerId = "DC",
fetcher = fetch,
signal,
}: {
reviewerId?: string;
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
} = {},
): Promise<E40OperatorReview> {
const response = await fetcher(
`/api/v1/laboratory/e40/results/${encodeURIComponent(resultId)}`
+ `/operator-review?reviewer_id=${encodeURIComponent(reviewerId)}`,
{
method: "GET",
headers: { Accept: "application/json" },
signal,
},
);
if (!response.ok) {
throw new E40CaseReviewContractError(
`Операторская проверка E40 недоступна: HTTP ${response.status}.`,
);
}
const review = parseE40OperatorReview(await response.json());
if (review.resultId !== resultId || review.reviewerId !== reviewerId) {
throw new E40CaseReviewContractError(
"Операторская проверка E40 вернула другой источник.",
);
}
return review;
}
export async function saveE40OperatorVerdict(
resultId: string,
itemId: string,
{
reviewerId = "DC",
expectedRevision,
idempotencyKey,
verdict,
fetcher = fetch,
signal,
}: {
reviewerId?: string;
expectedRevision: number;
idempotencyKey: string;
verdict: E40OperatorVerdict;
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
},
): Promise<E40OperatorReview> {
const response = await fetcher(
`/api/v1/laboratory/e40/results/${encodeURIComponent(resultId)}`
+ `/operator-review/decisions/${encodeURIComponent(itemId)}`,
{
method: "PUT",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
reviewer_id: reviewerId,
expected_revision: expectedRevision,
idempotency_key: idempotencyKey,
verdict,
}),
signal,
},
);
if (!response.ok) {
throw new E40CaseReviewContractError(
`Решение E40 не сохранено: HTTP ${response.status}.`,
);
}
const review = parseE40OperatorReview(await response.json());
if (
review.resultId !== resultId
|| review.reviewerId !== reviewerId
|| !review.decisions.some(
(decision) => decision.itemId === itemId
&& decision.verdict === verdict,
)
) {
throw new E40CaseReviewContractError(
"Решение E40 не подтверждено сервером.",
);
}
return review;
}
+1
View File
@@ -3,6 +3,7 @@
@import "./styles/shell.css";
@import "./styles/workspaces.css";
@import "./styles/laboratory.css";
@import "./styles/e40-case-review.css";
@import "./styles/laboratory-reporting.css";
@import "./styles/e34-temporal-layer.css";
@import "./styles/e35-degradation-recovery.css";
@@ -0,0 +1,297 @@
.e40-case-review {
display: grid;
height: clamp(38rem, 66vh, 52rem);
min-height: 38rem;
grid-template-rows: auto minmax(0, 1fr);
gap: 0.65rem;
border-radius: var(--nodedc-radius-option);
background: var(--nodedc-glass-panel-bg-soft);
padding: 0.75rem;
}
.e40-case-review__header {
display: flex;
min-width: 0;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.e40-case-review__header > div {
display: grid;
min-width: 0;
gap: 0.18rem;
}
.e40-case-review__header strong {
color: var(--nodedc-text-primary);
font-size: 0.78rem;
}
.e40-case-review__header small {
color: var(--nodedc-text-muted);
font-size: 0.56rem;
line-height: 1.45;
}
.e40-case-review__viewer {
display: grid;
min-width: 0;
min-height: 0;
}
.e40-case-review__viewer > .laboratory-evidence-viewer {
min-height: 0;
}
.e40-case-review__state {
display: flex;
min-height: 16rem;
align-items: center;
justify-content: center;
gap: 0.55rem;
border-radius: var(--nodedc-radius-option);
background: var(--nodedc-canvas);
color: var(--nodedc-text-muted);
font-size: 0.62rem;
}
.e40-case-review__controls {
display: flex;
flex: 1;
min-width: 0;
align-items: center;
gap: 0.45rem;
}
.e40-case-review__viewer
.laboratory-evidence-viewer__controls:has(.e40-case-review__controls) {
right: 0.6rem;
left: 0.6rem;
}
.e40-case-review__pagination {
display: flex;
flex: none;
gap: 0.35rem;
}
.e40-case-review__glass-button {
background: var(--nodedc-floating-surface);
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e40-case-review__controls .nodedc-select-anchor,
.e40-case-review__controls .nodedc-select {
width: clamp(15rem, 30vw, 26rem);
}
.e40-case-review__select .nodedc-select__value,
.e40-case-review__select .nodedc-select__toggle,
.e40-case-review__select-menu {
background: var(--nodedc-floating-surface);
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e40-case-review__layer-button {
margin-left: auto;
--nodedc-button-bg: var(--nodedc-floating-surface);
--nodedc-button-color: var(--nodedc-text-secondary);
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e40-case-review__layer-button[data-active="true"] {
--nodedc-button-bg: var(--nodedc-glass-control-active);
--nodedc-button-color: var(--nodedc-glass-control-active-text);
}
.e40-case-review__telemetry {
position: absolute;
z-index: 3;
left: 0.6rem;
bottom: 0.6rem;
display: grid;
width: min(46rem, calc(100% - 1.2rem));
grid-template-columns: minmax(0, 1fr) minmax(14rem, 18rem);
align-items: end;
gap: 0.75rem;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-floating-surface);
padding: 0.55rem 0.65rem;
color: var(--nodedc-text-secondary);
backdrop-filter: blur(var(--nodedc-blur-control));
pointer-events: none;
}
.e40-case-review__telemetry-content {
display: grid;
min-width: 0;
gap: 0.45rem;
}
.e40-case-review__question {
display: grid;
gap: 0.15rem;
border-radius: var(--nodedc-radius-control-compact);
background: var(--nodedc-glass-control-bg);
padding: 0.45rem 0.55rem;
}
.e40-case-review__question > span {
color: var(--nodedc-text-muted);
font-size: 0.46rem;
font-weight: 700;
letter-spacing: 0.12em;
}
.e40-case-review__question > strong {
color: var(--nodedc-text-primary);
font-size: 0.62rem;
line-height: 1.35;
}
.e40-case-review__question > small {
color: var(--nodedc-text-muted);
font-size: 0.49rem;
line-height: 1.35;
}
.e40-case-review__telemetry-content > span,
.e40-case-review__telemetry dt {
color: var(--nodedc-text-muted);
font-size: 0.49rem;
}
.e40-case-review__telemetry dl {
display: grid;
gap: 0.35rem;
margin: 0;
}
.e40-case-review__telemetry dl > div {
display: grid;
grid-template-columns: 6.2rem minmax(0, 1fr);
align-items: start;
gap: 0.45rem;
}
.e40-case-review__telemetry dt,
.e40-case-review__telemetry dd {
margin: 0;
}
.e40-case-review__telemetry dd {
display: grid;
min-width: 0;
gap: 0.12rem;
font-size: 0.55rem;
}
.e40-case-review__telemetry dd span,
.e40-case-review__telemetry dd strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.e40-case-review__telemetry dd strong {
color: var(--nodedc-text-primary);
}
.e40-case-review__verdict {
display: grid;
justify-items: end;
gap: 0.28rem;
pointer-events: auto;
}
.e40-case-review__verdict > span {
color: var(--nodedc-text-muted);
font-size: 0.49rem;
white-space: nowrap;
}
.e40-case-review__verdict-error {
max-width: 12rem;
color: rgb(var(--nodedc-danger-rgb));
font-size: 0.48rem;
line-height: 1.35;
text-align: right;
}
.e40-case-review__verdict-buttons {
display: flex;
gap: 0.2rem;
border-radius: 999px;
background: var(--nodedc-glass-control-bg);
padding: 0.18rem;
backdrop-filter: blur(var(--nodedc-blur-control));
}
.e40-case-review__verdict-guide {
display: grid;
justify-self: stretch;
gap: 0.26rem;
}
.e40-case-review__verdict-guide > span {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: start;
gap: 0.28rem;
color: var(--nodedc-text-muted);
font-size: 0.49rem;
line-height: 1.35;
}
.e40-case-review__verdict-guide > span[data-verdict="confirmed"] > svg {
color: rgb(var(--nodedc-success-rgb));
}
.e40-case-review__verdict-guide > span[data-verdict="rejected"] > svg {
color: rgb(var(--nodedc-danger-rgb));
}
.e40-case-review__commit-note {
max-width: 18rem;
color: var(--nodedc-text-muted);
font-size: 0.46rem;
line-height: 1.3;
text-align: right;
}
.e40-case-review__verdict-button {
width: 2rem;
height: 2rem;
min-width: 2rem;
background: transparent;
}
.e40-case-review__verdict-button[data-active="true"][data-verdict="confirmed"] {
background: rgb(var(--nodedc-success-rgb) / 0.12);
color: rgb(var(--nodedc-success-rgb));
}
.e40-case-review__verdict-button[data-active="true"][data-verdict="rejected"] {
background: rgb(var(--nodedc-danger-rgb) / 0.12);
color: rgb(var(--nodedc-danger-rgb));
}
@media (max-width: 900px) {
.e40-case-review {
height: 42rem;
}
.e40-case-review__controls .nodedc-select-anchor,
.e40-case-review__controls .nodedc-select {
width: clamp(10rem, 42vw, 18rem);
}
.e40-case-review__telemetry {
grid-template-columns: minmax(0, 1fr);
}
.e40-case-review__verdict {
justify-items: start;
}
}
@@ -460,7 +460,7 @@ export function E30EvidencePointCloud({ detail }: E30EvidencePointCloudProps) {
</span>
<span data-point="rejected">Отклонено · {rejectedCount}</span>
<span data-point="selected">
Выбрано E29 · {detail.selected.pointsMapXyzM.length}
Проверяемый кластер E29 · {detail.selected.pointsMapXyzM.length}
</span>
</div>
</div>
@@ -206,7 +206,7 @@ export function E30EvidenceProjection({
</span>
<span data-point="rejected">Кандидаты · {rejectedCount}</span>
<span data-point="selected">
Выбрано E29 · {detail.selected.pointsMapXyzM.length}
Проверяемый кластер E29 · {detail.selected.pointsMapXyzM.length}
</span>
</div>
) : null}
@@ -1,6 +1,11 @@
import type { ComponentType } from "react";
import type { LaboratoryOption } from "../../components/laboratory/LaboratoryPresentation";
import {
isAdvancedLaboratoryWorkId,
type AdvancedLaboratoryIndexItem,
type AdvancedLaboratoryWorkId,
} from "../../core/laboratory/advancedIndex";
import type { AdvancedLaboratoryResults } from "../../core/laboratory/advancedResults";
import type { ObservationSessionSummary } from "../../core/observation/sessionArchive";
import type { WorkspaceRendererProps } from "../contracts";
@@ -15,97 +20,29 @@ import { E39Result } from "./E39Result";
import { E40Result } from "./E40Result";
import { RecordedReplayEvidence } from "./RecordedReplayEvidence";
export type AdvancedLaboratoryWorkId =
| "e31-source-binding"
| "e32-track-geometry"
| "e33-worker-shadow"
| "e34-temporal-layer"
| "e35-degradation-recovery"
| "e37-ravnoves-acceptance"
| "e38-perception-baseline"
| "e39-perception-refinement"
| "e40-perception-product-gate";
export { isAdvancedLaboratoryWorkId };
export type { AdvancedLaboratoryWorkId };
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
export function isAdvancedLaboratoryWorkId(
value: string,
): value is AdvancedLaboratoryWorkId {
return (
value === "e31-source-binding"
|| value === "e32-track-geometry"
|| value === "e33-worker-shadow"
|| value === "e34-temporal-layer"
|| value === "e35-degradation-recovery"
|| value === "e37-ravnoves-acceptance"
|| value === "e38-perception-baseline"
|| value === "e39-perception-refinement"
|| value === "e40-perception-product-gate"
);
}
export function advancedLaboratoryWorkOptions(
results: AdvancedLaboratoryResults,
sourceSessions: ReadonlyMap<string, ObservationSessionSummary>,
index: readonly AdvancedLaboratoryIndexItem[],
): readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] {
const options: LaboratoryOption<AdvancedLaboratoryWorkId>[] = [];
if (results.e31 && sourceSessions.has(results.e31.sourceSessionId)) {
options.push({
id: "e31-source-binding",
label: "LAB E31 · source binding",
});
}
if (results.e32 && sourceSessions.has(results.e32.sourceSessionId)) {
options.push({
id: "e32-track-geometry",
label: "LAB E32 · TrackGeometry v1",
});
}
if (results.e33 && sourceSessions.has(results.e33.sourceSessionId)) {
options.push({
id: "e33-worker-shadow",
label: "LAB E33 · worker shadow 1×",
});
}
if (results.e34) {
options.push({
id: "e34-temporal-layer",
label: "LAB E34 · temporal occupied/unknown",
});
}
if (results.e35) {
options.push({
id: "e35-degradation-recovery",
label: "LAB E35 · degradation recovery",
});
}
if (results.e37) {
options.push({
id: "e37-ravnoves-acceptance",
label: "LAB E37 · RAVNOVES00 acceptance R0",
});
}
if (results.e38) {
options.push({
id: "e38-perception-baseline",
label: "LAB E38 · perception quality R1",
});
}
if (results.e39) {
options.push({
id: "e39-perception-refinement",
label: "LAB E39 · perception refinement R1",
});
}
if (results.e40) {
options.push({
id: "e40-perception-product-gate",
label: "LAB E40 · leakage-resistant product gate",
});
}
return options;
const available = new Set(index.map(({ workId }) => workId));
const options: readonly LaboratoryOption<AdvancedLaboratoryWorkId>[] = [
{ id: "e31-source-binding", label: "LAB E31 · source binding" },
{ id: "e32-track-geometry", label: "LAB E32 · TrackGeometry v1" },
{ id: "e33-worker-shadow", label: "LAB E33 · worker shadow 1×" },
{ id: "e34-temporal-layer", label: "LAB E34 · temporal occupied/unknown" },
{ id: "e35-degradation-recovery", label: "LAB E35 · degradation recovery" },
{ id: "e37-ravnoves-acceptance", label: "LAB E37 · RAVNOVES00 acceptance R0" },
{ id: "e38-perception-baseline", label: "LAB E38 · perception quality R1" },
{ id: "e39-perception-refinement", label: "LAB E39 · perception refinement R1" },
{ id: "e40-perception-product-gate", label: "LAB E40 · leakage-resistant product gate" },
];
return options.filter(({ id }) => available.has(id));
}
export function advancedLaboratorySourceSession(
@@ -0,0 +1,550 @@
import { useEffect, useState } from "react";
import {
Button,
Icon,
IconButton,
Select,
StatusBadge,
} from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import {
fetchE30ReviewItemDetail,
type E30ReviewItemDetail,
} from "../../core/laboratory/e30Review";
import {
fetchE40CaseCatalog,
fetchE40OperatorReview,
saveE40OperatorVerdict,
type E40CaseCatalog,
type E40CaseDimension,
type E40CaseSeverity,
type E40CaseState,
type E40CaseStratum,
type E40ErrorCase,
type E40OperatorReview,
type E40OperatorVerdict,
} from "../../core/laboratory/e40CaseReview";
import { E30EvidencePointCloud } from "../E30EvidencePointCloud";
import { E30EvidenceProjection } from "../E30EvidenceProjection";
type EvidenceMode = "camera" | "3d";
const SEVERITY_LABELS: Record<E40CaseSeverity, string> = {
high: "Высокий приоритет",
medium: "Средний приоритет",
standard: "Стандартный приоритет",
};
const STRATUM_LABELS: Record<E40CaseStratum, string> = {
agree: "согласованные источники",
"camera-only": "только камера",
conflict: "конфликт источников",
"geometry-only": "только геометрия",
unknown: "неопределённый источник",
};
const DIMENSION_LABELS: Record<E40CaseDimension, string> = {
presence: "Присутствие",
geometry_association: "Связь с геометрией",
freshness: "Актуальность",
};
const VALUE_LABELS: Readonly<Record<string, string>> = {
"background-or-noise": "Фон или шум",
"object-present": "Объект присутствует",
"occupied-environment": "Занятая среда",
"independent-occupied": "Независимая геометрия",
"insufficient-support": "Недостаточно опоры",
"object-associated": "Связано с объектом",
"rejected-nonobject": "Не объект",
unknown: "Не определено",
current: "Актуально",
stale: "Устарело",
unavailable: "Недоступно",
};
function stateValue(
state: E40CaseState,
dimension: E40CaseDimension,
): string {
if (dimension === "presence") return state.presence;
if (dimension === "geometry_association") {
return state.geometryAssociation;
}
return state.freshness;
}
function caseLabel(item: E40ErrorCase): string {
const dimensions = item.mismatchedDimensions
.map((dimension) => DIMENSION_LABELS[dimension])
.join(" + ");
return `${SEVERITY_LABELS[item.severity]} · кадр ${item.sourceFrameIndex} · ${dimensions}`;
}
function reviewPrompt(
item: E40ErrorCase,
detail: E30ReviewItemDetail,
): {
question: string;
evidence: string;
confirm: string;
reject: string;
} {
const hasSelectedPoints = detail.selected.pointsMapXyzM.length > 0;
const subject = hasSelectedPoints
? "Белый LiDAR-кластер"
: "Выделенная область";
const evidence = hasSelectedPoints
? detail.locatorKind === "geometry-only-cluster"
? (
"Белые точки — самостоятельный кластер занятой геометрии E29 без "
+ "класса. Это проверяемая LiDAR-опора, а не отметки ошибки."
)
: (
"Белые точки — LiDAR-опора, которую E29 связал с наблюдением. "
+ "Это проверяемые исходные точки, а не отметки ошибки."
)
: (
"Белого LiDAR-кластера в этом кейсе нет: решение принимается по "
+ "кадру и рамке наблюдения."
);
if (item.reference.presence === "object-present") {
return {
question: `${subject} относится к отдельному видимому объекту?`,
evidence,
confirm: (
"Да — объект виден; эталон верен, расхождение E40 подтверждается."
),
reject: (
"Нет — это фон или часть среды; предполагаемая ошибка отклоняется."
),
};
}
if (item.reference.presence === "background-or-noise") {
return {
question: `${subject} является фоном или шумом, а не объектом?`,
evidence,
confirm: (
"Да — это фон или шум; эталон верен, расхождение E40 подтверждается."
),
reject: (
"Нет — отдельный объект есть; предполагаемая ошибка отклоняется."
),
};
}
return {
question: "Проверяемый эталон точнее описывает кадр, чем решение E40?",
evidence,
confirm: "Да — эталон верен, расхождение E40 подтверждается.",
reject: "Нет — эталон спорен, предполагаемая ошибка отклоняется.",
};
}
export function E40CaseReview({ resultId }: { resultId: string }) {
const [catalog, setCatalog] = useState<E40CaseCatalog | null>(null);
const [selectedItemId, setSelectedItemId] = useState<string | null>(null);
const [detail, setDetail] = useState<E30ReviewItemDetail | null>(null);
const [review, setReview] = useState<E40OperatorReview | null>(null);
const [catalogLoading, setCatalogLoading] = useState(true);
const [detailLoading, setDetailLoading] = useState(false);
const [verdictPending, setVerdictPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const [reviewError, setReviewError] = useState<string | null>(null);
const [mode, setMode] = useState<EvidenceMode>("camera");
const [pointLayerVisible, setPointLayerVisible] = useState(true);
const [expanded, setExpanded] = useState(false);
useEffect(() => {
const controller = new AbortController();
setCatalogLoading(true);
setCatalog(null);
setDetail(null);
setReview(null);
setSelectedItemId(null);
setError(null);
setReviewError(null);
void Promise.all([
fetchE40CaseCatalog(resultId, { signal: controller.signal }),
fetchE40OperatorReview(resultId, { signal: controller.signal }),
]).then(([nextCatalog, nextReview]) => {
if (
nextReview.materializationId !== nextCatalog.materializationId
|| nextReview.reviewedItemCount > nextCatalog.total
) {
throw new Error(
"Операторская проверка E40 не совпадает с каталогом ошибок.",
);
}
setCatalog(nextCatalog);
setReview(nextReview);
setSelectedItemId(nextCatalog.items[0]?.itemId ?? null);
}).catch((caught: unknown) => {
if (controller.signal.aborted) return;
setError(
caught instanceof Error
? caught.message
: "Case-review E40 недоступен.",
);
}).finally(() => {
if (!controller.signal.aborted) setCatalogLoading(false);
});
return () => controller.abort();
}, [resultId]);
useEffect(() => {
if (!catalog || !selectedItemId) {
setDetail(null);
return;
}
const controller = new AbortController();
setDetailLoading(true);
setDetail(null);
setError(null);
void fetchE30ReviewItemDetail(
catalog.materializationId,
selectedItemId,
{ signal: controller.signal },
).then((next) => {
setDetail(next);
setMode(next.cameraFrame ? "camera" : "3d");
}).catch((caught: unknown) => {
if (controller.signal.aborted) return;
setError(
caught instanceof Error
? caught.message
: "Визуальное доказательство кейса недоступно.",
);
}).finally(() => {
if (!controller.signal.aborted) setDetailLoading(false);
});
return () => controller.abort();
}, [catalog, selectedItemId]);
const selectedCase = catalog?.items.find(
(item) => item.itemId === selectedItemId,
) ?? null;
const selectedIndex = selectedCase && catalog
? catalog.items.findIndex((item) => item.itemId === selectedCase.itemId)
: -1;
const selectedDecision = selectedCase
? review?.decisions.find(
(decision) => decision.itemId === selectedCase.itemId,
) ?? null
: null;
const prompt = selectedCase && detail
? reviewPrompt(selectedCase, detail)
: null;
const navigate = (offset: -1 | 1) => {
if (!catalog?.items.length || selectedIndex < 0) return;
const index = (
selectedIndex + offset + catalog.items.length
) % catalog.items.length;
setSelectedItemId(catalog.items[index].itemId);
};
const saveVerdict = async (verdict: E40OperatorVerdict) => {
if (!review || !selectedCase || verdictPending) return;
setVerdictPending(true);
setReviewError(null);
try {
setReview(await saveE40OperatorVerdict(
resultId,
selectedCase.itemId,
{
expectedRevision: review.revision,
idempotencyKey: `ui-${crypto.randomUUID()}`,
verdict,
},
));
} catch (caught) {
setReviewError(
caught instanceof Error
? caught.message
: "Решение оператора не сохранено.",
);
} finally {
setVerdictPending(false);
}
};
if (catalogLoading) {
return (
<div className="e40-case-review__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Проверяем индекс исторической видимой оценки E40</span>
</div>
);
}
if (error && !selectedCase) {
return (
<div className="e40-case-review__state" role="status">
<Icon name="alert" size={18} />
<span>{error}</span>
</div>
);
}
if (!catalog?.items.length) {
return (
<div className="e40-case-review__state" role="status">
<Icon name="check" size={18} />
<span>Расхождений исторической оценки E40 не опубликовано.</span>
</div>
);
}
return (
<div className="e40-case-review">
<header className="e40-case-review__header">
<div>
<span className="section-eyebrow">
CASE REVIEW · HISTORICAL VISIBLE EVALUATION
</span>
<strong>
Визуальная проверка {catalog.items.length} из {catalog.total}{" "}
расхождений
</strong>
<small>
Загружается только выбранный camera/LiDAR-кейс. Проверено
оператором: {review?.reviewedItemCount ?? 0} из {catalog.total}.
</small>
</div>
{selectedCase ? (
<StatusBadge
tone={selectedCase.severity === "high"
? "danger"
: selectedCase.severity === "medium"
? "warning"
: "neutral"}
>
{SEVERITY_LABELS[selectedCase.severity]}
</StatusBadge>
) : null}
</header>
<div className="e40-case-review__viewer">
{detailLoading ? (
<div className="e40-case-review__state" role="status">
<span className="busy-indicator" aria-hidden="true" />
<span>Проверяем точный кадр и LiDAR-геометрию</span>
</div>
) : error || !detail || !selectedCase ? (
<div className="e40-case-review__state" role="status">
<Icon name="alert" size={18} />
<span>{error ?? "Выберите ошибку для проверки."}</span>
</div>
) : (
<LaboratoryEvidenceViewer
label="Визуальная проверка расхождения E40"
mode={mode}
modes={[
{ value: "camera", label: "Камера" },
{ value: "3d", label: "3D" },
]}
expanded={expanded}
onModeChange={setMode}
onExpandedChange={setExpanded}
actions={(
<div className="e40-case-review__controls">
<div
className="e40-case-review__pagination"
aria-label="Последовательная проверка ошибок"
>
<IconButton
className="e40-case-review__glass-button"
label="Предыдущее расхождение"
disabled={catalog.items.length < 2}
onClick={() => navigate(-1)}
>
<Icon name="chevron-left" size={16} />
</IconButton>
<IconButton
className="e40-case-review__glass-button"
label="Следующее расхождение"
disabled={catalog.items.length < 2}
onClick={() => navigate(1)}
>
<Icon name="chevron-right" size={16} />
</IconButton>
</div>
<Select
className="e40-case-review__select"
menuClassName="e40-case-review__select-menu"
label="Расхождение исторической оценки E40"
value={selectedCase.itemId}
options={catalog.items.map((item) => ({
value: item.itemId,
label: caseLabel(item),
}))}
variant="split"
menuWidth="anchor"
onChange={setSelectedItemId}
/>
{mode === "camera" ? (
<Button
className="e40-case-review__layer-button"
size="compact"
variant="secondary"
shape="pill"
icon={<Icon name="sliders" size={16} />}
data-active={pointLayerVisible ? "true" : undefined}
aria-pressed={pointLayerVisible}
onClick={() => setPointLayerVisible((visible) => !visible)}
>
LiDAR
</Button>
) : null}
</div>
)}
overlay={(
<aside
className="e40-case-review__telemetry"
aria-label="Эталон и результат E40"
>
<div className="e40-case-review__telemetry-content">
<span>
Кадр {detail.sourceFrameIndex}
{" · "}
{STRATUM_LABELS[selectedCase.sourceStratum]}
{selectedCase.predictionBasis === "camera-only-softmax"
? (
<>
{" · "}
вероятность camera-only softmax{" "}
{(selectedCase.presenceConfidence * 100).toLocaleString(
"ru-RU",
{ maximumFractionDigits: 1 },
)}%
</>
)
: " · фиксированная stratum-policy, не вероятность"}
</span>
{prompt ? (
<div className="e40-case-review__question">
<span>ЧТО ПРОВЕРЯЕМ</span>
<strong>{prompt.question}</strong>
<small>{prompt.evidence}</small>
</div>
) : null}
<dl>
{selectedCase.mismatchedDimensions.map((dimension) => (
<div key={dimension}>
<dt>{DIMENSION_LABELS[dimension]}</dt>
<dd>
<span>
Эталон ·{" "}
{VALUE_LABELS[
stateValue(selectedCase.reference, dimension)
]}
</span>
<strong>
{selectedCase.predictionBasis
=== "camera-only-softmax"
? "Модель E40"
: "Правило E40"}{" "}
·{" "}
{VALUE_LABELS[
stateValue(selectedCase.prediction, dimension)
]}
</strong>
</dd>
</div>
))}
</dl>
</div>
<div className="e40-case-review__verdict">
<span>
{verdictPending
? "Сохраняем…"
: selectedDecision?.verdict === "confirmed-error"
? "Ошибка подтверждена"
: selectedDecision?.verdict === "rejected-error"
? "Расхождение отклонено"
: "Решение оператора"}
</span>
<div
className="e40-case-review__verdict-buttons"
aria-label="Решение оператора"
>
<IconButton
className="e40-case-review__verdict-button"
data-verdict="confirmed"
data-active={
selectedDecision?.verdict === "confirmed-error"
? "true"
: undefined
}
label={prompt?.confirm ?? "Подтвердить ошибку модели"}
aria-pressed={
selectedDecision?.verdict === "confirmed-error"
}
disabled={verdictPending}
onClick={() => void saveVerdict("confirmed-error")}
>
<Icon name="check" size={16} />
</IconButton>
<IconButton
className="e40-case-review__verdict-button"
data-verdict="rejected"
data-active={
selectedDecision?.verdict === "rejected-error"
? "true"
: undefined
}
label={prompt?.reject ?? "Отклонить ошибку модели"}
aria-pressed={
selectedDecision?.verdict === "rejected-error"
}
disabled={verdictPending}
onClick={() => void saveVerdict("rejected-error")}
>
<Icon name="close" size={16} />
</IconButton>
</div>
{prompt ? (
<div className="e40-case-review__verdict-guide">
<span data-verdict="confirmed">
<Icon name="check" size={12} />
{prompt.confirm}
</span>
<span data-verdict="rejected">
<Icon name="close" size={12} />
{prompt.reject}
</span>
</div>
) : null}
<small className="e40-case-review__commit-note">
Выбор сохраняется сразу; исходные E30/E40 не изменяются.
</small>
{reviewError ? (
<small
className="e40-case-review__verdict-error"
role="alert"
>
{reviewError}
</small>
) : null}
</div>
</aside>
)}
>
{mode === "camera" ? (
<E30EvidenceProjection
detail={detail}
projectionWidth={detail.materialization.projectionWidth}
projectionHeight={detail.materialization.projectionHeight}
pointLayerVisible={pointLayerVisible}
/>
) : (
<E30EvidencePointCloud detail={detail} />
)}
</LaboratoryEvidenceViewer>
)}
</div>
</div>
);
}
@@ -1,6 +1,5 @@
import {
LaboratoryEvidence,
LaboratoryMetricGrid,
LaboratoryResultSummary,
LaboratorySummary,
LaboratoryWorkTemplate,
@@ -9,6 +8,7 @@ import type {
E40PerceptionProductGateResult,
} from "../../core/laboratory/e40ProductGate";
import { formatNumber } from "../../presentation";
import { E40CaseReview } from "./E40CaseReview";
function percent(value: number): string {
return `${(value * 100).toLocaleString("ru-RU", {
@@ -32,14 +32,14 @@ export function E40Result({
result.developmentCrossValidation.protocols.wholeTrackOrSceneWindow.dimensions
);
const gateLabel = result.qualityGatePassed
? "RAVNOVES00 product gate пройден"
: "RAVNOVES00 product gate не пройден";
? "Историческая оценка E40 пройдена"
: "Историческая оценка E40 не пройдена";
return (
<LaboratoryWorkTemplate
summary={(
<LaboratorySummary
title="LAB E40 · leakage-resistant product gate"
description="После E39 проверена route-coordinate-free модель: смежные кадры, целые track и scene-window изолированы при development-проверке, а sealed validation исполнен один раз на Worker 006."
title="LAB E40 · historical visible engineering evaluation"
description="Историческая source-scoped оценка E40: camera-only softmax и фиксированная stratum-policy измерены на уже видимом инженерном контракте RAVNOVES00. Это не operational camera→LiDAR pipeline и не blind validation."
status={gateLabel}
statusTone={result.qualityGatePassed ? "success" : "warning"}
facts={[
@@ -53,7 +53,7 @@ export function E40Result({
},
{
label: "Validation",
value: `${formatNumber(metrics.validationItems, 0)} · sealed`,
value: `${formatNumber(metrics.validationItems, 0)} · historical visible`,
},
{
label: "Исполнение",
@@ -61,10 +61,10 @@ export function E40Result({
},
]}
brief={{
question: "Удерживает ли perception выбранного сенсорного рига не менее 90% по presence, geometry association и freshness после удаления route-coordinate leakage и группировки зависимых кадров?",
approach: "Для 340 development-кейсов зафиксированы 125 признаков без frame index, session time, review ordinal, track ID и абсолютных map-координат. Консервативная stratum-policy и camera-only softmax прошли два групповых пятифолдовых протокола; validation labels при выборе и обучении не использовались.",
principalResult: `Development: contiguous presence ${percent(contiguous.presence.accuracy)}, grouped presence ${percent(grouped.presence.accuracy)}. Sealed validation: presence ${percent(dimensions.presence.accuracy)}, geometry ${percent(dimensions.geometryAssociation.accuracy)}, freshness ${percent(dimensions.freshness.accuracy)}.`,
limitation: "Это source-scoped доказательство только для RAVNOVES00 на инженерно проверенной разметке. Оно не доказывает перенос на другой маршрут, растительность, камеру, риг или живой ровер и не выдаёт навигационных либо safety-полномочий.",
question: "Как camera-only softmax и фиксированная политика страт согласуются с уже видимым инженерным контрактом RAVNOVES00?",
approach: "Для 340 development-кейсов зафиксированы 125 признаков без frame index, session time, review ordinal, track ID и абсолютных map-координат. Только camera-only использует softmax; agree, conflict, geometry-only и unknown получают детерминированные policy-state, а не вероятность operational detector.",
principalResult: `Development: contiguous presence ${percent(contiguous.presence.accuracy)}, grouped presence ${percent(grouped.presence.accuracy)}. Historical visible evaluation: presence ${percent(dimensions.presence.accuracy)}, geometry ${percent(dimensions.geometryAssociation.accuracy)}, freshness ${percent(dimensions.freshness.accuracy)}.`,
limitation: "E40 — историческая source-scoped инженерная оценка поверх E29/E30, а не сама camera-first perception. Она не является blind validation и не доказывает независимую точность, перенос, навигацию или safety.",
}}
method={{
completeness: "complete",
@@ -74,7 +74,7 @@ export function E40Result({
{
kind: "source",
name: "E37 frozen acceptance contract",
version: `${formatNumber(metrics.developmentItems, 0)} development + ${formatNumber(metrics.validationItems, 0)} sealed validation`,
version: `${formatNumber(metrics.developmentItems, 0)} development + ${formatNumber(metrics.validationItems, 0)} historical visible evaluation`,
role: "неизменяемый denominator и reference-метрики трёх измерений",
identitySha256: null,
},
@@ -96,7 +96,7 @@ export function E40Result({
kind: "runtime",
name: "Worker 006",
version: result.workerNode,
role: "одно sealed evaluation в package-bound контейнере без командных полномочий",
role: "одно historical visible evaluation в package-bound контейнере без командных полномочий",
identitySha256: null,
},
],
@@ -105,34 +105,11 @@ export function E40Result({
)}
evidence={(
<LaboratoryEvidence
eyebrow="GROUPED DEVELOPMENT → SEALED VALIDATION"
eyebrow="GROUPED DEVELOPMENT → HISTORICAL VISIBLE EVALUATION"
title="Устойчивость без route-coordinate leakage"
kind="diagnostic-model"
>
<LaboratoryMetricGrid
metrics={[
{
label: "Contiguous time CV",
value: percent(contiguous.presence.accuracy),
hint: `presence · geometry ${percent(contiguous.geometryAssociation.accuracy)}`,
},
{
label: "Whole track / scene CV",
value: percent(grouped.presence.accuracy),
hint: `presence · geometry ${percent(grouped.geometryAssociation.accuracy)}`,
},
{
label: "Sealed presence",
value: percent(dimensions.presence.accuracy),
hint: `${formatNumber(dimensions.presence.correct, 0)} / ${formatNumber(dimensions.presence.total, 0)} · цель 90%`,
},
{
label: "Sealed freshness",
value: percent(dimensions.freshness.accuracy),
hint: `${formatNumber(dimensions.freshness.correct, 0)} / ${formatNumber(dimensions.freshness.total, 0)} · цель 90%`,
},
]}
/>
<E40CaseReview resultId={result.resultId} />
</LaboratoryEvidence>
)}
result={(
@@ -167,7 +144,7 @@ export function E40Result({
conclusion={{
proved: result.qualityGatePassed
? "На неизменяемом RAVNOVES00 все три task-level dimension достигли 90%, полный denominator учтён, ложное свободное пространство не опубликовано и high-severity ошибок нет."
: `Development-профиль устойчив к двум зависимым разбиениям; sealed evaluation завершён с полным учётом ${formatNumber(metrics.validationItems, 0)} кейсов и без ложного свободного пространства.`,
: `Development-профиль устойчив к двум зависимым разбиениям; historical visible evaluation завершён с полным учётом ${formatNumber(metrics.validationItems, 0)} кейсов и без ложного свободного пространства.`,
notProved: "Не доказаны независимая физическая ground truth, второй маршрут, другой риг, растительная среда, живой rover runtime, навигация, команды или safety.",
decision: result.qualityGatePassed
? "Зафиксировать RAVNOVES00 perception gate как закрытый source-scoped этап. Следующий шаг — temporal product state и live-rover replay без расширения полномочий."
@@ -29,7 +29,6 @@ import {
type E30ReviewResult,
} from "../../core/laboratory/e30Review";
import {
fetchAdvancedLaboratoryResults,
type AdvancedLaboratoryResults,
} from "../../core/laboratory/advancedResults";
import {
@@ -51,6 +50,7 @@ import {
e28LaboratoryBrief, e29LaboratoryBrief,
e30LaboratoryBrief, PUBLISHED_LABORATORY_BRIEF,
} from "./laboratoryArchiveBriefs";
import { useAdvancedLaboratoryCatalog } from "./useAdvancedLaboratoryCatalog";
type LaboratoryWorkspaceProps = WorkspaceRendererProps & {
SpatialView: ComponentType<WorkspaceRendererProps>;
};
@@ -63,18 +63,6 @@ type LaboratoryWorkId =
| AdvancedLaboratoryWorkId
| `session:${string}`;
const EMPTY_ADVANCED_RESULTS: AdvancedLaboratoryResults = {
e31: null,
e32: null,
e33: null,
e34: null,
e35: null,
e37: null,
e38: null,
e39: null,
e40: null,
};
function laboratoryWorkOrdinal(value: string): number {
const match = value.match(/\bE(\d+)\b/i);
return match ? Number(match[1]) : -1;
@@ -572,13 +560,11 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
const [e28Model, setE28Model] = useState<LidarLocalSurfaceModel | null>(null);
const [e29Result, setE29Result] = useState<E29EvidenceResult | null>(null);
const [e30Result, setE30Result] = useState<E30ReviewResult | null>(null);
const [advancedResults, setAdvancedResults] = useState(
EMPTY_ADVANCED_RESULTS,
);
const [evidenceLoading, setEvidenceLoading] = useState(true);
const [evidenceError, setEvidenceError] = useState<string | null>(null);
const sessions = useObservationSessions({
limit: 100,
pollingEnabled: false,
replayEnabled: props.sessionArchive.blockedReason === null,
onReplayBegin: props.sessionArchive.onReplayBegin,
onReplayAccepted: props.sessionArchive.onReplayAccepted,
@@ -603,6 +589,18 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
() => new Map(sessions.items.map((session) => [session.id, session])),
[sessions.items],
);
const advanced = useAdvancedLaboratoryCatalog({
selectedWorkId: workId,
onResultLoaded: (nextWorkId, result) => {
const sourceSession = advancedLaboratorySourceSession(
nextWorkId,
result,
sourceSessions,
);
if (sourceSession) void sessions.replay(sourceSession.id);
},
});
const advancedResults: AdvancedLaboratoryResults = advanced.results;
useEffect(() => {
const controller = new AbortController();
@@ -612,22 +610,18 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
fetchLidarLocalSurfaces({ signal: controller.signal }),
fetchE29EvidenceCatalog({ signal: controller.signal }),
fetchE30ReviewCatalog({ signal: controller.signal }),
fetchAdvancedLaboratoryResults({ signal: controller.signal }),
]).then(([e28, e29, e30, advanced]) => {
]).then(([e28, e29, e30]) => {
if (controller.signal.aborted) return;
const nextE28 = e28.status === "fulfilled" ? e28.value.items[0] ?? null : null;
const nextE29 = e29.status === "fulfilled" ? e29.value.items[0] ?? null : null;
const nextE30 = e30.status === "fulfilled" ? e30.value.items[0] ?? null : null;
const nextAdvanced = advanced.status === "fulfilled" ? advanced.value : null;
setE28Model(nextE28);
setE29Result(nextE29);
setE30Result(nextE30);
setAdvancedResults(nextAdvanced ?? EMPTY_ADVANCED_RESULTS);
const failures = [
e28.status === "rejected" ? "E28" : null,
e29.status === "rejected" ? "E29" : null,
e30.status === "rejected" ? "E30" : null,
advanced.status === "rejected" ? "E31E40" : null,
].filter(Boolean);
setEvidenceError(
failures.length
@@ -673,19 +667,15 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
label: "LAB E30 · evidence review A2",
});
}
items.push(...advancedLaboratoryWorkOptions(
advancedResults,
sourceSessions,
));
items.push(...advancedLaboratoryWorkOptions(advanced.index));
return items.sort(
(left, right) => laboratoryWorkOrdinal(right.label) - laboratoryWorkOrdinal(left.label),
);
}, [
advancedResults,
advanced.index,
e28Model,
e29Result,
e30Result,
sourceSessions,
]);
const profiles = useMemo(() => {
const items: LaboratoryOption<LaboratoryProfileId>[] = [];
@@ -726,6 +716,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
useEffect(() => {
if (
evidenceLoading
|| advanced.indexLoading
|| sessions.state === "idle"
|| sessions.state === "loading"
) return;
@@ -761,6 +752,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
}
}, [
evidenceLoading,
advanced.indexLoading,
profileId,
profiles,
publishedWorks,
@@ -813,6 +805,7 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
if (
evidenceLoading
|| advanced.indexLoading
|| sessions.state === "idle"
|| sessions.state === "loading"
) {
@@ -831,7 +824,10 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
<Icon name="database" size={20} />
<strong>Подтверждённых лабораторных работ нет</strong>
<p>
{evidenceError ?? sessions.error ?? "Непроверенные и отсутствующие результаты скрыты."}
{evidenceError
?? advanced.indexError
?? sessions.error
?? "Непроверенные и отсутствующие результаты скрыты."}
</p>
</div>
);
@@ -953,6 +949,18 @@ export function LaboratoryArchiveWorkspace(props: LaboratoryWorkspaceProps) {
result={e30Result}
sourceSession={e30SourceSession}
/>
) : advanced.loadingWorkId === workId ? (
<div className="laboratory-result-pending" role="status">
<span className="busy-indicator" aria-hidden="true" />
<strong>Подготавливаем выбранную лабораторную работу</strong>
<p>Сервер проверяет только её доказательства и связанные артефакты.</p>
</div>
) : advanced.failedWorkId === workId ? (
<div className="laboratory-result-pending">
<Icon name="database" size={20} />
<strong>Работа не прошла ревизию</strong>
<p>{advanced.resultError}</p>
</div>
) : isAdvancedLaboratoryWorkId(workId) ? (
<AdvancedLaboratoryResult
props={props}
@@ -0,0 +1,118 @@
import { useEffect, useRef, useState } from "react";
import {
advancedLaboratoryResultAvailable,
emptyAdvancedLaboratoryResults,
fetchAdvancedLaboratoryIndex,
fetchAdvancedLaboratoryResult,
isAdvancedLaboratoryWorkId,
type AdvancedLaboratoryIndexItem,
type AdvancedLaboratoryWorkId,
} from "../../core/laboratory/advancedIndex";
import type {
AdvancedLaboratoryResults,
} from "../../core/laboratory/advancedResults";
function mergeResults(
current: AdvancedLaboratoryResults,
next: AdvancedLaboratoryResults,
): AdvancedLaboratoryResults {
return {
e31: next.e31 ?? current.e31,
e32: next.e32 ?? current.e32,
e33: next.e33 ?? current.e33,
e34: next.e34 ?? current.e34,
e35: next.e35 ?? current.e35,
e37: next.e37 ?? current.e37,
e38: next.e38 ?? current.e38,
e39: next.e39 ?? current.e39,
e40: next.e40 ?? current.e40,
};
}
function errorMessage(error: unknown): string {
return error instanceof Error && error.message.trim()
? error.message
: "Выбранная лабораторная работа не прошла серверную проверку.";
}
export function useAdvancedLaboratoryCatalog({
selectedWorkId,
onResultLoaded,
}: {
selectedWorkId: string;
onResultLoaded?: (
workId: AdvancedLaboratoryWorkId,
result: AdvancedLaboratoryResults,
) => void | Promise<void>;
}) {
const [index, setIndex] = useState<readonly AdvancedLaboratoryIndexItem[]>([]);
const [indexLoading, setIndexLoading] = useState(true);
const [indexError, setIndexError] = useState<string | null>(null);
const [results, setResults] = useState<AdvancedLaboratoryResults>(
emptyAdvancedLaboratoryResults,
);
const [loadingWorkId, setLoadingWorkId] =
useState<AdvancedLaboratoryWorkId | null>(null);
const [failedWorkId, setFailedWorkId] =
useState<AdvancedLaboratoryWorkId | null>(null);
const [resultError, setResultError] = useState<string | null>(null);
const onResultLoadedRef = useRef(onResultLoaded);
onResultLoadedRef.current = onResultLoaded;
useEffect(() => {
const controller = new AbortController();
setIndexLoading(true);
setIndexError(null);
void fetchAdvancedLaboratoryIndex({ signal: controller.signal })
.then((items) => {
if (!controller.signal.aborted) setIndex(items);
})
.catch((error: unknown) => {
if (!controller.signal.aborted) {
setIndex([]);
setIndexError(errorMessage(error));
}
})
.finally(() => {
if (!controller.signal.aborted) setIndexLoading(false);
});
return () => controller.abort();
}, []);
useEffect(() => {
if (
!isAdvancedLaboratoryWorkId(selectedWorkId)
|| advancedLaboratoryResultAvailable(selectedWorkId, results)
) return;
const controller = new AbortController();
setLoadingWorkId(selectedWorkId);
setFailedWorkId(null);
setResultError(null);
void fetchAdvancedLaboratoryResult(selectedWorkId, {
signal: controller.signal,
}).then(async (next) => {
if (controller.signal.aborted) return;
setResults((current) => mergeResults(current, next));
await onResultLoadedRef.current?.(selectedWorkId, next);
}).catch((error: unknown) => {
if (!controller.signal.aborted) {
setFailedWorkId(selectedWorkId);
setResultError(errorMessage(error));
}
}).finally(() => {
if (!controller.signal.aborted) setLoadingWorkId(null);
});
return () => controller.abort();
}, [results, selectedWorkId]);
return {
index,
indexLoading,
indexError,
results,
loadingWorkId,
failedWorkId,
resultError,
} as const;
}
@@ -5,6 +5,8 @@ import { createServer } from "vite";
let server;
let fetchAdvancedLaboratoryResults;
let fetchAdvancedLaboratoryIndex;
let fetchAdvancedLaboratoryResult;
let AdvancedLaboratoryContractError;
const authority = {
@@ -637,6 +639,10 @@ before(async () => {
fetchAdvancedLaboratoryResults,
AdvancedLaboratoryContractError,
} = await server.ssrLoadModule("/src/core/laboratory/advancedResults.ts"));
({
fetchAdvancedLaboratoryIndex,
fetchAdvancedLaboratoryResult,
} = await server.ssrLoadModule("/src/core/laboratory/advancedIndex.ts"));
});
after(async () => {
@@ -703,6 +709,53 @@ test("decodes E31E40 from separate read-only catalogs", async () => {
]);
});
test("LAB bootstrap reads a lightweight advanced index", async () => {
const requests = [];
const decoded = await fetchAdvancedLaboratoryIndex({
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
return new Response(JSON.stringify({
schema_version: "missioncore.laboratory-advanced-index/v1",
items: [
{
work_id: "e40-perception-product-gate",
result_id: `e40-perception-product-gate-${"a".repeat(64)}`,
created_at_utc: "2026-07-28T11:00:00Z",
access: "read-only",
},
],
access: "read-only",
}), { status: 200 });
},
});
assert.deepEqual(decoded.map((item) => item.workId), [
"e40-perception-product-gate",
]);
assert.deepEqual(requests, [
{ input: "/api/v1/laboratory/advanced-index", method: "GET" },
]);
});
test("selected advanced LAB fetches only its own strict catalog", async () => {
const requests = [];
const decoded = await fetchAdvancedLaboratoryResult(
"e40-perception-product-gate",
{
fetcher: async (input, init) => {
requests.push({ input: String(input), method: init?.method });
return new Response(JSON.stringify(catalog(e40())), { status: 200 });
},
},
);
assert.equal(decoded.e40.qualityGatePassed, true);
assert.equal(decoded.e31, null);
assert.deepEqual(requests, [
{ input: "/api/v1/laboratory/e40/results?limit=1", method: "GET" },
]);
});
test("keeps valid LAB catalogs available when one transport endpoint fails", async () => {
const decoded = await fetchAdvancedLaboratoryResults({
fetcher: async (input) => {
@@ -73,6 +73,8 @@ function item(overrides = {}) {
materialization: {
frame_point_count: 1813,
projected_point_count: 3,
projection_width: 800,
projection_height: 600,
candidate_point_count: 2,
selected_point_count: 1,
rejected_candidate_point_count: 1,
@@ -0,0 +1,174 @@
import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let parseE40CaseCatalog;
let parseE40OperatorReview;
let fetchE40CaseCatalog;
let saveE40OperatorVerdict;
let E40CaseReviewContractError;
const resultId = `e40-perception-product-gate-${"a".repeat(64)}`;
const materializationId = `e30-materialization-${"b".repeat(64)}`;
const itemId = `e30-review-item-${"c".repeat(64)}`;
function payload(overrides = {}) {
return {
schema_version: "missioncore.laboratory-e40-case-catalog/v1",
result_id: resultId,
materialization_id: materializationId,
items: [{
item_id: itemId,
sequence: 3,
source_frame_index: 1080,
source_stratum: "geometry-only",
severity: "high",
presence_confidence: 1,
prediction_basis: "fixed-stratum-policy",
reference: {
presence: "object-present",
geometry_association: "object-associated",
freshness: "current",
},
prediction: {
presence: "occupied-environment",
geometry_association: "independent-occupied",
freshness: "current",
},
mismatched_dimensions: ["presence", "geometry_association"],
access: "read-only",
}],
total: 23,
truncated: false,
access: "read-only",
...overrides,
};
}
function reviewPayload(overrides = {}) {
return {
schema_version: "missioncore.e40-operator-review/v1",
review_id: `e40-operator-review-${"f".repeat(64)}`,
protocol: "sealed-error-adjudication/v1",
source: {
result_id: resultId,
materialization_id: materializationId,
case_catalog_sha256: "1".repeat(64),
item_count: 1,
item_set_sha256: "2".repeat(64),
},
reviewer_id: "DC",
revision: 1,
decisions: [{
item_id: itemId,
verdict: "confirmed-error",
revision: 1,
idempotency_key: "ui-test",
decided_at_utc: "2026-07-30T17:00:00.000Z",
event_id: `e40-operator-verdict-${"3".repeat(64)}`,
}],
reviewed_item_count: 1,
remaining_item_count: 0,
updated_at_utc: "2026-07-30T17:00:00.000Z",
access: "review-write",
...overrides,
};
}
before(async () => {
server = await createServer({
configFile: false,
appType: "custom",
optimizeDeps: { noDiscovery: true },
server: { middlewareMode: true, hmr: false },
});
({
parseE40CaseCatalog,
parseE40OperatorReview,
fetchE40CaseCatalog,
saveE40OperatorVerdict,
E40CaseReviewContractError,
} = await server.ssrLoadModule("/src/core/laboratory/e40CaseReview.ts"));
});
test("E40 operator review preserves the sealed source and verdict", () => {
const review = parseE40OperatorReview(reviewPayload());
assert.equal(review.resultId, resultId);
assert.equal(review.materializationId, materializationId);
assert.equal(review.caseCatalogSha256, "1".repeat(64));
assert.equal(review.reviewedItemCount, 1);
assert.equal(review.decisions[0].verdict, "confirmed-error");
});
after(async () => {
await server.close();
});
test("E40 case catalog preserves the sealed error binding", () => {
const catalog = parseE40CaseCatalog(payload());
assert.equal(catalog.resultId, resultId);
assert.equal(catalog.materializationId, materializationId);
assert.equal(catalog.items[0].sourceFrameIndex, 1080);
assert.equal(catalog.items[0].predictionBasis, "fixed-stratum-policy");
assert.deepEqual(
catalog.items[0].mismatchedDimensions,
["presence", "geometry_association"],
);
assert.equal(
catalog.items[0].prediction.geometryAssociation,
"independent-occupied",
);
});
test("E40 case fetch is bounded and rejects a foreign result", async () => {
let requestUrl = "";
const fetcher = async (input) => {
requestUrl = String(input);
return new Response(JSON.stringify(payload({
result_id: `e40-perception-product-gate-${"d".repeat(64)}`,
})), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
await assert.rejects(
fetchE40CaseCatalog(resultId, { fetcher }),
E40CaseReviewContractError,
);
assert.match(requestUrl, /cases\?limit=48$/);
});
test("E40 operator verdict is committed with revision and idempotency", async () => {
let requestUrl = "";
let requestInit;
const fetcher = async (input, init) => {
requestUrl = String(input);
requestInit = init;
return new Response(JSON.stringify(reviewPayload()), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
const review = await saveE40OperatorVerdict(resultId, itemId, {
expectedRevision: 0,
idempotencyKey: "ui-test",
verdict: "confirmed-error",
fetcher,
});
assert.match(requestUrl, /operator-review\/decisions\/e30-review-item-/);
assert.equal(requestInit.method, "PUT");
assert.deepEqual(JSON.parse(requestInit.body), {
reviewer_id: "DC",
expected_revision: 0,
idempotency_key: "ui-test",
verdict: "confirmed-error",
});
assert.equal(review.revision, 1);
});
@@ -42,6 +42,10 @@ const e40ResultUrl = new URL(
"../src/workspaces/laboratory/E40Result.tsx",
import.meta.url,
);
const e40CaseReviewUrl = new URL(
"../src/workspaces/laboratory/E40CaseReview.tsx",
import.meta.url,
);
const e35StylesUrl = new URL(
"../src/styles/e35-degradation-recovery.css",
import.meta.url,
@@ -265,9 +269,10 @@ test("E39 reports refinement and the CV-to-validation gap through the canonical
assert.match(advancedSource, /<E39Result/);
});
test("E40 reports dual grouped qualification and the sealed product gate through the canonical LAB anatomy", async () => {
const [e40Source, advancedSource] = await Promise.all([
test("E40 reports historical visible evaluation with bounded camera-LiDAR case review", async () => {
const [e40Source, caseReviewSource, advancedSource] = await Promise.all([
readFile(e40ResultUrl, "utf8"),
readFile(e40CaseReviewUrl, "utf8"),
readFile(advancedLaboratoryResultUrl, "utf8"),
]);
@@ -275,10 +280,22 @@ test("E40 reports dual grouped qualification and the sealed product gate through
assert.match(e40Source, /<LaboratoryEvidence/);
assert.match(e40Source, /<LaboratoryResultSummary/);
assert.match(e40Source, /frame index, session time, review ordinal, track ID/);
assert.match(e40Source, /validation labels при выборе и обучении не использовались/);
assert.match(e40Source, /Whole track \/ scene CV/);
assert.match(e40Source, /[Ss]ource-scoped perception gate/);
assert.match(e40Source, /Только camera-only использует softmax/);
assert.match(e40Source, /<E40CaseReview resultId=\{result\.resultId\}/);
assert.match(e40Source, /historical visible engineering evaluation/);
assert.match(e40Source, /не подбирая профиль по validation/);
assert.match(caseReviewSource, /<LaboratoryEvidenceViewer/);
assert.match(caseReviewSource, /<E30EvidenceProjection/);
assert.match(caseReviewSource, /<E30EvidencePointCloud/);
assert.match(caseReviewSource, /fetchE30ReviewItemDetail/);
assert.match(caseReviewSource, /fetchE40OperatorReview/);
assert.match(caseReviewSource, /saveE40OperatorVerdict/);
assert.match(caseReviewSource, /Расхождение исторической оценки E40/);
assert.match(caseReviewSource, /label="Предыдущее расхождение"/);
assert.match(caseReviewSource, /label="Следующее расхождение"/);
assert.match(caseReviewSource, /расхождение E40 подтверждается/);
assert.match(caseReviewSource, /предполагаемая ошибка отклоняется/);
assert.match(caseReviewSource, /фиксированная stratum-policy, не вероятность/);
assert.doesNotMatch(
e40Source,
/className="laboratory-(?:summary|result-summary|result-metrics|result-conclusion)"/,
+436
View File
@@ -0,0 +1,436 @@
from __future__ import annotations
import hashlib
import json
import os
import re
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Final, Literal
from k1link.artifacts import utc_now_iso, write_json_atomic
E40_OPERATOR_REVIEW_SCHEMA: Final = "missioncore.e40-operator-review/v1"
E40_OPERATOR_REVIEW_PROTOCOL: Final = "sealed-error-adjudication/v1"
E40OperatorVerdict = Literal["confirmed-error", "rejected-error"]
_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
_MATERIALIZATION_ID = re.compile(r"^e30-materialization-[a-f0-9]{64}$")
_ITEM_ID = re.compile(r"^e30-review-item-[a-f0-9]{64}$")
_REVIEW_ID = re.compile(r"^e40-operator-review-[a-f0-9]{64}$")
_EVENT_ID = re.compile(r"^e40-operator-verdict-[a-f0-9]{64}$")
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
_REVIEWER_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$")
_IDEMPOTENCY_KEY = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$")
_VERDICTS: Final = {"confirmed-error", "rejected-error"}
_MAX_JSON_BYTES: Final = 2 * 1024 * 1024
class E40OperatorReviewError(RuntimeError):
"""Base error for the E40 operator-review lifecycle."""
class E40OperatorReviewConflictError(E40OperatorReviewError):
"""The caller attempted to update a stale review revision."""
class E40OperatorReviewValidationError(E40OperatorReviewError):
"""The requested review operation violates the frozen protocol."""
class E40OperatorReviewIntegrityError(E40OperatorReviewError):
"""The stored review no longer matches its sealed E40 substrate."""
@dataclass(frozen=True)
class E40OperatorReviewSubject:
item_id: str
sequence: int
@dataclass(frozen=True)
class E40OperatorReviewSubstrate:
result_id: str
materialization_id: str
case_catalog_sha256: str
subjects: tuple[E40OperatorReviewSubject, ...]
def __post_init__(self) -> None:
if (
_RESULT_ID.fullmatch(self.result_id) is None
or _MATERIALIZATION_ID.fullmatch(self.materialization_id) is None
or _SHA256.fullmatch(self.case_catalog_sha256) is None
or not self.subjects
or len(self.subjects) > 64
):
raise E40OperatorReviewValidationError(
"E40 operator-review source binding is invalid"
)
if len({subject.item_id for subject in self.subjects}) != len(self.subjects):
raise E40OperatorReviewValidationError(
"E40 operator-review subjects are not unique"
)
if any(
_ITEM_ID.fullmatch(subject.item_id) is None or subject.sequence < 0
for subject in self.subjects
):
raise E40OperatorReviewValidationError(
"E40 operator-review subject is invalid"
)
@property
def item_set_sha256(self) -> str:
return hashlib.sha256(
_canonical_json(
[
{"item_id": subject.item_id, "sequence": subject.sequence}
for subject in self.subjects
]
)
).hexdigest()
def binding(self) -> dict[str, object]:
return {
"result_id": self.result_id,
"materialization_id": self.materialization_id,
"case_catalog_sha256": self.case_catalog_sha256,
"item_count": len(self.subjects),
"item_set_sha256": self.item_set_sha256,
}
def _canonical_json(value: object) -> bytes:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
def _safe_root(root: Path) -> Path:
root.mkdir(parents=True, exist_ok=True)
if root.is_symlink() or not root.is_dir():
raise E40OperatorReviewIntegrityError(
"E40 operator-review root is invalid"
)
return root.resolve()
def _reviewer_id(value: str) -> str:
value = value.strip()
if _REVIEWER_ID.fullmatch(value) is None:
raise E40OperatorReviewValidationError("reviewer_id is invalid")
return value
def _review_id(
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
) -> str:
identity = {
"protocol": E40_OPERATOR_REVIEW_PROTOCOL,
"source": substrate.binding(),
"reviewer_id": reviewer_id,
}
return f"e40-operator-review-{hashlib.sha256(_canonical_json(identity)).hexdigest()}"
@contextmanager
def _exclusive_lock(path: Path) -> Iterator[None]:
path.parent.mkdir(parents=True, exist_ok=True)
try:
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError as exc:
raise E40OperatorReviewConflictError(
"E40 operator review is being updated"
) from exc
try:
yield
finally:
os.close(descriptor)
path.unlink(missing_ok=True)
class E40OperatorReviewStore:
"""Mutable operator verdicts kept separate from immutable E40 evidence."""
def __init__(self, *, root: Path) -> None:
self.root = _safe_root(root)
def get(
self,
*,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
) -> dict[str, object]:
reviewer_id = _reviewer_id(reviewer_id)
review_id = _review_id(substrate, reviewer_id)
path = self.root / f"{review_id}.json"
if path.is_symlink():
raise E40OperatorReviewIntegrityError(
"E40 operator review must not be a symlink"
)
if not path.exists():
return self._empty(
review_id=review_id,
substrate=substrate,
reviewer_id=reviewer_id,
)
return self._read(
path=path,
review_id=review_id,
substrate=substrate,
reviewer_id=reviewer_id,
)
def record_verdict(
self,
*,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
item_id: str,
expected_revision: int,
idempotency_key: str,
verdict: E40OperatorVerdict,
) -> dict[str, object]:
reviewer_id = _reviewer_id(reviewer_id)
if (
_ITEM_ID.fullmatch(item_id) is None
or item_id not in {subject.item_id for subject in substrate.subjects}
):
raise E40OperatorReviewValidationError(
"item_id is outside the E40 error catalog"
)
if (
_IDEMPOTENCY_KEY.fullmatch(idempotency_key) is None
or verdict not in _VERDICTS
):
raise E40OperatorReviewValidationError(
"E40 operator verdict request is invalid"
)
if expected_revision < 0:
raise E40OperatorReviewValidationError(
"expected_revision is invalid"
)
review_id = _review_id(substrate, reviewer_id)
path = self.root / f"{review_id}.json"
lock_path = self.root / ".locks" / f"{review_id}.lock"
with _exclusive_lock(lock_path):
current = self.get(
substrate=substrate,
reviewer_id=reviewer_id,
)
decisions = list(current["decisions"]) # type: ignore[arg-type]
replay = next(
(
decision
for decision in decisions
if decision["idempotency_key"] == idempotency_key
),
None,
)
if replay is not None:
if (
replay["item_id"] != item_id
or replay["verdict"] != verdict
):
raise E40OperatorReviewConflictError(
"idempotency key was already used for another verdict"
)
return current
if current["revision"] != expected_revision:
raise E40OperatorReviewConflictError(
"E40 operator-review revision changed"
)
next_revision = expected_revision + 1
decided_at = utc_now_iso()
event_identity = {
"review_id": review_id,
"item_id": item_id,
"verdict": verdict,
"revision": next_revision,
"idempotency_key": idempotency_key,
"decided_at_utc": decided_at,
}
decision = {
"item_id": item_id,
"verdict": verdict,
"revision": next_revision,
"idempotency_key": idempotency_key,
"decided_at_utc": decided_at,
"event_id": (
"e40-operator-verdict-"
f"{hashlib.sha256(_canonical_json(event_identity)).hexdigest()}"
),
}
decisions = [
previous
for previous in decisions
if previous["item_id"] != item_id
]
decisions.append(decision)
subject_order = {
subject.item_id: index
for index, subject in enumerate(substrate.subjects)
}
decisions.sort(key=lambda value: subject_order[str(value["item_id"])])
payload = self._payload(
review_id=review_id,
substrate=substrate,
reviewer_id=reviewer_id,
revision=next_revision,
decisions=decisions,
updated_at_utc=decided_at,
)
write_json_atomic(path, payload)
path.chmod(0o600)
return payload
def _empty(
self,
*,
review_id: str,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
) -> dict[str, object]:
return self._payload(
review_id=review_id,
substrate=substrate,
reviewer_id=reviewer_id,
revision=0,
decisions=[],
updated_at_utc=None,
)
def _payload(
self,
*,
review_id: str,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
revision: int,
decisions: list[dict[str, object]],
updated_at_utc: str | None,
) -> dict[str, object]:
return {
"schema_version": E40_OPERATOR_REVIEW_SCHEMA,
"review_id": review_id,
"protocol": E40_OPERATOR_REVIEW_PROTOCOL,
"source": substrate.binding(),
"reviewer_id": reviewer_id,
"revision": revision,
"decisions": decisions,
"reviewed_item_count": len(decisions),
"remaining_item_count": len(substrate.subjects) - len(decisions),
"updated_at_utc": updated_at_utc,
"access": "review-write",
}
def _read(
self,
*,
path: Path,
review_id: str,
substrate: E40OperatorReviewSubstrate,
reviewer_id: str,
) -> dict[str, object]:
if (
path.is_symlink()
or not path.is_file()
or not 0 < path.stat().st_size <= _MAX_JSON_BYTES
):
raise E40OperatorReviewIntegrityError(
"E40 operator review is unavailable"
)
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise E40OperatorReviewIntegrityError(
"E40 operator review is invalid"
) from exc
if not isinstance(value, dict):
raise E40OperatorReviewIntegrityError(
"E40 operator review must be an object"
)
decisions = value.get("decisions")
revision = value.get("revision")
if (
value.get("schema_version") != E40_OPERATOR_REVIEW_SCHEMA
or value.get("review_id") != review_id
or value.get("protocol") != E40_OPERATOR_REVIEW_PROTOCOL
or value.get("source") != substrate.binding()
or value.get("reviewer_id") != reviewer_id
or value.get("access") != "review-write"
or isinstance(revision, bool)
or not isinstance(revision, int)
or revision < 0
or not isinstance(decisions, list)
or value.get("reviewed_item_count") != len(decisions)
or value.get("remaining_item_count")
!= len(substrate.subjects) - len(decisions)
):
raise E40OperatorReviewIntegrityError(
"E40 operator review identity is invalid"
)
known_items = {subject.item_id for subject in substrate.subjects}
item_ids: set[str] = set()
idempotency_keys: set[str] = set()
for decision in decisions:
decided_at = decision.get("decided_at_utc") if isinstance(
decision, dict
) else None
event_identity = {
"review_id": review_id,
"item_id": decision.get("item_id") if isinstance(
decision, dict
) else None,
"verdict": decision.get("verdict") if isinstance(
decision, dict
) else None,
"revision": decision.get("revision") if isinstance(
decision, dict
) else None,
"idempotency_key": decision.get("idempotency_key") if isinstance(
decision, dict
) else None,
"decided_at_utc": decided_at,
}
expected_event_id = (
"e40-operator-verdict-"
f"{hashlib.sha256(_canonical_json(event_identity)).hexdigest()}"
)
if (
not isinstance(decision, dict)
or decision.get("item_id") not in known_items
or decision.get("item_id") in item_ids
or decision.get("verdict") not in _VERDICTS
or not isinstance(decision.get("revision"), int)
or not 1 <= int(decision["revision"]) <= revision
or not isinstance(decision.get("decided_at_utc"), str)
or _EVENT_ID.fullmatch(str(decision.get("event_id"))) is None
or decision.get("event_id") != expected_event_id
or _IDEMPOTENCY_KEY.fullmatch(
str(decision.get("idempotency_key"))
)
is None
or decision.get("idempotency_key") in idempotency_keys
):
raise E40OperatorReviewIntegrityError(
"E40 operator verdict is invalid"
)
item_ids.add(str(decision["item_id"]))
idempotency_keys.add(str(decision["idempotency_key"]))
if decisions and max(
int(decision["revision"]) for decision in decisions
) != revision:
raise E40OperatorReviewIntegrityError(
"E40 operator-review revision is invalid"
)
return value
+159
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import copy
import json
import re
from collections.abc import Callable
from functools import lru_cache
@@ -58,6 +59,10 @@ from k1link.compute.e40_perception_product_gate import (
LABORATORY_ADVANCED_CATALOG_SCHEMA: Final = (
"missioncore.laboratory-advanced-catalog/v1"
)
LABORATORY_ADVANCED_INDEX_SCHEMA: Final = (
"missioncore.laboratory-advanced-index/v1"
)
_INDEX_DOCUMENT_MAX_BYTES: Final = 64 * 1024
_E31_RESULT_ID = re.compile(r"^e31-source-qualification-[a-f0-9]{64}$")
_E32_RESULT_ID = re.compile(r"^e32-track-geometry-[a-f0-9]{64}$")
@@ -71,6 +76,14 @@ _E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
RootProvider = Callable[[], Path | None]
_AdvancedIndexSpec = tuple[
str,
RootProvider,
re.Pattern[str],
str,
str,
]
def _result_signature(root: Path) -> tuple[int, ...]:
signature: list[int] = []
@@ -193,6 +206,82 @@ def _candidates(root: Path, pattern: re.Pattern[str]) -> list[Path]:
)
def _advanced_index_item(
candidate: Path,
*,
work_id: str,
document_name: str,
schema_version: str,
) -> dict[str, object]:
document_path = candidate / document_name
if document_path.is_symlink() or not document_path.is_file():
raise ValueError("advanced LAB index document is missing")
if document_path.stat().st_size > _INDEX_DOCUMENT_MAX_BYTES:
raise ValueError("advanced LAB index document is too large")
payload = json.loads(document_path.read_text(encoding="utf-8"))
document = _object(payload, "advanced LAB index document")
if document.get("schema_version") != schema_version:
raise ValueError("advanced LAB index schema is invalid")
if document.get("result_id") != candidate.name:
raise ValueError("advanced LAB index result identity is invalid")
identity_sha256 = document.get("identity_sha256")
if (
not isinstance(identity_sha256, str)
or re.fullmatch(r"[a-f0-9]{64}", identity_sha256) is None
or not candidate.name.endswith(identity_sha256)
):
raise ValueError("advanced LAB index digest is invalid")
identity = _object(document.get("identity"), "advanced LAB identity")
authority = _object(
identity.get("authority"),
"advanced LAB authority",
)
if (
authority.get("commands_enabled") is not False
or authority.get("navigation_or_safety_accepted") is not False
):
raise ValueError("advanced LAB authority is invalid")
if document.get("ground_truth") not in (None, False):
raise ValueError("advanced LAB ground-truth claim is invalid")
created_at_utc = document.get("created_at_utc")
if not isinstance(created_at_utc, str) or not created_at_utc.strip():
raise ValueError("advanced LAB creation time is invalid")
return {
"work_id": work_id,
"result_id": candidate.name,
"created_at_utc": created_at_utc,
"access": "read-only",
}
def _advanced_index(
specs: tuple[_AdvancedIndexSpec, ...],
) -> dict[str, object]:
items: list[dict[str, object]] = []
for work_id, provider, pattern, document_name, schema_version in specs:
root = _configured_root(provider)
if root is None:
continue
for candidate in _candidates(root, pattern):
try:
items.append(
_advanced_index_item(
candidate,
work_id=work_id,
document_name=document_name,
schema_version=schema_version,
)
)
break
except (json.JSONDecodeError, OSError, TypeError, ValueError):
continue
return {
"schema_version": LABORATORY_ADVANCED_INDEX_SCHEMA,
"items": items,
"access": "read-only",
}
def _object(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ValueError(f"{label} is invalid")
@@ -739,6 +828,76 @@ def build_advanced_laboratory_router(
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
@router.get("/advanced-index")
def list_advanced_results() -> dict[str, object]:
return _advanced_index(
(
(
"e31-source-binding",
e31_root_provider,
_E31_RESULT_ID,
"manifest.json",
"missioncore.e31-source-qualification/v1",
),
(
"e32-track-geometry",
e32_root_provider,
_E32_RESULT_ID,
"manifest.json",
"missioncore.e32-track-geometry-replay/v1",
),
(
"e33-worker-shadow",
e33_root_provider,
_E33_RESULT_ID,
"result.json",
"missioncore.e33-worker-shadow-result/v1",
),
(
"e34-temporal-layer",
e34_root_provider,
_E34_RESULT_ID,
"manifest.json",
"missioncore.e34-temporal-occupied-result/v1",
),
(
"e35-degradation-recovery",
e35_root_provider,
_E35_RESULT_ID,
"manifest.json",
"missioncore.e35-degradation-result/v1",
),
(
"e37-ravnoves-acceptance",
e37_root_provider,
_E37_RESULT_ID,
"manifest.json",
"missioncore.e37-acceptance-contract/v1",
),
(
"e38-perception-baseline",
e38_root_provider,
_E38_RESULT_ID,
"manifest.json",
"missioncore.e38-perception-baseline/v1",
),
(
"e39-perception-refinement",
e39_root_provider,
_E39_RESULT_ID,
"manifest.json",
"missioncore.e39-perception-refinement/v1",
),
(
"e40-perception-product-gate",
e40_root_provider,
_E40_RESULT_ID,
"manifest.json",
"missioncore.e40-perception-product-gate/v1",
),
)
)
@router.get("/e31/results")
def list_e31_results(
limit: int = Query(default=1, ge=1, le=10),
+19
View File
@@ -40,6 +40,7 @@ from k1link.web.device_plugin_composition import load_installed_device_plugins
from k1link.web.e30_engineering_api import build_e30_engineering_router
from k1link.web.e30_human_review_api import build_e30_human_review_router
from k1link.web.e30_review_api import build_e30_review_router
from k1link.web.e40_case_review_api import build_e40_case_review_router
from k1link.web.environment_api import build_environment_router
from k1link.web.laboratory_api import build_laboratory_router
from k1link.web.lidar_api import build_lidar_router
@@ -614,6 +615,24 @@ app.include_router(
),
)
)
app.include_router(
build_e40_case_review_router(
e40_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e40"
/ "results"
),
operator_review_root_provider=lambda: (
REPOSITORY_ROOT
/ ".runtime"
/ "compute-experiments"
/ "e40"
/ "operator-reviews"
),
)
)
app.include_router(
build_e30_engineering_router(
generation_root_provider=lambda: (
+479
View File
@@ -0,0 +1,479 @@
from __future__ import annotations
import hashlib
import json
import re
from collections.abc import Callable
from functools import lru_cache
from pathlib import Path
from typing import Annotated, Any, Final, Literal
from fastapi import APIRouter, HTTPException, Query
from fastapi import Path as ApiPath
from pydantic import BaseModel, ConfigDict, Field
from k1link.compute.e40_operator_review import (
E40OperatorReviewConflictError,
E40OperatorReviewIntegrityError,
E40OperatorReviewStore,
E40OperatorReviewSubject,
E40OperatorReviewSubstrate,
E40OperatorReviewValidationError,
)
from k1link.compute.e40_perception_product_gate import (
E40_PREDICTION_SCHEMA,
E40_PREDICTIONS_NAME,
E40PerceptionProductGate,
E40PerceptionProductGateError,
read_e40_perception_product_gate,
)
LABORATORY_E40_CASE_CATALOG_SCHEMA: Final = (
"missioncore.laboratory-e40-case-catalog/v1"
)
_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
_MATERIALIZATION_ID = re.compile(r"^e30-materialization-[a-f0-9]{64}$")
_ITEM_ID = re.compile(r"^e30-review-item-[a-f0-9]{64}$")
_MAX_PREDICTIONS_BYTES: Final = 8 * 1024 * 1024
_AUTHORITY: Final = {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
_DIMENSIONS: Final = (
"presence",
"geometry_association",
"freshness",
)
_STRATA: Final = {
"agree",
"camera-only",
"conflict",
"geometry-only",
"unknown",
}
_SEVERITIES: Final = {"high", "medium", "standard"}
_SPLITS: Final = {"development", "validation"}
_VALUES: Final = {
"presence": {
"background-or-noise",
"object-present",
"occupied-environment",
},
"geometry_association": {
"independent-occupied",
"insufficient-support",
"object-associated",
"rejected-nonobject",
"unknown",
},
"freshness": {"current", "stale", "unavailable"},
}
_SEVERITY_ORDER: Final = {"high": 0, "medium": 1, "standard": 2}
RootProvider = Callable[[], Path | None]
class E40OperatorVerdictRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
reviewer_id: str = Field(min_length=1, max_length=128)
expected_revision: int = Field(ge=0)
idempotency_key: str = Field(min_length=1, max_length=128)
verdict: Literal["confirmed-error", "rejected-error"]
def _result_signature(root: Path) -> tuple[int, ...]:
signature: list[int] = []
for path in sorted(root.iterdir(), key=lambda item: item.name):
if not path.is_file() or path.is_symlink():
continue
stat = path.stat()
signature.extend((stat.st_size, stat.st_mtime_ns))
return tuple(signature)
@lru_cache(maxsize=8)
def _read_result_cached(
root_text: str,
signature: tuple[int, ...],
) -> E40PerceptionProductGate:
del signature
return read_e40_perception_product_gate(Path(root_text))
def _configured_root(provider: RootProvider) -> Path | None:
root = provider()
if root is None:
return None
resolved = root.resolve()
return resolved if resolved.is_dir() else None
def _candidate(root: Path, result_id: str) -> Path:
if _RESULT_ID.fullmatch(result_id) is None:
raise HTTPException(status_code=404, detail="E40 result не найден")
candidate = root / result_id
if candidate.is_symlink() or not candidate.is_dir():
raise HTTPException(status_code=404, detail="E40 result не найден")
return candidate
def _object(value: object, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise E40PerceptionProductGateError(f"{label} must be an object")
return value
def _nonnegative_integer(value: object, label: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise E40PerceptionProductGateError(
f"{label} must be a nonnegative integer"
)
return value
def _bounded_number(value: object, label: str) -> float:
if (
isinstance(value, bool)
or not isinstance(value, (int, float))
or not 0 <= float(value) <= 1
):
raise E40PerceptionProductGateError(f"{label} must be within [0, 1]")
return float(value)
def _dimension_state(value: object, label: str) -> dict[str, str]:
source = _object(value, label)
if set(source) != set(_DIMENSIONS):
raise E40PerceptionProductGateError(
f"{label} has incompatible dimensions"
)
state: dict[str, str] = {}
for dimension in _DIMENSIONS:
item = source.get(dimension)
if not isinstance(item, str) or item not in _VALUES[dimension]:
raise E40PerceptionProductGateError(
f"{label}.{dimension} is invalid"
)
state[dimension] = item
return state
def _read_predictions(result: E40PerceptionProductGate) -> tuple[dict[str, Any], ...]:
path = result.result_root / E40_PREDICTIONS_NAME
if (
not path.is_file()
or path.is_symlink()
or path.stat().st_size > _MAX_PREDICTIONS_BYTES
):
raise E40PerceptionProductGateError(
"E40 sealed predictions are unavailable"
)
rows: list[dict[str, Any]] = []
sequences: set[int] = set()
item_ids: set[str] = set()
try:
with path.open("r", encoding="utf-8-sig") as stream:
for line in stream:
row = _object(json.loads(line), E40_PREDICTIONS_NAME)
sequence = _nonnegative_integer(
row.get("sequence"),
"E40 prediction.sequence",
)
item_id = row.get("item_id")
review_key = row.get("review_key")
source_frame_index = _nonnegative_integer(
row.get("source_frame_index"),
"E40 prediction.source_frame_index",
)
stratum = row.get("source_stratum")
severity = row.get("severity")
split = row.get("split")
if (
row.get("schema_version") != E40_PREDICTION_SCHEMA
or not isinstance(item_id, str)
or _ITEM_ID.fullmatch(item_id) is None
or not isinstance(review_key, str)
or not review_key
or stratum not in _STRATA
or severity not in _SEVERITIES
or split not in _SPLITS
or row.get("scored") is not (split == "validation")
or row.get("authority") != _AUTHORITY
or sequence in sequences
or item_id in item_ids
):
raise E40PerceptionProductGateError(
"E40 prediction identity is invalid"
)
prediction = _dimension_state(
row.get("prediction"),
"E40 prediction.prediction",
)
reference = _dimension_state(
row.get("reference"),
"E40 prediction.reference",
)
rows.append(
{
"sequence": sequence,
"item_id": item_id,
"review_key": review_key,
"source_frame_index": source_frame_index,
"source_stratum": stratum,
"severity": severity,
"split": split,
"prediction": prediction,
"reference": reference,
"presence_confidence": _bounded_number(
row.get("presence_confidence"),
"E40 prediction.presence_confidence",
),
}
)
sequences.add(sequence)
item_ids.add(item_id)
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise E40PerceptionProductGateError(
"E40 sealed predictions are invalid"
) from exc
validation_total = _object(
result.report.get("metrics"),
"E40 metrics",
).get("validation_items")
if (
not rows
or _nonnegative_integer(validation_total, "E40 validation_items")
!= sum(row["split"] == "validation" for row in rows)
):
raise E40PerceptionProductGateError(
"E40 sealed validation denominator changed"
)
return tuple(rows)
def _materialization_id(result: E40PerceptionProductGate) -> str:
identity = _object(result.manifest.get("identity"), "E40 identity")
source = _object(identity.get("source"), "E40 source")
value = source.get("materialization_id")
if not isinstance(value, str) or _MATERIALIZATION_ID.fullmatch(value) is None:
raise E40PerceptionProductGateError(
"E40 materialization binding is invalid"
)
return value
def _case(row: dict[str, Any]) -> dict[str, object]:
reference = _object(row["reference"], "E40 reference")
prediction = _object(row["prediction"], "E40 prediction")
source_stratum = str(row["source_stratum"])
mismatched = [
dimension
for dimension in _DIMENSIONS
if reference[dimension] != prediction[dimension]
]
return {
"item_id": row["item_id"],
"sequence": row["sequence"],
"source_frame_index": row["source_frame_index"],
"source_stratum": source_stratum,
"severity": row["severity"],
"presence_confidence": row["presence_confidence"],
"prediction_basis": (
"camera-only-softmax"
if source_stratum == "camera-only"
else "fixed-stratum-policy"
),
"reference": dict(reference),
"prediction": dict(prediction),
"mismatched_dimensions": mismatched,
"access": "read-only",
}
def _load_catalog(
root_provider: RootProvider,
result_id: str,
) -> tuple[dict[str, object], E40OperatorReviewSubstrate]:
root = _configured_root(root_provider)
if root is None:
raise HTTPException(status_code=404, detail="E40 result не найден")
candidate = _candidate(root, result_id)
result = _read_result_cached(
str(candidate.resolve()),
_result_signature(candidate),
)
rows = _read_predictions(result)
errors = [
row
for row in rows
if row["split"] == "validation"
and any(
row["reference"][dimension] != row["prediction"][dimension]
for dimension in _DIMENSIONS
)
]
errors.sort(
key=lambda row: (
_SEVERITY_ORDER[str(row["severity"])],
int(row["sequence"]),
)
)
if not errors or len(errors) > 64:
raise E40PerceptionProductGateError(
"E40 error catalog is outside the review boundary"
)
materialization_id = _materialization_id(result)
cases = [_case(row) for row in errors]
case_catalog_sha256 = hashlib.sha256(
json.dumps(
{
"result_id": result.result_id,
"materialization_id": materialization_id,
"items": cases,
},
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
).hexdigest()
catalog: dict[str, object] = {
"schema_version": LABORATORY_E40_CASE_CATALOG_SCHEMA,
"result_id": result.result_id,
"materialization_id": materialization_id,
"items": cases,
"total": len(errors),
"truncated": False,
"access": "read-only",
}
substrate = E40OperatorReviewSubstrate(
result_id=result.result_id,
materialization_id=materialization_id,
case_catalog_sha256=case_catalog_sha256,
subjects=tuple(
E40OperatorReviewSubject(
item_id=str(row["item_id"]),
sequence=int(row["sequence"]),
)
for row in errors
),
)
return catalog, substrate
def build_e40_case_review_router(
*,
e40_root_provider: RootProvider = lambda: None,
operator_review_root_provider: RootProvider = lambda: None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory/e40", tags=["laboratory"])
def catalog_and_source(
result_id: str,
) -> tuple[dict[str, object], E40OperatorReviewSubstrate]:
try:
return _load_catalog(e40_root_provider, result_id)
except HTTPException:
raise
except (
E40PerceptionProductGateError,
E40OperatorReviewValidationError,
KeyError,
OSError,
TypeError,
ValueError,
) as exc:
raise HTTPException(
status_code=409,
detail="E40 case-review не прошёл проверку целостности",
) from exc
def store() -> E40OperatorReviewStore:
root = operator_review_root_provider()
if root is None:
raise HTTPException(
status_code=503,
detail="E40 operator review не настроен",
)
try:
return E40OperatorReviewStore(root=root)
except (E40OperatorReviewIntegrityError, OSError) as exc:
raise HTTPException(
status_code=503,
detail="E40 operator review storage недоступен",
) from exc
def invoke(
operation: Callable[[], dict[str, object]],
) -> dict[str, object]:
try:
return operation()
except E40OperatorReviewValidationError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
except E40OperatorReviewConflictError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
except (E40OperatorReviewIntegrityError, OSError) as exc:
raise HTTPException(
status_code=409,
detail="E40 operator review не прошёл проверку целостности",
) from exc
@router.get("/results/{result_id}/cases")
def list_e40_error_cases(
result_id: str,
limit: int = Query(default=48, ge=1, le=64),
) -> dict[str, object]:
catalog, _ = catalog_and_source(result_id)
items = list(catalog["items"]) # type: ignore[arg-type]
return {
**catalog,
"items": items[:limit],
"truncated": len(items) > limit,
}
@router.get("/results/{result_id}/operator-review")
def get_operator_review(
result_id: str,
reviewer_id: str = Query(
default="DC",
min_length=1,
max_length=128,
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:@-]{0,127}$",
),
) -> dict[str, object]:
_, substrate = catalog_and_source(result_id)
review_store = store()
return invoke(
lambda: review_store.get(
substrate=substrate,
reviewer_id=reviewer_id,
)
)
@router.put(
"/results/{result_id}/operator-review/decisions/{item_id}"
)
def record_operator_verdict(
result_id: str,
item_id: Annotated[
str,
ApiPath(pattern=r"^e30-review-item-[a-f0-9]{64}$"),
],
request: E40OperatorVerdictRequest,
) -> dict[str, object]:
_, substrate = catalog_and_source(result_id)
review_store = store()
return invoke(
lambda: review_store.record_verdict(
substrate=substrate,
reviewer_id=request.reviewer_id,
item_id=item_id,
expected_revision=request.expected_revision,
idempotency_key=request.idempotency_key,
verdict=request.verdict,
)
)
return router
+88
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
@@ -41,6 +42,93 @@ def test_advanced_catalogs_are_empty_when_not_configured() -> None:
}
def test_advanced_index_is_empty_when_not_configured() -> None:
router = build_advanced_laboratory_router()
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
assert route() == { # type: ignore[operator]
"schema_version": "missioncore.laboratory-advanced-index/v1",
"items": [],
"access": "read-only",
}
def test_advanced_index_reads_only_bounded_identity_documents(
tmp_path: Path,
) -> None:
e31 = tmp_path / "e31"
invalid = e31 / f"e31-source-qualification-{'1' * 64}"
valid = e31 / f"e31-source-qualification-{'2' * 64}"
invalid.mkdir(parents=True)
valid.mkdir()
(invalid / "manifest.json").write_text("{}", encoding="utf-8")
(valid / "manifest.json").write_text(
json.dumps(
{
"schema_version": "missioncore.e31-source-qualification/v1",
"result_id": valid.name,
"created_at_utc": "2026-07-27T10:00:00Z",
"identity_sha256": "2" * 64,
"identity": {
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
}
},
}
),
encoding="utf-8",
)
router = build_advanced_laboratory_router(
e31_root_provider=lambda: e31,
)
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
assert route() == { # type: ignore[operator]
"schema_version": "missioncore.laboratory-advanced-index/v1",
"items": [
{
"work_id": "e31-source-binding",
"result_id": valid.name,
"created_at_utc": "2026-07-27T10:00:00Z",
"access": "read-only",
}
],
"access": "read-only",
}
def test_advanced_index_rejects_authority_escalation(
tmp_path: Path,
) -> None:
root = tmp_path / "e40"
candidate = root / f"e40-perception-product-gate-{'a' * 64}"
candidate.mkdir(parents=True)
(candidate / "manifest.json").write_text(
json.dumps(
{
"schema_version": "missioncore.e40-perception-product-gate/v1",
"result_id": candidate.name,
"created_at_utc": "2026-07-28T11:00:00Z",
"identity_sha256": "a" * 64,
"identity": {
"authority": {
"commands_enabled": True,
"navigation_or_safety_accepted": False,
}
},
}
),
encoding="utf-8",
)
router = build_advanced_laboratory_router(
e40_root_provider=lambda: root,
)
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
assert route()["items"] == [] # type: ignore[index,operator]
def test_advanced_catalogs_fail_closed_on_incomplete_results(
tmp_path: Path,
) -> None:
+177
View File
@@ -0,0 +1,177 @@
from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from fastapi import APIRouter
from fastapi.routing import APIRoute
from pytest import MonkeyPatch
import k1link.web.e40_case_review_api as case_api
from k1link.web.e40_case_review_api import build_e40_case_review_router
def _endpoint(router: APIRouter, path: str) -> object:
for route in router.routes:
if (
isinstance(route, APIRoute)
and route.path == path
and route.methods is not None
and "GET" in route.methods
):
return route.endpoint
raise AssertionError(f"GET {path} route is missing")
def _prediction(
*,
sequence: int,
severity: str,
split: str = "validation",
presence: str = "object-present",
predicted_presence: str = "occupied-environment",
) -> dict[str, Any]:
return {
"schema_version": "missioncore.e40-perception-prediction/v1",
"sequence": sequence,
"item_id": f"e30-review-item-{sequence:064x}",
"review_key": f"geometry:{sequence}:0",
"source_frame_index": 100 + sequence,
"source_stratum": "geometry-only",
"severity": severity,
"split": split,
"prediction": {
"presence": predicted_presence,
"geometry_association": "independent-occupied",
"freshness": "current",
},
"presence_confidence": 1.0,
"reference": {
"presence": presence,
"geometry_association": "object-associated",
"freshness": "current",
},
"scored": split == "validation",
"authority": {
"commands_enabled": False,
"navigation_or_safety_accepted": False,
},
}
def test_e40_case_review_is_bounded_sorted_and_source_bound(
tmp_path: Path,
monkeypatch: MonkeyPatch,
) -> None:
result_id = f"e40-perception-product-gate-{'a' * 64}"
materialization_id = f"e30-materialization-{'b' * 64}"
candidate = tmp_path / result_id
candidate.mkdir()
rows = [
_prediction(sequence=7, severity="medium"),
_prediction(
sequence=1,
severity="standard",
presence="object-present",
predicted_presence="object-present",
),
_prediction(sequence=4, severity="high"),
_prediction(sequence=2, severity="high"),
_prediction(sequence=9, severity="standard", split="development"),
]
rows[1]["prediction"]["geometry_association"] = "object-associated"
predictions = candidate / "predictions.jsonl"
predictions.write_text(
"".join(json.dumps(row) + "\n" for row in rows),
encoding="utf-8",
)
(candidate / "manifest.json").write_text("{}", encoding="utf-8")
result = SimpleNamespace(
result_id=result_id,
result_root=candidate,
manifest={
"identity": {
"source": {"materialization_id": materialization_id},
},
},
report={"metrics": {"validation_items": 4}},
)
def fake_read(
root_text: str,
signature: tuple[int, ...],
) -> SimpleNamespace:
assert root_text == str(candidate.resolve())
assert signature
return result
monkeypatch.setattr(case_api, "_read_result_cached", fake_read)
router = build_e40_case_review_router(
e40_root_provider=lambda: tmp_path,
)
route = _endpoint(
router,
"/api/v1/laboratory/e40/results/{result_id}/cases",
)
catalog = route(result_id=result_id, limit=2) # type: ignore[operator]
assert catalog == {
"schema_version": "missioncore.laboratory-e40-case-catalog/v1",
"result_id": result_id,
"materialization_id": materialization_id,
"items": [
{
"item_id": f"e30-review-item-{2:064x}",
"sequence": 2,
"source_frame_index": 102,
"source_stratum": "geometry-only",
"severity": "high",
"presence_confidence": 1.0,
"prediction_basis": "fixed-stratum-policy",
"reference": {
"presence": "object-present",
"geometry_association": "object-associated",
"freshness": "current",
},
"prediction": {
"presence": "occupied-environment",
"geometry_association": "independent-occupied",
"freshness": "current",
},
"mismatched_dimensions": [
"presence",
"geometry_association",
],
"access": "read-only",
},
{
"item_id": f"e30-review-item-{4:064x}",
"sequence": 4,
"source_frame_index": 104,
"source_stratum": "geometry-only",
"severity": "high",
"presence_confidence": 1.0,
"prediction_basis": "fixed-stratum-policy",
"reference": {
"presence": "object-present",
"geometry_association": "object-associated",
"freshness": "current",
},
"prediction": {
"presence": "occupied-environment",
"geometry_association": "independent-occupied",
"freshness": "current",
},
"mismatched_dimensions": [
"presence",
"geometry_association",
],
"access": "read-only",
},
],
"total": 3,
"truncated": True,
"access": "read-only",
}
+122
View File
@@ -0,0 +1,122 @@
from __future__ import annotations
from pathlib import Path
import pytest
from k1link.compute.e40_operator_review import (
E40OperatorReviewConflictError,
E40OperatorReviewStore,
E40OperatorReviewSubject,
E40OperatorReviewSubstrate,
)
def _substrate() -> E40OperatorReviewSubstrate:
return E40OperatorReviewSubstrate(
result_id=f"e40-perception-product-gate-{'a' * 64}",
materialization_id=f"e30-materialization-{'b' * 64}",
case_catalog_sha256="c" * 64,
subjects=(
E40OperatorReviewSubject(
item_id=f"e30-review-item-{'d' * 64}",
sequence=2,
),
E40OperatorReviewSubject(
item_id=f"e30-review-item-{'e' * 64}",
sequence=9,
),
),
)
def test_e40_operator_verdict_is_atomic_resumable_and_revisioned(
tmp_path: Path,
) -> None:
store = E40OperatorReviewStore(root=tmp_path)
substrate = _substrate()
first_item = substrate.subjects[0].item_id
empty = store.get(substrate=substrate, reviewer_id="DC")
assert empty["revision"] == 0
assert empty["reviewed_item_count"] == 0
assert empty["remaining_item_count"] == 2
saved = store.record_verdict(
substrate=substrate,
reviewer_id="DC",
item_id=first_item,
expected_revision=0,
idempotency_key="ui-first",
verdict="confirmed-error",
)
assert saved["revision"] == 1
assert saved["reviewed_item_count"] == 1
assert saved["decisions"][0]["verdict"] == "confirmed-error" # type: ignore[index]
resumed = E40OperatorReviewStore(root=tmp_path).get(
substrate=substrate,
reviewer_id="DC",
)
assert resumed == saved
replay = store.record_verdict(
substrate=substrate,
reviewer_id="DC",
item_id=first_item,
expected_revision=0,
idempotency_key="ui-first",
verdict="confirmed-error",
)
assert replay == saved
revised = store.record_verdict(
substrate=substrate,
reviewer_id="DC",
item_id=first_item,
expected_revision=1,
idempotency_key="ui-revise",
verdict="rejected-error",
)
assert revised["revision"] == 2
assert revised["reviewed_item_count"] == 1
assert revised["decisions"][0]["verdict"] == "rejected-error" # type: ignore[index]
with pytest.raises(E40OperatorReviewConflictError):
store.record_verdict(
substrate=substrate,
reviewer_id="DC",
item_id=substrate.subjects[1].item_id,
expected_revision=1,
idempotency_key="ui-stale",
verdict="confirmed-error",
)
def test_e40_operator_review_is_bound_to_exact_case_catalog(
tmp_path: Path,
) -> None:
store = E40OperatorReviewStore(root=tmp_path)
substrate = _substrate()
store.record_verdict(
substrate=substrate,
reviewer_id="DC",
item_id=substrate.subjects[0].item_id,
expected_revision=0,
idempotency_key="ui-bound",
verdict="confirmed-error",
)
changed = E40OperatorReviewSubstrate(
result_id=substrate.result_id,
materialization_id=substrate.materialization_id,
case_catalog_sha256="f" * 64,
subjects=substrate.subjects,
)
changed_review = store.get(substrate=changed, reviewer_id="DC")
assert changed_review["revision"] == 0
assert changed_review["review_id"] != store.get(
substrate=substrate,
reviewer_id="DC",
)["review_id"]