From 3cc90e45738a96cd7f94d99e7a79cc9d603f96dc Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Sun, 11 Oct 2026 03:35:12 +0800 Subject: [PATCH] feat(submissions): define bounded performance and scoped history contracts --- backend/src/kf_oj/api/v1/submissions.py | 23 +- backend/src/kf_oj/contracts/export.py | 9 + .../kf_oj/contracts/submission_performance.py | 126 +++++++ .../src/kf_oj/modules/submissions/queries.py | 13 +- .../test_submission_performance_contract.py | 78 +++++ contracts/openapi/platform.v1.json | 309 ++++++++++++++++++ .../submission-performance.v1.schema.json | 248 ++++++++++++++ frontend/src/api/generated/schema.d.ts | 134 ++++++++ .../submission-performance.valid.json | 38 +++ 9 files changed, 976 insertions(+), 2 deletions(-) create mode 100644 backend/src/kf_oj/contracts/submission_performance.py create mode 100644 backend/tests/test_submission_performance_contract.py create mode 100644 contracts/schemas/submission-performance.v1.schema.json create mode 100644 tests/fixtures/contracts/submission-performance.valid.json diff --git a/backend/src/kf_oj/api/v1/submissions.py b/backend/src/kf_oj/api/v1/submissions.py index fdd70df..13f695e 100644 --- a/backend/src/kf_oj/api/v1/submissions.py +++ b/backend/src/kf_oj/api/v1/submissions.py @@ -4,6 +4,8 @@ from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response from kf_oj.api.dependencies import current_principal, unsafe_principal +from kf_oj.contracts.judging_v2 import Language +from kf_oj.contracts.submission_performance import SubmissionPerformance from kf_oj.contracts.submissions import ( CancellationAccepted, CreateSubmission, @@ -65,9 +67,11 @@ async def history( principal: Annotated[Principal, Depends(current_principal)], after: Annotated[str | None, Query(max_length=128)] = None, limit: Annotated[int, Query(ge=1, le=100)] = 20, + problem_version_id: UUID | None = None, + language: Language | None = None, ): try: - return await queries(request).page(principal, after, limit) + return await queries(request).page(principal, after, limit, problem_version_id, language) except ValueError: raise HTTPException(422, "invalid history request") from None @@ -86,6 +90,23 @@ async def detail( raise HTTPException(503, "stored result unavailable") from None +@router.get("/{submission_id}/performance", response_model=SubmissionPerformance) +async def performance( + submission_id: UUID, + request: Request, + principal: Annotated[Principal, Depends(current_principal)], +): + service = getattr(request.app.state, "submission_performance", None) + if service is None: + raise HTTPException(503, "performance service unavailable") + try: + return await service.detail(principal, submission_id) + except SubmissionUnavailable: + raise HTTPException(404, "submission unavailable") from None + except ValueError: + raise HTTPException(503, "performance unavailable") from None + + @router.get("/{submission_id}/source", response_model=SubmissionSource) async def source( submission_id: UUID, diff --git a/backend/src/kf_oj/contracts/export.py b/backend/src/kf_oj/contracts/export.py index 0215106..5a2af30 100644 --- a/backend/src/kf_oj/contracts/export.py +++ b/backend/src/kf_oj/contracts/export.py @@ -8,6 +8,7 @@ from kf_oj.api.app import create_app from kf_oj.contracts.judge import JudgeTaskNotification from kf_oj.contracts.judging import WIRE_MODELS from kf_oj.contracts.judging_v2 import WIRE_MODELS as WIRE_MODELS_V2 +from kf_oj.contracts.submission_performance import SubmissionPerformance from kf_oj.core.settings import Settings @@ -34,6 +35,14 @@ def main() -> None: "budgets, result consistency, UUID canonical form and UTF-8 byte limits." ) outputs[root / f"contracts/schemas/{name}.v1.schema.json"] = document + performance = SubmissionPerformance.model_json_schema() + performance["$schema"] = "https://json-schema.org/draft/2020-12/schema" + performance["$id"] = "urn:kf-oj:submission-performance:v1" + performance["$comment"] = ( + "Anonymous bounded aggregates, decimal raw units and strict same-snapshot " + "counts; also run model validation and server-side ownership/environment checks." + ) + outputs[root / "contracts/schemas/submission-performance.v1.schema.json"] = performance for name, model in WIRE_MODELS_V2.items(): document = model.model_json_schema() document["$schema"] = "https://json-schema.org/draft/2020-12/schema" diff --git a/backend/src/kf_oj/contracts/submission_performance.py b/backend/src/kf_oj/contracts/submission_performance.py new file mode 100644 index 0000000..2cb4ff1 --- /dev/null +++ b/backend/src/kf_oj/contracts/submission_performance.py @@ -0,0 +1,126 @@ +"""Bounded anonymous aggregates; raw measurements never pass through JS floats.""" + +from datetime import datetime +from typing import Annotated, Literal, Self + +from pydantic import AfterValidator, Field, StrictBool, model_validator + +from kf_oj.contracts.judging import I64_MAX, Digest, Identifier, Version, WireModel +from kf_oj.contracts.judging_v2 import Language + +MAX_PERFORMANCE_SAMPLES = 10_000 +MIN_PERFORMANCE_SAMPLES = 5 +MAX_PERFORMANCE_BUCKETS = 64 + + +def bounded_decimal(value: str) -> str: + if int(value) > I64_MAX: + raise ValueError("measurement exceeds signed storage range") + return value + + +DecimalMeasurement = Annotated[ + str, + Field(strict=True, min_length=1, max_length=19, pattern=r"^(0|[1-9][0-9]*)$"), + AfterValidator(bounded_decimal), +] +SampleCount = Annotated[int, Field(strict=True, ge=0, le=MAX_PERFORMANCE_SAMPLES)] +UnavailableReason = Literal[ + "not_accepted", "measurement_missing", "environment_unverified", "insufficient_samples" +] + + +class PerformanceBucket(WireModel): + start: DecimalMeasurement + end: DecimalMeasurement + upper_inclusive: StrictBool + count: SampleCount + + @model_validator(mode="after") + def interval(self) -> Self: + if int(self.end) <= int(self.start): + raise ValueError("empty or reversed interval") + return self + + +class PerformanceMetric(WireModel): + unit: Literal["cpu_microseconds", "bytes"] + current: DecimalMeasurement | None + available: StrictBool + reason: UnavailableReason | None + sample_count: SampleCount + window_truncated: StrictBool + oldest_sample_at: datetime | None + # These counts are computed against the original integers, never rounded bins. + strictly_slower_count: SampleCount | None + tied_count: SampleCount | None + buckets: Annotated[tuple[PerformanceBucket, ...], Field(max_length=MAX_PERFORMANCE_BUCKETS)] + + @model_validator(mode="after") + def aggregate(self) -> Self: + if self.oldest_sample_at is not None and self.oldest_sample_at.utcoffset() is None: + raise ValueError("sample timestamp must have a timezone") + if self.available: + if ( + self.reason is not None + or self.current is None + or self.sample_count < MIN_PERFORMANCE_SAMPLES + or self.strictly_slower_count is None + or self.tied_count is None + or self.oldest_sample_at is None + or not self.buckets + ): + raise ValueError("incomplete available aggregate") + if self.strictly_slower_count + self.tied_count > self.sample_count: + raise ValueError("comparison counts exceed the cohort") + if sum(b.count for b in self.buckets) != self.sample_count: + raise ValueError("histogram loses samples") + if int(self.buckets[0].start) != 0: + raise ValueError("histogram must include zero") + for index, bucket in enumerate(self.buckets): + if bucket.upper_inclusive != (index == len(self.buckets) - 1): + raise ValueError("only the last interval includes its upper endpoint") + if index and self.buckets[index - 1].end != bucket.start: + raise ValueError("histogram intervals must be contiguous") + elif ( + self.reason is None + or self.buckets + or self.strictly_slower_count is not None + or self.tied_count is not None + ): + raise ValueError("unavailable aggregate cannot expose comparison data") + if self.reason == "measurement_missing" and self.current is not None: + raise ValueError("missing measurement cannot claim a value") + if self.reason == "insufficient_samples" and ( + self.sample_count >= MIN_PERFORMANCE_SAMPLES or self.current is None + ): + raise ValueError("inconsistent insufficient cohort") + return self + + +class SubmissionPerformance(WireModel): + schema_version: Version = 1 + kind: Literal["judge.performance"] = "judge.performance" + submission_id: Identifier + problem_version_id: Identifier + language: Language + as_of: datetime + # Opaque trusted comparison identity, not a worker name, path or credential. + comparison_group: Digest | None + window_limit: Literal[10_000] = MAX_PERFORMANCE_SAMPLES + minimum_samples: Literal[5] = MIN_PERFORMANCE_SAMPLES + cpu: PerformanceMetric + memory: PerformanceMetric + + @model_validator(mode="after") + def snapshot(self) -> Self: + if self.as_of.utcoffset() is None: + raise ValueError("snapshot timestamp must have a timezone") + if self.cpu.unit != "cpu_microseconds" or self.memory.unit != "bytes": + raise ValueError("metric unit mismatch") + for metric in (self.cpu, self.memory): + if metric.available and self.comparison_group is None: + raise ValueError("comparison requires trusted environment binding") + if metric.oldest_sample_at is not None and metric.oldest_sample_at > self.as_of: + raise ValueError("sample is newer than the snapshot") + return self diff --git a/backend/src/kf_oj/modules/submissions/queries.py b/backend/src/kf_oj/modules/submissions/queries.py index e254aa7..ac307bd 100644 --- a/backend/src/kf_oj/modules/submissions/queries.py +++ b/backend/src/kf_oj/modules/submissions/queries.py @@ -88,7 +88,14 @@ class SubmissionQueries: .where(Submission.id == submission_id, Submission.account_id == principal.account_id) ) - async def page(self, principal: Principal, after: str | None, limit: int) -> SubmissionPage: + async def page( + self, + principal: Principal, + after: str | None, + limit: int, + problem_version_id: UUID | None = None, + language: str | None = None, + ) -> SubmissionPage: if not 1 <= limit <= 100: raise ValueError("invalid history limit") query = ( @@ -98,6 +105,10 @@ class SubmissionQueries: .order_by(Submission.created_at.desc(), Submission.id.desc()) .limit(limit + 1) ) + if problem_version_id is not None: + query = query.where(Submission.problem_version_id == problem_version_id) + if language is not None: + query = query.where(Submission.language == language) if after is not None: query = query.where(tuple_(Submission.created_at, Submission.id) < cursor_decode(after)) async with self.sessions() as session: diff --git a/backend/tests/test_submission_performance_contract.py b/backend/tests/test_submission_performance_contract.py new file mode 100644 index 0000000..b7cacb2 --- /dev/null +++ b/backend/tests/test_submission_performance_contract.py @@ -0,0 +1,78 @@ +"""Protocol/route boundaries, not evidence of real measured execution.""" + +import copy +import json +from pathlib import Path +from uuid import UUID + +import pytest +from fastapi.testclient import TestClient +from jsonschema import Draft202012Validator + +from kf_oj.api.app import create_app +from kf_oj.api.dependencies import current_principal +from kf_oj.contracts.submission_performance import SubmissionPerformance +from kf_oj.core.settings import Settings +from kf_oj.modules.identity.public import Principal + +ROOT = Path(__file__).parents[2] + + +def fixture(): + return json.loads( + (ROOT / "tests/fixtures/contracts/submission-performance.valid.json").read_text() + ) + + +def test_decimal_measurements_zero_and_four_languages_roundtrip(): + data = fixture() + for language in ("c17", "cpp20", "python313", "java21"): + data["language"] = language + response = SubmissionPerformance.model_validate(data) + encoded = response.model_dump(mode="json") + assert encoded == data + assert encoded["cpu"]["current"] == "9007199254740993" + assert encoded["memory"]["current"] == "0" + Draft202012Validator(SubmissionPerformance.model_json_schema()).validate(encoded) + + +@pytest.mark.parametrize( + "change", + [ + lambda v: v.update(language="Rust"), + lambda v: v.update(schema_version=True), + lambda v: v.update(username="other_user"), + lambda v: v.update(comparison_group=None), + lambda v: v.update(as_of="2026-10-11T00:00:00"), + lambda v: v["cpu"].update(current=9007199254740993), + lambda v: v["cpu"].update(current="092"), + lambda v: v["cpu"].update(current="9223372036854775808"), + lambda v: v["cpu"].update(sample_count=4), + lambda v: v["cpu"].update(strictly_slower_count=6, tied_count=1), + lambda v: v["cpu"].update(oldest_sample_at="2026-10-12T00:00:00Z"), + lambda v: v["cpu"]["buckets"][0].update(upper_inclusive=True), + lambda v: v["cpu"]["buckets"][0].update(count=2), + lambda v: v["cpu"]["buckets"][1].update(start="9007199254740994"), + lambda v: v["memory"].update(sample_count=5), + lambda v: v["memory"].update(tied_count=1), + lambda v: v["memory"].update(reason="measurement_missing"), + ], +) +def test_no_rounded_fabricated_unbounded_or_inconsistent_aggregate(change): + data = copy.deepcopy(fixture()) + change(data) + with pytest.raises(ValueError): + SubmissionPerformance.model_validate(data) + + +def test_performance_unconfigured_and_bad_history_filters_fail_closed(): + app = create_app(Settings(_env_file=None, environment="test")) + sid = "11111111-1111-4111-8111-111111111111" + with TestClient(app) as client: + assert client.get(f"/api/v1/submissions/{sid}/performance").status_code == 503 + app.dependency_overrides[current_principal] = lambda: Principal(UUID(sid), "owner") + response = client.get(f"/api/v1/submissions/{sid}/performance") + assert response.status_code == 503 + assert response.headers["Cache-Control"] == "no-store" + assert client.get("/api/v1/submissions?language=rust").status_code == 422 + assert client.get("/api/v1/submissions?problem_version_id=1001").status_code == 422 diff --git a/contracts/openapi/platform.v1.json b/contracts/openapi/platform.v1.json index 9640b05..24ae15f 100644 --- a/contracts/openapi/platform.v1.json +++ b/contracts/openapi/platform.v1.json @@ -308,6 +308,161 @@ "title": "LoginResponse", "type": "object" }, + "PerformanceBucket": { + "additionalProperties": false, + "properties": { + "count": { + "maximum": 10000.0, + "minimum": 0.0, + "title": "Count", + "type": "integer" + }, + "end": { + "maxLength": 19, + "minLength": 1, + "pattern": "^(0|[1-9][0-9]*)$", + "title": "End", + "type": "string" + }, + "start": { + "maxLength": 19, + "minLength": 1, + "pattern": "^(0|[1-9][0-9]*)$", + "title": "Start", + "type": "string" + }, + "upper_inclusive": { + "title": "Upper Inclusive", + "type": "boolean" + } + }, + "required": [ + "start", + "end", + "upper_inclusive", + "count" + ], + "title": "PerformanceBucket", + "type": "object" + }, + "PerformanceMetric": { + "additionalProperties": false, + "properties": { + "available": { + "title": "Available", + "type": "boolean" + }, + "buckets": { + "items": { + "$ref": "#/components/schemas/PerformanceBucket" + }, + "maxItems": 64, + "title": "Buckets", + "type": "array" + }, + "current": { + "anyOf": [ + { + "maxLength": 19, + "minLength": 1, + "pattern": "^(0|[1-9][0-9]*)$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Current" + }, + "oldest_sample_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Oldest Sample At" + }, + "reason": { + "anyOf": [ + { + "enum": [ + "not_accepted", + "measurement_missing", + "environment_unverified", + "insufficient_samples" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason" + }, + "sample_count": { + "maximum": 10000.0, + "minimum": 0.0, + "title": "Sample Count", + "type": "integer" + }, + "strictly_slower_count": { + "anyOf": [ + { + "maximum": 10000.0, + "minimum": 0.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Strictly Slower Count" + }, + "tied_count": { + "anyOf": [ + { + "maximum": 10000.0, + "minimum": 0.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Tied Count" + }, + "unit": { + "enum": [ + "cpu_microseconds", + "bytes" + ], + "title": "Unit", + "type": "string" + }, + "window_truncated": { + "title": "Window Truncated", + "type": "boolean" + } + }, + "required": [ + "unit", + "current", + "available", + "reason", + "sample_count", + "window_truncated", + "oldest_sample_at", + "strictly_slower_count", + "tied_count", + "buckets" + ], + "title": "PerformanceMetric", + "type": "object" + }, "PreparationKind": { "enum": [ "compile", @@ -907,6 +1062,84 @@ "title": "SubmissionPage", "type": "object" }, + "SubmissionPerformance": { + "additionalProperties": false, + "properties": { + "as_of": { + "format": "date-time", + "title": "As Of", + "type": "string" + }, + "comparison_group": { + "anyOf": [ + { + "maxLength": 64, + "minLength": 64, + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Comparison Group" + }, + "cpu": { + "$ref": "#/components/schemas/PerformanceMetric" + }, + "kind": { + "const": "judge.performance", + "default": "judge.performance", + "title": "Kind", + "type": "string" + }, + "language": { + "$ref": "#/components/schemas/kf_oj__contracts__judging_v2__Language" + }, + "memory": { + "$ref": "#/components/schemas/PerformanceMetric" + }, + "minimum_samples": { + "const": 5, + "default": 5, + "title": "Minimum Samples", + "type": "integer" + }, + "problem_version_id": { + "format": "uuid", + "title": "Problem Version Id", + "type": "string" + }, + "schema_version": { + "const": 1, + "default": 1, + "title": "Schema Version", + "type": "integer" + }, + "submission_id": { + "format": "uuid", + "title": "Submission Id", + "type": "string" + }, + "window_limit": { + "const": 10000, + "default": 10000, + "title": "Window Limit", + "type": "integer" + } + }, + "required": [ + "submission_id", + "problem_version_id", + "language", + "as_of", + "comparison_group", + "cpu", + "memory" + ], + "title": "SubmissionPerformance", + "type": "object" + }, "SubmissionSource": { "additionalProperties": false, "properties": { @@ -1557,6 +1790,39 @@ "title": "Limit", "type": "integer" } + }, + { + "in": "query", + "name": "problem_version_id", + "required": false, + "schema": { + "anyOf": [ + { + "format": "uuid", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Problem Version Id" + } + }, + { + "in": "query", + "name": "language", + "required": false, + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Language-Input" + }, + { + "type": "null" + } + ], + "title": "Language" + } } ], "responses": { @@ -1712,6 +1978,49 @@ ] } }, + "/api/v1/submissions/{submission_id}/performance": { + "get": { + "operationId": "performance_api_v1_submissions__submission_id__performance_get", + "parameters": [ + { + "in": "path", + "name": "submission_id", + "required": true, + "schema": { + "format": "uuid", + "title": "Submission Id", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmissionPerformance" + } + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "summary": "Performance", + "tags": [ + "submissions" + ] + } + }, "/api/v1/submissions/{submission_id}/source": { "get": { "operationId": "source_api_v1_submissions__submission_id__source_get", diff --git a/contracts/schemas/submission-performance.v1.schema.json b/contracts/schemas/submission-performance.v1.schema.json new file mode 100644 index 0000000..9fa9f3d --- /dev/null +++ b/contracts/schemas/submission-performance.v1.schema.json @@ -0,0 +1,248 @@ +{ + "$comment": "Anonymous bounded aggregates, decimal raw units and strict same-snapshot counts; also run model validation and server-side ownership/environment checks.", + "$defs": { + "Language": { + "enum": [ + "c17", + "cpp20", + "python313", + "java21" + ], + "title": "Language", + "type": "string" + }, + "PerformanceBucket": { + "additionalProperties": false, + "properties": { + "count": { + "maximum": 10000, + "minimum": 0, + "title": "Count", + "type": "integer" + }, + "end": { + "maxLength": 19, + "minLength": 1, + "pattern": "^(0|[1-9][0-9]*)$", + "title": "End", + "type": "string" + }, + "start": { + "maxLength": 19, + "minLength": 1, + "pattern": "^(0|[1-9][0-9]*)$", + "title": "Start", + "type": "string" + }, + "upper_inclusive": { + "title": "Upper Inclusive", + "type": "boolean" + } + }, + "required": [ + "start", + "end", + "upper_inclusive", + "count" + ], + "title": "PerformanceBucket", + "type": "object" + }, + "PerformanceMetric": { + "additionalProperties": false, + "properties": { + "available": { + "title": "Available", + "type": "boolean" + }, + "buckets": { + "items": { + "$ref": "#/$defs/PerformanceBucket" + }, + "maxItems": 64, + "title": "Buckets", + "type": "array" + }, + "current": { + "anyOf": [ + { + "maxLength": 19, + "minLength": 1, + "pattern": "^(0|[1-9][0-9]*)$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Current" + }, + "oldest_sample_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Oldest Sample At" + }, + "reason": { + "anyOf": [ + { + "enum": [ + "not_accepted", + "measurement_missing", + "environment_unverified", + "insufficient_samples" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason" + }, + "sample_count": { + "maximum": 10000, + "minimum": 0, + "title": "Sample Count", + "type": "integer" + }, + "strictly_slower_count": { + "anyOf": [ + { + "maximum": 10000, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Strictly Slower Count" + }, + "tied_count": { + "anyOf": [ + { + "maximum": 10000, + "minimum": 0, + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Tied Count" + }, + "unit": { + "enum": [ + "cpu_microseconds", + "bytes" + ], + "title": "Unit", + "type": "string" + }, + "window_truncated": { + "title": "Window Truncated", + "type": "boolean" + } + }, + "required": [ + "unit", + "current", + "available", + "reason", + "sample_count", + "window_truncated", + "oldest_sample_at", + "strictly_slower_count", + "tied_count", + "buckets" + ], + "title": "PerformanceMetric", + "type": "object" + } + }, + "$id": "urn:kf-oj:submission-performance:v1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "as_of": { + "format": "date-time", + "title": "As Of", + "type": "string" + }, + "comparison_group": { + "anyOf": [ + { + "maxLength": 64, + "minLength": 64, + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Comparison Group" + }, + "cpu": { + "$ref": "#/$defs/PerformanceMetric" + }, + "kind": { + "const": "judge.performance", + "default": "judge.performance", + "title": "Kind", + "type": "string" + }, + "language": { + "$ref": "#/$defs/Language" + }, + "memory": { + "$ref": "#/$defs/PerformanceMetric" + }, + "minimum_samples": { + "const": 5, + "default": 5, + "title": "Minimum Samples", + "type": "integer" + }, + "problem_version_id": { + "format": "uuid", + "title": "Problem Version Id", + "type": "string" + }, + "schema_version": { + "const": 1, + "default": 1, + "title": "Schema Version", + "type": "integer" + }, + "submission_id": { + "format": "uuid", + "title": "Submission Id", + "type": "string" + }, + "window_limit": { + "const": 10000, + "default": 10000, + "title": "Window Limit", + "type": "integer" + } + }, + "required": [ + "submission_id", + "problem_version_id", + "language", + "as_of", + "comparison_group", + "cpu", + "memory" + ], + "title": "SubmissionPerformance", + "type": "object" +} diff --git a/frontend/src/api/generated/schema.d.ts b/frontend/src/api/generated/schema.d.ts index ec4b37f..166f00e 100644 --- a/frontend/src/api/generated/schema.d.ts +++ b/frontend/src/api/generated/schema.d.ts @@ -195,6 +195,23 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/submissions/{submission_id}/performance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Performance */ + get: operations["performance_api_v1_submissions__submission_id__performance_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/submissions/{submission_id}/source": { parameters: { query?: never; @@ -366,6 +383,43 @@ export interface components { expires_at: string; user: components["schemas"]["CurrentUser"]; }; + /** PerformanceBucket */ + PerformanceBucket: { + /** Count */ + count: number; + /** End */ + end: string; + /** Start */ + start: string; + /** Upper Inclusive */ + upper_inclusive: boolean; + }; + /** PerformanceMetric */ + PerformanceMetric: { + /** Available */ + available: boolean; + /** Buckets */ + buckets: components["schemas"]["PerformanceBucket"][]; + /** Current */ + current: string | null; + /** Oldest Sample At */ + oldest_sample_at: string | null; + /** Reason */ + reason: ("not_accepted" | "measurement_missing" | "environment_unverified" | "insufficient_samples") | null; + /** Sample Count */ + sample_count: number; + /** Strictly Slower Count */ + strictly_slower_count: number | null; + /** Tied Count */ + tied_count: number | null; + /** + * Unit + * @enum {string} + */ + unit: "cpu_microseconds" | "bytes"; + /** Window Truncated */ + window_truncated: boolean; + }; /** * PreparationKind * @enum {string} @@ -582,6 +636,53 @@ export interface components { /** Next After */ next_after: string | null; }; + /** SubmissionPerformance */ + SubmissionPerformance: { + /** + * As Of + * Format: date-time + */ + as_of: string; + /** Comparison Group */ + comparison_group: string | null; + cpu: components["schemas"]["PerformanceMetric"]; + /** + * Kind + * @default judge.performance + * @constant + */ + kind: "judge.performance"; + language: components["schemas"]["kf_oj__contracts__judging_v2__Language"]; + memory: components["schemas"]["PerformanceMetric"]; + /** + * Minimum Samples + * @default 5 + * @constant + */ + minimum_samples: 5; + /** + * Problem Version Id + * Format: uuid + */ + problem_version_id: string; + /** + * Schema Version + * @default 1 + * @constant + */ + schema_version: 1; + /** + * Submission Id + * Format: uuid + */ + submission_id: string; + /** + * Window Limit + * @default 10000 + * @constant + */ + window_limit: 10000; + }; /** SubmissionSource */ SubmissionSource: { language: components["schemas"]["kf_oj__contracts__judging_v2__Language"]; @@ -920,6 +1021,8 @@ export interface operations { query?: { after?: string | null; limit?: number; + problem_version_id?: string | null; + language?: components["schemas"]["Language-Input"] | null; }; header?: never; path?: never; @@ -1042,6 +1145,37 @@ export interface operations { }; }; }; + performance_api_v1_submissions__submission_id__performance_get: { + parameters: { + query?: never; + header?: never; + path: { + submission_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SubmissionPerformance"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; source_api_v1_submissions__submission_id__source_get: { parameters: { query?: never; diff --git a/tests/fixtures/contracts/submission-performance.valid.json b/tests/fixtures/contracts/submission-performance.valid.json new file mode 100644 index 0000000..d5708f2 --- /dev/null +++ b/tests/fixtures/contracts/submission-performance.valid.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "kind": "judge.performance", + "submission_id": "11111111-1111-4111-8111-111111111111", + "problem_version_id": "22222222-2222-4222-8222-222222222222", + "language": "python313", + "as_of": "2026-10-11T00:00:00Z", + "comparison_group": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "window_limit": 10000, + "minimum_samples": 5, + "cpu": { + "unit": "cpu_microseconds", + "current": "9007199254740993", + "available": true, + "reason": null, + "sample_count": 6, + "window_truncated": false, + "oldest_sample_at": "2026-10-10T00:00:00Z", + "strictly_slower_count": 2, + "tied_count": 1, + "buckets": [ + {"start": "0", "end": "9007199254740993", "upper_inclusive": false, "count": 3}, + {"start": "9007199254740993", "end": "9007199254740999", "upper_inclusive": true, "count": 3} + ] + }, + "memory": { + "unit": "bytes", + "current": "0", + "available": false, + "reason": "insufficient_samples", + "sample_count": 0, + "window_truncated": false, + "oldest_sample_at": null, + "strictly_slower_count": null, + "tied_count": null, + "buckets": [] + } +}