11 KiB
Guided Local Imports Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add an Imports page that scans the ignored local folder, maps files to accounts, and automatically commits only clean new transactions.
Architecture: A new ingestion-facing service discovers safe direct files and derives partial-card date context without persisting source contents. A second orchestration service creates the existing preview, applies only deterministic CREATE and exact-source SKIP decisions, and commits each ready file independently. Server-rendered views hold signed scan/preflight state and display safe summaries.
Tech Stack: Python, Django, PostgreSQL, Django signed values, existing ingestion services, pytest, Docker Compose.
Spec: docs/superpowers/specs/2026-09-09-guided-local-import-design.md
Global Constraints
- Scan direct files only under
IMPORT_ROOT; reject symlinks, traversal, unsupported inputs, and files over 10 MiB. - Never display or commit source descriptions, amounts, source rows, raw paths, parser exceptions, or database errors in UI messages, logs, or tests.
- Support only
BANK_CSV_V1,OUROCARD_FULL_V1, andOUROCARD_PARTIAL_V1; OFX and PDF remain unsupported. - Use active BRL accounts for every submitted source section; never infer mapping from owner names or filenames.
- Preserve parsing, normalization, validation, duplicate detection, preview, decision, and persistence boundaries.
- Clean rows use
CREATE; exact existing source rows useSKIP/ALREADY_IMPORTED; all other issues prevent only that file from committing. - Partial-card context comes only from a validated creation date at text line 21; dates later than that month use the preceding year.
- Use only synthetic test data and never commit local financial documents, review files, exports, backups, or credentials.
Task 1: Discover safe files and derive partial-card context
Files:
- Create:
ingestion/guided_imports.py - Modify:
ingestion/parsers.py - Create:
tests/test_guided_import_discovery.py - Modify:
tests/test_import_parsers.py
Interfaces:
-
Produces
DiscoveredFile(name: str, document_hash: str, profile: str | None, sections: tuple[str, ...], code: str | None). -
Produces
discover_imports() -> tuple[DiscoveredFile, ...]andpartial_context_from_creation_line(data: bytes) -> dict[str, str]. -
Step 1: Write failing pure discovery tests.
Use
override_settings(IMPORT_ROOT=tmp_path)and synthetic bytes. Assert direct supported files receive a profile and section list; nested files, symlinks, oversize files, PDFs, OFX, and invalid layouts return only safe code/status. Assert no result has a source-text field.rows = discover_imports() assert [(row.name, row.profile, row.sections, row.code) for row in rows] == [ ("bank.csv", "BANK_CSV_V1", ("1",), None), ("unknown.ofx", None, (), "UNSUPPORTED_FILE"), ]Add parser tests for valid synthetic line 21 creation date, absent/malformed line 21, and a January creation date that resolves
31/12to the previous year. -
Step 2: Verify RED.
Run:
docker compose run --rm test pytest -q tests/test_guided_import_discovery.py tests/test_import_parsers.pyExpected: FAIL because the discovery module and context extractor do not exist.
-
Step 3: Implement constrained discovery and context extraction.
discover_importsresolvessettings.IMPORT_ROOT, iterates onlyroot.iterdir(), ignores non-files and symlinks, reads no more thanMAX_BYTES + 1, hashes data with SHA-256, and determines a profile by attempting the supported pure grammars without persisting parse output. It returns sorted filename-only results.@dataclass(frozen=True) class DiscoveredFile: name: str document_hash: str profile: str | None sections: tuple[str, ...] code: str | NoneAdd
partial_context_from_creation_linebeside the pure parser helpers. It decodes through the existing encoding rules, reads exactly line 21, validates the approved creation-date layout, and returns an ISOstart_date/end_dateinterval no longer than 366 days. Keep the existing explicitparse_document(..., context=...)contract unchanged. -
Step 4: Verify GREEN and commit.
Run:
docker compose run --rm test pytest -q tests/test_guided_import_discovery.py tests/test_import_parsers.py docker compose run --rm web python manage.py checkInspect staged content for real data, then commit:
git -c user.name=Codex -c user.email=codex@localhost commit -m "feat: discover local import files"
Task 2: Preflight and commit deterministic imports
Files:
- Modify:
ingestion/guided_imports.py - Create:
tests/test_guided_import_workflow.py
Interfaces:
-
Consumes
DiscoveredFile,Actor, anddict[str, str]section mappings. -
Produces
PreparedImport(name, document_hash, profile, sections, state, batch_id, revision, counts, code)andprepare_import(...)/commit_prepared_import(...). -
Step 1: Write failing orchestration tests.
Create synthetic active BRL accounts and source bytes in a temporary import root. Test that a clean file preflights to
READY, an exact second import preflights toALREADY_IMPORTED, an ambiguous duplicate isNEEDS_ATTENTION, and one exceptional file does not prevent another ready file from committing.prepared = prepare_import(discovered=bank, section_mapping={"1": str(account.id)}, actor=ACTOR) assert prepared.state == "READY" committed = commit_prepared_import(prepared=prepared, actor=ACTOR) assert committed.counts["created"] == 2 -
Step 2: Verify RED.
Run:
docker compose run --rm test pytest -q tests/test_guided_import_workflow.pyExpected: FAIL because preparation and automatic decisions do not exist.
-
Step 3: Implement one service that composes existing lifecycle calls.
Re-read the file and require its SHA-256 to equal
discovered.document_hashbefore acting. Validate section keys exactly match and map to active BRLAccountrecords. For partial files obtain validated context from Task 1. Callcreate_preview, inspect its items, and build complete decisions:if item.issues == ["ALREADY_IMPORTED"]: decision = {"locator": item.locator, "action": "SKIP", "reason": "ALREADY_IMPORTED"} elif not item.issues and not item.duplicates["existing"] and not item.duplicates["within"]: decision = {"locator": item.locator, "action": "CREATE"} else: cancel_preview(batch_id=batch.pk, revision=batch.revision, actor=actor) return PreparedImport(..., state="NEEDS_ATTENTION", code="REVIEW_REQUIRED")Apply the complete decision list only for ready/already-imported batches.
commit_prepared_importrequires the stored batch id/revision and delegates tocommit_preview; it never writes transactions directly. Return counts and safe codes only. File changes, stale batches, domain failures, and bad mappings yield a safe exception result and cancel any unfinished preview. -
Step 4: Verify GREEN and commit.
Run:
docker compose run --rm test pytest -q tests/test_guided_import_workflow.py tests/test_import_workflow.py tests/test_import_concurrency.py docker compose run --rm web python manage.py checkInspect staged content for sensitive data, then commit:
git -c user.name=Codex -c user.email=codex@localhost commit -m "feat: prepare guided imports"
Task 3: Add the Imports page and owner workflow coverage
Files:
- Modify:
config/urls.py - Modify:
core/context.py - Modify:
core/forms.py - Modify:
core/views.py - Create:
core/templates/core/imports/index.html - Create:
tests/test_guided_import_views.py - Modify:
tests/test_mvp_acceptance.py - Modify:
README.md - Modify:
docs/local-workflows.md
Interfaces:
-
Route:
imports-indexat/imports/. -
View actions:
scan,prepare, andcommit. -
Signed payload contains only filename, hash, profile, section identifiers, prepared batch id, and revision.
-
Step 1: Write failing web tests.
Use a temporary synthetic
IMPORT_ROOT. Cover navigation, CSRF, scan results with safe filename/profile/status only, active-BRL account selectors, mapping submission, ready-file commit, exact reimport summary, one exception plus one successful file, and altered/stale signed state rejection.response = client.post(reverse("imports-index"), {"action": "scan"}) assert "bank.csv" in response.content.decode() assert "synthetic description" not in response.content.decode() -
Step 2: Verify RED.
Run:
docker compose run --rm test pytest -q tests/test_guided_import_views.pyExpected: FAIL because the page, routes, forms, and signed workflow do not exist.
-
Step 3: Implement server-rendered scan, mapping, and commit actions.
Add Import navigation and
imports-index.scancallsdiscover_importsand signs safe discovery metadata.preparerevalidates it, receives one typed active-account field per discovered section, and callsprepare_importindependently for every mapped file.commitaccepts only signed ready batch id/revision values and callscommit_prepared_importper file.token = signing.dumps(payload, salt="guided-imports") payload = signing.loads(request.POST["scan_token"], salt="guided-imports", max_age=900)Render only filename, profile, section label, mapping selection, state, safe code, and counts. Do not display candidate descriptions, amounts, source paths, raw exceptions, or batch audit details. Use
Actor("HUMAN", "local-owner")and redirect after each successful POST. -
Step 4: Document and verify the owner path.
Replace the primary manual-import instructions in
README.mdanddocs/local-workflows.mdwith: copy files toimports/, open Imports, scan, map accounts, import ready files, and resolve listed exceptions. Retain the management command as the advanced manual workflow.Add one synthetic MVP acceptance flow through scan, mapping, automatic import, and exact reimport skip. Run:
docker compose run --rm test pytest -q tests/test_guided_import_views.py tests/test_mvp_acceptance.py docker compose build docker compose run --rm test pytest -q docker compose run --rm web python manage.py migrate --check docker compose run --rm web python manage.py check docker compose run --rm permissions docker compose up -d web Invoke-RestMethod http://127.0.0.1:8000/health/ | ConvertTo-Json -Compress git diff --check -
Step 5: Commit the UI, documentation, and acceptance slice.
Inspect staged paths and content for financial data and credentials, then commit:
git -c user.name=Codex -c user.email=codex@localhost commit -m "feat: add guided import page"