Standardize the two classify tabs: By-transmittal drops the party→slot→bin
tree (and its multi-field "+ Transmittal" form) for a flat per-file grid that
mirrors By-tracking. Each file gets ONE editable text input — its full
transmittal folder path "<party>/<received|issued>/<YYYY-MM-DD_TN (STATUS) -
Title>". Committing it find-or-creates the party/slot/bin; structure stays
derived, never stored.
Drag-and-drop (from the source tree onto the grid):
- plain drop on a routed row → the dropped files JOIN that row's folder;
- ⌘/Ctrl-drop on a routed row → a prompt prefilled with that folder's path
lets you edit it into a NEW transmittal the files go to (the original is
untouched; an unedited path dedups via find-or-create);
- drop on empty space / an unrouted row → files are added as blank rows to fill.
Model (classify.js): adds a `transmittalWorkset` (parallel to trackingWorkset)
plus addToTransmittalGrid / removeFromTransmittalGrid / transmittalGridKeys and
setTransmittalPath(keys, path) — the single parser for "<party>/<slot>/<folder>"
that also prunes any bin a re-route empties. app.js importPaths now reuses
setTransmittalPath for its route axis (one parser, less duplication).
Removes the now-dead tree rendering/CRUD (party/bin nodes, binForm, the bin
filename editor, the bin drop zone). Tests updated to the grid model: tab
render shows the folder-path input; drop join/branch/empty; edit re-routes and
prunes the emptied folder; ✕ removes. 71/71 classify specs pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an AI-friendly classification round-trip alongside the By-tracking grid:
- "⬇ Export paths" (filetree header): downloads the filtered file list as a
1-column CSV of full (root-relative) paths — the same keys the importer
matches on. Hand it to an LLM to classify into
<party>/<direction>/<transmittal>/<file>.ext.
- "Import paths…" (above the target list): loads a 2-column CSV (old path,
new path). Each new path drives both axes — the trailing filename sets the
tracking number (rename, via parseFilename → tracking tree) and the leading
<party>/<direction>/<transmittal> segments route a transmittal (via
parseFolder → transmittal tree). MERGE semantics: only files named in the
CSV are touched; others keep their classification.
- Per-row problems (unknown old path, unparseable filename/transmittal, bad
direction) are collected and offered as a downloadable errors CSV, with a
summary toast — scales to thousands of rows. Either axis can apply
independently, so a filename-only new path is a rename with no error.
This replaces the JSON "Export for editing" / "Import edits" pair (the CSV
path form is fully expressive for this model and simpler to round-trip); the
TSV "Export list" clipboard→Excel button is kept. Buttons can grow into a
modal later if more options are needed.
Includes a Playwright test driving the real file-input import (rename+route,
filename-only, merge-preserves-unlisted, CSV-quoted comma in title, error row).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse the two-mode tool (Classify & copy / Rename in place) into a single
surface. The top mode toggle and the standalone Rename spreadsheet are gone; the
By-tracking grid is now the one editable table, and the two operations are framed
as the two physical things you can do with a classified file:
- By tracking number → "Rename…": a tracking number + rev + title with no
transmittal IS a rename, so this renames the name-complete grid files IN PLACE
on disk (rename.js, lifted from the spreadsheet — HTTP move / FS copy+remove,
resumable). Blocking red no-backup warning; a renamed file is now correctly
named so it leaves the grid (classify.forgetFile).
- By transmittal → "Copy…": the existing resumable/verified archive copy, moved
onto this tab; enabled once files are fully classified (tracking + transmittal).
"From a list" is folded into the By-tracking grid, not a separate tab. The grid
now holds two boring row kinds (one row ↔ 0-or-1 file):
- file rows (workset / placed), edited via setFileIdentity;
- placeholder rows = list rows with no file yet (Load…/Paste rows…/Match names),
edited via the worklist setters; drop or match a file and it becomes a file row.
Dropping N files on a placeholder fans out over consecutive placeholders
(fillFromRow) — the start of the Excel-style block fill. New "⊕ Add filtered
files" pulls every file the left-tree filter shows into the grid. Source / Latest
rev fold in as optional (default-hidden) columns.
Chrome: removed the mode toggle + spreadsheet pane from the template; severed the
spreadsheet/sort/filter/resize/selection inits in app.js (modules stay bundled,
store still drives folder selection + reset); setMode() is now a no-arg enabler;
welcome tutorial rewritten to the single flow.
Tests: bootstrap via app.setMode() (no mode button); mode-switch test asserts the
single surface; worklist test drives placeholder rows in #trackingTree. 69
classify + 56 classifier/tables/cap green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A round-trip workflow: export the file list, edit it in Excel, paste it back —
binding files by exact path or fuzzy name.
Export (left / folder tree, Classify mode):
- A "⬆ Export list" button in the Show toolbar copies the CURRENT filtered file
set (name search + the Show toggles) to TSV — header `path<TAB>file`, files
only, no folder rows. It honours every active filter but ignores expand/collapse
(display-only), so files inside collapsed folders are included. `path` is the
file's root-relative key; `file` is the bare filename. Clipboard copy, with a
.tsv download fallback when the clipboard API is blocked (e.g. file://).
- Reuses the tree's computeVisible() so the export matches exactly what the
filters show.
Paste-back (right / "Paste rows"): the Current name column now accepts either form
- Bare filename → today's behaviour: fuzzy name match / drop later.
- Full path (the exported `path`) → binds that EXACT file directly on paste:
proposeMatches detects currentName === a file's key as a confidence-1 `via:'path'`
match (the strongest signal), folded into the existing auto-assign pass. A path
that matches nothing falls back to its basename for fuzzy matching.
- A pasted full path on a row with NO tracking number yet is still CLAIMED
(recorded in row.bound); when a tracking/rev later lands, restampRow places the
claimed file onto the new leaf. (User choice A.)
Tests: export TSV honours the filter + includes collapsed folders; a full-path
Current name binds the exact file (and leaves others untouched); a path with no
tracking is claimed then placed once tracking is filled. 69 classify green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Instantiating deliverable yamls from existing archive files is an MDL-side
workflow (assigning files to deliverables stays here; registering tracking
numbers belongs with the MDL). It moved to the tables tool's project MDL
rollup in the prior commit, so remove the classifier copy:
- delete classifier/js/mdl-instantiate.js + its build entry
- remove the ⊞ MDL from archive header button + its app.js wiring
- drop the two mdl-instantiate unit tests (the equivalents now live in
tests/tables-mdl.spec.js)
The read-only Catalog button (MDL ∪ archive, drop-to-assign) is unaffected.
Classifier + classify suites: 58 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>