From f25c94f2d64d7ae2a0d31e52e99fa216672b8ce4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 05:55:18 +0000 Subject: [PATCH 1/2] docs: sample run demonstrates the #215 re-upload fix as check 13 Replaces the stale 'known defect, tracked as #215' limitation, which #220 made untrue, with a check: re-upload the T&E file, where every row now collides, and require status error, an unchanged registry record and an unchanged table. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011T6Wqm1jLdq3ZEfuKdp9aM --- scripts/sample_verification_run.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/scripts/sample_verification_run.py b/scripts/sample_verification_run.py index bb49868..d49e2a3 100644 --- a/scripts/sample_verification_run.py +++ b/scripts/sample_verification_run.py @@ -100,7 +100,7 @@ def collect(api: Api, conn) -> dict: schema, table = resp["tableName"].split(".") cur.execute(f'SELECT name, org_type, country FROM "{schema}"."{table}" ORDER BY name') db_rows = [list(r) for r in cur.fetchall()] - cur.execute(f'SELECT file_hash, row_count, total_rows, failed_rows FROM "{schema}".csv_files WHERE file_name=%s', + cur.execute(f'SELECT file_hash, row_count, total_rows, failed_rows, id FROM "{schema}".csv_files WHERE file_name=%s', ("orgs_sample.csv",)) reg = cur.fetchone() expected_loaded = sorted(r for i, r in enumerate(src_rows, 1) if i not in EXPECTED_REJECTS) @@ -108,7 +108,7 @@ def collect(api: Api, conn) -> dict: "file": "orgs_sample.csv", "bytes": len(orgs_bytes), "sha256": hashlib.sha256(orgs_bytes).hexdigest(), "source_rows": len(src_rows), "source_lines": {i: ",".join(r) for i, r in enumerate(src_rows, 1)}, "api": a, "target": resp["tableName"], "db_row_count": len(db_rows), "db_rows": db_rows, - "registry": {"file_hash": reg[0], "row_count": reg[1], "total_rows": reg[2], "failed_rows": reg[3]}, + "registry": {"file_hash": reg[0], "row_count": reg[1], "total_rows": reg[2], "failed_rows": reg[3], "id": reg[4]}, "expected_rejects": EXPECTED_REJECTS, "rejected_row_numbers": sorted(e["rowNumber"] for e in resp.get("rowErrors", [])), "loaded_matches_source": sorted(db_rows) == expected_loaded, @@ -134,6 +134,14 @@ def collect(api: Api, conn) -> dict: "duplicate_renamed": upload(api, "equipment_register_copy.csv", eq, "dynamic")["response"], } + # --- Re-upload of the T&E file (#215): every row now collides, so nothing may change -- + again = upload(api, "orgs_sample.csv", orgs, "te", "organisations")["response"] + cur.execute(f'SELECT id, row_count, failed_rows FROM "{schema}".csv_files WHERE file_name=%s', ("orgs_sample.csv",)) + after = cur.fetchall() + cur.execute(f'SELECT count(*) FROM "{schema}"."{table}"') + ev["te_reupload"] = {"status": again.get("status"), "inserted": again.get("insertedRows"), + "registry_after": [list(r) for r in after], "db_row_count_after": cur.fetchone()[0]} + # --- Observed deployment posture (read from the running system, not from our env) -- unauth = urllib.request.Request(api.base + "/api/csv/files") try: @@ -188,10 +196,17 @@ def checks(ev: dict) -> list[tuple[str, str, bool]]: ("Dynamic: the same content under a new filename is refused", f"{dy['duplicate_renamed'].get('status')} (reason: {dy['duplicate_renamed'].get('reason')})", dy["duplicate_renamed"].get("status") == "duplicate_file" and dy["duplicate_renamed"].get("reason") == "content"), - ("Audit: one entry per successful load; refused duplicates leave none", + ("Audit: one entry per successful load; refused or fully failed uploads leave none", f"{au['log']['total']} entries for 2 loads", au["log"]["total"] == 2 and len(entries) == 2), ("Audit: hash chain recomputed and intact", f"intact={au['verify'].get('intact')}, rows checked={au['verify'].get('rows')}", au["verify"].get("intact") is True), + ("T&E: re-uploading the same file, where every row now fails, is refused and changes nothing", + f"status {ev['te_reupload']['status']}; registry (id, loaded, failed) {ev['te_reupload']['registry_after']} " + f"vs before {[[te['registry']['id'], te['registry']['row_count'], te['registry']['failed_rows']]]}; " + f"database rows {ev['te_reupload']['db_row_count_after']}", + ev["te_reupload"]["status"] == "error" + and ev["te_reupload"]["registry_after"] == [[te["registry"]["id"], te["registry"]["row_count"], te["registry"]["failed_rows"]]] + and ev["te_reupload"]["db_row_count_after"] == te["db_row_count"]), ] @@ -273,7 +288,7 @@ def render(ev: dict, rows: list[tuple[str, str, bool]]) -> str: "## 8. What this report does and does not show", "", "**Shows:** for these two files, every source row is either in the database or rejected with the database's own reason; " - "loaded values equal source values; file hashes are recorded correctly; duplicates are refused; the audit trail has one entry per successful load and its hash chain verifies.", + "loaded values equal source values; file hashes are recorded correctly; duplicates are refused; a re-upload that fails every row changes nothing; the audit trail has one entry per successful load and its hash chain verifies.", "", "**Does not show:**", "", @@ -285,7 +300,8 @@ def render(ev: dict, rows: list[tuple[str, str, bool]]) -> str: f"{', so the database itself would not stop the API altering the audit log. The audit chain is designed to reveal such changes, but this run did not test that' if po['audit_log_owner'] in po['database_roles_connected'] else ''}.", "- Independent assurance. The checks were written and run by the system's developers. They are repeatable by a reviewer, not a third-party assessment.", "- Conformance to any Defence standard. No claim of alignment with ASDEFCON or any DID is made here.", - "- That re-uploading a file is safe. The re-upload case for T&E mode has a known defect, tracked as #215.", + "- Re-upload behaviour beyond check 13. Only one case was exercised: re-uploading an identical file, where every row fails. " + "Overwriting a file with changed content, and concurrent uploads, were not exercised here.", "", "**Reproduce:** deploy the dev schema to an empty database, create a tenant admin, start the API, then run " "`python scripts/sample_verification_run.py`. The raw responses behind this report are in " From 79ff7462953bbeb90831dd8b3ba2fc422117c6e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 05:56:01 +0000 Subject: [PATCH 2/2] docs: regenerate the sample verification report on current main (13 of 13) Re-run on a fresh PostgreSQL 16 database at code version f25c94f. The report no longer lists the #215 re-upload defect as a known limitation, since #220 fixed it; check 13 now demonstrates the fix. Run against the pre-#220 loader, check 13 fails with the #215 symptom (registry 6 loaded / 4 failed replaced by 0 / 10, status ok), and the audit check fails too (3 entries for 2 loads). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011T6Wqm1jLdq3ZEfuKdp9aM --- .../MIGRATION_VERIFICATION_REPORT_SAMPLE.md | 23 +++++----- docs/samples/verification-run-evidence.json | 43 +++++++++++++------ 2 files changed, 43 insertions(+), 23 deletions(-) diff --git a/docs/samples/MIGRATION_VERIFICATION_REPORT_SAMPLE.md b/docs/samples/MIGRATION_VERIFICATION_REPORT_SAMPLE.md index b868591..706bbb0 100644 --- a/docs/samples/MIGRATION_VERIFICATION_REPORT_SAMPLE.md +++ b/docs/samples/MIGRATION_VERIFICATION_REPORT_SAMPLE.md @@ -4,15 +4,15 @@ | | | |---|---| -| Run started (UTC) | 2026-11-09T09:07:53+00:00 | -| Run finished (UTC) | 2026-11-09T09:07:54+00:00 | -| Code version | `01602c18eb` (clean working tree) | +| Run started (UTC) | 2026-11-10T05:55:31+00:00 | +| Run finished (UTC) | 2026-11-10T05:55:32+00:00 | +| Code version | `f25c94f2d6` (clean working tree) | | Database | PostgreSQL 16.15 (Ubuntu 16.15-0ubuntu0.24.04.1) | -| Platform | Python 3.13.18, Linux-6.18.44-fc-v310-x86_64-with-glibc2.39 | +| Platform | Python 3.13.18, Linux-6.18.44-fc-v338-x86_64-with-glibc2.39 | ## 1. Result -**RECONCILED**: 12 of 12 checks passed. Of 10 source rows in the T&E file, **6 were loaded and 4 were rejected by the database**, for the reasons in section 4; nothing was lost or silently altered. The 8-row dynamic file loaded in full. +**RECONCILED**: 13 of 13 checks passed. Of 10 source rows in the T&E file, **6 were loaded and 4 were rejected by the database**, for the reasons in section 4; nothing was lost or silently altered. The 8-row dynamic file loaded in full. ## 2. Source files @@ -37,8 +37,9 @@ Each check compares the system's output against the database itself, a hash comp | 8 | Dynamic: file hash recorded by the system equals an independently computed SHA-256 | 14fab9c3bfb55400… | PASS | | 9 | Dynamic: re-upload of the same file is refused | duplicate_file | PASS | | 10 | Dynamic: the same content under a new filename is refused | duplicate_file (reason: content) | PASS | -| 11 | Audit: one entry per successful load; refused duplicates leave none | 2 entries for 2 loads | PASS | +| 11 | Audit: one entry per successful load; refused or fully failed uploads leave none | 2 entries for 2 loads | PASS | | 12 | Audit: hash chain recomputed and intact | intact=True, rows checked=2 | PASS | +| 13 | T&E: re-uploading the same file, where every row now fails, is refused and changes nothing | status error; registry (id, loaded, failed) [[1, 6, 4]] vs before [[1, 6, 4]]; database rows 6 | PASS | ## 4. Rejected rows (T&E file) @@ -66,7 +67,7 @@ Read back from `tenant_dev.organisations` after the load (6 rows): ## 6. Dynamic mode and duplicate handling -`equipment_register.csv` created table `csv_14fab9c3bfb55400` with 8 rows. Column types detected: +`equipment_register.csv` created table `csv_equipment_register_v1` with 8 rows. Column types detected: | Column | PostgreSQL type | |---|---| @@ -84,12 +85,12 @@ Re-uploading the same file returned `duplicate_file` (reason: `content`); upload | Action | File | Target | Mode | Actor | Time (UTC) | |---|---|---|---|---|---| -| upload | equipment_register.csv | csv_14fab9c3bfb55400 | dynamic | admin | 2026-11-09T09:07:54.050344+00:00 | -| upload | orgs_sample.csv | tenant_dev.organisations | te | admin | 2026-11-09T09:07:54.013017+00:00 | +| upload | equipment_register.csv | csv_equipment_register_v1 | dynamic | admin | 2026-11-10T05:55:32.044474+00:00 | +| upload | orgs_sample.csv | tenant_dev.organisations | te | admin | 2026-11-10T05:55:31.982480+00:00 | ## 8. What this report does and does not show -**Shows:** for these two files, every source row is either in the database or rejected with the database's own reason; loaded values equal source values; file hashes are recorded correctly; duplicates are refused; the audit trail has one entry per successful load and its hash chain verifies. +**Shows:** for these two files, every source row is either in the database or rejected with the database's own reason; loaded values equal source values; file hashes are recorded correctly; duplicates are refused; a re-upload that fails every row changes nothing; the audit trail has one entry per successful load and its hash chain verifies. **Does not show:** @@ -97,6 +98,6 @@ Re-uploading the same file returned `duplicate_file` (reason: `content`); upload - A hardened deployment. Observed during this run: a request with no credentials at all returned HTTP **200** (the API key is not set, so unauthenticated access is open); the API's database connections used role(s) **postgres** and the audit table is owned by **postgres**, so the database itself would not stop the API altering the audit log. The audit chain is designed to reveal such changes, but this run did not test that. - Independent assurance. The checks were written and run by the system's developers. They are repeatable by a reviewer, not a third-party assessment. - Conformance to any Defence standard. No claim of alignment with ASDEFCON or any DID is made here. -- That re-uploading a file is safe. The re-upload case for T&E mode has a known defect, tracked as #215. +- Re-upload behaviour beyond check 13. Only one case was exercised: re-uploading an identical file, where every row fails. Overwriting a file with changed content, and concurrent uploads, were not exercised here. **Reproduce:** deploy the dev schema to an empty database, create a tenant admin, start the API, then run `python scripts/sample_verification_run.py`. The raw responses behind this report are in `docs/samples/verification-run-evidence.json`. diff --git a/docs/samples/verification-run-evidence.json b/docs/samples/verification-run-evidence.json index 730d959..f4a5ebc 100644 --- a/docs/samples/verification-run-evidence.json +++ b/docs/samples/verification-run-evidence.json @@ -1,5 +1,5 @@ { - "started_utc": "2026-11-09T09:07:53+00:00", + "started_utc": "2026-11-10T05:55:31+00:00", "health": { "status": "ok", "database": "te_mgmt_dev", @@ -8,10 +8,10 @@ "uploads_schema": "csv_uploads" }, "environment": { - "code_commit": "01602c18eb10e4888fd9debb0c4d7fd7c573c008", + "code_commit": "f25c94f2d64d7ae2a0d31e52e99fa216672b8ce4", "tracked_files_modified": false, "python": "3.13.18", - "os": "Linux-6.18.44-fc-v310-x86_64-with-glibc2.39" + "os": "Linux-6.18.44-fc-v338-x86_64-with-glibc2.39" }, "te": { "file": "orgs_sample.csv", @@ -118,7 +118,8 @@ "file_hash": "593c2d41f415f178193b97643b7a5beb373f1c490d58b46ad27cf8ca3ec0a09b", "row_count": 6, "total_rows": 10, - "failed_rows": 4 + "failed_rows": 4, + "id": 1 }, "expected_rejects": { "6": "duplicate name (unique constraint)", @@ -155,7 +156,7 @@ "response": { "status": "ok", "fileId": "2", - "tableName": "csv_14fab9c3bfb55400", + "tableName": "csv_equipment_register_v1", "totalRows": 8, "insertedRows": 8, "duplicateRowsSkipped": 0, @@ -174,13 +175,19 @@ "date", "int8" ], + "typesProvenance": "inferred_unconfirmed", "rowErrors": [], + "dataset": "equipment_register", + "version": 1, + "schemaCheck": null, + "duplicateRowsAcrossUploads": 0, + "crossUploadDuplicates": [], "overwritten": false, "replacedFileName": null, "uploadId": "2" } }, - "table": "csv_14fab9c3bfb55400", + "table": "csv_equipment_register_v1", "db_row_count": 8, "db_columns": [ [ @@ -209,7 +216,7 @@ "status": "duplicate_file", "reason": "content", "existingFileName": "equipment_register.csv", - "tableName": "csv_14fab9c3bfb55400", + "tableName": "csv_equipment_register_v1", "existingRowCount": 8, "uploadId": "3" }, @@ -217,11 +224,23 @@ "status": "duplicate_file", "reason": "content", "existingFileName": "equipment_register.csv", - "tableName": "csv_14fab9c3bfb55400", + "tableName": "csv_equipment_register_v1", "existingRowCount": 8, "uploadId": "4" } }, + "te_reupload": { + "status": "error", + "inserted": 0, + "registry_after": [ + [ + 1, + 6, + 4 + ] + ], + "db_row_count_after": 6 + }, "posture": { "unauthenticated_request_status": 200, "database_roles_connected": [ @@ -240,9 +259,9 @@ "action": "upload", "fileId": 2, "fileName": "equipment_register.csv", - "tableName": "csv_14fab9c3bfb55400", + "tableName": "csv_equipment_register_v1", "mode": "dynamic", - "performedAt": "2026-11-09T09:07:54.050344+00:00", + "performedAt": "2026-11-10T05:55:32.044474+00:00", "actor": "admin" }, { @@ -252,7 +271,7 @@ "fileName": "orgs_sample.csv", "tableName": "tenant_dev.organisations", "mode": "te", - "performedAt": "2026-11-09T09:07:54.013017+00:00", + "performedAt": "2026-11-10T05:55:31.982480+00:00", "actor": "admin" } ] @@ -263,5 +282,5 @@ "firstBadId": null } }, - "finished_utc": "2026-11-09T09:07:54+00:00" + "finished_utc": "2026-11-10T05:55:32+00:00" }