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