Commit graph

27 commits

Author SHA1 Message Date
bfa94d0da8 chore(embedded): cut v0.0.27-beta 2026-06-15 23:53:25 -05:00
2b32aced6d feat(classifier): By Tracking Number is now a flat editable grid (one row per file)
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>
2026-06-15 16:51:39 -05:00
e58347d476 fix(classifier): By-tracking columns no longer reserve width for hover-only buttons
Each merged-cell column was sized to fit the per-node action buttons (+ ✎ 🗑),
even though they're invisible until you hover the cell. The buttons sat in flow
inside the sticky .tcell__inner. Float them absolutely over the right of the cell
instead (revealed on hover, pointer-events gated), so a column now sizes to its
value alone. Classifier suites 69 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 15:19:05 -05:00
c718334d25 fix(browse,classifier): backdrop dismiss no longer fires on a drag out of an input
Selecting text in a dialog input by click-dragging and releasing the mouse
outside the dialog closed it: the browser fires a `click` whose target is the
backdrop (mousedown was inside, mouseup outside), and the dismiss handler keyed
solely on `e.target === backdrop`.

Guard every backdrop click-to-close with a mousedown flag — close only when the
press ALSO started on the backdrop (a genuine backdrop click), not a drag that
began inside the dialog. Applied to the browse New file/folder party picker (the
reported case) and the other browse create dialogs (create/accept-transmittal,
stage), plus the classifier dialogs that share the pattern (copy chooser,
dir-picker, and the paste/match modal — whose textarea is a prime drag target).
The conflict/history dialogs already used mousedown and were unaffected.

Build + browse/classify/classifier suites green (80).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:36:07 -05:00
2989e6e847 ux(classifier): stack the folder count below the folder name
The "direct+total folders/files" badge was right-aligned on the folder row;
move it under the name. The name + count now live in a vertical .folder-namebox
(flex column, flex:1), and the row aligns items to the top so the toggle/icon
sit on the name line with the count beneath. Dropped the count's right margin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:09:46 -05:00
4830cec2f8 chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 5s
2026-06-15 09:58:00 -05:00
8473ed3393 feat(classifier): Folder Tree count badge reflects the active filters
The per-folder "direct+total folders / files" badge always showed the raw
scanned totals, even while a filter narrowed the tree — so "9+1799 folders,
0+6203 files" stayed put no matter what you filtered to.

computeVisible already does a single filtered pass (applying the Show checkboxes
via classifyAllows and the autofilter via nameHit); accumulate per-folder visible
direct/total counts there and have populateCount use them whenever a filter is
active (raw totals otherwise). So the badge now shows the post-filter totals for
both the autofilter and the Show checkboxes — a collapsed folder's badge tells
you how many matching items are inside.

Test: a filtered tree's root badge drops from "2 folders, 0+3 files" to
"1 folder, 0+1 file". Classifier suites 69 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:47:11 -05:00
7c158be73b chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 3s
2026-06-15 09:32:09 -05:00
0d8125a331 fix(classifier): the Folder Tree autofilter no longer auto-expands
The name filter (and the search-reveal path logic) used to force-expand every
folder on the way to a match, reshaping the tree the moment you typed. If you'd
collapsed everything except the one subtree you cared about, filtering blew the
rest open.

Now the filter only shows/hides rows IN PLACE: expansion is driven solely by the
user's folder.expanded. A collapsed folder that contains matches is still shown
(so you can open it) but stays collapsed — the filter never changes expand state.
Removed the autoOpen() force-expand and the `open` path-map it read.

Tests updated to the new contract: filter hides non-matches in place within
expanded folders, shows match-containing folders collapsed without revealing
their files, and leaves expand state untouched. Classifier suites 68 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:29:45 -05:00
60678e552d chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 4s
2026-06-15 08:55:43 -05:00
0847c7a844 feat(classifier): paste a "current name" column → match files by name
Add a 4th paste column and use it to assign tracking numbers reliably. The old
auto-matcher keyed on "the tracking number appears in the filename" — backwards
for unorganized files that don't yet carry their number. The pasted current name
is an authoritative join key the user already has.

- parsePastedRows: FIXED schema by position — tracking_number · rev (status) ·
  title · current name. Dropped the variant guessing (status-column merge,
  single-filename split); a header row is still skipped, trailing columns may be
  omitted. Rows carry currentName.
- proposeMatches: PRIMARY signal is now the current name (nameScore: exact on the
  normalized, extension-dropped key = conf 1; token-coverage 0.6–0.95; clean
  substring 0.7), FALLBACK is the old tracking-in-filename heuristic for rows
  without a current name. Each proposal carries `auto` — true only for an exact
  1:1 match (unique conf-1 for both its file and its row), the only kind safe to
  assign unprompted. Duplicate names → not auto.
- Paste dialog: 4th preview column; on Add, exact 1:1 matches are auto-assigned
  and a summary toast points to Match names for the rest. Match dialog pre-checks
  only the exact matches, shows confidence + name/tracking#, flags the review-only
  ones. New read-only "Current name" column in the worklist table.

Tests: fixed-schema parse, current-name exact(auto)+token match, ambiguous
duplicate (not auto), and the tracking fallback still holds. Suite 342 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:52:48 -05:00
36fe38b235 refactor(classifier): rename the misleading mdl* internals to worklist* (#8)
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>
2026-06-15 07:57:07 -05:00
605f4ab3e0 chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 4s
2026-06-13 12:17:16 -05:00
51f5947716 refactor(classifier): dedupe seltable CSS + tidy From-a-list scratch edges
- #7 seltable CSS: the classifier carried its own copy of the base seltable
  rules (diverged from shared/seltable.css — a stale sticky offset + a dead
  .seltable__filter). Bundle shared/seltable.css and keep only the
  classifier-specific catalog bits (.seltable__extra, .mdl-rev__input,
  .fromlist-*, .src-badge, #mdlTree). One source of truth, shared with tables.
- #9 Clear list now confirms when it would strand files that still need a
  revision (on a "pending" leaf) — they stay assigned under By tracking number,
  but the row to finish them here is going away, so warn first.
- #10 serialize() strips the transient row→keys hint (`placed`) — it's rebuilt
  as drops happen and was needlessly bloating every workspace autosave.

(#8, renaming the internal mdl* identifiers to match "From a list", is left as
deliberate churn-avoidance — purely cosmetic, spans template/css/js/tests/DOM
ids, and the user-facing strings already read "From a list".)

Full suite 340 passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:16:58 -05:00
921713d0a4 chore: clear tech debt — green the suite + delete dead code
The full Playwright suite had 5 pre-existing failures (stale assertions for
since-reworked behavior) and the classifier carried dead code from removed flows.

Stale tests refreshed to current behavior:
- toast.spec (×3): toast.js now STACKS (sticky/dismissible) rather than showing
  one at a time — assert stacking + the "Clear all" control, read the message
  from .zddc-toast__msg (the toast also holds a × button), and dismiss via the ×
  (clicking the body no longer dismisses, by design).
- browse.spec: "New folder/New file" moved from the toolbar into the context
  menu — drop the #newFolderBtn/#newFileBtn assertions (Sort + Show-hidden stay).
- tokens.spec XSS guard: rewritten to the current apiActions modal flow
  (#api-create-btn → .api-modal → #table-root) instead of the long-gone inline
  #desc form. The escaping assertion now actually runs and confirms it holds.

Dead code removed:
- classifier .mdl-overlay* CSS (orphaned when the "MDL from archive" instantiate
  flow moved to the tables tool).
- classify.js filesInNode() — defined + exported but called nowhere.
- "From a list" naming: refreshed a stale "catalog" comment and renamed the 3
  remaining "By existing:" test titles.

Full suite now 340 passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:02:51 -05:00
7c0b66590c feat(server,shared): tell denied users who can — subtly, before wasted effort
When a user lacks permission, the app should (a) not let them do data entry it
will reject and (b) subtly say who can. General mechanism + the key gates.

Server — compute & expose "who can <verb> here":
- zddc.WhoCan(chain, verb) → Authority{Roles, People}: the acl.permissions
  grantees holding the verb across the cascade (roles + their members) plus the
  admins (who bypass). New whocan.go + whocan_test.go.
- AccessView gains path_who_can (profilehandler.go), populated only for verbs the
  caller LACKS and only when they can read the path (mirrors .zddc readability),
  so one cap.at() answers "can I?" and "if not, who?".
- writeForbiddenWho enriches the 403 body with who_can for the missing verb
  (errors.go); authorizeAction uses it (fileapi.go) as the safety net for denials
  that weren't pre-checked.

Shared — shared/cap.js:
- cap.whoCan(view, verb) + cap.denyHint(view, verb) → {text, title}, role-first
  ("Only the document controller can create here") with the people in the tooltip.
- handleForbidden appends the hint (from the 403 body, else the cached view), so
  every tool that already routes 403s through it (form save, tables save, browse)
  now explains who can — for free.

Key gates:
- Browse party-create (the reported bug): pre-check create authority on ssr/ and
  the slot BEFORE opening the picker — if the user can do neither, show the hint
  instead of the form; if only existing parties are usable, disable "+ New party"
  with the who-can hint. The post-hoc 403 catch now names who can too.
- Tables +Add row disabled state shows the who-can hint.

Plus: subtle /_apps/{browse,archive,classifier}.html links in the landing footer.

Tests: Go WhoCan unit test (role/person split, admin bypass, dedupe); cap.spec.js
(denyHint role-first/people/fallback, whoCan, handleForbidden enrichment) — 5
green; Go handler+zddc+policy suites green. (Pre-existing stale browse toolbar
test browse.spec.js:274 unaffected.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:58:20 -05:00
3d553ce9d4 chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 2s
2026-06-12 11:07:37 -05:00
f66b9c5d55 feat(classifier): "From a list" — a scratch worklist that materializes tracking placements
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>
2026-06-12 10:48:26 -05:00
cfdf0f6db9 fix(classifier): fit-to-content catalog columns + sorted Folder Tree
Two UI fixes:

- "By existing" catalog columns were far too wide. The seltable forced the
  table to width:100% (auto-layout then stretches columns) and — in the
  classifier's copy — the per-column filter <input>s had no styling, so each
  fell back to its ~170px intrinsic width and dictated the column width. Set
  the table to width:auto (cells are already nowrap → fit header/longest cell)
  and style .seltable__colfilter to fill its column (min-width:2rem,
  box-sizing:border-box) so the inputs never widen a column. Applied to both
  the classifier copy and shared/seltable.css (same fix for the tables tool's
  "Add from archive" table).
- The left Folder Tree rendered folders and files in raw scan order. Sort both
  at render — case-insensitive, natural (so "Rev 2" precedes "Rev 10") — via a
  non-mutating slice().sort() at each render point in tree.js.

Tests: a new spec asserts the natural/case-insensitive tree order; 62 classify
+ classifier green (108 across classify/classifier/tables/tables-mdl).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 10:00:23 -05:00
8e10e5e5e6 chore(embedded): cut v0.0.27-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 3s
2026-06-11 21:15:43 -05:00
93ed0d361f feat(classifier): "By existing" tab + multi-select directory picker
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>
2026-06-11 19:39:55 -05:00
6c3c58bc70 refactor(classifier): drop "MDL from archive" — it lives in the tables tool now
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>
2026-06-11 15:50:16 -05:00
95c9e42270 feat(tables): "Add from archive" on the project MDL rollup
The MDL owns the workflow of registering deliverables; this is the
catch-up path for files that already exist in the archive but were never
listed. On the project MDL rollup (<project>/mdl/, addable:false), a new
"+ From archive" toolbar button opens an overlay that walks the project
archive into the shared seltable (per-column autofilter + ctrl-shift
selection), dedupes the selection to one deliverable per tracking number,
and PUTs a deliverable .yaml into each originator's archive/<originator>/
mdl/. Identity fields are split positionally from the tracking number per
the project's own table columns (originator is folder-pinned, so omitted
from the body); the server composes/validates the filename. Existing
deliverables are skipped; created/skipped/failed are reported.

- tables/js/mdl-from-archive.js: walkArchive / dedupe / deliverableFromFile
  / instantiateOne + the overlay UI; setup() shows the button only on an
  /mdl/ rollup over http, gated on archive create permission.
- shared/seltable.css: promoted seltable base styles + per-column filter
  row + the overlay chrome (bundled into tables; classifier keeps its
  inline copy).
- main.js wires setup(ctx); template.html adds the (hidden) button;
  build.sh bundles ../shared/seltable.{js,css} + the new module.
- tests/tables-mdl.spec.js (new project): split/dedupe/walk/instantiate
  against in-page mock FS handles; 7 green. tables suite still 47 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:48:22 -05:00
d4d48cad4a feat(classifier): MDL becomes a read-only catalog (MDL ∪ archive) overlay
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>
2026-06-11 14:58:31 -05:00
93f1eb8d63 refactor(seltable): promote to shared/ with per-column autofilters
Move classifier/js/seltable.js → shared/seltable.js so both the classifier and
the tables tool can use it (the MDL realignment splits work across both). Adds
per-column autofilter inputs (one per column, AND-combined) on top of the
programmatic global filter; selection + select-filtered + ctrl-Enter unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:40:56 -05:00
fd11278417 docs: pre-push PII guard + scrub conventions
main history was rewritten once to scrub a leaked work email. Document a
pre-push email guard (grep with a synthetic-domain allowlist; empty = clean) in
AGENTS.md and reference it from CLAUDE.md, plus the post-scrub conventions: no
real personal/work emails (use @example.com), the only real address allowed is
the maintainer contact caseywitt@proton.me, generic personas (admin/alice/sam),
party name Acme. Never push pre-scrub history or stale tags.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:40:56 -05:00
Me Here
2bc582fd9e ZDDC: document-control tools + zddc-server 2026-06-11 13:32:31 -05:00