Repository navigation
fix: address 15 open issues — security, data integrity, tests, CI - #202
Merged
Merged
Conversation
#183 — Rate limiter uses peer IP instead of API key as identity, preventing bucket-rotation bypass via arbitrary X-API-Key values. #178 — CSV row dedup uses JSON serialization (v2: prefix) instead of U+001F join, preventing distinct rows with embedded separators from hashing identically (silent data loss). #175 — Upload overwrite and T&E mode-switch now check delete permission via a centralised destructive_ops module; previously bypassed the gate. #170 — All-failed T&E import rolls back the transaction and preserves the prior dataset instead of running cleanup that destroys it. #189 — T&E row errors now report the original CSV row number instead of the position in the survivor list (post-validation subset). #171 — setup.sh writes config.local.env atomically (temp file + mv), so a failed heredoc expansion no longer truncates the prior config. #180 — Tier I idempotency eval adds ON_ERROR_STOP=1 to psql so SQL errors actually fail the eval instead of being silently swallowed. #181 — run_tests.sh parses psql output for FAIL/assertion lines, catching cases where psql exits 0 despite SQL-level test failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
The all-failed rollback (#170) was triggering prematurely for T&E reuploads: the duplicate-key violations caused all rows to fail, which rolled back the transaction before the registry cleanup could run. Move reupload detection and cleanup before the insert phase so the rollback does not undo it, and skip the all-failed error for reuploads where the data already exists. Also update 3 tests that send a single bad row to expect status='error' (correct behaviour when every row fails) instead of 'ok'. Fixes: integration CI failures on PR #202 - test_enum_constraint_violation_reported_per_row - test_null_in_not_null_column_reported_per_row - test_row_level_error_reported - test_te_reupload_same_file_replaces_registry - test_reupload_cleans_prior_registry_entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
…165 #166) - Add SUPERSEDED banner to API_INTEGRATION.md redirecting to OpenAPI docs - Update STARTER_GUIDE.md: add sign-in step (#166), remove dedup step (#165), point API reference to OpenAPI docs (#164) - Update QUICKSTART.md: replace API_INTEGRATION.md references with OpenAPI docs - Update README.md tree entry with "(historical)" annotation - Add accounting identity assertions and multi-ragged-row test (#192) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
T&E uploads stored an empty column-type list, so review fell back to text and returned errors: [] for rows a CHECK/FK/UNIQUE rejected. - te_loader returns the target's mapped column types, so review revalidates T&E values against them (unmapped types stay text). - review attaches the stored upload-time database reason as a "(database)" error on unedited T&E rows. - review table shows any error not tied to a column beside the row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T6Wqi8yaiaYWUVH8C4QAY
4 of 5 tasks
…test #193 — XLSX export path now sanitizes cell values through the same formula guard as CSV (OWASP CSV injection). Cells starting with =, +, -, @, tab or CR are prefixed with a single quote unless they are plain numbers. Added 6 vitest assertions verifying XLSX formula neutralization and that plain numbers pass through unchanged. #191 — New integration test (test_registry_race.py) exercises concurrent uploads of the same file and same-name-different-content uploads, asserting that the UNIQUE constraints on csv_files settle the race with exactly one registry row and no partial state. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
Throwaway, one-file Streamlit rebuild of the review screen against the existing API: paging, edit and exclude/include with required reason, history, role gating, red/blue cell tint. README records what was comfortable, where it strained, and what each #154 outcome means for the parked issues. Not wired into CI or the build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T6Wqi8yaiaYWUVH8C4QAY
#194 — Integration CI job now measures coverage for api/services and enforces per-file floors: csv_parse.py ≥ 79%, dynamic_loader.py ≥ 84%, te_loader.py ≥ 96%. A new script (scripts/check_coverage_floors.py) reads the JSON coverage report and fails the build if any loader drops below its floor. #169 — New script (scripts/test_catalogue.py) walks the test tree, extracts test names, markers and docstrings, and generates docs/TEST_CASES.md. The free-tier CI job runs --check mode to fail the build when the catalogue is stale. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
The guided walkthrough was drafted before ART Milestone 2 landed and described duplicate detection inaccurately. - Add a sign-in step: the app has required authentication since M2, so localhost:8080 opens on the sign-in page, not the upload area. Points at QUICKSTART's "Create your sign-in (first run only)" section rather than duplicating the create_tenant_admin.py command (#166). - Fix the duplicate-detection step. Content-hash dedup IS shipped for dynamic mode (api/services/dynamic_loader.py checks filename then content hash and returns status "duplicate_file"), but T&E mode replaces the prior registry entry instead of refusing (api/services/te_loader.py _cleanup_prior_registry_entries). The step now names the dynamic-mode file and scopes the claim to that mode, noting per-tenant/per-record dedup is still M5 scope (#165). - Renumber the sequence 1-6 and correct "all five" to "all six". - Soften the API_INTEGRATION.md pointer: it documents 8 of the 21 live routes, so "every frontend<->backend API call" overclaimed. Points at the OpenAPI schema for the complete list. - Drop the stale "API_INTEGRATION.md describes a superseded API" line from docs/prompts/ART_FRAMEWORK.md: the file documents current endpoints including M2 auth and was last updated 4 Nov 2026. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T6Wqm1jLdq3ZEfuKdp9aM
Remove unused datetime/timezone imports (F401) and replace empty f-strings with plain strings (F541). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
The contract test still expected types: [] for T&E uploads. Update it, and the M4 test-case note that described the old "reviewed as text" limit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T6Wqi8yaiaYWUVH8C4QAY
The frontend sent headerMapping but UploadRequest had no such field, so Pydantic dropped it and the imported table kept the CSV's original headers with no error. - UploadRequest accepts a validated headerMapping (bounded lengths/count). - apply_header_mapping renames raw headers before sanitisation; a blank rename keeps the original. Unknown or repeated originals, and renames that would collide with another column, return a structured invalid_header_mapping error and load nothing, rather than bending the name to name_2. - T&E mode rejects headerMapping with 422: its columns must match the fixed schema, so a rename could only be silently ignored. - Tests assert the resulting database column names (foo -> bar), not just the request object. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
…rrors fix(review): T&E review shows why the database rejected a row (#190)
…pike spike(#154): M4 review screen in Streamlit
) The hand-kept endpoint table documented 8 of the 22 live routes. Per the product owner's choice (option b on #207), drop it and point at the OpenAPI schema FastAPI generates from the code, so it cannot drift. The auth model, known-gaps findings and migrations sections are kept. The docs path is /docs (Swagger UI) and /openapi.json, verified against the real app with TestClient: /docs 200, /openapi.json 200 with 22 operations, /api/docs 404. No docs_url is set in api/main.py, and auth is per-router, so neither route needs a key. - Correct the earlier-introduced "/api/docs" in STARTER_GUIDE.md and ART_FRAMEWORK.md to "/docs" (the path was copied from #164 unchecked). - Re-label the API_INTEGRATION.md entries in README.md and QUICKSTART.md from "endpoint mapping" to what the file now is. - Add a STARTER_GUIDE.md row pointing at Swagger UI for routes. - Drop a stale "endpoint table" reference to README in the doc's header. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T6Wqm1jLdq3ZEfuKdp9aM
…text (#185) The browser decodes the CSV to text (dropping a BOM, replacing invalid bytes) and the API re-encoded that text before hashing and storing it, so the preserved "original" and its sha256 could not match the uploader's file. - UploadRequest accepts contentBase64, the file's raw bytes. record_upload hashes and stores those as the original; the loaders and source_rows still use the decoded text, so the decoded view is unchanged. Bad base64 is a 422, oversize a 413. Without contentBase64 the previous behaviour (UTF-8 encoding of content) is kept for API clients. - The uploader reads the file's bytes (readFileBase64) and sends them with the text. - Tests: BOM-prefixed and 0x80-byte CSVs round-trip byte-identical with a matching hash, for both successful and failed uploads. A negative control with the fix stashed fails those tests. Not done here: a separate encoding-metadata column for the decoded view. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
amar-python
marked this pull request as ready for review
November 9, 2026 04:15
…owns them My earlier docs commit duplicated PR #206 and had two defects, both verified against the app with TestClient: - It pointed readers at /api/docs, which returns 404. The live OpenAPI UI is /docs. - It removed the duplicate-detection step from STARTER_GUIDE.md on the strength of #165, but content-hash and filename duplicate detection are shipped for dynamic mode (dynamic_loader.py returns duplicate_file). Restore these four files to their base versions so this PR no longer touches them and cannot conflict with #206, which rewrites the same text with the correct path and scoping. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy
…ation-qvkl4l' into scratch-merge-202
#203 added test_te_review_shows_the_database_reason_a_row_was_rejected after this branch's catalogue was generated, so the new catalogue check failed once the two met. Regenerated with scripts/test_catalogue.py. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011T6Wqi8yaiaYWUVH8C4QAY
This was referenced Nov 9, 2026
This was referenced Nov 9, 2026
Closed
1 task done
This was referenced Nov 9, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes fifteen open issues across security, data integrity, test reliability and CI:
headerMapping)Correction: an earlier revision of this description also listed #195 (type provenance). That commit was pushed after this PR merged and is not in
main; it is PR #213.Not in this PR: the docs issues #164, #165 and #166. PR #206 rewrote the same text; my earlier docs edits had two defects (they linked
/api/docs, a 404 — the live path is/docs— and removed a duplicate-detection step that is shipped for dynamic mode) and were backed out in ac89545.Changes
Security & data integrity
api/ratelimit.py—_client_id()uses peer IP only; API key removed from identityapi/services/destructive_ops.py(new) — centralised auth gate for table dropsapi/services/dynamic_loader.py— row dedup switched to JSON serialization (v2:prefix)api/services/te_loader.py— tracks original CSV row numbers; rolls back on 100% failure; reupload detection moved before insertscsv-table-hub-main/src/lib/export.ts— XLSX export now sanitizes formula-starting cells (same guard as CSV)Header mapping (#186)
UploadRequestaccepts a validatedheaderMapping;apply_header_mapping()renames raw headers before sanitisation. Unknown or repeated originals, and renames that would collide with another column, are rejected (invalid_header_mapping) rather than bent toname_2. T&E mode returns 422 for a mapping instead of ignoring it.tests/test_header_mapping.pymapsfoo→barand asserts the database column name.Preserved original bytes (#185)
UploadRequestacceptscontentBase64;record_upload(..., original=)hashes and stores those bytes. The loaders andsource_rowsstill use the decoded text. The uploader sends the file's bytes (file-bytes.ts).tests/test_original_bytes.py: BOM-prefixed and lone-0x80CSVs round-trip byte-identical with a matching hash.readAsTextlegitimately decodes UTF-16.Test reliability, CI and bug fixes
tests/test_api_coverage.py,tests/test_ragged_rows.py,tests/test_registry_race.py(new), XLSX formula tests inexport.test.ts.github/workflows/quality-gate.yml— coverage gate and test catalogue freshness check;scripts/check_coverage_floors.pyandscripts/test_catalogue.py(new);docs/TEST_CASES.md(generated)build/setup.shatomic config write;evals/runner.pyON_ERROR_STOP=1;tests/run_tests.shparses output for FAIL linesNotes
fix/181-sql-suite-exit-code(fd305d7, no PR) makestests/run_all_tests.sqlraise at the end of a failing run and addsON_ERROR_STOP=1to the evals runner's suite call. That is the more robust root-cause fix; this PR'srun_tests.shchange greps psql output. Mine was checked on a real database (exit 0 on a clean dev DB, 142/142; exit 1 and the failing assertion named on a polluted one). Suggest taking the branch's and dropping this PR'srun_tests.shhunk.maingains tests that an open PR'sdocs/TEST_CASES.mdlacks (it hit this PR when RV-F13: T&E review displays invalid rows without validation errors (empty type list) #190 merged); the fix is to regenerate withpython3 scripts/test_catalogue.py.🤖 Generated with Claude Code
https://claude.ai/code/session_01VBrxqChRJtxdvSpFhiUWUy