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
+350
View File
@@ -0,0 +1,350 @@
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
from typing import Any
MARKER_RE = re.compile(r"^(\d+)-(\d+)$")
def read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def as_int(value: Any) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None
def tree_index(value: Any) -> int:
text = str(value or "")
if text.startswith("$."):
text = text[2:]
first = text.split(".", 1)[0]
return as_int(first) if as_int(first) is not None else 10**9
def marker_rows_from_payload(payload: dict[str, Any]) -> list[dict[str, Any]]:
if isinstance(payload.get("rows"), list):
return [row for row in payload["rows"] if isinstance(row, dict)]
probe = payload.get("probe") if isinstance(payload.get("probe"), dict) else {}
rows = probe.get("marker_matrix") if isinstance(probe.get("marker_matrix"), list) else []
result: list[dict[str, Any]] = []
for row in rows:
if not isinstance(row, dict):
continue
styles = row.get("styles") if isinstance(row.get("styles"), list) else []
style = styles[0] if styles and isinstance(styles[0], dict) else {}
next_record = style.get("next_moxel_record") if isinstance(style.get("next_moxel_record"), dict) else {}
result.append(
{
"text": row.get("text"),
"expected_row": row.get("expected_row"),
"expected_col": row.get("expected_col"),
"decoded_row": row.get("decoded_row"),
"decoded_col": row.get("decoded_col"),
"style_tree_position": style.get("tree_position"),
"next_value": next_record.get("value") or next_record.get("head"),
"preceding": style.get("last_7_preceding_values") or style.get("immediate_preceding_values"),
"row_ok": row.get("row_ok"),
"col_ok": row.get("col_ok"),
}
)
return result
def last_preceding_int(row: dict[str, Any]) -> int | None:
preceding = row.get("preceding") if isinstance(row.get("preceding"), list) else []
for value in reversed(preceding):
parsed = as_int(value)
if parsed is not None:
return parsed
return None
def analyze_marker_matrix(payloads: list[dict[str, Any]]) -> dict[str, Any]:
rows: list[dict[str, Any]] = []
for payload in payloads:
rows.extend(marker_rows_from_payload(payload))
marker_rows = [row for row in rows if MARKER_RE.match(str(row.get("text") or ""))]
total = len(marker_rows)
row_ok = sum(1 for row in marker_rows if row.get("row_ok") is True or row.get("decoded_row") == row.get("expected_row"))
col_ok = sum(1 for row in marker_rows if row.get("col_ok") is True or row.get("decoded_col") == row.get("expected_col"))
last_preceding_predictions = []
for row in marker_rows:
expected_col = as_int(row.get("expected_col"))
last = last_preceding_int(row)
if expected_col is None or last is None:
continue
predicted = last + 1
last_preceding_predictions.append(
{
"text": row.get("text"),
"style_tree_position": row.get("style_tree_position"),
"expected_col": expected_col,
"predicted_col": predicted,
"ok": predicted == expected_col,
"preceding_last": last,
"current_decoded_col": row.get("decoded_col"),
}
)
prediction_total = len(last_preceding_predictions)
prediction_ok = sum(1 for item in last_preceding_predictions if item.get("ok") is True)
rows_by_expected_row: dict[int, list[dict[str, Any]]] = {}
for row in marker_rows:
expected_row = as_int(row.get("expected_row"))
if expected_row is None:
continue
rows_by_expected_row.setdefault(expected_row, []).append(row)
row_runs = []
for expected_row, run_rows in sorted(rows_by_expected_row.items()):
sorted_rows = sorted(run_rows, key=lambda item: tree_index(item.get("style_tree_position")))
expected_cols = [as_int(row.get("expected_col")) for row in sorted_rows]
predicted_cols = [(last_preceding_int(row) + 1) if last_preceding_int(row) is not None else None for row in sorted_rows]
row_runs.append(
{
"expected_row": expected_row,
"texts": [row.get("text") for row in sorted_rows],
"style_positions": [row.get("style_tree_position") for row in sorted_rows],
"expected_columns": expected_cols,
"predicted_columns": predicted_cols,
"sequence_ok": predicted_cols == expected_cols,
}
)
confidence = "none"
if prediction_total and prediction_ok == prediction_total and prediction_total >= 3:
confidence = "high"
elif prediction_total and prediction_ok / prediction_total >= 0.8:
confidence = "medium"
elif prediction_total:
confidence = "low"
return {
"marker_rows": total,
"current_decoder": {
"row_accuracy": {"ok": row_ok, "total": total},
"column_accuracy": {"ok": col_ok, "total": total},
},
"candidate_rules": [
{
"id": "inline_text_column_from_last_preceding_scalar_plus_one",
"target": "moxel.inline_text_cell.column",
"expression": "one_based_column = int(last_numeric(preceding_scalars)) + 1",
"confidence": confidence,
"evidence": {"ok": prediction_ok, "total": prediction_total},
"samples": last_preceding_predictions[:20],
}
],
"row_runs": row_runs,
}
def raw_scalar_diffs(before: list[Any], after: list[Any]) -> list[int]:
return [index for index, pair in enumerate(zip(before, after)) if pair[0] != pair[1]]
def infer_named_range_rules(history_matrix: dict[str, Any], property_candidates: dict[str, Any]) -> dict[str, Any]:
coordinate_fields: dict[str, set[int]] = {"left": set(), "right": set(), "top": set(), "bottom": set()}
evidence: list[dict[str, Any]] = []
transitions: list[dict[str, Any]] = []
transitions.extend([item for item in history_matrix.get("transitions") or [] if isinstance(item, dict)])
transitions.extend([item for item in property_candidates.get("classified_transitions") or [] if isinstance(item, dict)])
for transition in transitions:
named = transition.get("named_range_changes") if isinstance(transition.get("named_range_changes"), dict) else {}
if "named_range_diff" in transition and isinstance(transition["named_range_diff"], dict):
named = {
"one_based": {
"before": transition["named_range_diff"].get("before_coords") or {},
"after": transition["named_range_diff"].get("after_coords") or {},
},
"raw_scalars": {
"before": transition["named_range_diff"].get("before_raw") or [],
"after": transition["named_range_diff"].get("after_raw") or [],
},
}
coords = named.get("one_based") if isinstance(named.get("one_based"), dict) else {}
raw = named.get("raw_scalars") if isinstance(named.get("raw_scalars"), dict) else {}
before_coords = coords.get("before") if isinstance(coords.get("before"), dict) else {}
after_coords = coords.get("after") if isinstance(coords.get("after"), dict) else {}
before_raw = raw.get("before") if isinstance(raw.get("before"), list) else []
after_raw = raw.get("after") if isinstance(raw.get("after"), list) else []
changed = raw_scalar_diffs(before_raw, after_raw)
if not changed:
continue
changed_coord_names = [
name
for name in ("left", "right", "top", "bottom", "row_start", "row_end", "column_start", "column_end")
if before_coords.get(name) != after_coords.get(name)
]
for name in changed_coord_names:
if name in {"left", "column_start"}:
coordinate_fields["left"].update(changed)
elif name in {"right", "column_end"}:
coordinate_fields["right"].update(changed)
elif name in {"top", "row_start"}:
coordinate_fields["top"].update(changed)
elif name in {"bottom", "row_end"}:
coordinate_fields["bottom"].update(changed)
evidence.append(
{
"from_file": transition.get("from_file"),
"to_file": transition.get("to_file"),
"changed_raw_indexes": changed,
"changed_coordinates": changed_coord_names,
}
)
rules = []
for target, indexes in coordinate_fields.items():
if not indexes:
continue
rules.append(
{
"target": f"moxel.named_range.{target}",
"raw_scalar_indexes": sorted(indexes),
"expression": "one_based = int(raw_scalar) + 1",
"confidence": "high" if len(indexes) == 1 else "medium",
}
)
return {"rules": rules, "evidence": evidence}
def summarize_property_candidates(property_candidates: dict[str, Any]) -> dict[str, Any]:
classified = [item for item in property_candidates.get("classified_transitions") or [] if isinstance(item, dict)]
label_counts: dict[str, int] = {}
for item in classified:
for label in item.get("labels") or []:
label_counts[str(label)] = label_counts.get(str(label), 0) + 1
unmapped = [
{
"from_file": item.get("from_file"),
"to_file": item.get("to_file"),
"labels": item.get("labels") or [],
"reason": "No stable property field yet; capture one-property-per-save snapshots on the same cell.",
}
for item in classified
if "uncategorized" in (item.get("labels") or []) or "tracked_signature_unchanged" in (item.get("labels") or [])
]
return {
"label_counts": [{"label": key, "count": label_counts[key]} for key in sorted(label_counts)],
"unmapped_transitions": unmapped,
"recommended_next_steps": property_candidates.get("recommended_next_steps") or [],
}
def build_registry(
marker_payloads: list[dict[str, Any]],
history_matrix: dict[str, Any],
property_candidates: dict[str, Any],
sources: dict[str, list[str] | str | None],
) -> dict[str, Any]:
marker_analysis = analyze_marker_matrix(marker_payloads)
named_range = infer_named_range_rules(history_matrix, property_candidates)
properties = summarize_property_candidates(property_candidates)
rules = []
rules.extend(marker_analysis.get("candidate_rules") or [])
rules.extend(named_range.get("rules") or [])
return {
"schema": "codex_1c_moxel_schema_discovery.v1",
"sources": sources,
"status": "ok",
"rules": rules,
"analysis": {
"inline_text_cells": marker_analysis,
"named_ranges": named_range,
"properties": properties,
},
"next_automation": [
"Capture one-property-per-save probes for alignment, font, border, fill, protection, wrapping, merge, row height, and column width.",
"Promote high-confidence rules into the adapter decoder only after fixture tests and a disposable 1C validation run.",
"For write support, require a separate round-trip proof for every scalar path before enabling mutation.",
],
}
def render_markdown(payload: dict[str, Any]) -> str:
lines: list[str] = []
lines.append("# 1C MOXCEL schema discovery")
lines.append("")
lines.append(f"- Status: `{payload.get('status')}`")
lines.append(f"- Rules: `{len(payload.get('rules') or [])}`")
inline = ((payload.get("analysis") or {}).get("inline_text_cells") or {})
current = inline.get("current_decoder") or {}
row_acc = current.get("row_accuracy") or {}
col_acc = current.get("column_accuracy") or {}
lines.append(f"- Inline row accuracy: `{row_acc.get('ok')}/{row_acc.get('total')}`")
lines.append(f"- Inline column accuracy: `{col_acc.get('ok')}/{col_acc.get('total')}`")
lines.append("")
lines.append("## Rules")
lines.append("")
lines.append("| Rule | Target | Confidence | Evidence |")
lines.append("| --- | --- | --- | --- |")
for rule in payload.get("rules") or []:
evidence = rule.get("evidence") or {}
evidence_text = f"{evidence.get('ok')}/{evidence.get('total')}" if evidence else ", ".join(map(str, rule.get("raw_scalar_indexes") or []))
lines.append(f"| `{rule.get('id') or rule.get('target')}` | `{rule.get('target')}` | `{rule.get('confidence')}` | `{evidence_text}` |")
lines.append("")
lines.append("## Property Gaps")
lines.append("")
properties = ((payload.get("analysis") or {}).get("properties") or {})
for item in properties.get("unmapped_transitions") or []:
lines.append(f"- `{item.get('from_file')}` -> `{item.get('to_file')}`: `{', '.join(item.get('labels') or [])}`")
if not properties.get("unmapped_transitions"):
lines.append("- none")
lines.append("")
lines.append("## Next Automation")
lines.append("")
for step in payload.get("next_automation") or []:
lines.append(f"- {step}")
lines.append("")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description="Infer 1C MOXCEL decoder/property rules from captured template probes.")
parser.add_argument("--marker-matrix", action="append", default=[], help="Marker matrix JSON. Repeatable.")
parser.add_argument("--probe-snapshot", action="append", default=[], help="Probe snapshot JSON from capture_1c_template_probe.py. Repeatable.")
parser.add_argument("--history-matrix", help="History matrix JSON from analyze_1c_template_history_matrix.py.")
parser.add_argument("--property-candidates", help="Property candidate JSON from infer_1c_template_property_candidates.py.")
parser.add_argument("--output-json", default="reports/1c-template-baselines/Primer3_moxel_schema_discovery.json")
parser.add_argument("--output-markdown", default="reports/1c-template-baselines/Primer3_moxel_schema_discovery.md")
args = parser.parse_args()
marker_paths = [Path(path) for path in args.marker_matrix]
probe_paths = [Path(path) for path in args.probe_snapshot]
marker_payloads = [read_json(path) for path in [*marker_paths, *probe_paths]]
history_matrix = read_json(Path(args.history_matrix)) if args.history_matrix else {}
property_candidates = read_json(Path(args.property_candidates)) if args.property_candidates else {}
payload = build_registry(
marker_payloads,
history_matrix,
property_candidates,
{
"marker_matrix": [str(path) for path in marker_paths],
"probe_snapshot": [str(path) for path in probe_paths],
"history_matrix": args.history_matrix,
"property_candidates": args.property_candidates,
},
)
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), "rules": len(payload["rules"])}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())