From fada301fe5a78c38d475556dffaf111a9f541651 Mon Sep 17 00:00:00 2001 From: Jean Luca Bez Date: Thu, 4 Jun 2026 11:34:04 -0700 Subject: [PATCH 01/49] design: multi-file batch analysis (phase 1) Spec for analyzing multiple independent files: a session-free core batch primitive (summarize_files) reused by web/CLI/library, plus a web active-file switcher (compatibility shim over the existing single-file session keys) and a combined cross-file summary. Local upload and Globus feed one source-agnostic file list; per-file type inferred from extension; max 50 files. Globus is remote compute, so the file list/UI is unified but metric execution stays dispatched by source (local routes vs remote_metric_runner); the combined summary shows Globus files with metadata only in phase 1. --- ...-06-04-multi-file-batch-analysis-design.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md diff --git a/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md b/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md new file mode 100644 index 00000000..b5dbd66b --- /dev/null +++ b/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md @@ -0,0 +1,235 @@ +# Multi-File Batch Analysis — Design + +**Date:** 2026-06-04 +**Branch:** `multi-file-analysis` (off `develop`) +**Status:** Approved design — pending implementation plan + +## Summary + +Let users analyze **multiple files at once** (a batch of independent datasets, +with possibly different schemas) instead of a single file. Phase 1 treats each +file independently: users analyze one **active file** at a time using the +existing inspector, and can view a **combined cross-file summary**. A later +**phase 2** will add relational/different-schema handling (joins across files); +this design deliberately leaves the seams for it but does not implement it. + +## Goals (Phase 1) + +- Upload / select **many files** via local multi-select + drag-drop, and via + the existing **Globus** remote integration. +- Per-file format is **inferred from the file extension** (mixed types allowed); + the manual "File type" dropdown is removed. +- An **active-file switcher**: one file is active at a time and the entire + current inspector (summary, metrics, visualizations) operates on it. +- A **combined summary**: a per-file overview table plus aggregate totals. +- Local uploads and Globus transfers feed **one** source-agnostic file list. +- The batch/aggregation logic is a **session-free core primitive** reused by the + web UI, the library, and the CLI. + +## Non-Goals (Phase 1 — YAGNI) + +- Directory upload (`webkitdirectory`) and zip-archive upload. +- Same-schema column-level comparison across files. +- Simultaneous side-by-side metric rendering for multiple files. +- Relational joins / multi-table modeling (**phase 2**). + +## Background / Current State + +The single-file assumption is deep in the web layer: + +- The Flask session holds `uploaded_file_path` / `uploaded_file_name` / + `uploaded_file_type` (a single file). These keys are referenced ~74 times + across `web/routes/{core,metrics,custom,llm}.py`, `web/routes/utils.py`, and + the inspector templates. +- `read_file((path, name, type))` returns one DataFrame; there are ~10 + `read_file` / `load_dataframe` call sites. +- The metric result cache key is built from a single `file_name` + (`generate_metric_cache_key`). +- **Globus is remote *compute*, not just a remote file source.** `web/globus.py` + serialises `remote_metric_runner` and ships it to a Globus Compute **endpoint**, + where `aidrin` runs the metric next to the data. A Globus file's + `globus_file_path` is a path **on the remote endpoint**, not on the AIDRIN + server. It has its own session keys (`globus_file_path/name/type`, + `globus_endpoint_id`), its own execution path, and its own frontend mode + (`globus_mode` / `AIDRIN_GLOBUS_*` / `fetchGlobusSummary`). This execution path + must remain — local metric routes cannot run on a remote path. +- The library API (`aidrin.calculate_*(file_info)`) is **session-free** and + already supports multiple files by iteration. The CLI/headless mode (on other + branches) is likewise config/arg driven and session-free. + +**Note on dependencies:** the per-file error handling described here mirrors the +`load_dataframe` helper and friendly-error mapping introduced on the +`parquet-support` branch. If this feature lands before that work is merged, it +should include an equivalent helper; otherwise it reuses it. + +## Architecture: Two Layers + +The central decision is to split the feature into a presentation-agnostic core +and a web-only state layer. + +### Layer 1 — Core batch primitive (session-free) + +A new `aidrin` function operates purely on a list of `file_info` tuples: + +```python +def summarize_files(file_infos): + """Return {"files": [per_file, ...], "totals": {...}}. + + per_file = { + "name": str, "type": str, + "records": int|None, "features": int|None, + "completeness": float|None, # fraction of non-null cells, 0..1 + "size_bytes": int|None, + "status": "ok"|"error", + "error": str|None, # short message when status == "error" + } + totals = { + "file_count": int, + "ok_count": int, "error_count": int, + "total_records": int, + "by_type": {type: count, ...}, + } + """ +``` + +- Reads each file via `read_file`; computes lightweight stats **without Celery** + (e.g. completeness = `df.notna().to_numpy().mean()`), so it is cheap and safe + to call synchronously. +- A file that fails to load becomes a `status: "error"` row with a short + message — **one bad file never aborts the batch**. +- Lives in the core package (e.g. `aidrin/batch.py`), re-exported from + `aidrin/__init__.py`. Consumed by the web combined-summary endpoint, the CLI's + batch mode, and library users. + +### Layer 2 — Web active-file state (Approach A shim) + +A presentation-only construct in the Flask session: + +- `session["uploaded_files"]`: list of + `{"id": uuid, "name": str, "type": str, "path": str, "source": "local"|"globus"}`. + Globus entries additionally carry `endpoint_id` (and the remote path lives in + `path`). The previously top-level `globus_file_*` keys are **folded into the + entry**, not kept separately. +- `session["active_file_id"]`: the currently selected file's id. +- A `set_active_file(file_id)` helper sets `active_file_id` **and writes the + active file's path/name/type into the existing `uploaded_file_*` session + keys**. For a Globus entry it also restores the Globus execution context + (endpoint id + remote path) the frontend's `globus_mode` needs. + +This compatibility shim means every existing **local** metric route, the ~10 +read sites, and the templates keep working **unchanged** — they always operate on +the active file. Execution stays **dispatched by source**: a local active file +runs through the local routes; a Globus active file runs through the existing +`remote_metric_runner` path. Only the upload/Globus entry points and a few new +file-management endpoints need to be multi-file aware. + +## Data Flow + +1. **Upload (local):** `POST /inspector` reads `request.files.getlist("file")`. + For each file: enforce count/size limits → save with a unique stored name → + `infer_file_type(filename)` → append `{id, name, type, path, source:"local"}` + to `uploaded_files`. Set the first newly-added file active (via the shim). +2. **Globus:** the Globus selection flow appends entries with `source:"globus"` + (carrying `endpoint_id` + remote `path`) to the **same** `uploaded_files` + list. The `globus_file_*` keys are folded into the entry. The **remote + execution path is unchanged** — activating a Globus file still dispatches + through `remote_metric_runner` (`globus_mode`), not the local routes. +3. **Switch active file:** `POST /files//activate` → `set_active_file(id)` → + the frontend re-renders the inspector for the new active file, in local or + Globus mode depending on the entry's `source`. +4. **Combined summary:** `GET /files/summary`. **Local** files are summarized + directly via `aidrin.summarize_files`. **Globus** files are listed with + metadata only (name, type, size, source; stats shown as `n/a`) in phase 1 — + computing remote stats would require a per-file Globus Compute call, deferred + to a later phase. +5. **Metrics:** unchanged — they read the active file through the legacy keys + (local) or the restored Globus context (remote). + +## New / Changed Endpoints + +| Endpoint | Change | +| --- | --- | +| `POST /inspector` | Accept multiple files; build the list; infer types. | +| `GET /files` | List `uploaded_files` (+ which is active) for the switcher. | +| `POST /files//activate` | Set the active file. | +| `POST /files//remove` | Remove a file (delete from disk + list). | +| `GET /files/summary` | Combined per-file overview + totals (Globus = metadata only). | +| Globus selection route | Append into the shared list (`source:"globus"`, `endpoint_id`); fold in `globus_file_*`. | +| All metric / summary / feature routes | **Unchanged**; execution dispatched by source (local routes vs `remote_metric_runner`). | + +## Type Inference + +New `infer_file_type(filename)` in `aidrin/file_handling/file_parser.py`: + +- Maps real extensions to reader keys via an explicit `EXTENSION_MAP`, resolving + the Excel quirk (`READER_MAP` currently uses the combined key + `".xls, .xlsb, .xlsx, .xlsm"`; the map points `.xls/.xlsb/.xlsx/.xlsm` at it). +- Unknown extension → `None` → the file is listed with `status: "error"` + ("Unsupported file type") and cannot be made active. + +## Frontend (inspector) + +- **Upload dropzone:** add `multiple`; support multi-file drag-drop. **Remove** + the "File type" `` (type is inferred). + the "File type" `` also drops its `accept`-attribute filtering and the existing + client-side "select a file type" validation guard (`main.js:31`); replace with + extension-based client validation. - **File switcher:** a list (in the sidebar) of uploaded files, each showing name + type/status badge, the active one highlighted; click to activate; per-file remove button. @@ -187,16 +224,29 @@ New `infer_file_type(filename)` in `aidrin/file_handling/file_parser.py`: - Per-file metric caching already works because the cache key includes the file. - **Change:** key the cache on the **unique stored filename / file_id** instead of the display `file_name`, to prevent collisions when two files in a batch - share a display name. Update `generate_metric_cache_key` and the `store_result` - user-key accordingly. + share a display name. This must move **all four** file-name-derived sites + together or cached results become unretrievable: `generate_metric_cache_key`, + the `store_result` user-key (`user:{id}:file:{name}:{metric}`), the + `cached_result` lookup (`core.py:286-294`), and the `clear_all_user_cache` + prefix match. (Telemetry `trace_metric(file_name=...)` may keep the display + name — cosmetic only.) ## Limits -- `MAX_CONTENT_LENGTH` already caps the **whole** multipart request (the - parquet/upload-limit work sets this; default 1 GB) — this naturally bounds the - combined batch size. No per-file cap needed. +- `MAX_CONTENT_LENGTH` (from the `upload-size-limit` branch; default 1 GB) caps + the **whole** multipart request, bounding the combined batch size. - New `AIDRIN_MAX_UPLOAD_FILES` (default **50**) bounds the number of files per - batch; exceeding it returns a clear error and rejects the upload. + batch. +- **Enforcement ordering:** `MAX_CONTENT_LENGTH` is enforced by Flask **before** + the body is parsed (returns a bare 413); `AIDRIN_MAX_UPLOAD_FILES` can only be + checked **after** parsing `getlist("file")`. Add a friendly 413 handler and a + clear "too many files" message so the two limits don't produce confusing UX. +- **Session storage (important):** Flask's default session is a client-side + signed cookie (~4 KB). A 50-entry `uploaded_files` list — especially with long + remote Globus paths — can exceed that and **silently drop the session**. + Therefore store `uploaded_files` **server-side** (e.g. in `TEMP_RESULTS_CACHE` + keyed by `user_id`), keeping only small pointers (`active_file_id`, the legacy + `uploaded_file_*`/`globus_file_*` shim values) in the cookie. ## Error Handling @@ -204,19 +254,34 @@ New `infer_file_type(filename)` in `aidrin/file_handling/file_parser.py`: with a short message; the batch continues. The active-file detail view shows the friendly read error (existing `load_dataframe` behavior). - **Too many files / unsupported type:** clear, user-facing messages. +- **Active Globus file vs. local existence check:** the inspector's stale-session + validation (`core.py:65-78`) and several routes guard on + `os.path.exists(uploaded_file_path)`. A Globus active file's path is **remote**, + so this is `False` on the AIDRIN server and would wrongly **wipe the session**. + Add a `source == "globus"` bypass to every such existence check (treat remote + files as present; let the Globus path handle reachability). - **Empty batch / no active file:** the inspector falls back to the upload panel (existing stale-session handling generalizes to "no files"). ## Testing - **Core:** `summarize_files` — per-file stats, totals, mixed types, a failing - file among good ones, empty input. `infer_file_type` — each supported - extension, Excel variants, unknown extension. + file among good ones, empty input, and `read_file` returning each of + `DataFrame`/`None`/`str`. Assert `completeness` equals the existing row-wise + metric on the same data. `infer_file_type` — each supported extension, Excel + variants, unknown extension. - **Web (integration):** multi-file upload builds the list and sets an active file; `activate` updates the legacy keys and an existing metric still works; - `/files/summary` shape + totals; `remove` deletes file + entry; per-file error - appears in the summary without 500s; `AIDRIN_MAX_UPLOAD_FILES` enforced. -- **Globus:** multi-file transfer appends to the shared list (mocked transfer). + `/files/summary` shape + totals; `remove` deletes file + entry; **removing the + active file** activates the next (or returns to the upload panel); per-file + error appears in the summary without 500s; `AIDRIN_MAX_UPLOAD_FILES` enforced + (and ordering vs. the 413 size cap); cached metric results survive the file_id + re-key (store then retrieve). +- **Globus:** multi-file selection appends to the shared list (mocked); a Globus + active file is **not** wiped by the local `os.path.exists` stale check, and the + shim repopulates `globus_file_*`/`globus_endpoint_id`. +- **Session storage:** a 50-file list with long remote paths persists (does not + overflow the cookie) — verifies the server-side storage decision. - Follow TDD (red → green) per the existing reader/route test patterns. ## Phase 2 Seam (relational / different schemas) From 30fa1cb34a6a192143df6e8d984ca4d1d6b6a126 Mon Sep 17 00:00:00 2001 From: Jean Luca Bez Date: Thu, 4 Jun 2026 11:56:05 -0700 Subject: [PATCH 03/49] spec: finalize combined-summary design (phase 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combined summary is the default landing view after a multi-file upload: totals strip + per-file table (File/Type/Source/Records/Features/Size/Status), table+totals only, no charts. No metrics in phase 1 — summarize_files does a single light read for records/features/status (local), Globus rows metadata-only. Drops completeness from the summary (and the row-wise/cell-wise concern with it). --- ...-06-04-multi-file-batch-analysis-design.md | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md b/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md index fe422bce..c1c03971 100644 --- a/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md +++ b/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md @@ -83,8 +83,7 @@ def summarize_files(file_infos): per_file = { "name": str, "type": str, - "records": int|None, "features": int|None, - "completeness": float|None, # row-wise, 0..1 (see note below) + "records": int|None, "features": int|None, # from a single read; NO metrics "size_bytes": int|None, "status": "ok"|"error", "error": str|None, # short message when status == "error" @@ -103,11 +102,12 @@ def summarize_files(file_infos): `None` (unsupported/missing name) or a `str` (read error message) → `status:"error"` with that message. (This is the same tri-state the `load_dataframe` helper handles — see dependency note.) -- Computes lightweight stats **without Celery** so it is cheap to call - synchronously. **Completeness must use the same row-wise definition as the - existing metric** (`completeness.py`): `1 - df.isnull().any(axis=1).mean()` - (fraction of rows with no missing value), **not** a cell-wise mean — otherwise - the summary column would disagree with the per-file Data Quality result. +- **Phase 1 computes NO metrics** — only structural facts available from a single + read: `records` (`len(df)`), `features` (`len(df.columns)`), `size_bytes` + (`os.path.getsize`, no parse), and load `status`. Completeness/duplicates/etc. + are deliberately excluded here; they remain in the per-file Data Quality view. + (Keeping metrics out also avoids the row-wise-vs-cell-wise completeness + question entirely for now.) - A file that fails to load becomes a `status: "error"` row with a short message — **one bad file never aborts the batch**. - **Synchronous cost:** summarizing up to 50 files (combined up to the request @@ -161,11 +161,14 @@ file-management endpoints need to be multi-file aware. 3. **Switch active file:** `POST /files//activate` → `set_active_file(id)` → the frontend re-renders the inspector for the new active file, in local or Globus mode depending on the entry's `source`. -4. **Combined summary:** `GET /files/summary`. **Local** files are summarized - directly via `aidrin.summarize_files`. **Globus** files are listed with - metadata only (name, type, size, source; stats shown as `n/a`) in phase 1 — - computing remote stats would require a per-file Globus Compute call, deferred - to a later phase. +4. **Combined summary:** after a multi-file upload the inspector **lands on the + combined summary** (the batch overview), not a single file's panels. + `GET /files/summary`: **local** files go through `aidrin.summarize_files` + (one read → records, features, size, status — no metrics); the web route + decorates each row with `source`. **Globus** files are listed with metadata + only (name, type, size, source; records/features `n/a`) — no remote read in + phase 1. Clicking any row activates that file and drills into its existing + per-file panels. 5. **Metrics:** unchanged — they read the active file through the legacy keys (local) or the restored Globus context (remote). @@ -211,10 +214,16 @@ New `infer_file_type(filename)` in `aidrin/file_handling/file_parser.py`: - **File switcher:** a list (in the sidebar) of uploaded files, each showing name + type/status badge, the active one highlighted; click to activate; per-file remove button. -- **Combined summary panel:** a new view rendering the per-file overview table - (name, type, #records, #features, completeness, size, status) and the totals; - clicking a row activates that file. Errors render inline per row. Globus rows - show metadata only with stats as `n/a` (phase 1). +- **Combined summary ("Batch Overview"):** the **default landing view** after a + multi-file upload. Two parts: + - **Totals strip** (cards): `# files`, `# loaded OK` / `# failed`, + `total records`, files-by-type, files-by-source, total size. + - **Per-file table**: columns **File · Type · Source · Records · Features · + Size · Status** (no metrics column in phase 1). Failed rows show the friendly + error inline (`—` for stats); Globus rows show a "remote" badge with + records/features `n/a`. Clicking a row activates that file → its per-file + panels. + - **Table + totals only** — no charts in phase 1. - **Globus panel:** allow selecting multiple remote files; each selected file is appended to the shared list as a `source:"globus"` entry. Activating one keeps the inspector in `globus_mode` (remote execution). @@ -265,11 +274,11 @@ New `infer_file_type(filename)` in `aidrin/file_handling/file_parser.py`: ## Testing -- **Core:** `summarize_files` — per-file stats, totals, mixed types, a failing - file among good ones, empty input, and `read_file` returning each of - `DataFrame`/`None`/`str`. Assert `completeness` equals the existing row-wise - metric on the same data. `infer_file_type` — each supported extension, Excel - variants, unknown extension. +- **Core:** `summarize_files` — records/features/size/status + totals, mixed + types, a failing file among good ones, empty input, and `read_file` returning + each of `DataFrame`/`None`/`str`. Assert **no metric** fields are present + (records = `len(df)`, features = `len(df.columns)` only). `infer_file_type` — + each supported extension, Excel variants, unknown extension. - **Web (integration):** multi-file upload builds the list and sets an active file; `activate` updates the legacy keys and an existing metric still works; `/files/summary` shape + totals; `remove` deletes file + entry; **removing the From 705156b261ea7ff026304e32cd8a18205fdb2f76 Mon Sep 17 00:00:00 2001 From: Jean Luca Bez Date: Thu, 4 Jun 2026 12:00:19 -0700 Subject: [PATCH 04/49] spec: sort batch overview by name; fetch Globus counts async Table sorted by file name (case-insensitive). Globus rows now fetch records/features asynchronously via the existing remote _summary_statistics (reusing the globus_summary cache) instead of showing n/a; total records updates as remote counts stream in. --- ...-06-04-multi-file-batch-analysis-design.md | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md b/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md index c1c03971..533e4736 100644 --- a/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md +++ b/docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md @@ -165,10 +165,13 @@ file-management endpoints need to be multi-file aware. combined summary** (the batch overview), not a single file's panels. `GET /files/summary`: **local** files go through `aidrin.summarize_files` (one read → records, features, size, status — no metrics); the web route - decorates each row with `source`. **Globus** files are listed with metadata - only (name, type, size, source; records/features `n/a`) — no remote read in - phase 1. Clicking any row activates that file and drills into its existing - per-file panels. + decorates each row with `source`. **Globus** files appear immediately with + metadata (name, type, size, source); their records/features are fetched + **asynchronously** via the existing remote `_summary_statistics` (which already + returns `records_count`/`features_count`, `web/globus.py:144-145`) and stream + into the row when ready — reusing the `globus_summary:{endpoint_id}:{file_path}` + cache so already-viewed files are instant. Clicking any row activates that file + and drills into its per-file panels. 5. **Metrics:** unchanged — they read the active file through the legacy keys (local) or the restored Globus context (remote). @@ -180,7 +183,7 @@ file-management endpoints need to be multi-file aware. | `GET /files` | List `uploaded_files` (+ which is active) for the switcher. | | `POST /files//activate` | Set the active file. | | `POST /files//remove` | Remove a file (delete local file from disk + list). **If the removed file was active**, activate the next remaining file (or, if none remain, clear `uploaded_file_*` so the inspector returns to the upload panel). Always re-run `set_active_file`. | -| `GET /files/summary` | Combined per-file overview + totals (Globus = metadata only). | +| `GET /files/summary` | Combined per-file overview + totals (local synchronous; Globus records/features fetched async via existing remote summary). | | Globus selection route | Append into the shared list (`source:"globus"`, `endpoint_id`); fold in `globus_file_*`. | | All metric / summary / feature routes | **Unchanged**; execution dispatched by source (local routes vs `remote_metric_runner`). | @@ -219,10 +222,14 @@ New `infer_file_type(filename)` in `aidrin/file_handling/file_parser.py`: - **Totals strip** (cards): `# files`, `# loaded OK` / `# failed`, `total records`, files-by-type, files-by-source, total size. - **Per-file table**: columns **File · Type · Source · Records · Features · - Size · Status** (no metrics column in phase 1). Failed rows show the friendly - error inline (`—` for stats); Globus rows show a "remote" badge with - records/features `n/a`. Clicking a row activates that file → its per-file - panels. + Size · Status** (no metrics column in phase 1), **sorted by file name** + (case-insensitive). Failed rows show the friendly error inline (`—` for + stats). Clicking a row activates that file → its per-file panels. + - **Local vs Globus rows:** local rows render fully on first paint (synchronous + read). Globus rows render with metadata immediately and a "loading…" + records/features cell that **fills in asynchronously** from the remote + `_summary_statistics` (cached per file). `total records` updates as remote + counts arrive. - **Table + totals only** — no charts in phase 1. - **Globus panel:** allow selecting multiple remote files; each selected file is appended to the shared list as a `source:"globus"` entry. Activating one keeps @@ -288,7 +295,9 @@ New `infer_file_type(filename)` in `aidrin/file_handling/file_parser.py`: re-key (store then retrieve). - **Globus:** multi-file selection appends to the shared list (mocked); a Globus active file is **not** wiped by the local `os.path.exists` stale check, and the - shim repopulates `globus_file_*`/`globus_endpoint_id`. + shim repopulates `globus_file_*`/`globus_endpoint_id`; the batch overview + fills a Globus row's records/features from the remote `_summary_statistics` + result (mocked) and serves a second request from the `globus_summary` cache. - **Session storage:** a 50-file list with long remote paths persists (does not overflow the cookie) — verifies the server-side storage decision. - Follow TDD (red → green) per the existing reader/route test patterns. From 5d5dfa99cf9c82a62364b4728d1cad3a1ad532c6 Mon Sep 17 00:00:00 2001 From: Jean Luca Bez Date: Thu, 4 Jun 2026 13:42:55 -0700 Subject: [PATCH 05/49] plan: multi-file batch analysis implementation 13 TDD tasks across 6 milestones: core batch layer (summarize_files, infer_file_type), server-side file list + active-file shim, cache re-key, file-management endpoints, multi-file upload, limits, Globus unification, and the frontend switcher + batch-overview landing view. --- .../2026-06-04-multi-file-batch-analysis.md | 1327 +++++++++++++++++ 1 file changed, 1327 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-04-multi-file-batch-analysis.md diff --git a/docs/superpowers/plans/2026-06-04-multi-file-batch-analysis.md b/docs/superpowers/plans/2026-06-04-multi-file-batch-analysis.md new file mode 100644 index 00000000..c7e99c32 --- /dev/null +++ b/docs/superpowers/plans/2026-06-04-multi-file-batch-analysis.md @@ -0,0 +1,1327 @@ +# Multi-File Batch Analysis 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:** Let AIDRIN analyze many independent files at once — an active-file switcher plus a "Batch Overview" landing summary — fed by both local multi-upload and Globus. + +**Architecture:** Two layers. (1) A session-free **core** primitive in `aidrin` (`summarize_files`, `infer_file_type`). (2) A **web** active-file shim: a server-side `uploaded_files` list + `active_file_id`; `set_active_file` mirrors the active file's identity into the existing `uploaded_file_*` (and, for Globus, `globus_file_*`) session keys so every existing route keeps working unchanged. Execution stays dispatched by source (local routes vs. remote `remote_metric_runner`). + +**Tech Stack:** Python 3.10+, Flask, pandas, pytest (unit + integration via `tests/integration/conftest.py`), vanilla JS + Tailwind. + +**Spec:** `docs/superpowers/specs/2026-06-04-multi-file-batch-analysis-design.md` + +--- + +## Prerequisites & Cross-Branch Notes + +- This branch (`multi-file-analysis`) is off `develop`. It does **not** have + `MAX_CONTENT_LENGTH` (on `upload-size-limit`) or `load_dataframe` (on + `parquet-support`). This plan adds the small pieces it needs directly + (Milestone 4 adds the size cap + 413; `summarize_files` handles `read_file`'s + tri-state itself), so it is self-contained. +- Run tests with the project venv: `.venv/bin/python -m pytest`. +- Follow TDD: write the failing test, watch it fail, implement minimally, watch it pass, commit. + +## File Structure + +**Create** +- `aidrin/batch.py` — `summarize_files(file_infos)` (session-free, no Celery). +- `tests/unit/test_infer_file_type.py` +- `tests/unit/test_summarize_files.py` +- `web/routes/files.py` — file-management blueprint (`/files`, activate, remove, summary). +- `web/templates/_components/file_switcher.html` — sidebar file list. +- `web/templates/_panels/_batch_overview.html` — batch overview panel. +- `tests/integration/test_files_routes.py` +- `tests/integration/test_multi_upload.py` + +**Modify** +- `aidrin/file_handling/file_parser.py` — add `EXTENSION_MAP` + `infer_file_type`. +- `aidrin/__init__.py` — export `summarize_files`. +- `web/routes/utils.py` — file-list store + `set_active_file`; cache key → file_id. +- `web/routes/core.py` — multi-file `/inspector`; Globus-aware stale check; land on overview. +- `web/routes/__init__.py` — register the `files` blueprint. +- `web/routes/globus.py` — append selections into the shared list; fold `globus_file_*`. +- `web/__init__.py` — `MAX_CONTENT_LENGTH`, `AIDRIN_MAX_UPLOAD_FILES`, 413 handler. +- `web/templates/inspector.html` — include switcher + batch overview; default panel. +- `web/templates/_components/upload_panel.html` — `multiple`; remove type `` + +**Files:** +- Modify: `web/templates/_components/upload_panel.html`, `web/static/js/main.js` + +- [ ] **Step 1: Edit the template** — add `multiple` and drop the select. + +In `web/templates/_components/upload_panel.html`: delete the `