From 5fb2c07467629c84aa9cc5e7b402c9441ce18f68 Mon Sep 17 00:00:00 2001 From: Daniel Kats Date: Tue, 1 Sep 2026 14:32:41 +0200 Subject: [PATCH 1/3] Add check-elemco-sync: guard the hand-curated method registry against catalog drift js/elemco-methods.js is deliberately hand-maintained, so it can silently drift from the generated ElemCo.jl catalogs: a registry entry can point at a macro or option group that no longer exists in the pinned version, and new backend macros can stay unreachable from the UI unnoticed (e.g. the pending scf.stability/:search + UNO-CAS work will land as new scf options and macros). scripts/check-elemco-sync.js makes the drift loud: - errors (exit 1): registry macro missing from elemco-macros.js; referenced option group/field missing from elemco-options.js; the two generated catalogs pinned to different ElemCo versions - warnings: catalog macros not surfaced by the registry (curation candidates; documented "Alias for @x" macros count as covered by the original) Wired as `npm run check-elemco-sync` and chained onto both update-elemco-* scripts so every catalog regeneration is checked automatically. Currently passes clean: 26 registry entries against 43 macros / 16 option groups @ v0.16.0. Co-Authored-By: Claude Fable 5 --- package.json | 5 +- scripts/check-elemco-sync.js | 120 +++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 scripts/check-elemco-sync.js diff --git a/package.json b/package.json index dabe983..bb8c9f3 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,9 @@ "build:mac": "electron-builder --mac", "build:linux": "electron-builder --linux", "check-version": "node build-version.js", - "update-elemco-options": "node scripts/parse-elemco-options.js", - "update-elemco-macros": "node scripts/parse-elemco-macros.js", + "update-elemco-options": "node scripts/parse-elemco-options.js && node scripts/check-elemco-sync.js", + "update-elemco-macros": "node scripts/parse-elemco-macros.js && node scripts/check-elemco-sync.js", + "check-elemco-sync": "node scripts/check-elemco-sync.js", "smoke": "electron . --smoke --disable-gpu", "start-debug:win": "node_modules/electron/dist/electron.exe . --enable-logging=file --log-file=jlmol-debug.log --log-level=0" }, diff --git a/scripts/check-elemco-sync.js b/scripts/check-elemco-sync.js new file mode 100644 index 0000000..0a6e311 --- /dev/null +++ b/scripts/check-elemco-sync.js @@ -0,0 +1,120 @@ +#!/usr/bin/env node + +/** + * Cross-check the hand-curated method registry (js/elemco-methods.js) against + * the generated ElemCo.jl catalogs (js/elemco-macros.js, js/elemco-options.js). + * + * The registry is deliberately hand-maintained, so it can silently drift from + * the backend: a registry entry can point at a macro or option group that no + * longer exists in the pinned ElemCo version, and new backend macros can stay + * unreachable from the UI without anyone noticing. This script makes the drift + * loud: + * + * errors (exit 1): + * - a registry macro missing from ELEMCO_MACROS + * - a referenced option group (per-method `groups`, ELEMCO_GLOBAL_GROUPS, + * ELEMCO_GLOBAL_EXCLUDE) missing from ELEMCO_OPTIONS + * - an ELEMCO_GLOBAL_EXCLUDE field missing from its group + * - the two generated catalogs pinned to different ElemCo versions + * + * warnings (informational): + * - catalog macros not covered by any registry entry (curation candidates) + * + * Usage: + * node scripts/check-elemco-sync.js + * npm run check-elemco-sync + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const JS_DIR = path.join(__dirname, '..', 'js'); + +// The catalog files assign to `window.*`; elemco-methods.js declares consts and +// copies them onto `window` when present. Evaluate all three against one shared +// fake window and read everything back from it. +global.window = {}; +for (const f of ['elemco-macros.js', 'elemco-options.js', 'elemco-methods.js']) { + // eslint-disable-next-line no-eval + eval(fs.readFileSync(path.join(JS_DIR, f), 'utf8')); +} +const w = global.window; + +const errors = []; +const warnings = []; + +// ---- catalog version pins must agree ----------------------------------------- +if (w.ELEMCO_MACROS.sourceRef !== w.ELEMCO_OPTIONS.sourceRef) { + errors.push( + `catalog version mismatch: elemco-macros.js @ ${w.ELEMCO_MACROS.sourceRef}, ` + + `elemco-options.js @ ${w.ELEMCO_OPTIONS.sourceRef} — regenerate both with the same --ref` + ); +} + +// ---- every registry macro must exist in the macros catalog ------------------- +const catalogMacros = new Set(w.ELEMCO_MACROS.macros.map((m) => m.name)); +const registryEntries = [ + ...(w.ELEMCO_METHODS.reference || []).map((m) => ({ where: `reference/${m.id}`, macro: m.macro, groups: m.groups })), + ...w.ELEMCO_CORRELATION_GROUPS.flatMap((g) => + g.methods.map((m) => ({ where: `${g.group}/${m.id}`, macro: m.macro, groups: m.groups }))), +]; +const usedMacros = new Set(); +for (const e of registryEntries) { + const name = (e.macro || '').replace(/^@/, ''); + usedMacros.add(name); + if (!catalogMacros.has(name)) { + errors.push(`${e.where}: macro ${e.macro} not in elemco-macros.js (${w.ELEMCO_MACROS.sourceRef})`); + } +} + +// ---- every referenced option group must exist in the options catalog --------- +const catalogGroups = w.ELEMCO_OPTIONS.groups; +const checkGroup = (where, gid) => { + if (!catalogGroups[gid]) { + errors.push(`${where}: option group '${gid}' not in elemco-options.js (${w.ELEMCO_OPTIONS.sourceRef})`); + } +}; +for (const e of registryEntries) (e.groups || []).forEach((gid) => checkGroup(e.where, gid)); +(w.ELEMCO_GLOBAL_GROUPS || []).forEach((gid) => checkGroup('ELEMCO_GLOBAL_GROUPS', gid)); +for (const [gid, fields] of Object.entries(w.ELEMCO_GLOBAL_EXCLUDE || {})) { + checkGroup('ELEMCO_GLOBAL_EXCLUDE', gid); + const known = catalogGroups[gid] ? catalogGroups[gid].options : null; + for (const f of fields) { + if (known && !known[f]) { + errors.push(`ELEMCO_GLOBAL_EXCLUDE: field '${gid}.${f}' not in elemco-options.js (${w.ELEMCO_OPTIONS.sourceRef})`); + } + } +} + +// ---- reverse direction: catalog macros the registry does not surface --------- +// Utility/IO macros are legitimately absent from the method registry; list the +// rest as curation candidates so a new backend method cannot stay invisible. +const UTILITY = new Set([ + 'check_molproinfo', 'copyfile', 'copywf', 'deletefile', 'dummy', 'export_molden', + 'dfints', 'ints', 'moints', 'transform_ints', 'write_ints', 'import_matrix', + 'freeze_orbs', 'rotate_orbs', 'show_orbs', 'localize', 'region', 'loadfile', + 'loadwf', 'savefile', 'savewf', 'usewf', 'molpro_input', 'molpro_output', + 'opt', 'reset', 'run', 'set', 'set_default_eltype', +]); +for (const m of w.ELEMCO_MACROS.macros) { + if (usedMacros.has(m.name) || UTILITY.has(m.name)) continue; + // Documented aliases of a surfaced macro are covered by the original. + const alias = /^Alias for @(\S+?)\.?(?:\s|$)/.exec(m.doc || ''); + if (alias && usedMacros.has(alias[1])) continue; + warnings.push(`macro @${m.name} is in elemco-macros.js but not surfaced by the method registry`); +} + +// ---- report ------------------------------------------------------------------ +for (const m of warnings) console.warn(`warning: ${m}`); +if (errors.length) { + for (const m of errors) console.error(`ERROR: ${m}`); + console.error(`\ncheck-elemco-sync: ${errors.length} error(s), ${warnings.length} warning(s)`); + process.exit(1); +} +console.log( + `check-elemco-sync: OK — ${registryEntries.length} registry entries against ` + + `${catalogMacros.size} macros / ${Object.keys(catalogGroups).length} option groups ` + + `@ ${w.ELEMCO_OPTIONS.sourceRef}${warnings.length ? `, ${warnings.length} warning(s)` : ''}` +); From 8e9e61608ce71d0de32c1aed36e2986aebec9e70 Mon Sep 17 00:00:00 2001 From: Daniel Kats Date: Thu, 3 Sep 2026 07:39:15 +0200 Subject: [PATCH 2/3] check-elemco-sync: read group `fields`, run catalogs in a vm sandbox Both from Copilot's review of #53: - The ELEMCO_GLOBAL_EXCLUDE field check was dead: catalog groups keep their options under `fields`, so `known` was always null and a bogus excluded field passed silently. Verified by injecting `wf.bogus_field` before (OK) and after (ERROR, exit 1). - The three catalog files ran under eval() in this process's global scope. They now run in a vm context whose only global is the fake `window`; the script reads everything back from that. Co-Authored-By: Claude Fable 5.1 --- scripts/check-elemco-sync.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/scripts/check-elemco-sync.js b/scripts/check-elemco-sync.js index 0a6e311..caa800e 100644 --- a/scripts/check-elemco-sync.js +++ b/scripts/check-elemco-sync.js @@ -29,18 +29,19 @@ const fs = require('fs'); const path = require('path'); +const vm = require('vm'); const JS_DIR = path.join(__dirname, '..', 'js'); // The catalog files assign to `window.*`; elemco-methods.js declares consts and -// copies them onto `window` when present. Evaluate all three against one shared -// fake window and read everything back from it. -global.window = {}; +// copies them onto `window` when present. Run all three in one sandboxed +// context whose only global is a fake `window`, and read everything back from +// it -- nothing here needs this process's globals, so nothing gets them. +const context = vm.createContext({ window: {} }); for (const f of ['elemco-macros.js', 'elemco-options.js', 'elemco-methods.js']) { - // eslint-disable-next-line no-eval - eval(fs.readFileSync(path.join(JS_DIR, f), 'utf8')); + vm.runInContext(fs.readFileSync(path.join(JS_DIR, f), 'utf8'), context, { filename: f }); } -const w = global.window; +const w = context.window; const errors = []; const warnings = []; @@ -80,7 +81,8 @@ for (const e of registryEntries) (e.groups || []).forEach((gid) => checkGroup(e. (w.ELEMCO_GLOBAL_GROUPS || []).forEach((gid) => checkGroup('ELEMCO_GLOBAL_GROUPS', gid)); for (const [gid, fields] of Object.entries(w.ELEMCO_GLOBAL_EXCLUDE || {})) { checkGroup('ELEMCO_GLOBAL_EXCLUDE', gid); - const known = catalogGroups[gid] ? catalogGroups[gid].options : null; + // Catalog groups keep their options under `fields` (see parse-elemco-options.js). + const known = catalogGroups[gid] ? catalogGroups[gid].fields : null; for (const f of fields) { if (known && !known[f]) { errors.push(`ELEMCO_GLOBAL_EXCLUDE: field '${gid}.${f}' not in elemco-options.js (${w.ELEMCO_OPTIONS.sourceRef})`); From aa7f1d646d48139e792b535c369aeb5c25b99b03 Mon Sep 17 00:00:00 2001 From: Daniel Kats Date: Thu, 3 Sep 2026 07:42:10 +0200 Subject: [PATCH 3/3] lockfile: fast-uri 3.1.5 -> 3.1.7 (GHSA-5jgf-p345-68v8) Dependabot alert #15, high: host confusion via skipped IDN canonicalization on scheme-relative references. Transitive dev dependency only (electron-builder -> app-builder-lib -> ajv -> fast-uri), so build-time, not shipped. npm audit clean afterwards; no other package moved. Co-Authored-By: Claude Fable 5.1 --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e13a3f0..9ff7891 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1714,9 +1714,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "dev": true, "funding": [ {