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>
This commit is contained in:
parent
95c9e42270
commit
6c3c58bc70
5 changed files with 0 additions and 275 deletions
|
|
@ -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" \
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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/<originator>/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 <archiveRoot>/<originator>/mdl/<tracking>.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/<originator>/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,
|
||||
};
|
||||
})();
|
||||
|
|
@ -34,7 +34,6 @@
|
|||
<button id="modeClassifyBtn" class="mode-btn active" title="Map files onto tracking numbers and transmittals, then copy renamed copies to an output directory — the source is never modified">Classify & copy</button>
|
||||
<button id="modeRenameBtn" class="mode-btn" title="Edit a spreadsheet and rename the files in place (edits the source)">Rename in place</button>
|
||||
</div>
|
||||
<button id="mdlInstantiateBtn" class="btn btn-secondary btn-sm" title="Populate a project's Master Deliverables List from its existing archive files (server)">⊞ MDL from archive</button>
|
||||
<button id="workspacesBtn" class="btn btn-secondary btn-sm" title="Workspaces — open or create a classification project">≡ Workspaces</button>
|
||||
<button id="connectDirBtn" class="btn btn-primary btn-sm" title="Connect this workspace's source directory to preview, copy, or finish scanning" hidden>⮷ Connect directory</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue