Skip to content

Commit 8f2f4b4

Browse files
sandeep-agamiclaude
andcommitted
reconcile: the review panel's fixes
Spec: ACE-128 Three review passes over #304 to #310. Must-fixes: a "same answer, different query" row was never offered for keep (keep_allowed is the gate's word, whatever the fix); a date window the ledger had confirmed still read "could not check" and was counted twice (a claim the ledger graded takes the ledger's word, and a check counts once); the relative-window fold recursed without a depth budget (eight steps, past which it reads None, so read_claims keeps its promise never to raise). Nits taken: resume refuses a corrupt checkpoint the way next-chunk does; a definitional claim that could not be read makes the query fact "not comparable" rather than "same"; a match where agami returned an extra column stays a match, with the sentence naming the column; a claim that differs between the two queries is its own state (amber, ≠), so red keeps one meaning; the intake parser refuses a rows file that is not the intake's output and ties the block to the run named by --out; intake refuses a file over 20 MB and a CSV cell over the default field limit with one line, never a traceback; the page shows only the first line of an error; an example is never offered on a row whose ledger holds a mistake (example_not_offered, read from ledger.json); the fix words live in one place (the items); the Node page test asserts on CI; three titles lose an em-dash; a tautological assertion, a duplicated check and two comment periods. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 179fe34 commit 8f2f4b4

17 files changed

Lines changed: 157 additions & 56 deletions

packages/agami-core/src/semantic_model/comparator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,8 @@ def _column_pairs(
653653
ordered=ordered, quantize=match == "values",
654654
)
655655
except Exception:
656+
# The score itself has already reported a ragged or malformed result as an error with a
657+
# value-free reason; the pairing is a courtesy on top and must never turn that into a raise.
656658
return (), ()
657659
pairs = tuple((golden.columns[g], generated.columns[i]) for g, i in sorted(pairing.items()))
658660
extra = tuple(name for i, name in enumerate(generated.columns) if i not in set(pairing.values()))

packages/agami-core/src/semantic_model/golden_claims.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,10 @@ def _unit_name(node: "exp.Expression | None") -> Optional[str]:
470470
return _RELATIVE_UNITS.get(str(text).strip().strip("'\"").lower())
471471

472472

473-
def _relative_bound(node: "exp.Expression | None") -> Optional[str]:
473+
_MAX_RELATIVE_DEPTH = 8
474+
475+
476+
def _relative_bound(node: "exp.Expression | None", depth: int = _MAX_RELATIVE_DEPTH) -> Optional[str]:
474477
"""The bound a node spells RELATIVE to the run date, as words: `today`, `start of this year`,
475478
`start of this year + 7 month`, `today - 30 day`. None for any other shape.
476479
@@ -480,36 +483,38 @@ def _relative_bound(node: "exp.Expression | None") -> Optional[str]:
480483
date it computed would be a bound neither statement wrote. It is compared only against another
481484
relative bound (`_window_status`), never against a literal date.
482485
"""
483-
if node is None:
486+
if node is None or depth <= 0:
487+
# A relative bound deeper than a handful of steps is not a window anyone wrote; past the
488+
# budget it reads None, so `read_claims` keeps its promise never to raise on a pathological tree.
484489
return None
485490
if isinstance(node, exp.Paren):
486-
return _relative_bound(node.this)
491+
return _relative_bound(node.this, depth - 1)
487492
if isinstance(node, exp.Cast):
488-
return _relative_bound(node.this)
493+
return _relative_bound(node.this, depth - 1)
489494
if isinstance(node, exp.CurrentDate):
490495
return "today"
491496
if isinstance(node, exp.CurrentTimestamp) or (isinstance(node, exp.Anonymous) and str(node.this).lower() in ("now", "getdate", "sysdate", "current_timestamp")):
492497
return "now"
493498
if isinstance(node, (exp.DateTrunc, exp.TimestampTrunc)):
494-
inner = _relative_bound(node.this if isinstance(node, exp.TimestampTrunc) else node.args.get("this"))
499+
inner = _relative_bound(node.this if isinstance(node, exp.TimestampTrunc) else node.args.get("this"), depth - 1)
495500
unit = _unit_name(node.args.get("unit"))
496501
if isinstance(node, exp.DateTrunc):
497502
# sqlglot's DateTrunc holds the unit in `unit` and the value in `this`; some dialects
498503
# parse the argument order the other way round, so both are tried.
499-
inner = _relative_bound(node.this) or _relative_bound(node.args.get("unit"))
504+
inner = _relative_bound(node.this, depth - 1) or _relative_bound(node.args.get("unit"), depth - 1)
500505
unit = _unit_name(node.args.get("unit")) or _unit_name(node.this)
501506
if inner in ("today", "now") and unit:
502507
return f"start of this {unit}"
503508
return None
504509
if isinstance(node, (exp.Add, exp.Sub)):
505-
base = _relative_bound(node.this)
510+
base = _relative_bound(node.this, depth - 1)
506511
step = _interval_words(node.expression)
507512
if base and step:
508513
return _step(base, isinstance(node, exp.Add), *step)
509514
return None
510515
date_add_types = tuple(t for t in (exp.DateAdd, getattr(exp, "TsOrDsAdd", None)) if t is not None)
511516
if isinstance(node, date_add_types + (exp.DateSub,)):
512-
base = _relative_bound(node.this)
517+
base = _relative_bound(node.this, depth - 1)
513518
n = node.expression
514519
unit = _unit_name(node.args.get("unit"))
515520
count: Optional[int] = None

plugins/agami/scripts/parse_reconcile_intake.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
_KEYS = {"profile", "reconcile-run", "intake"}
3131

3232

33-
def _key_of(line: str):
33+
def _key_of(line: str) -> str | None:
3434
low = line.strip().lower()
3535
for k in _KEYS:
3636
if low.startswith(k + ":") or low == k + ":":
@@ -144,8 +144,15 @@ def main(argv=None) -> int:
144144
print(json.dumps({"ok": False, "data": None, "anomalies": [{"kind": "bad_argument", "detail": str(exc)}],
145145
"needs_judgment": {"kind": "bad_argument", "ask": "pass the rows file `reconcile.py intake` wrote and the pasted block"}}, indent=2))
146146
return 2
147+
if not isinstance(intake, dict) or not isinstance(intake.get("rows"), list):
148+
print(json.dumps({"ok": False, "data": None, "anomalies": [{"kind": "bad_argument", "detail": "the rows file is not the output of `reconcile.py intake`"}],
149+
"needs_judgment": {"kind": "bad_argument", "ask": "pass the rows file `reconcile.py intake` wrote (an object with a `rows` list)"}}, indent=2))
150+
return 2
147151
known = {r.get("row", n) for n, r in enumerate(intake.get("rows", []), 1)}
148-
data, anomalies, needs = parse(text, known, run=args.run)
152+
# The block must name the run it is applied to. When --run is not given, the run is the folder
153+
# --out lands in, so a block from another run can never be applied by leaving the flag off.
154+
run = args.run or (Path(args.out).expanduser().resolve().parent.name if args.out else None)
155+
data, anomalies, needs = parse(text, known, run=run)
149156
counts = None
150157
if needs is None:
151158
applied, counts = apply(intake, data["decisions"])

plugins/agami/scripts/parse_reconcile_report.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,27 @@
4343
_FIELDS = ("row", "decision", "words")
4444
_DROPPED_KINDS = frozenset({"unknown_decision", "decision_missing_row", "row_decided_twice",
4545
"decision_not_an_object", "keep_not_offered", "words_ignored_on_keep",
46-
"words_ignored_on_nothing", "words_not_text"})
46+
"words_ignored_on_nothing", "words_not_text", "example_not_offered"})
47+
48+
49+
def example_blocked_rows(run_dir: Path) -> set[int]:
50+
"""Rows whose ledger holds a part the data proved wrong: a statement with a mistake in it is never
51+
offered as a prompt example, whatever the page suggested. Read from each row's ledger.json."""
52+
blocked: set[int] = set()
53+
rows_dir = run_dir / "rows"
54+
if not rows_dir.is_dir():
55+
return blocked
56+
for row_dir in rows_dir.iterdir():
57+
ledger = row_dir / "ledger.json"
58+
if not row_dir.name.isdigit() or not ledger.exists():
59+
continue
60+
try:
61+
parts = json.loads(ledger.read_text(encoding="utf-8")).get("rows", [])
62+
except (OSError, ValueError):
63+
continue
64+
if any(isinstance(p, dict) and p.get("verdict") == "query_defect" for p in parts):
65+
blocked.add(int(row_dir.name))
66+
return blocked
4767

4868

4969
def keepable_rows(run_dir: Path) -> set[int]:
@@ -81,7 +101,7 @@ def keepable_rows(run_dir: Path) -> set[int]:
81101
return keep
82102

83103

84-
def _key_of(line: str):
104+
def _key_of(line: str) -> str | None:
85105
low = line.strip().lower()
86106
for k in _KEYS:
87107
if low.startswith(k + ":") or low == k + ":":
@@ -112,7 +132,8 @@ def _sections(text: str) -> tuple[dict, list[str]]:
112132
return out, repeated
113133

114134

115-
def parse(text: str, keepable: set[int] | None = None, run: str | None = None) -> tuple[dict, list, dict | None]:
135+
def parse(text: str, keepable: set[int] | None = None, run: str | None = None,
136+
example_blocked: set[int] | None = None) -> tuple[dict, list, dict | None]:
116137
sec, repeated = _sections(text)
117138
anomalies: list = [{"kind": "key_repeated", "detail": key} for key in repeated]
118139
needs: dict | None = None
@@ -156,6 +177,9 @@ def parse(text: str, keepable: set[int] | None = None, run: str | None = None) -
156177
if row in seen:
157178
anomalies.append({"kind": "row_decided_twice", "row": row})
158179
continue
180+
if decision == "example" and row in (example_blocked or set()):
181+
anomalies.append({"kind": "example_not_offered", "row": row})
182+
continue # dropped like a keep the run did not offer; _DROPPED_KINDS carries the kind
159183
if decision == "keep" and row not in (keepable or set()):
160184
# The offer's predicate belongs to the ledger: a keep the run's own files do not
161185
# allow is not the person's to grant from a page.
@@ -200,7 +224,7 @@ def main(argv=None) -> int:
200224
print(json.dumps({"ok": False, "data": None, "anomalies": [{"kind": "bad_argument", "detail": str(exc)}],
201225
"needs_judgment": {"kind": "bad_argument", "ask": "the block file could not be read"}}, indent=2))
202226
return 2
203-
data, anomalies, needs = parse(text, keepable_rows(run_dir), run=run_dir.name)
227+
data, anomalies, needs = parse(text, keepable_rows(run_dir), run=run_dir.name, example_blocked=example_blocked_rows(run_dir))
204228
print(json.dumps({"ok": needs is None, "data": data, "anomalies": anomalies, "needs_judgment": needs}, indent=2))
205229
return 0
206230

0 commit comments

Comments
 (0)