Initial SQL-only 1C adapter baseline

This commit is contained in:
2026-07-22 03:03:47 +03:00
commit e2503b77e7
545 changed files with 184711 additions and 0 deletions
@@ -0,0 +1,363 @@
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
XML_KIND_TO_SQL_KIND = {
"Catalogs": "Catalog",
"Documents": "Document",
"Reports": "Report",
"DataProcessors": "DataProcessor",
"ChartsOfCharacteristicTypes": "ChartOfCharacteristicTypes",
"ChartsOfAccounts": "ChartOfAccounts",
"ChartsOfCalculationTypes": "ChartOfCalculationTypes",
"InformationRegisters": "InformationRegister",
"AccumulationRegisters": "AccumulationRegister",
"AccountingRegisters": "AccountingRegister",
"CalculationRegisters": "CalculationRegister",
"BusinessProcesses": "BusinessProcess",
"Tasks": "Task",
}
def read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def xml_route(item: dict[str, Any]) -> dict[str, Any]:
parts = Path(str(item.get("path") or item.get("relative_path") or "")).parts
for index, part in enumerate(parts):
if part in XML_KIND_TO_SQL_KIND and index + 3 < len(parts):
owner_name = parts[index + 1]
if parts[index + 2] == "Templates":
return {
"owner_kind": XML_KIND_TO_SQL_KIND[part],
"owner_name": owner_name,
"template_name": parts[index + 3],
"source": "xml_path",
}
rel_parts = Path(str(item.get("relative_path") or "")).parts
if len(rel_parts) >= 4 and rel_parts[0] == "Templates":
return {"template_name": rel_parts[1], "source": "xml_relative_path"}
return {"template_name": None, "source": "unknown"}
def sql_key(item: dict[str, Any]) -> tuple[str | None, str | None, str | None]:
owner = item.get("owner") if isinstance(item.get("owner"), dict) else {}
template = item.get("template") if isinstance(item.get("template"), dict) else {}
return owner.get("kind"), owner.get("name"), template.get("name")
def xml_key(item: dict[str, Any]) -> tuple[str | None, str | None, str | None]:
route = item.get("route") if isinstance(item.get("route"), dict) else xml_route(item)
return route.get("owner_kind"), route.get("owner_name"), route.get("template_name")
def dimension_pair(value: Any) -> tuple[int | None, int | None]:
if not isinstance(value, dict):
return None, None
rows = value.get("rows")
columns = value.get("columns")
try:
parsed_rows = int(rows) if rows is not None else None
except (TypeError, ValueError):
parsed_rows = None
try:
parsed_columns = int(columns) if columns is not None else None
except (TypeError, ValueError):
parsed_columns = None
return parsed_rows, parsed_columns
def dimension_delta(left: tuple[int | None, int | None], right: tuple[int | None, int | None]) -> dict[str, int | None]:
rows = left[0] - right[0] if left[0] is not None and right[0] is not None else None
columns = left[1] - right[1] if left[1] is not None and right[1] is not None else None
return {"rows": rows, "columns": columns}
def format_dimensions(value: Any) -> str:
rows, columns = dimension_pair(value)
if rows is None and columns is None:
return "-"
return f"{rows if rows is not None else '-'}x{columns if columns is not None else '-'}"
def format_delta(value: dict[str, int | None] | None) -> str:
if not isinstance(value, dict):
return "-"
rows = value.get("rows")
columns = value.get("columns")
if rows is None and columns is None:
return "-"
row_text = f"{rows:+d}" if isinstance(rows, int) else "-"
column_text = f"{columns:+d}" if isinstance(columns, int) else "-"
return f"{row_text}x{column_text}"
def sql_count(profile: dict[str, Any], key: str, sample_key: str | None = None) -> int | None:
counts = profile.get("counts") if isinstance(profile.get("counts"), dict) else {}
value = counts.get(key)
if value is not None:
try:
return int(value)
except (TypeError, ValueError):
return None
if sample_key and isinstance(profile.get(sample_key), list):
return len(profile.get(sample_key) or [])
return None
def merge_record_analysis_counts(profile: dict[str, Any]) -> dict[str, int]:
blocks = 0
records = 0
for candidate in profile.get("sample_merge_record_block_candidates") or []:
if not isinstance(candidate, dict):
continue
evidence = candidate.get("evidence") if isinstance(candidate.get("evidence"), dict) else {}
analysis = evidence.get("record_analysis") if isinstance(evidence.get("record_analysis"), dict) else {}
if analysis.get("schema") != "moxel_numeric_block_records.v1":
continue
blocks += 1
try:
records += int(analysis.get("records_analyzed") or 0)
except (TypeError, ValueError):
pass
return {"blocks": blocks, "records": records}
def compare_item(sql_item: dict[str, Any] | None, xml_item: dict[str, Any]) -> dict[str, Any]:
route = xml_item.get("route") if isinstance(xml_item.get("route"), dict) else xml_route(xml_item)
xml_counts = xml_item.get("counts") if isinstance(xml_item.get("counts"), dict) else {}
xml_capacity = dimension_pair(xml_item.get("capacity_dimensions"))
xml_used = dimension_pair(xml_item.get("used_dimensions"))
result: dict[str, Any] = {
"route": route,
"xml": {
"xml_kind": xml_item.get("xml_kind"),
"capacity_dimensions": xml_item.get("capacity_dimensions"),
"used_dimensions": xml_item.get("used_dimensions"),
"counts": xml_counts,
},
"status": "ok",
"gaps": [],
}
if sql_item is None:
result["status"] = "missing_sql_profile"
result["gaps"].append({"code": "missing_sql_profile", "severity": "error", "message": "No matching SQL template profile was found."})
return result
profile = sql_item.get("profile") if isinstance(sql_item.get("profile"), dict) else {}
sql_capacity = dimension_pair(profile.get("capacity_dimensions") or profile.get("dimensions"))
sql_used = dimension_pair(profile.get("used_dimensions"))
used_delta = dimension_delta(sql_used, xml_used)
sql_counts = {
"cells": sql_count(profile, "cells", None),
"cell_coordinate_hints": sql_count(profile, "cell_coordinate_hints", "sample_cell_coordinate_hints"),
"cell_style_coordinate_hints": sql_count(profile, "cell_style_coordinate_hints", "sample_style_coordinate_hints"),
"parameters": sql_count(profile, "cell_parameters", "sample_cell_parameters"),
"text_ids": sql_count(profile, "cell_text_identifiers", "sample_text_identifiers"),
"merges": sql_count(profile, "merged_ranges", None),
"merge_record_block_candidates": sql_count(profile, "merge_record_block_candidates", "sample_merge_record_block_candidates"),
"merge_count_hints": sql_count(profile, "merge_count_hints", "sample_merge_count_hints"),
"format_indexes": sql_count(profile, "cell_style_candidates", "sample_style_texts"),
}
merge_record_analysis = merge_record_analysis_counts(profile)
result["sql"] = {
"owner": sql_item.get("owner"),
"template": sql_item.get("template"),
"capacity_dimensions": profile.get("capacity_dimensions") or profile.get("dimensions"),
"used_dimensions": profile.get("used_dimensions"),
"used_delta": used_delta,
"counts": sql_counts,
"merge_record_analysis": merge_record_analysis,
"capabilities": profile.get("capabilities") or {},
}
if sql_capacity != xml_capacity:
result["gaps"].append(
{
"code": "capacity_dimensions_mismatch",
"severity": "error",
"sql": {"rows": sql_capacity[0], "columns": sql_capacity[1]},
"xml": {"rows": xml_capacity[0], "columns": xml_capacity[1]},
}
)
if sql_used == (None, None):
result["gaps"].append({"code": "used_dimensions_missing", "severity": "warning", "xml": {"rows": xml_used[0], "columns": xml_used[1]}})
elif sql_used != xml_used:
result["gaps"].append(
{
"code": "used_dimensions_mismatch",
"severity": "warning",
"sql": {"rows": sql_used[0], "columns": sql_used[1]},
"xml": {"rows": xml_used[0], "columns": xml_used[1]},
}
)
checks = [
("cells", "cells_missing_or_limited"),
("parameters", "parameters_missing_or_limited"),
("merges", "merged_ranges_missing"),
("format_indexes", "format_indexes_missing_or_limited"),
]
for key, code in checks:
xml_value = xml_counts.get(key)
sql_value = sql_counts.get(key)
if not xml_value:
continue
if sql_value in {None, 0}:
result["gaps"].append({"code": code, "severity": "warning", "sql": sql_value, "xml": xml_value})
elif int(sql_value) < int(xml_value):
result["gaps"].append({"code": code, "severity": "info", "sql": sql_value, "xml": xml_value})
hint_count = int(sql_counts.get("cell_coordinate_hints") or 0)
xml_cells = int(xml_counts.get("cells") or 0)
if xml_cells and hint_count:
result.setdefault("progress", []).append(
{
"code": "cell_coordinate_hints_available",
"sql": hint_count,
"xml_cells": xml_cells,
"coverage_ratio": round(hint_count / xml_cells, 4),
"message": "SQL decoder returned coordinate hints. These are progress evidence, not authoritative decoded cells.",
}
)
merge_block_count = int(sql_counts.get("merge_record_block_candidates") or 0)
merge_count_hint_count = int(sql_counts.get("merge_count_hints") or 0)
xml_merges = int(xml_counts.get("merges") or 0)
if xml_merges and merge_block_count:
result.setdefault("progress", []).append(
{
"code": "merge_record_blocks_available",
"sql": merge_block_count,
"xml_merges": xml_merges,
"message": "SQL decoder found MOXCEL merge-record block candidates. These are progress evidence, not authoritative merged ranges.",
}
)
if xml_merges and merge_count_hint_count:
result.setdefault("progress", []).append(
{
"code": "merge_count_hints_available",
"sql": merge_count_hint_count,
"xml_merges": xml_merges,
"message": "SQL decoder found MOXCEL merge-count hints. These confirm merge presence/count slots, not authoritative merged ranges.",
}
)
if xml_merges and merge_record_analysis["blocks"]:
result.setdefault("progress", []).append(
{
"code": "merge_record_analysis_available",
"sql_blocks": merge_record_analysis["blocks"],
"sql_records_analyzed": merge_record_analysis["records"],
"xml_merges": xml_merges,
"message": "SQL decoder returned normalized merge-block record analysis for slot-formula discovery.",
}
)
if result["gaps"]:
result["status"] = "gap"
return result
def compare(sql_profile: dict[str, Any], xml_profile: dict[str, Any]) -> dict[str, Any]:
sql_items = [item for item in sql_profile.get("items") or [] if isinstance(item, dict)]
sql_by_key = {sql_key(item): item for item in sql_items}
comparisons = []
for xml_item in xml_profile.get("templates") or []:
if not isinstance(xml_item, dict) or xml_item.get("xml_kind") != "tabular_document":
continue
route = xml_route(xml_item)
xml_item = {**xml_item, "route": route}
key = xml_key(xml_item)
sql_item = sql_by_key.get(key)
if sql_item is None and key[2]:
matches = [item for item in sql_items if sql_key(item)[2] == key[2]]
sql_item = matches[0] if len(matches) == 1 else None
comparisons.append(compare_item(sql_item, xml_item))
gap_counts: dict[str, int] = {}
progress_counts: dict[str, int] = {}
for item in comparisons:
for gap in item.get("gaps") or []:
code = str(gap.get("code") or "unknown")
gap_counts[code] = gap_counts.get(code, 0) + 1
for progress in item.get("progress") or []:
code = str(progress.get("code") or "unknown")
progress_counts[code] = progress_counts.get(code, 0) + 1
return {
"schema": "codex_1c_template_sql_xml_profile_compare.v1",
"source": "analysis_only_xml_fixture",
"sql_profile": sql_profile.get("base_id") or sql_profile.get("schema"),
"xml_profile": xml_profile.get("root"),
"comparisons": comparisons,
"counts": {
"xml_tabular_templates": len(comparisons),
"matched": sum(1 for item in comparisons if item.get("sql")),
"missing_sql_profile": sum(1 for item in comparisons if item.get("status") == "missing_sql_profile"),
"with_gaps": sum(1 for item in comparisons if item.get("gaps")),
"gap_counts": gap_counts,
"progress_counts": progress_counts,
},
}
def render_markdown(payload: dict[str, Any]) -> str:
lines = ["# 1C Template SQL/XML Profile Compare", ""]
counts = payload.get("counts") or {}
lines.append(f"- XML tabular templates: `{counts.get('xml_tabular_templates')}`")
lines.append(f"- Matched SQL profiles: `{counts.get('matched')}`")
lines.append(f"- With gaps: `{counts.get('with_gaps')}`")
lines.append(f"- Progress signals: `{counts.get('progress_counts') or {}}`")
lines.append("")
lines.append(
"| Owner | Template | Status | SQL capacity | XML capacity | SQL used | XML used | Used delta | SQL cells | Coord hints | Merge blocks | Merge count hints | Merge records analyzed | Gaps | Progress |"
)
lines.append("| --- | --- | --- | --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | --- | --- |")
for item in payload.get("comparisons") or []:
route = item.get("route") or {}
sql = item.get("sql") or {}
xml = item.get("xml") or {}
sql_counts = sql.get("counts") or {}
merge_record_analysis = sql.get("merge_record_analysis") or {}
gaps = ", ".join(str(gap.get("code")) for gap in (item.get("gaps") or []))
progress = ", ".join(str(entry.get("code")) for entry in (item.get("progress") or []))
lines.append(
f"| `{route.get('owner_kind') or ''}.{route.get('owner_name') or ''}` | "
f"`{route.get('template_name') or ''}` | `{item.get('status')}` | "
f"`{format_dimensions(sql.get('capacity_dimensions'))}` | "
f"`{format_dimensions(xml.get('capacity_dimensions'))}` | "
f"`{format_dimensions(sql.get('used_dimensions'))}` | "
f"`{format_dimensions(xml.get('used_dimensions'))}` | "
f"`{format_delta(sql.get('used_delta'))}` | "
f"{sql_counts.get('cells') or 0} | {sql_counts.get('cell_coordinate_hints') or 0} | "
f"{sql_counts.get('merge_record_block_candidates') or 0} | {sql_counts.get('merge_count_hints') or 0} | "
f"{merge_record_analysis.get('records') or 0} | "
f"`{gaps}` | `{progress}` |"
)
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description="Compare SQL-decoded template profiles with XML analysis fixtures.")
parser.add_argument("--sql-profile", default="reports/1c-template-baselines/upo_test_tabular_template_profiles.json")
parser.add_argument("--xml-profile", required=True)
parser.add_argument("--output-json", default="reports/1c-template-baselines/sql-xml-template-profile-compare.json")
parser.add_argument("--output-markdown", default="reports/1c-template-baselines/sql-xml-template-profile-compare.md")
args = parser.parse_args()
payload = compare(read_json(Path(args.sql_profile)), read_json(Path(args.xml_profile)))
json_path = Path(args.output_json)
md_path = Path(args.output_markdown)
json_path.parent.mkdir(parents=True, exist_ok=True)
md_path.parent.mkdir(parents=True, exist_ok=True)
json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
md_path.write_text(render_markdown(payload), encoding="utf-8")
print(json.dumps({"status": "ok", "json": str(json_path), "markdown": str(md_path), "counts": payload["counts"]}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())