TT Lab
Get started
Learn Learning paths Courses

Irreversible Changes

The Festival Is Cancelled, but the Done Button Is Red

Continue in TT Lab

Goal

You write a coordinator that receives a change request for the space festival and connects approval, execution, observation, reporting, and a separate compensation. You confirm, with a real DB and CLI, the incidents that arise when the SQL operations are right but the coordinator is wrong.

Why it matters

Do this after finishing the earlier approved-version, compensation, and evidence-report lessons. You need Python functions, dicts, lists, exceptions, JSON, and CLI usage. This time you do not rewrite all the DB code you already learned, and instead combine the provided operations. The expected time is 140 minutes, so extend with +time before it expires. The maximum is 180 minutes, and files disappear when the session ends. Keep the code you need separately.

Environment and deliverable

The deliverable is /root/change-capstone/coordinator.py. PostgreSQL 16, psycopg 3.2.3, and Python 3 are in the image, and no additional installation, network, or permission is needed. The student writes to /root as the postgres user.

The provided adapter is Operations(dsn) in /opt/lab/fixtures/change_capstone/operations.py. revision_ops.py, compensation_ops.py, and evidence_ops.py in the same folder are libraries generated from the cumulative answers of the previous three units. You may read them, and they do not contain the answer for this coordinator. The grader creates fictional order, approval, audit, and compensation tables in a UUID temporary schema of the local labdb and cleans up only the schema it created. Use the ops, DSN, and output path it passes, and do not modify public or a production DB.

The exact format of a request

Every request is an exact dict and allows only the keys below for each action.

action is exactly one English str for that action. change_id, tenant, and undo_id are exact str values of 1–64 ASCII letters, digits, underscores, and hyphens. The approved of apply and undo allows only a real True, and 1, strings, and False are rejected. This flag is not authentication or a signature but the input contract of the fictional business.

ids is an exact list of 1–16 exact ints 1–2147483647, normalized to ascending order with no duplicates. targets is a list of 1–16 exact dicts that have only the three keys id, revision, and qty. The id range is the same as above, revision is an exact int 0–2147483646, and qty is an exact int 1–1000. A bool is not accepted as an integer. Duplicate IDs are forbidden, and you make a deep copy in ID order. No function changes the original request.

reason is an exact str of 1–200 characters and forbids leading or trailing whitespace and the control characters U+0000–001F and U+007F. report_path is an exact str absolute path and a regular file location inside an existing owned directory. A nonexistent file is allowed, but a symbolic link, a directory, or a relative path is rejected, and the parent is not created automatically. It assumes a trusted folder that you own, and it is not a contract that blocks every parent-path replacement attack.

The provided operation contract

Each call opens and cleans up its own DB connection. The coordinator does not wrap them in an outer transaction or close connections separately. You do not hardcode values, IDs, or paths, and you do not reimplement the SQL.

The grading also provides stand-ins in which each method raises an Exception or returns something outside the contract. Only operation errors after a verified request has been passed on are classified into the state of each step. It does not mean you should catch the whole BaseException family such as KeyboardInterrupt and SystemExit. ops.Conflict and the student's Conflict are separate classes.

The coordination result and the CLI contract

The dispatch result of apply has five keys: change_id, execution, observed, report, and sha256. execution is one of applied, replayed, conflict, and uncertain, and you do not erase or change it with the observation result. observed is complete, incomplete, hold, unknown, mismatch, or not_checked. report is saved, failed, or not_attempted, and when saving failed or was not attempted, sha256=None.

The preview dispatch returns only an apply proposal with approved=False. The report dispatch returns three keys, change_id and the result of the report function, and the undo dispatch returns three keys, including the result of compensate. In dispatch, validate the request first and respect the boundaries of each action. Even if it is applied, if the observation is unknown, it does not attempt the report, and even if it is replayed, if the observation is hold, it may report that result as it is.

decode(raw) interprets exact bytes of 1–65536 as UTF-8 JSON and returns the validated request. Duplicate keys, non-finite constants, truncated JSON, invalid UTF-8, and oversized input are ValueError. main(argv,ops) takes only one request file path, reads at most 65537 bytes, and runs decode then dispatch. On success it prints one line of result JSON to stdout and returns 0. On an argument, read, parse, or processing Exception, it returns one line {"error":"request_failed"} and 2, and does not print the DSN, the original exception, or a traceback.

When run as a script, add /opt/lab/fixtures/change_capstone to sys.path and read operations.Operations. Build Operations with the local-only DSN in the environment variable LABHUB_CAPSTONE_DSN, and exit with the return value of main(sys.argv[1:],ops). This environment variable is always provided in the normal CLI verification. You must not run the CLI when it is imported as a module. CLI code 0 means the request was processed successfully and does not mean business completion.

The observation and the report collection are separate calls. observed is the earlier observation and report is the state of the later file saving, and there is no guarantee that the two calls are at the same point in time. Do not assume from saved alone that the business conclusion inside the file is complete. Check separately the actual observation time and the classification of the report.

Steps

  1. Accept only explicit approvals as input — implement Conflict(Exception) and validate(value). Check the exact keys, types, and ranges for each action below and return a deep copy with the lists sorted. apply and undo are allowed only when approved is a real True, and an error is ValueError.
  2. Do not automatically approve a preview — preview(value,ops) validates a preview request and calls ops.preview(tenant, the sorted ids) once. It validates and sorts the targets it receives and checks whether the ID list is exactly the same as the request. If it differs, it is a Conflict. If it is the same, it returns a proposal dict with action=apply, the original change_id, tenant, and report_path, the observed targets, and approved=False.
  3. Tell apart a different approval under the same ID — compare(value,observation) validates an apply request. If observation is None, it returns unknown. Otherwise, it checks whether change_id, tenant, and the normalized targets in the observation format below are the same as the request, and if they differ, it is a Conflict. If they are the same, it returns the complete, incomplete, or hold of report.decision as it is. Another top-level key or an unknown verdict is a Conflict.
  4. Separate the execution response from an unknown outcome — execute(value,ops) validates an apply request and calls ops.apply(change_id,tenant,targets) exactly once. True is applied, False is replayed, ops.Conflict is conflict, and any other Exception or a return that is not a bool is uncertain. A validation error outside the call is propagated as it is.
  5. Observe the committed record to expose the uncertainty — settle(value,ops,outcome) validates the apply request and the four execution result values. If it is conflict, it is observed=not_checked without observe. Otherwise it calls ops.observe(change_id) once and decides observed with compare. A lookup Exception is unknown, and a comparison's Conflict, ValueError, KeyError, or TypeError is mismatch. It returns a dict of change_id, execution=the original outcome, and observed.
  6. Regenerate the report without touching the business — report(value,ops) allows only an apply or report request. It calls ops.publish(change_id,report_path) once, and if it is a lowercase 64-digit SHA-256 str, it returns a dict with report=saved and sha256=that value, and if the call raises an Exception or returns something wrong, a dict with report=failed and sha256=None.
  7. Execute only a separately approved compensation — compensate(value,ops) validates an undo request and then calls ops.undo(undo_id,change_id,reason) once. It classifies True, False, ops.Conflict, and any other Exception or return as applied, replayed, conflict, and uncertain, respectively. It returns a dict of change_id, undo_id, and compensation and makes no follow-up call.
  8. Connect the real JSON request and the CLI to the end — implement dispatch(value,ops), decode(raw), main(argv,ops), and the CLI entry point under the contract below. apply calls report only for a complete, incomplete, or hold observation after execute then settle. Otherwise it is report=not_attempted and sha256=None. The other three actions call only their own function. You check the whole flow with a real DB, files, and the CLI.

Notes

Accept only explicit approvals as input

Implement Conflict(Exception) and validate(value). Check the exact keys, types, and ranges for each action below and return a deep copy with the lists sorted. apply and undo are allowed only when approved is a real True, and an error is ValueError.

bool is a subtype of int. Having a truth value and giving an explicit approval are different.

Do not automatically approve a preview

preview(value,ops) validates a preview request and calls ops.preview(tenant, the sorted ids) once. It validates and sorts the targets it receives and checks whether the ID list is exactly the same as the request. If it differs, it is a Conflict. If it is the same, it returns a proposal dict with action=apply, the original change_id, tenant, and report_path, the observed targets, and approved=False.

The proposal must not be directly executable through validate. Do not call the execute, compensate, or report operations here.

Tell apart a different approval under the same ID

compare(value,observation) validates an apply request. If observation is None, it returns unknown. Otherwise, it checks whether change_id, tenant, and the normalized targets in the observation format below are the same as the request, and if they differ, it is a Conflict. If they are the same, it returns the complete, incomplete, or hold of report.decision as it is. Another top-level key or an unknown verdict is a Conflict.

The provided observe performs the evidence collection and classification of the earlier unit. The responsibility this time is to check whether that observation is the same as the current approval.

Separate the execution response from an unknown outcome

execute(value,ops) validates an apply request and calls ops.apply(change_id,tenant,targets) exactly once. True is applied, False is replayed, ops.Conflict is conflict, and any other Exception or a return that is not a bool is uncertain. A validation error outside the call is propagated as it is.

Even if you received an exception, it may have committed. Do not create a new ID, a new goal, or an automatic retry.

Observe the committed record to expose the uncertainty

settle(value,ops,outcome) validates the apply request and the four execution result values. If it is conflict, it is observed=not_checked without observe. Otherwise it calls ops.observe(change_id) once and decides observed with compare. A lookup Exception is unknown, and a comparison's Conflict, ValueError, KeyError, or TypeError is mismatch. It returns a dict of change_id, execution=the original outcome, and observed.

Do not merge a lookup failure and the fact that it is a different approval into the same state. This step does no write operation.

Regenerate the report without touching the business

report(value,ops) allows only an apply or report request. It calls ops.publish(change_id,report_path) once, and if it is a lowercase 64-digit SHA-256 str, it returns a dict with report=saved and sha256=that value, and if the call raises an Exception or returns something wrong, a dict with report=failed and sha256=None.

Do not reinterpret the execution response or compensate. Even after a saving failure, do not make additional business operation calls.

Execute only a separately approved compensation

compensate(value,ops) validates an undo request and then calls ops.undo(undo_id,change_id,reason) once. It classifies True, False, ops.Conflict, and any other Exception or return as applied, replayed, conflict, and uncertain, respectively. It returns a dict of change_id, undo_id, and compensation and makes no follow-up call.

The original approval does not imply the compensation approval. Do not automatically retry a compensation exception with a new undo_id either.

Connect the real JSON request and the CLI to the end

Implement dispatch(value,ops), decode(raw), main(argv,ops), and the CLI entry point under the contract below. apply calls report only for a complete, incomplete, or hold observation after execute then settle. Otherwise it is report=not_attempted and sha256=None. The other three actions call only their own function. You check the whole flow with a real DB, files, and the CLI.

Exit code 0 is not business completion but a successful processing of the request. Read the execution, observation, and file states of the JSON separately.