fix(v1-gate): rows 1-2 stop at FORM OK — what files cannot show, a person has to say
The PM checkpoint of 18.09 paid M-6's price three times and row 2 still read 3 of 3 GREEN:
a whole artefact family written by hand and made internally consistent (verdict_id minted
with the product's own rule, about sixty lines of script), four REAL runs' artefacts under
a handwritten feedback file, and `<n>/outbox` symlinked out of the round to a real run
elsewhere. `RUN_ATTESTATION` stood in the output the whole time, and the row was green
anyway — which is the finding: a row that PRINTS its limit and reports GREEN has not
stated a limit, it has decorated one.
One of the three is a hole and is closed; two are not holes at all.
1. The outbox must lie in the round ITSELF (`outbox_escape`). Derived is only derived if
the filesystem cannot redirect it: `<n>/outbox` as a symlink, or any symlinked artefact
inside it, is refused by name.
2. Rows 1 and 2 can no longer be green from files. What the computation gives is
`FORM_OK` ("FORM OK, IKKE BEVIST"): every check the gate knows how to make passed, the
round counts 0 against the criterion, and the exit code still fails — it is un-green in
every way RED is. What separates the two is what the reader must do next, not what the
gate concludes.
3. Green is `<n>/attestering.txt`, per round: the operator's own statement, naming the
round, the run and the date. Plain text with three keyed lines, deliberately not JSON —
it is the one artefact in this contract a PERSON types, and a missing brace must never
be able to read as "not confirmed". Nothing in the product writes one, and a test in
`src/` says so: a product that can produce a witness to its own run has produced
exactly the thing these rows cannot.
A MISSING attestation is FORM OK: nobody has confirmed anything yet, and the gate is
not entitled to call that a lie. A PRESENT one that names another round or run, carries
no date, an unparsable date, or a date before the run, is RED: it is a statement about
something this is not. Row 2 needs round 0 attested as well, since round 1 is measured
against it — an unattested baseline is an unattested comparison.
The attestation is read raw off `outcome.json` (`_declared_run`), not through
`read_outcome`, so row 1 — which reads no run — does not inherit row 2's artefact checks
through the back door.
Nothing moved on the criterion: 0/3 · 0/3 · 3/8 · no report · 3/8 · IKKE MÅLT · 1/20,
exit 1. What changed is that rows 1-2 now name what they are waiting for, and it is not
another file.
Suite after `git add` on a clean tree: `uv run pytest -q` = 2014 passed, 5 skipped,
5 xfailed (508 s). The attack of b769537/ab00016 is the red half of this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ab00016ed5
commit
1b48124a01
2 changed files with 204 additions and 21 deletions
|
|
@ -27,7 +27,7 @@ import tempfile
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from collections.abc import Callable, Iterable, Mapping, Sequence
|
from collections.abc import Callable, Iterable, Mapping, Sequence
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
import difflib
|
import difflib
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -58,8 +58,26 @@ RUN_ATTESTATION = (
|
||||||
"operatøren"
|
"operatøren"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Rows 1-2: the file the gate can neither derive nor write, and the one thing that turns their
|
||||||
|
#: computation into GREEN. Plain text with three keyed lines, deliberately not JSON: it is the one
|
||||||
|
#: artefact in the contract a PERSON types by hand, and a missing brace must never be able to read
|
||||||
|
#: as "not confirmed".
|
||||||
|
ATTEST_FILE = "attestering.txt"
|
||||||
|
#: What an attestation has to name: which round, which run, and when it was given.
|
||||||
|
ATTEST_KEYS = ("runde", "kjøring", "dato")
|
||||||
|
#: Printed on rows 1-2 on every run: what GREEN on those rows means, and whose statement it is.
|
||||||
|
ATTEST_RULE = (
|
||||||
|
"grønt på rad 1-2 regner gaten ikke ut: filene kan stemme innbyrdes og likevel beskrive en "
|
||||||
|
"runde ingen har holdt. Målingen gir FORM OK; skrittet derfra til grønt er operatørens egen "
|
||||||
|
f"attestering i <runde>/{ATTEST_FILE} (runde, kjøring, dato), som gaten ALDRI skriver selv"
|
||||||
|
)
|
||||||
|
|
||||||
GREEN = "GRØNN"
|
GREEN = "GRØNN"
|
||||||
RED = "RØD"
|
RED = "RØD"
|
||||||
|
#: Rows 1-2 only: every computed check passed, and the round is still waiting for the operator's
|
||||||
|
#: attestation. Never green, so it fails the exit code exactly as RED does — what separates it
|
||||||
|
#: from RED is what the reader must do next, not what the gate concludes.
|
||||||
|
FORM_OK = "FORM OK, IKKE BEVIST"
|
||||||
DIAGNOSIS = "DIAGNOSE"
|
DIAGNOSIS = "DIAGNOSE"
|
||||||
#: A row whose evidence could not be read. Never green: on a failing row it fails the exit code.
|
#: A row whose evidence could not be read. Never green: on a failing row it fails the exit code.
|
||||||
NOT_MEASURED = "IKKE MÅLT"
|
NOT_MEASURED = "IKKE MÅLT"
|
||||||
|
|
@ -75,6 +93,7 @@ Rundekatalogen (--rounds-dir) har fast form. Runde n = tilbakemelding på rappor
|
||||||
<rounds-dir>/<n>/outbox/ kjøring n sin EGEN utboks, kopiert hit urørt
|
<rounds-dir>/<n>/outbox/ kjøring n sin EGEN utboks, kopiert hit urørt
|
||||||
<rounds-dir>/<n>/report.md rapporten bygget fra kjøring n
|
<rounds-dir>/<n>/report.md rapporten bygget fra kjøring n
|
||||||
<rounds-dir>/3/report.kept.md runde 3-rapporten slik fagpersonen BEHOLDT den
|
<rounds-dir>/3/report.kept.md runde 3-rapporten slik fagpersonen BEHOLDT den
|
||||||
|
<rounds-dir>/<n>/attestering.txt operatørens bekreftelse på runden (n = 0, 1, 2, 3)
|
||||||
|
|
||||||
feedback.json:
|
feedback.json:
|
||||||
{"author": "<fagpersonen>", "given_at": "<ISO-8601 med tidssone>",
|
{"author": "<fagpersonen>", "given_at": "<ISO-8601 med tidssone>",
|
||||||
|
|
@ -101,6 +120,17 @@ kjøring må stå inne for seg selv: hver EVALUERT tilnærming har <run_id>-<id>
|
||||||
hvis verdict_id gaten selv minter PÅ NYTT fra forslagets egen IR (verdicts.verdict_key). En
|
hvis verdict_id gaten selv minter PÅ NYTT fra forslagets egen IR (verdicts.verdict_key). En
|
||||||
not_evaluated-rad har ingen av delene, og en artefakt for en tilnærming coverage ikke nevner hører
|
not_evaluated-rad har ingen av delene, og en artefakt for en tilnærming coverage ikke nevner hører
|
||||||
til en annen kjøring. Dette beviser ikke at kjøringen skjedde — se attesteringen for rad 2.
|
til en annen kjøring. Dette beviser ikke at kjøringen skjedde — se attesteringen for rad 2.
|
||||||
|
attestering.txt (ren tekst, skrevet for hånd av operatøren — gaten skriver den ALDRI):
|
||||||
|
runde: <n>
|
||||||
|
kjøring: <run_id slik rundens outcome.json oppgir den>
|
||||||
|
dato: <ISO-dato, ikke før kjøringen ble gjort>
|
||||||
|
<fritekst under, om ønskelig>
|
||||||
|
|
||||||
|
Rad 1 og 2 kan ALDRI bli grønne av filer alene. Uten attesteringen er taket FORM OK, IKKE BEVIST:
|
||||||
|
runden teller 0 mot ferdig-kriteriet og gaten feller fortsatt. En attestering som FINNES, men
|
||||||
|
navngir en annen runde eller kjøring, mangler dato eller er datert før kjøringen, er RØD - den er
|
||||||
|
et utsagn om noe annet. Rad 2 krever i tillegg at grunnkjøringen (runde 0) er attestert: det er
|
||||||
|
den runde 1 måles mot. Utboksen må dessuten ligge i runden SELV - en lenke ut av runden avvises.
|
||||||
Rad 4 teller innholdslinjer (ikke blanke, skillelinjer eller tabellrammer) som står uendret og i
|
Rad 4 teller innholdslinjer (ikke blanke, skillelinjer eller tabellrammer) som står uendret og i
|
||||||
samme rekkefølge; fagpersonens tillegg vises som eget tall. --rounds-dir inne i repoet må være
|
samme rekkefølge; fagpersonens tillegg vises som eget tall. --rounds-dir inne i repoet må være
|
||||||
gitignored.
|
gitignored.
|
||||||
|
|
@ -181,6 +211,111 @@ def _parse_time(value: Any) -> datetime | None:
|
||||||
return stamp if stamp.tzinfo is not None else None
|
return stamp if stamp.tzinfo is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_date(value: str) -> date | None:
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(value).date()
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Attestation:
|
||||||
|
"""One round's attestation, read back.
|
||||||
|
|
||||||
|
``present`` separates the two ways it can fail, because they ask different things of the
|
||||||
|
reader: no file at all means nobody has confirmed the round YET (the row waits at
|
||||||
|
``FORM_OK``), while a file that contradicts the round is a statement about some other round
|
||||||
|
(``RED``). Both are un-green, and neither is a measurement."""
|
||||||
|
|
||||||
|
present: bool
|
||||||
|
ok: bool
|
||||||
|
why: str
|
||||||
|
|
||||||
|
|
||||||
|
def _declared_run(round_dir: Path) -> tuple[str, datetime | None]:
|
||||||
|
"""The run id and run time the round's own ``outcome.json`` DECLARES — read raw, without the
|
||||||
|
verification ``read_outcome`` does, so row 1 (which reads no run) does not inherit row 2's
|
||||||
|
artefact checks through the attestation."""
|
||||||
|
data, _ = _read_json(round_dir / "outcome.json")
|
||||||
|
if not isinstance(data, Mapping):
|
||||||
|
return "", None
|
||||||
|
return str(data.get("run_id", "")).strip(), _parse_time(data.get("ran_at"))
|
||||||
|
|
||||||
|
|
||||||
|
def read_attestation(round_dir: Path) -> Attestation:
|
||||||
|
"""The operator's confirmation for ONE round: that it is there, and that it is about THIS
|
||||||
|
round and the run this round declares.
|
||||||
|
|
||||||
|
This is the gate's one input it cannot compute, and that is the point. Measured 18.09: a
|
||||||
|
whole artefact family written by hand and made internally consistent — coverage, artefact
|
||||||
|
pairs, ``verdict_id`` minted with the product's own rule — took row 2 to 3 of 3 GREEN, as did
|
||||||
|
four real runs' artefacts under a handwritten feedback file. Neither is a hole ``verify_run``
|
||||||
|
can close: files agreeing with each other is not a witness that anything happened. So the
|
||||||
|
computation stops at ``FORM_OK`` and green comes from a person saying so, per round."""
|
||||||
|
path = round_dir / ATTEST_FILE
|
||||||
|
if not path.is_file():
|
||||||
|
return Attestation(
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
f"{ATTEST_FILE} mangler — operatøren har ikke bekreftet at runden ble holdt",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
return Attestation(True, False, f"{ATTEST_FILE} uleselig ({exc!r})")
|
||||||
|
fields: dict[str, str] = {}
|
||||||
|
for line in text.splitlines():
|
||||||
|
key, sep, value = line.partition(":")
|
||||||
|
if sep and key.strip().casefold() in ATTEST_KEYS:
|
||||||
|
fields.setdefault(key.strip().casefold(), value.strip())
|
||||||
|
missing = [k for k in ATTEST_KEYS if not fields.get(k)]
|
||||||
|
if missing:
|
||||||
|
return Attestation(True, False, f"{ATTEST_FILE} mangler {', '.join(missing)}")
|
||||||
|
if fields["runde"] != round_dir.name:
|
||||||
|
return Attestation(
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
f"{ATTEST_FILE} attesterer runde {fields['runde']!r}, ikke {round_dir.name!r}",
|
||||||
|
)
|
||||||
|
run_id, ran_at = _declared_run(round_dir)
|
||||||
|
if not run_id:
|
||||||
|
return Attestation(
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
f"{ATTEST_FILE} navngir kjøring {fields['kjøring']!r}, men runden oppgir ingen",
|
||||||
|
)
|
||||||
|
if fields["kjøring"] != run_id:
|
||||||
|
return Attestation(
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
f"{ATTEST_FILE} navngir kjøring {fields['kjøring']!r}, runden er kjøring {run_id!r}",
|
||||||
|
)
|
||||||
|
given = _parse_date(fields["dato"])
|
||||||
|
if given is None:
|
||||||
|
return Attestation(
|
||||||
|
True, False, f"{ATTEST_FILE}: dato {fields['dato']!r} er ikke en ISO-dato"
|
||||||
|
)
|
||||||
|
if ran_at is not None and given < ran_at.date():
|
||||||
|
return Attestation(
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
f"{ATTEST_FILE}: attestert {given.isoformat()}, før kjøringen {run_id} ble gjort "
|
||||||
|
f"({ran_at.date().isoformat()})",
|
||||||
|
)
|
||||||
|
return Attestation(True, True, "")
|
||||||
|
|
||||||
|
|
||||||
|
def _cap(k: int, required: int, exceptions: Sequence[str], pending: Sequence[str]) -> str:
|
||||||
|
"""Rows 1-2 stop where the files stop. Green is not something these rows can COMPUTE: a
|
||||||
|
tree can pass every check the gate knows how to make and still describe a round nobody held.
|
||||||
|
So a round that holds in form but has no attestation leaves the row at ``FORM_OK`` — un-green,
|
||||||
|
still failing the exit code, and counted as 0 against the criterion — while something the
|
||||||
|
gate CAN refuse (bad form, or an attestation about another round) is ``RED``."""
|
||||||
|
if k >= required:
|
||||||
|
return GREEN
|
||||||
|
return RED if exceptions else FORM_OK
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Feedback:
|
class Feedback:
|
||||||
ids: frozenset[str]
|
ids: frozenset[str]
|
||||||
|
|
@ -280,20 +415,27 @@ def read_feedback(round_dir: Path, ai: tuple[str, str] | None) -> tuple[set[str]
|
||||||
|
|
||||||
def score_rounds(rounds_dir: Path, required: int, ai: tuple[str, str] | None) -> Row:
|
def score_rounds(rounds_dir: Path, required: int, ai: tuple[str, str] | None) -> Row:
|
||||||
exceptions: list[str] = []
|
exceptions: list[str] = []
|
||||||
|
pending: list[str] = []
|
||||||
k = 0
|
k = 0
|
||||||
if not rounds_dir.is_dir():
|
if not rounds_dir.is_dir():
|
||||||
exceptions.append(f"{rounds_dir} finnes ikke")
|
exceptions.append(f"{rounds_dir} finnes ikke")
|
||||||
else:
|
else:
|
||||||
for _, (feedback, why) in sorted(read_rounds(rounds_dir, required, ai).items()):
|
for n, (feedback, why) in sorted(read_rounds(rounds_dir, required, ai).items()):
|
||||||
if feedback is not None:
|
if feedback is None:
|
||||||
k += 1
|
|
||||||
else:
|
|
||||||
exceptions.append(why)
|
exceptions.append(why)
|
||||||
status = GREEN if k == required else RED
|
continue
|
||||||
|
attested = read_attestation(rounds_dir / str(n))
|
||||||
|
if attested.ok:
|
||||||
|
k += 1
|
||||||
|
elif attested.present:
|
||||||
|
exceptions.append(f"runde {n}: {attested.why}")
|
||||||
|
else:
|
||||||
|
pending.append(f"runde {n}: {attested.why}")
|
||||||
|
status = _cap(k, required, exceptions, pending)
|
||||||
reason = (
|
reason = (
|
||||||
"form verifisert i alle runder (forfatterskap: se attestering)"
|
"form verifisert i alle runder, og hver runde attestert av operatøren"
|
||||||
if k == required
|
if status == GREEN
|
||||||
else exceptions[0]
|
else (exceptions + pending)[0]
|
||||||
)
|
)
|
||||||
return Row(
|
return Row(
|
||||||
"rounds",
|
"rounds",
|
||||||
|
|
@ -302,7 +444,8 @@ def score_rounds(rounds_dir: Path, required: int, ai: tuple[str, str] | None) ->
|
||||||
required,
|
required,
|
||||||
status,
|
status,
|
||||||
reason,
|
reason,
|
||||||
exceptions=tuple(exceptions),
|
exceptions=tuple(exceptions + pending),
|
||||||
|
attests=(ATTEST_RULE,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -440,6 +583,31 @@ def verify_run(outbox: Path, run_id: str, coverage: Sequence[Mapping[str, Any]])
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def outbox_escape(round_dir: Path) -> str:
|
||||||
|
"""``""`` when the run the round presents lies IN the round, else why not.
|
||||||
|
|
||||||
|
The outbox is DERIVED (``RUN_OUTBOX``) rather than declared — but a derived path is only
|
||||||
|
derived if the filesystem cannot redirect it. Measured 18.09: ``<n>/outbox`` made a symlink
|
||||||
|
to a real run's directory elsewhere let a round present another run's artefacts as its own,
|
||||||
|
and row 2 read 3 of 3 GREEN. Every artefact the round stands on must therefore be a real file
|
||||||
|
under the round itself, symlink-free in both directions."""
|
||||||
|
outbox = round_dir / RUN_OUTBOX
|
||||||
|
if outbox.is_symlink():
|
||||||
|
return (
|
||||||
|
f"{RUN_OUTBOX}/ er en lenke ut av runden ({os.readlink(outbox)}) — kjøringen må "
|
||||||
|
f"ligge i {round_dir.name}/{RUN_OUTBOX}/ selv"
|
||||||
|
)
|
||||||
|
if not outbox.is_dir():
|
||||||
|
return ""
|
||||||
|
for entry in sorted(outbox.rglob("*")):
|
||||||
|
if entry.is_symlink():
|
||||||
|
return (
|
||||||
|
f"{RUN_OUTBOX}/{entry.relative_to(outbox)} er en lenke "
|
||||||
|
f"({os.readlink(entry)}) — kjøringens artefakter må ligge i runden selv"
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def read_outcome(round_dir: Path) -> tuple[Outcome | None, str]:
|
def read_outcome(round_dir: Path) -> tuple[Outcome | None, str]:
|
||||||
"""A round's outcome file, VERIFIED against the run it names — and the run is the one in the
|
"""A round's outcome file, VERIFIED against the run it names — and the run is the one in the
|
||||||
round's OWN ``outbox/``, never a path the file points at.
|
round's OWN ``outbox/``, never a path the file points at.
|
||||||
|
|
@ -473,6 +641,9 @@ def read_outcome(round_dir: Path) -> tuple[Outcome | None, str]:
|
||||||
)
|
)
|
||||||
if ran_at is None:
|
if ran_at is None:
|
||||||
return None, f"{path}: ran_at er ikke et ISO-tidsstempel med tidssone"
|
return None, f"{path}: ran_at er ikke et ISO-tidsstempel med tidssone"
|
||||||
|
escape = outbox_escape(round_dir)
|
||||||
|
if escape:
|
||||||
|
return None, f"{path}: {escape}"
|
||||||
outbox = round_dir / RUN_OUTBOX
|
outbox = round_dir / RUN_OUTBOX
|
||||||
coverage_path = outbox / f"{run_id}-coverage.json"
|
coverage_path = outbox / f"{run_id}-coverage.json"
|
||||||
payload, why = _read_json(coverage_path)
|
payload, why = _read_json(coverage_path)
|
||||||
|
|
@ -548,11 +719,18 @@ def outcomes_changed(prev: Outcome, cur: Outcome, feedback_ids: set[str]) -> tup
|
||||||
|
|
||||||
def score_changes(rounds_dir: Path, required: int, ai: tuple[str, str] | None) -> Row:
|
def score_changes(rounds_dir: Path, required: int, ai: tuple[str, str] | None) -> Row:
|
||||||
exceptions: list[str] = []
|
exceptions: list[str] = []
|
||||||
|
pending: list[str] = []
|
||||||
k = 0
|
k = 0
|
||||||
base_path = rounds_dir / "0" / "outcome.json"
|
base_path = rounds_dir / "0" / "outcome.json"
|
||||||
base_outcome, base_why = read_outcome(rounds_dir / "0")
|
base_outcome, base_why = read_outcome(rounds_dir / "0")
|
||||||
base = f"runde 0 = {base_path}"
|
base = f"runde 0 = {base_path}"
|
||||||
base += f" (kjøring {base_outcome.run_id})" if base_outcome else f" — {base_why}"
|
base += f" (kjøring {base_outcome.run_id})" if base_outcome else f" — {base_why}"
|
||||||
|
# Round 1 is measured AGAINST round 0's run, so an unattested baseline is an unattested
|
||||||
|
# comparison and no round can count. Said ONCE here rather than repeated on every round.
|
||||||
|
base_attested = read_attestation(rounds_dir / "0")
|
||||||
|
if not base_attested.ok:
|
||||||
|
target = exceptions if base_attested.present else pending
|
||||||
|
target.append(f"runde 0 (grunnkjøringen rad 2 måler mot): {base_attested.why}")
|
||||||
feedback_by_round = read_rounds(rounds_dir, required, ai) if rounds_dir.is_dir() else {}
|
feedback_by_round = read_rounds(rounds_dir, required, ai) if rounds_dir.is_dir() else {}
|
||||||
for n in range(1, required + 1):
|
for n in range(1, required + 1):
|
||||||
feedback, why = feedback_by_round.get(n, (None, f"runde {n}: {rounds_dir} finnes ikke"))
|
feedback, why = feedback_by_round.get(n, (None, f"runde {n}: {rounds_dir} finnes ikke"))
|
||||||
|
|
@ -570,20 +748,24 @@ def score_changes(rounds_dir: Path, required: int, ai: tuple[str, str] | None) -
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
ok, detail = outcomes_changed(prev, cur, set(feedback.ids))
|
ok, detail = outcomes_changed(prev, cur, set(feedback.ids))
|
||||||
if ok:
|
if not ok:
|
||||||
k += 1
|
|
||||||
else:
|
|
||||||
exceptions.append(f"runde {n}: {detail}")
|
exceptions.append(f"runde {n}: {detail}")
|
||||||
status = GREEN if k == required else RED
|
continue
|
||||||
|
attested = read_attestation(rounds_dir / str(n))
|
||||||
|
if not attested.ok:
|
||||||
|
(exceptions if attested.present else pending).append(f"runde {n}: {attested.why}")
|
||||||
|
continue
|
||||||
|
if base_attested.ok:
|
||||||
|
k += 1
|
||||||
return Row(
|
return Row(
|
||||||
"changes",
|
"changes",
|
||||||
"2 runder med målbar endring",
|
"2 runder med målbar endring",
|
||||||
k,
|
k,
|
||||||
required,
|
required,
|
||||||
status,
|
_cap(k, required, exceptions, pending),
|
||||||
base,
|
base,
|
||||||
exceptions=tuple(exceptions),
|
exceptions=tuple(exceptions + pending),
|
||||||
attests=(RUN_ATTESTATION,),
|
attests=(RUN_ATTESTATION, ATTEST_RULE),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -521,12 +521,13 @@ def _forged_rounds(root: Path) -> Path:
|
||||||
|
|
||||||
|
|
||||||
def test_m6_a_handwritten_outbox_is_not_a_run(tmp_path: Path) -> None:
|
def test_m6_a_handwritten_outbox_is_not_a_run(tmp_path: Path) -> None:
|
||||||
"""Row 2 must not be satisfiable by files a forger wrote. Row 1 stays GREEN on the same tree —
|
"""Row 2 must not be satisfiable by files a forger wrote. Row 1 reads FORM OK on the same
|
||||||
the feedback there IS well formed, and that is what makes this an attack on row 2 rather than
|
tree — the feedback there IS well formed, and that is what makes this an attack on row 2
|
||||||
a broken fixture. The control that the row can still go green is ``_green_rounds``, which
|
rather than a broken fixture; since M-7 that form is as far as row 1 can get without the
|
||||||
carries a whole run family (``test_row2_a_traced_change_counts``)."""
|
operator's attestation. The control that the row can still go green is ``_green_rounds``,
|
||||||
|
which carries a whole run family AND an attestation (``test_row2_a_traced_change_counts``)."""
|
||||||
root = _forged_rounds(tmp_path)
|
root = _forged_rounds(tmp_path)
|
||||||
assert gate.score_rounds(root, 3, _AI).status == gate.GREEN
|
assert gate.score_rounds(root, 3, _AI).status == gate.FORM_OK
|
||||||
row = gate.score_changes(root, 3, _AI)
|
row = gate.score_changes(root, 3, _AI)
|
||||||
assert (row.k, row.status) == (0, gate.RED), row.exceptions
|
assert (row.k, row.status) == (0, gate.RED), row.exceptions
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue