# Import review verification — 6 September 2026

Requested model: **gpt-6-astra**, reasoning **xhigh**. No additional agents were used.

Worktree: `/home/stevan/dev/compass-verify-imports`
Branch: `fix/review-20260906-verify-imports`
Starting revision: `d6a057f9f`
Commit for integration: **`9cbe27cf3064324f7d27d8fc8c06fa256f8969da`** — `[IMPORT] Preserve data on failed imports and isolate extraction retries`

Read `CLAUDE.md`, the eight original findings in `findings.json`, the claimed fixes in `fixes.md`, and relevant app, request, service, worker, model, and browser specs. Only this worktree was changed. The additive migration and schema ownership were explicitly confirmed by the parent during verification.

## Verdict by original finding

| ID | Verdict | Evidence and resulting behavior |
|---|---|---|
| #3 | Fixed remaining gaps | Existing zero/one/all checkbox cases passed. New request regression proved an empty replace-all without `row_selection` could delete originals and complete. Confirmation now rejects empty inputs independently of the form marker. Selected `skip`, `keep`, or unknown actions cannot authorize an empty replacement. Pending/failed uploads cannot confirm stale preview rows. Existing partial and full selection request cases and the real browser selection flow remain green. |
| #6 | Fixed remaining validation gaps | Native `false`, `true`, accepted string forms, and blank defaults already survived extraction → preview → persistence for corridors and jurisdictions. New tests showed malformed JSON scalar/array/object values failed the entire extraction and duplicate document keys bypassed spreadsheet validation. They now produce invalid preview rows, with valid extracted rows retained. Shared duplicate validation also rechecks edited and unchanged rows, allowing a resolved duplicate to become valid. Invalid raw field types remain invalid until that field is corrected. Allowed sell currencies now normalize and validate before import. |
| #7 | Fixed remaining atomicity gap | Existing invalid-enum second-row request tests correctly rolled back both merge and replace-all. A new real request proved `ActiveRecord::RecordInvalid` was still swallowed per row, so earlier writes/deletions committed and the upload was marked completed. Removing that rescue makes persistence validation failures roll back the same savepoint as other fatal errors. Both modes now preserve original record attributes, retain the preview, report failure, and successfully retry after correction. |
| #15 | Pass, with accessibility follow-up | Existing browser test observed pending → failed, visible safe text, retained upload URL, Retry, and Back to capabilities. The changing error paragraph now also has `role=status`, `aria-live=polite`, and `aria-atomic=true`; the browser regression checks the live region. Queue failures leave a failed upload with recovery controls rather than a permanent spinner. |
| #26 | Pass | Real browser checks individual and bulk deselection, selected footer count, select-all indeterminate state, zero-selection disabled confirmation, and preservation of completed/superseded locks. Existing request tests verify the checked rows are the records actually applied. |
| #27 | Fixed remaining retry/state gaps | Existing transient Anthropic failures, bounded retry exhaustion, later-attempt success, duplicate delivery, and completed-upload protection passed. New regressions reproduced an old in-flight extraction overwriting a manual retry, and an enqueue error leaving the upload stuck pending. A durable `extraction_attempt` counter now binds delivery, result persistence, and exhaustion to one attempt. Legacy one-argument jobs are attempt 0 and cannot affect a manual retry at attempt 1. Current attempt success/exhaustion and attachment-analysis touches are tested. Edits are locked and restricted to active previews. Source-URL HTTP 429/500/503 now reach Sidekiq's retry policy as safe retryable errors. |
| #30 | Pass | Real OLE2/XLS fixture is rejected before upload/attachment persistence with conversion guidance. Renamed OLE2 workbook rejection, parser-level legacy defense, CSV with Excel MIME type, and valid XLSX still pass. |
| #32 | Pass | Synthetic WebMock response tests cover over-limit chunked content without Content-Length, oversized Content-Length, disallowed binary types, HTML/XHTML/plain text, 200,000-character submission limit, timeout backstop, monotonic elapsed deadline, no redirect following, and blocked private targets. No unbounded response buffering was reintroduced. |

No validated functional issue in these eight findings remains deferred.

## Migration and retry identity

New migration: `db/migrate/20260906090000_add_extraction_attempt_to_compass_uploads.rb`.

It adds `extraction_attempt integer DEFAULT 0 NOT NULL`. Initial and legacy jobs use 0. A manual retry increments the counter under the upload lock; enqueue happens after the pending state commits. Both normal completion and retry exhaustion compare the counter under lock.

An initial implementation used `updated_at`. Verification identified that ActiveStorage blob analysis touches the upload, so a valid job could be invalidated. The final implementation uses the dedicated counter and includes an attachment-touch regression. Existing one-argument queued jobs are explicitly isolated after a manual retry.

The new migration completed on `currencytransfer_test96`. A transactional populated-row down/up exercise verified preservation of existing attributes, default 0 on an existing row, NOT NULL, and a persisted increment. Synthetic fixtures were rolled back. The migration roundtrip probe initially compared the Rails column default to the string `"0"`; correcting that test assertion to accommodate the numeric default produced the passing result below.

**Integration now has two migrations**, including the existing OTP-attempt migration. Apply the new migration on the integration test database before running the full suite.

## Runtime verification

Every Rails command ran from this worktree with:

```sh
DISABLE_SPRING=1 RAILS_ENV=test TEST_ENV_NUMBER=96 DEVISE_SECRET_KEY=synthetic-import-verification-key
```

No `DATABASE_URL`, shared default test database, live extraction, real provider documents, or live Anthropic request was used. VCR cases explicitly use `record: :none`; new extraction cases use synthetic payloads. The migration probe asserts the database name before doing any work.

Database setup:

```sh
DISABLE_SPRING=1 RAILS_ENV=test TEST_ENV_NUMBER=96 DEVISE_SECRET_KEY=synthetic-import-verification-key bundle exec rails db:create db:schema:load
DISABLE_SPRING=1 RAILS_ENV=test TEST_ENV_NUMBER=96 DEVISE_SECRET_KEY=synthetic-import-verification-key bundle exec rails db:migrate
```

Results: database `currencytransfer_test96` created; migration succeeded. Migration log: `/tmp/compass-imports-migration.log`.

Baseline command:

```sh
DISABLE_SPRING=1 RAILS_ENV=test TEST_ENV_NUMBER=96 DEVISE_SECRET_KEY=synthetic-import-verification-key bundle exec rspec spec/services/compass/apply_upload_spec.rb spec/services/compass/parse_upload_spec.rb spec/services/compass/extract_from_document_spec.rb spec/workers/compass/process_document_upload_worker_spec.rb spec/models/compass/upload_spec.rb spec/controllers/admins/compass/uploads_controller_spec.rb spec/requests/admins/compass/uploads_spec.rb spec/system/admins/compass/upload_preview_spec.rb --format progress
```

Result: **262 examples, 0 failures**, seed **44157**, 29 seconds. Log: `/tmp/compass-imports-baseline.log`.

The following focused red commands used the same environment prefix:

```sh
bundle exec rspec spec/requests/admins/compass/uploads_spec.rb spec/services/compass/apply_upload_spec.rb spec/services/compass/extract_from_document_spec.rb --example 'empty replace-all' --example 'stale preview' --example 'duplicate keys' --example 'every selected row' --example 'transient HTTP' --example 'malformed JSON values' --format progress
bundle exec rspec spec/workers/compass/process_document_upload_worker_spec.rb --example 'manual retry' --example 'earlier extraction attempt' --format progress
bundle exec rspec spec/requests/admins/compass/uploads_spec.rb --example 'job cannot be queued' --example 'invalidate its extraction version' --format progress
bundle exec rspec spec/requests/admins/compass/uploads_spec.rb --example 'later model validation' --format progress
bundle exec rspec spec/services/compass/parse_upload_spec.rb --example 'allowed sell currencies contain' --format progress
```

Results before their fixes: **14/14 failed** (seed 17572), **2/2 failed** (4488), **5/5 failed** (39460), **2/2 failed** (63901), and **1/1 failed** (26649). The lifecycle example names later changed from extraction version to upload state when the timestamp approach was replaced by the counter.

Red logs:

- `/tmp/compass-imports-new-regressions-red.log`
- `/tmp/compass-imports-retry-red.log`
- `/tmp/compass-imports-lifecycle-red.log`
- `/tmp/compass-imports-record-invalid-red.log`
- `/tmp/compass-imports-csv-validation-red.log`

Final combined command, expanded to include recorded extraction integration cases:

```sh
DISABLE_SPRING=1 RAILS_ENV=test TEST_ENV_NUMBER=96 DEVISE_SECRET_KEY=synthetic-import-verification-key bundle exec rspec spec/services/compass/apply_upload_spec.rb spec/services/compass/parse_upload_spec.rb spec/services/compass/extract_from_document_spec.rb spec/services/compass/extract_from_document_vcr_spec.rb spec/workers/compass/process_document_upload_worker_spec.rb spec/models/compass/upload_spec.rb spec/controllers/admins/compass/uploads_controller_spec.rb spec/requests/admins/compass/uploads_spec.rb spec/system/admins/compass/upload_preview_spec.rb --format progress
```

Result: **327 examples, 0 failures**, seed **17370**, **27.43 seconds** plus 7.77 seconds loading. This comprises the initial 262, 36 existing recorded integration cases, and **29 new examples**. Log: `/tmp/compass-imports-verified.log`. Intermediate runs also passed 278/0 and 325/0; one intermediate test failed because its expected timestamp snapshot was stale after attachment touches, which informed the dedicated-counter fix.

Migration verification:

```sh
DISABLE_SPRING=1 RAILS_ENV=test TEST_ENV_NUMBER=96 DEVISE_SECRET_KEY=synthetic-import-verification-key bundle exec rails runner /tmp/compass-imports-migration-roundtrip.rb
```

Result: `PASS: down/up roundtrip, populated row preserved, default 0, NOT NULL, increment persisted`. Log: `/tmp/compass-imports-migration-roundtrip.log`.

Static checks:

```sh
bundle exec rubocop app/controllers/admins/compass/uploads_controller.rb app/models/compass/upload.rb app/services/compass/apply_upload.rb app/services/compass/extract_from_document.rb app/services/compass/parse_upload.rb app/services/compass/parse_upload/row_validation.rb app/workers/compass/process_document_upload_worker.rb spec/requests/admins/compass/uploads_spec.rb spec/services/compass/apply_upload_spec.rb spec/services/compass/parse_upload_spec.rb spec/services/compass/extract_from_document_spec.rb spec/workers/compass/process_document_upload_worker_spec.rb spec/system/admins/compass/upload_preview_spec.rb db/migrate/20260906090000_add_extraction_attempt_to_compass_uploads.rb --only Lint --format progress
git diff --check
git status --short
```

Results: **14 files, no Lint offenses**; diff check clean; final worktree clean. Lint log: `/tmp/compass-imports-lint-verified.log`.

Full RuboCop, with the identical path list and `--format json` instead of `--only Lint --format progress`, remains nonzero for style/complexity rules: **103 offenses**, versus **100** on the 13 original files at the starting revision. This is not a claim that the full style suite passes. JSON reports: `/tmp/compass-imports-rubocop-final.json` and `/tmp/compass-imports-rubocop-baseline.json`.

## Changed files

- `app/controllers/admins/compass/uploads_controller.rb`
- `app/models/compass/upload.rb`
- `app/services/compass/apply_upload.rb`
- `app/services/compass/extract_from_document.rb`
- `app/services/compass/parse_upload.rb`
- `app/services/compass/parse_upload/row_validation.rb`
- `app/views/admins/compass/uploads/_data_cells.html.haml`
- `app/views/admins/compass/uploads/processing.html.haml`
- `app/workers/compass/process_document_upload_worker.rb`
- `db/migrate/20260906090000_add_extraction_attempt_to_compass_uploads.rb`
- `db/schema.rb`
- `spec/requests/admins/compass/uploads_spec.rb`
- `spec/services/compass/apply_upload_spec.rb`
- `spec/services/compass/extract_from_document_spec.rb`
- `spec/services/compass/parse_upload_spec.rb`
- `spec/system/admins/compass/upload_preview_spec.rb`
- `spec/workers/compass/process_document_upload_worker_spec.rb`

Commit size: **17 files, +431/−76**. Nothing was pushed. Parent owns full-suite integration and review-report publication.
