Files
llm/scripts/analyze_1c_template_history_matrix.py

190 lines
7.7 KiB
Python

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())