From d2de622744212ea0ba720b3f4212b82940ffe84b Mon Sep 17 00:00:00 2001 From: Agando Date: Tue, 27 Oct 2026 11:23:10 +0100 Subject: fix(newos4): rollback keeps scaffolding, checkpoints cannot collide, tests gate the build Review of the 08:30 implementer run (v4.6.102, checkpoint name fix). The shipped fix was correct; these are the defects it introduced or left behind, each measured first. - virtual-fs.js rollback(): the snapshot strips _snapshots/_sessions/.trash for bloat. Restoring it verbatim deleted every checkpoint, every agent session and the trash, made rollback irreversible and unrepeatable. Re-attach the live copies after the root swap. - checkpoint(): names are millisecond-resolution, so tight calls overwrote each other (measured 6 calls -> 3 snapshots). Dedupe with a -N suffix, refuse names containing "/" (written but unlistable), and prune by snapshot createdAt instead of filename sort, which let a custom name ("aaa-oldest") evict newer auto-named checkpoints. - module-predictor.js: legacy pre-v2 points have no `usable`, so a 0.079 s aborted run re-entered similarity as evidence and floored every prediction at 1 s. Backfill `usable` from the measurement on load, drop the ones it rejects, add a belt-and-braces guard in _findSimilar and count only usable points as actual executions. - newos4-maintain.py: run every node-runnable modules/test/test-*.js on each maintenance pass. The v4.6.102 regression was written but no job ever executed it. - newos4-steer.py: `done` now requires --commit that exists in the repo, and gains `reopen`. A written plan was being closed as done at 06:17 while the predictor fix shipped nowhere, so the implementer's pre-flight queue was empty. - steering.json: predictor item reopened, checkpoint item stamped with f7249ff. - maintenance board: MAINT-002/005/015 were fixed in v4.6.81-85 but still listed open/high; closed with their commit ids, and MAINT-010/016 annotated with the probes that keep them open. Tests: test-vfs-checkpoint 23/23 (RED 14/9 before), test-predictor-accuracy 38/38 (T1-T3 RED on HEAD), maintain --fix all PASS, browser round-trip "Checkpoint created: " -> "Rolled back to checkpoint: " on v4.6.103. Also carries the already-deployed 2026-10-26 syntax-gate hunk in newos4-maintain.py (syntax_failures + gated rebuild) that had never been committed. --- scripts/newos4-maintain.py | 63 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 9 deletions(-) (limited to 'scripts/newos4-maintain.py') diff --git a/scripts/newos4-maintain.py b/scripts/newos4-maintain.py index dd6c898..c28fb8b 100644 --- a/scripts/newos4-maintain.py +++ b/scripts/newos4-maintain.py @@ -21,22 +21,41 @@ def check_versions(): n = html.count(ver) add('PASS' if n == 3 else 'FAIL', f'version {ver} x{n} in main.html (expect 3)') -def check_bundles(): +def syntax_failures(): + bad = [] + for p in glob.glob(os.path.join(ROOT, 'modules/core/*.js')) + glob.glob(os.path.join(ROOT, 'modules/ext/*.js')): + if os.path.basename(p) in ('core-bundle.js', 'critical-ext-bundle.js'): continue + if run(['node', '--check', p]).returncode: bad.append(os.path.basename(p)) + return bad + +def check_bundles(bad_syntax=None): + """Rebuild only when every module parses. + + rebuild-bundles.py concatenates blindly (no validation), so a module with a + syntax error would be baked straight into the served core-bundle.js. Since + --fix now runs daily, the rebuild is gated on check_syntax and the produced + bundles are re-checked before the run is called clean. + """ paths = ('modules/core/core-bundle.js', 'modules/ext/critical-ext-bundle.js') before = {p: sh(f'md5sum {p}') for p in paths} if not FIX: add('PASS', 'bundle check skipped (use --fix)'); return + if bad_syntax is None: bad_syntax = syntax_failures() + if bad_syntax: + add('FAIL', 'bundles rebuild BLOCKED: syntax fail in ' + ', '.join(bad_syntax[:5])); return r = run([sys.executable, 'scripts/rebuild-bundles.py'], cwd=ROOT) + if r.returncode: + add('FAIL', 'bundles FAILED'); return + invalid = [p for p in paths if run(['node', '--check', os.path.join(ROOT, p)]).returncode] + if invalid: + add('FAIL', 'bundles rebuilt but INVALID: ' + ', '.join(invalid)); return after = {p: sh(f'md5sum {p}') for p in paths} changed = [p for p in paths if before[p] != after[p]] - add('FAIL' if r.returncode else ('WARN' if changed else 'PASS'), - 'bundles ' + ('FAILED' if r.returncode else ('rebuilt: ' + ', '.join(changed) if changed else 'clean'))) + add('WARN' if changed else 'PASS', + 'bundles ' + ('rebuilt: ' + ', '.join(changed) if changed else 'clean')) -def check_syntax(): - bad = [] - for p in glob.glob(os.path.join(ROOT, 'modules/core/*.js')) + glob.glob(os.path.join(ROOT, 'modules/ext/*.js')): - if os.path.basename(p) in ('core-bundle.js', 'critical-ext-bundle.js'): continue - if run(['node', '--check', p]).returncode: bad.append(os.path.basename(p)) +def check_syntax(bad=None): + bad = syntax_failures() if bad is None else bad add('PASS' if not bad else 'FAIL', 'syntax ' + ('OK' if not bad else 'fail: ' + ', '.join(bad))) def check_perms(): @@ -128,6 +147,31 @@ def check_help_coverage(): f'help coverage {len(cmds) - len(missing)}/{len(cmds)} commands' + ('' if not missing else f' — undocumented: {", ".join(missing[:8])}')) +def check_regression_tests(): + """Every node-runnable test under modules/test/ must pass (2026-10-27). + + The v4.6.102 checkpoint regression was written but no job executed it, so drift could ship + silently. Two header conventions exist: '// ... run: node' and '// node modules/test/x.js'. + test-suite.js starts with a class (browser-only) and is correctly skipped. + """ + tests, failed = [], [] + for p in sorted(glob.glob(os.path.join(ROOT, 'modules/test/test-*.js'))): + head = ''.join(open(p, encoding='utf-8', errors='ignore').readlines()[:3]) + if 'run: node' not in head and not head.startswith('// node modules/test/'): + continue + tests.append(p) + try: + r = subprocess.run(['node', p], cwd=ROOT, capture_output=True, text=True, timeout=120) + except subprocess.TimeoutExpired: + failed.append(os.path.basename(p) + ': timeout') + continue + if r.returncode != 0: + lines = (r.stdout or r.stderr).strip().splitlines() or ['no output'] + failed.append(os.path.basename(p) + ': ' + lines[-1][:120]) + add('PASS' if not failed else 'FAIL', + 'node tests %d/%d pass' % (len(tests) - len(failed), len(tests)) + + ('' if not failed else ' — ' + '; '.join(failed[:3]))) + def check_links(): if not DEEP: return broken = [] @@ -143,7 +187,8 @@ def check_links(): def main(): os.chdir(ROOT) - check_versions(); check_bundles(); check_syntax(); check_perms(); check_index(); check_data(); check_live(); check_atlas(); check_course(); check_help_coverage(); check_links() + bad = syntax_failures() # computed once, gates the bundle rebuild + check_versions(); check_syntax(bad); check_regression_tests(); check_bundles(bad); check_perms(); check_index(); check_data(); check_live(); check_atlas(); check_course(); check_help_coverage(); check_links() fails = [m for l, m in issues if l == 'FAIL'] print('newOS4 maintenance — ' + datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%MZ') + (' [fix]' if FIX else '') + (' [deep]' if DEEP else '')) for l, m in issues: print(f'{l}: {m}') -- cgit v1.2.3