diff --git a/.agents/logs/2026-06-18.jsonl b/.agents/logs/2026-06-18.jsonl deleted file mode 100644 index 9263ce6a..00000000 --- a/.agents/logs/2026-06-18.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent":"build","session_id":"prei-trivy-perl-fix-2026-06-18","timestamp":"2026-06-18T00:00:00Z","skills_loaded":["build/governance-enforcement"],"findings":[{"id":"trivy-137","severity":"critical","component":"perl-archive-tar","description":"Path traversal via crafted symlinks allows arbitrary file access","actionable":true,"manual_review_needed":false,"resolution":"Removed Perl packages from Docker image (not used by project) and added apt-get upgrade for all OS packages"},{"id":"trivy-136","severity":"critical","component":"perl","description":"Heap buffer overflow in Perl through 5.43.10","actionable":true,"manual_review_needed":false,"resolution":"Removed Perl packages from Docker image (not used by project) and added apt-get upgrade for all OS packages"}],"decision":"fix","changes":["Dockerfile: Added apt-get update/upgrade and perl removal in base stage"],"blockers":[]} -{"agent":"build","session_id":"prei-vrm-properties-2026-06-18","timestamp":"2026-06-18T21:00:00Z","skills_loaded":["build/code-generation","build/refactoring","build/gitops-overlay-generation"],"findings":[{"id":"vrm-ui-tests-fix","severity":"high","component":"tests/test_vrm_api.py","description":"Django 4.2 + Python 3.14 template context copy bug causes UI filter tests to fail when using test Client","actionable":true,"manual_review_needed":false,"resolution":"Replaced Client-based tests with RequestFactory-based tests to avoid template rendering"},{"id":"vrm-scrape-notnull","severity":"high","component":"core/api_views.py","description":"VRM scraper returns listings without scraped_at/last_seen_at, violating NOT NULL constraint","actionable":true,"manual_review_needed":false,"resolution":"Added timezone.now() timestamps to scrape endpoint defaults"},{"id":"vrm-scraper-limitation","severity":"medium","component":"core/integrations/sources/vrm_scraper.py","description":"VRM scraper only extracts property IDs and URLs, not detailed fields (address, city, state, zip, price)","actionable":true,"manual_review_needed":true,"resolution":"Known limitation — scraper needs enhancement to fetch individual property pages for full details"}],"decision":"complete","changes":["core/api_views.py: Added scraped_at/last_seen_at timestamps to scrape endpoint","core/api_views.py: Added VrmPropertyScrapeAPI with state validation","core/api_views.py: Added VrmPropertyListAPI with state/zip filtering","core/api_urls.py: Added /v1/vrm-properties/ and /v1/vrm-properties/scrape/ routes","core/serializers.py: Added VrmPropertySerializer","core/views.py: Added vrm_properties_list view with state/zip filtering","core/urls.py: Added /vrm-properties/ route","templates/vrm_properties/list.html: Created VRM properties UI with state dropdown, zip input, scrape button","templates/base.html: Added Bootstrap JS bundle, VRM Properties navbar link","static/js/theme.js: Created dark mode toggle with localStorage persistence","static/css/theme.css: Created dark mode styles and transitions","tests/test_vrm_api.py: Created 18 tests for serializer, API, and UI"],"blockers":[]} diff --git a/.agents/logs/2026-06-21.jsonl b/.agents/logs/2026-06-21.jsonl deleted file mode 100644 index da76daaa..00000000 --- a/.agents/logs/2026-06-21.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"agent":"build","session_id":"ci-fix-staticfiles-343-1782035890","skills_loaded":["build","build/code-generation","build/governance-enforcement"],"findings":[{"finding":"staticfiles/ removed from git tracking but CI lacks collectstatic step","actionable":true,"manual_review_needed":false},{"finding":"43 tests fail with ValueError: Missing staticfiles manifest entry for css/tokens.css","actionable":true,"manual_review_needed":false},{"finding":"BDD test test_dashboard_bdd.py had pre-existing setup errors masked by staticfiles failure","actionable":true,"manual_review_needed":false}],"decision":"Added collectstatic --noinput to test.yml and ci-quality.yml workflows; fixed BDD test assertions and missing UserInvestmentTargets setup","blockers":null,"timestamp":"2026-06-21T09:58:10Z"} diff --git a/.agents/logs/2026-07-03.jsonl b/.agents/logs/2026-07-03.jsonl deleted file mode 100644 index 099bd784..00000000 --- a/.agents/logs/2026-07-03.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent": "review", "session_id": "review-phase-b-2026-07-03", "triggered_by": "feature-flow", "started_at": "2026-07-03T00:00:00Z", "duration_ms": 180000, "skills_loaded": ["review", "code-quality", "spec-compliance", "design-compliance", "acceptance-criteria", "security-rbac", "secret-governance", "policy-validation"], "findings": [{"category": "architecture", "finding": "All adapters follow stateless function pattern; command orchestrates; view only reads models. Service-layer boundaries respected.", "actionable": false, "manual_review_needed": false, "severity": "note"}, {"category": "tests", "finding": "29 new tests covering B1-B4 all pass. Full suite: 86/87 pass (1 pre-existing failure unrelated to Phase B).", "actionable": false, "manual_review_needed": false, "severity": "note"}, {"category": "security", "finding": "No hardcoded secrets. API keys passed as params, read from env only at command entry. .env.example contains only placeholders.", "actionable": false, "manual_review_needed": false, "severity": "note"}, {"category": "maintainability", "finding": "MAJOR_CITIES list hardcoded (73 cities). Adding cities requires code change. --file option provides workaround.", "actionable": true, "manual_review_needed": true, "severity": "note"}, {"category": "maintainability", "finding": "composite_score weights hardcoded in model property. Not configurable without code change.", "actionable": true, "manual_review_needed": true, "severity": "note"}, {"category": "code-quality", "finding": "BLS fetch_employment_growth imports datetime inside function; makes testing with fixed years harder.", "actionable": true, "manual_review_needed": false, "severity": "note"}, {"category": "pre-existing", "finding": "market_data.py line 179: comps fallback calls get_comps_for_listing inside except handler without its own try/except. Unrelated to Phase B.", "actionable": true, "manual_review_needed": true, "severity": "defect"}], "decision": "APPROVE", "blockers": []} -{"agent": "repair-flow", "session_id": "repair-flow-pr196-2026-07-03", "originating_session_id": "review-phase-b-2026-07-03", "triggered_by": "feature-flow", "started_at": "2026-07-03T20:10:00Z", "duration_ms": 420000, "root_cause_category": "Code", "skills_loaded": ["repair-flow", "lang-python", "code-quality"], "findings": [{"category": "test-failure", "description": "test_refresh_market_snapshot_comps_fails_logged failed: get_comps_for_listing inside except handler without its own try/except at market_data.py:179", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"category": "lint-failure", "description": "ruff format --check failed on 3 files (core/tests/test_finance_utils.py, core/tests/test_neighborhood_insights.py, tests/test_container_startup.py)", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"category": "security", "description": "GitGuardian/CodeQL flagged .agents/logs/ file — likely false positive; dismiss in GitHub UI", "actionable": false, "manual_review_needed": true, "severity": "note"}], "decision": "FIXED — wrapped comps fallback in try/except; ran ruff format on 3 files", "blockers": []} diff --git a/.agents/logs/2026-07-04.jsonl b/.agents/logs/2026-07-04.jsonl deleted file mode 100644 index e1cd2b74..00000000 --- a/.agents/logs/2026-07-04.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"agent": "repair-flow", "session_id": "repair-flow-devcontainer-2026-07-04", "originating_session_id": "repair-flow-pr196-2026-07-03", "triggered_by": "user-direct", "started_at": "2026-07-04T10:00:00Z", "duration_ms": 1800000, "root_cause_category": "Infrastructure", "skills_loaded": ["repair-flow", "lang-python", "dev-container-setup", "local-environment-simulation"], "findings": [{"category": "config-mismatch", "description": "Devcontainer configuration misaligned with trunk (was Django 4.2 + Postgres; trunk uses Django 5.2 + SQLite default)", "actionable": true, "manual_review_needed": false, "severity": "blocker"}, {"category": "host-mismatch", "description": "DisallowedHost 500 error on local Docker run because ALLOWED_HOSTS lacked 0.0.0.0 Host header binding", "actionable": true, "manual_review_needed": false, "severity": "blocker"}, {"category": "dns-failure", "description": "Container OperationalError translating host name 'db' because web loaded Postgres URL from local .env but Postgres service was commented out", "actionable": true, "manual_review_needed": false, "severity": "blocker"}], "decision": "FIXED — aligned devcontainer.json with Django 5.2 + SQLite + Codespace env; added 0.0.0.0 to .env.example ALLOWED_HOSTS; explicitly overrode DATABASE_URL to SQLite in docker-compose.yml", "blockers": []} -{"agent": "repair-flow", "session_id": "repair-flow-devcontainer-sqlite-perm-2026-07-04", "originating_session_id": "repair-flow-devcontainer-2026-07-04", "triggered_by": "repair-flow-direct", "started_at": "2026-07-04T11:18:00Z", "timestamp": "2026-07-04T11:20:49Z", "duration_ms": 169000, "root_cause_category": "Infrastructure", "skills_loaded": ["lang-python"], "findings": [{"category": "permission-error", "description": "Devcontainer bind-mounts repo to /workspaces/prei (root-owned); relative DATABASE_URL=sqlite:///db.sqlite3 resolves to /workspaces/prei/db.sqlite3 which app user cannot write; Dockerfile chown only covers /app", "actionable": true, "manual_review_needed": false, "severity": "blocker"}, {"category": "network-misconfig", "description": "User typed port 800 instead of 8000 for localhost access — no code change needed", "actionable": false, "severity": "note"}, {"category": "browser-dns", "description": "ERR_TIMED_OUT on localhost:8000 is a macOS browser/DNS issue; Django serves fine (gunicorn control socket active, /favicon.ico 404 from Django)", "actionable": false, "severity": "note"}], "decision": "FIXED — changed devcontainer.json DATABASE_URL from relative sqlite:///db.sqlite3 to absolute sqlite:////app/db.sqlite3 so database always written to writable /app directory regardless of bind-mount workspace", "blockers": []} -{"agent": "repair-flow", "session_id": "repair-flow-docker-compose-sqlite-perm-2026-07-04", "originating_session_id": "repair-flow-devcontainer-sqlite-perm-2026-07-04", "triggered_by": "user-direct", "started_at": "2026-07-04T12:00:00Z", "timestamp": "2026-07-04T13:10:00Z", "duration_ms": 4200000, "root_cause_category": "Infrastructure", "skills_loaded": ["lang-python"], "findings": [{"category": "permission-error", "description": "Root docker-compose.yml also used relative sqlite:///db.sqlite3 path. While it resolves to /app/db.sqlite3 (writable), the devcontainer.json uses absolute path; inconsistency between the two causes confusion when switching between deployment modes", "actionable": true, "manual_review_needed": false, "severity": "defect"}], "decision": "FIXED — changed root docker-compose.yml DATABASE_URL from relative sqlite:///db.sqlite3 to absolute sqlite:////app/db.sqlite3 for consistency with devcontainer.json; verified container starts, migrations pass, health check returns {\"status\":\"ok\"}", "blockers": []} -{"agent": "repair-flow", "session_id": "repair-flow-2026-07-04-001", "triggered_by": "repair-flow-direct", "started_at": "2026-07-04T21:00:00Z", "duration_ms": 900000, "root_cause_category": "Infrastructure", "findings": [{"severity": "blocker", "message": "sqlite3.OperationalError: unable to open database file at /app/db.sqlite3 in Codespaces container", "location": "docker-compose.yml:16"}, {"severity": "note", "message": "401 from Codespaces port URL is GitHub OAuth gate on private port forwarding, not Django ALLOWED_HOSTS", "location": "N/A"}], "decision": "Moved DATABASE_URL to /app/.runtime/db.sqlite3 (writable by app user per Dockerfile chown); removed railway.toml and fly.toml configs for unused providers", "blockers": []} diff --git a/.agents/logs/2026-07-05.jsonl b/.agents/logs/2026-07-05.jsonl deleted file mode 100644 index b45d53fa..00000000 --- a/.agents/logs/2026-07-05.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent": "repair-flow", "session_id": "repair-flow-devcontainer-vscode-perm-2026-07-05", "originating_session_id": "repair-flow-devcontainer-sqlite-perm-2026-07-04", "triggered_by": "user-direct", "started_at": "2026-07-05T20:31:00Z", "timestamp": "2026-07-05T20:34:11Z", "duration_ms": 191000, "root_cause_category": "Infrastructure", "skills_loaded": ["repair-flow"], "findings": [{"category": "permission-error", "description": "Devcontainer devcontainer.json containerEnv sets DATABASE_URL=sqlite:////app/db.sqlite3 but .devcontainer/Dockerfile is based on mcr.microsoft.com/devcontainers/python (not root Dockerfile) and does not create /app/ or app user; vscode user cannot write to /app/", "actionable": true, "manual_review_needed": false, "severity": "blocker"}], "decision": "FIXED — changed devcontainer.json DATABASE_URL from sqlite:////app/db.sqlite3 to sqlite:////home/vscode/db.sqlite3 (vscode home directory, always writable); updated devcontainer.md docs and README.md table to reflect the corrected path", "blockers": []} -{"agent": "feature-flow", "session_id": "feature-flow-nav-links-2026-07-05", "triggered_by": "manual", "started_at": "2026-07-05T20:42:00Z", "timestamp": "2026-07-05T20:57:16Z", "duration_ms": 916000, "phase_reached": "delivery", "decision": "PASS", "blockers": [], "subagents_invoked": [{"agent": "self", "session_id": "feature-flow-nav-links-2026-07-05"}]} diff --git a/.agents/logs/2026-07-06.jsonl b/.agents/logs/2026-07-06.jsonl deleted file mode 100644 index b40c9f23..00000000 --- a/.agents/logs/2026-07-06.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent": "feature-flow", "session_id": "feature-flow-tests-t3-t4-2026-07-06", "triggered_by": "manual", "started_at": "2026-07-06T00:30:00Z", "timestamp": "2026-07-06T01:45:00Z", "duration_ms": 4500000, "phase_reached": "delivery", "decision": "PASS", "blockers": [], "subagents_invoked": [{"agent": "self", "session_id": "feature-flow-tests-t3-t4-2026-07-06"}]} -{"agent": "build", "session_id": "build-20260706-001", "triggered_by": "manual", "started_at": "2026-07-06T20:00:00Z", "timestamp": "2026-07-06T18:10:13Z", "duration_ms": 3600000, "skills_loaded": ["code-generation", "refactoring"], "findings": [{"id": "FIND-001", "description": "BLS API free-tier rate limit (25 req/day) exhausted during bulk population \u2014 16 states failed with 'no series' due to rate limiting, not invalid series IDs", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-002", "description": "NOT NULL constraint on employment_growth_rate caused 26 cities to fail creation when BLS data unavailable \u2014 fixed by making field nullable (migration 0021)", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-003", "description": "Incorrect Census place codes: Nashville (52000\u219252006), Indianapolis (36000\u219236003) \u2014 both returned HTTP 204/503, fixed to correct 'balance' codes", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-004", "description": "Duplicate Seattle entry in MAJOR_CITIES list (lines 60 and 69) \u2014 removed duplicate", "actionable": true, "manual_review_needed": false, "severity": "note"}, {"id": "FIND-005", "description": "ATTOM API key (541e2820...4e1e) returns 401 on all endpoints \u2014 requires new key from user", "actionable": true, "manual_review_needed": true, "severity": "note"}], "decision": "proceed", "blockers": ["ATTOM API key expired \u2014 GA-5/GA-6 require new key", "BLS API rate limit requires caching (fixed) but key may still need upgrade for bulk runs"]} diff --git a/.agents/logs/2026-07-07.jsonl b/.agents/logs/2026-07-07.jsonl deleted file mode 100644 index 17a9cd7e..00000000 --- a/.agents/logs/2026-07-07.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"agent": "repair-flow", "session_id": "repair-flow-20260707-001", "triggered_by": "repair-flow-direct", "started_at": "2026-07-07T23:30:00Z", "timestamp": "2026-07-07T23:58:00Z", "duration_ms": 1680000, "root_cause_category": "Code", "findings": [{"id": "FIND-001", "description": "Census ACS vintage cache (_acs_vintage_cache) persisted across tests causing side_effect mismatch in mock. Fixed by clearing cache in setUp().", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-002", "description": "ATTOM adapter raised ATTOMAPIError for 400 'SuccessWithoutResult' and 404 'No rule matched' responses, which are valid empty-result responses, not errors. Fixed in _execute_get_request.", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-003", "description": "ATTOM integration tests used outdated parameter names (address instead of address1) and passed combined addresses as single strings. Fixed test calls.", "actionable": true, "manual_review_needed": false, "severity": "defect"}], "decision": "PASS", "blockers": [], "originating_session_id": "feature-flow-tests-t3-t4-2026-07-06"} diff --git a/.agents/logs/2026-07-08.jsonl b/.agents/logs/2026-07-08.jsonl deleted file mode 100644 index c8f49dff..00000000 --- a/.agents/logs/2026-07-08.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent": "feature-flow", "session_id": "feature-flow-models-fix-2026-07-08T10-00Z", "triggered_by": "manual", "started_at": "2026-07-08T07:50:00Z", "timestamp": "2026-07-08T08:20:06Z", "duration_ms": 1806000, "phase_reached": 5, "decision": "PASS", "blockers": [], "subagents_invoked": [], "notes": "Restored ForeclosureProperty, added PipelineProperty + ScreeningCriteria, generated migration 0027, committed, pushed, opened PR #216"} -{"agent": "repair-flow", "session_id": "repair-230-fix", "triggered_by": "repair-flow-direct", "started_at": "2026-07-08T11:10:00Z", "duration_ms": 420000, "root_cause_category": "Pipeline", "findings": [{"severity": "blocker", "description": "test_bdd_leasing_detail teardown IntegrityError due to stale PipelineCtx state across scenarios"}], "decision": "Moved _reset_ctx from pipeline_acceptance_steps.py to conftest.py with db dependency", "blockers": []} diff --git a/.agents/logs/2026-07-10.jsonl b/.agents/logs/2026-07-10.jsonl deleted file mode 100644 index 853de1db..00000000 --- a/.agents/logs/2026-07-10.jsonl +++ /dev/null @@ -1,4 +0,0 @@ -{"agent":"repair-flow","session_id":"repair-276-20260710","triggered_by":"repair-flow-direct","started_at":"2026-07-10T09:30:00Z","duration_ms":2760000,"root_cause_category":"Pipeline","findings":[{"description":"Migration 0039_growtharea_county_fips.py conflicts with 0039_alter_growtharea_rent_growth_rate already on main","severity":"blocker"}],"decision":"Rebased onto main, deleted conflicting 0039 migration, regenerated as 0040_growtharea_county_fips.py","blockers":[],"timestamp":"2026-07-10T09:56:00Z"} -{"agent":"feature-flow","session_id":"feature-flow-20260710T140536-56608","triggered_by":"manual","started_at":"2026-07-10T14:05:36Z","duration_ms":4500000,"phase_reached":"delivery","decision":"PASS","blockers":[],"subagents":[{"name":"build","session_id":"feature-flow-20260710T140536-56608"},{"name":"test-execution","session_id":"feature-flow-20260710T140536-56608"}],"timestamp":"2026-07-10T15:20:36Z"} -{"agent":"repair-flow","session_id":"repair-278-20260710","triggered_by":"repair-flow-direct","started_at":"2026-07-10T15:10:00Z","duration_ms":3000000,"root_cause_category":"Pipeline","findings":[{"description":"Missing migration 0042 for rent_growth_rate help_text change","severity":"blocker"},{"description":"Inline import for lookup_county_fips caused F401 false positive","severity":"note"}],"decision":"Created migration 0042, switched to inline import for lookup_county_fips, fixed PR title to lowercase","blockers":[],"originating_session_id":"feature-flow-20260710T140536-56608","timestamp":"2026-07-10T15:50:00Z"} -{"agent":"repair-flow","session_id":"repair-279-20260710","triggered_by":"repair-flow-direct","started_at":"2026-07-10T15:20:00Z","duration_ms":2400000,"root_cause_category":"Code","findings":[{"description":"4 tests had expected values based on old GACS weights (3.25/3.20/2.80/3.20)","severity":"defect"},{"description":"PR title started with uppercase after colon","severity":"note"}],"decision":"Updated 4 test assertions to match new weights, fixed PR title","blockers":[],"originating_session_id":"feature-flow-20260710T141627-61928","timestamp":"2026-07-10T15:50:00Z"} diff --git a/.agents/logs/2026-07-11.jsonl b/.agents/logs/2026-07-11.jsonl deleted file mode 100644 index fc8786eb..00000000 --- a/.agents/logs/2026-07-11.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"agent":"feature-flow","session_id":"4740182F-EE56-4629-B13F-1A6FBEDDBD37","triggered_by":"manual","started_at":"2026-07-11T07:50:00Z","duration_ms":2065000,"phase_reached":"delivery","decision":"PASS","blockers":[],"subagents":[],"timestamp":"2026-07-11T08:24:25Z"} -{"agent":"feature-flow","session_id":"FC363287-9297-45E4-ACEB-071B7D2DB897","triggered_by":"manual","started_at":"2026-07-11T08:05:00Z","duration_ms":2723000,"phase_reached":"delivery","decision":"PASS","blockers":[],"subagents":[],"timestamp":"2026-07-11T08:50:23Z"} -{"agent":"repair-flow","session_id":"8B44C85B-9826-42E1-8509-4A35B4D7B377","triggered_by":"repair-flow-direct","started_at":"2026-07-11T08:48:00Z","duration_ms":180000,"root_cause_category":"Code","findings":[{"description":"ruff format --check failing on core/views/__init__.py — file had formatting inconsistencies from VIEW-SCREENER-1/VIEW-DISCOVERY-1 implementation","severity":"blocker"}],"decision":"Ran ruff format core/views/__init__.py — pure formatting fix, no logic changes. Pushed to feature/view-discovery-screener-redesign.","blockers":[],"originating_session_id":"FC363287-9297-45E4-ACEB-071B7D2DB897","timestamp":"2026-07-11T08:55:00Z"} diff --git a/.agents/logs/2026-07-18.jsonl b/.agents/logs/2026-07-18.jsonl deleted file mode 100644 index ea8960c7..00000000 --- a/.agents/logs/2026-07-18.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"agent": "repair-flow", "session_id": "repair-pr315-001", "triggered_by": "repair-flow-direct", "started_at": "2026-07-18T22:00:00Z", "timestamp": "2026-07-18T22:30:00Z", "duration_ms": 1800000, "root_cause_category": "Code", "findings": [{"severity": "defect", "description": "Missing CENSUS_API_KEY env patch on test_multiple_places_sorted_by_score caused view to return early with no GrowthArea objects"}, {"severity": "defect", "description": "ruff format not applied to test_growth_explorer_view.py after manual edit"}, {"severity": "defect", "description": "Missing {{ error }} block in growth_explorer.html template — validation errors silently ignored"}, {"severity": "note", "description": "Pipeline bridge tests referenced removed UI elements (Property Discovery)"}], "decision": "FIXED", "blockers": []} diff --git a/.agents/logs/2026-07-27.jsonl b/.agents/logs/2026-07-27.jsonl deleted file mode 100644 index 80df26a4..00000000 --- a/.agents/logs/2026-07-27.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"agent": "feature-flow", "session_id": "top01-phase-a-20260727", "triggered_by": "user-direct", "started_at": "2026-07-27T00:00:00Z", "timestamp": "2026-07-27T11:20:00Z", "duration_ms": null, "root_cause_category": "CI/Test Quality", "findings": [{"severity": "defect", "description": "pipeline_list view had no @login_required, unlike sibling leasing_list, causing a 500 (AnonymousUser filter TypeError) instead of a redirect for unauthenticated access"}, {"severity": "defect", "description": "tests/acceptance/test_leasing.py hardcoded a stale route (/leasing/list/) that had never executed under --collect-only"}, {"severity": "note", "description": "Implemented Phase A of docs/TOP_01_PLAN.md: A-2 (BDD suite over real HTTP via live_server), A-3 (acceptance suite runs pre-merge via live_server fallback), A-4 (real 10-minute build-time budget on docker-publish.yml), A-5 (schema-based response validation for all tests/acceptance/*.py files). A-1 was already done."}], "decision": "IMPLEMENTED", "blockers": []} diff --git a/.agents/logs/2026-07-28.jsonl b/.agents/logs/2026-07-28.jsonl deleted file mode 100644 index 1f8f0957..00000000 --- a/.agents/logs/2026-07-28.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"agent": "feature-flow", "session_id": "top01-phase-b-20260727", "triggered_by": "user-direct", "started_at": "2026-07-27T20:00:00Z", "timestamp": "2026-07-28T00:00:00Z", "duration_ms": null, "root_cause_category": "Financial Math Verification", "findings": [{"severity": "defect", "description": "prei/pipeline/handlers/underwriting.py was a second, fully float-based implementation of NOI/cap-rate/cash-on-cash living outside services/utils — AGENTS.md Never-Do #1 and #3 violation. Fixed by converting to Decimal and importing the canonical cap_rate/cash_on_cash from investor_app.finance.utils."}, {"severity": "defect", "description": "ref_one_percent_rule/ref_gross_rent_multiplier in tests/finance_reference.py silently returned a value on zero/negative input instead of raising ValueError like production, so those edge cases could diverge undetected between reference and production."}, {"severity": "defect", "description": "mypy's pre-commit hook (broader scope than the CI-only mypy core/ investor_app/finance/ command) flagged bare int/float literals passed to UnderwritingInput's now-Decimal fields in prei/pipeline/tests/test_underwriting.py and tests/test_underwriting_integration.py — fixed by wrapping literals in Decimal(...); pydantic coerces them fine at runtime but the pydantic mypy plugin isn't enabled in mypy.ini."}, {"severity": "note", "description": "Implemented Phase B of docs/TOP_01_PLAN.md: B-1 (IRR reference implementation, bisection-based, no numpy dependency), B-2 (expanded edge-case coverage to 50+ cases per function, 546 total), B-4 (mathematical derivation docstrings on noi/cap_rate/cash_on_cash/dscr/irr). B-3 (CI gate) was already wired. Bundled the user-approved underwriting.py float-to-Decimal fix. Recorded LIMIT-20 (bare-function vs calculate_* contract divergence, duplicate score_listing_v2) and LIMIT-21 (offer.py remains float currency) as deliberately out of scope. Archived Phase A's specification.md/design.md/tasks.json to features/top01-phase-a/ (merged but never archived) and regenerated root files for Phase B."}], "decision": "IMPLEMENTED", "blockers": [], "pr": "https://github.com/paruff/prei/pull/324"} diff --git a/.agents/logs/2026-07-31.jsonl b/.agents/logs/2026-07-31.jsonl deleted file mode 100644 index 6ce2d3f6..00000000 --- a/.agents/logs/2026-07-31.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"agent": "build", "session_id": "build-20260731-finance-split-001", "triggered_by": "feature-flow", "started_at": "2026-07-31T16:00:00Z", "timestamp": "2026-07-31T17:45:00Z", "duration_ms": 6300000, "skills_loaded": ["code-generation", "refactoring", "template-application"], "findings": [{"id": "FIND-001", "description": "Moved copy of total_return_summary dropped the purchase_price key from its return dict, breaking test_dict_keys_present — restored the key to match the original contract", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-002", "description": "after_tax_irr in taxes.py had a local import of irr from utils that was unused (function reimplements npf.irr inline) — removed per ruff F401", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-003", "description": "tests/test_underwriting_score.py still tested the deleted pure score_listing_v2 (audit finding #2); kept one_percent_rule/gross_rent_multiplier primitive tests, deleted TestScoreListingV2 since production score lives only in core/services/scoring.py (covered by core/tests/test_scoring_v2.py)", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-004", "description": "Service-layer duplicate calculate_noi in core/services/property_service.py was exported but imported by no production code — deleted function, export, and its test file", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-005", "description": "Dual-pipeline investigation: Django (PipelineAsset/PipelineProperty + core/services/pipeline.py) is the load-bearing pipeline; the pydantic prei FastAPI router, CLI, and orchestrator are not mounted in any Django URLconf/INSTALLED_APPS/docker-compose; only core/views/__init__.py couples to prei (get_state_landlord_score + lazy DiscoveryProcessor/BatchScreeningProcessor/discover_from_all for the Growth Explorer bridge). Full removal of pydantic state machine requires PM sign-off", "actionable": false, "manual_review_needed": true, "severity": "note"}, {"id": "FIND-006", "description": "Re-export backfill from utils.py was unnecessary: after updating all 17 importers, no remaining importer pulls a moved name from investor_app.finance.utils; keeping the monolith aliases would defeat the split", "actionable": false, "manual_review_needed": false, "severity": "note"}, {"id": "FIND-007", "description": "Pre-existing mypy error in tests/acceptance/conftest.py:69 (no-any-return) unrelated to this change — file unmodified", "actionable": false, "manual_review_needed": false, "severity": "note"}], "decision": "implemented", "blockers": [], "pr": null} -{"agent": "build", "session_id": "build-20260731-pydantic-django-consolidation-001", "triggered_by": "feature-flow", "started_at": "2026-07-31T09:00:00Z", "timestamp": "2026-07-31T10:04:32Z", "duration_ms": 3872000, "skills_loaded": ["code-generation", "refactoring", "template-application", "governance-enforcement"], "findings": [{"id": "FIND-001", "description": "Offer math ported to Decimal in core/services/offer.py with OfferInput.__post_init__ validation, resolving LIMIT-21; old tests using model_copy rewritten with dataclasses.replace and Decimal equality", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-002", "description": "New dataclasses are pydantic-free so coercion must be explicit: DiscoverySanitizer.transform_input now applies _coerce_float/_coerce_beds/_coerce_baths/_coerce_sqft/_coerce_year; fixed 5 test failures", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-003", "description": "prei/integrations/landlord_data.py moved to core/services/landlord_data.py; core/views/__init__.py:50 import updated", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-004", "description": "pydantic retained in requirements.txt because tests/acceptance/{schemas,test_api}.py still import it; fastapi/uvicorn/click removed with no remaining consumers", "actionable": true, "manual_review_needed": false, "severity": "note"}, {"id": "FIND-005", "description": "ghcr.io/paruff/prei docker image is the Django web image, not a separate FastAPI/CLI surface; docker-compose/ZAP refs out of scope for this plan", "actionable": false, "manual_review_needed": false, "severity": "note"}, {"id": "FIND-006", "description": "mypy has 2 pre-existing errors unrelated to this work: tests/acceptance/conftest.py:69 (no-any-return) and tests_bdd/conftest.py:31 (__init__ misc); left untouched", "actionable": false, "manual_review_needed": true, "severity": "note"}, {"id": "FIND-007", "description": "Single remaining prei.pipeline reference is a logger namespace string in core/services/pipeline.py, not an import", "actionable": false, "manual_review_needed": false, "severity": "note"}], "decision": "Approved plan executed in full: prei package deleted, services ported to core/services with Decimal offer math, both view bridges rewritten, tests migrated, deps trimmed, docs updated. Full suite green (1791 passed).", "blockers": []} diff --git a/.agents/logs/2026-08-01.jsonl b/.agents/logs/2026-08-01.jsonl deleted file mode 100644 index 3743f7d6..00000000 --- a/.agents/logs/2026-08-01.jsonl +++ /dev/null @@ -1,3 +0,0 @@ -{"agent": "build", "session_id": "build-20260801-docs-review-001", "triggered_by": "feature-flow", "started_at": "2026-08-01T13:40:00Z", "timestamp": "2026-08-01T13:51:51Z", "duration_ms": 711000, "skills_loaded": ["plan", "plan/task-decomposition", "documentation", "verification"], "findings": [{"id": "FIND-001", "description": "GACS_GUIDE.md documented stale GACS v1 (6 signals, 35/20/15/15/10/5 weights, 0-infinity scale); implementation is GACS v2 (7 signals, 30/15/15/10/15/10/5 weights, 0-100 index, QCEW county employment with FRED fallback). Rewrote weights table, score ranges, examples, confidence explanation, and technical notes to match core/models/growth.py", "actionable": true, "manual_review_needed": false, "severity": "defect"}, {"id": "FIND-002", "description": "docs/explanation/index.md has 15 pre-existing broken links to planned-but-unwritten pages (code-quality.md, roadmap.md, etc.) present at HEAD before this change; creating placeholder docs is out of scope for this docs-review feature", "actionable": false, "manual_review_needed": true, "severity": "note"}], "decision": "implemented", "blockers": [], "pr": null} -{"agent":"review","session_id":"review-20260801-gacs-docs-handoff-001","triggered_by":"manual","started_at":"2026-08-01T14:00:00Z","timestamp":"2026-08-01T14:18:00Z","duration_ms":1080000,"skills_loaded":["review","review/spec-compliance"],"findings":[{"id":"FIND-001","description":"discover-properties.md:56,60-66 claims POST redirects to screener; code renders in-page results on /discovery/ (views/__init__.py:3801-3815)","actionable":true,"manual_review_needed":false,"severity":"blocker"},{"id":"FIND-002","description":"investor-workflow.md:29,33,37 still says 6 signals and state-level employment, contradicting GACS v2 rewrite in same PR","actionable":true,"manual_review_needed":false,"severity":"blocker"},{"id":"FIND-003","description":"analyze-growth-areas.md:13 says 35% employment weight and FRED-missing=0; v2 weight is 30% and QCEW attempted first","actionable":true,"manual_review_needed":false,"severity":"blocker"},{"id":"FIND-004","description":"analyze-growth-areas.md:28-29 documents API-key config note in explorer UI that does not exist in template","actionable":true,"manual_review_needed":false,"severity":"defect"},{"id":"FIND-005","description":"school quality GreatSchools call never fires (run_school_api flag never set, views/__init__.py:1118); docs imply ~40 cities get real school data","actionable":true,"manual_review_needed":false,"severity":"defect"},{"id":"FIND-006","description":"expandable breakdown row claim non-functional: no JS toggles hidden row in growth_areas.html","actionable":true,"manual_review_needed":false,"severity":"defect"},{"id":"FIND-007","description":"discover-properties.md:52 claims HUD/USDA/VRM/ATTOM/County collection in background threads; only VRM is threaded, HUD/USDA ingest synchronously with 120s/60s timeouts vs 30s gunicorn timeout","actionable":true,"manual_review_needed":false,"severity":"defect"},{"id":"FIND-008","description":"supply constraint source claim (ACS housing-unit growth) aspirational: explorer never sets supply_constraint_index, always default 50","actionable":true,"manual_review_needed":false,"severity":"defect"},{"id":"FIND-009","description":"confidence logic doc mismatch: composite_score never returns None (0.00 instead) and county_fips stored on FRED fallback inflates confidence 17 vs 8","actionable":false,"manual_review_needed":true,"severity":"note"}],"decision":"needs_work","blockers":["FIND-001","FIND-002","FIND-003"],"pr":null} -{"timestamp":"2026-08-01T15:15:00Z","agent":"build","session_id":"build-20260801-gacs-docs-review-fix-001","triggered_by":"feature-flow","started_at":"2026-08-01T15:06:00Z","duration_ms":540000,"skills":["build/refactoring","review/design-compliance"],"findings":[{"id":"FIND-001","severity":"defect","actionable":true,"manual_review_needed":false,"title":"discovery POST renders in-page, not redirect","detail":"discover-properties.md and investor-workflow.md claimed redirect to screener; code renders results in-page with View in Screener button. Docs corrected."},{"id":"FIND-002","severity":"defect","actionable":true,"manual_review_needed":false,"title":"6 signals / state-level employment wording","detail":"investor-workflow.md still said 6 signals and state-level employment; corrected to 7 signals and county QCEW preferred."},{"id":"FIND-003","severity":"defect","actionable":true,"manual_review_needed":false,"title":"35% employment weight in analyze-growth-areas","detail":"Line said 35% of GACS and defaults to 0; actual weight is 30% with QCEW attempted first. Corrected."},{"id":"FIND-004","severity":"defect","actionable":true,"manual_review_needed":false,"title":"API-key config note not in explorer UI","detail":"Doc claimed explorer shows key-config note; template only shows Census key error on POST. Step 1 corrected."},{"id":"FIND-005","severity":"defect","actionable":true,"manual_review_needed":false,"title":"School quality signal never fires","detail":"run_school_api flag never set; GreatSchools source not wired up. Docs updated to placeholder status in both analyze-growth-areas.md and GACS_GUIDE.md."},{"id":"FIND-006","severity":"defect","actionable":true,"manual_review_needed":false,"title":"Expandable breakdown rows non-functional","detail":"Template rows have hidden attr with no JS toggle; doc's click-to-expand claim removed."},{"id":"FIND-007","severity":"defect","actionable":true,"manual_review_needed":false,"title":"Not all sources background-threaded","detail":"Only VRM scrape runs in background thread; HUD/USDA run synchronously with retry-on-lock. discover-properties.md corrected."},{"id":"FIND-008","severity":"defect","actionable":true,"manual_review_needed":false,"title":"Supply constraint source aspirational","detail":"Explorer never sets supply_constraint_index; always default 50. Doc and GACS_GUIDE corrected."},{"id":"FIND-009","severity":"note","actionable":false,"manual_review_needed":true,"title":"Confidence logic nuance","detail":"composite_score returns 0.00 not None; FRED-fallback rows store county_fips inflating confidence 17 vs 8. Not doc-blocking; flagged for code review."}],"decision":"fixed_and_pushed","blockers":[]} diff --git a/.gitignore b/.gitignore index 7c2595cf..d72e362d 100644 --- a/.gitignore +++ b/.gitignore @@ -86,3 +86,6 @@ verification-report.md .pytest-report.jsonl .zap/prei-auth-context-runtime.xml drafts/ + +# Agent logs +.agents/logs/ diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 43c994c2..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/Dockerfile b/Dockerfile index eccb0480..772ba5c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ # PYTHON_IMAGE_VERSION = full semver for the FROM tag. # PYTHON_VERSION = major.minor for the filesystem path # (Python always installs to /usr/local/lib/python3.X regardless of patch). -ARG PYTHON_IMAGE_VERSION=3.14.6 +ARG PYTHON_IMAGE_VERSION=3.14.7 ARG PYTHON_VERSION=3.14 # ── Version metadata (injected by CI, or default for local dev) ──────────── @@ -23,10 +23,10 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ PREI_VERSION=${VERSION} \ PREI_COMMIT=${COMMIT} -# Remove Perl (not needed by this Python project) to eliminate Trivy alerts: -# #137 — perl-archive-tar path traversal via symlinks (Critical) -# #136 — Perl heap buffer overflow through 5.43.10 (Critical) +# Remove Perl (not needed by this Python project) and upgrade all OS packages +# to resolve Trivy alerts for perl, openssl, sqlite, zlib, ncurses, libblkid, etc. RUN apt-get update && \ + apt-get upgrade -y && \ apt-get remove --purge -y --auto-remove perl libperl* && \ rm -rf /var/lib/apt/lists/* @@ -43,7 +43,7 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN --mount=type=cache,target=/root/.cache/pip \ - pip install --upgrade pip==26.1.2 setuptools==83.0.0 wheel==0.46.2 && \ + pip install --upgrade setuptools==83.0.0 wheel==0.46.2 && \ pip install -r requirements.txt # ── final image ────────────────────────────────────────────────────────── @@ -55,11 +55,10 @@ ARG BUILD_DATE ARG BRANCH COPY --from=deps /usr/local/lib/python${PYTHON_VERSION} /usr/local/lib/python${PYTHON_VERSION} COPY --from=deps /usr/local/bin /usr/local/bin -# Re-upgrade pip, setuptools, and wheel so the base image's stale dist-info is replaced. -# setuptools 79.x vendors jaraco.context 5.3.0 (CVE-2026-23949) and wheel 0.45.1 -# (CVE-2026-24049); upgrading brings the patched vendored versions. -# wheel 0.45.1 (standalone) also carries CVE-2026-24049; upgrade to 0.46.2+. -RUN pip install --upgrade pip==26.1.2 setuptools==83.0.0 wheel==0.46.2 +# Upgrade setuptools and wheel for CVE patches (setuptools 83.0.0 vendors +# jaraco.context 5.3.0 for CVE-2026-23949; wheel 0.46.2+ for CVE-2026-24049). +# Don't pin pip — the base image ships 26.2.1+ and downgrading breaks imports. +RUN pip install --upgrade setuptools==83.0.0 wheel==0.46.2 COPY . . # Bake version into files so the runtime can read them without a .git dir diff --git a/PIPE-10-migration.py b/PIPE-10-migration.py deleted file mode 100644 index 31c3cddd..00000000 --- a/PIPE-10-migration.py +++ /dev/null @@ -1,142 +0,0 @@ -# Generated by Django 5.2.15 on 2026-07-08 10:24 -# -# Migration C of three pipeline migrations (PIPE-10). -# Creates: LeasingPipelineProperty. -# Confirm Migration A (0027_pipeline_v2) and Migration B (0028_pipe7_migration_b) -# have been applied before running this. -# Do not apply without paruff review. -# - -import django.core.validators -import django.db.models.deletion -from decimal import Decimal -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("core", "0028_pipe7_migration_b"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name="LeasingPipelineProperty", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "stage", - models.CharField( - choices=[ - ("LISTING", "Listed / Marketing"), - ("SHOWING", "Showings Scheduled"), - ("APPLICATION", "Application Received"), - ("SCREENING", "Applicant Screening"), - ("APPROVED", "Applicant Approved"), - ("LEASE_SIGNED", "Lease Signed"), - ("MOVE_IN", "Move-In Complete"), - ("STABILIZED", "Stabilized"), - ], - default="LISTING", - max_length=20, - ), - ), - ( - "status", - models.CharField( - choices=[ - ("ACTIVE", "Active"), - ("FILLED", "Unit Filled"), - ("ON_HOLD", "On Hold"), - ], - default="ACTIVE", - max_length=20, - ), - ), - ( - "asking_rent", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=10, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ("listed_date", models.DateField(blank=True, null=True)), - ( - "listing_source", - models.CharField(blank=True, default="", max_length=128), - ), - ( - "applicant_name", - models.CharField(blank=True, default="", max_length=128), - ), - ("application_date", models.DateField(blank=True, null=True)), - ("screening_passed", models.BooleanField(null=True)), - ("screening_notes", models.TextField(blank=True, default="")), - ("lease_start_date", models.DateField(blank=True, null=True)), - ("lease_end_date", models.DateField(blank=True, null=True)), - ( - "monthly_rent", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=10, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ( - "security_deposit", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=10, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ("move_in_date", models.DateField(blank=True, null=True)), - ("stabilized_date", models.DateField(blank=True, null=True)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "property_record", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="leasing_entries", - to="core.property", - ), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="leasing_properties", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "verbose_name": "Leasing Pipeline Property", - "verbose_name_plural": "Leasing Pipeline Properties", - "ordering": ["-updated_at"], - }, - ), - ] diff --git a/PIPE-7-migration.py b/PIPE-7-migration.py deleted file mode 100644 index 93e4a090..00000000 --- a/PIPE-7-migration.py +++ /dev/null @@ -1,330 +0,0 @@ -# Generated by Django 5.2.15 on 2026-07-08 09:43 -# -# Migration B of three pipeline migrations (PIPE-7). -# Creates: OfferRecord, DueDiligenceChecklist, ClosingRecord, RenovationRecord. -# Confirm Migration A (0027_pipeline_v2) has been applied before running this. -# Do not apply without paruff review. -# - -import django.core.validators -import django.db.models.deletion -from decimal import Decimal -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("core", "0027_pipeline_v2"), - ] - - operations = [ - migrations.CreateModel( - name="ClosingRecord", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "final_purchase_price", - models.DecimalField( - decimal_places=2, - max_digits=12, - validators=[ - django.core.validators.MinValueValidator(Decimal("0.01")) - ], - ), - ), - ("closing_date", models.DateField()), - ( - "closing_costs", - models.DecimalField( - decimal_places=2, - default=Decimal("0"), - max_digits=12, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ( - "loan_amount", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=12, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ( - "down_payment", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=12, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ("lender", models.CharField(blank=True, default="", max_length=255)), - ("notes", models.TextField(blank=True, default="")), - ("created_at", models.DateTimeField(auto_now_add=True)), - ( - "pipeline_property", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="closing_record", - to="core.pipelineproperty", - ), - ), - ], - options={ - "verbose_name": "Closing Record", - "verbose_name_plural": "Closing Records", - }, - ), - migrations.CreateModel( - name="DueDiligenceChecklist", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("inspection_scheduled", models.BooleanField(default=False)), - ("inspection_completed", models.BooleanField(default=False)), - ("inspection_findings", models.TextField(blank=True, default="")), - ("title_search_ordered", models.BooleanField(default=False)), - ("title_clear", models.BooleanField(blank=True, null=True)), - ("appraisal_ordered", models.BooleanField(default=False)), - ( - "appraisal_value", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=12, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ("insurance_quoted", models.BooleanField(default=False)), - ( - "insurance_annual_cost", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=10, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ("contractor_estimate_obtained", models.BooleanField(default=False)), - ( - "contractor_estimate_amount", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=12, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ( - "go_no_go", - models.CharField( - choices=[ - ("pending", "Pending"), - ("go", "Go"), - ("no_go", "No Go"), - ], - default="pending", - max_length=10, - ), - ), - ("no_go_reason", models.TextField(blank=True, default="")), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "pipeline_property", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="due_diligence", - to="core.pipelineproperty", - ), - ), - ], - options={ - "verbose_name": "Due Diligence Checklist", - "verbose_name_plural": "Due Diligence Checklists", - }, - ), - migrations.CreateModel( - name="OfferRecord", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "offer_price", - models.DecimalField( - decimal_places=2, - max_digits=12, - validators=[ - django.core.validators.MinValueValidator(Decimal("0.01")) - ], - ), - ), - ("offer_date", models.DateField()), - ("offer_expiry", models.DateField(blank=True, null=True)), - ("contingencies", models.JSONField(blank=True, default=list)), - ( - "status", - models.CharField( - choices=[ - ("pending", "Pending"), - ("accepted", "Accepted"), - ("rejected", "Rejected"), - ("countered", "Countered"), - ("withdrawn", "Withdrawn"), - ], - default="pending", - max_length=20, - ), - ), - ( - "counter_price", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=12, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0.01")) - ], - ), - ), - ("notes", models.TextField(blank=True, default="")), - ("created_at", models.DateTimeField(auto_now_add=True)), - ( - "pipeline_property", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="offers", - to="core.pipelineproperty", - ), - ), - ], - options={ - "verbose_name": "Offer Record", - "verbose_name_plural": "Offer Records", - "ordering": ["-created_at"], - }, - ), - migrations.CreateModel( - name="RenovationRecord", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "estimated_budget", - models.DecimalField( - decimal_places=2, - default=Decimal("0"), - max_digits=12, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ( - "actual_cost", - models.DecimalField( - blank=True, - decimal_places=2, - max_digits=12, - null=True, - validators=[ - django.core.validators.MinValueValidator(Decimal("0")) - ], - ), - ), - ("start_date", models.DateField(blank=True, null=True)), - ("completion_date", models.DateField(blank=True, null=True)), - ( - "contractor", - models.CharField(blank=True, default="", max_length=255), - ), - ("scope_of_work", models.TextField(blank=True, default="")), - ( - "status", - models.CharField( - choices=[ - ("not_started", "Not Started"), - ("in_progress", "In Progress"), - ("complete", "Complete"), - ], - default="not_started", - max_length=20, - ), - ), - ("notes", models.TextField(blank=True, default="")), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ( - "pipeline_property", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="renovation_record", - to="core.pipelineproperty", - ), - ), - ( - "property_record", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="renovations", - to="core.property", - ), - ), - ], - options={ - "verbose_name": "Renovation Record", - "verbose_name_plural": "Renovation Records", - }, - ), - ] diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md new file mode 100644 index 00000000..f23a365c --- /dev/null +++ b/docs/CURRENT_STATE.md @@ -0,0 +1,125 @@ +# Current State — prei + +> **Last updated:** 2026-08-20 +> **Replaces:** All files in `docs/assessments/archive/` and `docs/planning/PRODUCT_STRATEGY.md` maturity sections. + +--- + +## Executive Summary + +prei is a **passive residential real estate investment analytics** platform for buy-and-hold investors. The core workflow spans Growth Areas → Discovery → Screening → Underwriting → Offer → Pipeline CRM → Portfolio → Leasing. + +**Test coverage:** 1,981 tests collected (1,952 passing, 29 E2E Playwright tests). +**23 Django models** across pipeline, growth, and sources. + +--- + +## Component Maturity (August 2026) + +### ✅ Pipeline CRM Kanban — BETA → GA +| Attribute | Status | +|-----------|--------| +| Model | `PipelineProperty` — 11 stages, 4 statuses, 5 source types | +| Views | List, kanban (drag-drop), detail, advance/kill/hold/reactivate endpoints | +| Templates | Kanban board with drag-and-drop, action dropdown, list view | +| Tests | Unit, integration, E2E (Playwright drag-drop) | +| **Recent work** | Added REST endpoints (advance/kill/hold/reactivate), action dropdown UI, 15 E2E tests | + +### ✅ Property Discovery — BETA +| Attribute | Status | +|-----------|--------| +| Sources | VRM, HUD REO, USDA REO, 11 TX county foreclosure scrapers, sheriff sales | +| Views | Discovery page with state selector, AJAX results | +| Tests | 264 pipeline tests + E2E workflow journey | +| **Recent work** | Data health dashboard, circuit breaker, refresh-all with polling, 4 E2E tests | + +### ✅ Screening — BETA +| Attribute | Status | +|-----------|--------| +| Logic | 9-criteria screening (4 hard kill, 5 soft) | +| Views | Screener with AJAX filter bar, criteria settings, preview impact | +| Tests | Unit, integration, E2E (filter bar, preview, version history) | +| **Recent work** | AJAX filter bar, preview impact button, auto-version on save, version history display, 3 E2E tests | + +### ✅ Underwriting — BETA +| Attribute | Status | +|-----------|--------| +| Code | 40+ KPI functions, v2 scoring, deal comparison view | +| Tests | 58 parametrized edge cases, 5 comparison tests | +| UX | Property report, side-by-side comparison (2-4 properties) | + +### ✅ Offer Management — BETA +| Attribute | Status | +|-----------|--------| +| Code | 3 offer strategies, MAO visualization | +| Tests | Offer handler tests included in pipeline suite | + +### ✅ Portfolio Tracking — BETA +| Attribute | Status | +|-----------|--------| +| Code | Property model, rental income, operating expense, investment analysis | +| Tests | Property analysis, portfolio aggregation | + +### ✅ BRRRR Analysis — BETA +| Attribute | Status | +|-----------|--------| +| Code | Server-side engine + client-side calculator | +| Tests | 25+ BRRRR-specific tests | + +### ✅ Leasing Pipeline — BETA +| Attribute | Status | +|-----------|--------| +| Model | `LeasingPipelineProperty` — 8 stages, 3 statuses | +| Views | List, kanban (drag-drop), add, detail, 6 stage-specific forms | +| Tests | Unit, integration, acceptance, BDD | +| **Recent work** | Fully implemented end-to-end (was THIN SHELL in Jul 2026) | + +### ✅ Growth Areas — BETA → GA +| Attribute | Status | +|-----------|--------| +| Code | Market scoring, growth area population, Census ACS + FRED | +| Tests | 8 test files, E2E system page | +| **Recent work** | E2E tests for system status page, data health dashboard | + +### ✅ Data Health Dashboard — NEW +| Attribute | Status | +|-----------|--------| +| Code | `DataSourceHealth` model, `DataSourceHealthMonitor` | +| Views | System status with refresh-all button, health JSON endpoint | +| Features | Circuit breaker (3-error threshold), retry decorator, live polling | +| Tests | 4 E2E tests | + +--- + +## Remaining Gaps + +| Component | Gap | Priority | +|-----------|-----|----------| +| **Growth Areas** | No seeded data on deploy (requires CLI command) | P2 | +| **Discovery** | 6/7 sources still stubs (only VRM, HUD, USDA, counties real) | P2 | +| **Offer** | No competition analysis display | P2 | +| **BRRRR** | No visual projection timeline | P3 | +| **Portfolio** | No time-series charts, no equity dashboard | P2 | + +--- + +## Tech Stack + +- **Backend:** Django 6.0, Python 3.14 +- **Database:** SQLite (dev), PostgreSQL (prod) +- **Frontend:** Vanilla JS, custom design system (CSS tokens) +- **Testing:** pytest 9.1, Playwright 1.62 (E2E), 1,981 tests +- **CI:** GitHub Actions (lint, typecheck, unit, integration, E2E, ZAP scan) +- **Deploy:** Docker (python:3.14.7-slim-bookworm), Render + +--- + +## Architecture + +``` +Growth Areas → Discovery → Screening → Underwriting → Offer → Pipeline CRM → Portfolio → Leasing + ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ + Census/FRED VRM/HUD 9-criteria 40+ KPIs 3 strategies 11 stages Aggregates 8 stages + + FRED /USDA screening + v2 score + MAO + kanban + variance + kanban + /Counties + deal compare +``` diff --git a/docs/COPILOT_COST_GUIDE.md b/docs/archive/COPILOT_COST_GUIDE.md similarity index 100% rename from docs/COPILOT_COST_GUIDE.md rename to docs/archive/COPILOT_COST_GUIDE.md diff --git a/docs/DEVEX_LOG.md b/docs/archive/DEVEX_LOG.md similarity index 100% rename from docs/DEVEX_LOG.md rename to docs/archive/DEVEX_LOG.md diff --git a/docs/DEVEX_PLAN.md b/docs/archive/DEVEX_PLAN.md similarity index 100% rename from docs/DEVEX_PLAN.md rename to docs/archive/DEVEX_PLAN.md diff --git a/docs/DOCS_AUDIT.md b/docs/archive/DOCS_AUDIT.md similarity index 100% rename from docs/DOCS_AUDIT.md rename to docs/archive/DOCS_AUDIT.md diff --git a/docs/GITOPS_COMPLIANCE_AUDIT.md b/docs/archive/GITOPS_COMPLIANCE_AUDIT.md similarity index 100% rename from docs/GITOPS_COMPLIANCE_AUDIT.md rename to docs/archive/GITOPS_COMPLIANCE_AUDIT.md diff --git a/docs/MODEL_ROUTING_GUIDE.md b/docs/archive/MODEL_ROUTING_GUIDE.md similarity index 100% rename from docs/MODEL_ROUTING_GUIDE.md rename to docs/archive/MODEL_ROUTING_GUIDE.md diff --git a/docs/PROMPT_LIBRARY.md b/docs/archive/PROMPT_LIBRARY.md similarity index 100% rename from docs/PROMPT_LIBRARY.md rename to docs/archive/PROMPT_LIBRARY.md diff --git a/docs/TEAM_ARCHETYPE.md b/docs/archive/TEAM_ARCHETYPE.md similarity index 100% rename from docs/TEAM_ARCHETYPE.md rename to docs/archive/TEAM_ARCHETYPE.md diff --git a/docs/TEST_PYRAMID_PLAN.md b/docs/archive/TEST_PYRAMID_PLAN.md similarity index 100% rename from docs/TEST_PYRAMID_PLAN.md rename to docs/archive/TEST_PYRAMID_PLAN.md diff --git a/docs/TOP_01_PLAN.md b/docs/archive/TOP_01_PLAN.md similarity index 100% rename from docs/TOP_01_PLAN.md rename to docs/archive/TOP_01_PLAN.md diff --git a/docs/UFAWKES_OBS_SETUP.md b/docs/archive/UFAWKES_OBS_SETUP.md similarity index 100% rename from docs/UFAWKES_OBS_SETUP.md rename to docs/archive/UFAWKES_OBS_SETUP.md diff --git a/docs/VALUE_STREAM_MAP.md b/docs/archive/VALUE_STREAM_MAP.md similarity index 100% rename from docs/VALUE_STREAM_MAP.md rename to docs/archive/VALUE_STREAM_MAP.md diff --git a/docs/archive/security-alert-remediation-plan.md b/docs/archive/security-alert-remediation-plan.md new file mode 100644 index 00000000..ee73a533 --- /dev/null +++ b/docs/archive/security-alert-remediation-plan.md @@ -0,0 +1,53 @@ +# Security Alert Remediation Plan + +## Source +GitHub Code Scanning (Trivy) — 25 alerts on `main` branch. +All are OS-level CVEs in the Docker base image `python:3.14.6-slim-bookworm`. + +## Root Cause +The Dockerfile pins to a specific Python image version but does not run `apt-get upgrade` after the base image is pulled. The Debian bookworm base ships with outdated system packages. + +## Alerts by Category + +### Critical (6 alerts) +| Alert | Package | CVE | Fix | +|-------|---------|-----|-----| +| #367 | perl | Signed integer overflow (Storable) | Already removed via `apt-get remove perl` | +| #365 | perl | Incorrect regex processing | Already removed via `apt-get remove perl` | +| #362 | zlib | Integer overflow in zipOpenNewFileInZip4_6 | `apt-get upgrade` | +| #351 | sqlite | Integer overflow | `apt-get upgrade` | +| #137 | perl-archive-tar | Path traversal via symlinks | Already removed via `apt-get remove perl` | +| #136 | perl | Heap buffer overflow on 32-bit builds | Already removed via `apt-get remove perl` | + +### High (19 alerts) +| Alert | Package | Fix | +|-------|---------|-----| +| #399, #398 | openssl (QUIC DoS) | `apt-get upgrade` | +| #366 | perl (pack/unpack info disclosure) | Already removed | +| #265 | acl (symlink traversal) | `apt-get upgrade` | +| #264 | GNU gzip (buffer overflow) | `apt-get upgrade` | +| #247, #243, #239, #235, #231, #227, #223, #219 | libblkid (integer overflow ×8) | `apt-get upgrade` | +| #143 | perl-IO-Compress | Already removed | +| #139, #138 | perl-Archive-Tar | Already removed | +| #120, #118, #92 | ncurses (buffer overflow ×3) | `apt-get upgrade` | + +## Fix Strategy + +### Option A: Add `apt-get upgrade` to Dockerfile (Recommended) +Add `apt-get upgrade -y` to the base image build step. This pulls in security patches for all OS packages without changing the base image version. + +### Option B: Upgrade Python image version +Bump `PYTHON_IMAGE_VERSION` from `3.14.6` to the latest available (e.g., `3.14.7`). This may include newer Debian packages. + +### Option C: Both A + B +Upgrade image + run apt-get upgrade for maximum coverage. + +## Recommendation +**Option C** — Upgrade the Python image and add `apt-get upgrade`. This resolves all alerts and provides defense-in-depth. + +## Verification +After deploying the fix: +1. Rebuild the Docker image +2. Run Trivy scan locally: `trivy image prei:latest` +3. Verify all 25 alerts are resolved +4. Confirm the app still passes all tests diff --git a/docs/assessments/APP_REVIEW.md b/docs/assessments/archive/APP_REVIEW.md similarity index 100% rename from docs/assessments/APP_REVIEW.md rename to docs/assessments/archive/APP_REVIEW.md diff --git a/docs/assessments/AUDIT_GA_PIPELINE.md b/docs/assessments/archive/AUDIT_GA_PIPELINE.md similarity index 100% rename from docs/assessments/AUDIT_GA_PIPELINE.md rename to docs/assessments/archive/AUDIT_GA_PIPELINE.md diff --git a/docs/assessments/DISCOVERY_ASSESSMENT.md b/docs/assessments/archive/DISCOVERY_ASSESSMENT.md similarity index 100% rename from docs/assessments/DISCOVERY_ASSESSMENT.md rename to docs/assessments/archive/DISCOVERY_ASSESSMENT.md diff --git a/docs/assessments/GROWTH_AREAS_AUDIT.md b/docs/assessments/archive/GROWTH_AREAS_AUDIT.md similarity index 100% rename from docs/assessments/GROWTH_AREAS_AUDIT.md rename to docs/assessments/archive/GROWTH_AREAS_AUDIT.md diff --git a/docs/assessments/GROWTH_AREA_ASSESSMENT.md b/docs/assessments/archive/GROWTH_AREA_ASSESSMENT.md similarity index 100% rename from docs/assessments/GROWTH_AREA_ASSESSMENT.md rename to docs/assessments/archive/GROWTH_AREA_ASSESSMENT.md diff --git a/docs/assessments/REPO_AUDIT_2026-07.md b/docs/assessments/archive/REPO_AUDIT_2026-07.md similarity index 100% rename from docs/assessments/REPO_AUDIT_2026-07.md rename to docs/assessments/archive/REPO_AUDIT_2026-07.md diff --git a/docs/assessments/implementation-summary-growth-areas.md b/docs/assessments/archive/implementation-summary-growth-areas.md similarity index 100% rename from docs/assessments/implementation-summary-growth-areas.md rename to docs/assessments/archive/implementation-summary-growth-areas.md diff --git a/docs/planning/FEATURE_FLOW_AUDIT.md b/docs/planning/archive/FEATURE_FLOW_AUDIT.md similarity index 100% rename from docs/planning/FEATURE_FLOW_AUDIT.md rename to docs/planning/archive/FEATURE_FLOW_AUDIT.md diff --git a/docs/planning/PM Critique.md b/docs/planning/archive/PM Critique.md similarity index 100% rename from docs/planning/PM Critique.md rename to docs/planning/archive/PM Critique.md diff --git a/drafts/devto-v0.3.1-alpha.2.md b/drafts/devto-v0.3.1-alpha.2.md deleted file mode 100644 index f611588a..00000000 --- a/drafts/devto-v0.3.1-alpha.2.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "prei v0.3.1-alpha.2: Pipeline Lifecycle, Auto-Versioning, and Market Intelligence" -published: false -tags: realestate, investing, django, python, opensource ---- - -## Why - -Passive real estate investors need more than just a deal calculator — they need a **workflow**. The gap between "this deal looks good on paper" and "I own this property" is where most analysis tools stop. prei now bridges that gap with an end-to-end pipeline that tracks a property from discovery through screening, offer, renovation, closing, and into leasing. - -## What - -This alpha release ships the full pipeline lifecycle system (PIPE-0 through PIPE-14) alongside major improvements to market intelligence and infrastructure. You can now screen a property against 9 criteria, move it through offer/DD/renovation/closing stages, and track leasing — all within the app. - -We also overhauled our versioning system. Every build now carries its git tag and commit SHA as baked Docker metadata, visible in the footer, in logs, and via `docker inspect`. Releases are auto-tagged from conventional commits via `python-semantic-release`. - -## How - -The pipeline introduces new models (`PipelineProperty`, `ScreeningCriteria`, `PipelineTransaction`, `LeasingProperty`) and views for each stage. The nav now organizes actions into Buy / Maintain / Sell groups. - -For operators: `docker compose pull && docker compose up -d` gets you the latest. The footer will show `v0.3.1-alpha.2` with the commit SHA. - -## Proof - -105 unit tests pass across models, validators, finance utilities, and forms. The pipeline views have been verified through our live-system acceptance test suite (PIPE-14). Every Docker image is scanned for CRITICAL/HIGH CVEs, signed with build provenance attestation, and published to GHCR. - -## What's Next - -- Property comparison and portfolio scenario modeling -- Enhanced notifications and collaboration features -- Additional data source integrations (Redfin, Zillow) -- Stable v0.3.0 release - -[GitHub Repo](https://github.com/paruff/prei) diff --git a/drafts/linkedin-v0.3.1-alpha.2.md b/drafts/linkedin-v0.3.1-alpha.2.md deleted file mode 100644 index 9cc67308..00000000 --- a/drafts/linkedin-v0.3.1-alpha.2.md +++ /dev/null @@ -1,9 +0,0 @@ -prei v0.3.1-alpha.2 is out — our first pre-release with the full deal pipeline lifecycle. - -We built the screens, forms, and models to track a property from discovery (Growth Area Explorer) through 9-criteria screening, offer, due diligence, renovation, closing, and leasing. No more spreadsheets for deal workflow — it's all in one place. - -Also shipping: auto-versioned Docker builds (git tag → baked metadata → structured logs), ATTOM/FRED market data integration, and growth area pagination/CSV export. - -Try it: `docker compose up -d` at github.com/paruff/prei - -#realestateinvesting #opensource #django #proptech