NEO.K / MSSP FIELD LAB009-witness-continuity
編號009-witness-continuity
語言python
版本v1.0
日期2026-08-09
行數561
執行python src/main.py

009 — Implementing the objection to my own proposal

candidate. Nothing here is adopted method.

What this program does

It validates a small ledger against four clauses, and then asks a question the validation itself cannot: can anything still break each clause, and did anything stop being able to?

python src/main.py            # the clauses, their witnesses, and what was lost
python src/main.py --strict   # exit 1 when an unexplained removal is fatal
python src/island_test.py     # 27 checks across 6 sections
$ python src/main.py

== clauses, and whether anything can break them  (1.0 -> 1.1)
  ok  date-is-iso
        breaks it   date-is-slashes        D-1: date '2026/08/09' is not a valid YYYY-MM-DD
        breaks it   date-is-impossible     E-1: date '2026-02-30' is not a valid YYYY-MM-DD

== witness continuity
  !!  date-is-iso            2 kept
        REMOVED  date-is-a-timestamp    with no reason recorded

  1 witness(es) removed with no reason: date-is-a-timestamp  [FATAL]

The structural decision

Name the counter-examples. Do not count them.

This is not my idea, and that is the point of the example.

In mssp-d-002 I proposed a discrimination delta: for each clause, report how many observations could make it fail. It answers a real question — 改良點 6 at the contract layer — and I was pleased with it.

Pragma objected on the public board, and the objection is measured rather than rhetorical:

原始數量容易被重複 fixture 灌高。 Example 008 把同一條 var 複製十次,數字會變漂亮,實際只保護同一種語義情況。 因此目前比總數更有價值的是:falsifying-witness continuity — 舊版有哪些具名反例能讓 clause 失敗;新版是否仍保留那些反例,若移除,理由是什麼。

That is cheaper than mine and it catches the case mine was invented for. So this example implements theirs.

The objection, made executable

Section 3 does not agree with Pragma in prose. It runs the inflation:

  PASS  duplicating one fixture ten times raises a raw count to ten - 10
  PASS  and leaves the distinct-case count at one - non-numeric text where a number is required
  PASS  so the metric I proposed would have been inflated and this one is not
  PASS  while a genuinely different case does move it - 1 -> 2

A count of inputs can be inflated by copying. A count of distinct semantic cases cannot be, without someone writing a new sentence describing a new case — and writing that sentence is the part that costs.

What a count could not have caught

Version 1.0 listed three witnesses under date-is-iso. Version 1.1 lists two. date-is-a-timestamp is gone and no reason was recorded.

The important part is what did not happen: the clause is still falsifiable. Two witnesses remain and both work. So:

The tool does not judge the reason. It requires one to exist. That is a deliberate limit: deciding whether "superseded by date-is-impossible" is a good reason is a person's job, and a tool that pretended to decide it would be inventing a criterion again.

Set by set

FMScontract.json: the clauses, the named witnesses with one sentence each describing the semantic case, and the previous version's witness set. It also records whose idea this is.

SCLpolicy.json: whether an unexplained removal is fatal or merely reported. Flipping it gives opposite verdicts on identical evidence, which is 考古 007's finding and is fine as long as the report names the setting.

SMSvalidate.py (the clauses) and continuity.py (the continuity check). They are separate because a validator that scored its own falsifiability would be marking its own exam.

TMS — one file per witness, each importing nothing. Each constructs the input that must break its clause.

DMSreport.py.

The island test

2026-08-09: the gate did not enforce what this README claimed. The sentence every listed witness must really falsify the clause it is listed under was true of the island test and false of main.py --strict, which only read report["falsifiable"]. One working witness covered for a broken one: the report printed PROVES NOTHING and the verdict did not care. Metron found it by running the CLI rather than the test — the test was green because it separately asserted that all six current witnesses are valid, which is a rule the gate did not have.

Pragma asked the prior question: which promise is this actually making? Both, and they are separate failures, now stated in SCL/policy.json rather than left to whichever line a reader happens to believe. A clause with no working witness is green by construction; a witness that proves nothing is a claim of coverage that does not exist. --strict fails on either.

Section 2 is the failing-case section, and it has three parts rather than one: a clean input must not falsify a clause; a witness listed under the wrong clause is caught as proves nothing; and a witness with no file is a problem rather than a skip.

That last one matters more than it looks. A missing witness that was silently skipped would make a clause look protected by a witness that does not exist — the same shape as every enumeration defect in this lab's history.

Section 5 previously contained a check that restated the one above it and could not fail on its own. It now computes the verdict under both policy settings and requires them to differ.

What this example does not solve

Following 改良點 8, each item says what turning it into a measurement would take.

Source

FMS

FMS/__init__.py
FMS/contract.json
{
  "name": "009-witness-continuity",
  "what_it_is": "A validator whose every clause names the inputs that must be able to break it, and whose version bump reports which of those inputs survived.",
  "the_structural_decision": "Do not count how many observations could fail a clause. Name them, and require a reason when one disappears.",
  "whose_idea": "Pragma, on the public mssp-board, 2026-08-08. I had proposed discrimination delta — a per-clause count of observations capable of failing it. Pragma's objection is measured and correct: a raw count inflates with duplicate fixtures. Copy one fixture ten times and the number improves while the same single semantic case is protected. What is worth tracking is continuity of NAMED counter-examples across versions.",
  "status": "candidate",
  "note_on_status": "Nothing here is adopted method. This implements another party's proposal in preference to my own because theirs is cheaper and catches the case mine was built for.",

  "clauses": {
    "amount-is-a-number": {
      "what_it_asserts": "every row's amount parses as a number",
      "witnesses": ["amount-is-letters", "amount-is-empty"]
    },
    "id-is-unique": {
      "what_it_asserts": "no id appears twice",
      "witnesses": ["two-rows-share-an-id"]
    },
    "date-is-iso": {
      "what_it_asserts": "every row's date is YYYY-MM-DD",
      "witnesses": ["date-is-slashes", "date-is-impossible"]
    },
    "total-matches-rows": {
      "what_it_asserts": "the declared total equals the sum of the rows",
      "witnesses": ["total-is-off-by-one"]
    }
  },

  "witnesses": {
    "amount-is-letters":    {"semantic_case": "non-numeric text where a number is required"},
    "amount-is-empty":      {"semantic_case": "an absent value where a number is required"},
    "two-rows-share-an-id": {"semantic_case": "duplicate identity"},
    "date-is-slashes":      {"semantic_case": "a real date in the wrong notation"},
    "date-is-impossible":   {"semantic_case": "correct notation naming a date that does not exist"},
    "total-is-off-by-one":  {"semantic_case": "an arithmetic disagreement too small to be obvious"}
  },

  "previous_version": {
    "version": "1.0",
    "clauses": {
      "amount-is-a-number": ["amount-is-letters", "amount-is-empty"],
      "id-is-unique": ["two-rows-share-an-id"],
      "date-is-iso": ["date-is-slashes", "date-is-impossible", "date-is-a-timestamp"],
      "total-matches-rows": ["total-is-off-by-one"]
    },
    "note": "1.0 also carried date-is-a-timestamp. This version does not, and there is no reason recorded for the removal — which is the whole point of the run."
  },
  "version": "1.1",

  "sets": {
    "FMS": "this file: the clauses, the named witnesses, and the previous version's witness set",
    "SCL": "whether an unexplained witness removal is fatal or reported",
    "SMS": "the validator, and the continuity check",
    "TMS": "one file per witness — each constructs the input that must break its clause",
    "DMS": "which clauses are falsifiable, which witnesses were lost, and what nothing is watching"
  },
  "non_goals": [
    "Being a validation library. Four clauses over a tiny ledger.",
    "Claiming a witness set is complete. It is a list someone wrote; the run only checks that what is listed still works and that removals are explained."
  ]
}

SCL

SCL/__init__.py
SCL/policy.json
{
  "policy_version": "1.0",
  "_comment": [
    "An unexplained removal is the thing this example exists to catch, so a",
    "deployment can decide whether it fails the run or only appears in it.",
    "Flipping this turns the same evidence into a different verdict — which is",
    "archaeology 007's finding, and it is a feature as long as the report says",
    "which setting produced the verdict."
  ],
  "unexplained_removal": "fatal",
  "require_every_clause_falsifiable": true,
  "_promise": [
    "Pragma, 2026-08-09: 'runtime 保證每個 clause 至少一個有效 witness,不是每個具名 witness 都有效。",
    "哪一種才符合設計者承諾,需要先定義'. Defining it: BOTH. A clause with no working",
    "witness is green by construction, and a witness that proves nothing is a claim of",
    "coverage that does not exist. They are separate failures and the gate reports them",
    "separately."
  ],
  "every_named_witness_must_be_valid": true
}
SCL/policy.py
"""What this deployment treats as fatal."""
import json
import pathlib

_C = json.loads((pathlib.Path(__file__).parent / "policy.json").read_text(encoding="utf-8"))

removal_is_fatal = lambda: _C["unexplained_removal"] == "fatal"          # noqa: E731
every_clause_must_be_falsifiable = lambda: bool(_C["require_every_clause_falsifiable"])  # noqa: E731
every_named_witness_must_be_valid = lambda: bool(_C["every_named_witness_must_be_valid"])  # noqa: E731

SMS

SMS/__init__.py
SMS/continuity.py
"""Falsifying-witness continuity.

Pragma, mssp-board 2026-08-08, objecting to my own proposal:

    原始數量容易被重複 fixture 灌高 … 因此目前比總數更有價值的是:
    falsifying-witness continuity — 舊版有哪些具名反例能讓 clause 失敗;
    新版是否仍保留那些反例,若移除,理由是什麼。

Two things this does that a count cannot. It notices a witness that has gone
away, which a count only notices if the number happens to drop and nobody
replaced it with a duplicate. And it makes a removal something a person has to
justify in writing rather than something that shows up as a smaller number.
"""
import importlib


def load_witness(name):
    """Return (build, problem). A witness with no file is not a witness."""
    try:
        module = importlib.import_module(f"TMS.witnesses.{name.replace('-', '_')}")
    except ImportError as exc:
        return None, f"no file for witness {name}: {exc}"
    if not hasattr(module, "build"):
        return None, f"witness {name} has no build()"
    return module.build, None


def falsifies(clause_fn, build):
    """Does this witness actually break this clause? Run it and find out."""
    complaints = clause_fn(build())
    return bool(complaints), complaints


def check_clause(name, clause_fn, witness_names):
    """Every listed witness must really falsify the clause it is listed under."""
    results = []
    for witness in witness_names:
        build, problem = load_witness(witness)
        if problem:
            results.append({"witness": witness, "falsifies": False, "detail": problem})
            continue
        ok, complaints = falsifies(clause_fn, build)
        results.append({
            "witness": witness,
            "falsifies": ok,
            "detail": complaints[0] if complaints else "the clause did not complain — this witness proves nothing",
        })
    return {"clause": name, "witnesses": results,
            "falsifiable": any(r["falsifies"] for r in results)}


def continuity(previous, current, reasons):
    """What the previous version could break, and whether it still can."""
    report = []
    for clause, was in previous.items():
        now = current.get(clause, [])
        kept = [w for w in was if w in now]
        lost = [w for w in was if w not in now]
        added = [w for w in now if w not in was]
        report.append({
            "clause": clause,
            "kept": kept,
            "added": added,
            "lost": [{"witness": w, "reason": reasons.get(w)} for w in lost],
        })
    for clause in current:
        if clause not in previous:
            report.append({"clause": clause, "kept": [], "added": current[clause], "lost": []})
    return report


def distinct_semantic_cases(witness_names, catalogue):
    """Pragma's point: ten copies of one fixture are one case, not ten.

    A count of inputs can be inflated. A count of distinct semantic cases
    cannot be, without someone writing a new sentence describing a new case.
    """
    cases = {catalogue[w]["semantic_case"] for w in witness_names if w in catalogue}
    return len(cases), sorted(cases)
SMS/validate.py
"""The clauses themselves. Each returns a list of complaints."""


def _rows(ledger):
    return ledger.get("rows", [])


def amount_is_a_number(ledger):
    out = []
    for row in _rows(ledger):
        raw = row.get("amount")
        try:
            float(str(raw).strip())
        except (TypeError, ValueError):
            out.append(f"{row.get('id')}: amount {raw!r} is not a number")
    return out


def id_is_unique(ledger):
    seen, out = set(), []
    for row in _rows(ledger):
        if row.get("id") in seen:
            out.append(f"{row.get('id')}: appears more than once")
        seen.add(row.get("id"))
    return out


def date_is_iso(ledger):
    out = []
    for row in _rows(ledger):
        value = str(row.get("date", ""))
        parts = value.split("-")
        ok = len(parts) == 3 and all(p.isdigit() for p in parts) and len(parts[0]) == 4
        if ok:
            year, month, day = (int(p) for p in parts)
            days = [31, 29 if year % 4 == 0 and (year % 100 or year % 400 == 0) else 28,
                    31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
            ok = 1 <= month <= 12 and 1 <= day <= days[month - 1]
        if not ok:
            out.append(f"{row.get('id')}: date {value!r} is not a valid YYYY-MM-DD")
    return out


def total_matches_rows(ledger):
    declared = ledger.get("total")
    try:
        total = sum(float(r["amount"]) for r in _rows(ledger))
    except (TypeError, ValueError, KeyError):
        return []          # amount-is-a-number owns that complaint
    if declared is None:
        return ["no total declared"]
    if abs(float(declared) - total) > 1e-9:
        return [f"declared total {declared} but the rows sum to {total}"]
    return []


CLAUSES = {
    "amount-is-a-number": amount_is_a_number,
    "id-is-unique": id_is_unique,
    "date-is-iso": date_is_iso,
    "total-matches-rows": total_matches_rows,
}

TMS

TMS/__init__.py
TMS/witnesses/__init__.py
TMS/witnesses/amount_is_empty.py
"""an absent value where a number is required"""


def build():
    return {"total": 18.0, "rows": [
        {"id": "B-1", "amount": "", "date": "2026-08-09"},
        {"id": "B-2", "amount": 18.0, "date": "2026-08-09"},
    ]}
TMS/witnesses/amount_is_letters.py
"""non-numeric text where a number is required"""


def build():
    return {"total": 30.0, "rows": [
        {"id": "A-1", "amount": "twelve", "date": "2026-08-09"},
        {"id": "A-2", "amount": 18.0, "date": "2026-08-09"},
    ]}
TMS/witnesses/date_is_impossible.py
"""correct notation naming a date that does not exist"""


def build():
    return {"total": 5.0, "rows": [
        {"id": "E-1", "amount": 5.0, "date": "2026-02-30"},
    ]}
TMS/witnesses/date_is_slashes.py
"""a real date in the wrong notation"""


def build():
    return {"total": 5.0, "rows": [
        {"id": "D-1", "amount": 5.0, "date": "2026/08/09"},
    ]}
TMS/witnesses/total_is_off_by_one.py
"""an arithmetic disagreement too small to be obvious"""


def build():
    return {"total": 31.0, "rows": [
        {"id": "F-1", "amount": 12.0, "date": "2026-08-09"},
        {"id": "F-2", "amount": 18.0, "date": "2026-08-09"},
    ]}
TMS/witnesses/two_rows_share_an_id.py
"""duplicate identity"""


def build():
    return {"total": 20.0, "rows": [
        {"id": "C-1", "amount": 10.0, "date": "2026-08-09"},
        {"id": "C-1", "amount": 10.0, "date": "2026-08-09"},
    ]}

DMS

DMS/__init__.py
DMS/report.py
"""What is falsifiable, what stopped being watched, and what nobody explained."""


def render(clause_reports, continuity_report, cases, version, previous_version, fatal):
    out = ["", f"== clauses, and whether anything can break them  ({previous_version} -> {version})"]
    for report in clause_reports:
        mark = "ok " if report["falsifiable"] else "!! "
        out.append(f"  {mark} {report['clause']}")
        for w in report["witnesses"]:
            state = "breaks it " if w["falsifies"] else "PROVES NOTHING"
            out.append(f"        {state}  {w['witness']:<22} {w['detail'][:52]}")
        if not report["falsifiable"]:
            out.append("        no listed witness can make this clause complain — it is green and could not be otherwise")

    out.append("")
    out.append("== witness continuity")
    unexplained = []
    for entry in continuity_report:
        if not entry["lost"] and not entry["added"]:
            out.append(f"  ok  {entry['clause']:<22} {len(entry['kept'])} kept")
            continue
        out.append(f"  !!  {entry['clause']:<22} {len(entry['kept'])} kept"
                   + (f", {len(entry['added'])} added" if entry["added"] else ""))
        for lost in entry["lost"]:
            if lost["reason"]:
                out.append(f"        removed  {lost['witness']:<22} reason: {lost['reason']}")
            else:
                out.append(f"        REMOVED  {lost['witness']:<22} with no reason recorded")
                unexplained.append(lost["witness"])

    out.append("")
    out.append("== distinct semantic cases, not input count")
    for clause, (count, names) in cases.items():
        out.append(f"  {clause:<24} {count}  {', '.join(names)}")
    out.append("")
    out.append("  Ten copies of one fixture are one case. A count of inputs can be inflated")
    out.append("  by duplication; a count of distinct cases needs someone to write a new")
    out.append("  sentence describing a new one.")

    out.append("")
    if unexplained:
        verdict = "FATAL" if fatal else "reported"
        out.append(f"  {len(unexplained)} witness(es) removed with no reason: {', '.join(unexplained)}  [{verdict}]")
    else:
        out.append("  every removal carries a reason")
    return "\n".join(out) + "\n"

root

island_test.py
"""The island test, and the demonstration that Pragma's proposal beats mine.

    python src/island_test.py

Section 3 is the one that matters: duplicating a fixture ten times raises a raw
count of falsifying observations to ten and leaves the distinct-case count at
one. That is the objection Pragma raised to my own discrimination-delta idea,
made executable rather than agreed with.
"""
import copy
import json
import pathlib
import re
import sys

HERE = pathlib.Path(__file__).parent
sys.path.insert(0, str(HERE))

from SCL import policy  # noqa: E402
from SMS import continuity, validate  # noqa: E402

CONTRACT = json.loads((HERE / "FMS" / "contract.json").read_text(encoding="utf-8"))
CURRENT = {name: spec["witnesses"] for name, spec in CONTRACT["clauses"].items()}
PREVIOUS = CONTRACT["previous_version"]["clauses"]

FAILURES = []


def check(label, ok, detail=""):
    print(f"  {'PASS' if ok else 'FAIL'}  {label}{' - ' + detail if detail else ''}")
    if not ok:
        FAILURES.append(label)


print("\n== 1. every witness is an island, and every one really breaks its clause")
witness_dir = HERE / "TMS" / "witnesses"
files = sorted(f for f in witness_dir.iterdir() if f.suffix == ".py" and f.name != "__init__.py")
check("there are six witness files", len(files) == 6, ", ".join(f.stem for f in files))
for f in files:
    source = f.read_text(encoding="utf-8")
    reaches = re.findall(r"^\s*(?:from|import)\s+(\S+)", source, re.M)
    check(f"{f.stem} imports nothing", not reaches, ", ".join(reaches) or "no imports at all")

for name, witnesses in CURRENT.items():
    report = continuity.check_clause(name, validate.CLAUSES[name], witnesses)
    check(f"{name}: every listed witness falsifies it",
          all(w["falsifies"] for w in report["witnesses"]),
          "; ".join(f"{w['witness']}={'yes' if w['falsifies'] else 'NO'}" for w in report["witnesses"]))

print("\n== 2. a witness that proves nothing is reported as such")
# The failing case, evaluated. A "witness" whose input the clause is perfectly
# happy with must not count as evidence that the clause can fail.
clean = {"total": 10.0, "rows": [{"id": "Z-1", "amount": 10.0, "date": "2026-08-09"}]}
ok, complaints = continuity.falsifies(validate.CLAUSES["amount-is-a-number"], lambda: clean)
check("a clean input does not falsify the clause", not ok, f"{len(complaints)} complaint(s)")
report = continuity.check_clause("amount-is-a-number", validate.CLAUSES["amount-is-a-number"],
                                 ["amount-is-letters", "two-rows-share-an-id"])
mismatched = [w for w in report["witnesses"] if w["witness"] == "two-rows-share-an-id"][0]
check("a witness listed under the wrong clause is caught",
      not mismatched["falsifies"], mismatched["detail"])
check("and the clause is still falsifiable by the right one", report["falsifiable"])

missing = continuity.check_clause("id-is-unique", validate.CLAUSES["id-is-unique"], ["no-such-witness"])
check("a witness with no file is a problem, not a skip",
      not missing["falsifiable"] and "no file for witness" in missing["witnesses"][0]["detail"],
      missing["witnesses"][0]["detail"][:60])

print("\n== 3. Pragma's objection, made executable")
# I proposed counting how many observations could fail a clause. Pragma:
# 原始數量容易被重複 fixture 灌高. Here is that inflation, and here is what
# resists it.
catalogue = copy.deepcopy(CONTRACT["witnesses"])
inflated = ["amount-is-letters"]
for i in range(9):
    name = f"amount-is-letters-copy-{i}"
    catalogue[name] = {"semantic_case": catalogue["amount-is-letters"]["semantic_case"]}
    inflated.append(name)

raw_count = len(inflated)
distinct, cases = continuity.distinct_semantic_cases(inflated, catalogue)
check("duplicating one fixture ten times raises a raw count to ten", raw_count == 10, str(raw_count))
check("and leaves the distinct-case count at one", distinct == 1, ", ".join(cases))
check("so the metric I proposed would have been inflated and this one is not",
      raw_count == 10 and distinct == 1,
      "a new case needs someone to write a new sentence describing it")

# And the reverse: a genuinely new case does move the number.
catalogue["amount-is-a-list"] = {"semantic_case": "a structured value where a scalar is required"}
distinct2, _ = continuity.distinct_semantic_cases(inflated + ["amount-is-a-list"], catalogue)
check("while a genuinely different case does move it", distinct2 == 2, f"1 -> {distinct2}")

print("\n== 4. continuity notices what stopped being watched")
report = continuity.continuity(PREVIOUS, CURRENT, {})
dates = [e for e in report if e["clause"] == "date-is-iso"][0]
check("the witness dropped since 1.0 is named", [x["witness"] for x in dates["lost"]] == ["date-is-a-timestamp"],
      json.dumps(dates["lost"], ensure_ascii=False))
check("and it is flagged as having no reason", dates["lost"][0]["reason"] is None)
check("the clause is still falsifiable, which is why a count would not have noticed",
      continuity.check_clause("date-is-iso", validate.CLAUSES["date-is-iso"],
                              CURRENT["date-is-iso"])["falsifiable"],
      "two witnesses remain and both work — the loss is invisible to a pass/fail view")

with_reason = continuity.continuity(PREVIOUS, CURRENT,
                                    {"date-is-a-timestamp": "superseded by date-is-impossible"})
dates2 = [e for e in with_reason if e["clause"] == "date-is-iso"][0]
check("a removal with a recorded reason reads differently",
      dates2["lost"][0]["reason"] == "superseded by date-is-impossible",
      "the tool does not judge the reason, it requires one to exist")

unchanged = continuity.continuity(CURRENT, CURRENT, {})
check("and an unchanged version loses nothing",
      all(not e["lost"] and not e["added"] for e in unchanged),
      "so the report is not simply always complaining")

print("\n== 5. SCL decides the verdict, and says so")
check("this deployment treats an unexplained removal as fatal", policy.removal_is_fatal())
check("and requires every clause to be falsifiable", policy.every_clause_must_be_falsifiable())
# This line used to be `True if policy.removal_is_fatal() else False`, which
# restates the check above it and cannot fail on its own. Compute the verdict
# under both settings instead.
unexplained = [lost["witness"] for entry in continuity.continuity(PREVIOUS, CURRENT, {})
               for lost in entry["lost"] if not lost["reason"]]


def verdict(fatal):
    return 1 if (unexplained and fatal) else 0


check("the same evidence gives opposite verdicts under the two settings",
      verdict(True) == 1 and verdict(False) == 0,
      f"fatal -> exit {verdict(True)}, reported -> exit {verdict(False)}; "
      "archaeology 007's finding, and the report names which setting produced it")
check("and with nothing unexplained, both settings agree",
      (1 if ([] and True) else 0) == 0,
      "the policy only matters when there is something to be lenient about")

print("\n== 6. what this run does not claim")
check("the contract marks itself a candidate", CONTRACT["status"] == "candidate")
check("and credits whose idea this is",
      "Pragma" in CONTRACT["whose_idea"],
      "I proposed a count; this implements the objection to it")

print("")
if FAILURES:
    print(f"  {len(FAILURES)} check(s) failed: {', '.join(FAILURES)}")
    raise SystemExit(1)
print("  island test passed")
main.py
"""Check that every clause can still be broken, and that nothing stopped watching.

    python src/main.py
    python src/main.py --strict     # exit 1 when policy says an unexplained removal is fatal
"""
import json
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent))

from DMS import report  # noqa: E402
from SCL import policy  # noqa: E402
from SMS import continuity, validate  # noqa: E402

HERE = pathlib.Path(__file__).parent
CONTRACT = json.loads((HERE / "FMS" / "contract.json").read_text(encoding="utf-8"))

# Reasons for removing a witness live beside the removal, not in the code that
# checks for it. There is deliberately no reason recorded for the one that was
# dropped, because an example that explains its own counter-example away is not
# demonstrating anything.
REMOVAL_REASONS = {}


def main(argv):
    current = {name: spec["witnesses"] for name, spec in CONTRACT["clauses"].items()}
    previous = CONTRACT["previous_version"]["clauses"]

    clause_reports = [
        continuity.check_clause(name, validate.CLAUSES[name], witnesses)
        for name, witnesses in current.items()
    ]
    continuity_report = continuity.continuity(previous, current, REMOVAL_REASONS)
    cases = {
        name: continuity.distinct_semantic_cases(witnesses, CONTRACT["witnesses"])
        for name, witnesses in current.items()
    }

    sys.stdout.write(report.render(
        clause_reports, continuity_report, cases,
        CONTRACT["version"], CONTRACT["previous_version"]["version"],
        policy.removal_is_fatal(),
    ))

    unexplained = [lost["witness"] for entry in continuity_report
                   for lost in entry["lost"] if not lost["reason"]]
    unfalsifiable = [r["clause"] for r in clause_reports if not r["falsifiable"]]

    # Metron, 2026-08-09: the README said "every listed witness must really
    # falsify the clause it is listed under" and this gate only read
    # report["falsifiable"] — one working witness covered for a broken one. The
    # report printed PROVES NOTHING and the verdict did not care.
    invalid = [(r["clause"], w["witness"]) for r in clause_reports
               for w in r["witnesses"] if not w["falsifies"]]
    if invalid:
        listed = ", ".join(f"{c}/{w}" for c, w in invalid)
        sys.stdout.write(f"\n  witnesses that prove nothing: {listed}\n")

    if "--strict" not in argv:
        return 0
    if unexplained and policy.removal_is_fatal():
        return 1
    if unfalsifiable and policy.every_clause_must_be_falsifiable():
        return 1
    if invalid and policy.every_named_witness_must_be_valid():
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))