From 11060f7db1c0de8e5767de19aef14d8ab4a6b4bd Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Sun, 26 Jul 2026 17:39:06 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20superseded=20=EC=83=81=ED=83=9C=EB=A5=BC?= =?UTF-8?q?=20check=EC=97=90=EC=84=9C=20=EC=9D=B8=EC=8B=9D=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- factlog/common.py | 4 ++++ tests/test_check_empty_policy.sh | 22 ++++++++++++++++++++++ tools/run_logic_check.py | 3 ++- tools/validate.py | 5 ++--- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/factlog/common.py b/factlog/common.py index 41c7fc8d..677fd6ef 100644 --- a/factlog/common.py +++ b/factlog/common.py @@ -108,6 +108,10 @@ def _atomic_write_text(path: Path, text: str) -> None: # Superseded rows are retained in candidates.csv for audit but are NOT engine # input (they never reach accepted.dl) and are ignored by conflict detection. SUPERSEDED_STATUSES = {"superseded"} +# Every lifecycle status that may legitimately appear in candidates.csv. Keep +# this derived from the policy-specific groups so validation and diagnostics do +# not mistake a retired (non-engine) fact for an unknown status. +KNOWN_STATUSES = ENGINE_STATUSES | REVIEW_STATUSES | SUPERSEDED_STATUSES QUERY_PREDICATES = {"relation", "path", "count", "conflict", "review_required"} RELATION_FACT_RE = re.compile(r"^relation\((.*)\)\.$") # 1.0.3 is the floor: it bundles/validates wirelog v0.52.0, the first release diff --git a/tests/test_check_empty_policy.sh b/tests/test_check_empty_policy.sh index bbb848c0..74696d69 100755 --- a/tests/test_check_empty_policy.sh +++ b/tests/test_check_empty_policy.sh @@ -21,6 +21,7 @@ PLUGIN_ROOT="$(cd "$(dirname "$0")/.." && pwd)" export PYTHONPATH="$PLUGIN_ROOT${PYTHONPATH:+:$PYTHONPATH}" PYTHON="${PYTHON:-python3}" RLC="$PLUGIN_ROOT/tools/run_logic_check.py" +COMPILE="$PLUGIN_ROOT/tools/compile_facts.py" pass=0 fail=0 @@ -53,6 +54,27 @@ out="$(FACTLOG_ROOT="$KB" "$PYTHON" "$RLC" 2>&1)"; rc=$? [ -f "$KB/facts/logic_report.txt" ] && ok "logic_report.txt produced" || bad "logic_report.txt missing" grep -qF "policy findings: 0" "$KB/facts/logic_report.txt" && ok "report shows 'policy findings: 0'" || bad "report missing 'policy findings: 0'" +# --- 1a. lifecycle status contract: retired is known, invalid still warns ---- +# `superseded` is a durable audit row, not engine input. It must not be +# reported as unknown merely because it is intentionally excluded from +# accepted.dl; a genuinely unrecognised value must remain visible. +printf '%s\n%s\n%s\n%s\n' \ + 'subject,relation,object,source,status,confidence,note' \ + 'A,uses,B,sources/x.md,confirmed,0.9,' \ + 'Old,uses,C,sources/x.md,superseded,0.9,retired' \ + 'Broken,uses,D,sources/x.md,not_a_status,0.9,' > "$KB/facts/candidates.csv" +out_compile="$(FACTLOG_ROOT="$KB" "$PYTHON" "$COMPILE" 2>&1)"; rc_compile=$? +[ "$rc_compile" -eq 0 ] && ok "compile_facts accepts known retired status" || bad "compile_facts exited $rc_compile: $out_compile" +grep -qF '"Old"' "$KB/facts/accepted.dl" && bad "superseded row leaked into accepted.dl" || ok "superseded row remains outside accepted.dl" +out_status="$(FACTLOG_ROOT="$KB" "$PYTHON" "$RLC" 2>&1)"; rc_status=$? +[ "$rc_status" -eq 0 ] && ok "run_logic_check accepts lifecycle statuses" || bad "run_logic_check exited $rc_status: $out_status" +grep -qF 'unknown status treated as non-engine input: superseded' "$KB/facts/logic_report.txt" \ + && bad "superseded is still reported as unknown" \ + || ok "superseded is recognised as a retired lifecycle status" +grep -qF 'unknown status treated as non-engine input: not_a_status' "$KB/facts/logic_report.txt" \ + && ok "unrecognised status still produces a warning" \ + || bad "unrecognised status warning disappeared" + # --- 2. ask/check consistency: 0 policy predicates on the same KB ------------- preds="$(FACTLOG_ROOT="$KB" "$PYTHON" -c "from factlog import common; print(len(common.policy_predicates(common.load_logic_policy())))" 2>&1)" [ "$preds" = "0" ] && ok "ask path sees 0 policy predicates (empty policy)" || bad "expected 0 policy predicates, got: $preds" diff --git a/tools/run_logic_check.py b/tools/run_logic_check.py index 320236f8..2a180db4 100644 --- a/tools/run_logic_check.py +++ b/tools/run_logic_check.py @@ -6,6 +6,7 @@ from common import ( FACTS_DIR, + KNOWN_STATUSES, QUERY_PREDICATES, allowed_relations, dependency_path, @@ -173,7 +174,7 @@ def main() -> None: for row in candidates: if not row["subject"] or not row["relation"] or not row["object"]: errors.append(f"incomplete fact row: {row}") - if row["status"] not in {"confirmed", "accepted", "needs_review", "candidate"}: + if row["status"] not in KNOWN_STATUSES: warnings.append(f"unknown status treated as non-engine input: {row['status']}") for predicate in sorted(policy_query_predicates): diff --git a/tools/validate.py b/tools/validate.py index 40df1430..d63a8300 100644 --- a/tools/validate.py +++ b/tools/validate.py @@ -12,9 +12,8 @@ import sys from pathlib import Path +from common import FACT_HEADER, KNOWN_STATUSES -FACT_HEADER = ["subject", "relation", "object", "source", "status", "confidence", "note"] -VALID_STATUSES = {"confirmed", "accepted", "needs_review", "candidate", "superseded"} def read(path: Path) -> str: @@ -189,7 +188,7 @@ def validate(root: Path) -> list[str]: if reader.fieldnames != FACT_HEADER: errors.append(f"facts/candidates.csv header must be {','.join(FACT_HEADER)}") for idx, row in enumerate(rows, start=2): - if row.get("status") not in VALID_STATUSES: + if row.get("status") not in KNOWN_STATUSES: errors.append(f"facts/candidates.csv line {idx} invalid status: {row.get('status')!r}") confidence_error = validate_confidence(row.get("confidence", "")) if confidence_error: