Files
llm/scripts/analyze_1c_moxel_merge_row_bands.py
T

242 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import argparse
import json
import urllib.request
from pathlib import Path
from typing import Any
import xml.etree.ElementTree as ET
from analyze_1c_template_xml_profiles import merge_ranges
def rpc(adapter_url: str, method: str, payload: dict[str, Any]) -> dict[str, Any]:
body = json.dumps({"method": method, "payload": payload}, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(
f"{adapter_url.rstrip('/')}/rpc",
data=body,
headers={"Content-Type": "application/json; charset=utf-8"},
method="POST",
)
with urllib.request.urlopen(req, timeout=180) as resp:
return json.loads(resp.read().decode("utf-8", errors="replace"))
def runs(values: list[int]) -> list[dict[str, int]]:
if not values:
return []
result: list[dict[str, int]] = []
start = previous = values[0]
for value in values[1:]:
if value == previous + 1:
previous = value
continue
result.append({"start": start, "end": previous, "length": previous - start + 1})
start = previous = value
result.append({"start": start, "end": previous, "length": previous - start + 1})
return result
def div32_values(numbers: list[Any]) -> list[int]:
return [
int(value) // 32
for value in numbers
if isinstance(value, int) and value > 0 and value <= 4096 and value % 32 == 0
]
def small_values(numbers: list[Any]) -> list[int]:
return [int(value) for value in numbers if isinstance(value, int) and 2 <= value <= 128]
def template_xml_path(root: Path, template: str) -> Path:
return root / template / "Ext" / "Template.xml"
def merge_block_candidate(adapter_url: str, base_id: str, owner_kind: str, owner_name: str, template: str) -> dict[str, Any]:
data = rpc(
adapter_url,
"templates.read",
{
"base_id": base_id,
"kind": owner_kind,
"name": owner_name,
"template": template,
"sections": "merges",
"refresh_cache": False,
},
)
return (((data.get("templates") or [{}])[0].get("structure") or {}).get("merge_record_block_candidates") or [{}])[0]
def merge_block_records(
adapter_url: str,
base_id: str,
owner_kind: str,
owner_name: str,
template: str,
candidate: dict[str, Any],
) -> list[dict[str, Any]]:
position = str(candidate.get("tree_position") or "$.0")
try:
start = int(position.split(".")[1]) + 1
except (IndexError, ValueError):
start = 0
count = int(candidate.get("count") or 0)
data = rpc(
adapter_url,
"templates.read",
{
"base_id": base_id,
"kind": owner_kind,
"name": owner_name,
"template": template,
"sections": "moxel_records",
"max_moxel_records": count + 40,
"moxel_record_start": start,
"moxel_record_end": start + count + 35,
"refresh_cache": False,
},
)
diagnostics = ((data.get("templates") or [{}])[0].get("structure") or {}).get("moxel_record_diagnostics") or [{}]
if isinstance(diagnostics, list):
diagnostics = diagnostics[0] if diagnostics else {}
return [record for record in diagnostics.get("top_level_records") or [] if isinstance(record, dict)][:count]
def analyze_template(
*,
adapter_url: str,
base_id: str,
owner_kind: str,
owner_name: str,
template: str,
xml_root: Path,
) -> dict[str, Any]:
xml_path = template_xml_path(xml_root, template)
merges = merge_ranges(ET.parse(xml_path).getroot(), limit=500)
xml_rows = sorted(set(int(item["row"]) for item in merges))
xml_columns = sorted(set(int(item["column"]) for item in merges) | set(int(item["column"]) + int(item["width"]) - 1 for item in merges))
candidate = merge_block_candidate(adapter_url, base_id, owner_kind, owner_name, template)
records = merge_block_records(adapter_url, base_id, owner_kind, owner_name, template, candidate)
by_value: dict[int, list[int]] = {}
coordinate_records: list[dict[str, Any]] = []
for index, record in enumerate(records, 1):
numbers = record.get("numeric_items") or []
for value in set(small_values(numbers)):
by_value.setdefault(value, []).append(index)
packed_columns = div32_values(numbers)
if packed_columns:
coordinate_records.append(
{
"index": index,
"tree_position": record.get("tree_position"),
"numeric_items": numbers,
"div32": packed_columns,
"small": small_values(numbers),
}
)
value_summaries = [
{
"value": value,
"count": len(indexes),
"record_indexes": indexes[:30],
"runs": runs(indexes),
"matches_xml_row": value in xml_rows,
"matches_xml_column_or_edge": value in xml_columns,
}
for value, indexes in sorted(by_value.items())
]
xml_row_hits = [
{
"row": row,
"count": len(by_value.get(row) or []),
"record_indexes": (by_value.get(row) or [])[:20],
"runs": runs(by_value.get(row) or [])[:8],
}
for row in xml_rows
if by_value.get(row)
]
return {
"template": template,
"xml_merge_count": len(merges),
"sql_block_count": int(candidate.get("count") or 0),
"tree_position": candidate.get("tree_position"),
"xml_rows": xml_rows,
"xml_row_runs": runs(xml_rows),
"xml_columns_and_right_edges": xml_columns,
"value_summaries": value_summaries,
"xml_row_hits": xml_row_hits,
"coordinate_records": coordinate_records[:120],
"coordinate_record_runs": runs([item["index"] for item in coordinate_records]),
}
def render_markdown(payload: dict[str, Any]) -> str:
lines = ["# MOXCEL merge row-band analysis", ""]
for item in payload.get("items") or []:
lines.append(f"## {item.get('template')}")
lines.append("")
lines.append(f"- XML merges: `{item.get('xml_merge_count')}`")
lines.append(f"- SQL block count: `{item.get('sql_block_count')}` at `{item.get('tree_position')}`")
lines.append(f"- XML row runs: `{item.get('xml_row_runs')}`")
lines.append(f"- SQL coordinate-record runs: `{(item.get('coordinate_record_runs') or [])[:20]}`")
lines.append("")
lines.append("### XML Row Hits In SQL Small Scalars")
lines.append("")
lines.append("| Row | Count | Runs | First indexes |")
lines.append("| ---: | ---: | --- | --- |")
for hit in (item.get("xml_row_hits") or [])[:60]:
lines.append(f"| {hit.get('row')} | {hit.get('count')} | `{hit.get('runs')}` | `{(hit.get('record_indexes') or [])[:12]}` |")
lines.append("")
lines.append("### Top Small Scalar Values")
lines.append("")
lines.append("| Value | Count | XML row | XML col/edge | Runs |")
lines.append("| ---: | ---: | --- | --- | --- |")
for value in sorted(item.get("value_summaries") or [], key=lambda row: (-int(row.get("count") or 0), int(row.get("value") or 0)))[:30]:
lines.append(
f"| {value.get('value')} | {value.get('count')} | `{value.get('matches_xml_row')}` | "
f"`{value.get('matches_xml_column_or_edge')}` | `{(value.get('runs') or [])[:8]}` |"
)
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description="Analyze SQL MOXCEL merge-block row/size scalar bands against XML merge rows.")
parser.add_argument("--adapter-url", default="http://docker-gpu.cin.su:8011")
parser.add_argument("--base-id", default="upo_test")
parser.add_argument("--owner-kind", default="Document")
parser.add_argument("--owner-name", default="АвансовыйОтчет")
parser.add_argument(
"--xml-root",
default=r"Z:\codex\1C\XML\UPO\Структура базы 1с\Конфигурация\Documents\АвансовыйОтчет\Templates",
)
parser.add_argument("--template", action="append", required=True)
parser.add_argument("--output-json", default="reports/1c-template-baselines/moxel-merge-row-band-analysis.json")
parser.add_argument("--output-markdown", default="reports/1c-template-baselines/moxel-merge-row-band-analysis.md")
args = parser.parse_args()
payload = {
"schema": "codex_1c_moxel_merge_row_band_analysis.v1",
"items": [
analyze_template(
adapter_url=args.adapter_url,
base_id=args.base_id,
owner_kind=args.owner_kind,
owner_name=args.owner_name,
template=template,
xml_root=Path(args.xml_root),
)
for template in args.template
],
}
Path(args.output_json).write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
Path(args.output_markdown).write_text(render_markdown(payload), encoding="utf-8")
print(json.dumps({"status": "ok", "json": args.output_json, "markdown": args.output_markdown, "items": len(payload["items"])}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())