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>
Replace the merged-cell positional table (one column per tracking-number segment,
hierarchy via shared ancestors, built by creating folders) with a plain editable
spreadsheet: one row per file, with the tracking number, the rev (status), and
the title as three separate editable columns. Columns are hideable + resizable.
The storage model is unchanged — a file's tracking identity is still its
placement in the tracking-folder tree. The grid is a flat presentation + inline-
edit layer over it; editing a cell re-materializes the placement via the existing
path (addTrackingPath → place(…,'tracking') → setTitleOverride), generalized to
per-field.
- classify.js: `trackingWorkset` (serialized) so a dropped file is a row before
it has a number; `addToTrackingGrid`/`removeFromTrackingGrid`/`trackingGridKeys`
(union with files that have a tracking placement — incl. ones named via "From a
list"); `setFileIdentity(key, {tracking, rev, title})` re-files + prunes the old
leaf; blank tracking = an unfilled row, blank rev = a PENDING_REV leaf.
- target-tree.js: `renderTrackingGrid` (Status badge · Original name preview ·
Tracking number · Rev (status) · Title · ✕); drag onto the grid adds rows and
auto-fills any file whose own name already parses as ZDDC; a "Columns ▾" chooser
+ drag-resize (resize.js, now parameterized) persisted to localStorage. The
status badge validates the NAME only (the transmittal is a different tab).
Removed the merged-cell machinery + per-node CRUD (+ Root folder, ✎/🗑, brace
expansion) and the now-dead drop-on-node path.
- template/css: tracking toolbar → Columns chooser + hint; flat-grid + chooser CSS.
Tests: replaced the merged-cell/+Root-folder/drop-on-leaf/filename-edit tests with
grid tests (render, drop+auto-fill, per-cell re-file, filter, hide/persist,
preview link). Suite 342 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "From a list" scratch worklist still carried "mdl"/"existing" identifiers
from when it was the MDL/By-existing catalog — misleading now that it's a generic
scratch list and the real MDL is just the archive's mdl/ folder. Mechanical
rename across classify.js, target-tree.js, template.html, layout.css, and the
spec, leaving the genuine MDL concept (readMdlYamls, the mdl/ folder, inMdl,
migrateLegacyMdl) untouched:
- API/state: setMdlList→setWorklist, appendMdlRows→appendWorklist,
clearMdlList→clearWorklist, getMdlList→getWorklist, getMdlRow→getWorklistRow,
state.mdlList→state.worklist.
- DOM/UI: #mdlPanel→#worklistPanel, #mdlTree→#worklistTable,
#existingTab→#worklistTab, #loadMdlBtn→#loadWorklistBtn, the currentTab value
'existing'→'worklist'.
- internals: mdlTable→worklistGrid, mdlPlaced→worklistPlaced,
renderMdlInto→renderWorklist, ensureMdlTable→ensureWorklistGrid,
renderMdlPlaced→renderWorklistFiles, loadMdl→loadWorklist; CSS .mdl-rev__input
+ .fromlist-* → .worklist-*.
No behavior change; classify + classifier suites stay 66 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the "By existing" catalog into a "From a list" scratch worklist that can
be populated three ways and cleared without losing work.
Core model — retire the separate `mdl` assignment axis. Dropping a file on a row
now MATERIALIZES a real "By tracking number" placement (assignFromRow →
addTrackingPath + place(…,'tracking') + title override), reusing the typed-
filename path. Consequences:
- Clearing the list can't lose classifications (no assignment references a row);
dropped files show under By tracking number.
- The two name axes collapse into one (place/deriveTarget/fileCategory simplified).
- Workspace migration: load() materializes any legacy `mdlNodeId` into a tracking
placement BEFORE anything can prune, so saved workspaces keep their work.
- A tracking number's last segment is an ancestor and the revision is always the
leaf, so a blank-revision drop lands on a `pending` placeholder leaf; editing
the row's tracking number or revision re-stamps the row's files onto the new
leaf and prunes the emptied one.
UI:
- One editable Tracking number column (no per-field split) + editable Title — so
you can drop on an entry and bump e.g. the sequence for a new drawing.
- Source column (MDL / arch / pasted) with an amber "new"/"unverified" badge when
a pasted number matches nothing scanned — the typo catcher.
- Toolbar: Load… (now appends) · Paste rows… · ⚡ Match names · Clear list ·
Hide assigned. Tab renamed "From a list"; goal line + hint teach the scratch-pad.
- Paste dialog (Ctrl-V on the panel too) with a live parse preview; parsePastedRows
handles 3-col, a split status column, a pasted full filename, header + bad rows.
- Match names reviews proposeMatches (filename ⊇ a known tracking number) and
assigns the checked pairs.
Tests: materialize+clear, tracking-edit re-stamp + prune, legacy migration,
parsePastedRows, proposeMatches; 66 classify+classifier green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reshape the catalog from a button+overlay into a proper tab and let the user
choose which directories feed it.
- Tabs: the Catalog button becomes a third tab "By existing", grouped with "By
tracking number" (both assign a tracking number) and visually separated from
"By transmittal" (which assigns the path). A brief goal line above the tabs
states the workflow. The overlay #mdlPanel becomes a normal in-flow tab panel.
- Load: instead of auto-scanning a whole project, "Load…" opens a lazy,
multi-select (checkbox) directory tree (new classifier/js/dir-picker.js).
Ticking a directory includes its whole subtree; confirm resolves the topmost
ticked handles. Scope follows where the classifier is served: /_apps/… → all
accessible projects, under <project>/… → that one project, file:// → a picked
folder. The picker is handle-agnostic (HttpDirectoryHandle or native FS).
- Rows: every ticked directory is walked recursively into the union of existing
files (zddc.parseFilename) and MDL deliverables (mdl/*.yaml → inMdl + title),
deduped to one row per tracking number. The "Archive revs" (all) column
becomes a single informational "Latest rev" computed via zddc.compareRevisions.
Drop still assigns the tracking number only; the Revision cell stays blank.
- classify.js is unchanged — the mdl axis model and row shape are reused as-is
(Latest rev derives from the preserved archiveRevisions).
- Tests: the catalog test now asserts latest-rev; new unit tests cover
walkDirInto union/dedupe-to-latest, _latestRevOf draft/modifier ordering,
_detectScope routing, and the dir-picker topmost-ticked resolution. 61 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>
Reframes the By-MDL tab as a "⊞ Catalog" button that opens an overlay over the
target pane (the left filetree stays the drag source). The catalog is the
de-duplicated union of everything the project knows about a tracking number:
its MDL deliverables AND its archive files, merged by tracking number, with an
informational "Archive revs" column (which revisions already exist) and an "MDL"
flag. Nothing is written or altered — the Revision column is classifier-local
and starts blank (never pre-filled from an old archive rev).
- Drag a source file onto a row → assigns the tracking number only (the mdl
axis); set the revision in the bulk-editable Revision column (ctrl-shift
select rows + ctrl-Enter). Per-file Title: MDL/file toggle + ✕ remove kept.
- Columns split the tracking number into the configured pattern fields, each with
its own autofilter (per-column, via the shared seltable). Default pattern is
now the 8-field ORIG-PHASE-PROJECT-AREA-DISC-TYPE-SEQ-SUFFIX.
- Server load merges every party's archive/<party>/mdl/*.yaml with a recursive
walk of the archive documents; local load reads a folder of deliverable yamls.
Test: catalog shows merged archive revisions; drop names a file (tracking only);
bulk revision feeds the derived name.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>