diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index 35a048e..e0f32a8 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -24,7 +24,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '24' cache: 'npm' cache-dependency-path: frontend/package-lock.json diff --git a/.gitignore b/.gitignore index 351419c..e57f482 100644 --- a/.gitignore +++ b/.gitignore @@ -53,5 +53,14 @@ terraform-provider-*.log *.zip +# Large / non-source assets and local artifacts +*.docx +*.pdf +*.png +*.ico +*.patch +*.svg +test-artifacts/ + # Serena tool workspace .serena/ diff --git a/CSV_Loading_Guide.docx b/CSV_Loading_Guide.docx deleted file mode 100644 index 55ac02f..0000000 Binary files a/CSV_Loading_Guide.docx and /dev/null differ diff --git a/Pending Items 28072026.docx b/Pending Items 28072026.docx deleted file mode 100644 index bd16bf2..0000000 Binary files a/Pending Items 28072026.docx and /dev/null differ diff --git a/PostgresDataMigration.ico b/PostgresDataMigration.ico deleted file mode 100644 index df796b2..0000000 Binary files a/PostgresDataMigration.ico and /dev/null differ diff --git a/csv-table-hub-main.zip b/csv-table-hub-main.zip deleted file mode 100644 index 44bd3e4..0000000 Binary files a/csv-table-hub-main.zip and /dev/null differ diff --git a/favicon-16.png b/favicon-16.png deleted file mode 100644 index fc2e385..0000000 Binary files a/favicon-16.png and /dev/null differ diff --git a/favicon-32.png b/favicon-32.png deleted file mode 100644 index e7c0045..0000000 Binary files a/favicon-32.png and /dev/null differ diff --git a/favicon.ico b/favicon.ico deleted file mode 100644 index fbee083..0000000 Binary files a/favicon.ico and /dev/null differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico deleted file mode 100644 index 3c01d69..0000000 Binary files a/frontend/public/favicon.ico and /dev/null differ diff --git a/frontend/src/hooks/use-local-storage.ts b/frontend/src/hooks/use-local-storage.ts index 409c850..df0ef54 100644 --- a/frontend/src/hooks/use-local-storage.ts +++ b/frontend/src/hooks/use-local-storage.ts @@ -2,7 +2,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; const KEY = "csv-migrator:jobs:v1"; -export function useLocalStorageState(defaultValue: T): [T, (updater: T | ((prev: T) => T)) => void, () => void] { +export function useLocalStorageState( + defaultValue: T, +): [T, (updater: T | ((prev: T) => T)) => void, () => void] { const [state, setState] = useState(defaultValue); const hydrated = useRef(false); diff --git a/frontend/src/lib/csv-preview.ts b/frontend/src/lib/csv-preview.ts index 1c2b4e4..6ecd0ac 100644 --- a/frontend/src/lib/csv-preview.ts +++ b/frontend/src/lib/csv-preview.ts @@ -23,18 +23,49 @@ function parseCsv(text: string): string[][] { const ch = src[i]; if (inQuotes) { if (ch === '"') { - if (src[i + 1] === '"') { field += '"'; i += 2; continue; } - inQuotes = false; i++; continue; + if (src[i + 1] === '"') { + field += '"'; + i += 2; + continue; + } + inQuotes = false; + i++; + continue; } - field += ch; i++; continue; + field += ch; + i++; + continue; } - if (ch === '"') { inQuotes = true; i++; continue; } - if (ch === ",") { row.push(field); field = ""; i++; continue; } - if (ch === "\r") { i++; continue; } - if (ch === "\n") { row.push(field); rows.push(row); row = []; field = ""; i++; continue; } - field += ch; i++; + if (ch === '"') { + inQuotes = true; + i++; + continue; + } + if (ch === ",") { + row.push(field); + field = ""; + i++; + continue; + } + if (ch === "\r") { + i++; + continue; + } + if (ch === "\n") { + row.push(field); + rows.push(row); + row = []; + field = ""; + i++; + continue; + } + field += ch; + i++; + } + if (field.length > 0 || row.length > 0) { + row.push(field); + rows.push(row); } - if (field.length > 0 || row.length > 0) { row.push(field); rows.push(row); } while (rows.length && rows[rows.length - 1].every((c) => c === "")) rows.pop(); return rows; } @@ -91,7 +122,10 @@ function pickType(candidates: Set): ColumnType { return "text"; } -export async function parseCsvPreview(file: File, opts?: { sampleRows?: number; maxBytes?: number }): Promise { +export async function parseCsvPreview( + file: File, + opts?: { sampleRows?: number; maxBytes?: number }, +): Promise { const maxBytes = opts?.maxBytes ?? 512 * 1024; // 512KB for preview const sampleCount = opts?.sampleRows ?? 10; const inferRowLimit = 200; @@ -105,8 +139,14 @@ export async function parseCsvPreview(file: File, opts?: { sampleRows?: number; if (rows.length === 0) { return { - headers: [], sanitizedHeaders: [], sampleRows: [], inferredTypes: [], - totalRowsApprox: 0, bytesRead: slice.size, bytesTotal: file.size, truncated, + headers: [], + sanitizedHeaders: [], + sampleRows: [], + inferredTypes: [], + totalRowsApprox: 0, + bytesRead: slice.size, + bytesTotal: file.size, + truncated, }; } @@ -116,7 +156,17 @@ export async function parseCsvPreview(file: File, opts?: { sampleRows?: number; const sample = dataRows.slice(0, sampleCount); // Infer types - const perCol: Set[] = headers.map(() => new Set(["int8", "numeric", "date", "timestamptz", "boolean", "text"])); + const perCol: Set[] = headers.map( + () => + new Set([ + "int8", + "numeric", + "date", + "timestamptz", + "boolean", + "text", + ]), + ); for (let r = 0; r < Math.min(dataRows.length, inferRowLimit); r++) { const row = dataRows[r]; for (let c = 0; c < headers.length; c++) { @@ -138,8 +188,14 @@ export async function parseCsvPreview(file: File, opts?: { sampleRows?: number; } return { - headers, sanitizedHeaders: sanitized, sampleRows: sample, inferredTypes, - totalRowsApprox, bytesRead: slice.size, bytesTotal: file.size, truncated, + headers, + sanitizedHeaders: sanitized, + sampleRows: sample, + inferredTypes, + totalRowsApprox, + bytesRead: slice.size, + bytesTotal: file.size, + truncated, }; } diff --git a/frontend/src/routes/_authenticated/index.tsx b/frontend/src/routes/_authenticated/index.tsx index eb23f7d..f99bbe5 100644 --- a/frontend/src/routes/_authenticated/index.tsx +++ b/frontend/src/routes/_authenticated/index.tsx @@ -30,7 +30,14 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { toast, Toaster } from "sonner"; import { diff --git a/g3tierxe.patch b/g3tierxe.patch deleted file mode 100644 index a5102b8..0000000 --- a/g3tierxe.patch +++ /dev/null @@ -1,643 +0,0 @@ -diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml -index efce425..1c24a15 100644 ---- a/.github/workflows/quality-gate.yml -+++ b/.github/workflows/quality-gate.yml -@@ -139,6 +139,9 @@ jobs: - bash build/deploy_all.sh "$env" - done - -+ - name: Evals — Tiers X, E (post-deploy) -+ run: python3 evals/runner.py --tiers x,e --verbose -+ - # Prints a final block accounting for every test: PASSED / FAILED / - # ERROR / SKIPPED (with reasons) / NOT RUN (deselected). --strict fails - # the build on any skip. -@@ -280,6 +283,9 @@ jobs: - bash build/deploy_all.sh "$env" - done - -+ - name: Evals — Tiers X, E (post-deploy) -+ run: python3 evals/runner.py --tiers x,e --verbose -+ - - name: Full test suite — final result with skip accounting - run: python3 scripts/test_report.py --strict - -diff --git a/GAP_ANALYSIS.md b/GAP_ANALYSIS.md -index 6034d0e..409fc23 100644 ---- a/GAP_ANALYSIS.md -+++ b/GAP_ANALYSIS.md -@@ -20,7 +20,7 @@ claim below was reproduced, not inferred from reading code. - |---|---|---|---| - | G1 | ~~`config.env.example` names do not match `setup.sh` / loaders~~ | **Closed** | Renamed to `PG_*_` scheme | - | G2 | ~~Windows CI cannot run database-backed tests~~ | **Closed** | Added `windows-postgres` job to `quality-gate.yml` | --| G3 | Tiers X and E remain unimplemented | Medium | No — deferred by design | -+| G3 | ~~Tiers X and E remain unimplemented~~ | **Closed** | Implemented Tier X (CSV round-trip) and Tier E (cross-env parity) | - | G4 | ~~Runtime artifacts are not gitignored~~ | **Closed** | Added to `.gitignore` | - | G5 | ~~`VCRM.md` BR-20 assertion count edited~~ | **Closed** | Confirmed: 142 matches suite output and Tier S JSON | - -@@ -48,15 +48,25 @@ The existing `python-validator-tests.yml` Windows job continues to run - database-free markers as a fast signal; the new quality-gate job covers the - full surface. - --### G3 — Tiers X and E unimplemented (Medium) -+### G3 — Tiers X and E unimplemented (Closed) - --`evals/PLAN.md` defines five tiers; P, I and S are implemented. **X** --(cross-engine schema equivalence) and **E** (cross-environment structural --parity) remain deferred, so cross-engine claims for MariaDB, SQLite, InfluxDB, --Redis and Teradata rest on code review rather than execution. -+**Resolution:** Implemented both remaining eval tiers in `evals/runner.py`: - --Partially mitigated: `tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables` --now runs against all four PostgreSQL environments. -+- **Tier X** — CSV round-trip fidelity: loads each sample CSV into PostgreSQL -+ via `csv_loader.sh`, exports it back via `csv_utilise.sh export`, and diffs -+ data columns against the original. Proves the full load → DB → export -+ pipeline preserves data for arbitrary CSV shapes (including quoted commas -+ and UTF-8 characters). -+ -+- **Tier E** — Cross-environment structural parity: queries -+ `information_schema.columns` for all four environments (dev, test, staging, -+ prod) and asserts they have identical table names, column names, column -+ types, and column order. -+ -+Run with: `python3 evals/runner.py --tiers x,e --verbose` -+ -+The existing `tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables` -+provides complementary coverage at the pytest level. - - ### G4 — Runtime artifacts not gitignored (Closed) - -@@ -91,6 +101,6 @@ update to 142 is correct. No revert needed. - | Python unit / regression / security / snapshot | 54 tests, 0 skipped | `05_test_report_full.log` | - | SQL assertions | 142 / 142, 100% | `03_sql_test_suite.log` | - | Eval tiers P, I, S | 25 / 25, 0 skipped | `04_evals_p_i_s.log` | --| Eval tiers X, E | Not implemented | G3 | -+| Eval tiers X, E | Implemented (PostgreSQL) | `evals/runner.py --tiers x,e` | - | PostgreSQL engine | Fully exercised | above | - | Other five engines | Code review only | G3 | -diff --git a/evals/FAILURE_MODES.md b/evals/FAILURE_MODES.md -index 75e264f..c6868b0 100644 ---- a/evals/FAILURE_MODES.md -+++ b/evals/FAILURE_MODES.md -@@ -68,23 +68,48 @@ Tier S initial scope: only S1. - - --- - -+## Tier X — CSV round-trip fidelity -+ -+| # | Failure mode | Example scenario | Expected behaviour | Current | Eval ID | -+|---|--------------|------------------|--------------------|---------|---------| -+| X1 | Load → export round-trip loses data | Load customers.csv, export back, diff | All data columns match original exactly; marker columns excluded from diff | ✅ | 01 | -+| X2 | Round-trip with quoted commas and special chars | Load orders.csv (has quoted commas) | Quoted fields survive load/export cycle intact | ✅ | 01 | -+| X3 | Round-trip with UTF-8 special characters | Load inventory.csv (has en-dash) | UTF-8 preserved through PostgreSQL TEXT columns | ✅ | 01 | -+ -+Tier X initial scope: X1–X3 are all covered by scenario 01 which loops over all sample CSVs. -+ -+--- -+ -+## Tier E — Cross-environment structural parity -+ -+| # | Failure mode | Example scenario | Expected behaviour | Current | Eval ID | -+|---|--------------|------------------|--------------------|---------|---------| -+| E1 | Dev and test have different table sets | Compare information_schema across envs | All four envs have identical table names | ✅ | 01 | -+| E2 | Column type drift between environments | Dev has TEXT, staging has VARCHAR | Column names, types, and order match across all envs | ✅ | 01 | -+| E3 | Missing table in one environment | prod missing evidence_artifacts | Detected and reported as structural mismatch | ✅ | 01 | -+ -+Tier E initial scope: E1–E3 are all covered by scenario 01 which compares schema fingerprints. -+ -+--- -+ - ## What this catalogue does NOT yet cover - --- **Multi-DB equivalence** (cross-engine schema parity) — deferred until PG is locked in. - - **Performance / scale** (1M-row load timing) — separate suite if needed later. --- **Cross-environment structural equivalence** (Dev vs Test vs Staging vs Prod) — Tier E, future. - - **Domain-rule deep dives beyond suite 05** — Tier D, future. - - **Validator behaviour on >128KB single field** — beyond the current 50KB eval and Python `csv` default field-size assumptions. -+- **Cross-engine CSV round-trip** (MariaDB, SQLite) — Tier X currently covers PostgreSQL only. - - --- - - ## Summary - --| Tier | Modes catalogued | Modes in initial eval set | Deferred | --|------|------------------|---------------------------|----------| -+| Tier | Modes catalogued | Modes in eval set | Deferred | -+|------|------------------|-------------------|----------| - | P | 22 | 22 | 0 | - | I | 4 | 1 | 3 | - | S | 3 | 1 | 2 | --| **Total** | **29** | **21** | **7** | -+| X | 3 | 3 | 0 | -+| E | 3 | 3 | 0 | -+| **Total** | **35** | **30** | **5** | - --The current eval set covers every catalogued Tier P mode plus the initial Tier I and Tier S operational scenarios. The remaining deferred items are PostgreSQL oper -+The eval set now covers all five tiers. Tier X and E require a live PostgreSQL instance with all four environment databases deployed; they fail (not skip) when prerequisites are unavailable. -diff --git a/evals/PLAN.md b/evals/PLAN.md -index b59b544..ef9c885 100644 ---- a/evals/PLAN.md -+++ b/evals/PLAN.md -@@ -22,9 +22,10 @@ In short: `tests/` proves the **code is correct**; `evals/` proves the **framewo - - **Tier P** — Python CSV validator (`build/csv/validator.py`). Pure data-in / files-out. No DB. - - **Tier I** — Idempotency of `deploy_all.sh` against a clean Dev PostgreSQL. - - **Tier S** — SQL test suite integration: deploy fresh + run all 5 suites and assert 142/142. --- **Tiers deferred:** -- - **Tier X** — Cross-DB schema equivalence (MariaDB/SQLite). Out until Postgres is locked in. -+- **Tiers added (G3 closure):** -+ - **Tier X** — CSV round-trip fidelity: load → export → diff against original (PostgreSQL). - - **Tier E** — Cross-environment (Dev/Test/Staging/Prod) structural equivalence. -+- **Tiers deferred:** - - **Tier D** — Extended domain-rule evals beyond what suite 05 already covers. - - ## Folder layout -@@ -47,8 +48,16 @@ PostgreDataMigrationApp/ - │ │ └── 01_deploy_dev_twice/ - │ │ └── NOTES.txt ← what the runner does (no CSV needed) - │ │ -- │ └── tier_s/ ← SQL suite integration -- │ └── 01_fresh_deploy_then_all_tests_pass/ -+ │ ├── tier_s/ ← SQL suite integration -+ │ │ └── 01_fresh_deploy_then_all_tests_pass/ -+ │ │ └── NOTES.txt -+ │ │ -+ │ ├── tier_x/ ← CSV round-trip fidelity -+ │ │ └── 01_csv_round_trip_postgresql/ -+ │ │ └── NOTES.txt -+ │ │ -+ │ └── tier_e/ ← cross-environment parity -+ │ └── 01_all_envs_same_tables/ - │ └── NOTES.txt - │ - ├── expected/ -@@ -58,8 +67,12 @@ PostgreDataMigrationApp/ - │ │ └── … - │ ├── tier_i/ - │ │ └── 01_deploy_dev_twice.json -- │ └── tier_s/ -- │ └── 01_fresh_deploy_then_all_tests_pass.json -+ │ ├── tier_s/ -+ │ │ └── 01_fresh_deploy_then_all_tests_pass.json -+ │ ├── tier_x/ -+ │ │ └── 01_csv_round_trip_postgresql.json -+ │ └── tier_e/ -+ │ └── 01_all_envs_same_tables.json - │ - └── reports/ ← runtime output (gitignored) - └── / -@@ -117,7 +130,8 @@ Exit code: 0 if all scenarios in selected tiers pass, 1 otherwise. CI-friendly. - | 3 | Execute Tier P locally; show results | DONE / awaiting your review | - | 4 | Tier I scaffolding + runner extension | next | - | 5 | Tier S scaffolding + runner extension | next | --| 6 | (Future) Tier X across MariaDB/SQLite once Postgres is locked in | deferred | -+| 6 | Tier X (CSV round-trip fidelity, PostgreSQL) | DONE | -+| 7 | Tier E (cross-environment structural parity) | DONE | - - ## What this DOES NOT do - -diff --git a/evals/datasets/tier_e/01_all_envs_same_tables/NOTES.txt b/evals/datasets/tier_e/01_all_envs_same_tables/NOTES.txt -new file mode 100644 -index 0000000..df167be ---- /dev/null -+++ b/evals/datasets/tier_e/01_all_envs_same_tables/NOTES.txt -@@ -0,0 +1,10 @@ -+Tier E — Cross-environment structural parity. -+ -+After all four environments (dev, test, staging, prod) have been deployed, -+their te_core_schema tables must be structurally identical: same table names, -+same column names, same column types, same column order. -+ -+This scenario queries information_schema.columns for each environment and -+asserts the structural fingerprints match. -+ -+Requires: PostgreSQL reachable, all four environment databases deployed. -diff --git a/evals/datasets/tier_x/01_csv_round_trip_postgresql/NOTES.txt b/evals/datasets/tier_x/01_csv_round_trip_postgresql/NOTES.txt -new file mode 100644 -index 0000000..54bad3a ---- /dev/null -+++ b/evals/datasets/tier_x/01_csv_round_trip_postgresql/NOTES.txt -@@ -0,0 +1,11 @@ -+Tier X — CSV round-trip through PostgreSQL. -+ -+Loads each sample CSV (build/csv/samples/*.csv) into the dev database via -+csv_loader.sh, exports it back via csv_utilise.sh export, and diffs the -+data columns against the original. Marker columns (_csv_row_id, _loaded_at) -+are excluded from the diff. -+ -+Proves that the loader → DB → export pipeline preserves data fidelity for -+arbitrary CSV shapes. -+ -+Requires: PostgreSQL reachable via psql, config.local.env present. -diff --git a/evals/expected/tier_e/01_all_envs_same_tables.json b/evals/expected/tier_e/01_all_envs_same_tables.json -new file mode 100644 -index 0000000..16cff95 ---- /dev/null -+++ b/evals/expected/tier_e/01_all_envs_same_tables.json -@@ -0,0 +1,9 @@ -+{ -+ "scenario": "01_all_envs_same_tables", -+ "description": "All four environments must have identical table structure (names, columns, types).", -+ "expected": { -+ "all_envs_match": true, -+ "min_envs_compared": 4, -+ "min_tables_checked": 12 -+ } -+} -diff --git a/evals/expected/tier_x/01_csv_round_trip_postgresql.json b/evals/expected/tier_x/01_csv_round_trip_postgresql.json -new file mode 100644 -index 0000000..b9263e7 ---- /dev/null -+++ b/evals/expected/tier_x/01_csv_round_trip_postgresql.json -@@ -0,0 +1,8 @@ -+{ -+ "scenario": "01_csv_round_trip_postgresql", -+ "description": "Load sample CSVs into PostgreSQL, export back, and diff data columns against originals.", -+ "expected": { -+ "all_round_trips_match": true, -+ "min_csvs_tested": 3 -+ } -+} -diff --git a/evals/runner.py b/evals/runner.py -index 2e330fa..8547a31 100644 ---- a/evals/runner.py -+++ b/evals/runner.py -@@ -34,6 +34,9 @@ from typing import Any, Dict, List, Optional - EVALS_DIR = Path(__file__).resolve().parent - PROJECT_ROOT = EVALS_DIR.parent - VALIDATOR = PROJECT_ROOT / "build" / "csv" / "validator.py" -+CSV_LOADER = PROJECT_ROOT / "build" / "csv_loader.sh" -+CSV_UTILISE = PROJECT_ROOT / "build" / "csv_utilise.sh" -+SAMPLES_DIR = PROJECT_ROOT / "build" / "csv" / "samples" - - DATASETS_DIR = EVALS_DIR / "datasets" - EXPECTED_DIR = EVALS_DIR / "expected" -@@ -541,6 +544,359 @@ def _run_fresh_deploy_then_tests( - return result - - -+# --------------------------------------------------------------------------- -+# Tier X — CSV round-trip (load → export → diff) -+ -+_ENV_CONFIG = { -+ "dev": ("te_mgmt_dev", "te_dev"), -+ "test": ("te_mgmt_test", "te_test"), -+ "staging": ("te_mgmt_staging", "te_staging"), -+ "prod": ("te_mgmt_prod", "te_prod"), -+} -+ -+_REQUIRED_TABLES = [ -+ "organisations", "personnel", "test_programs", "temp_documents", -+ "test_phases", "requirements", "test_cases", "vcrm_entries", -+ "test_events", "test_results", "defect_reports", "evidence_artifacts", -+] -+ -+ -+def _find_bash() -> Optional[str]: -+ if sys.platform == "win32": -+ for c in (r"C:\Program Files\Git\bin\bash.exe", -+ r"C:\Program Files (x86)\Git\bin\bash.exe"): -+ if Path(c).exists(): -+ return c -+ which = shutil.which("bash") -+ if which and "system32" not in which.lower(): -+ return which -+ return None -+ return shutil.which("bash") or "bash" -+ -+ -+def _round_trip_one_csv( -+ csv_path: Path, bash: str, env: Dict[str, str] -+) -> Dict[str, Any]: -+ """Load a CSV into dev, export it, compare data columns.""" -+ table_name = csv_path.stem.lower().replace(" ", "_").replace("-", "_") -+ result: Dict[str, Any] = {"csv": csv_path.name, "table": table_name} -+ -+ load = subprocess.run( -+ [bash, str(CSV_LOADER), str(csv_path), "--env", "dev"], -+ capture_output=True, text=True, cwd=PROJECT_ROOT, -+ env=env, timeout=60, -+ ) -+ if load.returncode != 0: -+ result["error"] = "loader failed: " + load.stderr[-300:] -+ return result -+ -+ with tempfile.NamedTemporaryFile( -+ suffix=".csv", delete=False, mode="w" -+ ) as tmp: -+ export_path = tmp.name -+ -+ try: -+ export = subprocess.run( -+ [bash, str(CSV_UTILISE), "export", table_name, export_path, -+ "--env", "dev"], -+ capture_output=True, text=True, cwd=PROJECT_ROOT, -+ env=env, timeout=30, -+ ) -+ if export.returncode != 0: -+ result["error"] = "export failed: " + export.stderr[-300:] -+ return result -+ -+ original_rows = _read_csv_rows(csv_path) -+ exported_rows = _read_csv_rows(Path(export_path)) -+ -+ if not exported_rows: -+ result["error"] = "exported CSV is empty" -+ return result -+ -+ exported_header = exported_rows[0] -+ orig_header = original_rows[0] if original_rows else [] -+ -+ orig_col_names = [h.strip().lower().replace(" ", "_") for h in orig_header] -+ marker_indices = set() -+ data_indices = [] -+ for i, col in enumerate(exported_header): -+ if col in ("_csv_row_id", "_loaded_at"): -+ marker_indices.add(i) -+ else: -+ data_indices.add(i) -+ -+ exported_data_header = [exported_header[i] for i in data_indices] -+ if exported_data_header != orig_col_names: -+ result["error"] = ( -+ "column name mismatch: original=" + str(orig_col_names) -+ + " exported=" + str(exported_data_header) -+ ) -+ return result -+ -+ orig_data = [row for row in original_rows[1:]] -+ exported_data = [ -+ [row[i] for i in data_indices] -+ for row in exported_rows[1:] -+ ] -+ -+ if len(orig_data) != len(exported_data): -+ result["error"] = ( -+ "row count mismatch: original=" + str(len(orig_data)) -+ + " exported=" + str(len(exported_data)) -+ ) -+ return result -+ -+ mismatches = [] -+ for row_idx, (orig_row, exp_row) in enumerate( -+ zip(orig_data, exported_data) -+ ): -+ if orig_row != exp_row: -+ mismatches.append({ -+ "row": row_idx + 1, -+ "original": orig_row, -+ "exported": exp_row, -+ }) -+ if mismatches: -+ result["error"] = "data mismatch in " + str(len(mismatches)) + " row(s)" -+ result["mismatches"] = mismatches[:5] -+ return result -+ -+ result["match"] = True -+ result["rows_compared"] = len(orig_data) -+ finally: -+ subprocess.run( -+ [bash, str(CSV_UTILISE), "drop", table_name, "--yes", "--env", "dev"], -+ capture_output=True, text=True, cwd=PROJECT_ROOT, -+ env=env, timeout=15, -+ ) -+ try: -+ os.unlink(export_path) -+ except OSError: -+ pass -+ -+ return result -+ -+ -+def run_tier_x_scenario(scenario_dir: Path) -> ScenarioResult: -+ name = scenario_dir.name -+ result = ScenarioResult(tier="x", name=name) -+ -+ expected = _load_expected("x", name) -+ if expected is None: -+ result.errors.append("No expected file at expected/tier_x/" + name + ".json") -+ return result -+ result.expected = expected -+ -+ if not _can_connect_pg(): -+ result.errors.append( -+ "PostgreSQL not reachable via psql — needed for round-trip eval." -+ ) -+ return result -+ -+ bash = _find_bash() -+ if bash is None: -+ result.errors.append("No working bash found.") -+ return result -+ -+ if name == "01_csv_round_trip_postgresql": -+ return _run_csv_round_trip(result, expected, bash) -+ -+ result.errors.append("Unknown tier-X scenario: " + name) -+ return result -+ -+ -+def _run_csv_round_trip( -+ result: ScenarioResult, expected: Dict[str, Any], bash: str -+) -> ScenarioResult: -+ sample_csvs = sorted(SAMPLES_DIR.glob("*.csv")) -+ if not sample_csvs: -+ result.errors.append("No sample CSVs in " + str(SAMPLES_DIR)) -+ return result -+ -+ env = _pg_env() -+ trip_results = [] -+ for csv_path in sample_csvs: -+ trip = _round_trip_one_csv(csv_path, bash, env) -+ trip_results.append(trip) -+ -+ actual = { -+ "csvs_tested": len(trip_results), -+ "all_round_trips_match": all(t.get("match") for t in trip_results), -+ "details": trip_results, -+ } -+ result.actual = actual -+ -+ exp = expected.get("expected", {}) -+ errors: List[str] = [] -+ -+ if exp.get("all_round_trips_match") and not actual["all_round_trips_match"]: -+ failed = [t for t in trip_results if not t.get("match")] -+ for t in failed: -+ errors.append(t["csv"] + ": " + t.get("error", "unknown failure")) -+ -+ min_csvs = exp.get("min_csvs_tested", 0) -+ if actual["csvs_tested"] < min_csvs: -+ errors.append( -+ "csvs_tested: expected >= " + str(min_csvs) -+ + ", got " + str(actual["csvs_tested"]) -+ ) -+ -+ result.errors = errors -+ result.passed = not errors -+ return result -+ -+ -+# --------------------------------------------------------------------------- -+# Tier E — Cross-environment structural parity -+ -+ -+def _get_schema_fingerprint( -+ db: str, schema: str -+) -> Optional[List[Dict[str, str]]]: -+ # schema comes from the hardcoded _ENV_CONFIG constant — not injectable -+ query = ( -+ "SELECT table_name, column_name, data_type, ordinal_position " -+ "FROM information_schema.columns " -+ "WHERE table_schema = '" + schema + "' " # nosec B608 -+ "ORDER BY table_name, ordinal_position;" -+ ) -+ r = subprocess.run( -+ ["psql", "-tA", "-F", "|", "-d", db, "-c", query], -+ env=_pg_env(), capture_output=True, text=True, timeout=10, -+ ) -+ if r.returncode != 0: -+ return None -+ rows = [] -+ for line in r.stdout.strip().splitlines(): -+ parts = line.split("|") -+ if len(parts) >= 4: -+ rows.append({ -+ "table": parts[0], -+ "column": parts[1], -+ "type": parts[2], -+ "position": parts[3], -+ }) -+ return rows -+ -+ -+def run_tier_e_scenario(scenario_dir: Path) -> ScenarioResult: -+ name = scenario_dir.name -+ result = ScenarioResult(tier="e", name=name) -+ -+ expected = _load_expected("e", name) -+ if expected is None: -+ result.errors.append("No expected file at expected/tier_e/" + name + ".json") -+ return result -+ result.expected = expected -+ -+ if not _can_connect_pg(): -+ result.errors.append( -+ "PostgreSQL not reachable via psql — needed for cross-env parity eval." -+ ) -+ return result -+ -+ if name == "01_all_envs_same_tables": -+ return _run_all_envs_same_tables(result, expected) -+ -+ result.errors.append("Unknown tier-E scenario: " + name) -+ return result -+ -+ -+def _run_all_envs_same_tables( -+ result: ScenarioResult, expected: Dict[str, Any] -+) -> ScenarioResult: -+ fingerprints: Dict[str, Optional[List[Dict[str, str]]]] = {} -+ for env_name, (db, schema) in _ENV_CONFIG.items(): -+ fingerprints[env_name] = _get_schema_fingerprint(db, schema) -+ -+ available = {k: v for k, v in fingerprints.items() if v is not None} -+ unavailable = [k for k, v in fingerprints.items() if v is None] -+ -+ tables_per_env = {} -+ for env_name, cols in available.items(): -+ tables_per_env[env_name] = sorted(set(c["table"] for c in cols)) -+ -+ ref_env = "dev" if "dev" in available else next(iter(available), None) -+ -+ actual: Dict[str, Any] = { -+ "envs_compared": len(available), -+ "envs_unavailable": unavailable, -+ "tables_per_env": {k: len(v) for k, v in tables_per_env.items()}, -+ } -+ -+ errors: List[str] = [] -+ exp = expected.get("expected", {}) -+ -+ if not ref_env: -+ errors.append("No environments reachable.") -+ result.actual = actual -+ result.errors = errors -+ return result -+ -+ ref_fingerprint = available[ref_env] -+ ref_tables = tables_per_env[ref_env] -+ -+ def _cols_for_table(fp: List[Dict[str, str]], tbl: str) -> List[Dict[str, str]]: -+ return [c for c in fp if c["table"] == tbl] -+ -+ all_match = True -+ diffs: List[str] = [] -+ for env_name, fp in available.items(): -+ if env_name == ref_env: -+ continue -+ env_tables = tables_per_env[env_name] -+ missing_in_env = set(ref_tables) - set(env_tables) -+ extra_in_env = set(env_tables) - set(ref_tables) -+ if missing_in_env: -+ all_match = False -+ diffs.append( -+ env_name + " missing tables vs " + ref_env + ": " -+ + ", ".join(sorted(missing_in_env)) -+ ) -+ if extra_in_env: -+ all_match = False -+ diffs.append( -+ env_name + " has extra tables vs " + ref_env + ": " -+ + ", ".join(sorted(extra_in_env)) -+ ) -+ for tbl in set(ref_tables) & set(env_tables): -+ ref_cols = _cols_for_table(ref_fingerprint, tbl) -+ env_cols = _cols_for_table(fp, tbl) -+ if ref_cols != env_cols: -+ all_match = False -+ diffs.append( -+ env_name + "." + tbl + " columns differ from " -+ + ref_env + "." + tbl -+ ) -+ -+ actual["all_envs_match"] = all_match -+ actual["diffs"] = diffs -+ actual["tables_checked"] = len(ref_tables) -+ result.actual = actual -+ -+ if exp.get("all_envs_match") and not all_match: -+ for d in diffs: -+ errors.append(d) -+ -+ min_envs = exp.get("min_envs_compared", 0) -+ if len(available) < min_envs: -+ errors.append( -+ "envs_compared: expected >= " + str(min_envs) -+ + ", got " + str(len(available)) -+ ) -+ -+ min_tables = exp.get("min_tables_checked", 0) -+ if actual["tables_checked"] < min_tables: -+ errors.append( -+ "tables_checked: expected >= " + str(min_tables) -+ + ", got " + str(actual["tables_checked"]) -+ ) -+ -+ result.errors = errors -+ result.passed = not errors -+ return result -+ -+ - # --------------------------------------------------------------------------- - # Orchestration - -@@ -548,6 +904,8 @@ TIER_RUNNERS = { - "p": run_tier_p_scenario, - "i": run_tier_i_scenario, - "s": run_tier_s_scenario, -+ "x": run_tier_x_scenario, -+ "e": run_tier_e_scenario, - } - - diff --git a/postgres-migration-icon.svg b/postgres-migration-icon.svg deleted file mode 100644 index 42617e8..0000000 --- a/postgres-migration-icon.svg +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/preview-512.png b/preview-512.png deleted file mode 100644 index f4aa1e8..0000000 Binary files a/preview-512.png and /dev/null differ diff --git a/test-artifacts/00_SUMMARY.md b/test-artifacts/00_SUMMARY.md deleted file mode 100644 index 21fbec1..0000000 --- a/test-artifacts/00_SUMMARY.md +++ /dev/null @@ -1,43 +0,0 @@ -# Test Artifacts — Verification Run - -**Run:** `20260721T080414Z` -**Commit:** `b255262ad97b` (base `b255262` + audit changes) -**PostgreSQL:** 16.14 (Ubuntu 16.14-0ubuntu0.24.04.1) -**Python:** Python 3.12.3 -**Platform:** Ubuntu 24.04 container, clean clone - -## Results - -| # | Artifact | What it proves | Result | -|---|---|---|---| -| 01 | `01_provision.log` | All 4 environments provisioned from committed templates | PASS | -| 02 | `02_deploy_dev.log` | Fresh-clone deploy succeeds: 12 tables, seed loaded | PASS | -| 03 | `03_sql_test_suite.log` | Full SQL suite | **142 / 142 — 100%** | -| 04 | `04_evals_p_i_s.log` | Eval tiers P, I, S | **25 / 25, 0 skipped** | -| 05 | `05_test_report_full.log` | Full suite with skip accounting | **54 / 54, 0 skipped, 0 not run** | -| 06 | `06_lint.log` | flake8 + bandit | PASS | -| 07 | `07_health_check.log` | Repository health check | PASS | -| 08 | `08_test_report_dbfree_markers.log` | Windows-job scope; 15 not-run listed by name | PASS | -| 09 | `09_negative_control_unprovisioned.log` | Missing prerequisites FAIL, never skip | **RESULT: FAIL (intended)** | -| 10 | `10_evals_summary.json` | Machine-readable eval outcomes | 25 / 25 | -| 11 | `11_vcrm_gap_report.md` | Per-run VCRM traceability | generated | - -## Reading these - -Artifacts **01–08** are the green path: every gate passes, nothing is skipped. - -Artifact **09 is a deliberate failure** and is the most important one. It runs -the suite with no environment files, no `config.local.env` and no deployed -databases. It reports `RESULT: FAIL` with **0 skipped** — proving that an -unavailable prerequisite fails loudly instead of quietly skipping. Before this -change, that same state reported green. - -Artifact **08** shows `NOT RUN : 15`, each named. Those are deselected by the -marker filter (they run in the Linux integration job), not skipped. - -## Reproduce - -```bash -bash scripts/provision_full_test_env.sh -python3 scripts/test_report.py --strict -``` diff --git a/test-artifacts/01_provision.log b/test-artifacts/01_provision.log deleted file mode 100644 index 507d3fa..0000000 --- a/test-artifacts/01_provision.log +++ /dev/null @@ -1,17 +0,0 @@ -[✓] PostgreSQL reachable -[✓] env_dev.sql created from template -[✓] env_test.sql created from template -[✓] env_staging.sql created from template -[✓] env_prod.sql created from template -[✓] config.local.env generated (PG_*_ names the loaders expect) -[»] Deploying dev… -[✓] dev deployed -[»] Deploying test… -[✓] test deployed -[»] Deploying staging… -[✓] staging deployed -[»] Deploying prod… -[✓] prod deployed - -[✓] All prerequisites provisioned. The suite should now skip nothing: - python3 -m unittest discover -s tests -p "test*.py" diff --git a/test-artifacts/02_deploy_dev.log b/test-artifacts/02_deploy_dev.log deleted file mode 100644 index ae50d9d..0000000 --- a/test-artifacts/02_deploy_dev.log +++ /dev/null @@ -1,195 +0,0 @@ -──────────────────────────────────────────────────────────── - Defence T&E Database Deployment - Host : localhost:5432 - User : postgres - Targets : dev -──────────────────────────────────────────────────────────── - -[⚠] Deploying environment: DEV (database: te_mgmt_dev) -[✓] Database te_mgmt_dev already exists — skipping CREATE. - -============================================================ - DEFENCE T&E DATABASE SETUP - Environment : DEV - Database : te_mgmt_dev - Schema : te_dev - App User : te_dev_user - Seed Data : true -============================================================ - - set_config | set_config | set_config | set_config | set_config -------------+-------------+-------------+----------------------+------------ - DEV | te_mgmt_dev | te_dev_user | __INJECT_AT_DEPLOY__ | 10 -(1 row) - ->> [1/6] Creating database: te_mgmt_dev ->> [2/6] Creating application user: te_dev_user -psql:/home/claude/audit/build/te_core_schema.sql:67: NOTICE: [DEV] Role "te_dev_user" already exists — password and conn_limit refreshed. -DO -GRANT -You are now connected to database "te_mgmt_dev" as user "postgres". - set_config | set_config | set_config | set_config | set_config | set_config | set_config | set_config | set_config | set_config | set_config | set_config | set_config | set_config | set_config -------------+------------+-------------+---------------+------------+---------------+----------------+-------------+--------------+------------+--------------+-------------+--------------+----------------+-------------------- - DEV | te_dev | te_dev_user | organisations | personnel | test_programs | temp_documents | test_phases | requirements | test_cases | vcrm_entries | test_events | test_results | defect_reports | evidence_artifacts -(1 row) - ->> [3/6] Setting up schema: te_dev -psql:/home/claude/audit/build/te_core_schema.sql:98: NOTICE: extension "uuid-ossp" already exists, skipping -CREATE EXTENSION -psql:/home/claude/audit/build/te_core_schema.sql:99: NOTICE: extension "pg_trgm" already exists, skipping -CREATE EXTENSION -psql:/home/claude/audit/build/te_core_schema.sql:100: NOTICE: extension "dblink" already exists, skipping -CREATE EXTENSION -psql:/home/claude/audit/build/te_core_schema.sql:102: NOTICE: schema "te_dev" already exists, skipping -CREATE SCHEMA - set_config ---------------- - te_dev,public -(1 row) - -GRANT -GRANT -ALTER DEFAULT PRIVILEGES ->> [4/6] Creating tables -psql:/home/claude/audit/build/te_core_schema.sql:128: NOTICE: relation "organisations" already exists, skipping -CREATE TABLE -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:153: NOTICE: relation "personnel" already exists, skipping -CREATE TABLE -COMMENT -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:177: NOTICE: relation "test_programs" already exists, skipping -CREATE TABLE -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:199: NOTICE: relation "temp_documents" already exists, skipping -CREATE TABLE -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:223: NOTICE: relation "test_phases" already exists, skipping -CREATE TABLE -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:247: NOTICE: relation "requirements" already exists, skipping -CREATE TABLE -COMMENT -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:273: NOTICE: relation "test_cases" already exists, skipping -CREATE TABLE -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:293: NOTICE: relation "vcrm_entries" already exists, skipping -CREATE TABLE -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:319: NOTICE: relation "test_events" already exists, skipping -CREATE TABLE -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:340: NOTICE: relation "test_results" already exists, skipping -CREATE TABLE -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:366: NOTICE: relation "defect_reports" already exists, skipping -CREATE TABLE -COMMENT -psql:/home/claude/audit/build/te_core_schema.sql:386: NOTICE: relation "evidence_artifacts" already exists, skipping -CREATE TABLE -COMMENT ->> [4/6] Creating indexes -psql:/home/claude/audit/build/te_core_schema.sql:398: NOTICE: relation "idx_personnel_org" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:400: NOTICE: relation "idx_personnel_email_trgm" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:403: NOTICE: relation "idx_programs_org" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:405: NOTICE: relation "idx_programs_status" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:407: NOTICE: relation "idx_programs_code_trgm" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:410: NOTICE: relation "idx_phases_program" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:412: NOTICE: relation "idx_phases_status" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:415: NOTICE: relation "idx_requirements_program" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:417: NOTICE: relation "idx_requirements_type" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:420: NOTICE: relation "idx_testcases_phase" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:422: NOTICE: relation "idx_testcases_status" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:425: NOTICE: relation "idx_vcrm_req" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:427: NOTICE: relation "idx_vcrm_tc" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:430: NOTICE: relation "idx_events_phase" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:432: NOTICE: relation "idx_events_status" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:435: NOTICE: relation "idx_results_event" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:437: NOTICE: relation "idx_results_verdict" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:440: NOTICE: relation "idx_defects_program" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:442: NOTICE: relation "idx_defects_severity" already exists, skipping -CREATE INDEX -psql:/home/claude/audit/build/te_core_schema.sql:444: NOTICE: relation "idx_defects_status" already exists, skipping -CREATE INDEX -CREATE FUNCTION -DO ->> [5/6] Seeding realistic T&E data -INSERT 0 0 -INSERT 0 0 -INSERT 0 0 -INSERT 0 0 -INSERT 0 0 -INSERT 0 0 -INSERT 0 0 -INSERT 0 0 -INSERT 0 0 -INSERT 0 0 -INSERT 0 0 ->> [6/6] Verification — row counts for DEV - table_name | rows | status ---------------------+------+--------- - defect_reports | 3 | ✓ OK - evidence_artifacts | 0 | ⚠ EMPTY - organisations | 5 | ✓ OK - personnel | 6 | ✓ OK - requirements | 8 | ✓ OK - temp_documents | 3 | ✓ OK - test_cases | 8 | ✓ OK - test_events | 3 | ✓ OK - test_phases | 3 | ✓ OK - test_programs | 2 | ✓ OK - test_results | 7 | ✓ OK - vcrm_entries | 8 | ✓ OK -(12 rows) - - req_identifier | title | req_type | test_cases_mapped | coverage_status -----------------+-----------------------------------------------+-------------+-------------------+----------------- - IFV-PERF-001 | Cross-Country Speed — 40 km/h Minimum | performance | 0 | ✗ NO COVERAGE - IFV-SAF-001 | Crew Survivability — STANAG 4569 Level 4 | safety | 0 | ✗ NO COVERAGE - SYS-COMP-001 | ISM Control Compliance — Section 3 (Gateways) | compliance | 1 | ✓ COVERED - SYS-FUNC-001 | Audit Log — All User Actions Captured | functional | 1 | ✓ COVERED - SYS-FUNC-002 | Role-Based Access Control (RBAC) Enforcement | functional | 1 | ✓ COVERED - SYS-PERF-001 | System Availability — 99.5% Uptime SLA | performance | 1 | ✓ COVERED - SYS-SEC-001 | Multi-Factor Authentication Enforcement | security | 2 | ✓ COVERED - SYS-SEC-002 | Data-at-Rest Encryption (AES-256) | security | 2 | ✓ COVERED -(8 rows) - - severity | status | count -----------+-------------+------- - major | in_progress | 1 - major | open | 1 - minor | open | 1 -(3 rows) - - -============================================================ - Setup complete for environment: DEV - Database : te_mgmt_dev | Schema: te_dev -============================================================ - -[✓] Environment DEV deployed successfully. -──────────────────────────────────────────────────────────── - - Deployment Summary -──────────────────────────────────────────────────────────── -[✓] Succeeded : dev - diff --git a/test-artifacts/03_sql_test_suite.log b/test-artifacts/03_sql_test_suite.log deleted file mode 100644 index c0481ae..0000000 --- a/test-artifacts/03_sql_test_suite.log +++ /dev/null @@ -1,254 +0,0 @@ -──────────────────────────────────────────────────────────── - Defence T&E — Test Suite Runner - Host : localhost:5432 - User : postgres - Targets : dev -──────────────────────────────────────────────────────────── -──────────────────────────────────────────────────────────── -[i] Running tests against: DEV (db=te_mgmt_dev schema=te_dev) -──────────────────────────────────────────────────────────── - -============================================================ - DEFENCE T&E TEST SUITE - Schema: te_dev -============================================================ - - set_config | set_config | set_config | set_config ----------------+------------+-------------+------------ - te_dev,public | te_dev | te_dev_user | 10 -(1 row) - ->> Loading test framework... -CREATE TABLE -COMMENT -TRUNCATE TABLE -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION -CREATE FUNCTION - [framework] Test framework loaded. - ->> Running test suites... - - [suite 01] organisations & personnel -DO - [suite 02] programs, TEMP documents & test phases -DO - [suite 03] requirements & VCRM coverage -DO - [suite 04] test cases, events, results & defect reports -DO - [suite 05] schema structure, indexes, triggers & business rules -DO - -============================================================ - REPORT 1: Suite Summary -============================================================ - suite | total | passed | failed | skipped | pass_rate | suite_status -----------------+-------+--------+--------+---------+-----------+-------------- - business_rules | 10 | 10 | 0 | 0 | 100.0% | ✓ ALL PASS - defect_reports | 12 | 12 | 0 | 0 | 100.0% | ✓ ALL PASS - organisations | 11 | 11 | 0 | 0 | 100.0% | ✓ ALL PASS - personnel | 12 | 12 | 0 | 0 | 100.0% | ✓ ALL PASS - programs | 13 | 13 | 0 | 0 | 100.0% | ✓ ALL PASS - requirements | 13 | 13 | 0 | 0 | 100.0% | ✓ ALL PASS - schema | 23 | 23 | 0 | 0 | 100.0% | ✓ ALL PASS - temp_documents | 6 | 6 | 0 | 0 | 100.0% | ✓ ALL PASS - test_cases | 9 | 9 | 0 | 0 | 100.0% | ✓ ALL PASS - test_events | 8 | 8 | 0 | 0 | 100.0% | ✓ ALL PASS - test_phases | 6 | 6 | 0 | 0 | 100.0% | ✓ ALL PASS - test_results | 9 | 9 | 0 | 0 | 100.0% | ✓ ALL PASS - vcrm | 10 | 10 | 0 | 0 | 100.0% | ✓ ALL PASS -(13 rows) - - -============================================================ - REPORT 2: Failures Only (empty = all green) -============================================================ - suite | test_name | expected | actual | message --------+-----------+----------+--------+--------- -(0 rows) - - -============================================================ - REPORT 3: Full Detail (all assertions) -============================================================ - suite | test_name | status | expected | actual | message -----------------+----------------------------------------------------------------------------+--------+------------------+--------------------------------------------------------------------------------------------------------+--------- - business_rules | S01 — All results: test case phase matches event phase | PASS | 0 | 0 | - business_rules | S02 — All fail verdicts have a matching DR raised | PASS | 0 | 0 | - business_rules | S03 — No results recorded against planned (future) events | PASS | 0 | 0 | - business_rules | S04 — All open DRs belong to an active program | PASS | 0 | 0 | - business_rules | S05 — No baseline-cleared personnel authoring cases in PROTECTED+ programs | PASS | 0 | 0 | - business_rules | S06 — All VCRM entries: requirement and test case share same program | PASS | 0 | 0 | - business_rules | S07 — No test events have planned_end before planned_start | PASS | 0 | 0 | - business_rules | S08 — evidence_artifacts table is empty (no files uploaded yet) | PASS | 0 | 0 | - business_rules | S09 — App role exists in pg_roles | PASS | 1 | 1 | - business_rules | S10 — App role conn limit matches env config | PASS | 10 | 10 | - defect_reports | O01 — Row count = 3 | PASS | 3 | 3 | - defect_reports | O02 — DR-CYB-0001 (Audit Log gap) exists | PASS | 1 | 1 | - defect_reports | O03 — DR-CYB-0002 (TLS 1.2 gap) exists | PASS | 1 | 1 | - defect_reports | O04 — DR-CYB-0003 (Session Timeout) exists | PASS | 1 | 1 | - defect_reports | O05 — No critical-severity defects in seed data | PASS | 0 | 0 | - defect_reports | O06 — 2 major-severity defects seeded | PASS | 2 | 2 | - defect_reports | O07 — No closed/resolved defects (all active) | PASS | 0 | 0 | - defect_reports | O08 — All defects reference a valid program (FK check) | PASS | 0 | 0 | - defect_reports | O09 — All defect raisers exist in personnel (FK check) | PASS | 0 | 0 | - defect_reports | O10 — All defect_ref values are unique (UNIQUE enforced) | PASS | 0 | 0 | - defect_reports | O11 — All resolved defects have a resolved_at timestamp | PASS | 0 | 0 | - defect_reports | O12 — All severity values are valid | PASS | 0 | 0 | - organisations | A01 — Row count = 5 | PASS | 5 | 5 | - organisations | A02 — CASG record exists | PASS | 1 | 1 | - organisations | A03 — All organisations active by default | PASS | 0 | 0 | - organisations | A04 — All country codes are 2 chars (ISO 3166-1) | PASS | 0 | 0 | - organisations | A05 — All org_type values are valid | PASS | 0 | 0 | - organisations | A06 — No duplicate organisation names | PASS | 0 | 0 | - organisations | A07 — At least one test_unit organisation exists | PASS | TRUE | TRUE | - organisations | A08 — created_at populated on all organisations | PASS | 0 | 0 | - organisations | B01 — Invalid org_type rejected by CHECK constraint | PASS | exception raised | new row for relation "organisations" violates check constraint "organisations_org_type_check" | - organisations | B02 — NULL name rejected by NOT NULL constraint | PASS | exception raised | null value in column "name" of relation "organisations" violates not-null constraint | - organisations | B03 — Duplicate org name rejected by UNIQUE constraint | PASS | exception raised | duplicate key value violates unique constraint "organisations_name_key" | - personnel | C01 — Row count = 6 | PASS | 6 | 6 | - personnel | C02 — No orphaned personnel (all linked to valid org) | PASS | 0 | 0 | - personnel | C03 — At least one NV2-cleared person exists | PASS | TRUE | TRUE | - personnel | C04 — All clearance values are within allowed set | PASS | 0 | 0 | - personnel | C05 — All emails contain @ | PASS | 0 | 0 | - personnel | C06 — No duplicate email addresses | PASS | 0 | 0 | - personnel | C07 — All password hashes are bcrypt format ($2b$/$2a$) | PASS | 0 | 0 | - personnel | C08 — All te_role values are valid | PASS | 0 | 0 | - personnel | C09 — All personnel active by default | PASS | 0 | 0 | - personnel | D01 — Invalid clearance level rejected by CHECK | PASS | exception raised | new row for relation "personnel" violates check constraint "personnel_clearance_check" | - personnel | D02 — Invalid te_role rejected by CHECK | PASS | exception raised | new row for relation "personnel" violates check constraint "personnel_te_role_check" | - personnel | D03 — Invalid org_id rejected by FK constraint | PASS | exception raised | insert or update on table "personnel" violates foreign key constraint "personnel_org_id_fkey" | - programs | E01 — Row count = 2 | PASS | 2 | 2 | - programs | E02 — CYB9131 program present | PASS | 1 | 1 | - programs | E03 — LAND400-P3 program present | PASS | 1 | 1 | - programs | E04 — All programs have a director assigned | PASS | 0 | 0 | - programs | E05 — All program directors exist in personnel table | PASS | 0 | 0 | - programs | E06 — All program status values are valid | PASS | 0 | 0 | - programs | E07 — All classification markings are valid | PASS | 0 | 0 | - programs | E08 — No programs have end_date before start_date | PASS | 0 | 0 | - programs | E09 — All program_codes are unique | PASS | 0 | 0 | - programs | E10 — CYB9131 classification is PROTECTED | PASS | PROTECTED | PROTECTED | - programs | F01 — Inverted date range rejected by CHECK constraint | PASS | exception raised | new row for relation "test_programs" violates check constraint "chk_program_dates" | - programs | F02 — Invalid classification rejected by CHECK constraint | PASS | exception raised | new row for relation "test_programs" violates check constraint "test_programs_classification_check" | - programs | F03 — Duplicate program_code rejected by UNIQUE constraint | PASS | exception raised | duplicate key value violates unique constraint "test_programs_program_code_key" | - requirements | I01 — Row count = 8 | PASS | 8 | 8 | - requirements | I02 — CYB9131 has 6 requirements | PASS | 6 | 6 | - requirements | I03 — LAND400-P3 has 2 requirements | PASS | 2 | 2 | - requirements | I04 — All requirements have a non-empty req_identifier | PASS | 0 | 0 | - requirements | I05 — All mandatory requirements have a verification method | PASS | 0 | 0 | - requirements | I06 — All verification_method values are valid | PASS | 0 | 0 | - requirements | I07 — All req_type values are valid | PASS | 0 | 0 | - requirements | I08 — All priority values are between 1 and 3 | PASS | 0 | 0 | - requirements | I09 — No duplicate req_identifier per program (UNIQUE enforced) | PASS | 0 | 0 | - requirements | I10 — All requirements reference a valid program (FK check) | PASS | 0 | 0 | - requirements | I11 — SYS-SEC-001 (MFA Enforcement) requirement exists | PASS | 1 | 1 | - requirements | J01 — Priority outside 1–3 rejected by CHECK constraint | PASS | exception raised | new row for relation "requirements" violates check constraint "requirements_priority_check" | - requirements | J02 — Invalid verification_method rejected by CHECK | PASS | exception raised | new row for relation "requirements" violates check constraint "requirements_verification_method_check" | - schema | P01 — Table organisations exists | PASS | 1 | 1 | - schema | P02 — Table personnel exists | PASS | 1 | 1 | - schema | P03 — Table test_programs exists | PASS | 1 | 1 | - schema | P04 — Table temp_documents exists | PASS | 1 | 1 | - schema | P05 — Table test_phases exists | PASS | 1 | 1 | - schema | P06 — Table requirements exists | PASS | 1 | 1 | - schema | P07 — Table test_cases exists | PASS | 1 | 1 | - schema | P08 — Table vcrm_entries exists | PASS | 1 | 1 | - schema | P09 — Table test_events exists | PASS | 1 | 1 | - schema | P10 — Table test_results exists | PASS | 1 | 1 | - schema | P11 — Table defect_reports exists | PASS | 1 | 1 | - schema | P12 — Table evidence_artifacts exists | PASS | 1 | 1 | - schema | Q01 — Index idx_personnel_org exists | PASS | 1 | 1 | - schema | Q02 — Index idx_programs_status exists | PASS | 1 | 1 | - schema | Q03 — Index idx_testcases_phase exists | PASS | 1 | 1 | - schema | Q04 — Index idx_results_verdict exists | PASS | 1 | 1 | - schema | Q05 — Index idx_defects_severity exists | PASS | 1 | 1 | - schema | Q06 — Index idx_vcrm_req exists | PASS | 1 | 1 | - schema | Q07 — GIN trigram index idx_personnel_email_trgm exists | PASS | 1 | 1 | - schema | R01 — Trigger fires: organisations.updated_at advances on UPDATE | PASS | TRUE | TRUE | - schema | R02 — Trigger fires: test_programs.updated_at advances on UPDATE | PASS | TRUE | TRUE | - schema | R03 — Trigger trg_updated_at registered on personnel | PASS | 1 | 1 | - schema | R04 — Trigger trg_updated_at registered on defect_reports | PASS | 1 | 1 | - temp_documents | G01 — Row count = 3 | PASS | 3 | 3 | - temp_documents | G02 — CYB9131 has an approved TEMP | PASS | 1 | 1 | - temp_documents | G03 — All TEMP authors exist in personnel | PASS | 0 | 0 | - temp_documents | G04 — No duplicate version per program (UNIQUE enforced) | PASS | 0 | 0 | - temp_documents | G05 — All TEMP status values are valid | PASS | 0 | 0 | - temp_documents | G06 — At least one TEMP in active draft/review state | PASS | TRUE | TRUE | - test_cases | L01 — Row count = 8 | PASS | 8 | 8 | - test_cases | L02 — All 8 test cases belong to CYB9131 OT&E phase | PASS | 8 | 8 | - test_cases | L03 — All seeded test cases are in approved status | PASS | 0 | 0 | - test_cases | L04 — TC-OTE-001 (MFA valid TOTP) exists | PASS | 1 | 1 | - test_cases | L05 — All test cases have a non-empty title | PASS | 0 | 0 | - test_cases | L06 — All tc_type values are valid | PASS | 0 | 0 | - test_cases | L07 — All test case authors exist in personnel (FK check) | PASS | 0 | 0 | - test_cases | L08 — No duplicate tc_identifier per phase (UNIQUE enforced) | PASS | 0 | 0 | - test_cases | L09 — All test cases have a defined objective | PASS | 0 | 0 | - test_events | M01 — Row count = 3 | PASS | 3 | 3 | - test_events | M02 — All event_codes are unique (UNIQUE enforced) | PASS | 0 | 0 | - test_events | M03 — CYB9131-OTE-EV01 status is completed | PASS | completed | completed | - test_events | M04 — CYB9131-OTE-EV02 status is in_progress | PASS | in_progress | in_progress | - test_events | M05 — CYB9131-OTE-EV03 status is planned | PASS | planned | planned | - test_events | M06 — All completed events have actual_start and actual_end | PASS | 0 | 0 | - test_events | M07 — Planned events have no actual_end date | PASS | 0 | 0 | - test_events | M08 — All test events reference a valid phase (FK check) | PASS | 0 | 0 | - test_phases | H01 — Row count = 3 | PASS | 3 | 3 | - test_phases | H02 — CYB9131 has DT&E and OT&E phases | PASS | 2 | 2 | - test_phases | H03 — All completed phases have an actual_start date | PASS | 0 | 0 | - test_phases | H04 — All phase_type values are valid | PASS | 0 | 0 | - test_phases | H05 — No duplicate phase_code per program (UNIQUE enforced) | PASS | 0 | 0 | - test_phases | H06 — Planned phases have no actual_start date | PASS | 0 | 0 | - test_results | N01 — Row count = 7 | PASS | 7 | 7 | - test_results | N02 — EV01 has 6 test results | PASS | 6 | 6 | - test_results | N03 — EV02 has 1 test result so far | PASS | 1 | 1 | - test_results | N04 — EV01 has 4 pass verdicts | PASS | 4 | 4 | - test_results | N05 — EV01 has 2 fail verdicts | PASS | 2 | 2 | - test_results | N06 — EV02 result verdict is inconclusive | PASS | 1 | 1 | - test_results | N07 — All verdict values are valid | PASS | 0 | 0 | - test_results | N08 — All test results reference a valid test case (FK check) | PASS | 0 | 0 | - test_results | N09 — All results have actual_result or notes recorded | PASS | 0 | 0 | - vcrm | K01 — VCRM row count = 8 | PASS | 8 | 8 | - vcrm | K02 — All CYB9131 requirements have VCRM coverage | PASS | 0 | 0 | - vcrm | K03 — SYS-SEC-001 (MFA) mapped to exactly 2 test cases | PASS | 2 | 2 | - vcrm | K04 — SYS-PERF-001 (Availability) mapped to exactly 1 test case | PASS | 1 | 1 | - vcrm | K05 — No duplicate req↔tc entries in VCRM (UNIQUE enforced) | PASS | 0 | 0 | - vcrm | K06 — All VCRM entries reference a valid requirement (FK check) | PASS | 0 | 0 | - vcrm | K07 — All VCRM entries reference a valid test case (FK check) | PASS | 0 | 0 | - vcrm | K08 — All coverage_type values are valid | PASS | 0 | 0 | - vcrm | K09 — LAND400-P3 requirements correctly have no VCRM entries yet | PASS | 0 | 0 | - vcrm | K10 — CYB9131 VCRM coverage is 100% | PASS | 100.0 | 100.0 | -(142 rows) - - -============================================================ - REPORT 4: Overall Result -============================================================ - total_tests | passed | failed | skipped | pass_rate | overall --------------+--------+--------+---------+-----------+-------------------- - 142 | 142 | 0 | 0 | 100.0% | ✓ ALL TESTS PASSED -(1 row) - - -============================================================ - Test run complete. -============================================================ - -[✓] Test run for DEV completed. - -──────────────────────────────────────────────────────────── - OVERALL TEST SUMMARY -──────────────────────────────────────────────────────────── - Environment Result - ----------- ------ - DEV 142 | 142 | 0 | 0 | 100.0% | ✓ ALL TESTS PASSED -──────────────────────────────────────────────────────────── -[✓] All test runs completed successfully. diff --git a/test-artifacts/04_evals_p_i_s.log b/test-artifacts/04_evals_p_i_s.log deleted file mode 100644 index b0258ab..0000000 --- a/test-artifacts/04_evals_p_i_s.log +++ /dev/null @@ -1,40 +0,0 @@ - -=== Tier P - 23 scenarios === -PASS tier_p/01_happy_path -PASS tier_p/02_empty_file -PASS tier_p/03_empty_header_only_newline -PASS tier_p/04_no_valid_rows -PASS tier_p/05_mixed_valid_skipped -PASS tier_p/06_duplicate_headers -PASS tier_p/07_column_mismatch_short -PASS tier_p/08_column_mismatch_long -PASS tier_p/09_empty_row -PASS tier_p/10_utf8_bom -PASS tier_p/11_utf8_emoji -PASS tier_p/12_crlf_line_endings -PASS tier_p/13_quoted_comma -PASS tier_p/14_quoted_newline -PASS tier_p/15_quoted_quote -PASS tier_p/16_whitespace_only_row -PASS tier_p/17_header_whitespace -PASS tier_p/18_utf8_cjk -PASS tier_p/19_missing_env_vars -PASS tier_p/20_missing_csv_file -PASS tier_p/21_utf8_arabic -PASS tier_p/22_very_long_field -PASS tier_p/23_invalid_utf8_bytes - -=== Tier I - 1 scenarios === -PASS tier_i/01_deploy_dev_twice - -=== Tier S - 1 scenarios === -PASS tier_s/01_fresh_deploy_then_all_tests_pass - -=== Summary === - total: 25 - passed: 25 - failed: 0 - skipped: 0 - - report: /home/claude/audit/evals/reports/20260721T080526Z-5372c3/summary.json - gap report: /home/claude/audit/evals/reports/20260721T080526Z-5372c3/VCRM_GAPS_20260721T080526Z-5372c3.md diff --git a/test-artifacts/05_test_report_full.log b/test-artifacts/05_test_report_full.log deleted file mode 100644 index d97a351..0000000 --- a/test-artifacts/05_test_report_full.log +++ /dev/null @@ -1,23 +0,0 @@ -...................................................... [100%] - -================================================================== - FINAL RESULT — every test accounted for -================================================================== - collected : 54 - executed : 54 - PASSED : 54 - FAILED : 0 - ERROR : 0 - SKIPPED : 0 - NOT RUN : 0 (deselected by the marker filter) - - SKIPPED (0) - none — no test was skipped - - NOT RUN (0) - none — every collected test was executed - -================================================================== - RESULT: PASS -================================================================== - diff --git a/test-artifacts/06_lint.log b/test-artifacts/06_lint.log deleted file mode 100644 index 5409f49..0000000 --- a/test-artifacts/06_lint.log +++ /dev/null @@ -1,10 +0,0 @@ - -=== flake8 (style + logic) === -✓ flake8 clean - -=== bandit (security) === -[tester] WARNING nosec encountered (B608), but no failed test on file ./evals/runner.py:309 -✓ bandit clean - -All lint checks passed. - diff --git a/test-artifacts/07_health_check.log b/test-artifacts/07_health_check.log deleted file mode 100644 index 253a1f6..0000000 --- a/test-artifacts/07_health_check.log +++ /dev/null @@ -1,94 +0,0 @@ - -PostgreDataMigrationApp — Health Dashboard -============================================================ - - PASS validator.py syntax - PASS runner.py syntax - PASS gap_report.py syntax - PASS build/deploy_all.sh - PASS build/setup.sh - PASS build/csv_loader.sh - PASS preflight.sh - PASS build/adapters/adapter_postgresql.sh - PASS build/adapters/adapter_sqlite.sh - PASS build/adapters/adapter_redis.sh - PASS build/adapters/adapter_mariadb.sh - PASS build/adapters/adapter_teradata.sh - PASS build/adapters/adapter_influxdb.sh - PASS build/csv/loader_sqlite.sh - PASS build/csv/loader_influxdb.sh - PASS build/csv/loader_postgresql.sh - PASS build/csv/loader_teradata.sh - PASS build/csv/loader_mariadb.sh - PASS build/csv/loader_redis.sh - PASS postgresql schema - PASS env template (env_dev.example.sql) - PASS env_dev.sql (local, gitignored) - PASS env_test.sql (local, gitignored) - PASS env_staging.sql (local, gitignored) - PASS env_prod.sql (local, gitignored) - PASS test suite: test_01_organisations_personnel.sql - PASS test suite: test_02_programs_phases.sql - PASS test suite: test_03_requirements_vcrm.sql - PASS test suite: test_04_execution_defects.sql - PASS test suite: test_05_schema_and_business_rules.sql - PASS test_framework.sql - PASS tier_i/01_deploy_dev_twice: expected file - PASS tier_p/01_happy_path: expected file - PASS tier_p/01_happy_path: input.csv - PASS tier_p/02_empty_file: expected file - PASS tier_p/02_empty_file: input.csv - PASS tier_p/03_empty_header_only_newline: expected file - PASS tier_p/03_empty_header_only_newline: input.csv - PASS tier_p/04_no_valid_rows: expected file - PASS tier_p/04_no_valid_rows: input.csv - PASS tier_p/05_mixed_valid_skipped: expected file - PASS tier_p/05_mixed_valid_skipped: input.csv - PASS tier_p/06_duplicate_headers: expected file - PASS tier_p/06_duplicate_headers: input.csv - PASS tier_p/07_column_mismatch_short: expected file - PASS tier_p/07_column_mismatch_short: input.csv - PASS tier_p/08_column_mismatch_long: expected file - PASS tier_p/08_column_mismatch_long: input.csv - PASS tier_p/09_empty_row: expected file - PASS tier_p/09_empty_row: input.csv - PASS tier_p/10_utf8_bom: expected file - PASS tier_p/10_utf8_bom: input.csv - PASS tier_p/11_utf8_emoji: expected file - PASS tier_p/11_utf8_emoji: input.csv - PASS tier_p/12_crlf_line_endings: expected file - PASS tier_p/12_crlf_line_endings: input.csv - PASS tier_p/13_quoted_comma: expected file - PASS tier_p/13_quoted_comma: input.csv - PASS tier_p/14_quoted_newline: expected file - PASS tier_p/14_quoted_newline: input.csv - PASS tier_p/15_quoted_quote: expected file - PASS tier_p/15_quoted_quote: input.csv - PASS tier_p/16_whitespace_only_row: expected file - PASS tier_p/16_whitespace_only_row: input.csv - PASS tier_p/17_header_whitespace: expected file - PASS tier_p/17_header_whitespace: input.csv - PASS tier_p/18_utf8_cjk: expected file - PASS tier_p/18_utf8_cjk: input.csv - PASS tier_p/19_missing_env_vars: expected file - PASS tier_p/19_missing_env_vars: input.csv (generated by runner_action=omit_env_vars) - PASS tier_p/20_missing_csv_file: expected file - PASS tier_p/20_missing_csv_file: input.csv (generated by runner_action=point_at_missing_file) - PASS tier_p/21_utf8_arabic: expected file - PASS tier_p/21_utf8_arabic: input.csv - PASS tier_p/22_very_long_field: expected file - PASS tier_p/22_very_long_field: input.csv (generated by runner_action=write_long_field_file) - PASS tier_p/23_invalid_utf8_bytes: expected file - PASS tier_p/23_invalid_utf8_bytes: input.csv (generated by runner_action=write_invalid_utf8_file) - PASS tier_s/01_fresh_deploy_then_all_tests_pass: expected file - PASS pytest.ini - PASS conftest.py - PASS requirements-dev - PASS Makefile - PASS run_qa.ps1 - PASS python test files (9 found) - PASS config.env.example: PG_PASSWORD is empty (safe default) - -============================================================ - 86 passed 0 warnings 0 failed - diff --git a/test-artifacts/08_test_report_dbfree_markers.log b/test-artifacts/08_test_report_dbfree_markers.log deleted file mode 100644 index d4fac72..0000000 --- a/test-artifacts/08_test_report_dbfree_markers.log +++ /dev/null @@ -1,39 +0,0 @@ -....................................... [100%] - -================================================================== - FINAL RESULT — every test accounted for -================================================================== - marker filter : -m "unit or regression or security or snapshot" - collected : 54 - executed : 39 - PASSED : 39 - FAILED : 0 - ERROR : 0 - SKIPPED : 0 - NOT RUN : 15 (deselected by the marker filter) - - SKIPPED (0) - none — no test was skipped - - NOT RUN (15) - - tests/test_csv_loader_arbitrary_shapes.py::CsvLoaderArbitraryShapes::test_shape_medium - - tests/test_csv_loader_arbitrary_shapes.py::CsvLoaderArbitraryShapes::test_shape_skinny - - tests/test_csv_loader_arbitrary_shapes.py::CsvLoaderArbitraryShapes::test_shape_tiny - - tests/test_e2e_pipeline.py::E2EPipelineValidateOnly::test_all_invalid_rows_exit_nonzero - - tests/test_e2e_pipeline.py::E2EPipelineValidateOnly::test_happy_path_two_valid_rows - - tests/test_e2e_pipeline.py::E2EPipelineValidateOnly::test_mixed_rows_splits_correctly - - tests/test_e2e_pipeline.py::E2EPipelineValidateOnly::test_output_directory_is_created_automatically - - tests/test_e2e_pipeline.py::E2EPipelineWithDatabase::test_tier_i_idempotency - - tests/test_e2e_pipeline.py::E2EPipelineWithDatabase::test_tier_p_scenarios_all_pass - - tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables::test_dev_has_required_tables - - tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables::test_prod_has_required_tables - - tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables::test_staging_has_required_tables - - tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables::test_test_has_required_tables - - tests/test_parity.py::TestDevSeedCounts::test_dev_seed_row_counts - - tests/test_parity.py::TestIdempotentDeployParity::test_dev_row_counts_stable_after_second_deploy - Deselected by the marker filter, not skipped. They must run in another job. - -================================================================== - RESULT: PASS -================================================================== - diff --git a/test-artifacts/09_negative_control_unprovisioned.log b/test-artifacts/09_negative_control_unprovisioned.log deleted file mode 100644 index 94f2014..0000000 --- a/test-artifacts/09_negative_control_unprovisioned.log +++ /dev/null @@ -1,225 +0,0 @@ -EEE.................F........EFFFFF................... [100%] -==================================== ERRORS ==================================== -_________ ERROR at setup of CsvLoaderArbitraryShapes.test_shape_medium _________ - -cls = - - @classmethod - def setUpClass(cls): - if not (_PG_AVAILABLE and _CONFIG_PRESENT): -> raise AssertionError( - f"{_SKIP_REASON}. Run 'bash scripts/provision_full_test_env.sh'.") -E AssertionError: build/config.local.env not present — run ./build/setup.sh. Run 'bash scripts/provision_full_test_env.sh'. - -tests/test_csv_loader_arbitrary_shapes.py:82: AssertionError -_________ ERROR at setup of CsvLoaderArbitraryShapes.test_shape_skinny _________ - -cls = - - @classmethod - def setUpClass(cls): - if not (_PG_AVAILABLE and _CONFIG_PRESENT): -> raise AssertionError( - f"{_SKIP_REASON}. Run 'bash scripts/provision_full_test_env.sh'.") -E AssertionError: build/config.local.env not present — run ./build/setup.sh. Run 'bash scripts/provision_full_test_env.sh'. - -tests/test_csv_loader_arbitrary_shapes.py:82: AssertionError -__________ ERROR at setup of CsvLoaderArbitraryShapes.test_shape_tiny __________ - -cls = - - @classmethod - def setUpClass(cls): - if not (_PG_AVAILABLE and _CONFIG_PRESENT): -> raise AssertionError( - f"{_SKIP_REASON}. Run 'bash scripts/provision_full_test_env.sh'.") -E AssertionError: build/config.local.env not present — run ./build/setup.sh. Run 'bash scripts/provision_full_test_env.sh'. - -tests/test_csv_loader_arbitrary_shapes.py:82: AssertionError -_________ ERROR at setup of TestDevSeedCounts.test_dev_seed_row_counts _________ - -cls = - -> setUpClass = classmethod(lambda cls: _require_deployed()) - ^^^^^^^^^^^^^^^^^^^ - -tests/test_parity.py:135: -_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - - def _require_deployed(): - _require_pg() - if not _DEV_DEPLOYED: -> raise AssertionError(f"Dev database not deployed. {_HELP}") -E AssertionError: Dev database not deployed. Run 'bash scripts/provision_full_test_env.sh' (needs a reachable PostgreSQL and PGUSER/PGHOST/PGPORT set). - -tests/test_parity.py:131: AssertionError -=================================== FAILURES =================================== -_______________ E2EPipelineWithDatabase.test_tier_i_idempotency ________________ - -self = - - def test_tier_i_idempotency(self): - """Tier I (idempotency) scenarios must pass against the running PostgreSQL.""" - r = subprocess.run( - [sys.executable, str(RUNNER), "--tiers", "i"], - capture_output=True, text=True, cwd=ROOT, - ) -> self.assertEqual(r.returncode, 0, - f"Tier I eval run failed:\n{r.stdout[-2000:]}\n{r.stderr[-500:]}") -E AssertionError: 1 != 0 : Tier I eval run failed: -E -E === Tier I - 1 scenarios === -E FAIL tier_i/01_deploy_dev_twice -E first_run_exit_code: expected 0, got 2; stderr: it/build/te_core_schema.sql:67: NOTICE: [DEV] Role "te_dev_user" already exists — password and conn_limit refreshed. -E psql:/home/claude/audit/build/te_core_schema.sql:69: ERROR: database "te_mgmt_dev" does not exist -E psql:/home/claude/audit/build/te_core_schema.sql:76: error: \connect: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: database "te_mgmt_dev" does not exist -E -E second_run_exit_code: expected 0, got 2; stderr: it/build/te_core_schema.sql:67: NOTICE: [DEV] Role "te_dev_user" already exists — password and conn_limit refreshed. -E psql:/home/claude/audit/build/te_core_schema.sql:69: ERROR: database "te_mgmt_dev" does not exist -E psql:/home/claude/audit/build/te_core_schema.sql:76: error: \connect: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: database "te_mgmt_dev" does not exist -E -E tables_present: expected >= 11, got 0 -E -E === Summary === -E total: 1 -E passed: 0 -E failed: 1 -E skipped: 0 -E -E report: /home/claude/audit/evals/reports/20260721T080448Z-f4b944/summary.json -E gap report: /home/claude/audit/evals/reports/20260721T080448Z-f4b944/VCRM_GAPS_20260721T080448Z-f4b944.md - -tests/test_e2e_pipeline.py:187: AssertionError -______ TestAllEnvironmentsHaveRequiredTables.test_dev_has_required_tables ______ - -self = - - def test_dev_has_required_tables(self): -> self._check_env("dev") - -tests/test_parity.py:178: -_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ -tests/test_parity.py:165: in _check_env - self.fail(f"Database {db!r} not deployed. {_HELP}") -E AssertionError: Database 'te_mgmt_dev' not deployed. Run 'bash scripts/provision_full_test_env.sh' (needs a reachable PostgreSQL and PGUSER/PGHOST/PGPORT set). -_____ TestAllEnvironmentsHaveRequiredTables.test_prod_has_required_tables ______ - -self = - - def test_prod_has_required_tables(self): -> self._check_env("prod") - -tests/test_parity.py:187: -_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ -tests/test_parity.py:165: in _check_env - self.fail(f"Database {db!r} not deployed. {_HELP}") -E AssertionError: Database 'te_mgmt_prod' not deployed. Run 'bash scripts/provision_full_test_env.sh' (needs a reachable PostgreSQL and PGUSER/PGHOST/PGPORT set). -____ TestAllEnvironmentsHaveRequiredTables.test_staging_has_required_tables ____ - -self = - - def test_staging_has_required_tables(self): -> self._check_env("staging") - -tests/test_parity.py:184: -_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ -tests/test_parity.py:165: in _check_env - self.fail(f"Database {db!r} not deployed. {_HELP}") -E AssertionError: Database 'te_mgmt_staging' not deployed. Run 'bash scripts/provision_full_test_env.sh' (needs a reachable PostgreSQL and PGUSER/PGHOST/PGPORT set). -_____ TestAllEnvironmentsHaveRequiredTables.test_test_has_required_tables ______ - -self = - - def test_test_has_required_tables(self): -> self._check_env("test") - -tests/test_parity.py:181: -_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ -tests/test_parity.py:165: in _check_env - self.fail(f"Database {db!r} not deployed. {_HELP}") -E AssertionError: Database 'te_mgmt_test' not deployed. Run 'bash scripts/provision_full_test_env.sh' (needs a reachable PostgreSQL and PGUSER/PGHOST/PGPORT set). -__ TestIdempotentDeployParity.test_dev_row_counts_stable_after_second_deploy ___ - -self = - - def test_dev_row_counts_stable_after_second_deploy(self): - env_sql = ROOT / "build" / "environments" / "env_dev.sql" - if not env_sql.exists(): - self.fail(f"env_dev.sql not found at {env_sql}. {_HELP}") - - db, schema = _ENV_CONFIG["dev"] - - def _snapshot() -> dict[str, int | None]: - return {t: _count_rows(db, schema, t) for t in _REQUIRED_TABLES} - - counts_before = _snapshot() - - r = subprocess.run( - ["psql", "-f", str(env_sql)], - env=_pg_env(), capture_output=True, text=True, timeout=180, - ) - if r.returncode != 0: -> self.fail(f"Second deploy failed:\n{r.stderr[-500:]}") -E AssertionError: Second deploy failed: -E psql:/home/claude/audit/build/te_core_schema.sql:67: NOTICE: [DEV] Role "te_dev_user" already exists — password and conn_limit refreshed. -E psql:/home/claude/audit/build/te_core_schema.sql:69: ERROR: database "te_mgmt_dev" does not exist -E psql:/home/claude/audit/build/te_core_schema.sql:76: error: \connect: connection to server at "localhost" (127.0.0.1), port 5432 failed: FATAL: database "te_mgmt_dev" does not exist - -tests/test_parity.py:215: AssertionError -=========================== short test summary info ============================ -ERROR tests/test_csv_loader_arbitrary_shapes.py::CsvLoaderArbitraryShapes::test_shape_medium -ERROR tests/test_csv_loader_arbitrary_shapes.py::CsvLoaderArbitraryShapes::test_shape_skinny -ERROR tests/test_csv_loader_arbitrary_shapes.py::CsvLoaderArbitraryShapes::test_shape_tiny -ERROR tests/test_parity.py::TestDevSeedCounts::test_dev_seed_row_counts - Ass... -FAILED tests/test_e2e_pipeline.py::E2EPipelineWithDatabase::test_tier_i_idempotency -FAILED tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables::test_dev_has_required_tables -FAILED tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables::test_prod_has_required_tables -FAILED tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables::test_staging_has_required_tables -FAILED tests/test_parity.py::TestAllEnvironmentsHaveRequiredTables::test_test_has_required_tables -FAILED tests/test_parity.py::TestIdempotentDeployParity::test_dev_row_counts_stable_after_second_deploy - -================================================================== - FINAL RESULT — every test accounted for -================================================================== - collected : 54 - executed : 54 - PASSED : 44 - FAILED : 6 - ERROR : 4 - SKIPPED : 0 - NOT RUN : 0 (deselected by the marker filter) - - FAILED - - tests.test_e2e_pipeline.E2EPipelineWithDatabase::test_tier_i_idempotency - AssertionError: 1 != 0 : Tier I eval run failed: - - tests.test_parity.TestAllEnvironmentsHaveRequiredTables::test_dev_has_required_tables - AssertionError: Database 'te_mgmt_dev' not deployed. Run 'bash scripts/provision_full_test_env.sh' (needs a re - - tests.test_parity.TestAllEnvironmentsHaveRequiredTables::test_prod_has_required_tables - AssertionError: Database 'te_mgmt_prod' not deployed. Run 'bash scripts/provision_full_test_env.sh' (needs a r - - tests.test_parity.TestAllEnvironmentsHaveRequiredTables::test_staging_has_required_tables - AssertionError: Database 'te_mgmt_staging' not deployed. Run 'bash scripts/provision_full_test_env.sh' (needs - - tests.test_parity.TestAllEnvironmentsHaveRequiredTables::test_test_has_required_tables - AssertionError: Database 'te_mgmt_test' not deployed. Run 'bash scripts/provision_full_test_env.sh' (needs a r - - tests.test_parity.TestIdempotentDeployParity::test_dev_row_counts_stable_after_second_deploy - AssertionError: Second deploy failed: - - ERROR - - tests.test_csv_loader_arbitrary_shapes.CsvLoaderArbitraryShapes::test_shape_medium - failed on setup with "AssertionError: build/config.local.env not present — run ./build/setup.sh. Run 'bash scr - - tests.test_csv_loader_arbitrary_shapes.CsvLoaderArbitraryShapes::test_shape_skinny - failed on setup with "AssertionError: build/config.local.env not present — run ./build/setup.sh. Run 'bash scr - - tests.test_csv_loader_arbitrary_shapes.CsvLoaderArbitraryShapes::test_shape_tiny - failed on setup with "AssertionError: build/config.local.env not present — run ./build/setup.sh. Run 'bash scr - - tests.test_parity.TestDevSeedCounts::test_dev_seed_row_counts - failed on setup with "AssertionError: Dev database not deployed. Run 'bash scripts/provision_full_test_env.sh' - - SKIPPED (0) - none — no test was skipped - - NOT RUN (0) - none — every collected test was executed - -================================================================== - RESULT: FAIL -================================================================== - diff --git a/test-artifacts/10_evals_summary.json b/test-artifacts/10_evals_summary.json deleted file mode 100644 index adbf289..0000000 --- a/test-artifacts/10_evals_summary.json +++ /dev/null @@ -1,1422 +0,0 @@ -{ - "run_id": "20260721T080526Z-5372c3", - "started_at": "2026-07-21T08:05:28.379784+00:00", - "tiers": [ - "p", - "i", - "s" - ], - "totals": { - "total": 25, - "passed": 25, - "failed": 0, - "skipped": 0 - }, - "scenarios": [ - { - "tier": "p", - "name": "01_happy_path", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 3 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 3\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ], - [ - "3", - "Carol" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "01_happy_path", - "description": "Three valid rows, no headers issues.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 3" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ], - [ - "3", - "Carol" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "02_empty_file", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 1, - "stdout": "", - "stderr": "\u001b[0;31m [validator ERR]\u001b[0m CSV file is empty — no header row found.\n", - "valid_csv_rows": [], - "skip_csv_rows": [], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "02_empty_file", - "description": "0-byte CSV.", - "runner_action": "default", - "expected": { - "exit_code": 1, - "stdout_contains": [], - "stderr_contains": [ - "CSV file is empty" - ], - "valid_csv_rows": null, - "skip_csv_row_count": null, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "03_empty_header_only_newline", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 1, - "stdout": "", - "stderr": "\u001b[0;31m [validator ERR]\u001b[0m Header row is empty.\n", - "valid_csv_rows": [], - "skip_csv_rows": [], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "03_empty_header_only_newline", - "description": "File contains only a newline — header row exists but has zero columns.", - "runner_action": "default", - "expected": { - "exit_code": 1, - "stdout_contains": [], - "stderr_contains": [ - "Header row is empty" - ], - "valid_csv_rows": null, - "skip_csv_row_count": null, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "04_no_valid_rows", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 1, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 0\n\u001b[1;33m [validator WARN]\u001b[0m Skipped rows : 2 — written to: /tmp/eval_04_no_valid_rows_2h785vht/skip.csv\n", - "stderr": "\u001b[0;31m [validator ERR]\u001b[0m No valid rows found. Nothing to load.\n", - "valid_csv_rows": [ - [ - "id", - "name" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ], - [ - "empty row — all values blank" - ], - [ - "", - "", - "empty row — all values blank" - ] - ], - "skip_csv_row_count": 2 - }, - "expected": { - "scenario": "04_no_valid_rows", - "description": "Header present, but every data row is empty/blank.", - "runner_action": "default", - "expected": { - "exit_code": 1, - "stdout_contains": [], - "stderr_contains": [ - "No valid rows found" - ], - "valid_csv_rows": [ - [ - "id", - "name" - ] - ], - "skip_csv_row_count": 2, - "skip_reasons_contain": [ - "empty row" - ] - } - } - }, - { - "tier": "p", - "name": "05_mixed_valid_skipped", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 4 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n\u001b[1;33m [validator WARN]\u001b[0m Skipped rows : 2 — written to: /tmp/eval_05_mixed_valid_skipped_mc1hth__/skip.csv\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "3", - "Bob" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ], - [ - "empty row — all values blank" - ], - [ - "oops", - "too", - "many", - "fields", - "column mismatch — expected 2, got 4" - ] - ], - "skip_csv_row_count": 2 - }, - "expected": { - "scenario": "05_mixed_valid_skipped", - "description": "Two valid rows, one empty, one with too many fields.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2", - "Skipped rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "3", - "Bob" - ] - ], - "skip_csv_row_count": 2, - "skip_reasons_contain": [ - "empty row", - "column mismatch" - ] - } - } - }, - { - "tier": "p", - "name": "06_duplicate_headers", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 3 columns — id | id | name\n\u001b[1;33m [validator WARN]\u001b[0m Duplicate column names: id\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 1 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 1\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "id", - "name" - ], - [ - "1", - "2", - "Alice" - ] - ], - "skip_csv_rows": [ - [ - "id", - "id", - "name", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "06_duplicate_headers", - "description": "Duplicate column name in header — validator warns but still processes row.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Duplicate column names", - "id" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "id", - "name" - ], - [ - "1", - "2", - "Alice" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "07_column_mismatch_short", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 3 columns — id | first | last\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 3 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n\u001b[1;33m [validator WARN]\u001b[0m Skipped rows : 1 — written to: /tmp/eval_07_column_mismatch_short_cdk64d5u/skip.csv\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "first", - "last" - ], - [ - "1", - "Alice", - "Anderson" - ], - [ - "3", - "Carol", - "Smith" - ] - ], - "skip_csv_rows": [ - [ - "id", - "first", - "last", - "_skip_reason" - ], - [ - "2", - "Bob", - "column mismatch — expected 3, got 2" - ] - ], - "skip_csv_row_count": 1 - }, - "expected": { - "scenario": "07_column_mismatch_short", - "description": "Header has 3 columns; one data row has only 2.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2", - "Skipped rows : 1" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "first", - "last" - ], - [ - "1", - "Alice", - "Anderson" - ], - [ - "3", - "Carol", - "Smith" - ] - ], - "skip_csv_row_count": 1, - "skip_reasons_contain": [ - "column mismatch — expected 3, got 2" - ] - } - } - }, - { - "tier": "p", - "name": "08_column_mismatch_long", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 3 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n\u001b[1;33m [validator WARN]\u001b[0m Skipped rows : 1 — written to: /tmp/eval_08_column_mismatch_long_m5gh8fm2/skip.csv\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "3", - "Carol" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ], - [ - "2", - "Bob", - "extra", - "extra", - "extra", - "column mismatch — expected 2, got 5" - ] - ], - "skip_csv_row_count": 1 - }, - "expected": { - "scenario": "08_column_mismatch_long", - "description": "Header has 2 columns; one data row has 5.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2", - "Skipped rows : 1" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "3", - "Carol" - ] - ], - "skip_csv_row_count": 1, - "skip_reasons_contain": [ - "column mismatch — expected 2, got 5" - ] - } - } - }, - { - "tier": "p", - "name": "09_empty_row", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 3 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n\u001b[1;33m [validator WARN]\u001b[0m Skipped rows : 1 — written to: /tmp/eval_09_empty_row_7exit5kj/skip.csv\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ], - [ - "", - "", - "empty row — all values blank" - ] - ], - "skip_csv_row_count": 1 - }, - "expected": { - "scenario": "09_empty_row", - "description": "A row with only commas (every cell empty) should be skipped.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2", - "Skipped rows : 1" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_row_count": 1, - "skip_reasons_contain": [ - "empty row" - ] - } - } - }, - { - "tier": "p", - "name": "10_utf8_bom", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "10_utf8_bom", - "description": "File starts with the UTF-8 BOM — utf-8-sig should consume it so headers parse cleanly.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "11_utf8_emoji", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice 👋" - ], - [ - "2", - "Bob 🚀" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "11_utf8_emoji", - "description": "Emoji in text values — must survive round-trip through the validator.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice 👋" - ], - [ - "2", - "Bob 🚀" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "12_crlf_line_endings", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "12_crlf_line_endings", - "description": "CRLF (Windows) line endings — csv module handles them natively.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "13_quoted_comma", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | note\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "note" - ], - [ - "1", - "Smith, John" - ], - [ - "2", - "Doe, Jane" - ] - ], - "skip_csv_rows": [ - [ - "id", - "note", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "13_quoted_comma", - "description": "Quoted field containing a comma must be preserved as a single field.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "note" - ], - [ - "1", - "Smith, John" - ], - [ - "2", - "Doe, Jane" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "14_quoted_newline", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | note\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "note" - ], - [ - "1", - "line1\nline2" - ], - [ - "2", - "single line" - ] - ], - "skip_csv_rows": [ - [ - "id", - "note", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "14_quoted_newline", - "description": "Quoted field containing an embedded newline must be parsed as one row.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "note" - ], - [ - "1", - "line1\nline2" - ], - [ - "2", - "single line" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "15_quoted_quote", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | note\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "note" - ], - [ - "1", - "she said \"hi\"" - ], - [ - "2", - "plain" - ] - ], - "skip_csv_rows": [ - [ - "id", - "note", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "15_quoted_quote", - "description": "Quoted field with escaped double-quote becomes a literal quote in the value.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "note" - ], - [ - "1", - "she said \"hi\"" - ], - [ - "2", - "plain" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "16_whitespace_only_row", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 3 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n\u001b[1;33m [validator WARN]\u001b[0m Skipped rows : 1 — written to: /tmp/eval_16_whitespace_only_row_h21f61qs/skip.csv\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ], - [ - " ", - "", - "empty row — all values blank" - ] - ], - "skip_csv_row_count": 1 - }, - "expected": { - "scenario": "16_whitespace_only_row", - "description": "A row with only whitespace in every cell should be treated as empty (cell.strip()).", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2", - "Skipped rows : 1" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_row_count": 1, - "skip_reasons_contain": [ - "empty row" - ] - } - } - }, - { - "tier": "p", - "name": "17_header_whitespace", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "17_header_whitespace", - "description": "Leading/trailing whitespace in header cells should be stripped.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ], - [ - "2", - "Bob" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "18_utf8_cjk", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "田中花子" - ], - [ - "2", - "佐藤一郎" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "18_utf8_cjk", - "description": "CJK characters must survive the round-trip.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "田中花子" - ], - [ - "2", - "佐藤一郎" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "19_missing_env_vars", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 1, - "stdout": "", - "stderr": "\u001b[0;31m [validator ERR]\u001b[0m Missing required environment variables: CSV_FILE, VALID_CSV, SKIP_FILE\n", - "valid_csv_rows": null, - "skip_csv_rows": null, - "skip_csv_row_count": null - }, - "expected": { - "scenario": "19_missing_env_vars", - "description": "Runner intentionally omits CSV_FILE, VALID_CSV, SKIP_FILE — validator should reject.", - "runner_action": "omit_env_vars", - "expected": { - "exit_code": 1, - "stdout_contains": [], - "stderr_contains": [ - "Missing required environment variables" - ], - "valid_csv_rows": null, - "skip_csv_row_count": null, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "20_missing_csv_file", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 1, - "stdout": "", - "stderr": "\u001b[0;31m [validator ERR]\u001b[0m CSV file not found: /tmp/eval_20_missing_csv_file_qwta_esp/does_not_exist.csv\n", - "valid_csv_rows": null, - "skip_csv_rows": null, - "skip_csv_row_count": null - }, - "expected": { - "scenario": "20_missing_csv_file", - "description": "Runner sets CSV_FILE to a path that does not exist.", - "runner_action": "point_at_missing_file", - "expected": { - "exit_code": 1, - "stdout_contains": [], - "stderr_contains": [ - "CSV file not found" - ], - "valid_csv_rows": null, - "skip_csv_row_count": null, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "21_utf8_arabic", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 2\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "محمد" - ], - [ - "2", - "فاطمة" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "21_utf8_arabic", - "description": "Arabic / RTL Unicode text must survive the round-trip.", - "runner_action": "default", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 2" - ], - "stderr_contains": [], - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "محمد" - ], - [ - "2", - "فاطمة" - ] - ], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "22_very_long_field", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | payload\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 1 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 1\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "payload" - ], - [ - "1", - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ] - ], - "skip_csv_rows": [ - [ - "id", - "payload", - "_skip_reason" - ] - ], - "skip_csv_row_count": 0 - }, - "expected": { - "scenario": "22_very_long_field", - "description": "A 50KB single-field value should be accepted as one valid row.", - "runner_action": "write_long_field_file", - "field_size_bytes": 50000, - "table_name": "payloads", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 1" - ], - "stderr_contains": [], - "skip_csv_row_count": 0, - "skip_reasons_contain": [] - } - } - }, - { - "tier": "p", - "name": "23_invalid_utf8_bytes", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "exit_code": 0, - "stdout": "\u001b[0;32m [validator OK]\u001b[0m Delimiter: comma\n\u001b[0;32m [validator OK]\u001b[0m Header: 2 columns — id | name\n\u001b[0;32m [validator OK]\u001b[0m Validation complete — 2 rows processed.\n\u001b[0;32m [validator OK]\u001b[0m Valid rows : 1\n\u001b[1;33m [validator WARN]\u001b[0m Skipped rows : 1 — written to: /tmp/eval_23_invalid_utf8_bytes_810kv0l9/skip.csv\n", - "stderr": "", - "valid_csv_rows": [ - [ - "id", - "name" - ], - [ - "1", - "Alice" - ] - ], - "skip_csv_rows": [ - [ - "id", - "name", - "_skip_reason" - ], - [ - "2", - "?", - "invalid UTF-8 bytes at line 3" - ] - ], - "skip_csv_row_count": 1 - }, - "expected": { - "scenario": "23_invalid_utf8_bytes", - "description": "Invalid UTF-8 bytes in a data row are skipped with a reason; valid rows still pass. Only an undecodable header aborts the file.", - "runner_action": "write_invalid_utf8_file", - "expected": { - "exit_code": 0, - "stdout_contains": [ - "Valid rows : 1", - "Skipped rows : 1" - ], - "stderr_contains": [], - "skip_csv_row_count": 1, - "skip_reasons_contain": [ - "invalid UTF-8 bytes" - ] - } - } - }, - { - "tier": "i", - "name": "01_deploy_dev_twice", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "first_run_exit_code": 0, - "second_run_exit_code": 0, - "row_counts_first": { - "organisations": 5, - "personnel": 6, - "test_programs": 2, - "temp_documents": 3, - "test_phases": 3, - "requirements": 8, - "test_cases": 8, - "vcrm_entries": 8, - "test_events": 3, - "test_results": 7, - "defect_reports": 3 - }, - "row_counts_second": { - "organisations": 5, - "personnel": 6, - "test_programs": 2, - "temp_documents": 3, - "test_phases": 3, - "requirements": 8, - "test_cases": 8, - "vcrm_entries": 8, - "test_events": 3, - "test_results": 7, - "defect_reports": 3 - }, - "row_counts_unchanged": true, - "tables_present": 11 - }, - "expected": { - "scenario": "01_deploy_dev_twice", - "description": "Deploy env_dev.sql twice; row counts must not change between runs.", - "expected": { - "first_run_exit_code": 0, - "second_run_exit_code": 0, - "row_counts_unchanged": true, - "min_seeded_tables_present": 11 - } - } - }, - { - "tier": "s", - "name": "01_fresh_deploy_then_all_tests_pass", - "passed": true, - "skipped": false, - "errors": [], - "actual": { - "deploy_exit_code": 0, - "tests_exit_code": 0, - "stdout_tail": " | \n vcrm | K05 — No duplicate req↔tc entries in VCRM (UNIQUE enforced) | PASS | 0 | 0 | \n vcrm | K06 — All VCRM entries reference a valid requirement (FK check) | PASS | 0 | 0 | \n vcrm | K07 — All VCRM entries reference a valid test case (FK check) | PASS | 0 | 0 | \n vcrm | K08 — All coverage_type values are valid | PASS | 0 | 0 | \n vcrm | K09 — LAND400-P3 requirements correctly have no VCRM entries yet | PASS | 0 | 0 | \n vcrm | K10 — CYB9131 VCRM coverage is 100% | PASS | 100.0 | 100.0 | \n(142 rows)\n\n\n============================================================\n REPORT 4: Overall Result\n============================================================\n total_tests | passed | failed | skipped | pass_rate | overall \n-------------+--------+--------+---------+-----------+--------------------\n 142 | 142 | 0 | 0 | 100.0% | ✓ ALL TESTS PASSED\n(1 row)\n\n\n============================================================\n Test run complete.\n============================================================\n\n", - "stderr_tail": "", - "total_assertions": 142, - "pass_rate": 100.0 - }, - "expected": { - "scenario": "01_fresh_deploy_then_all_tests_pass", - "description": "Fresh Dev deploy followed by run_all_tests.sql must report 142/142 pass.", - "expected": { - "deploy_exit_code": 0, - "tests_exit_code": 0, - "stdout_contains": [ - "ALL TESTS PASSED" - ], - "min_total_assertions": 142, - "min_pass_rate_percent": 100.0 - } - } - } - ] -} \ No newline at end of file diff --git a/test-artifacts/11_vcrm_gap_report.md b/test-artifacts/11_vcrm_gap_report.md deleted file mode 100644 index 665fd2f..0000000 --- a/test-artifacts/11_vcrm_gap_report.md +++ /dev/null @@ -1,60 +0,0 @@ -# VCRM Gap Report - run 20260721T080526Z-5372c3 - -Generated: 2026-07-21T08:05:28.383412+00:00 -Run started: 2026-07-21T08:05:28.379784+00:00 -Tier eval totals: {"total": 25, "passed": 25, "failed": 0, "skipped": 0} - -Auto-generated by `evals/gap_report.py` at the end of `evals/runner.py`. -Reflects this run's outcomes only. For the static catalogue see `VCRM.md` and `VCRM_GAPS.md`. - ---- - -## Summary - -| Status | Count | Meaning | -|--------|------:|---------| -| VERIFIED | 17 | Baseline=full and >=1 covering eval passed | -| PARTIAL | 1 | Baseline=partial; some aspects verified | -| REGRESSION | 0 | A covering eval FAILED this run - investigate | -| SKIPPED | 0 | All covering evals skipped (e.g. PG unavailable) | -| UNVERIFIED | 2 | No covering eval - this is a real gap | -| OUT-OF-BAND | 0 | Verified by PU/SQL suites, not by evals | -| DEFERRED | 2 | Out-of-scope by project decision | - -## Unverified - no eval coverage - -- **BR-02** Six DB engines supported via adapters - - No non-PG engine has any test coverage (Tier X gap) -- **BR-15** Per-environment connection limits enforced - - No test asserts pg_roles.rolconnlimit. 1-hour fix available. - -## Per-requirement status (all 22 BRs) - -| ID | Status | Title | Evidence (this run) | -|----|--------|-------|---------------------| -| BR-01 | PARTIAL | Multi-environment deployment (Dev/Test/Staging/Prod isolated) | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-02 | GAP | Six DB engines supported via adapters | No eval covers this BR | -| BR-03 | OK | Schema is fully parameterised via \set | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-04 | OK | 12-table T&E data model present | tier_i/01_deploy_dev_twice: PASS; tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-05 | OK | 100% VCRM coverage for CYB9131 + gap detection for LAND400 | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-06 | OK | TEMP versioning (draft -> approved -> superseded) | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-07 | OK | Test result verdict enum + linkage | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-08 | OK | DR severity + resolved_at lifecycle | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-09 | OK | Idempotent deployment | tier_i/01_deploy_dev_twice: PASS; tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-10 | OK | CSV pre-ingestion validation rejects malformed input | tier_p/02_empty_file: PASS; tier_p/03_empty_header_only_newline: PASS; tier_p/04_no_valid_rows: PASS; tier_p/07_column_mismatch_short: PASS; tier_p/08_column_mismatch_long: PASS; tier_p/19_missing_env_vars: PASS; tier_p/20_missing_csv_file: PASS; tier_p/23_invalid_utf8_bytes: PASS | -| BR-11 | OK | Valid / skip row separation with reasons | tier_p/05_mixed_valid_skipped: PASS; tier_p/09_empty_row: PASS; tier_p/16_whitespace_only_row: PASS | -| BR-12 | OK | Clearance enum {baseline,NV1,NV2,PV} | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-13 | OK | ISM classification marking enum | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-14 | OK | Phase type enum (DT&E/AT&E/OT&E/...) | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-15 | GAP | Per-environment connection limits enforced | No eval covers this BR | -| BR-16 | OK | Automated single-command regression | tier_p/(any scenario): PASS; tier_i/(any scenario): PASS; tier_s/(any scenario): PASS | -| BR-17 | OK | Graceful degradation when PG unavailable | tier_i/01_deploy_dev_twice: PASS; tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-18 | OK | Machine-readable JSON report per run | Implicit (Inspection) | -| BR-19 | OK | Build/tests/evals physically segregated | Implicit (Inspection) | -| BR-20 | OK | 142/142 SQL assertions pass | tier_s/01_fresh_deploy_then_all_tests_pass: PASS | -| BR-21 | DEFERRED | Cross-engine schema equivalence | - | -| BR-22 | DEFERRED | Performance at >= 1M rows | - | - ---- - -Companion: `VCRM.md` (catalogue), `VCRM_GAPS.md` (static gap analysis), `TEST_CONDITIONS.md` (every test).