Commit graph

14 commits

Author SHA1 Message Date
9851cc4463 feat(classifier): switch dataset export/import to a filename-per-file format
Replaces the ID-based dataset export/import (which required an external editor
to build a nested tree and keep node ids consistent) with a flat, AI-friendly
list: one record per input file carrying its full ZDDC filename — and an
optional transmittal {party, slot, date, type, seq, status, title}.

- Export: one {source, originalName, filename, excluded, transmittal?} record
  per source file (filename = the derived ZDDC name, "" if unassigned).
- Import: parses each filename and rebuilds the tracking tree (parseFolderLevels
  + addTrackingPath, sharing ancestors); excluded files are marked; transmittals
  are reconstructed with party/bin dedup. No node ids for the editor to manage.

New classify helpers: transmittalRecord (export), findOrAddParty /
findOrAddTransmittalBin (import dedup). serialize/load stay for workspace
persistence. Test rewritten for the filename round-trip (classify.spec.js -> 34).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:52:44 -05:00
4425a599f0 feat(classifier): export/import the classification dataset as JSON
Adds Export / Import buttons to the Classify & Copy header so the full dataset
(tracking + transmittal trees, per-file assignments, output name) round-trips
through a JSON file — export it, edit externally (e.g. with an AI), re-import.

- Export downloads a self-documenting JSON (canonical classify.serialize() state
  + an informational sourceFiles inventory + a _format note). Lossless: empty
  tree branches and unassigned state survive.
- Import validates, confirms before replacing a non-empty current dataset, and
  loads via classify.load() (ignores the wrapper/_format/sourceFiles keys).

Test: serialize → JSON → load preserves trees (incl. an empty branch) +
assignments (classify.spec.js -> 34 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:18:34 -05:00
139171481e feat(classifier): three-state filters, expand/collapse-all, drop-prompt, preview + editable filenames
Classify & Copy interaction pass (replaces the single "Hide Assigned" toggle):

- Source-tree filters: three "Show Unassigned / Show Assigned / Show Excluded"
  checkboxes (classify mode only) with live per-tab counts; "Hide Compliant" is
  now rename-mode only. Folders with nothing visible collapse out.
- Target tree: ctrl/cmd-click a toggle to expand/collapse the whole subtree.
- Tracking drop-to-any-level: dropping on a node that isn't already a complete
  leaf prompts for the remaining levels (e.g. "0001_0 (IFU)"), which are parsed
  and nested under the drop target. Dropping on a finished leaf assigns directly.
- Placed-file rows: click to preview; the derived filename is now an inline
  input — edit it (full "TRACKING_REV (STATUS) - Title.ext") and the item is
  re-filed onto the parsed tracking path (created if needed) + title override.

New classify helpers: trackingNodeComplete, trackingPathLabel. tree.setShowFilters
replaces setHideAssigned. Tests updated/added (classify.spec.js -> 33 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:08:30 -05:00
055f4cf4e0 fix(classifier): parse add-folder names into nested levels; controls back to right/hover
Follow-up to the Classify & Copy add-folder work:

- Add-folder now parses each (brace-expanded) name into the nested tracking
  levels it represents — split on "-", then the FINAL "_" splits the leaf
  revision. "CPO-0001_0 (IFU)" → CPO / 0001 / 0 (IFU); a braced pattern nests
  every expansion and shares common ancestors. New classify.parseFolderLevels
  + addTrackingPath (ensure-path with name reuse).
- Node add/edit/delete controls moved back to the RIGHT of the level name and
  revealed on hover (was left + always-visible).

Tests: parseFolderLevels cases + a nested-chain/shared-ancestor test; updated
the "+ Root folder" test for the new nesting (classify.spec.js -> 31 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:58:42 -05:00
8f839fc0c9 feat(classifier): Hide Assigned filter, left-aligned node controls, brace-expand add
Classify & Copy polish — in either target tab the goal is to assign or exclude
every left-pane file until nothing remains:

- Hide Assigned checkbox (classify mode, in the folder-tree pane header):
  collapses the source tree to only what's left on the ACTIVE axis — hides
  files already assigned in the current tab (or excluded) and any folder whose
  scanned subtree is thereby empty. Re-renders on tab switch; target-tree
  exposes activeAxis().
- Node add/edit/delete controls moved to the LEFT of the level name and made
  always-visible (was right-aligned + hover-only), so building/pruning the
  tracking and transmittal trees is one click.
- Brace expansion in the add-folder box: "BMB-187023-{PM,EL,EM}-MOM-
  {0001-0002,0005}_A (IFR)" creates all 9 folders — {a,b} alternation +
  {N-M} zero-padded numeric ranges, cartesian product across groups; a
  multi-create is confirmed first. New classify.expandFolderPattern().

Tests: expandFolderPattern unit cases + a Hide-Assigned DOM test
(classify.spec.js → 29 passed; classifier.spec.js → 4 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:31:14 -05:00
01b01f8f7a feat(classifier): welcome rewrite + resumable scan + reconnect on restore
- Welcome: drop the 'absorbed into Browse' notice; bigger, inviting intro with
  a two-method tutorial (Classify & copy — recommended/non-destructive; Rename
  in place — edits files) and a OneDrive 'keep on device' tip.
- Resumable scan: the snapshot now records per-folder scan state, the workspace
  record is created up front, and the partial snapshot is persisted every 5s
  during the (slow) scan. scanner.resumeScan() resolves handles for only the
  still-pending folders and drains them — so an interrupted scan picks up where
  it left off instead of starting over.
- Reconnect on restore: opening a workspace no longer assumes the source is
  connected; a header 'Connect directory' button (and a prompt) re-grants the
  persisted handle in one click or lets you re-pick it. Until connected you can
  still edit the data model; connecting also resumes any pending scan.
- Tests: resume-scan via mock root handle (31 classify/classifier green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:38:08 -05:00
975c804cc7 feat(classifier): click-to-preview in Classify & Copy mode
Identifying a file is half the workflow — you preview it to see what it is,
then assign its tracking number by drag. Preview was only wired into the old
Rename grid; in Classify & Copy a source file now previews on single-click
(drag still assigns, right-click excludes). preview.previewFile() resolves a
snapshot file's handle from the workspace root (one-click read re-grant) before
opening, so it works for resumed workspaces too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:08:57 -05:00
eb07e7622d fix(classifier): file-only folders are expandable in Classify mode
A folder with files but no subfolders got no expand toggle, so in Classify &
Copy mode its files (the drag source) could never be revealed — and leaf
folders full of files are exactly where the work is. Make a folder expandable
when it has files in classify mode; expanding lists the draggable file rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:23:26 -05:00
1d09abdc8b feat(classifier): workspaces — scan-once, resume from snapshot (phase 6)
The classifier re-scanned the source on every session; on cloud-backed mounts
(OneDrive/Samba) that's minutes of per-op latency. Workspaces fix it: scan a
folder ONCE, snapshot the completed tree, and resume instantly — all
classification runs on the data model; the filesystem is only touched at copy.

- persist.js v2: multi-workspace IndexedDB (tiny 'index' store for the welcome
  list + 'data' store holding the source handle, tree snapshot, and map). DB v2.
- scanner.js: snapshotTree()/loadSnapshot() (compact, handle-less, marked done,
  totals recomputed) + lazy resolveFileHandle/resolveDirHandle from the root.
- workspace.js: welcome manager (new/open/rename/delete), debounced autosave of
  the active workspace, 'Refresh from disk' (re-scan → re-snapshot, path-keyed
  map carries over). New workspace = the one slow full scan; reopen = instant.
- copy.js: resolves snapshot files' handles from the workspace root with a
  one-click read permission re-grant; missing-on-disk files surface as errors.
- app.js: enterAppShell() shared by rename/workspace flows; exposes setMode;
  classify.js decoupled from persistence.
- template/css: welcome workspace list + header 'Workspaces' button.
- tests: snapshot round-trip, persist CRUD + classify-only-preserves-tree,
  copy-from-snapshot via mock root handle (28 classify/classifier tests green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:07:40 -05:00
420f735e89 feat(classifier): copy-out with duplicate detection + map restore (phase 5)
The Copy button (enabled once >=1 file is fully classified) copies the mapped
files into a user-chosen output directory under their canonical names/layout
<party>/{received,issued}/<transmittal>/<filename> — reading the source, never
writing it.

- copy.js: plan() (complete, non-excluded files) → conflict scan (two sources
  → same output path are reported + skipped) → copyTo() engine on the generic
  FS-Access shape (ensureDir + getFileHandle + createWritable). Per-file dedup:
  identical target (sha256) is skipped; existing-but-different is left
  untouched and reported; live footer progress; completion toast.
- app.js: restores the saved map on launch (keyed by source-relative path, so
  it re-attaches when the same directory is re-opened) and persists the source
  handle on open; Copy button wired.
- target-tree.js: enables/labels the Copy button from the done count.
- 2 copy-engine tests with mock FS handles (copy/skip/differ + conflict);
  24 classify+classifier tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:37:44 -05:00
eb1e3ec948 feat(classifier): left-tree markers, exclude, cross-tree find (phase 4)
- Each source file row shows a classification state dot (unassigned →
  has-tracking/transmittal → done), and each folder shows an aggregate dot
  over its subtree.
- Right-click a file or folder to Exclude/Include from the copy (folder applies
  to its whole subtree) or clear an axis; excluded files are struck through and
  never copied.
- Cross-tree find is bidirectional: click a placed file in the target pane to
  reveal+flash it in the source tree (expanding its folders); click a source
  file to switch the target pane to its placed axis and flash the node.
- Target pane now reverse-looks-up over ALL scanned files (the left tree), not
  the selection-scoped grid, with placements grouped in one pass per render.
- classify.getAssignment() read-only accessor; 5 new tests (18 total green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:32:42 -05:00
47cf58b0e9 feat(classifier): drag-and-drop assignment (phase 3)
In Classify & Copy mode the left tree now lists each folder's files as
draggable rows (with a classification state dot), and folder rows are
draggable for a group-drag of the whole subtree. Target-tree nodes are drop
zones: a tracking folder (any node) or a transmittal bin; dropping assigns the
dragged source key(s) along that axis via classify.place().

- dnd.js: drag-payload bus (keys held in a module var since dataTransfer can't
  be read during dragover; carries a marker for the copy cursor).
- tree.js: createFileElement + group-drag dragstart; classify-mode file rows.
- target-tree.js: setupDropZone with dragover highlight + drop assignment
  (tracking = any node, transmittal = bins only).
- app.js: source tree re-renders on classify state change.
- 2 DnD drop-handler tests (14 total green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:23:38 -05:00
a8403d1f73 feat(classifier): mode toggle + dual-pane target trees (phase 2)
Header gets a Rename / Classify & Copy switch. In Classify & Copy mode the
spreadsheet pane is replaced by a tabbed target pane (By tracking number /
By transmittal), while the source tree stays on the left.

- target-tree.js: renders both trees from classify state; tracking-folder
  create/rename/delete (leaf folders styled as the revision); party CRUD +
  per-slot inline transmittal-bin form (date + TRN/SUB + seq + optional
  status/title); shows the derived filename + a validation badge for each
  placed file; live header stats (done / in progress / unassigned / excluded).
- app.js setMode(): swaps panes, toggles classify mode, re-renders both trees.
- 3 UI smoke tests added to classify.spec.js (12 total green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:19:35 -05:00
a8f116734d feat(classifier): Classify & Copy state model + persistence (phase 1)
Foundation for the non-destructive map+copy workflow: source stays read-only,
files are mapped onto two orthogonal target trees, a later step copies renamed
copies to a separate output dir.

- classify.js: the single source of truth. assignments map keyed by
  source-relative path (survives re-pick); tracking tree (positional: ancestors
  joined '-' = tracking number, immediate parent 'REV (STATUS)' leaf = rev+status,
  title from original name) and transmittal tree (<party>/{received,issued}/<bin>).
  deriveTarget() computes filename + output path + validation purely; pub/sub +
  debounced autosave; node CRUD with dangling-placement cleanup.
- persist.js: IndexedDB store of the serialized map + the source
  FileSystemDirectoryHandle, with queryPermission/requestPermission re-grant on
  reload and a re-pick fallback.
- tests/classify.spec.js: 9 in-page unit tests for the derive/assignment logic
  (no FS Access needed) — tracking join, leaf REV (STATUS) parse incl. invalid
  status, title derivation/override, transmittal path composition, exclude,
  cascade delete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:11:04 -05:00