DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path scoping-notes/loomworks-folder-upload-scoping-note-v0_1.md

Loomworks — folder upload — scoping note — v0.1

Document: loomworks-folder-upload-scoping-note-v0_1 Version: v0.1 Status: scoping note — precedes a CR. The extend is laid out; the Operator settles scope (multi-file restore alone vs. full folder extend) and the parent-persistence decision before a CR is drafted. Date: 2026-06-27 Origin: Operator request to restore folder/file upload — flagged as a regression: upload is a built engine capability (Phase 58/59 endpoint + ObjectStore) that was dropped when the Companion frontend was rebuilt to mockups that omitted it. Operator specifically wants folder upload (a directory of files, structure preserved). Inspection: read-only CC inspection 2026-06-27 (engine uploads.py, storage/object_store.py on main; OL AttachButton.tsx, postUpload, lib/folder-upload.ts).


The headline: restore vs. extend

The inspection drew a clean line, and it determines the size of the work.

Flat multi-file upload is a RESTORE — frontend-only. The engine already accepts list[UploadFile] end-to-end (uploads.py operator_upload, line 441): multiple files in one request, each chunk-read (50 MB cap), run through the executor (detection → transformation → assertion drafting), persisted to the ObjectStore via put_blob, written as an UploadEventReceived event. The frontend AttachButton already supports multiple. If the Operator wants "multi-file back," it is re-surfacing/confirming the existing frontend pathway — no engine work.

Folder upload with preserved directory structure is an EXTEND — touches engine + frontend. The current pathway deliberately discards structure: AttachButton.tsx:9-13 documents it plainly ("FILE only… the client sends basenames, not webkitRelativePath… the Phase 60 OL folder pathway is incomplete"), and postUpload appends each file as file.name (basename), never webkitRelativePath. The engine cannot receive structure today either. So folder upload needs new work on both sides.

What the extend requires

Frontend

  1. Add a webkitdirectory input (a folder picker) alongside the existing file picker.
  2. Transmit webkitRelativePath per file (today postUpload sends basenames only). The relative path is what carries the directory structure.
  3. The platform-system-file filtering scaffolding already exists (lib/folder-upload.ts filterPlatformSystemFiles strips .DS_Store etc.) — reuse it.

Engine — three pieces

  1. Accept the relative paths. operator_upload has no relative_path/paths Form field (signature lines 443-446). The multipart filename is basename-only and can't carry the path reliably, so add a new multipart Form field (e.g. relative_paths: list[str], index-aligned with files).
  2. Thread them into the manifest. FolderManifestEntry.relative_path is currently hardcoded to filename (line 697, the path component discarded). Wire the received relative paths through to the manifest entries.
  3. Persist the parent aggregation event. Today the parent (parent_upload_event_id + folder_manifest) exists only in the HTTP response, not as a stored event (per-file children carry parent_upload_event_id=None, folder_manifest=None, lines 651-652; "multi-file aggregation persistence is a Step 1 follow-on," lines 608-610). A folder upload that leaves no stored parent tying its files together is a folder upload that isn't really recorded as a folder. This is the data-model addition — and the substrate piece that makes this an extend, not a re-wire.

ObjectStore — no structural change (a decision, not a blocker)

Keys are flat, UUID-based: {prefix}/{uuid4}-{safe_filename} (generate_key, line 161). Directory structure does not need to be encoded in the storage key — it lives in folder_manifest metadata (relative_path). This is the simplest path and matches the content-addressed design. Encoding structure into keys would be a larger change and is not required. Recommended: leave the ObjectStore flat, structure in the manifest.

The latent finding worth surfacing

The parent-aggregation-event-is-response-only limitation (engine piece 3) is not only a folder-upload gap. It means even today's flat multi-file uploads leave no stored parent tying the files together — the aggregation is computed for the response and discarded. Folder upload forces fixing it, but it is arguably worth fixing regardless (a multi-file upload, folder or not, that records no parent is a gap in the provenance/Memory model — the files arrive as orphaned siblings with no record they came in together). The CR should decide whether to fix parent-persistence as the general thing (benefiting flat multi-file too) or narrowly for folder uploads. Lean: fix it generally — it's the same code either way, and a stored parent event is the honest record of "these arrived together," which the provenance-threads discipline wants.

Operator decisions before a CR

  1. Scope. Multi-file restore alone (frontend-only, fast), or the full folder extend (engine + frontend)? The request was specifically folder-with-structure, so the lean is the full extend — but if "just give me upload back" is the near-term need, the restore ships in a fraction of the time and the folder extend follows.
  2. Parent-persistence breadth (engine piece 3). Fix it generally (flat multi-file gets a stored parent too — lean) or narrowly for folder uploads only?
  3. ObjectStore — confirm the lean: leave keys flat, structure in folder_manifest metadata (recommended), not encoded in keys.
  4. Relative-path transport — confirm the lean: a new index-aligned relative_paths: list[str] Form field, rather than trying to smuggle the path through the multipart filename (which is basename-only and unreliable).

Shape of the eventual CR

If the full extend is chosen, the natural split mirrors the list-loading arc: engine first (accept paths → thread to manifest → persist parent event; one migration if the parent event needs a new stored shape — confirm at CR drafting whether the event store already has a place for it or needs schema), get it merged, then frontend (webkitdirectory input + webkitRelativePath transmission) against the merged engine. Engine-before-frontend, the same discipline CR-1→CR-2 used, because the frontend's transmission shape depends on the engine's accepted Form field.

None of this blocks the timestamp work (frontend-only, independent) or the voice-panel fix (independent).


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — folder upload — scoping note — v0.1 — 2026-06-27 Multi-file = restore (frontend-only). Folder-with-structure = extend (engine: accept paths + thread to manifest + persist parent event; frontend: webkitdirectory + webkitRelativePath). ObjectStore unchanged. Parent-persistence is a latent gap worth fixing generally. Precedes a CR pending Operator scope decisions.