- 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
186 lines
7.1 KiB
Python
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:]))
|