Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user