Files
skills-win/simulink-rtl-redev/check_sync.py
T
bowei ea4541ee90 sync: SHANGHAI-U9 2026-07-27 三个 Simulink→RTL skill 转系统级 + 新增 simulink-rtl-redev
- simulink_rtl_dev / rtl-ip-replace: 从工程级 .claude/skills 迁到用户级并去工程耦合
  (模型名/Notes 文档依赖/工程实测基线移出; addpath 与脚本路径改用户级)
- rtl-ip-replace/scripts/gen_cmp_tb.py: Vivado 路径不再写死, 改读 VIVADO_PATH /
  XILINX_VIVADO (兼容 install root / bin / bin\vivado.bat 三种写法), 回落时提示
  去设 skills_env.md 的 VIVADO_PATH
- simulink-rtl-redev: 新增。原生模型改动后的仿真模型同步规范, 含两个实测过的工具
  check_sync.py (三条时序链新鲜度闸门) 与 measure_k.m (时域/频域/包络三估计器互证)
- sim-rtl-flow/SKILL.md: 加入 sync_sim:* 步骤, 引用上述三个 skill
2026-07-27 10:39:53 +08:00

186 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""Freshness gate for the native-model / sim-model dual-track flow (Task16-3 section 8).
python check_sync.py <manifest.json> [--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:]))