Files
VRCT/src-python/scripts/verify_docs_vs_code.py

162 lines
6.1 KiB
Python

import importlib.util
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MAINLOOP = ROOT / 'mainloop.py'
CONTROLLER = ROOT / 'controller.py'
DOC_API = ROOT / 'docs' / 'api.md'
DOC_RUN = ROOT / 'docs' / 'run_events_payloads.md'
def extract_mapping_from_mainloop():
"""
Import mainloop.py and read 'mapping' and 'run_mapping' objects directly.
This executes the module in an isolated module object; mainloop has some
initialization but exposing these dicts is acceptable for verification.
"""
run_mapping = {}
mapping = {}
try:
spec = importlib.util.spec_from_file_location('project_mainloop', str(MAINLOOP))
module = importlib.util.module_from_spec(spec)
loader = spec.loader
if loader is None:
raise RuntimeError('Could not load mainloop module')
loader.exec_module(module)
mapping = getattr(module, 'mapping', {}) or {}
run_mapping = getattr(module, 'run_mapping', {}) or {}
return mapping, run_mapping
except Exception as e:
print('Error importing mainloop.py', e)
# Fallback: simple regex-based extraction from mainloop.py text
try:
text = MAINLOOP.read_text(encoding='utf-8')
# run_mapping entries like: "transcription_mic": "/run/transcription_send_mic_message",
for mm in re.finditer(r"[\'\"]([^\'\"]+)[\'\"]\s*:\s*[\'\"](/run/[a-zA-Z0-9_\-\/]+)[\'\"]", text):
run_mapping[mm.group(1)] = mm.group(2)
# mapping endpoints: any '/get/...' or '/set/...' literal in file
for mm in re.finditer(r"[\'\"](/(?:get|set)/[a-zA-Z0-9_\-\/]+)[\'\"]", text):
mapping[mm.group(1)] = True
except Exception as e:
print('Error parsing mainloop.py via fallback', e)
return mapping, run_mapping
def extract_run_events_from_controller():
code = CONTROLLER.read_text(encoding='utf-8')
# find self.run( ... , self.run_mapping["key"], ... ) and direct self.run(...,
run_keys = set()
# pattern for self.run(..., self.run_mapping["xxx"], ...)
pattern = re.compile(r"self\.run\([^\)]*self\.run_mapping\[\s*[\'\"]([^\'\"]+)[\'\"]\s*\]", re.M)
for m in pattern.finditer(code):
run_keys.add(m.group(1))
# also find self.run(..., "/run/xxx", ...)
pattern2 = re.compile(r"self\.run\([^\)]*\"(/run/[^\'\"]+)\"", re.M)
for m in pattern2.finditer(code):
run_keys.add(m.group(1))
return run_keys
def extract_endpoints_from_docs():
api = DOC_API.read_text(encoding='utf-8')
run = DOC_RUN.read_text(encoding='utf-8') if DOC_RUN.exists() else ''
endpoints = set()
run_events = set()
# conservative extraction: match endpoints that start with /get/ /set/ /run/
pattern = re.compile(r"(/(?:get|set|run)(?:/[a-zA-Z0-9_\-]+)+)")
for m in pattern.finditer(api):
token = m.group(1)
# drop umbrella placeholders and tokens that end with '/'
if token in ('/get', '/set', '/run', '/get/data', '/set/data'):
continue
if token.endswith('/'):
continue
if token.startswith('/run/'):
run_events.add(token)
else:
endpoints.add(token)
for m in pattern.finditer(run):
token = m.group(1)
if token in ('/get', '/set', '/run', '/get/data', '/set/data'):
continue
if token.endswith('/'):
continue
if token.startswith('/run/'):
run_events.add(token)
else:
endpoints.add(token)
return endpoints, run_events
def main():
mapping, run_mapping = extract_mapping_from_mainloop()
code_endpoints = set(mapping.keys())
code_run_events = set(run_mapping.values())
# normalize run events: run_mapping values likely like '/run/…'
controller_run_keys = extract_run_events_from_controller()
doc_endpoints, doc_run_events = extract_endpoints_from_docs()
report = []
report.append('=== Summary ===')
report.append(f'Code endpoints (/get,/set,/run): {len(code_endpoints)}')
report.append(f'Code run_mapping entries: {len(code_run_events)}')
report.append(f'Controller-run keys found by scan: {len(controller_run_keys)}')
report.append(f'Documented endpoints found in docs/api.md: {len(doc_endpoints)}')
report.append(f'Documented run events found in docs: {len(doc_run_events)}')
# endpoints present in code but not in docs
missing_in_docs = code_endpoints - doc_endpoints
extra_in_docs = doc_endpoints - code_endpoints
report.append('\n=== Endpoints present in code but NOT documented ===')
if missing_in_docs:
for e in sorted(missing_in_docs):
report.append(' - ' + e)
else:
report.append(' - None')
report.append('\n=== Endpoints documented but NOT in code ===')
if extra_in_docs:
for e in sorted(extra_in_docs):
report.append(' - ' + e)
else:
report.append(' - None')
report.append('\n=== Run events present in code (run_mapping) but NOT documented ===')
missing_run_in_docs = code_run_events - doc_run_events
if missing_run_in_docs:
for e in sorted(missing_run_in_docs):
report.append(' - ' + e)
else:
report.append(' - None')
report.append('\n=== Run keys emitted in controller (self.run mapping keys) but NOT in run_mapping values ===')
# controller_run_keys are keys like 'connected_network' or '/run/connected_network'
# normalize controller keys to values: if key starts with '/run/' keep, else map via run_mapping if possible
normalized = set()
for k in controller_run_keys:
if k.startswith('/run/'):
normalized.add(k)
else:
if k in run_mapping:
normalized.add(run_mapping[k])
else:
normalized.add(k)
# compare normalized with code_run_events
extra_controller_keys = normalized - code_run_events
if extra_controller_keys:
for e in sorted(extra_controller_keys):
report.append(' - ' + e)
else:
report.append(' - None')
out = '\n'.join(report)
print(out)
if __name__ == '__main__':
main()