Three Changes Done, but the Space Festival Report Is Wrong
Goal
You observe the approval, the audit, and the current rows of the space festival cancellation job, and build a report in which the customer explanation and the machine verdict share the same evidence. You connect read-only collection with publishing that preserves the existing file.
Why it matters
The same count, a hash that passed, and a long report sentence are not evidence of the right targets and the current state. First study Python dicts, lists, exceptions, and files, SQL transactions, and the earlier approved-version and chunk-resume lessons. The expected time is 130 minutes, so extend with +time before it expires. You must finish within 180 minutes at most, and files disappear when the session ends. Keep the code you need separately.
Environment and common contract
The deliverable is /root/evidence/worker.py. PostgreSQL 16, psycopg 3.2.3, and Python 3 are in the image. You write to /root as the postgres user with no additional installation, network, or permissions.
The grader creates the tables below and fictional data in a unique temporary schema of the local labdb and cleans up only the schema it created. Use the search_path and the DSN of the con you are given, and do not modify public or a production DB. The table and column names are the fixed contract below, and SQL data values are passed as parameters. Do not hardcode customers, IDs, observation times, or the schema.
CREATE TABLE orders(id integer PRIMARY KEY,tenant text NOT NULL,
qty integer NOT NULL CHECK(qty BETWEEN 1 AND 1000),
state text NOT NULL CHECK(state IN ('pending','paid','cancelled')),
revision integer NOT NULL CHECK(revision>=0));
CREATE TABLE changes(change_id text PRIMARY KEY,tenant text NOT NULL,targets jsonb NOT NULL);
CREATE TABLE audit(change_id text REFERENCES changes(change_id),id integer NOT NULL,
previous_revision integer NOT NULL,new_revision integer NOT NULL,qty integer NOT NULL,
PRIMARY KEY(change_id,id));
change_id and tenant are exact str values of 1–64 ASCII letters, digits, underscores, and hyphens. id is an exact int 1–2147483647, qty is an exact int 1–1000, and the approved revision is an exact int 0–2147483646. The current and audit revision, previous_revision, and new_revision are exact ints 0–2147483647. bool is not allowed as an integer.
targets is an exact list of 1–64 items, each an exact dict that has only the three keys id, revision, and qty. audit is a list of exact dicts with the keys id, previous_revision, new_revision, and qty, and current is a list of exact dicts with the keys id, tenant, qty, state, and revision, each with 0–64 items. Duplicate IDs within each list are forbidden. The state of current is one of pending, paid, and cancelled, and an ID outside the approval is forbidden. An ID in audit that is outside the approval is not a format error but an anomaly to analyze. When normalizing, lists become deep copies in ID order and the input is not changed.
The borrowed con is autocommit=True and Read Committed and is IDLE at the start of a call. capture does not close it, and after success or failure it restores the original isolation level, read-only setting, and lock and statement limits and returns to IDLE. Do not wrap it in another outer transaction. Only publish owns a connection. When fault is present, call it with the string given in each step, and do not hide the original error.
Evidence and classification format
evidence is an exact dict that has only eight keys: format, change_id, tenant, targets, audit, current, snapshot, and observed_at. format is the int 1, not a bool. snapshot is the text of PostgreSQL pg_current_snapshot(), at most 4096 characters, in the form decimal:decimal:an optional comma-separated list of decimals. observed_at is a UTC ISO string ending in Z, the same form as Python datetime.isoformat() output in UTC. Example: 2026-09-13T01:02:03.123456Z. It is converted from the DB's transaction_timestamp() and does not mean an officially certified time.
The targets of capture are normalized with plan. Put only the audit for that change_id into audit, and only the current rows that correspond to approved IDs into current. Even if a current row has changed to a different customer, you have to observe that ID to see the difference. You do not fix or delete an original audit because it is damaged. You do not mix in the audit of another change or orders outside the approval.
All the ID lists of analyze are in ascending order.
- approved: all the originally approved IDs.
- committed: an approved ID whose audit has previous_revision = the approved revision, new_revision = the approved revision+1, and qty = the approved qty, all matching.
- invalid_audit: an ID outside the approval, or an audit ID that differs in any of the above values. It is not put in committed.
- remaining: the IDs of approved minus committed. It does not mean it can be rerun.
- matching: a committed ID whose current row matches in customer, qty, cancelled, and revision = the approved revision+1.
- missing: a committed ID whose current row does not exist.
- drifted: a committed ID whose row exists but is not matching.
- decision: hold if there is even one of invalid_audit, missing, or drifted; otherwise incomplete if there is a remaining; and otherwise complete. This is the verdict at the observation time.
Customer explanation and bundle contract
describe returns plain text in the following order from the results of normalize and analyze alone. Each line ends with LF, and there is also one LF after the last line. Put the actual values in the bracketed places. The IDs are comma-separated with no spaces, and an empty list is written as the Korean word for "none".
변경: [change_id] / 고객: [tenant]
관측: [observed_at] / 스냅샷: [snapshot]
판정: [hold=보류, incomplete=미완료, complete=관측 시점 완료]
승인 ID: [approved]
확정 ID: [committed]
미확정 ID: [remaining]
현재 일치 ID: [matching]
후속 차이 ID: [drifted]
누락 ID: [missing]
감사 불일치 ID: [invalid_audit]
범위: 일관된 과거 관측이며 현재 상태·출처 인증을 보장하지 않습니다.
A bundle has two keys, payload and sha256. payload has five keys: format=1, scope=historical-snapshot-not-current-state-or-authenticity, the normalized evidence, the analysis report, and the plain-text summary. sha256 is a lowercase 64-digit hexadecimal of the canonical bytes of the payload. The verifier recomputes the payload from evidence and compares not only the hash but the data, classification, explanation, and scope in full. A True for the format version is not accepted as 1. The fact that a bundle consistently rebuilt with a different observation time can pass is the limit of this approach's authenticity certification.
File output contract
The output path is an exact str absolute path and a regular file location inside an existing directory owned by the user. A nonexistent file is allowed, but a symbolic link, a directory, or a relative path is a ValueError. It does not create the parent directory automatically. The grader gives a temporary output folder it made as an argument, so do not hardcode the output path either. If a student tests directly, use their own folder such as /root/evidence/report.json.
You write the finished bytes to a unique temporary file in the same folder as the destination, and replace after flush, fsync, and close. On an ordinary exception, it cleans up the temporary file and preserves the existing or the new finished version. It is not a filesystem security boundary that covers a forced kill, a power failure, or an attack in which another process swaps the parent path. Use it only in a trusted folder that you own.
Steps
- Fix the meaning of the approval list — implement a Conflict that subclasses Exception and plan(change_id,tenant,targets). Validate the common input contract and return a new dict that has only change_id, tenant, and targets. targets is a deep copy in ascending ID order, and the input is not changed. A wrong input is a ValueError.
- Collect so that different points in time do not get mixed — capture(con,change_id,fault=None) validates the ID and then applies REPEATABLE READ READ ONLY and the lock 500ms and statement 2000ms limits in its own transaction. It collects, in order, the matching approval in changes, the audit for that change_id, and the current orders that correspond to the approved IDs. After each query it calls the after-plan, after-audit, and after-current hooks. It puts in the pg_current_snapshot() notation and the UTC string of transaction_timestamp() from the same observation and returns the evidence format below. A missing approval or a damaged stored approval format is a Conflict.
- Check the evidence's format and duplicates — normalize(evidence) validates the exact keys, types, ranges, and observation notation below and returns a deep copy with targets, audit, and current sorted in ID order. A duplicate ID, a current outside the approval, or an unknown state is a ValueError. An audit outside the approval is not deleted and is preserved so that it can be analyzed afterward.
- Separate the past commit and the present difference — after normalize, analyze(evidence) returns ascending ID lists of approved, committed, remaining, matching, drifted, missing, and invalid_audit and the decision, according to the classification contract below. It does not change the input. It does not just count the audit rows or automatically restore current values.
- Build the customer explanation and the machine result from the same evidence — canonical(value) returns JSON bytes with UTF-8, ensure_ascii=False, sort_keys=True, separators=(comma,colon), and allow_nan=False. describe(evidence) explains the analysis result in the plain-text format below. bundle(evidence) puts the normalized evidence, the report from analyze, and the summary from describe into the payload, and puts the lowercase SHA-256 of the payload's canonical bytes into sha256.
- Reject even a false conclusion whose hash was recomputed — verify_bundle(value) validates the exact bundle structure, format, scope, and hash notation and checks the payload's hash. It rebuilds the bundle from evidence, compares the whole canonical bytes with the existing bundle, and if they match, returns a new dict of the analysis report. A mismatch of bytes, conclusion, or explanation is a Conflict, and a wrong format is a ValueError. decode(raw) interprets exact bytes of 1–1048576 as UTF-8 JSON and returns the result of verify_bundle, and rejects duplicate keys, non-finite constants, truncated JSON, and invalid UTF-8 with ValueError.
- Make sure a failed file replacement does not erase the previous result — export(path,evidence,fault=None) validates the output path and first completes the canonical bundle as bytes of at most 1MiB. It writes to a separate temporary file in the same directory as the destination, closes after flush and fsync, and calls after-write. After it replaces the destination with os.replace, it calls after-replace and returns the bundle's sha256. An error before the replacement preserves the existing file, and an error after the replacement preserves the new finished version. In both the normal case and an ordinary exception, it cleans up only its own temporary file and propagates the original error.
- Connect everything from the DB observation to publishing the report — publish(dsn,change_id,path,fault=None) validates the ID and the output path before creating a connection. It opens an owned connection with psycopg.connect(dsn,autocommit=True,connect_timeout=2), runs capture, and closes it on both success and failure. After a successful collection, with the DB connection closed, it calls export and returns the sha256. It propagates the collection and publishing hooks and errors as they are, and does no automatic retry or business DB modification.
Notes
- Self-diagnosis: python3 -B /opt/lab/fixtures/evidence/check.py 8 /root/evidence/worker.py. If you change the number to the current step, it checks cumulatively up to that step. One grading has a 40-second limit.
- It checks another connection's commit between observations, the rejection of a write under READ ONLY, the same count with a different ID, a later revision, rehashing of a false conclusion, and errors before and after a file replacement.
- Do not eval the JSON. You can reject duplicate keys with object_pairs_hook and NaN and similar with parse_constant. UTF-8 decode errors and JSON parse errors are also in the ValueError family.
- Being read-only does not create authentication or per-customer access permissions. Collect only the fictional data that you are allowed, and do not use real personal information or a production DB or deliver to an external customer.
- This lab does not verify a DB server shutdown, a forced kill of the file process, or power failure durability. It verifies the internal consistency of the snapshot, the classification contract, and file preservation on an ordinary exception.
Fix the meaning of the approval list
Implement a Conflict that subclasses Exception and plan(change_id,tenant,targets). Validate the common input contract and return a new dict that has only change_id, tenant, and targets. targets is a deep copy in ascending ID order, and the input is not changed. A wrong input is a ValueError.
Even with the same count, it may not be the same set of IDs. Do not accept a bool as an int.
Collect so that different points in time do not get mixed
capture(con,change_id,fault=None) validates the ID and then applies REPEATABLE READ READ ONLY and the lock 500ms and statement 2000ms limits in its own transaction. It collects, in order, the matching approval in changes, the audit for that change_id, and the current orders that correspond to the approved IDs. After each query it calls the after-plan, after-audit, and after-current hooks. It puts in the pg_current_snapshot() notation and the UTC string of transaction_timestamp() from the same observation and returns the evidence format below. A missing approval or a damaged stored approval format is a Conflict.
Another connection really commits between the hooks. If you bind several SELECTs with BEGIN alone, the points in time get mixed under the default isolation level.
Check the evidence's format and duplicates
normalize(evidence) validates the exact keys, types, ranges, and observation notation below and returns a deep copy with targets, audit, and current sorted in ID order. A duplicate ID, a current outside the approval, or an unknown state is a ValueError. An audit outside the approval is not deleted and is preserved so that it can be analyzed afterward.
Distinguish a format error from a business mismatch. An audit outside the approval is not material to hide but material to report.
Separate the past commit and the present difference
After normalize, analyze(evidence) returns ascending ID lists of approved, committed, remaining, matching, drifted, missing, and invalid_audit and the decision, according to the classification contract below. It does not change the input. It does not just count the audit rows or automatically restore current values.
The audit is the basis of the past commit, and the current row is the material for seeing later differences. Even if the state is the same, the customer, quantity, and revision can differ.
Build the customer explanation and the machine result from the same evidence
canonical(value) returns JSON bytes with UTF-8, ensure_ascii=False, sort_keys=True, separators=(comma,colon), and allow_nan=False. describe(evidence) explains the analysis result in the plain-text format below. bundle(evidence) puts the normalized evidence, the report from analyze, and the summary from describe into the payload, and puts the lowercase SHA-256 of the payload's canonical bytes into sha256.
If the translation, whitespace, or key order changes, the hash changes too. Do not call the normalization rules of this format a guarantee of the whole JSON standard.
Reject even a false conclusion whose hash was recomputed
verify_bundle(value) validates the exact bundle structure, format, scope, and hash notation and checks the payload's hash. It rebuilds the bundle from evidence, compares the whole canonical bytes with the existing bundle, and if they match, returns a new dict of the analysis report. A mismatch of bytes, conclusion, or explanation is a Conflict, and a wrong format is a ValueError. decode(raw) interprets exact bytes of 1–1048576 as UTF-8 JSON and returns the result of verify_bundle, and rejects duplicate keys, non-finite constants, truncated JSON, and invalid UTF-8 with ValueError.
Anyone can recompute the hash of a tampered conclusion as well. Even if the internal comparison passes, it is neither authentication of the origin nor a check of the current DB.
Make sure a failed file replacement does not erase the previous result
export(path,evidence,fault=None) validates the output path and first completes the canonical bundle as bytes of at most 1MiB. It writes to a separate temporary file in the same directory as the destination, closes after flush and fsync, and calls after-write. After it replaces the destination with os.replace, it calls after-replace and returns the bundle's sha256. An error before the replacement preserves the existing file, and an error after the replacement preserves the new finished version. In both the normal case and an ordinary exception, it cleans up only its own temporary file and propagates the original error.
If you open the destination in write mode first, the previous report gets truncated. Finish it in the same directory and then replace once.
Connect everything from the DB observation to publishing the report
publish(dsn,change_id,path,fault=None) validates the ID and the output path before creating a connection. It opens an owned connection with psycopg.connect(dsn,autocommit=True,connect_timeout=2), runs capture, and closes it on both success and failure. After a successful collection, with the DB connection closed, it calls export and returns the sha256. It propagates the collection and publishing hooks and errors as they are, and does no automatic retry or business DB modification.
Do not hold the snapshot while waiting for the file work. Even if the conclusion of the stored report and the new observation differ, do not force either one to match.