190 lines
7.7 KiB
Python
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())
|