feat: classify gear and weapon mods
Validate and deploy production / validate (push) Successful in 24s
Validate and deploy staging / validate (push) Successful in 44s
Validate and deploy production / deploy (push) Failing after 29s
Validate and deploy staging / deploy (push) Failing after 6s

This commit is contained in:
jb
2026-07-31 21:54:47 -04:00
parent 1bd9facd3f
commit f48cc878aa
21 changed files with 595 additions and 39 deletions
+14 -2
View File
@@ -1,6 +1,6 @@
# Game Data Capture Contracts
RG-1 freezes contract version 1 for the taxonomy, workflow states, evidence model, archive manifest, and ISAC handoff boundary.
RG-1 freezes contract version 1 for the taxonomy, workflow states, evidence model, archive manifest, and ISAC handoff boundary. The approved GDC-001 mod-family change extends version 1 with `weapon_mod` and `gear_mod`.
## Files
@@ -8,11 +8,23 @@ RG-1 freezes contract version 1 for the taxonomy, workflow states, evidence mode
- `screenshot-classification.schema.json` validates one hierarchical screen classification.
- `evidence-archive-manifest-v1.schema.json` validates a private evidence archive manifest.
- `isac-handoff-v1.schema.json` validates a bounded draft handoff for authenticated ISAC intake.
- `examples/classification/` contains one valid and one invalid classification for each family, including explicit `unknown` abstention.
- `examples/classification/` contains one valid and one invalid classification for each family, including Weapon Mod, Gear Mod, and explicit `unknown` abstention.
- `examples/archive/` and `examples/handoff/` exercise each versioned document schema.
The schemas use JSON Schema Draft 2020-12, internal `$ref` values, standard formats, and no implementation-specific keywords. Python `jsonschema` and TypeScript validators such as Ajv can compile the files without model conversion. Consumers must reject a document unless `schema_version` equals the schema's `const` value. Consumers must also run semantic reference validation because JSON Schema cannot prove that an ID points to an item in another array.
## Mod Screen Semantics
Repository source data and editors establish `weapon_mod` and `gear_mod` as separate canonical aggregates. Each family currently supports only the `detail` view and its same-named record type. A Mod classification never sets the six-slot `gear_slot` field. The Weapon Mod slot, quality, compatibility, secondary-option, Mod Effect, exotic-attribute, and weapon-lock data and the Gear Mod quality and secondary-option data remain evidence-backed fields or relationships on the owning aggregate. They are not separate screen views or record types.
The `record_types` array still supports composite screens. No repository evidence establishes another independent canonical record type on a Weapon Mod or Gear Mod detail screen, so these two mappings each contain one record type. A classifier must use `unknown` with an abstention reason for another Mod view, an ambiguous family, or unsupported content.
## Version Decision
The approved additive taxonomy change remains schema version 1. The document shapes, trust boundary, bounds, and meanings of every existing value are unchanged, and all existing version 1 labels and documents remain valid. This utility and its payloads are prototype formats; no production-stable archive or handoff version has been declared. A version increment would force a document migration without representing a structural or semantic break.
Consumers must update their closed version 1 vocabularies before they accept Mod labels. Older consumers reject the new values, which keeps the boundary fail closed. Existing stored labels need validation against the revised vocabulary but no value rewrite. The capture-label UI, storage regression coverage, classifier, extraction dispatch, evidence export, and ISAC adapter mappings must consume both families and their exact `detail`/record-type pairs before their respective gates can pass. This contract change does not alter those downstream implementations.
## Evidence Rules
Capture workers store source text, a normalized value, confidence, engine identity, an image digest, a bounded region, and append-only correction history for each evidence value. A null normalized value requires a `missing_reason`. A present normalized value requires a null `missing_reason`. Each correction's `before` state must match the preceding `after` state. Timestamps must increase, and the final `after` state must match the current evidence value.
@@ -48,6 +48,18 @@
"rank_details": ["specialization_tree_rank"]
}
},
"weapon_mod": {
"gear_slot": false,
"views": {
"detail": ["weapon_mod"]
}
},
"gear_mod": {
"gear_slot": false,
"views": {
"detail": ["gear_mod"]
}
},
"unknown": {
"gear_slot": false,
"views": {"unknown": []}
@@ -40,13 +40,13 @@
"id": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"},
"sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"},
"timestamp": {"type": "string", "format": "date-time", "maxLength": 40},
"family": {"enum": ["weapon", "gear", "skill_chip", "os_protocol", "specialization", "unknown"]},
"family": {"enum": ["weapon", "gear", "skill_chip", "os_protocol", "specialization", "weapon_mod", "gear_mod", "unknown"]},
"recordType": {"enum": [
"weapon", "weapon_talent", "weapon_augmentation_profile", "gear", "brand_set",
"gear_talent", "gear_augmentation_profile", "skill_chip_set", "skill_chip_set_bonus",
"skill_chip_roll_pool", "skill_chip_talent", "os_protocol", "os_protocol_secondary_profile",
"os_protocol_talent", "specialization", "specialization_focus", "specialization_ability",
"specialization_tree_node", "specialization_tree_rank"
"specialization_tree_node", "specialization_tree_rank", "weapon_mod", "gear_mod"
]},
"missingReason": {"enum": [
"not_visible", "occluded", "unreadable", "not_present_on_screen", "unsupported",
@@ -177,6 +177,8 @@
{"properties": {"family": {"const": "skill_chip"}, "view": {"enum": ["detail", "set_bonus", "talent"]}, "gear_slot": {"const": null}, "record_types": {"minItems": 1, "items": {"enum": ["skill_chip_set", "skill_chip_set_bonus", "skill_chip_roll_pool", "skill_chip_talent"]}}, "abstention_reason": {"const": null}}},
{"properties": {"family": {"const": "os_protocol"}, "view": {"enum": ["detail", "secondary_profile", "talent"]}, "gear_slot": {"const": null}, "record_types": {"minItems": 1, "items": {"enum": ["os_protocol", "os_protocol_secondary_profile", "os_protocol_talent"]}}, "abstention_reason": {"const": null}}},
{"properties": {"family": {"const": "specialization"}, "view": {"enum": ["overview", "details", "ability", "focus_details", "focus_tree", "rank_details"]}, "gear_slot": {"const": null}, "record_types": {"minItems": 1, "items": {"enum": ["specialization", "specialization_focus", "specialization_ability", "specialization_tree_node", "specialization_tree_rank"]}}, "abstention_reason": {"const": null}}},
{"properties": {"family": {"const": "weapon_mod"}, "view": {"const": "detail"}, "gear_slot": {"const": null}, "record_types": {"minItems": 1, "maxItems": 1, "items": {"const": "weapon_mod"}}, "abstention_reason": {"const": null}}},
{"properties": {"family": {"const": "gear_mod"}, "view": {"const": "detail"}, "gear_slot": {"const": null}, "record_types": {"minItems": 1, "maxItems": 1, "items": {"const": "gear_mod"}}, "abstention_reason": {"const": null}}},
{"properties": {"family": {"const": "unknown"}, "view": {"const": "unknown"}, "gear_slot": {"const": null}, "record_types": {"maxItems": 0}, "abstention_reason": {"enum": ["low_confidence", "ambiguous_family", "ambiguous_view", "unsupported_family", "unsupported_view", "insufficient_visual_evidence", "occluded", "corrupt_image", "resource_limit", "cancelled"]}}}
]
},
@@ -0,0 +1,8 @@
{
"family": "gear_mod",
"view": "detail",
"gear_slot": "Mask",
"record_types": ["gear_mod"],
"confidence": 0.95,
"abstention_reason": null
}
@@ -0,0 +1,8 @@
{
"family": "gear_mod",
"view": "detail",
"gear_slot": null,
"record_types": ["gear_mod"],
"confidence": 0.95,
"abstention_reason": null
}
@@ -0,0 +1,8 @@
{
"family": "weapon_mod",
"view": "effect",
"gear_slot": null,
"record_types": ["weapon_mod"],
"confidence": 0.96,
"abstention_reason": null
}
@@ -0,0 +1,8 @@
{
"family": "weapon_mod",
"view": "detail",
"gear_slot": null,
"record_types": ["weapon_mod"],
"confidence": 0.96,
"abstention_reason": null
}
@@ -44,7 +44,7 @@
"gear_talent", "gear_augmentation_profile", "skill_chip_set", "skill_chip_set_bonus",
"skill_chip_roll_pool", "skill_chip_talent", "os_protocol", "os_protocol_secondary_profile",
"os_protocol_talent", "specialization", "specialization_focus", "specialization_ability",
"specialization_tree_node", "specialization_tree_rank"
"specialization_tree_node", "specialization_tree_rank", "weapon_mod", "gear_mod"
]},
"missingReason": {"enum": [
"not_visible", "occluded", "unreadable", "not_present_on_screen", "unsupported",
@@ -6,7 +6,7 @@
"additionalProperties": false,
"required": ["family", "view", "gear_slot", "record_types", "confidence", "abstention_reason"],
"properties": {
"family": {"enum": ["weapon", "gear", "skill_chip", "os_protocol", "specialization", "unknown"]},
"family": {"enum": ["weapon", "gear", "skill_chip", "os_protocol", "specialization", "weapon_mod", "gear_mod", "unknown"]},
"view": {"type": "string"},
"gear_slot": {"type": ["string", "null"]},
"record_types": {"type": "array", "uniqueItems": true, "maxItems": 8, "items": {"type": "string"}},
@@ -59,6 +59,24 @@
"abstention_reason": {"const": null}
}
},
{
"properties": {
"family": {"const": "weapon_mod"},
"view": {"const": "detail"},
"gear_slot": {"const": null},
"record_types": {"minItems": 1, "maxItems": 1, "items": {"const": "weapon_mod"}},
"abstention_reason": {"const": null}
}
},
{
"properties": {
"family": {"const": "gear_mod"},
"view": {"const": "detail"},
"gear_slot": {"const": null},
"record_types": {"minItems": 1, "maxItems": 1, "items": {"const": "gear_mod"}},
"abstention_reason": {"const": null}
}
},
{
"properties": {
"family": {"const": "unknown"},
@@ -14,6 +14,39 @@ type MissingReason = typing.Literal[
"not_applicable",
"redacted",
]
type ScreenshotFamily = typing.Literal[
"weapon",
"gear",
"skill_chip",
"os_protocol",
"specialization",
"weapon_mod",
"gear_mod",
"unknown",
]
type CanonicalRecordType = typing.Literal[
"weapon",
"weapon_talent",
"weapon_augmentation_profile",
"gear",
"brand_set",
"gear_talent",
"gear_augmentation_profile",
"skill_chip_set",
"skill_chip_set_bonus",
"skill_chip_roll_pool",
"skill_chip_talent",
"os_protocol",
"os_protocol_secondary_profile",
"os_protocol_talent",
"specialization",
"specialization_focus",
"specialization_ability",
"specialization_tree_node",
"specialization_tree_rank",
"weapon_mod",
"gear_mod",
]
class SourceEngine(typing.TypedDict):
@@ -76,14 +109,12 @@ class FieldClaim(typing.TypedDict):
class ScreenshotClassification(typing.TypedDict):
family: typing.Literal[
"weapon", "gear", "skill_chip", "os_protocol", "specialization", "unknown"
]
family: ScreenshotFamily
view: str
gear_slot: (
typing.Literal["Mask", "Gloves", "Backpack", "Body Armor", "Holster", "Knee Pads"] | None
)
record_types: list[str]
record_types: list[CanonicalRecordType]
confidence: float
abstention_reason: str | None
@@ -99,7 +130,7 @@ class ObservedItemInstance(typing.TypedDict):
"""Values shown for one configured item; they are not reusable definitions."""
instance_id: str
family: str
family: ScreenshotFamily
source_screen_ids: list[str]
observed_context: ObservedContext
fields: list[FieldClaim]
@@ -109,7 +140,7 @@ class CanonicalDefinitionCandidate(typing.TypedDict):
"""Evidence-backed reusable claims, kept separate from the observed item."""
candidate_id: str
record_type: str
record_type: CanonicalRecordType
source_instance_ids: list[str]
evidence_refs: list[str]
fields: list[FieldClaim]
@@ -12,6 +12,8 @@ class Family(StrEnum):
SKILL_CHIP = "skill_chip"
OS_PROTOCOL = "os_protocol"
SPECIALIZATION = "specialization"
WEAPON_MOD = "weapon_mod"
GEAR_MOD = "gear_mod"
UNKNOWN = "unknown"
@@ -57,6 +59,8 @@ class RecordType(StrEnum):
SPECIALIZATION_ABILITY = "specialization_ability"
SPECIALIZATION_TREE_NODE = "specialization_tree_node"
SPECIALIZATION_TREE_RANK = "specialization_tree_rank"
WEAPON_MOD = "weapon_mod"
GEAR_MOD = "gear_mod"
SCREEN_VIEWS: Final[Mapping[Family, tuple[str, ...]]] = MappingProxyType(
@@ -80,6 +84,8 @@ SCREEN_VIEWS: Final[Mapping[Family, tuple[str, ...]]] = MappingProxyType(
"focus_tree",
"rank_details",
),
Family.WEAPON_MOD: ("detail",),
Family.GEAR_MOD: ("detail",),
Family.UNKNOWN: ("unknown",),
}
)
@@ -117,6 +123,8 @@ RECORD_TYPES_BY_VIEW: Final[Mapping[tuple[Family, str], tuple[RecordType, ...]]]
RecordType.SPECIALIZATION_TREE_NODE,
),
(Family.SPECIALIZATION, "rank_details"): (RecordType.SPECIALIZATION_TREE_RANK,),
(Family.WEAPON_MOD, "detail"): (RecordType.WEAPON_MOD,),
(Family.GEAR_MOD, "detail"): (RecordType.GEAR_MOD,),
(Family.UNKNOWN, "unknown"): (),
}
)
@@ -86,7 +86,13 @@ def validate_classification(classification: Mapping[str, object]) -> None:
raise ContractValidationError("record_types must contain strings")
if len(record_types) != len(set(record_types)):
raise ContractValidationError("record_types must be unique")
allowed = {record_type.value for record_type in taxonomy.RECORD_TYPES_BY_VIEW[(family, view)]}
if len(record_types) > 8:
raise ContractValidationError("record_types cannot contain more than 8 values")
try:
normalized_record_types = {taxonomy.RecordType(record_type) for record_type in record_types}
except ValueError as error:
raise ContractValidationError("Unknown canonical record type") from error
allowed = set(taxonomy.RECORD_TYPES_BY_VIEW[(family, view)])
if family is taxonomy.Family.UNKNOWN:
if record_types:
raise ContractValidationError("Unknown screens cannot claim canonical record types")
@@ -94,7 +100,7 @@ def validate_classification(classification: Mapping[str, object]) -> None:
if reason not in taxonomy.ABSTENTION_REASONS:
raise ContractValidationError("Unknown screens require an abstention reason")
else:
if not record_types or not set(record_types).issubset(allowed):
if not record_types or not normalized_record_types.issubset(allowed):
raise ContractValidationError(
f"record_types are incompatible with {family.value}/{view}"
)
@@ -40,6 +40,8 @@ from game_data_capture.web.security import new_bootstrap_token
_POLICY_VERSION = "capture-v1"
_PROCESSING_VERSION = "capture-v1"
_POLL_SECONDS = 0.1
# This is a bounded latest-history projection, not a corpus total or pagination.
_MAX_CAPTURE_PROJECTIONS = 256
class RuntimeFailure(RuntimeError):
@@ -462,7 +464,7 @@ class CaptureRuntime:
app = create_app(
config=WebConfig(
server=self.config.server,
max_capture_items=min(64, self.config.limits.max_queue_items),
max_capture_items=_MAX_CAPTURE_PROJECTIONS,
max_image_bytes=self.config.limits.max_image_bytes,
),
capture_control=control,
@@ -22,7 +22,7 @@
* { box-sizing: border-box; }
body { margin: 0; min-width: 20rem; background: var(--canvas); color: var(--text); }
body { margin: 0; background: var(--canvas); color: var(--text); }
button, input { font: inherit; }
@@ -67,15 +67,20 @@ h2 { margin-bottom: 0; font-size: 1.15rem; }
.control-actions, .form-actions, .dialog-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
.workspace-grid { display: grid; grid-template-columns: minmax(17rem, 24rem) minmax(0, 1fr); border: 1px solid var(--line); background: var(--surface); }
.capture-panel { border-right: 1px solid var(--line); min-height: 42rem; }
.capture-panel { border-right: 1px solid var(--line); display: flex; flex-direction: column; height: min(42rem, calc(100vh - 10rem)); min-height: 24rem; min-width: 0; }
.capture-panel, .review-panel { padding: 1rem; }
.panel-heading { border-bottom: 1px solid var(--line); padding-bottom: .85rem; }
.queue-filter { display: flex; gap: .55rem; align-items: center; min-height: 44px; color: var(--muted); font-size: .88rem; }
.queue-filter input { inline-size: 1.1rem; block-size: 1.1rem; flex: 0 0 auto; }
.capture-scroll { min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding-right: .25rem; scrollbar-gutter: stable; }
.empty-copy, .review-empty { color: var(--muted); padding: 1rem 0; line-height: 1.5; }
.capture-list { margin: 0; padding: 0; list-style: none; }
.capture-list button { width: 100%; display: grid; grid-template-columns: 1fr auto; gap: .25rem .6rem; margin-top: .55rem; text-align: left; background: var(--well); }
.capture-list button { width: 100%; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .25rem .6rem; margin-top: .55rem; text-align: left; background: var(--well); }
.capture-list button[aria-current="true"] { border-color: var(--orange); color: var(--text); }
.capture-list .capture-id { overflow-wrap: anywhere; }
.capture-list .capture-meta { color: var(--muted); font-size: .8rem; }
.capture-list .capture-id { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.capture-list .capture-meta { display: grid; gap: .15rem; color: var(--muted); font-size: .72rem; line-height: 1.2; text-align: right; white-space: nowrap; }
.capture-list .capture-review { color: var(--text); font-size: .75rem; font-weight: 800; letter-spacing: .04em; }
.capture-list .capture-time { overflow: hidden; text-overflow: ellipsis; }
.review-content { padding-top: 1rem; }
.evidence-frame { margin: 0 0 1rem; border: 1px solid var(--line); background: var(--well); padding: .65rem; }
@@ -103,7 +108,8 @@ dialog p { line-height: 1.5; }
.command-header, .control-strip { align-items: flex-start; flex-direction: column; }
.header-state { text-align: left; }
.workspace-grid { grid-template-columns: 1fr; }
.capture-panel { min-height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
.capture-panel { height: auto; min-height: 0; border-right: 0; border-bottom: 1px solid var(--line); }
.capture-scroll { overflow: visible; padding-right: 0; scrollbar-gutter: auto; }
.capture-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: .55rem; }
.capture-list button { margin-top: 0; }
}
@@ -4,6 +4,8 @@ const taxonomy = Object.freeze({
skill_chip: { views: ["detail", "set_bonus", "talent"], records: { detail: ["skill_chip_set", "skill_chip_roll_pool"], set_bonus: ["skill_chip_set_bonus"], talent: ["skill_chip_talent"] } },
os_protocol: { views: ["detail", "secondary_profile", "talent"], records: { detail: ["os_protocol"], secondary_profile: ["os_protocol_secondary_profile"], talent: ["os_protocol_talent"] } },
specialization: { views: ["overview", "details", "ability", "focus_details", "focus_tree", "rank_details"], records: { overview: ["specialization"], details: ["specialization"], ability: ["specialization_ability"], focus_details: ["specialization_focus"], focus_tree: ["specialization_focus", "specialization_tree_node"], rank_details: ["specialization_tree_rank"] } },
weapon_mod: { views: ["detail"], records: { detail: ["weapon_mod"] } },
gear_mod: { views: ["detail"], records: { detail: ["gear_mod"] } },
unknown: { views: ["unknown"], records: { unknown: [] } }
});
const families = Object.keys(taxonomy);
@@ -13,7 +15,7 @@ const csrfPattern = /^[A-Za-z0-9_-]{32,128}$/;
const $ = (id) => document.getElementById(id);
const els = Object.freeze({
sessionState: $("session-state"), queueState: $("queue-state"), captureState: $("capture-state"), notice: $("notice"), noticeTitle: $("notice-title"), noticeCopy: $("notice-copy"), start: $("start-button"), stop: $("stop-button"), refresh: $("refresh-button"), list: $("capture-list"), listEmpty: $("capture-empty"), count: $("capture-count"), reviewTitle: $("review-title"), reviewState: $("review-state"), reviewEmpty: $("review-empty"), reviewContent: $("review-content"), media: $("capture-media"), meta: $("capture-meta"), labelForm: $("label-form"), validation: $("label-validation"), save: $("save-label-button"), reject: $("reject-button"), remove: $("delete-button"), family: $("family-options"), view: $("view-options"), gear: $("gear-slot-options"), records: $("record-type-options"), abstention: $("abstention-options"), viewFieldset: $("view-fieldset"), gearFieldset: $("gear-slot-fieldset"), recordFieldset: $("record-types-fieldset"), abstentionFieldset: $("abstention-fieldset"), dialog: $("confirmation-dialog"), dialogKicker: $("confirmation-kicker"), dialogTitle: $("confirmation-title"), dialogCopy: $("confirmation-copy"), dialogSubmit: $("confirmation-submit")
sessionState: $("session-state"), queueState: $("queue-state"), captureState: $("capture-state"), notice: $("notice"), noticeTitle: $("notice-title"), noticeCopy: $("notice-copy"), start: $("start-button"), stop: $("stop-button"), refresh: $("refresh-button"), filter: $("hide-reviewed"), list: $("capture-list"), listEmpty: $("capture-empty"), count: $("capture-count"), reviewTitle: $("review-title"), reviewState: $("review-state"), reviewEmpty: $("review-empty"), reviewContent: $("review-content"), media: $("capture-media"), meta: $("capture-meta"), labelForm: $("label-form"), validation: $("label-validation"), save: $("save-label-button"), reject: $("reject-button"), remove: $("delete-button"), family: $("family-options"), view: $("view-options"), gear: $("gear-slot-options"), records: $("record-type-options"), abstention: $("abstention-options"), viewFieldset: $("view-fieldset"), gearFieldset: $("gear-slot-fieldset"), recordFieldset: $("record-types-fieldset"), abstentionFieldset: $("abstention-fieldset"), dialog: $("confirmation-dialog"), dialogKicker: $("confirmation-kicker"), dialogTitle: $("confirmation-title"), dialogCopy: $("confirmation-copy"), dialogSubmit: $("confirmation-submit")
});
let csrfToken = null;
@@ -22,6 +24,7 @@ let captures = [];
let armed = null;
let pending = false;
let returnFocus = null;
let hideReviewed = false;
function setText(element, value) { element.textContent = value; }
function clearNode(node) { while (node.firstChild) node.removeChild(node.firstChild); }
@@ -32,12 +35,30 @@ function selected(name) { return document.querySelector(`input[name="${name}"]:c
function selectedAll(name) { return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map((input) => input.value); }
function currentFamily() { return selected("family"); }
function currentView() { return selected("view"); }
function isReviewedCapture(capture) { return capture?.classification !== null && capture?.classification !== undefined; }
function visibleCaptures() { return hideReviewed ? captures.filter((capture) => !isReviewedCapture(capture)) : captures; }
function abbreviateCaptureId(value) { return value.length <= 22 ? value : `${value.slice(0, 11)}…${value.slice(-8)}`; }
function formatCaptureTime(value) {
if (typeof value !== "string" || value.length > 64) return "TIME UNAVAILABLE";
const timestamp = Date.parse(value);
if (!Number.isFinite(timestamp)) return "TIME UNAVAILABLE";
const date = new Date(timestamp);
const parts = [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes()];
if (parts.some((part) => !Number.isInteger(part))) return "TIME UNAVAILABLE";
const [year, month, day, hour, minute] = parts;
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")} ${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")} UTC`;
}
function activeCaptureProjection() {
if (!activeCapture) return null;
return captures.find((capture) => capture.capture_id === activeCapture.capture_id) || activeCapture;
}
function syncControls() {
const locked = !csrfToken;
els.start.disabled = pending || locked || armed === true;
els.stop.disabled = pending || locked || armed !== true;
els.refresh.disabled = pending || locked;
els.filter.disabled = pending || locked;
els.save.disabled = pending || locked || !activeCapture || !isValidLabel();
els.reject.disabled = pending || locked || !activeCapture;
els.remove.disabled = pending || locked || !activeCapture;
@@ -71,7 +92,7 @@ function lock(message) {
els.listEmpty.hidden = false;
setText(els.listEmpty, message);
clearNode(els.list);
els.count.textContent = "0 items";
els.count.textContent = "0 shown / 0 loaded";
setBusy(false);
}
function validSession(payload) {
@@ -178,24 +199,45 @@ function renderLabelForm(classification) {
validateLabel();
}
function renderCaptures() {
clearNode(els.list); els.count.textContent = `${captures.length} ${captures.length === 1 ? "item" : "items"}`; els.listEmpty.hidden = captures.length > 0;
if (!captures.length) { setText(els.listEmpty, "EMPTY QUEUE: capture evidence will appear after a manual refresh."); return; }
captures.forEach((capture) => {
const item = document.createElement("li"); const button = document.createElement("button"); button.type = "button"; button.setAttribute("aria-current", String(activeCapture?.capture_id === capture.capture_id));
const id = document.createElement("span"); id.className = "capture-id"; id.textContent = capture.capture_id;
const meta = document.createElement("span"); meta.className = "capture-meta"; meta.textContent = `${capture.state} · ${capture.received_at}`;
const visible = visibleCaptures();
clearNode(els.list); els.count.textContent = `${visible.length} shown / ${captures.length} loaded`; els.listEmpty.hidden = visible.length > 0;
if (!visible.length) {
setText(els.listEmpty, captures.length ? "ALL LOADED CAPTURES HIDDEN: clear Hide reviewed captures to inspect loaded captures." : "NO LOADED CAPTURES: refresh to check the latest local queue.");
return;
}
visible.forEach((capture) => {
const item = document.createElement("li"); const button = document.createElement("button"); button.type = "button";
if (activeCapture?.capture_id === capture.capture_id) button.setAttribute("aria-current", "true");
const reviewState = isReviewedCapture(capture) ? "REVIEWED" : "UNREVIEWED";
const displayedTime = formatCaptureTime(capture.received_at);
button.setAttribute("aria-label", `Capture ${capture.capture_id}. ${reviewState}. ${displayedTime}.`);
const id = document.createElement("span"); id.className = "capture-id"; id.textContent = abbreviateCaptureId(capture.capture_id);
const meta = document.createElement("span"); meta.className = "capture-meta";
const review = document.createElement("span"); review.className = "capture-review"; review.textContent = reviewState;
const time = document.createElement("span"); time.className = "capture-time"; time.textContent = displayedTime;
meta.append(review, time);
button.append(id, meta); button.addEventListener("click", () => selectCapture(capture.capture_id)); item.append(button); els.list.append(item);
});
syncControls();
}
function focusIfAvailable(control) { if (control && !control.disabled) control.focus(); }
function focusVisibleCaptureOrFilter() { focusIfAvailable(els.list.querySelector("button:not(:disabled)") || els.filter); }
function applyReviewedFilter() {
hideReviewed = els.filter.checked;
const activeProjection = activeCaptureProjection();
const activeIsHidden = hideReviewed && isReviewedCapture(activeProjection);
if (activeIsHidden) clearActiveDetail();
renderCaptures();
if (activeIsHidden) focusVisibleCaptureOrFilter();
}
async function refreshCaptures({ announce = true, fallbackFocus = null } = {}) {
if (!csrfToken || pending) return false;
setBusy(true); if (announce) notice("info", "LOADING", "Refreshing the local capture queue.");
const payload = await request("/api/captures");
if (!payload || !Array.isArray(payload.captures)) { setBusy(false); if (announce) focusIfAvailable(fallbackFocus); return false; }
captures = payload.captures;
if (activeCapture && !captures.some((capture) => capture.capture_id === activeCapture.capture_id)) clearActiveDetail();
const activeProjection = activeCaptureProjection();
if (activeCapture && (!activeProjection || (hideReviewed && isReviewedCapture(activeProjection)))) clearActiveDetail();
renderCaptures(); if (announce) notice("success", "QUEUE REFRESHED", "Local capture list updated."); setBusy(false); if (announce) focusIfAvailable(fallbackFocus); return true;
}
async function selectCapture(captureId, { announce = true } = {}) {
@@ -238,6 +280,7 @@ async function confirmed(action, fallbackFocus) {
focusIfAvailable(successFocus);
}
async function initialize() {
els.filter.checked = false; hideReviewed = false;
renderLabelForm(null);
let fragment = window.location.hash; let token = null;
if (fragment.length > 1 && fragment.length <= 256) { const params = new URLSearchParams(fragment.slice(1)); const candidate = params.get("bootstrap"); if (candidate && csrfPattern.test(candidate)) token = candidate; }
@@ -267,10 +310,15 @@ els.labelForm.addEventListener("submit", async (event) => {
if (!payload || !acceptMutationCsrf(payload)) { setBusy(false); return; }
setBusy(false);
if (!(await refreshCaptures({ announce: false }))) { focusIfAvailable(els.save); return; }
if (captures.some((capture) => capture.capture_id === captureId)) await selectCapture(captureId, { announce: false });
const savedCapture = captures.find((capture) => capture.capture_id === captureId);
const savedCaptureIsHidden = Boolean(savedCapture && hideReviewed && isReviewedCapture(savedCapture));
if (savedCapture && !savedCaptureIsHidden) await selectCapture(captureId, { announce: false });
else { clearActiveDetail(); renderCaptures(); }
notice("success", "LABEL SAVED", "Manual reviewed label saved with confidence 1.0.");
focusIfAvailable(els.save);
if (savedCaptureIsHidden) focusVisibleCaptureOrFilter();
else focusIfAvailable(els.save);
});
els.filter.addEventListener("change", applyReviewedFilter);
els.reject.addEventListener("click", () => showConfirmation("reject", els.reject));
els.remove.addEventListener("click", () => showConfirmation("delete", els.remove));
els.dialog.addEventListener("close", () => {
@@ -46,10 +46,16 @@
<p class="kicker">PRIVATE QUEUE</p>
<h2 id="capture-list-title">Captures</h2>
</div>
<span id="capture-count">0 items</span>
<span id="capture-count">0 shown / 0 loaded</span>
</div>
<label class="queue-filter" for="hide-reviewed">
<input id="hide-reviewed" type="checkbox" />
<span>Hide reviewed captures</span>
</label>
<div class="capture-scroll" id="capture-scroll">
<p class="empty-copy" id="capture-empty">Authenticate to inspect the local capture queue.</p>
<ol class="capture-list" id="capture-list" aria-label="Available captures"></ol>
</div>
</aside>
<section class="review-panel" aria-labelledby="review-title">
@@ -17,7 +17,12 @@ from game_data_capture.domain.state_machines import (
InvalidStateTransition,
require_transition,
)
from game_data_capture.domain.taxonomy import RECORD_TYPES_BY_VIEW, SCREEN_VIEWS, Family
from game_data_capture.domain.taxonomy import (
RECORD_TYPES_BY_VIEW,
SCREEN_VIEWS,
Family,
RecordType,
)
from game_data_capture.domain.validation import (
ContractValidationError,
validate_archive_document,
@@ -28,6 +33,10 @@ from game_data_capture.domain.validation import (
ROOT = Path(__file__).resolve().parents[2]
CONTRACTS = ROOT / "contracts"
CLASSIFICATION_EXAMPLES = CONTRACTS / "examples" / "classification"
MOD_FAMILY_CONTRACT = {
Family.WEAPON_MOD: RecordType.WEAPON_MOD,
Family.GEAR_MOD: RecordType.GEAR_MOD,
}
def load_json(path: Path) -> dict[str, Any]:
@@ -82,6 +91,31 @@ def test_domain_contract_and_python_taxonomy_cover_every_family_view() -> None:
assert families[family.value]["views"][view] == expected
def test_mod_families_have_exact_frozen_detail_semantics() -> None:
contract = load_json(CONTRACTS / "domain-contract-v1.json")
assert contract["contract_version"] == 1
assert set(MOD_FAMILY_CONTRACT) == {Family.WEAPON_MOD, Family.GEAR_MOD}
for family, record_type in MOD_FAMILY_CONTRACT.items():
assert SCREEN_VIEWS[family] == ("detail",)
assert RECORD_TYPES_BY_VIEW[(family, "detail")] == (record_type,)
assert contract["families"][family.value] == {
"gear_slot": False,
"views": {"detail": [record_type.value]},
}
def test_approved_taxonomy_change_remains_contract_version_one() -> None:
archive_schema = load_json(CONTRACTS / "evidence-archive-manifest-v1.schema.json")
handoff_schema = load_json(CONTRACTS / "isac-handoff-v1.schema.json")
classification_schema = load_json(CONTRACTS / "screenshot-classification.schema.json")
assert archive_schema["properties"]["schema_version"] == {"const": 1}
assert handoff_schema["properties"]["schema_version"] == {"const": 1}
assert handoff_schema["properties"]["source_archive"]["properties"]["schema_version"] == {
"const": 1
}
assert classification_schema["$id"].endswith("screenshot-classification-v1.json")
def test_domain_contract_and_python_state_machines_match() -> None:
contract_states = load_json(CONTRACTS / "domain-contract-v1.json")["states"]
assert set(contract_states) == set(STATE_MACHINES)
@@ -106,6 +140,79 @@ def test_each_family_has_a_valid_and_invalid_example(family: str) -> None:
validate_classification(invalid)
@pytest.mark.parametrize(("family", "record_type"), MOD_FAMILY_CONTRACT.items())
def test_mod_family_examples_validate_through_schema_and_semantics(
family: Family, record_type: RecordType
) -> None:
validator = schema_validator("screenshot-classification.schema.json")
valid_path = CLASSIFICATION_EXAMPLES / f"{family.value}.valid.json"
invalid_path = CLASSIFICATION_EXAMPLES / f"{family.value}.invalid.json"
assert valid_path.is_file()
assert invalid_path.is_file()
valid = load_json(valid_path)
invalid = load_json(invalid_path)
assert valid == {
"family": family.value,
"view": "detail",
"gear_slot": None,
"record_types": [record_type.value],
"confidence": valid["confidence"],
"abstention_reason": None,
}
validator.validate(valid)
validate_classification(valid)
with pytest.raises(ValidationError):
validator.validate(invalid)
with pytest.raises(ContractValidationError):
validate_classification(invalid)
@pytest.mark.parametrize(("family", "record_type"), MOD_FAMILY_CONTRACT.items())
def test_archive_and_handoff_schemas_accept_mod_record_types(
family: Family, record_type: RecordType
) -> None:
archive = load_json(CONTRACTS / "examples" / "archive" / "manifest.valid.json")
archive["screens"][0]["classification"] = {
"family": family.value,
"view": "detail",
"gear_slot": None,
"record_types": [record_type.value],
"confidence": 0.96,
"abstention_reason": None,
}
archive["observed_instances"][0]["family"] = family.value
archive["canonical_candidates"][0]["record_type"] = record_type.value
schema_validator("evidence-archive-manifest-v1.schema.json").validate(archive)
validate_archive_document(archive)
handoff = load_json(CONTRACTS / "examples" / "handoff" / "isac-handoff.valid.json")
handoff["records"][0]["record_type"] = record_type.value
schema_validator("isac-handoff-v1.schema.json").validate(handoff)
validate_handoff_document(handoff)
@pytest.mark.parametrize(("family", "record_type"), MOD_FAMILY_CONTRACT.items())
@pytest.mark.parametrize(
("field", "invalid_value"),
[
("view", "quality_profile"),
("record_types", ["weapon"]),
("record_types", ["weapon_mod_effect"]),
("gear_slot", "Mask"),
],
)
def test_mod_classifications_fail_closed_on_unsupported_values(
family: Family, record_type: RecordType, field: str, invalid_value: object
) -> None:
classification = load_json(CLASSIFICATION_EXAMPLES / f"{family.value}.valid.json")
assert classification["record_types"] == [record_type.value]
classification[field] = invalid_value
with pytest.raises(ValidationError):
schema_validator("screenshot-classification.schema.json").validate(classification)
with pytest.raises(ContractValidationError):
validate_classification(classification)
def test_composite_classification_preserves_multiple_record_types() -> None:
classification = load_json(CLASSIFICATION_EXAMPLES / "weapon.valid.json")
validate_classification(classification)
@@ -121,6 +228,15 @@ def test_unknown_enum_value_is_rejected() -> None:
validate_classification(classification)
def test_semantic_validator_enforces_the_bounded_record_type_collection() -> None:
classification = load_json(CLASSIFICATION_EXAMPLES / "weapon.valid.json")
classification["record_types"] = [f"future_record_{index}" for index in range(9)]
with pytest.raises(ValidationError):
schema_validator("screenshot-classification.schema.json").validate(classification)
with pytest.raises(ContractValidationError, match="more than 8"):
validate_classification(classification)
@pytest.mark.parametrize(
("schema_name", "valid_fixture", "invalid_fixture"),
[
@@ -7,6 +7,7 @@ from pathlib import Path
import pytest
from game_data_capture.domain.validation import validate_classification
from game_data_capture.storage import CaptureCorpus, CaptureRepository, ContentAddressedBlobStore
from game_data_capture.storage.blobs import BlobCorruptionError, BlobLockError
from game_data_capture.storage.database import StorageDatabase
@@ -24,6 +25,17 @@ def _label(*, view: str = "detail", records: list[str] | None = None) -> dict[st
}
def _mod_label(family: str) -> dict[str, object]:
return {
"family": family,
"view": "detail",
"gear_slot": None,
"record_types": [family],
"confidence": 1.0,
"abstention_reason": None,
}
@pytest.fixture
def corpus(tmp_path: Path) -> tuple[StorageDatabase, CaptureRepository, CaptureCorpus]:
root = (tmp_path / "utility").resolve()
@@ -420,6 +432,91 @@ def test_label_transaction_rolls_back_on_evidence_failure(
assert stored is not None and '"view":"detail"' in stored["classification_json"]
def test_mod_labels_round_trip_through_restart_projections_and_shared_validation(
corpus: tuple[StorageDatabase, CaptureRepository, CaptureCorpus],
monkeypatch: pytest.MonkeyPatch,
) -> None:
database, repository, port = corpus
weapon_mod = _mod_label("weapon_mod")
gear_mod = _mod_label("gear_mod")
_receive(repository, "capture-0001", "receipt-0001")
_receive(repository, "capture-0002", "receipt-0002")
assert port.label("capture-0001", weapon_mod) == "updated"
assert port.label("capture-0002", gear_mod) == "updated"
shared_validator = validate_classification
validated: list[dict[str, object]] = []
def record_shared_validation(value: object) -> None:
assert isinstance(value, dict)
validated.append(value.copy())
shared_validator(value)
monkeypatch.setattr(
"game_data_capture.storage.corpus.validate_classification", record_shared_validation
)
restarted = CaptureCorpus(
database, ContentAddressedBlobStore(database.root), max_media_bytes=1024
)
listed = {
record.capture_id: record.classification for record in restarted.list_captures(limit=2)
}
assert listed == {"capture-0001": weapon_mod, "capture-0002": gear_mod}
weapon_detail = restarted.get_capture("capture-0001")
gear_detail = restarted.get_capture("capture-0002")
assert weapon_detail is not None and weapon_detail.classification == weapon_mod
assert gear_detail is not None and gear_detail.classification == gear_mod
assert validated == [gear_mod, weapon_mod, weapon_mod, gear_mod]
def test_invalid_mod_label_leaves_current_label_and_evidence_unchanged(
corpus: tuple[StorageDatabase, CaptureRepository, CaptureCorpus],
) -> None:
database, repository, port = corpus
weapon_mod = _mod_label("weapon_mod")
gear_mod = _mod_label("gear_mod")
_receive(repository, "capture-0001", "receipt-0001")
assert port.label("capture-0001", weapon_mod) == "updated"
assert port.label("capture-0001", gear_mod) == "updated"
with database.connect() as connection:
current = connection.execute(
"SELECT classification_json, revision FROM capture_labels WHERE capture_id = ?",
("capture-0001",),
).fetchone()
assert current is not None
before_current = tuple(current)
before_evidence = [
tuple(row)
for row in connection.execute(
"SELECT replaced_revision, classification_json FROM capture_label_evidence "
"WHERE capture_id = ? ORDER BY replaced_revision",
("capture-0001",),
)
]
malformed = {**weapon_mod, "gear_slot": "Mask"}
unsupported = {**weapon_mod, "view": "blueprint"}
for invalid in (malformed, unsupported):
with pytest.raises(ValueError, match="frozen taxonomy shape"):
port.label("capture-0001", invalid)
with database.connect() as connection:
current = connection.execute(
"SELECT classification_json, revision FROM capture_labels WHERE capture_id = ?",
("capture-0001",),
).fetchone()
assert current is not None
after_current = tuple(current)
after_evidence = [
tuple(row)
for row in connection.execute(
"SELECT replaced_revision, classification_json FROM capture_label_evidence "
"WHERE capture_id = ? ORDER BY replaced_revision",
("capture-0001",),
)
]
assert after_current == before_current
assert after_evidence == before_evidence
def test_deletion_intent_recovers_after_unlink_failure(
corpus: tuple[StorageDatabase, CaptureRepository, CaptureCorpus],
monkeypatch: pytest.MonkeyPatch,
@@ -122,6 +122,44 @@ def test_private_capture_labeling_flow_preserves_rejection_before_retention_dele
assert corpus.deleted is True
def test_mod_labels_round_trip_through_the_reviewed_label_route() -> None:
token = new_bootstrap_token()
client = TestClient(
create_app(
config=WebConfig(ServerConfig()),
capture_control=CaptureControl(),
corpus=Corpus(),
bootstrap_token=token,
),
base_url="http://127.0.0.1:8765",
)
origin = "http://127.0.0.1:8765"
csrf = str(
client.post("/api/bootstrap", headers={"Origin": origin}, json={"token": token}).json()[
"csrf_token"
]
)
for family, record_type in (("weapon_mod", "weapon_mod"), ("gear_mod", "gear_mod")):
label = {
"family": family,
"view": "detail",
"gear_slot": None,
"record_types": [record_type],
"confidence": 1.0,
"abstention_reason": None,
}
response = client.post(
f"/api/captures/{_ID}/label",
headers={"Origin": origin, "X-CSRF-Token": csrf},
json=label,
)
assert response.status_code == 200
csrf = str(response.json()["csrf_token"])
assert client.get(f"/api/captures/{_ID}").json()["capture"]["classification"] == label
def test_review_ui_uses_local_external_assets_and_keeps_guardrails_in_static_source() -> None:
web_root = Path(__file__).parents[2] / "src" / "game_data_capture" / "web"
template = (web_root / "templates" / "index.html").read_text(encoding="utf-8")
@@ -146,12 +184,15 @@ def test_review_ui_uses_local_external_assets_and_keeps_guardrails_in_static_sou
assert 'credentials: "same-origin"' in script
assert "localStorage" not in script
assert "sessionStorage" not in script
assert "document.cookie" not in script
for expected in (
"weapon",
"gear",
"skill_chip",
"os_protocol",
"specialization",
"weapon_mod",
"gear_mod",
"unknown",
"Mask",
"Gloves",
@@ -193,3 +234,4 @@ def test_review_page_and_local_static_assets_serve_with_security_headers() -> No
assert "csrfPattern" in script.text
assert "setCheckedMembers" in script.text
assert "announce: false" in script.text
assert 'id="hide-reviewed"' in page.text
@@ -85,6 +85,92 @@ def test_review_script_guards_session_taxonomy_pending_and_selection_state() ->
assert "fallbackFocus?.focus();" in script
assert "function clearActiveDetail()" in script
assert 'if (action === "delete") clearActiveDetail();' in script
assert "captures.some((capture) => capture.capture_id === captureId)" in script
assert (
"const savedCapture = captures.find((capture) => capture.capture_id === captureId);"
in script
)
assert "if (savedCapture && !savedCaptureIsHidden) await selectCapture" in script
assert "else { clearActiveDetail(); renderCaptures(); }" in script
assert "refreshCaptures({ fallbackFocus: els.refresh })" in script
assert "focusIfAvailable(els.save);" in script
def test_queue_filter_keeps_reviewed_captures_local_and_focus_safe() -> None:
web_root = Path(__file__).parents[2] / "src" / "game_data_capture" / "web"
template = (web_root / "templates" / "index.html").read_text(encoding="utf-8")
script = (web_root / "static" / "review.js").read_text(encoding="utf-8")
stylesheet = (web_root / "static" / "review.css").read_text(encoding="utf-8")
assert '<input id="hide-reviewed" type="checkbox" />' in template
assert "Hide reviewed captures" in template
assert (
"checked"
not in template[
template.index('id="hide-reviewed"') - 20 : template.index('id="hide-reviewed"') + 50
]
)
assert "let hideReviewed = false;" in script
assert "els.filter.checked = false; hideReviewed = false;" in script
assert "function isReviewedCapture(capture)" in script
assert "capture?.classification !== null" in script
assert "function visibleCaptures()" in script
assert "captures.filter((capture) => !isReviewedCapture(capture))" in script
assert "`${visible.length} shown / ${captures.length} loaded`" in script
assert "ALL LOADED CAPTURES HIDDEN" in script
assert 'button.setAttribute("aria-current", "true")' in script
assert "els.filter.disabled = pending || locked;" in script
assert 'els.filter.addEventListener("change", applyReviewedFilter);' in script
filter_start = script.index("function applyReviewedFilter()")
filter_end = script.index("async function refreshCaptures", filter_start)
filter_source = script[filter_start:filter_end]
assert "const activeProjection = activeCaptureProjection();" in filter_source
assert (
"const activeIsHidden = hideReviewed && isReviewedCapture(activeProjection);"
in filter_source
)
assert "isReviewedCapture(activeCapture)" not in filter_source
assert "function activeCaptureProjection()" in script
assert (
"captures.find((capture) => capture.capture_id === activeCapture.capture_id)"
" || activeCapture" in script
)
assert "if (activeIsHidden) clearActiveDetail();" in script
assert "function focusVisibleCaptureOrFilter()" in script
assert 'els.list.querySelector("button:not(:disabled)") || els.filter' in script
assert "const savedCaptureIsHidden" in script
assert "if (savedCapture && !savedCaptureIsHidden) await selectCapture" in script
assert "if (savedCaptureIsHidden) focusVisibleCaptureOrFilter();" in script
assert ".capture-scroll { min-height: 0; overflow-x: hidden; overflow-y: auto;" in stylesheet
assert ".capture-panel { height: auto; min-height: 0;" in stylesheet
assert ".capture-scroll { overflow: visible;" in stylesheet
assert "body { margin: 0; min-width:" not in stylesheet
assert "function abbreviateCaptureId(value)" in script
assert "value.slice(0, 11)" in script
assert "function formatCaptureTime(value)" in script
assert '"TIME UNAVAILABLE"' in script
assert 'typeof value !== "string" || value.length > 64' in script
assert 'const reviewState = isReviewedCapture(capture) ? "REVIEWED" : "UNREVIEWED";' in script
assert 'button.setAttribute("aria-label", `Capture ${capture.capture_id}.' in script
assert "id.textContent = abbreviateCaptureId(capture.capture_id);" in script
assert "selectCapture(capture.capture_id)" in script
assert "els.reviewTitle.textContent = activeCapture.capture_id;" in script
assert ".capture-list .capture-id { min-width: 0; overflow: hidden;" in stylesheet
assert "text-overflow: ellipsis" in stylesheet
assert ".capture-list .capture-review" in stylesheet
def test_mod_taxonomy_maps_exact_labels_without_gear_slots_and_restores_them() -> None:
script = (
Path(__file__).parents[2] / "src" / "game_data_capture" / "web" / "static" / "review.js"
).read_text(encoding="utf-8")
for family, record_type in (("weapon_mod", "weapon_mod"), ("gear_mod", "gear_mod")):
assert (
f'{family}: {{ views: ["detail"], records: {{ detail: ["{record_type}"] }} }}' in script
)
assert 'const isGear = family === "gear";' in script
assert "if (isGear) gearSlots.forEach" in script
assert 'gear_slot: family === "gear" ? selected("gear_slot") : null' in script
assert 'setChecked("family", classification.family);' in script
assert 'setChecked("view", classification.view);' in script
assert 'setCheckedMembers("record_type", classification.record_types || []);' in script
@@ -25,6 +25,7 @@ from game_data_capture.runtime import (
_CaptureControl,
)
from game_data_capture.storage import CaptureCorpus, CaptureRepository, StorageDatabase
from game_data_capture.web.app import WebConfig
_PNG = PNG_SIGNATURE + b"unit-png"
@@ -451,6 +452,37 @@ def test_serve_constructs_durable_corpus_without_auto_arming_and_uses_one_fragme
reusable.release()
def test_serve_loads_bounded_corpus_history_independently_of_intake_capacity(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
runtime = CaptureRuntime(
AppConfig(
limits=CaptureLimits(max_queue_items=64),
lifecycle=LifecycleConfig(lock_timeout_seconds=0),
),
_paths(tmp_path),
migrations_dir=(Path(__file__).parents[3] / "migrations").resolve(),
)
captured: dict[str, object] = {}
def factory(**kwargs: object) -> object:
captured.update(kwargs)
return object()
def runner(_: object, **__: object) -> None:
control = cast(_CaptureControl, captured["capture_control"])
assert control.status().queue_capacity == runtime.config.limits.max_queue_items
monkeypatch.setattr(runtime_module, "create_app", factory)
monkeypatch.setattr(uvicorn, "run", runner)
runtime.serve()
web_config = cast(WebConfig, captured["config"])
assert web_config.max_capture_items == 256
assert web_config.max_capture_items > runtime.config.limits.max_queue_items
def test_service_startup_reconciliation_failure_releases_the_instance_lock(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None: