#!/usr/bin/env python3 """Freshness gate for the native-model / sim-model dual-track flow (Task16-3 section 8). python check_sync.py [--json] [--quiet] Two models describe the same algorithm: xxx_h.slx native model -- the ONLY source RTL is generated from xxx_hs.slx sim model -- derived from it, native core swapped for a hand-written m implementation, runs the scenarios The m implementation is NOT derived from the native core: regenerating the sim model just re-wraps the same unchanged m file. So a change to the native core that is not mirrored into the m implementation leaves the RTL changed while the simulation still validates the OLD behaviour -- silently. This tool checks the three ordering chains that catch that: chain 1 (derived fresh) : max(sources) < min(derived products) chain 2 (gate fresh) : max(derived) < gate result chain 3 (tools fresh) : max(gate tools) < gate result Chain 1 is the one people forget. "gate is newer than every source" alone passes even when the sim model was built before the source changed. Exit code 0 = all chains hold, the sim evidence may be trusted. Exit code 1 = a chain is broken (stale artefact) -- regenerate and re-run the gate. Exit code 2 = manifest problem (missing file, bad JSON). Timestamps are a stopgap: copy/touch/checkout perturb them. The stronger form is to have the generator and gate record checksums of the sources they used. This tool is the cheap version that needs no changes to the existing scripts. """ import json import os import sys from datetime import datetime # manifest key -> (role, which side of chain 1) SOURCE_KEYS = [ ('native_model', 'native model'), ('build_scripts', 'build script'), ('gen_scripts', 'generator script'), ('mcode_impl', 'm implementation'), ('runtime_deps', 'runtime dep (.m / .mat / config)'), ] DERIVED_KEYS = [ ('sim_model', 'sim model'), ('rtl_source_model', 'RTL source model'), ] def _as_list(v): if v is None: return [] return v if isinstance(v, list) else [v] def _resolve(root, p): return p if os.path.isabs(p) else os.path.normpath(os.path.join(root, p)) def collect(root, manifest, keys): """-> list of (abspath, role, mtime) ; missing files raise.""" out = [] for key, role in keys: for p in _as_list(manifest.get(key)): ap = _resolve(root, p) if not os.path.exists(ap): raise FileNotFoundError(f'{key}: {ap}') out.append((ap, role, os.path.getmtime(ap))) return out def ts(t): return datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M:%S') def rel(root, p): try: return os.path.relpath(p, root) except ValueError: return p def main(argv): args = [a for a in argv if not a.startswith('--')] flags = {a for a in argv if a.startswith('--')} if not args: sys.exit(__doc__) mf_path = os.path.abspath(args[0]) try: manifest = json.load(open(mf_path, encoding='utf-8-sig')) except Exception as e: # noqa: BLE001 print(f'ERROR: cannot read manifest {mf_path}: {e}') return 2 # paths in the manifest are relative to "root", default = manifest's directory root = manifest.get('root') or os.path.dirname(mf_path) root = os.path.abspath(_resolve(os.path.dirname(mf_path), root)) try: sources = collect(root, manifest, SOURCE_KEYS) derived = collect(root, manifest, DERIVED_KEYS) tools = collect(root, manifest, [('gate_tools', 'gate tool')]) gate_p = manifest.get('gate_result') if not gate_p: raise KeyError('gate_result') gate_ap = _resolve(root, gate_p) if not os.path.exists(gate_ap): raise FileNotFoundError(f'gate_result: {gate_ap}') gate_t = os.path.getmtime(gate_ap) except (FileNotFoundError, KeyError) as e: print(f'ERROR: manifest refers to something that is missing -> {e}') print(' every declared path must exist; fix the manifest or generate the file.') return 2 if not sources: print('ERROR: manifest declares no sources'); return 2 if not derived: print('ERROR: manifest declares no derived products'); return 2 newest_src = max(sources, key=lambda r: r[2]) oldest_der = min(derived, key=lambda r: r[2]) newest_der = max(derived, key=lambda r: r[2]) newest_tool = max(tools, key=lambda r: r[2]) if tools else None chains = [ ('1 derived fresh', newest_src[2] < oldest_der[2], f'newest source {ts(newest_src[2])} {rel(root, newest_src[0])}', f'oldest derived {ts(oldest_der[2])} {rel(root, oldest_der[0])}'), ('2 gate fresh', newest_der[2] < gate_t, f'newest derived {ts(newest_der[2])} {rel(root, newest_der[0])}', f'gate result {ts(gate_t)} {rel(root, gate_ap)}'), ] if newest_tool: chains.append(('3 tools fresh', newest_tool[2] < gate_t, f'newest tool {ts(newest_tool[2])} {rel(root, newest_tool[0])}', f'gate result {ts(gate_t)} {rel(root, gate_ap)}')) ok = all(c[1] for c in chains) if '--json' in flags: print(json.dumps({ 'model': manifest.get('model'), 'ok': ok, 'chains': [{'name': n, 'ok': o} for n, o, _, _ in chains], 'gate_result_mtime': ts(gate_t), }, ensure_ascii=False, indent=2)) return 0 if ok else 1 print(f'check_sync: {manifest.get("model") or "(unnamed)"} root={root}') if '--quiet' not in flags: print('\n all declared files, oldest first:') for ap, role, t in sorted(sources + derived + tools + [(gate_ap, 'gate result', gate_t)], key=lambda r: r[2]): print(f' {ts(t)} {role:<32} {rel(root, ap)}') print() for name, good, a, b in chains: print(f' [{"PASS" if good else "FAIL"}] chain {name}') print(f' {a}') print(f' {b}') if not good: print(' -> the second is OLDER than the first, i.e. it is STALE') print() if ok: print('VERDICT: PASS -- ordering holds; sim evidence may be trusted.') else: print('VERDICT: FAIL -- a stale artefact is in play.') broken = {c[0][0] for c in chains if not c[1]} # '1' / '2' / '3' if '1' in broken: print(' chain 1: the derived model predates its source.') print(' FIRST mirror any native-core change into the m implementation,') print(' THEN regenerate the derived model(s), THEN re-run the gate.') print(' Regenerating alone does NOT sync the m implementation.') if '2' in broken: print(' chain 2: the gate ran before the current derived model existed -- re-run the gate.') if '3' in broken: print(' chain 3: the gate tooling changed after the recorded pass -- re-run the gate.') return 0 if ok else 1 if __name__ == '__main__': sys.exit(main(sys.argv[1:]))