P23-03: 生活の観察
@@ -0,0 +1,253 @@
|
||||
# P23-03 offline repair — evidence gap
|
||||
|
||||
Status: blocked, not static-complete. No acceptance credit. The failed 0b7727a
|
||||
OS run remains failure history. No receipts, mirror files, product, values,
|
||||
styles or test gates were changed. No git writes or engine/OS/collector/full
|
||||
runs were performed.
|
||||
|
||||
## Corrected direction
|
||||
|
||||
The three previous pixel assumptions are superseded:
|
||||
|
||||
1. `viewport_to_screen` maps to content coordinates, not screenshot pixels.
|
||||
Actual request-0002 and request-0004 CUA receipt calibration records window
|
||||
size 1440x810, screenshot 1440x842, titlebar 32, backing scale 1 and stretch 1.
|
||||
The image lookup must apply the calibrated content origin and independently
|
||||
check agreement with actual CUA click offsets; the number 32 must not become
|
||||
a checker constant. Existing affine composition checks remain required.
|
||||
2. Source RGB cannot be required unchanged after JPEG conversion, NO_STAFF
|
||||
dimming, ghost outlines and antialiasing. Palette identity is source
|
||||
provenance, not a screenshot color oracle. Pixel proof needs measured
|
||||
within-image noise and independent expected-class/state/shift alternatives.
|
||||
3. Every report row must be accounted for, but not every row is visible. Use
|
||||
report/audit fields for visible, clipped/clamped, outside with reason,
|
||||
ghost/inactive and facility-state dimming. Require eligible visible coverage
|
||||
of all six categories on both FRONT and BACK. Never silently skip a row.
|
||||
|
||||
The spec requires draw-consumed rows, pixel rectangles and provenance to
|
||||
correspond; it does not require unchanged source RGB. The function docstring
|
||||
now explicitly warns that the implementation still uses the superseded checks.
|
||||
|
||||
## Verified material and blocking dependency
|
||||
|
||||
Mirror:
|
||||
`/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07`
|
||||
|
||||
All 94 files named by MIRROR_INDEX.json were read and their SHA256 verified;
|
||||
zero missing files or mismatches. The only indexed conversion record is
|
||||
`os-pair/A/writer-request-0002/root-early-conversion.json`, with stdout/stderr.
|
||||
The only decoded PNG is the corresponding `root-early-decoded.png`, SHA256
|
||||
`206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a`.
|
||||
|
||||
The other three required stable screenshots are JPEG bytes despite .png names:
|
||||
|
||||
- `os-pair/A/writer-request-0002/screen-after.png`
|
||||
- `os-pair/A/writer-request-0004/screen-before.png`
|
||||
- `os-pair/A/writer-request-0004/screen-after.png`
|
||||
|
||||
Their magic starts `ffd8ffe000104a46`. No frozen decoded PNG/conversion records
|
||||
for these files are in the mirror. Searching the original source root named
|
||||
by MIRROR_INDEX for `*decoded*.png` and `*conversion*.json` also returned none.
|
||||
The existing conversion verifier requires a separate PNG, original raw hash,
|
||||
CUA call attribution and pinned `/usr/bin/sips` conversion record. Creating
|
||||
replacement conversion assertions, rewriting receipt paths, or silently
|
||||
changing that converter authority would not preserve this proof gate.
|
||||
|
||||
Minimal next input: Root supplies separate decoded PNGs and genuine conversion
|
||||
records/logs for those three existing raw images, adds their hashes to a new
|
||||
mirror inventory while preserving the original inventory and receipts. This
|
||||
does not require another OS capture. Alternatively the commander must explicitly
|
||||
specify an offline Python JPEG decoder authority and its provenance contract.
|
||||
Worker must not execute sips under the Python-only direction.
|
||||
|
||||
Until then the requested full frozen before/after comparison, BACK cropped
|
||||
pixel fixtures, calibrated noise and per-row pixel results cannot be submitted
|
||||
as complete. Geometry/report preparation alone cannot meet all requested tests.
|
||||
The behavioral repair and new fixtures/tests have not been implemented.
|
||||
|
||||
## Local commands and outputs
|
||||
|
||||
These read-only Python checks used the mirror path above as `p`:
|
||||
|
||||
```python
|
||||
import json, pathlib, hashlib
|
||||
p = pathlib.Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07')
|
||||
j = json.loads((p / 'MIRROR_INDEX.json').read_text())
|
||||
bad = [r['path'] for r in j['files'] if not (p/r['path']).is_file()
|
||||
or hashlib.sha256((p/r['path']).read_bytes()).hexdigest() != r['sha256']]
|
||||
print('indexed', len(j['files']), 'mismatches', bad)
|
||||
print('conversion records', [r['path'] for r in j['files'] if 'conversion' in r['path']])
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
indexed 94 mismatches []
|
||||
conversion records ['os-pair/A/writer-request-0002/root-early-conversion.json', 'os-pair/A/writer-request-0002/root-early-conversion.json.stderr', 'os-pair/A/writer-request-0002/root-early-conversion.json.stdout']
|
||||
```
|
||||
|
||||
Calling the existing `screenshot_pixels(p/name)` for each of the three paths
|
||||
above (without manufacturing a conversion argument) produced:
|
||||
```
|
||||
writer-request-0002/screen-after.png: JPEG screenshot requires conversion record
|
||||
writer-request-0004/screen-before.png: JPEG screenshot requires conversion record
|
||||
writer-request-0004/screen-after.png: JPEG screenshot requires conversion record
|
||||
```
|
||||
|
||||
Command: `python3 tools/test_p23_life_observation_evidence.py`
|
||||
```
|
||||
................................
|
||||
----------------------------------------------------------------------
|
||||
Ran 32 tests in 0.058s
|
||||
|
||||
OK
|
||||
```
|
||||
|
||||
These are existing unit tests, not the requested new repair tests, and do not
|
||||
prove the repair or acceptance. Root native/OS/full review remains pending.
|
||||
Seven conditions, six categories, 30/88, no partial credit, timeout 900 and
|
||||
cap null remain unchanged.
|
||||
|
||||
## Follow-on: 106-entry mirror (2026-10-15)
|
||||
|
||||
The missing-conversion dependency above is **resolved**, not a current blocker.
|
||||
The prior text is retained as failure history. The worker read all 106 indexed
|
||||
files and verified every byte count and SHA256, plus the preserved initial94
|
||||
inventory SHA and unchanged entries. No mirror or original receipt was edited.
|
||||
|
||||
`OFFLINE_MIRROR_106_DIAGNOSTICS.json` contains the explicit original absolute
|
||||
path -> mirrored absolute path -> equal indexed SHA256 mapping for all106 files,
|
||||
including the eight named PNG/conversion files. Each conversion was checked
|
||||
against its corresponding actual CUA screenshot call ID/raw hash, pinned sips
|
||||
binary SHA, original argv, output PNG magic/dimensions/hash and stdout/stderr
|
||||
hashes. Original raw/png path strings and argv remain unchanged. A read-only
|
||||
resolver locates indexed mirrored bytes; it does not rewrite conversion records.
|
||||
Direct reads of the source artifact directory were denied by the host
|
||||
(`PermissionError: Operation not permitted`); equality to the frozen inventory
|
||||
and conversion-record hashes was verified, not a fresh direct source-directory
|
||||
comparison. This limitation is not represented as a missing PNG conversion.
|
||||
|
||||
New `--offline-mirror` preparation diagnostics check content/image dimensions,
|
||||
backing scale, content origin derived from recorded titlebar, affine composition,
|
||||
and agreement with the separately recorded click events. They independently
|
||||
derive existing audit row identities/local rectangles from report positions and
|
||||
source palette provenance. Every existing audit row has visible/clipped/outside
|
||||
accounting and a calibrated screenshot rectangle. Room enum/operation names are
|
||||
read from the actual FacilityInstance source. Dimming is derived from operation,
|
||||
not merely the NO_STAFF state name. These are preparation diagnostics, not a
|
||||
replacement acceptance pixel gate. In particular, audit font_width remains an
|
||||
engine assertion, and receipt click events have not been verified against the
|
||||
full helper-definition/tool trace. No source RGB tolerance was invented.
|
||||
|
||||
Actual frozen-frame results:
|
||||
|
||||
- FRONT request0002: 9 audit rows (3 room,1 queue,2 guest,3 business), all visible.
|
||||
EV/staff/goods coverage pending. Two rooms are INACTIVE and one OPERATING.
|
||||
One actual draw-consumed traffic overlay at cell[-6,2,0], local rect
|
||||
[261,294,60,92], has no corresponding rectangle-audit row.
|
||||
- BACK request0004: 4 audit rows (2 room,2 business), all visible and both rooms
|
||||
INACTIVE. Queue/EV/staff/goods coverage pending; no eligible traffic overlay.
|
||||
- Before/after temporal differences within all13 calibrated audit rectangles
|
||||
are zero. This establishes stability only, not spatial JPEG error or correct
|
||||
drawing class/state. It supplies no pixel acceptance credit.
|
||||
- The existing independent typed-stream checker recomputed all14 frozen stream
|
||||
records successfully; the CLI still returned blocked/pending(exit2), as required.
|
||||
|
||||
Current repair status: **incomplete / blocked**, not genuinely submitted.
|
||||
The legacy `compare_rectangles` and its normal-OS call site have deliberately
|
||||
not been relabelled as repaired. Remaining worker work includes an independent
|
||||
expected-class/state/shift pixel discriminator with measured within-image noise,
|
||||
state negatives, full ghost/inactive/traffic draw accounting, independent badge
|
||||
width provenance and integration into the strict OS checker. The added
|
||||
preparation path does not solve these items and must not be used for acceptance.
|
||||
Missing frozen provenance includes the FRONT traffic-overlay rectangle audit,
|
||||
opposite-layer ghost draw audit, and independent font measurements for badge
|
||||
widths. Existing reports/source can support further preparation; these missing
|
||||
records do not establish that a fresh OS capture is already necessary.
|
||||
|
||||
Local validation: Python AST syntax checks; 42 checker unit tests (existing32
|
||||
plus10 preparation tests); actual mirrored checker commands above. Tests cover
|
||||
wrong titlebar/click shifts, PID, nonfinite scales, clipped/outside rectangles,
|
||||
measured temporal noise, report rectangle/identity/coverage/palette tampering.
|
||||
They do not include the still-required pixel-state discriminator negatives.
|
||||
No Godot, native, sips execution, collector, OS run, full regression, git write,
|
||||
branch/integration, Runner action or agent delegation occurred. Root native,
|
||||
complete A/B FRONT/BACK pan/zoom/distinct-PID OS matrix, mandatory full regression
|
||||
and independent Opus review remain pending. Historical baselines remain32 tests,
|
||||
17+17 semantic,319 native,7153/2585 and7/6/30/88; the new local42 count is not a
|
||||
replacement acceptance baseline. Timeout900 and cap null are unchanged.
|
||||
|
||||
## Owner proof-method repair (2026-10-15)
|
||||
|
||||
The owner's recorded decision supersedes the proposed automatic class/state/
|
||||
style discriminator, shift/swap alternative scoring, measured-noise gate and
|
||||
independent badge-width measurement gate. They are no longer required work.
|
||||
Palette RGB is provenance only. Font width remains the recorded engine value;
|
||||
no independent font measurement or numeric image tolerance was introduced.
|
||||
All earlier entries above are retained as history, not current requirements.
|
||||
|
||||
Implemented within the narrowed Python edit scope:
|
||||
|
||||
- Shared `frame_rows` now handles both offline and normal OS screenshot frames,
|
||||
uses receipt calibration checked against actual click events, checks affine
|
||||
composition, independently derives row identity/local geometry/text/palette,
|
||||
validates room state/operation and actor enum names, and retains every audit
|
||||
row with visibility, clipping reason, calibrated image rectangle and failures.
|
||||
- The legacy `compare_rectangles` and its tests are removed and replaced by
|
||||
grounded negatives. Fully outside eligible image crops and exactly uniform
|
||||
crops fail; source RGB is never compared to screenshot RGB.
|
||||
- `drawing_rectangle_rows` now expects every draw-consumed traffic overlay.
|
||||
Missing/extra rows fail the mechanical frame, without synthesizing audit rows.
|
||||
- Pure-Python PNG crops and review manifests are written for each frozen frame.
|
||||
Manifests bind decoded whole-image/raw/conversion/request/receipt hashes,
|
||||
per-row source hash, recorded font width, state, operation, layer and geometry.
|
||||
A separate SHA file hashes the exact manifest bytes. Normal OS frames use the
|
||||
same path and emit per-image review materials. Pixel categories receive no
|
||||
credit without an independent direct-image review verdict bound to that SHA,
|
||||
covering every eligible row with category/state/position verdicts. The worker
|
||||
created no actual review verdict; synthetic verdict files exist only inside
|
||||
temporary unit-test fixtures.
|
||||
|
||||
Current scope blocker, found by reading the actual dependencies:
|
||||
`tests/fixtures/p23-03/passive_observation.gd:150` owns `draw_rectangles`, not
|
||||
`world_preview.gd`. Its explicit room/queue/cabin/actor/badge loops at lines
|
||||
152-171 do not consume traffic audit rows from the view or report. The transform
|
||||
and palette audit path at lines172-182 only processes that locally built array.
|
||||
`world_preview.gd:762` draws the traffic overlay, but has no existing rectangle
|
||||
audit emitter or callback. Changing only world_preview cannot connect traffic
|
||||
rows to the actual request's `rectangles` field without altering the fixture.
|
||||
No inert emitter, mislabelled badge, or checker-generated observed audit was
|
||||
added to pretend this connection exists. World source remains unchanged.
|
||||
|
||||
Minimal separate scope proposal: permit one read-only traffic-row loop in
|
||||
`passive_observation.gd::draw_rectangles`, before its existing transform/palette
|
||||
loop, consuming report traffic rows with density>0 or temporary_block. Use
|
||||
category `traffic`, comma-separated cell coordinates as id, the report rectangle
|
||||
and `scenes/main/world_preview.gd` as style source. Alternatively explicitly
|
||||
authorize wiring a view-owned traffic audit emitter into that same fixture.
|
||||
The broad contract lists this fixture, but the latest finish recipe explicitly
|
||||
narrows edits to checker/tests and world only; the worker did not override it.
|
||||
Step4 is therefore incomplete and this repair is **blocked**, not submitted.
|
||||
|
||||
Local validation: both Python files parse with `ast.parse`; 47 local checker
|
||||
tests pass. New tests exercise blank crops, missing/extra/traffic rows, enum/name
|
||||
errors, affine/click calibration shifts, image-outside crops, exact crop hashes,
|
||||
wrong-manifest/missing/extra/rejected review rows and no credit without review.
|
||||
The r07 offline CLI verified all106 indexed entries and exited2 as blocked:
|
||||
|
||||
- FRONT request0002: expected10 rows, observed9; mechanical failure
|
||||
`offline rectangle coverage`. The traffic overlay at [-6,2,0] remains a genuine
|
||||
unaudited overlay. EV/staff/goods coverage pending; zero pixel credit.
|
||||
- BACK request0004: expected4/observed4; mechanical checks pass.
|
||||
Queue/EV/staff/goods coverage pending; zero pixel credit.
|
||||
|
||||
Current review artifacts are in `offline-r07-review/`; complete diagnostics are
|
||||
in `OFFLINE_REPAIR_OWNER_METHOD_DIAGNOSTICS.json`. The prior
|
||||
`OFFLINE_MIRROR_106_DIAGNOSTICS.json` is preserved unchanged.
|
||||
|
||||
Still pending: Root frozen native checks, fresh A/B FRONT/BACK ordinary pan/zoom
|
||||
OS observation and formal distinct-PID resume, the exact full177 regression,
|
||||
and independent Opus direct-image/all-condition review. No Godot, Runner,
|
||||
native/OS, full regression, git write, integration or agent execution occurred.
|
||||
All7 conditions, all6 categories on both layers/layouts, source recomputation,
|
||||
raw/request/CUA/conversion provenance, calibrated geometry, 30/88 without partial
|
||||
credit, timeout900 and cap null remain unchanged. No acceptance PASS is claimed.
|
||||
@@ -0,0 +1,922 @@
|
||||
{
|
||||
"errors": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"expected_row_count": 10,
|
||||
"images": [
|
||||
{
|
||||
"conversion_sha256": "d9e92a5b1925d6e1855f6eaff2820007d7db8dd79a5348ec801a38251b40fe90",
|
||||
"raw_sha256": "ab073ed1055ca2356a050fdd47176c454c109ebe5c008b194ea8a5f149efa7f5",
|
||||
"receipt_sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860",
|
||||
"request_sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167",
|
||||
"whole_image_sha256": "206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a"
|
||||
},
|
||||
{
|
||||
"conversion_sha256": "b25b2224963f9a02ad87675969ab51c90aff29891b136095198afc2fe95acb73",
|
||||
"raw_sha256": "7027a7f1813aa8c9fcff38e6e4b022ed718d6f9c9fd13d31ecc3d6d10818ac10",
|
||||
"receipt_sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860",
|
||||
"request_sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167",
|
||||
"whole_image_sha256": "e6ab48ad864a1b5fdca2da587d95fe7949ac0c59e664a4524db66e3e861a73aa"
|
||||
}
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"rows": [
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0000-image-0.png",
|
||||
"sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0000-image-1.png",
|
||||
"sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286"
|
||||
}
|
||||
],
|
||||
"dimmed": true,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "facility_00000002",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
131.0,
|
||||
388.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 0,
|
||||
"screenshot_rect": [
|
||||
210.0,
|
||||
485.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0001-image-0.png",
|
||||
"sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0001-image-1.png",
|
||||
"sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69"
|
||||
}
|
||||
],
|
||||
"dimmed": true,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "facility_00000003",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
1027.0,
|
||||
388.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 1,
|
||||
"screenshot_rect": [
|
||||
1106.0,
|
||||
485.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0002-image-0.png",
|
||||
"sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0002-image-1.png",
|
||||
"sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "facility_00000007",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
131.0,
|
||||
292.0,
|
||||
128.0,
|
||||
96.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "OPERATING",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 2,
|
||||
"screenshot_rect": [
|
||||
210.0,
|
||||
389.0,
|
||||
128.0,
|
||||
96.0
|
||||
],
|
||||
"state": "OPEN",
|
||||
"state_name": "OPEN",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "queue",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0003-image-0.png",
|
||||
"sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0003-image-1.png",
|
||||
"sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": 36.0,
|
||||
"id": "0",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
259.0,
|
||||
276.0,
|
||||
42.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": null,
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 3,
|
||||
"screenshot_rect": [
|
||||
338.0,
|
||||
373.0,
|
||||
42.0,
|
||||
14.0
|
||||
],
|
||||
"state": {
|
||||
"members": [
|
||||
"guest/guest-auto-00000019"
|
||||
],
|
||||
"overflow": 0,
|
||||
"visible_count": 1
|
||||
},
|
||||
"state_name": null,
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "待ち1人"
|
||||
},
|
||||
{
|
||||
"category": "guest",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0004-image-0.png",
|
||||
"sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0004-image-1.png",
|
||||
"sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "guest/guest-auto-00000019",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
267.0,
|
||||
325.0,
|
||||
48.0,
|
||||
54.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": null,
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 4,
|
||||
"screenshot_rect": [
|
||||
346.0,
|
||||
422.0,
|
||||
48.0,
|
||||
54.0
|
||||
],
|
||||
"state": "QUEUING",
|
||||
"state_name": "QUEUING",
|
||||
"style_colors": [
|
||||
"688da0",
|
||||
"b87659",
|
||||
"a69a63",
|
||||
"89789e",
|
||||
"638d72",
|
||||
"edc49d",
|
||||
"c99371",
|
||||
"a56f50",
|
||||
"303c40",
|
||||
"719d94",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"eee6c7",
|
||||
"e3dfc2",
|
||||
"956940",
|
||||
"d3ad6b",
|
||||
"b48850",
|
||||
"af9059",
|
||||
"d8bc78",
|
||||
"f3ebcf",
|
||||
"efe7ce",
|
||||
"805e44"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_people.gd",
|
||||
"style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "guest",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0005-image-0.png",
|
||||
"sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0005-image-1.png",
|
||||
"sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": null,
|
||||
"id": "guest/guest-auto-00000020",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
139.0,
|
||||
301.0,
|
||||
48.0,
|
||||
54.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": null,
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 5,
|
||||
"screenshot_rect": [
|
||||
218.0,
|
||||
398.0,
|
||||
48.0,
|
||||
54.0
|
||||
],
|
||||
"state": "RECEIVING_SERVICE",
|
||||
"state_name": "RECEIVING_SERVICE",
|
||||
"style_colors": [
|
||||
"688da0",
|
||||
"b87659",
|
||||
"a69a63",
|
||||
"89789e",
|
||||
"638d72",
|
||||
"edc49d",
|
||||
"c99371",
|
||||
"a56f50",
|
||||
"303c40",
|
||||
"719d94",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"eee6c7",
|
||||
"e3dfc2",
|
||||
"956940",
|
||||
"d3ad6b",
|
||||
"b48850",
|
||||
"af9059",
|
||||
"d8bc78",
|
||||
"f3ebcf",
|
||||
"efe7ce",
|
||||
"805e44"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_people.gd",
|
||||
"style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0006-image-0.png",
|
||||
"sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0006-image-1.png",
|
||||
"sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": 73.0,
|
||||
"id": "0",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
133.0,
|
||||
390.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 6,
|
||||
"screenshot_rect": [
|
||||
212.0,
|
||||
487.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "職員不足 未稼働"
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0007-image-0.png",
|
||||
"sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0007-image-1.png",
|
||||
"sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": 73.0,
|
||||
"id": "1",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
1029.0,
|
||||
390.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 7,
|
||||
"screenshot_rect": [
|
||||
1108.0,
|
||||
487.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "職員不足 未稼働"
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0008-image-0.png",
|
||||
"sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0008-image-1.png",
|
||||
"sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [
|
||||
"offline rectangle coverage"
|
||||
],
|
||||
"font_width": 69.0,
|
||||
"id": "2",
|
||||
"layer": 0,
|
||||
"local_rect": [
|
||||
133.0,
|
||||
294.0,
|
||||
75.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": false,
|
||||
"operation": "OPERATING",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 8,
|
||||
"screenshot_rect": [
|
||||
212.0,
|
||||
391.0,
|
||||
75.0,
|
||||
14.0
|
||||
],
|
||||
"state": "OPEN",
|
||||
"state_name": "OPEN",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "営業中 利用1人"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
646334f4ef3c78a8cfe5d68d3738c4d772cdd71cbf80b2dba7c153693ead4419
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
@@ -0,0 +1,443 @@
|
||||
{
|
||||
"errors": [],
|
||||
"expected_row_count": 4,
|
||||
"images": [
|
||||
{
|
||||
"conversion_sha256": "363c2bdb1da77458226d01e971b9aea1220fcc81f70fb5c3c816a12b7039fac3",
|
||||
"raw_sha256": "2abb4675f9978e7b86599c4334732ebadcb7cd5c5c51deeacaa7d0dee858e9b9",
|
||||
"receipt_sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17",
|
||||
"request_sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093",
|
||||
"whole_image_sha256": "abf7d2e89413431438f35cc3eecb5534a7257cb033906c5397592678b2e9bce4"
|
||||
},
|
||||
{
|
||||
"conversion_sha256": "d9f5e79103258a50bb5d0866725a8488017a9cc8ef150312d16de9043618b55c",
|
||||
"raw_sha256": "e5a7ac0c3ba3f001baae7c771304d1a9a9cf024240176e8f8735a9dcf1091b31",
|
||||
"receipt_sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17",
|
||||
"request_sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093",
|
||||
"whole_image_sha256": "b82e6ef061d9b4e8cbc3321405553f3f447c8abfb61ce38532254130a704ee95"
|
||||
}
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"rows": [
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0000-image-0.png",
|
||||
"sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0000-image-1.png",
|
||||
"sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43"
|
||||
}
|
||||
],
|
||||
"dimmed": true,
|
||||
"eligibility": "visible",
|
||||
"failures": [],
|
||||
"font_width": null,
|
||||
"id": "facility_00000004",
|
||||
"layer": 1,
|
||||
"local_rect": [
|
||||
131.0,
|
||||
388.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 0,
|
||||
"screenshot_rect": [
|
||||
210.0,
|
||||
485.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "room",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0001-image-0.png",
|
||||
"sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0001-image-1.png",
|
||||
"sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54"
|
||||
}
|
||||
],
|
||||
"dimmed": true,
|
||||
"eligibility": "visible",
|
||||
"failures": [],
|
||||
"font_width": null,
|
||||
"id": "facility_00000005",
|
||||
"layer": 1,
|
||||
"local_rect": [
|
||||
1027.0,
|
||||
388.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 1,
|
||||
"screenshot_rect": [
|
||||
1106.0,
|
||||
485.0,
|
||||
256.0,
|
||||
192.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"34413b",
|
||||
"8e6348",
|
||||
"c39665",
|
||||
"eee5c9",
|
||||
"608d84",
|
||||
"c6b999",
|
||||
"a6b6a0",
|
||||
"d4c7a5",
|
||||
"acb9a5",
|
||||
"897487",
|
||||
"f1e9cb",
|
||||
"b39b77",
|
||||
"615563",
|
||||
"a57852",
|
||||
"587f7b",
|
||||
"edc97c",
|
||||
"d6ad52",
|
||||
"709b94",
|
||||
"a37b91",
|
||||
"72b6b8",
|
||||
"c1e0d4",
|
||||
"445652",
|
||||
"a2b2a3",
|
||||
"d3aa8f",
|
||||
"ca9c62",
|
||||
"ffe9a8",
|
||||
"6c8993",
|
||||
"adc5b4",
|
||||
"52836c",
|
||||
"c7c6a6",
|
||||
"a8b6aa",
|
||||
"d8bf89",
|
||||
"aec0bf",
|
||||
"8a7155",
|
||||
"617f82",
|
||||
"3e514e",
|
||||
"404f63",
|
||||
"83958a",
|
||||
"283a39",
|
||||
"586c62",
|
||||
"526177",
|
||||
"dcc08b",
|
||||
"9daa8d",
|
||||
"879eaa",
|
||||
"556e62",
|
||||
"4d6472",
|
||||
"e2bd6c",
|
||||
"ba7157",
|
||||
"bfbb9a",
|
||||
"a9aa91",
|
||||
"9ab9b3",
|
||||
"d5e3d0",
|
||||
"f2ca81",
|
||||
"fff0d6",
|
||||
"a67c53"
|
||||
],
|
||||
"style_source": "scripts/ui/pixel_tower_art.gd",
|
||||
"style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df",
|
||||
"text": null
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0002-image-0.png",
|
||||
"sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0002-image-1.png",
|
||||
"sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [],
|
||||
"font_width": 73.0,
|
||||
"id": "0",
|
||||
"layer": 1,
|
||||
"local_rect": [
|
||||
133.0,
|
||||
390.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 2,
|
||||
"screenshot_rect": [
|
||||
212.0,
|
||||
487.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "職員不足 未稼働"
|
||||
},
|
||||
{
|
||||
"category": "business",
|
||||
"crops": [
|
||||
{
|
||||
"image_index": 0,
|
||||
"path": "row-0003-image-0.png",
|
||||
"sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869"
|
||||
},
|
||||
{
|
||||
"image_index": 1,
|
||||
"path": "row-0003-image-1.png",
|
||||
"sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869"
|
||||
}
|
||||
],
|
||||
"dimmed": false,
|
||||
"eligibility": "visible",
|
||||
"failures": [],
|
||||
"font_width": 73.0,
|
||||
"id": "1",
|
||||
"layer": 1,
|
||||
"local_rect": [
|
||||
1029.0,
|
||||
390.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"mechanical_pass": true,
|
||||
"operation": "INACTIVE",
|
||||
"reason": "whole rectangle in viewport",
|
||||
"row_index": 3,
|
||||
"screenshot_rect": [
|
||||
1108.0,
|
||||
487.0,
|
||||
79.0,
|
||||
14.0
|
||||
],
|
||||
"state": "NO_STAFF",
|
||||
"state_name": "NO_STAFF",
|
||||
"style_colors": [
|
||||
"b5cbb5",
|
||||
"273c49",
|
||||
"526d68",
|
||||
"233d40",
|
||||
"8cae98",
|
||||
"749981",
|
||||
"567e68",
|
||||
"c6d3b6",
|
||||
"d5dec4",
|
||||
"574d39",
|
||||
"776445",
|
||||
"8a7350",
|
||||
"69583d",
|
||||
"65583e",
|
||||
"385d45",
|
||||
"47463e",
|
||||
"5a594b",
|
||||
"aaa083",
|
||||
"829a8d",
|
||||
"b69465",
|
||||
"514839",
|
||||
"8d7951",
|
||||
"e1bd6c",
|
||||
"dfd4ad",
|
||||
"2f493f",
|
||||
"e4d9b9",
|
||||
"58784c",
|
||||
"4c7950",
|
||||
"5e8b57",
|
||||
"739957",
|
||||
"95ad69",
|
||||
"648957",
|
||||
"e34332",
|
||||
"e3a62f",
|
||||
"55c96b",
|
||||
"f5d582",
|
||||
"34413b",
|
||||
"fff2ce",
|
||||
"3c4e40",
|
||||
"f5d58a",
|
||||
"304b41",
|
||||
"f0e2b9",
|
||||
"303c40",
|
||||
"d69a56",
|
||||
"4ca5c7",
|
||||
"efbd72",
|
||||
"79d3e7",
|
||||
"e5f8fa",
|
||||
"64d48c",
|
||||
"e16262",
|
||||
"8ff0ab",
|
||||
"ff8a80",
|
||||
"52bdd0",
|
||||
"8ce6ee",
|
||||
"d7ebef",
|
||||
"29483d",
|
||||
"f0e2bd",
|
||||
"8ca4af",
|
||||
"6f8b67",
|
||||
"427582",
|
||||
"a24b45",
|
||||
"a77d3f",
|
||||
"6b6258",
|
||||
"89914d",
|
||||
"6f9a55",
|
||||
"67518e",
|
||||
"ffd080"
|
||||
],
|
||||
"style_source": "scenes/main/world_preview.gd",
|
||||
"style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0",
|
||||
"text": "職員不足 未稼働"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
f013e0d1d6231833b656859cc73ad0b58b389047e421ad2c43cc598820e21067
|
||||
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -169,6 +169,12 @@ func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary:
|
||||
for index: int in range(report.get(&"badges", []).size()):
|
||||
var badge: Dictionary = report[&"badges"][index]
|
||||
rows.append(badge_rectangle("goods" if badge[&"kind"] == &"supply" else "business", str(index), badge[&"at"], badge[&"text"]))
|
||||
for entry: Dictionary in report.get(&"traffic", []):
|
||||
var traffic: Dictionary = entry[&"traffic"]
|
||||
if traffic[&"current_density"] > 0 or traffic[&"temporary_block"]:
|
||||
var cell: Vector3i = entry[&"cell"]
|
||||
rows.append({"category": "traffic", "id": str(cell.x) + "," + str(cell.y) + "," + str(cell.z),
|
||||
"rect": entry[&"rect"], "style_source": "scenes/main/world_preview.gd"})
|
||||
var transform := view.get_global_transform_with_canvas()
|
||||
for row: Dictionary in rows:
|
||||
row["viewport_rect"] = transform * (row["rect"] as Rect2)
|
||||
|
||||
@@ -155,9 +155,9 @@ def image_kind(data):
|
||||
raise ValueError('unknown screenshot byte magic')
|
||||
|
||||
|
||||
def inspect_conversion(rec, cua_receipt, converter_sha256=None):
|
||||
def inspect_conversion(rec, cua_receipt, converter_sha256=None, resolve_path=Path):
|
||||
"""CUA receipt and converter pin are independent of conversion assertions."""
|
||||
raw, png = Path(rec['raw_path']), Path(rec['png_path'])
|
||||
raw, png = resolve_path(rec['raw_path']), resolve_path(rec['png_path'])
|
||||
require(raw.resolve() != png.resolve() and not raw.is_symlink() and not png.is_symlink(),
|
||||
'conversion must preserve a separate raw file')
|
||||
require(rec['raw_sha256'] == sha(raw) == cua_receipt['raw_sha256']
|
||||
@@ -169,7 +169,7 @@ def inspect_conversion(rec, cua_receipt, converter_sha256=None):
|
||||
require(rec['converter'] == '/usr/bin/sips', 'conversion executable')
|
||||
pin = converter_sha256 if converter_sha256 is not None else sha('/usr/bin/sips')
|
||||
require(rec['converter_sha256'] == pin, 'converter SHA mismatch')
|
||||
require(rec['argv'] == ['/usr/bin/sips', '-s', 'format', 'png', str(raw), '--out', str(png)],
|
||||
require(rec['argv'] == ['/usr/bin/sips', '-s', 'format', 'png', rec['raw_path'], '--out', rec['png_path']],
|
||||
'converter argv mismatch')
|
||||
require(type(rec['exit_code']) is int and rec['exit_code'] == 0, 'converter exit code')
|
||||
for key in ('stdout_sha256', 'stderr_sha256'):
|
||||
@@ -206,27 +206,92 @@ def affine_rect(matrix, rect):
|
||||
return lo + [max(p[i] for p in points)-lo[i] for i in (0, 1)]
|
||||
|
||||
|
||||
def compare_rectangles(report, audit, local_to_viewport, viewport_to_screen, local_to_screen, image, root=ROOT):
|
||||
"""All draw rows, full affine composition, and exact existing style colors.
|
||||
Font advance is recorded by the pinned engine/font, not approximated using
|
||||
a new text width threshold. No image similarity threshold is introduced.
|
||||
def screenshot_transform(calibration, request, image_size, clicks):
|
||||
"""Content -> window screenshot pixels, cross-checked against actual clicks.
|
||||
|
||||
This is independent of the renderer's content-space screen_rect. In
|
||||
particular, window_position is a desktop coordinate, not an image origin.
|
||||
No titlebar height, scale, or image extent is assumed by the checker.
|
||||
"""
|
||||
for point in ([0, 0], [1, 0], [0, 1]):
|
||||
close(affine_point(viewport_to_screen, affine_point(local_to_viewport, point)),
|
||||
affine_point(local_to_screen, point), 'viewport/screen composition')
|
||||
rows = audit['rows']
|
||||
scale = calibration['backing_scale']
|
||||
stretch = calibration['canvas_stretch']
|
||||
titlebar = calibration['titlebar_pixels']
|
||||
require(all(type(v) in (int, float) and math.isfinite(v) for v in
|
||||
(scale, stretch, titlebar)) and scale > 0 and stretch > 0 and titlebar >= 0,
|
||||
'invalid screenshot calibration')
|
||||
close(calibration['window_size'], request['window_size'], 'calibrated window size')
|
||||
close(calibration['window_position'], request['window_position'], 'calibrated window position')
|
||||
close(calibration['screenshot_pixels'], list(image_size), 'calibrated image size')
|
||||
close([request['screen_scale'], request['content_scale_factor']], [scale, stretch],
|
||||
'calibrated engine scale')
|
||||
close([request['window_size'][0]*scale, request['window_size'][1]*scale+titlebar],
|
||||
list(image_size), 'calibrated content extent')
|
||||
matrix = [scale, 0, 0, scale, 0, titlebar]
|
||||
matched = 0
|
||||
for receipt in clicks:
|
||||
if receipt.get('target_pid') != request['target_pid']:
|
||||
continue
|
||||
for event in receipt['events']:
|
||||
if event.get('action') != 'click':
|
||||
continue
|
||||
close(event['calibration'], calibration, 'click calibration')
|
||||
point = event['point']
|
||||
close(affine_point(point['viewport_to_screen'], point['viewport']),
|
||||
point['screen'], 'click content affine')
|
||||
close(affine_point(matrix, point['screen']), event['coordinates'],
|
||||
'click screenshot offset')
|
||||
matched += 1
|
||||
require(matched > 0, 'missing independent click calibration')
|
||||
return matrix
|
||||
|
||||
|
||||
def rect_intersection(a, b):
|
||||
require(len(a) == len(b) == 4 and all(type(v) in (int, float) and math.isfinite(v)
|
||||
for v in a+b) and min(a[2:]+b[2:]) >= 0, 'invalid clipping rectangle')
|
||||
x, y = max(a[0], b[0]), max(a[1], b[1])
|
||||
return [x, y, max(0, min(a[0]+a[2], b[0]+b[2])-x),
|
||||
max(0, min(a[1]+a[3], b[1]+b[3])-y)]
|
||||
|
||||
|
||||
def rectangle_eligibility(rect, viewport):
|
||||
clipped = rect_intersection(list(rect), [0, 0, *viewport])
|
||||
if not clipped[2] or not clipped[3]:
|
||||
return 'outside', clipped
|
||||
return ('visible' if clipped == list(rect) else 'clipped'), clipped
|
||||
|
||||
|
||||
def paired_crop_noise(before, after, rect):
|
||||
"""Measured temporal differences, never an invented JPEG RGB tolerance.
|
||||
|
||||
A stable pair alone does not establish class or state identity. The result
|
||||
is diagnostic data only and must not be promoted to pixels_<category>.
|
||||
"""
|
||||
require(before[:2] == after[:2], 'paired image dimensions')
|
||||
x, y, w, h = rect_intersection(list(rect), [0, 0, *before[:2]])
|
||||
errors = [max(abs(a-b) for a, b in zip(before[2][py][px][:3], after[2][py][px][:3]))
|
||||
for py in range(math.ceil(y), math.floor(y+h))
|
||||
for px in range(math.ceil(x), math.floor(x+w))]
|
||||
require(errors, 'empty noise crop')
|
||||
return dict(samples=len(errors), max_channel_delta=max(errors),
|
||||
mean_channel_delta=sum(errors)/len(errors), unchanged=sum(v == 0 for v in errors))
|
||||
|
||||
|
||||
def drawing_rectangle_rows(report):
|
||||
"""Derive identities and local positions; do not read audit verdicts."""
|
||||
expected = []
|
||||
tower, people, world = 'scripts/ui/pixel_tower_art.gd', 'scripts/ui/pixel_people.gd', 'scenes/main/world_preview.gd'
|
||||
tower = 'scripts/ui/pixel_tower_art.gd'
|
||||
people = 'scripts/ui/pixel_people.gd'
|
||||
world = 'scenes/main/world_preview.gd'
|
||||
def actor(a, category=None):
|
||||
expected.append((category or ('staff' if a['staff'] else 'guest'), a['key'],
|
||||
a['at'] + [16*a['scale'], 18*a['scale']], people, None))
|
||||
def badge(category, identifier, at, text):
|
||||
expected.append((category, identifier, at + [None, 14], world, text))
|
||||
def badge(category, identifier, b):
|
||||
expected.append((category, identifier, b['at'] + [None, 14], world, b['text']))
|
||||
for fid, room in report['rooms'].items():
|
||||
if 'rect' in room:
|
||||
expected.append(('room', fid, room['rect'], tower, None))
|
||||
for i, b in enumerate(report['landing_badges']):
|
||||
badge('queue', str(i), b['at'], b['text'])
|
||||
badge('queue', str(i), b)
|
||||
for cabin in report['cabins']:
|
||||
expected.append(('EV', cabin['shaft_id'], cabin['rect'], tower, None))
|
||||
for a in cabin['passengers']:
|
||||
@@ -236,36 +301,254 @@ def compare_rectangles(report, audit, local_to_viewport, viewport_to_screen, loc
|
||||
if a['staff'] and a['key'].removeprefix('staff/') in report['resting_staff']:
|
||||
actor(a, 'rest')
|
||||
for i, b in enumerate(report['badges']):
|
||||
badge('goods' if b['kind'] == 'supply' else 'business', str(i), b['at'], b['text'])
|
||||
require(len(rows) == len(expected), 'rectangle coverage')
|
||||
width, height, pixels = image
|
||||
checked = []
|
||||
for row, (category, identifier, rect, source, text) in zip(rows, expected):
|
||||
require((row['category'], row['id'], row['style_source']) == (category, identifier, source), 'rectangle row identity')
|
||||
badge('goods' if b['kind'] == 'supply' else 'business', str(i), b)
|
||||
for row in report.get('traffic', []):
|
||||
if row['traffic']['current_density'] > 0 or row['traffic']['temporary_block']:
|
||||
expected.append(('traffic', ','.join(map(str, row['cell'])), row['rect'], world, None))
|
||||
return expected
|
||||
|
||||
|
||||
def validate_drawing_rows(report, audit, root=ROOT):
|
||||
expected = drawing_rectangle_rows(report)
|
||||
require(len(audit['rows']) == len(expected), 'offline rectangle coverage')
|
||||
for row, (category, identifier, rect, source, text) in zip(audit['rows'], expected):
|
||||
require((row['category'], row['id'], row['style_source']) == (category, identifier, source),
|
||||
'offline rectangle identity')
|
||||
rect = list(rect)
|
||||
if text is not None:
|
||||
require(row['text'] == text and type(row['font_width']) in (int, float) and row['font_width'] > 0, 'badge font/text')
|
||||
require(row['text'] == text and type(row['font_width']) in (int, float)
|
||||
and math.isfinite(row['font_width']) and row['font_width'] > 0, 'offline font/text')
|
||||
rect[2] = row['font_width'] + 6
|
||||
close(row['rect'], rect, 'local drawing rectangle')
|
||||
viewport = affine_rect(local_to_viewport, rect)
|
||||
screen = affine_rect(viewport_to_screen, viewport)
|
||||
close(row['viewport_rect'], viewport, 'viewport rectangle')
|
||||
close(row['screen_rect'], screen, 'screen rectangle')
|
||||
close(row['rect'], rect, 'offline report drawing rectangle')
|
||||
colors = list(dict.fromkeys(re.findall(r'Color\("([0-9a-fA-F]{6})"', (root/source).read_text())))
|
||||
require(row['style_colors'] == colors and colors, 'drawing style palette')
|
||||
x, y, w, h = screen
|
||||
require(w > 0 and h > 0 and x >= 0 and y >= 0 and x+w <= width and y+h <= height,
|
||||
'drawing rectangle outside screenshot')
|
||||
# Badge ink is distinct from its translucent backing. Actor ink/skin
|
||||
# and facility colors are taken verbatim from their pinned draw source.
|
||||
palette = {'fff2ce'} if text is not None else set(colors)
|
||||
palette -= {'303c40', 'c6d3b6', 'd5dec4'} # shadow / sky decorations
|
||||
rgb = {tuple(bytes.fromhex(c)) for c in palette}
|
||||
found = any(tuple(pixels[py][px][:3]) in rgb
|
||||
for py in range(math.ceil(y), math.floor(y+h))
|
||||
for px in range(math.ceil(x), math.floor(x+w)))
|
||||
require(found, 'missing drawing pixels: ' + category + '/' + str(identifier))
|
||||
checked.append(category + '/' + str(identifier))
|
||||
return checked
|
||||
require(row['style_colors'] == colors and colors, 'offline source palette provenance')
|
||||
|
||||
|
||||
def encode_png(rows):
|
||||
"""Lossless review crop from decoded pixels, without an imaging dependency."""
|
||||
require(rows and rows[0], 'empty crop')
|
||||
width, channels = len(rows[0]), len(rows[0][0])
|
||||
require(channels in (3, 4), 'crop channels')
|
||||
def chunk(kind, body):
|
||||
return (struct.pack('>I', len(body)) + kind + body
|
||||
+ struct.pack('>I', zlib.crc32(kind + body)))
|
||||
raw = b''.join(b'\0' + bytes(v for pixel in row for v in pixel) for row in rows)
|
||||
return (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack(
|
||||
'>IIBBBBB', width, len(rows), 8, 2 if channels == 3 else 6, 0, 0, 0))
|
||||
+ chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b''))
|
||||
|
||||
|
||||
def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT):
|
||||
"""One mechanical path for frozen diagnostics and actual OS frames.
|
||||
|
||||
Errors are retained as failures, including incomplete frozen audits. Every
|
||||
supplied row is accounted for; no palette value is an image oracle.
|
||||
"""
|
||||
errors, rows = [], []
|
||||
try:
|
||||
validate_drawing_rows(report, audit, root)
|
||||
except (ValueError, KeyError, TypeError) as exc:
|
||||
errors.append(str(exc))
|
||||
calibration = screenshot_transform(receipt['calibration'], request, images[0][:2], clicks)
|
||||
for image in images:
|
||||
require(image[:2] == images[0][:2], 'frame image dimensions')
|
||||
for point in ([0, 0], [1, 0], [0, 1]):
|
||||
close(affine_point(request['viewport_to_screen'], affine_point(request['local_to_viewport'], point)),
|
||||
affine_point(request['local_to_screen'], point), 'frame affine composition')
|
||||
for index, row in enumerate(audit['rows']):
|
||||
failures = list(errors)
|
||||
close(row['viewport_rect'], affine_rect(request['local_to_viewport'], row['rect']),
|
||||
'frame viewport rectangle')
|
||||
close(row['screen_rect'], affine_rect(request['local_to_screen'], row['rect']),
|
||||
'frame content rectangle')
|
||||
status, clipped = rectangle_eligibility(row['rect'], report['viewport'])
|
||||
screenshot_rect = affine_rect(calibration, affine_rect(request['local_to_screen'], clipped))
|
||||
room = report['rooms'].get(row['id']) if row['category'] == 'room' else None
|
||||
if row['category'] in ('business', 'goods'):
|
||||
room = report['rooms'].get(report['badges'][int(row['id'])]['facility'])
|
||||
operation, state = None, None
|
||||
if room is not None:
|
||||
states = enum_names(root, 'scripts/facility/facility_instance.gd', 'State')
|
||||
operations = enum_names(root, 'scripts/facility/facility_instance.gd', 'OperationState')
|
||||
require(type(room['state']) is int and 0 <= room['state'] < len(states), 'frame room enum')
|
||||
state = states[room['state']]
|
||||
require(state == room['state_name'], 'frame room state name')
|
||||
require(type(room['operation']) is int and 0 <= room['operation'] < len(operations), 'frame operation enum')
|
||||
operation = operations[room['operation']]
|
||||
actors = list(report['actor_drawings']) + [a for c in report['cabins'] for a in c['passengers']]
|
||||
actor = next((a for a in actors if a['key'] == row['id']), None)
|
||||
if actor is not None:
|
||||
actor_states = enum_names(root, 'scripts/staff/staff_runtime.gd' if actor['staff']
|
||||
else 'scripts/guest/guest_runtime.gd', 'State')
|
||||
require(type(actor['state']) is int and 0 <= actor['state'] < len(actor_states),
|
||||
'frame actor state enum')
|
||||
state = actor_states[actor['state']]
|
||||
if row['category'] == 'queue':
|
||||
badge = report['landing_badges'][int(row['id'])]
|
||||
state = {k: badge[k] for k in ('members', 'visible_count', 'overflow')}
|
||||
if row['category'] == 'EV':
|
||||
cabin = next(c for c in report['cabins'] if c['shaft_id'] == row['id'])
|
||||
state = {k: cabin[k] for k in ('operational', 'doors_open')}
|
||||
if row['category'] == 'traffic':
|
||||
traffic = next(r['traffic'] for r in report.get('traffic', [])
|
||||
if ','.join(map(str, r['cell'])) == row['id'])
|
||||
state = {k: traffic[k] for k in ('current_density', 'temporary_block')}
|
||||
result = dict(row_index=index, category=row['category'], id=row['id'], layer=report['layer'],
|
||||
state=state, state_name=state if isinstance(state, str) else None, operation=operation,
|
||||
dimmed=row['category'] == 'room' and operation != 'OPERATING',
|
||||
text=row.get('text'), local_rect=row['rect'], screenshot_rect=screenshot_rect,
|
||||
style_source=row['style_source'], style_source_sha256=sha(root/row['style_source']),
|
||||
style_colors=row['style_colors'], font_width=row.get('font_width'),
|
||||
eligibility=status, reason={'visible': 'whole rectangle in viewport',
|
||||
'clipped': 'rectangle intersects viewport', 'outside': 'no viewport intersection'}[status],
|
||||
crops=[])
|
||||
if status != 'outside':
|
||||
for number, (width, height, pixels) in enumerate(images):
|
||||
x, y, w, h = rect_intersection(screenshot_rect, [0, 0, width, height])
|
||||
crop = [line[math.ceil(x):math.floor(x+w)]
|
||||
for line in pixels[math.ceil(y):math.floor(y+h)]]
|
||||
if not crop or not crop[0]:
|
||||
failures.append('eligible crop outside image')
|
||||
continue
|
||||
if len({pixel for line in crop for pixel in line}) == 1:
|
||||
failures.append('blank crop')
|
||||
data = encode_png(crop)
|
||||
result['crops'].append(dict(image_index=number, sha256=hashlib.sha256(data).hexdigest(),
|
||||
png_bytes=data))
|
||||
result['mechanical_pass'] = not failures
|
||||
result['failures'] = failures
|
||||
rows.append(result)
|
||||
return dict(rows=rows, errors=errors, mechanical_pass=not errors and all(r['mechanical_pass'] for r in rows),
|
||||
expected_row_count=len(drawing_rectangle_rows(report)))
|
||||
|
||||
|
||||
def write_frame_review(directory, frame, provenance):
|
||||
directory = Path(directory)
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
manifest = copy.deepcopy(frame)
|
||||
manifest['images'] = provenance
|
||||
for row in manifest['rows']:
|
||||
for crop in row['crops']:
|
||||
name = 'row-%04d-image-%d.png' % (row['row_index'], crop['image_index'])
|
||||
(directory/name).write_bytes(crop.pop('png_bytes'))
|
||||
crop['path'] = name
|
||||
data = (json.dumps(manifest, sort_keys=True, ensure_ascii=False, indent=2) + '\n').encode()
|
||||
(directory/'manifest.json').write_bytes(data)
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
(directory/'manifest.sha256').write_text(digest + '\n')
|
||||
return manifest, digest
|
||||
|
||||
|
||||
def reviewed_pixel_categories(manifest, digest, verdict_path):
|
||||
"""The worker never writes verdicts; absent independent review earns nothing."""
|
||||
if not Path(verdict_path).is_file():
|
||||
return set()
|
||||
verdict = json.loads(Path(verdict_path).read_bytes())
|
||||
require(verdict['manifest_sha256'] == digest, 'review manifest SHA mismatch')
|
||||
require(verdict['reviewer'] == 'independent-opus-direct-image', 'independent image reviewer required')
|
||||
eligible = [r for r in manifest['rows'] if r['eligibility'] != 'outside']
|
||||
require([v['row_index'] for v in verdict['rows']] == [r['row_index'] for r in eligible],
|
||||
'review row coverage')
|
||||
for row, decision in zip(eligible, verdict['rows']):
|
||||
require(all(decision[k] is True for k in ('category', 'state', 'position')), 'review row rejected')
|
||||
if not manifest['mechanical_pass']:
|
||||
return set()
|
||||
return {r['category'] for r in eligible if r['mechanical_pass']}
|
||||
|
||||
|
||||
def mirrored_inventory(directory):
|
||||
"""Read every indexed byte; preserve original receipt paths as authority."""
|
||||
directory = Path(directory).resolve()
|
||||
index = json.loads((directory/'MIRROR_INDEX.json').read_bytes())
|
||||
require(Path(index['mirror']).resolve() == directory, 'mirror root mismatch')
|
||||
paths, mapping = set(), []
|
||||
source = Path(index['source'])
|
||||
for row in index['files']:
|
||||
name = Path(row['path'])
|
||||
require(not name.is_absolute() and '..' not in name.parts and str(name) not in paths,
|
||||
'invalid/duplicate mirror path')
|
||||
paths.add(str(name))
|
||||
local = directory/name
|
||||
require(local.resolve().is_relative_to(directory) and not local.is_symlink(), 'mirror escape')
|
||||
data = local.read_bytes()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
require(digest == row['sha256'] and len(data) == row['bytes'], 'mirror hash/size mismatch')
|
||||
mapping.append(dict(original=str(source/name), mirror=str(local), sha256=digest))
|
||||
prior = index['previous_inventory']
|
||||
prior_name = Path(prior['path'])
|
||||
require(not prior_name.is_absolute() and '..' not in prior_name.parts
|
||||
and (directory/prior_name).resolve().is_relative_to(directory)
|
||||
and not (directory/prior_name).is_symlink(), 'previous inventory escape')
|
||||
require(sha(directory/prior['path']) == prior['sha256'], 'previous mirror inventory hash')
|
||||
old = json.loads((directory/prior['path']).read_bytes())
|
||||
require(all(row in index['files'] for row in old['files']), 'previous mirror entries changed')
|
||||
return index, mapping
|
||||
|
||||
|
||||
def inspect_offline_mirror(directory):
|
||||
"""Preparation diagnostics, deliberately separate from the acceptance gate.
|
||||
|
||||
Frozen incomplete draw audits cannot acquire missing rows by relabelling
|
||||
source-derived rectangles as observed engine output.
|
||||
"""
|
||||
directory = Path(directory).resolve()
|
||||
index, mapping = mirrored_inventory(directory)
|
||||
by_original = {row['original']: row for row in mapping}
|
||||
def resolve(original):
|
||||
require(original in by_original, 'unindexed original path')
|
||||
return Path(by_original[original]['mirror'])
|
||||
pair = directory/'os-pair/A'
|
||||
clicks = [json.loads((pair/name/'cua-receipt.json').read_bytes())
|
||||
for name in ('writer-request-0001', 'writer-request-0003')]
|
||||
results, inspected = [], []
|
||||
for name in ('writer-request-0002', 'writer-request-0004'):
|
||||
folder = pair/name
|
||||
request = json.loads((folder/'request.json').read_bytes())
|
||||
receipt = json.loads((folder/'cua-receipt.json').read_bytes())
|
||||
require(sha(folder/'request.json') == receipt['request_sha256'], 'offline request binding')
|
||||
require(receipt['target_pid'] == request['target_pid'], 'offline screenshot PID')
|
||||
report, audit = decode(request['report']), decode(request['rectangles'])
|
||||
images = []
|
||||
for number, suffix in enumerate(('', '-after')):
|
||||
conversion_path = folder/('root-early-conversion'+suffix+'.json')
|
||||
conversion = json.loads(conversion_path.read_bytes())
|
||||
shot = receipt['screenshots'][number]
|
||||
require(resolve(conversion['raw_path']) == directory/shot['path'], 'offline raw path binding')
|
||||
image = inspect_conversion(conversion, shot, resolve_path=resolve)
|
||||
for stream in ('stdout', 'stderr'):
|
||||
require(sha(Path(str(conversion_path)+'.'+stream)) == conversion[stream+'_sha256'],
|
||||
'offline converter log hash')
|
||||
images.append(image)
|
||||
for original in (str(Path(index['source'])/conversion_path.relative_to(directory)),
|
||||
conversion['png_path']):
|
||||
inspected.append(by_original[original])
|
||||
frame = frame_rows(report, audit, request, receipt, images, clicks, ROOT)
|
||||
provenance = []
|
||||
for number, suffix in enumerate(('', '-after')):
|
||||
conversion_path = folder/('root-early-conversion'+suffix+'.json')
|
||||
conversion = json.loads(conversion_path.read_bytes())
|
||||
provenance.append(dict(whole_image_sha256=conversion['png_sha256'],
|
||||
raw_sha256=conversion['raw_sha256'],
|
||||
conversion_sha256=sha(conversion_path),
|
||||
request_sha256=sha(folder/'request.json'),
|
||||
receipt_sha256=sha(folder/'cua-receipt.json')))
|
||||
review_dir = ROOT/'docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review'/name
|
||||
manifest, digest = write_frame_review(review_dir, frame, provenance)
|
||||
credited = reviewed_pixel_categories(manifest, digest, review_dir/'independent-verdict.json')
|
||||
present = {r['category'] for r in frame['rows'] if r['eligibility'] != 'outside'}
|
||||
overlays = [dict(cell=r['cell'], rect=r['rect']) for r in report['traffic']
|
||||
if (r['traffic']['current_density'] > 0 or r['traffic']['temporary_block'])
|
||||
and not any(a['category'] == 'traffic' and a['id'] == ','.join(map(str, r['cell']))
|
||||
for a in audit['rows'])]
|
||||
results.append(dict(request=name, layer=report['layer'], manifest_sha256=digest,
|
||||
manifest_path=str(review_dir/'manifest.json'), rows=manifest['rows'],
|
||||
mechanical_pass=frame['mechanical_pass'], errors=frame['errors'],
|
||||
unaudited_traffic_overlays=overlays,
|
||||
pending_categories=[c for c in CATEGORIES if c not in present],
|
||||
pixel_categories=sorted(credited), independent_review='pending'))
|
||||
return dict(task_id='P23-03', acceptance='pending', status='blocked',
|
||||
inventory_count=len(mapping), path_map=mapping, inspected_eight=inspected,
|
||||
frames=results, root_native_os_full_regression_independent_review='pending')
|
||||
|
||||
|
||||
def enum_names(root, relative, name):
|
||||
@@ -1171,9 +1454,18 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT):
|
||||
# Screenshot actions observe a stable report; input before/after
|
||||
# screenshots are provenance only and never relabelled as this frame.
|
||||
if request['action'] == 'screenshot':
|
||||
checked = compare_rectangles(report, decode(request['rectangles']), request['local_to_viewport'],
|
||||
request['viewport_to_screen'], request['local_to_screen'], image, root)
|
||||
pixel_categories.update(c.split('/')[0] for c in checked)
|
||||
clicks = [document(json.loads(path(name).read_bytes())['input_receipt'])
|
||||
for name in listing['requests']]
|
||||
frame = frame_rows(report, decode(request['rectangles']), request, cua, [image], clicks, root)
|
||||
review_dir = request_path.parent/('review-image-' + str(number))
|
||||
manifest, manifest_sha = write_frame_review(review_dir, frame, [dict(
|
||||
whole_image_sha256=binding['png_sha256'], raw_sha256=shot['raw_sha256'],
|
||||
conversion_sha256=sha(path(binding['conversion'])) if conversion else None,
|
||||
request_sha256=digest, receipt_sha256=sha(path(ack['input_receipt'])))])
|
||||
require(frame['mechanical_pass'], 'frame mechanical checks: ' + str(frame['errors'])
|
||||
+ str([r['failures'] for r in frame['rows'] if not r['mechanical_pass']]))
|
||||
pixel_categories.update(reviewed_pixel_categories(
|
||||
manifest, manifest_sha, review_dir/'independent-verdict.json'))
|
||||
records.append(dict(request=request, ack=ack, observed=observed, cua=cua, request_sha256=digest))
|
||||
require(len(seen) == len(bindings), 'unattributed screenshot bindings')
|
||||
return records, pixel_categories
|
||||
@@ -1301,9 +1593,15 @@ def main(argv=None):
|
||||
parser.add_argument('--verify-only', action='store_true')
|
||||
parser.add_argument('--project-root', type=Path, default=ROOT)
|
||||
parser.add_argument('--diagnostics', type=Path)
|
||||
parser.add_argument('--offline-mirror', type=Path)
|
||||
parser.add_argument('--layout-a', type=Path)
|
||||
parser.add_argument('--layout-b', type=Path)
|
||||
args = parser.parse_args(argv)
|
||||
if args.offline_mirror:
|
||||
require(not any((args.verify_only, args.diagnostics, args.layout_a, args.layout_b)),
|
||||
'offline preparation must not be combined with acceptance checks')
|
||||
print(json.dumps(inspect_offline_mirror(args.offline_mirror), ensure_ascii=False))
|
||||
return 2
|
||||
if args.verify_only:
|
||||
build = os.environ.get('BUILD_OUTPUT_DIR')
|
||||
if not build:
|
||||
|
||||
@@ -288,27 +288,6 @@ class InfrastructureTests(unittest.TestCase):
|
||||
if change == 'id': q[0]['ack']['sequence'] = 2
|
||||
with self.assertRaises(ValueError): check.inspect_resume(writer, r, saved, q, members)
|
||||
|
||||
def test_rectangle_pixel_and_transform_negatives(self):
|
||||
source = 'scripts/ui/pixel_people.gd'
|
||||
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"', (check.ROOT/source).read_text())))
|
||||
report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[],
|
||||
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1)])
|
||||
row = dict(category='staff', id='staff/s', rect=[1, 1, 16, 18], viewport_rect=[1, 1, 16, 18],
|
||||
screen_rect=[1, 1, 16, 18], style_source=source, style_colors=colors)
|
||||
audit = dict(rows=[row])
|
||||
matrix = [1, 0, 0, 1, 0, 0]
|
||||
image = (24, 24, [[(113, 157, 148)]*24 for _ in range(24)])
|
||||
self.assertEqual(check.compare_rectangles(report, audit, matrix, matrix, matrix, image), ['staff/staff/s'])
|
||||
for change in ('rectangle', 'pixels', 'transform', 'coverage', 'palette'):
|
||||
with self.subTest(change=change):
|
||||
a, m, img = copy.deepcopy(audit), matrix[:], image
|
||||
if change == 'rectangle': a['rows'][0]['rect'][0] += 1
|
||||
if change == 'pixels': img = (24, 24, [[(0, 0, 0)]*24 for _ in range(24)])
|
||||
if change == 'transform': m[4] = 10
|
||||
if change == 'coverage': a['rows'].clear()
|
||||
if change == 'palette': a['rows'][0]['style_colors'] = ['000000']
|
||||
with self.assertRaises(ValueError): check.compare_rectangles(report, a, matrix, m, matrix, img)
|
||||
|
||||
def test_interpolation_history_tampering(self):
|
||||
state = dict(systems=dict(runtime=dict(guests=dict(guests=[]), staff=dict(staff=[]),
|
||||
elevators=dict(shafts=[], trips=[], current_minute=0))))
|
||||
@@ -568,5 +547,215 @@ class ComparisonTests(unittest.TestCase):
|
||||
check.compare_layouts(a, b)
|
||||
|
||||
|
||||
class OfflineGeometryTests(unittest.TestCase):
|
||||
"""Synthetic calibration examples; no OS or acceptance credit."""
|
||||
|
||||
def setUp(self):
|
||||
self.calibration = dict(window_size=[100, 80], window_position=[240, 150],
|
||||
screenshot_pixels=[200, 190], backing_scale=2,
|
||||
canvas_stretch=1, titlebar_pixels=30)
|
||||
self.request = dict(window_size=[100, 80], window_position=[240, 150],
|
||||
screen_scale=2, content_scale_factor=1, target_pid=123)
|
||||
self.clicks = [dict(target_pid=123, events=[dict(action='click',
|
||||
coordinates=[20, 70], calibration=copy.deepcopy(self.calibration),
|
||||
point=dict(screen=[10, 20], viewport=[10, 20],
|
||||
viewport_to_screen=[1, 0, 0, 1, 0, 0]))])]
|
||||
|
||||
def transform(self):
|
||||
return check.screenshot_transform(self.calibration, self.request, [200, 190], self.clicks)
|
||||
|
||||
def test_uses_measured_scale_origin_not_desktop_position(self):
|
||||
self.assertEqual(check.affine_rect(self.transform(), [10, 20, 5, 7]), [20, 70, 10, 14])
|
||||
|
||||
def test_shifted_titlebar_fails_independent_click(self):
|
||||
self.calibration['titlebar_pixels'] += 1
|
||||
self.calibration['screenshot_pixels'][1] += 1
|
||||
self.clicks[0]['events'][0]['calibration'] = copy.deepcopy(self.calibration)
|
||||
with self.assertRaises(ValueError):
|
||||
check.screenshot_transform(self.calibration, self.request, [200, 191], self.clicks)
|
||||
|
||||
def test_wrong_click_offset_fails(self):
|
||||
self.clicks[0]['events'][0]['coordinates'][1] -= 30
|
||||
with self.assertRaisesRegex(ValueError, 'click screenshot offset'):
|
||||
self.transform()
|
||||
|
||||
def test_wrong_pid_cannot_calibrate(self):
|
||||
self.clicks[0]['target_pid'] += 1
|
||||
with self.assertRaisesRegex(ValueError, 'missing independent click'):
|
||||
self.transform()
|
||||
|
||||
def test_nonfinite_and_false_scale_rejected(self):
|
||||
for scale in (False, float('nan'), 0, -1):
|
||||
self.calibration['backing_scale'] = scale
|
||||
with self.assertRaisesRegex(ValueError, 'invalid screenshot calibration'):
|
||||
self.transform()
|
||||
|
||||
def test_all_visibility_cases_are_accounted_for(self):
|
||||
self.assertEqual(check.rectangle_eligibility([2, 3, 4, 5], [10, 10]),
|
||||
('visible', [2, 3, 4, 5]))
|
||||
self.assertEqual(check.rectangle_eligibility([-2, 3, 4, 5], [10, 10]),
|
||||
('clipped', [0, 3, 2, 5]))
|
||||
self.assertEqual(check.rectangle_eligibility([10, 3, 4, 5], [10, 10])[0], 'outside')
|
||||
self.assertEqual(check.rectangle_eligibility([2, 3, 0, 5], [10, 10])[0], 'outside')
|
||||
|
||||
def test_invalid_rectangles_rejected(self):
|
||||
for rect in ([0, 0, -1, 4], [float('nan'), 0, 1, 4]):
|
||||
with self.assertRaisesRegex(ValueError, 'invalid clipping rectangle'):
|
||||
check.rectangle_eligibility(rect, [10, 10])
|
||||
|
||||
def test_noise_is_measured_and_does_not_assert_class(self):
|
||||
before = (2, 2, [[(10, 20, 30)]*2 for _ in range(2)])
|
||||
after = copy.deepcopy(before)
|
||||
after[2][1][1] = (10, 27, 30)
|
||||
noise = check.paired_crop_noise(before, after, [0, 0, 2, 2])
|
||||
self.assertEqual(noise, dict(samples=4, max_channel_delta=7,
|
||||
mean_channel_delta=1.75, unchanged=3))
|
||||
self.assertNotIn('pass', noise)
|
||||
self.assertNotIn('state', noise)
|
||||
|
||||
def test_wrong_dimensions_and_empty_crop_rejected(self):
|
||||
image = (1, 1, [[(0, 0, 0)]])
|
||||
with self.assertRaisesRegex(ValueError, 'paired image dimensions'):
|
||||
check.paired_crop_noise(image, (2, 1, []), [0, 0, 1, 1])
|
||||
with self.assertRaisesRegex(ValueError, 'empty noise crop'):
|
||||
check.paired_crop_noise(image, image, [1, 0, 1, 1])
|
||||
|
||||
def test_report_positions_and_source_provenance_are_recomputed(self):
|
||||
source = 'scripts/ui/pixel_people.gd'
|
||||
report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[],
|
||||
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1)])
|
||||
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||
(check.ROOT/source).read_text())))
|
||||
audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18],
|
||||
style_source=source, style_colors=colors)])
|
||||
check.validate_drawing_rows(report, audit)
|
||||
for change in ('shift', 'identity', 'missing', 'palette'):
|
||||
tampered = copy.deepcopy(audit)
|
||||
if change == 'shift': tampered['rows'][0]['rect'][0] += 1
|
||||
if change == 'identity': tampered['rows'][0]['category'] = 'guest'
|
||||
if change == 'missing': tampered['rows'] = []
|
||||
if change == 'palette': tampered['rows'][0]['style_colors'] = ['000000']
|
||||
with self.subTest(change=change), self.assertRaises(ValueError):
|
||||
check.validate_drawing_rows(report, tampered)
|
||||
|
||||
|
||||
class FrameReviewTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
OfflineGeometryTests.setUp(self)
|
||||
self.request.update(local_to_viewport=[1, 0, 0, 1, 0, 0],
|
||||
viewport_to_screen=[1, 0, 0, 1, 0, 0],
|
||||
local_to_screen=[1, 0, 0, 1, 0, 0])
|
||||
self.report = dict(layer=0, viewport=[100, 80], rooms={}, cabins=[],
|
||||
landing_badges=[], badges=[], resting_staff={}, traffic=[],
|
||||
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1, state=0)])
|
||||
source = 'scripts/ui/pixel_people.gd'
|
||||
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||
(check.ROOT/source).read_text())))
|
||||
self.audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18],
|
||||
viewport_rect=[1, 1, 16, 18], screen_rect=[1, 1, 16, 18],
|
||||
style_source=source, style_colors=colors)])
|
||||
self.image = (200, 190, [[(x % 256, y % 256, 0) for x in range(200)] for y in range(190)])
|
||||
|
||||
def frame(self):
|
||||
return check.frame_rows(self.report, self.audit, self.request,
|
||||
dict(calibration=self.calibration), [self.image], self.clicks)
|
||||
|
||||
def test_blank_crop_fails_without_threshold(self):
|
||||
self.image = (200, 190, [[(0, 0, 0)] * 200 for _ in range(190)])
|
||||
result = self.frame()
|
||||
self.assertFalse(result['mechanical_pass'])
|
||||
self.assertIn('blank crop', result['rows'][0]['failures'])
|
||||
|
||||
def test_missing_extra_and_unaudited_traffic_fail(self):
|
||||
for change in ('missing', 'extra', 'traffic'):
|
||||
with self.subTest(change=change):
|
||||
original = copy.deepcopy(self.audit)
|
||||
if change == 'missing': self.audit['rows'].clear()
|
||||
if change == 'extra': self.audit['rows'] *= 2
|
||||
if change == 'traffic':
|
||||
self.report['traffic'] = [dict(cell=[-6, 2, 0], rect=[1, 1, 5, 5],
|
||||
traffic=dict(current_density=1, temporary_block=False))]
|
||||
result = self.frame()
|
||||
self.assertFalse(result['mechanical_pass'])
|
||||
self.assertEqual(len(result['rows']), len(self.audit['rows']))
|
||||
self.assertIn('offline rectangle coverage', result['errors'])
|
||||
self.audit = original
|
||||
|
||||
def test_dense_or_blocked_traffic_row_closes_audit_gap(self):
|
||||
source = 'scenes/main/world_preview.gd'
|
||||
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||
(check.ROOT/source).read_text())))
|
||||
for density, blocked in ((1, False), (0, True), (0, False)):
|
||||
with self.subTest(density=density, blocked=blocked):
|
||||
self.report['traffic'] = [dict(cell=[-6, 2, 0], rect=[1, 1, 5, 5],
|
||||
traffic=dict(current_density=density, temporary_block=blocked))]
|
||||
active = density > 0 or blocked
|
||||
missing = self.frame()
|
||||
self.assertEqual(missing['mechanical_pass'], not active)
|
||||
if active:
|
||||
self.assertIn('offline rectangle coverage', missing['errors'])
|
||||
self.audit['rows'].append(dict(category='traffic', id='-6,2,0',
|
||||
rect=[1, 1, 5, 5], viewport_rect=[1, 1, 5, 5],
|
||||
screen_rect=[1, 1, 5, 5], style_source=source, style_colors=colors))
|
||||
complete = self.frame()
|
||||
self.assertTrue(complete['mechanical_pass'], complete['errors'])
|
||||
self.audit['rows'].pop()
|
||||
|
||||
def test_wrong_state_enum_and_name_fail(self):
|
||||
source = 'scripts/ui/pixel_tower_art.gd'
|
||||
self.report['actor_drawings'] = []
|
||||
room = dict(rect=[1, 1, 16, 18], state=3, state_name='NO_STAFF', operation=0)
|
||||
self.report['rooms'] = {'r': room}
|
||||
self.audit['rows'][0].update(category='room', id='r', style_source=source,
|
||||
style_colors=list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"',
|
||||
(check.ROOT/source).read_text()))))
|
||||
self.assertEqual(self.frame()['rows'][0]['operation'], 'INACTIVE')
|
||||
for state, name in ((99, 'NO_STAFF'), (3, 'OPEN'), (True, 'NO_ACCESS')):
|
||||
room.update(state=state, state_name=name)
|
||||
with self.assertRaises(ValueError): self.frame()
|
||||
|
||||
def test_frame_rejects_affine_and_calibration_shift(self):
|
||||
self.request['local_to_screen'][4] += 1
|
||||
with self.assertRaisesRegex(ValueError, 'affine composition'): self.frame()
|
||||
self.request['local_to_screen'][4] -= 1
|
||||
self.clicks[0]['events'][0]['coordinates'][1] += 1
|
||||
with self.assertRaisesRegex(ValueError, 'click screenshot offset'): self.frame()
|
||||
|
||||
def test_eligible_crop_fully_outside_image_fails(self):
|
||||
for key in ('viewport_to_screen', 'local_to_screen'):
|
||||
self.request[key][4] = 300
|
||||
self.audit['rows'][0]['screen_rect'][0] += 300
|
||||
result = self.frame()
|
||||
self.assertFalse(result['mechanical_pass'])
|
||||
self.assertIn('eligible crop outside image', result['rows'][0]['failures'])
|
||||
|
||||
def test_manifest_crop_and_independent_review_binding(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
manifest, digest = check.write_frame_review(directory, self.frame(),
|
||||
[dict(whole_image_sha256='a'*64, raw_sha256='b'*64, conversion_sha256='c'*64)])
|
||||
self.assertEqual(check.sha(directory/'manifest.json'), digest)
|
||||
crop = manifest['rows'][0]['crops'][0]
|
||||
self.assertEqual(check.sha(directory/crop['path']), crop['sha256'])
|
||||
self.assertEqual(check.png_pixels((directory/crop['path']).read_bytes())[:2], (32, 36))
|
||||
verdict_path = directory/'independent-verdict.json'
|
||||
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set())
|
||||
verdict = dict(manifest_sha256=digest, reviewer='independent-opus-direct-image',
|
||||
rows=[dict(row_index=0, category=True, state=True, position=True)])
|
||||
for change in ('wrong_sha', 'missing_row', 'extra_row', 'rejected_state'):
|
||||
bad = copy.deepcopy(verdict)
|
||||
if change == 'wrong_sha': bad['manifest_sha256'] = '0'*64
|
||||
if change == 'missing_row': bad['rows'] = []
|
||||
if change == 'extra_row': bad['rows'] *= 2
|
||||
if change == 'rejected_state': bad['rows'][0]['state'] = False
|
||||
verdict_path.write_text(json.dumps(bad))
|
||||
with self.subTest(change=change), self.assertRaises(ValueError):
|
||||
check.reviewed_pixel_categories(manifest, digest, verdict_path)
|
||||
verdict_path.write_text(json.dumps(verdict))
|
||||
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), {'staff'})
|
||||
manifest['mechanical_pass'] = False
|
||||
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||