From 6c3c58bc70ad83062883838459cdd5e0e7404738 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Thu, 11 Jun 2026 15:50:16 -0500 Subject: [PATCH] =?UTF-8?q?refactor(classifier):=20drop=20"MDL=20from=20ar?= =?UTF-8?q?chive"=20=E2=80=94=20it=20lives=20in=20the=20tables=20tool=20no?= =?UTF-8?q?w?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- classifier/build.sh | 1 - classifier/js/app.js | 2 - classifier/js/mdl-instantiate.js | 209 ------------------------------- classifier/template.html | 1 - tests/classify.spec.js | 62 --------- 5 files changed, 275 deletions(-) delete mode 100644 classifier/js/mdl-instantiate.js diff --git a/classifier/build.sh b/classifier/build.sh index 1791c2a..cfa7358 100755 --- a/classifier/build.sh +++ b/classifier/build.sh @@ -62,7 +62,6 @@ concat_files \ "js/tree.js" \ "js/target-tree.js" \ "js/copy.js" \ - "js/mdl-instantiate.js" \ "js/spreadsheet.js" \ "js/selection.js" \ "js/preview.js" \ diff --git a/classifier/js/app.js b/classifier/js/app.js index 05c5ca8..f5161f7 100644 --- a/classifier/js/app.js +++ b/classifier/js/app.js @@ -374,8 +374,6 @@ if (app.dom.modeClassifyBtn) app.dom.modeClassifyBtn.addEventListener('click', function () { setMode('classify'); }); if (app.dom.copyOutputBtn) app.dom.copyOutputBtn.addEventListener('click', function () { app.modules.copy.run(); }); if (app.dom.checkDuplicatesBtn) app.dom.checkDuplicatesBtn.addEventListener('click', function () { app.modules.copy.audit(); }); - var mdlBtn = document.getElementById('mdlInstantiateBtn'); - if (mdlBtn) mdlBtn.addEventListener('click', function () { app.modules.mdlInstantiate.open(); }); // Live source-tree filter (matches file path + name; reveals the hierarchy). if (app.dom.treeFilterInput) app.dom.treeFilterInput.addEventListener('input', function () { diff --git a/classifier/js/mdl-instantiate.js b/classifier/js/mdl-instantiate.js deleted file mode 100644 index b00b42a..0000000 --- a/classifier/js/mdl-instantiate.js +++ /dev/null @@ -1,209 +0,0 @@ -/** - * ZDDC Classifier — instantiate MDL deliverables from existing archive files. - * - * Catch-up flow: the archive already holds issued documents, but the Master - * Deliverables List is empty. This reads a project's archive subtree as a flat - * file list, lets the user build a selection set (autofilter + ctrl-shift via - * the shared seltable), dedupes the selected files to one deliverable per - * tracking number, and PUTs a new deliverable .yaml into the originator's - * `archive//mdl/` on the server. Server-only (needs http + auth). - * - * A deliverable .yaml's filename IS its tracking number; the server pins - * `originator` from the folder and composes the filename, so the body carries - * only project/discipline/type/sequence/suffix + title. - */ -(function () { - 'use strict'; - if (!window.app) window.app = {}; - if (!window.app.modules) window.app.modules = {}; - - function T(m, l, o) { if (window.zddc && window.zddc.toast) window.zddc.toast(m, l, o); } - function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; } - - // ── pure core (test seams) ─────────────────────────────────────────────── - - // A tracking number → deliverable {tracking, originator, body{...}} or null - // if it doesn't fit the MDL schema (needs orig-proj-disc-type-seq, + suffix). - function deliverableFromFile(f) { - var segs = String(f.tracking || '').split('-'); - if (segs.length < 5) return null; - var body = { project: segs[1], discipline: segs[2], type: segs[3], sequence: segs[4], title: f.title || '' }; - if (segs.length >= 6) body.suffix = segs.slice(5).join('-'); - return { tracking: f.tracking, originator: segs[0], body: body }; - } - // Dedupe a list of archive files to one deliverable per tracking number. - function dedupe(files) { - var seen = Object.create(null), out = []; - (files || []).forEach(function (f) { - if (seen[f.tracking]) return; - var d = deliverableFromFile(f); - if (d) { seen[f.tracking] = true; out.push(d); } - }); - return out; - } - - // Recursively walk an archive directory handle → flat list of ZDDC-named - // files (skips dot/underscore folders; non-ZDDC names like the mdl yamls - // naturally fall out because parseFilename rejects them). - async function walkArchive(rootHandle) { - var out = []; - async function walk(dirH, parts) { - for await (var entry of dirH.values()) { - var nm = String(entry.name || '').replace(/\/$/, ''); - if (entry.kind === 'directory') { - var c = nm.charAt(0); - if (c === '.' || c === '_' || nm === 'mdl' || nm === 'rsk') continue; - var childH = await dirH.getDirectoryHandle(nm); - await walk(childH, parts.concat(nm)); - } else { - var p = window.zddc.parseFilename(nm); - if (p && p.valid) { - out.push({ - id: parts.concat(nm).join('/'), - party: parts[0] || '', slot: parts[1] || '', transmittal: parts[2] || '', - name: nm, tracking: p.trackingNumber, revision: p.revision, status: p.status, title: p.title, - }); - } - } - } - } - await walk(rootHandle, []); - return out; - } - - // Write one deliverable into //mdl/.yaml. - // Returns 'created' | 'skipped' (already present). Throws on server error. - async function instantiateOne(archiveRoot, d) { - var dir = await archiveRoot.getDirectoryHandle(d.originator, { create: true }); - dir = await dir.getDirectoryHandle('mdl', { create: true }); - var fname = d.tracking + '.yaml'; - try { await dir.getFileHandle(fname); return 'skipped'; } catch (e) { /* NotFound → create */ } - var yaml = window.jsyaml.dump(d.body); - var fh = await dir.getFileHandle(fname, { create: true }); - var w = await fh.createWritable(); - await w.write(new Blob([yaml], { type: 'application/yaml' })); - await w.close(); - return 'created'; - } - - async function instantiateAll(archiveRoot, deliverables, onProgress) { - var s = { created: 0, skipped: 0, errors: 0 }; - for (var i = 0; i < deliverables.length; i++) { - if (onProgress) onProgress(i + 1, deliverables.length, deliverables[i].tracking); - try { s[await instantiateOne(archiveRoot, deliverables[i])]++; } - catch (e) { s.errors++; T('Failed to create ' + deliverables[i].tracking + ' — ' + (e.message || e), 'error'); } - } - return s; - } - - // ── UI ─────────────────────────────────────────────────────────────────── - var overlay = null, statusEl = null, table = null, files = [], archiveRoot = null; - - function close() { if (overlay) { overlay.remove(); overlay = null; table = null; } } - function setStatus(t) { if (statusEl) statusEl.textContent = t; } - - async function open() { - var copy = window.app.modules.copy; - var src = window.zddc && window.zddc.source; - if (!src || location.protocol === 'file:') { - T('Populating the MDL from the archive needs the classifier served by a zddc-server (open it over http).', 'error'); - return; - } - var projects = await copy.fetchAccessProjects(); - if (projects == null) { T('Could not load your projects from the server.', 'error'); return; } - if (!projects.length) { T('No projects you can access on this server.', 'warning'); return; } - var proj = await copy.chooseProject(projects); - if (!proj) return; - buildOverlay(proj); - await scan(proj); - } - - function buildOverlay(proj) { - close(); - overlay = el('div', 'mdl-overlay'); - var box = el('div', 'mdl-overlay__box'); - var head = el('div', 'mdl-overlay__head'); - head.appendChild(el('h2', null, 'Populate MDL from archive — ' + (proj.title || proj.name))); - var x = el('button', 'mdl-overlay__close', '×'); x.title = 'Close'; x.addEventListener('click', close); - head.appendChild(x); - box.appendChild(head); - statusEl = el('div', 'mdl-overlay__status', 'Scanning archive…'); - box.appendChild(statusEl); - var host = el('div', 'mdl-overlay__table'); - box.appendChild(host); - var foot = el('div', 'mdl-overlay__foot'); - var create = el('button', 'btn btn-primary', 'Create deliverables'); - create.addEventListener('click', function () { runCreate(create); }); - foot.appendChild(create); - var cancel = el('button', 'btn btn-secondary', 'Close'); cancel.addEventListener('click', close); - foot.appendChild(cancel); - box.appendChild(foot); - overlay.appendChild(box); - document.body.appendChild(overlay); - - table = window.app.modules.seltable.create({ - container: host, - filterPlaceholder: 'Filter by party, transmittal, tracking number, title…', - rows: function () { return files; }, - rowId: function (r) { return r.id; }, - columns: [ - { key: 'party', title: 'Party' }, - { key: 'slot', title: 'Slot' }, - { key: 'transmittal', title: 'Transmittal' }, - { key: 'tracking', title: 'Tracking number' }, - { key: 'revision', title: 'Rev', get: function (r) { return r.revision + (r.status ? ' (' + r.status + ')' : ''); } }, - { key: 'title', title: 'Title' }, - ], - onSelectionChange: function (ids) { create.textContent = ids.length ? ('Create deliverables (' + dedupe(selectedFiles(ids)).length + ')') : 'Create deliverables'; }, - }); - table.render(); - } - - function selectedFiles(ids) { - var set = {}; ids.forEach(function (i) { set[i] = true; }); - return files.filter(function (f) { return set[f.id]; }); - } - - async function scan(proj) { - var src = window.zddc.source; - var rel = (proj.url || ('/' + proj.name + '/')); - if (rel.charAt(rel.length - 1) !== '/') rel += '/'; - try { - archiveRoot = new src.HttpDirectoryHandle(new URL(rel + 'archive/', location.origin).href, 'archive'); - setStatus('Scanning archive…'); - files = await walkArchive(archiveRoot); - table.renderBody(); - setStatus(files.length + ' document file' + (files.length === 1 ? '' : 's') + ' found. Filter + ctrl-shift select, then “Create deliverables”.'); - } catch (e) { - setStatus('Archive scan failed — ' + (e.message || e)); - T('Archive scan failed — ' + (e.message || e), 'error'); - } - } - - async function runCreate(btn) { - if (!table) return; - var sel = table.getSelection(); - if (!sel.length) { T('Select some archive files first (filter + ctrl-shift).', 'warning'); return; } - var deliverables = dedupe(selectedFiles(sel)); - if (!deliverables.length) { T('None of the selected files have a tracking number that fits the deliverable schema.', 'warning'); return; } - if (!confirm('Create ' + deliverables.length + ' deliverable(s) in the project MDL?\n\n' - + 'One .yaml per tracking number, in archive//mdl/. Already-present ones are skipped.')) return; - btn.disabled = true; - var s = await instantiateAll(archiveRoot, deliverables, function (i, n, tn) { setStatus('Creating ' + i + '/' + n + ' — ' + tn); }); - btn.disabled = false; - setStatus(s.created + ' created, ' + s.skipped + ' already there' - + (s.errors ? (', ' + s.errors + ' failed') : '') + '. ' + files.length + ' files scanned.'); - T('MDL: ' + s.created + ' created, ' + s.skipped + ' already there' - + (s.errors ? (', ' + s.errors + ' failed') : '') + '.', s.errors ? 'warning' : 'success'); - } - - window.app.modules.mdlInstantiate = { - open: open, - // test seams - deliverableFromFile: deliverableFromFile, - dedupe: dedupe, - walkArchive: walkArchive, - instantiateOne: instantiateOne, - instantiateAll: instantiateAll, - }; -})(); diff --git a/classifier/template.html b/classifier/template.html index 5f6f08e..76131e3 100644 --- a/classifier/template.html +++ b/classifier/template.html @@ -34,7 +34,6 @@ - diff --git a/tests/classify.spec.js b/tests/classify.spec.js index 688dad5..08d237d 100644 --- a/tests/classify.spec.js +++ b/tests/classify.spec.js @@ -1216,68 +1216,6 @@ test('seltable: autofilter + ctrl-shift selection builds complex sets', async ({ expect(r.ctrlShiftRange).toBe('c,d'); // ctrl-shift range runs over the FILTERED order }); -test('mdl-instantiate: walks the archive subtree and dedupes to one deliverable per tracking number', async ({ page }) => { - const r = await page.evaluate(async () => { - const M = window.app.modules.mdlInstantiate; - const file = (name) => ({ kind: 'file', name }); - const dir = (name, dirs, files) => ({ - kind: 'directory', name, - getDirectoryHandle: async (n) => dirs[n], - values: async function* () { for (const d of Object.values(dirs)) yield d; for (const f of files) yield f; }, - }); - const T1 = dir('T1', {}, [ - file('ACM-PRJ-EL-SPC-0001_A (IFR) - Spec.pdf'), - file('ACM-PRJ-EL-SPC-0001_B (IFC) - Spec.pdf'), // 2nd revision of same deliverable - file('ACM-PRJ-ME-DWG-0003_0 (IFC) - Plan.pdf'), - file('notes.txt'), // non-ZDDC → ignored - ]); - const issued = dir('issued', { T1 }, []); - const mdl = dir('mdl', {}, [file('ACM-PRJ-EL-SPC-0001.yaml')]); // mdl/ skipped - const root = dir('archive', { ACM: dir('ACM', { issued, mdl }, []) }, []); - const files = await M.walkArchive(root); - const dd = M.dedupe(files); - const spc = dd.find((d) => d.tracking === 'ACM-PRJ-EL-SPC-0001'); - return { - count: files.length, party: files[0].party, slot: files[0].slot, transmittal: files[0].transmittal, - deliverables: dd.map((d) => d.tracking).sort(), - originator: spc.originator, body: spc.body, hasOriginator: 'originator' in spc.body, - }; - }); - expect(r.count).toBe(3); // 2 SPC revisions + 1 DWG (txt + the mdl yaml ignored) - expect(r.party).toBe('ACM'); - expect(r.slot).toBe('issued'); - expect(r.transmittal).toBe('T1'); - expect(r.deliverables).toEqual(['ACM-PRJ-EL-SPC-0001', 'ACM-PRJ-ME-DWG-0003']); // deduped by tracking number - expect(r.originator).toBe('ACM'); - expect(r.body).toEqual({ project: 'PRJ', discipline: 'EL', type: 'SPC', sequence: '0001', title: 'Spec' }); - expect(r.hasOriginator).toBe(false); // server pins originator from the folder -}); - -test('mdl-instantiate: writes the deliverable yaml then skips an existing one', async ({ page }) => { - const r = await page.evaluate(async () => { - const M = window.app.modules.mdlInstantiate; - const store = {}; - const mkdir = (base) => ({ - getDirectoryHandle: async (n) => mkdir(base + n + '/'), - getFileHandle: async (n, opts) => { - const full = base + n; - if ((!opts || !opts.create) && !(full in store)) { const e = new Error('NF'); e.name = 'NotFoundError'; throw e; } - return { createWritable: async () => ({ write: async (b) => { store[full] = await b.text(); }, close: async () => {} }) }; - }, - }); - const d = { tracking: 'ACM-PRJ-EL-SPC-0001', originator: 'ACM', body: { project: 'PRJ', discipline: 'EL', type: 'SPC', sequence: '0001', title: 'Spec' } }; - const first = await M.instantiateOne(mkdir(''), d); - const root = mkdir(''); // fresh facade over the same store - const second = await M.instantiateOne(root, d); - const path = Object.keys(store)[0]; - return { first, second, path, parsed: window.jsyaml.load(store[path]) }; - }); - expect(r.first).toBe('created'); - expect(r.second).toBe('skipped'); // already present → resumable - expect(r.path).toBe('ACM/mdl/ACM-PRJ-EL-SPC-0001.yaml'); - expect(r.parsed).toEqual({ project: 'PRJ', discipline: 'EL', type: 'SPC', sequence: '0001', title: 'Spec' }); -}); - test('classify: an MDL placement names a file; revision from the cell, transmittal for the path', async ({ page }) => { await page.click('#modeClassifyBtn'); const r = await page.evaluate(() => {