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,160 @@
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 diff_named_range(change: dict[str, Any] | None) -> dict[str, Any]:
change = change or {}
one_based = change.get("one_based") if isinstance(change.get("one_based"), dict) else {}
raw_scalars = change.get("raw_scalars") if isinstance(change.get("raw_scalars"), dict) else {}
before_coords = one_based.get("before") if isinstance(one_based.get("before"), dict) else {}
after_coords = one_based.get("after") if isinstance(one_based.get("after"), dict) else {}
before_raw = raw_scalars.get("before") if isinstance(raw_scalars.get("before"), list) else []
after_raw = raw_scalars.get("after") if isinstance(raw_scalars.get("after"), list) else []
changed_fields: list[int] = []
for index, (before, after) in enumerate(zip(before_raw, after_raw)):
if before != after:
changed_fields.append(index)
return {
"before_coords": before_coords,
"after_coords": after_coords,
"before_raw": before_raw,
"after_raw": after_raw,
"changed_scalar_indexes": changed_fields,
}
def classify_transition(transition: dict[str, Any]) -> dict[str, Any]:
named = diff_named_range(transition.get("named_range_changes") if isinstance(transition.get("named_range_changes"), dict) else {})
before_coords = named["before_coords"]
after_coords = named["after_coords"]
text_changes = transition.get("text_match_changes") if isinstance(transition.get("text_match_changes"), dict) else {}
before_next = (text_changes.get("next_moxel_record") or {}).get("before")
after_next = (text_changes.get("next_moxel_record") or {}).get("after")
count_changes = transition.get("count_changes") if isinstance(transition.get("count_changes"), dict) else {}
top_before = before_coords.get("row_start")
top_after = after_coords.get("row_start")
left_before = before_coords.get("column_start")
left_after = after_coords.get("column_start")
labels: list[str] = []
explanation: list[str] = []
if top_before is not None and top_after is not None and left_before is not None and left_after is not None:
if top_before != top_after and left_before == left_after:
labels.append("row_move")
explanation.append("Named range moved vertically while column stayed stable.")
if left_before != left_after and top_before == top_after:
labels.append("column_move")
explanation.append("Named range moved horizontally while row stayed stable.")
if left_before == left_after and top_before == top_after and before_coords:
labels.append("same_named_range_coordinates")
explanation.append("Named range coordinates stayed stable.")
if isinstance(before_next, dict) and isinstance(after_next, dict):
if before_next.get("type") != after_next.get("type"):
labels.append("next_record_type_change")
explanation.append("Text-cell next_moxel_record changed node type.")
if before_next.get("head") != after_next.get("head"):
labels.append("next_record_head_change")
explanation.append("Text-cell next_moxel_record changed list head.")
if before_next.get("scalar_prefix") != after_next.get("scalar_prefix") and before_next.get("head") == after_next.get("head"):
labels.append("next_record_payload_change")
explanation.append("Text-cell next_moxel_record kept the same head but changed payload.")
elif before_next != after_next:
labels.append("next_record_presence_change")
explanation.append("Text-cell next_moxel_record appeared/disappeared or changed from scalar to structured form.")
if "cell_style_candidates" in count_changes:
labels.append("candidate_count_change")
explanation.append("The number of visible inline text/style candidates changed.")
target_signal_change_keys = {
"cell_id",
"tree_position",
"next_moxel_record",
"immediate_preceding_values",
"last_7_preceding_values",
}
target_signal_changed = any(key in text_changes for key in target_signal_change_keys)
named_coordinates_changed = bool(before_coords or after_coords)
bytes_changed = transition.get("from_bytes") != transition.get("to_bytes")
if bytes_changed and not named_coordinates_changed and not target_signal_changed and not count_changes:
labels.append("tracked_signature_unchanged")
explanation.append("Template bytes changed, but tracked coordinates and text-cell signature stayed stable.")
if not labels:
labels.append("uncategorized")
explanation.append("No simple heuristic label matched this transition.")
return {
"from_file": transition.get("from_file"),
"to_file": transition.get("to_file"),
"labels": labels,
"explanation": explanation,
"named_range_diff": named,
"next_moxel_record_before": before_next,
"next_moxel_record_after": after_next,
"count_changes": count_changes,
}
def summarize_patterns(classified: list[dict[str, Any]]) -> dict[str, Any]:
label_counts: dict[str, int] = {}
for item in classified:
for label in item.get("labels") or []:
label_counts[label] = label_counts.get(label, 0) + 1
return {
"label_counts": [{"label": label, "count": count} for label, count in sorted(label_counts.items(), key=lambda pair: (-pair[1], pair[0]))]
}
def recommend_next_steps(classified: list[dict[str, Any]]) -> list[str]:
labels = {label for item in classified for label in (item.get("labels") or [])}
steps: list[str] = []
if "row_move" in labels and "column_move" in labels:
steps.append("Coordinates for named cell/range are already proven; prioritize property-only changes on the same cell.")
if "next_record_head_change" in labels or "next_record_type_change" in labels:
steps.append("Change one presentation/property setting on `Ячейка 7 - 2` without moving it: `ВертикальноеПоложение`, then `ГоризонтальноеПоложение`, then `Защита`, then `Гиперссылка`.")
if "candidate_count_change" in labels:
steps.append("Avoid adding/removing neighbor cells during property experiments; that changes the candidate count and adds noise.")
if "tracked_signature_unchanged" in labels:
steps.append("When a save changes template bytes but leaves the tracked signature untouched, the edited property is either stored elsewhere or belongs to another cell record; capture one more isolated property change on the same cell before widening the search.")
if not steps:
steps.append("Continue with one-property-per-save experiments on the same tracked cell.")
return steps
def main() -> int:
parser = argparse.ArgumentParser(description="Infer heuristic property candidates from a 1C template history matrix.")
parser.add_argument("matrix_json", help="Path to JSON generated by analyze_1c_template_history_matrix.py")
parser.add_argument("--output", help="Optional JSON output path.")
args = parser.parse_args()
matrix = read_json(Path(args.matrix_json))
transitions = matrix.get("transitions") or []
classified = [classify_transition(item) for item in transitions if isinstance(item, dict)]
payload = {
"schema": "codex_1c_template_property_candidates.v1",
"source": args.matrix_json,
"classified_transitions": classified,
"pattern_summary": summarize_patterns(classified),
"recommended_next_steps": recommend_next_steps(classified),
}
rendered = json.dumps(payload, ensure_ascii=False, indent=2)
if args.output:
Path(args.output).write_text(rendered, encoding="utf-8")
print(rendered)
return 0
if __name__ == "__main__":
raise SystemExit(main())