from __future__ import annotations import argparse import json from pathlib import Path from typing import Any def read_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def dig(mapping: dict[str, Any] | None, *keys: str) -> Any: current: Any = mapping or {} for key in keys: if not isinstance(current, dict): return None current = current.get(key) return current def short_next_record(match: dict[str, Any] | None) -> dict[str, Any] | None: node = dig(match, "next_moxel_record") if not isinstance(node, dict): return None result: dict[str, Any] = {"type": node.get("type")} for key in ("head", "value", "scalar_prefix", "tree_position"): if key in node: result[key] = node.get(key) return result def compact_item(item: dict[str, Any]) -> dict[str, Any]: named = ((item.get("named_range_matches") or [{}])[0]) if item.get("named_range_matches") else {} text = ((item.get("text_matches") or [{}])[0]) if item.get("text_matches") else {} return { "file_name": item.get("file_name"), "bytes": item.get("bytes"), "modified": item.get("modified"), "counts": item.get("counts") or {}, "named_range": { "name": named.get("name"), "tree_position": named.get("tree_position"), "one_based": dig(named, "range", "one_based"), "raw_scalars": dig(named, "range_candidate", "raw_scalars"), }, "text_match": { "text": text.get("text"), "cell_id": text.get("cell_id"), "tree_position": text.get("tree_position"), "next_moxel_record": short_next_record(text), "immediate_preceding_values": dig(text, "style_evidence", "immediate_preceding_values"), "last_7_preceding_values": dig(text, "style_evidence", "last_7_preceding_values"), }, } def diff_dict(before: dict[str, Any], after: dict[str, Any]) -> dict[str, dict[str, Any]]: changed: dict[str, dict[str, Any]] = {} for key in sorted(set(before) | set(after)): if before.get(key) != after.get(key): changed[key] = {"before": before.get(key), "after": after.get(key)} return changed def build_transitions(items: list[dict[str, Any]]) -> list[dict[str, Any]]: compact = [compact_item(item) for item in reversed(items)] transitions: list[dict[str, Any]] = [] for before, after in zip(compact, compact[1:]): named_before = before.get("named_range") if isinstance(before.get("named_range"), dict) else {} named_after = after.get("named_range") if isinstance(after.get("named_range"), dict) else {} text_before = before.get("text_match") if isinstance(before.get("text_match"), dict) else {} text_after = after.get("text_match") if isinstance(after.get("text_match"), dict) else {} transition = { "from_file": before.get("file_name"), "to_file": after.get("file_name"), "from_modified": before.get("modified"), "to_modified": after.get("modified"), "from_bytes": before.get("bytes"), "to_bytes": after.get("bytes"), "count_changes": diff_dict(before.get("counts") or {}, after.get("counts") or {}), "named_range_changes": diff_dict(named_before, named_after), "text_match_changes": diff_dict(text_before, text_after), } transitions.append(transition) return transitions def build_signature_groups(items: list[dict[str, Any]]) -> dict[str, Any]: signatures: dict[str, list[dict[str, Any]]] = {} for item in items: for match in item.get("text_matches") or []: if not isinstance(match, dict): continue signature = json.dumps(short_next_record(match), ensure_ascii=False, sort_keys=True) signatures.setdefault(signature, []).append( { "file_name": item.get("file_name"), "modified": item.get("modified"), "text": match.get("text"), "cell_id": match.get("cell_id"), "tree_position": match.get("tree_position"), "immediate_preceding_values": dig(match, "style_evidence", "immediate_preceding_values"), "last_7_preceding_values": dig(match, "style_evidence", "last_7_preceding_values"), } ) result: list[dict[str, Any]] = [] for signature, occurrences in signatures.items(): result.append( { "signature": json.loads(signature), "occurrences": occurrences, "count": len(occurrences), } ) result.sort(key=lambda item: (-int(item.get("count") or 0), json.dumps(item.get("signature"), ensure_ascii=False))) return {"next_moxel_record_signatures": result} def build_markdown(history: dict[str, Any], transitions: list[dict[str, Any]], signature_groups: dict[str, Any]) -> str: lines: list[str] = [] lines.append("# 1C template history matrix") lines.append("") lines.append(f"- Base: `{history.get('base_id')}`") lines.append(f"- Track name: `{history.get('track_name')}`") lines.append(f"- Track text: `{history.get('track_text')}`") lines.append(f"- Snapshots: `{len(history.get('items') or [])}`") lines.append("") lines.append("## Current state") current = compact_item((history.get("items") or [{}])[0] if history.get("items") else {}) lines.append("") lines.append("```json") lines.append(json.dumps(current, ensure_ascii=False, indent=2)) lines.append("```") lines.append("") lines.append("## Transitions") for transition in transitions: lines.append("") lines.append( f"- `{transition['from_file']}` -> `{transition['to_file']}` " f"({transition['from_modified']} -> {transition['to_modified']})" ) lines.append("") lines.append("```json") lines.append(json.dumps(transition, ensure_ascii=False, indent=2)) lines.append("```") lines.append("") lines.append("## Signatures") lines.append("") lines.append("```json") lines.append(json.dumps(signature_groups, ensure_ascii=False, indent=2)) lines.append("```") lines.append("") return "\n".join(lines) def main() -> int: parser = argparse.ArgumentParser(description="Build a transition matrix from tracked 1C template history.") parser.add_argument("history_json", help="Path to JSON generated by track_1c_template_history.py") parser.add_argument("--json-output", help="Optional JSON output path.") parser.add_argument("--markdown-output", help="Optional Markdown output path.") args = parser.parse_args() history_path = Path(args.history_json) history = read_json(history_path) items = history.get("items") or [] transitions = build_transitions(items) signature_groups = build_signature_groups(items) payload = { "schema": "codex_1c_template_history_matrix.v1", "source": str(history_path), "track_name": history.get("track_name"), "track_text": history.get("track_text"), "current": compact_item(items[0] if items else {}), "transitions": transitions, "signatures": signature_groups, } rendered = json.dumps(payload, ensure_ascii=False, indent=2) if args.json_output: Path(args.json_output).write_text(rendered, encoding="utf-8") markdown = build_markdown(history, transitions, signature_groups) if args.markdown_output: Path(args.markdown_output).write_text(markdown, encoding="utf-8") print(rendered) return 0 if __name__ == "__main__": raise SystemExit(main())