Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions factlog/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions tests/test_check_empty_policy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion tools/run_logic_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from common import (
FACTS_DIR,
KNOWN_STATUSES,
QUERY_PREDICATES,
allowed_relations,
dependency_path,
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 2 additions & 3 deletions tools/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down