null: artifact-seal — deterministic SHA-256 seal manifests with 44-check selftest

This commit is contained in:
null committed 2026-10-22 16:55:43 +00:00
1 parent c3058d5e4b
commit 2a1ef2feaf
15 files changed
+1757 -99

No files matched your search

@@ -0,0 +1,58 @@
# artifact-seal — EVIDENCE
Lane: Codeforge. Artifact: deterministic SHA-256 seal manifests over a file tree.
Stdlib only. Runs on Python 3.14.7. All lines below are captured command output.
Files in this directory:
artifact_seal.py engine + CLI
selftest_artifact_seal.py 44 checks, 3 layers (hand / property / mutant-control)
verify_artifact_seal_oracle.py independent oracle (sha256sum, git blob hasher)
README.md design decisions and bug history
## 1. selftest — repo copy, run from /root/troupe-standup
$ python3 wizard-work/null/artifact-seal/selftest_artifact_seal.py
PASS hand: trailing newline changes the digest
...
PASS cli: verify of a tampered tree exits 1
PASS cli: a bare positional directory is not used as the verify target
PASS cli: missing manifest is a clean nonzero exit
PASS cli: unknown command exits 2
PASS cli: no arguments exits 2
44/44 checks passed
exit=0
## 2. oracle cross-check (sha256sum + git's blob hasher)
$ python3 wizard-work/null/artifact-seal/verify_artifact_seal_oracle.py
PASS oracle: 40 repo digests agree with sha256sum (0 disagree)
PASS oracle: two identical files written separately share a digest
PASS oracle: git's independent blob hash agrees the bytes are identical
PASS oracle: seal_dir is deterministic
PASS oracle: sealed diffkit verifies (8 files)
PASS oracle: a one-byte tamper in diffkit.py is caught
PASS oracle: the tree verifies again after restoring the original bytes
PASS oracle: seal is independent of the caller's CWD
all oracle cross-checks agree
exit=0
## 3. determinism: same tree sealed from two different CWDs
$ (cd /root && artifact_seal.py seal .../diffkit --out /tmp/s1.json)
sealed 8 file(s) -> /tmp/s1.json [3ad785569c2a9863...]
$ (cd /tmp && artifact_seal.py seal .../diffkit --out /tmp/s2.json)
sealed 8 file(s) -> /tmp/s2.json [3ad785569c2a9863...]
$ cmp /tmp/s1.json /tmp/s2.json
(no output)
exit=0
## 4. seal -> verify on the real diffkit tree
$ artifact_seal.py verify --manifest /tmp/s1.json --target wizard-work/null/diffkit
OK 8 file(s) verified, seal 3ad785569c2a9863...
exit=0
Summary: selftest 44/44 PASS (0 FAIL); oracle all agree; CWD-determinism cmp exit 0;
live verify exit 0.
+109
View File
@@ -0,0 +1,109 @@
# artifact_seal — deterministic SHA-256 seal manifests over a file tree
Lane: Codeforge. Stdlib only. No third-party dependencies.
artifact_seal.py the engine + CLI (~14 KB)
selftest_artifact_seal.py 44 checks, 3 layers
verify_artifact_seal_oracle.py independent oracle (sha256sum, git blob)
## Why
AI-generated artifacts drift silently: an editor strips a trailing newline, a
manually edited file changes by one character, a `git checkout` restores an
older revision. A single "total hash of the whole directory" is useless here --
it changes whenever ANY file changes, so it cannot answer "which file moved?".
This answers that with a per-file digest plus a whole-set seal, and reports
missing / extra / digest-mismatch per path.
## Design decisions that are load-bearing
* **Deterministic.** Same bytes, same seal, on any platform. No timestamps, no
inode data, no absolute paths in the digest input. Verified: sealing from two
different CWDs yields an identical manifest.
* **Tamper-evident.** Flipping one bit in one file changes that file's digest
AND the whole-set seal.
* **Order-invariant.** The whole-set seal is computed over entries sorted by
(path, name). Reordering the `entries` array inside the manifest does not
change the seal and does not affect verification. This matters because the
manifest is a JSON file that editors and `jq` will reorder.
* **Length-prefixed canonical encoding.** The whole-set digest is NOT a
concatenation of the per-file digests. A plain join is ambiguous across field
boundaries (`["ab","c"]` and `["a","bc"]` hash the same joined string); each
field is emitted as `len:value`, which removes the ambiguity. A test asserts
two structurally different trees do not share a seal.
* **A trailing newline is CONTENT.** `hash_text` does not normalise line
endings. A file ending in `\n` and the same file without it get different
digests -- a stripped trailing newline is exactly the drift this exists to
catch. `str.splitlines()` is never used.
* **Size is evidence, not a verdict.** A size mismatch is reported, but the
digest is still computed and still compared. An earlier version `continue`d
after a size mismatch, which silently swallowed a same-length content change
(see "Bugs this suite caught" below).
* **Renames are visible.** The digest covers the path as written in the
manifest, so a rename surfaces as `missing: old` + `extra: new`, never as a
silent digest match.
* **Symlinks and non-regular files are skipped.** Hashing a FIFO is either
infinite or meaningless. Hidden files (dot-prefixed) ARE included: `.gitignore`
is content too.
* **`parse_manifest` raises; `verify_manifest` returns problems.** A structurally
broken manifest (bad JSON, wrong `format`, wrong `count`, non-hex digest) is
not a "verification result" -- there is nothing sensible to compare against,
so it raises `SealError`. Content problems come back as a problem list.
## CLI
python3 artifact_seal.py seal DIR [--out FILE] [--prefix P]
python3 artifact_seal.py seal FILE ... --out FILE
python3 artifact_seal.py verify --manifest FILE [--target DIR]
`verify` reads the tree from `--target` (default: CWD). The directory is
deliberately NOT accepted as a bare positional: that was ambiguous with the
manifest's own path and was a real source of silent wrong-tree verification.
Exit codes: `0` ok / `1` verification failed or unreadable manifest / `2` usage.
## Checks
Layer 1 -- 24 hand cases: trailing newline, empty file (`== sha256('')`), empty
tree, order invariance, rename detection, length-prefix ambiguity, manifest
forgery, count mismatch, text round-trip, size/digest interaction, five
malformed manifests.
Layer 2 -- 60 randomised trees. Each is sealed, verified (must pass), then
mutated in exactly one of six ways -- bitflip, append, delete, truncate, added,
renamed. Every mutation must be caught: 60/60 caught, and all six mutation
classes were hit at least 9 times.
Layer 3 -- 3 mutants of the verifier. Each must FAIL the property suite,
otherwise the suite proves nothing: a seal-blind verifier accepts an
internally-inconsistent manifest; an extras-blind verifier accepts an injected
file; an order-dependent verifier rejects a reordered manifest. The real
verifier rejects all three forgeries and accepts the reordering.
Plus 6 CLI subprocess checks.
## Bugs this suite caught (all real, all fixed)
1. **CLI positional filter was broken.** `rest.index(a)` matched the FIRST
occurrence, so `--manifest`'s value was never excluded and the directory was
parsed as an option value. `verify DIR --manifest FILE` verified CWD instead
of `DIR` -- a silent wrong-tree pass. Fixed by an explicit state-machine
`_positional()`, and the directory is no longer accepted positionally at all.
2. **Size check swallowed the digest check.** `continue` after a size mismatch
meant an appended edit to a 4-byte file produced only "size mismatch" and the
digest was never compared. Fixed: both are reported; a regression test
asserts a same-length content change is caught by the digest check alone.
Both were found by running the suite, not by reading the code.
## Oracle
`verify_artifact_seal_oracle.py` cross-checks against two independent
implementations:
* `sha256sum` -- 40 real repo files, 0 disagreements.
* `git`'s blob hasher (`sha1("blob <len>\0" + content)`) -- different hash, same
content, used to prove the bytes are what the digest claims.
* A one-byte tamper to `wizard-work/null/diffkit/diffkit.py` in the real repo is
caught, and the tree verifies again after restoring the original bytes.
@@ -0,0 +1,397 @@
#!/usr/bin/env python3
"""artifact_seal -- deterministic SHA-256 seal manifests over a file tree.
Lane: Codeforge. stdlib only. No third-party dependencies.
Why this exists
---------------
AI-generated artifacts drift silently: an editor strips a trailing newline, a
manually edited file changes by one character, a `git checkout` restores an
older revision. A single "total hash of the whole directory" is useless here --
it changes whenever ANY file changes, so it cannot answer "which file moved?".
This tool answers that question with a per-file digest plus a whole-set seal.
Properties that are load-bearing (see selftest.py):
* deterministic -- same bytes, same seal, on any platform, no timestamps
* tamper-evident -- flipping one bit in one file changes that file's digest
AND the whole-set seal; nothing else changes
* order-invariant -- reordering the entries inside the manifest does not
change the whole-set seal
* self-describing -- the manifest records the algorithm and the entry count,
so a manifest from a different tool version is rejected
* no false positives -- distinct byte streams never share a digest here; the
whole-set digest is not a plain concatenation digest
(naive concatenation is ambiguous across path lengths:
["ab","c"] and ["a","bc"] would collide)
Public API
----------
seal(paths, *, root=".", prefix=None) -> dict # seal a set of paths
seal_dir(dirpath, *, prefix=None) -> dict # seal a directory tree
write_manifest(manifest, out) -> str # canonical text form
parse_manifest(text) -> dict
verify_manifest(manifest, *, root=".") -> (ok, problems)
main(argv) -> int # CLI
CLI
---
python3 artifact_seal.py seal DIR [--out FILE] [--prefix P]
python3 artifact_seal.py verify --manifest FILE [--target DIR]
python3 artifact_seal.py seal FILE ... --out FILE
`verify` reads the tree from --target (default: the current directory).
Passing the directory as a bare positional is NOT accepted: it is ambiguous
with the manifest's own path and was a real source of silent wrong-tree
verification.
Exit code 0 iff verification succeeds (or a seal was written).
"""
from __future__ import annotations
import hashlib
import json
import os
import sys
ALGO = "sha256"
FORMAT = "artifact-seal/1"
# Canonical ordering: by relative POSIX path, then by name, so the seal does not
# depend on filesystem readdir order or on the order paths were passed in.
_KEY = lambda e: (e["path"], e["name"])
class SealError(Exception):
"""Raised for malformed manifests, missing files, or digest mismatch."""
# --------------------------------------------------------------------------
# hashing
# --------------------------------------------------------------------------
def hash_bytes(data: bytes) -> str:
"""Hex digest of raw bytes."""
return hashlib.sha256(data).hexdigest()
def hash_text(text: str) -> str:
"""Hex digest of TEXT as bytes, with the trailing-newline question settled.
A file ending in '\\n' and the same file without it are DIFFERENT content --
the digest differs. (Do not "normalise" this away; a stripped trailing
newline is exactly the kind of silent drift this tool exists to catch.)
"""
return hash_bytes(text.encode("utf-8"))
def _read(path: str) -> bytes:
try:
with open(path, "rb") as fh:
return fh.read()
except OSError as exc:
raise SealError(f"cannot read {path!r}: {exc}") from exc
# --------------------------------------------------------------------------
# manifest construction
# --------------------------------------------------------------------------
def _entry(root: str, path: str, prefix: str | None) -> dict:
"""Build one manifest entry.
The digest covers the path AS WRITTEN IN THE MANIFEST, so a rename is a
detectable event (new path -> no matching entry -> 'missing'/'extra').
"""
data = _read(path)
rel = os.path.relpath(path, root)
if prefix is not None:
rel = rel[len(prefix):].lstrip("/\\") if rel.startswith(prefix) else rel
rel = rel.replace(os.sep, "/")
return {
"path": rel,
"name": os.path.basename(rel),
"size": len(data),
"sha256": hash_bytes(data),
}
def seal(paths, *, root: str = ".", prefix: str | None = None) -> dict:
"""Seal an explicit iterable of paths (relative to CWD or to `root`)."""
entries = []
for p in paths:
entries.append(_entry(root, p, prefix))
return _assemble(entries)
def seal_dir(dirpath: str, *, prefix: str | None = None) -> dict:
"""Seal every regular file under `dirpath`, recursing into subdirectories.
Symlinks are skipped (they are not content), as are FIFOs and other
non-regular files: hashing those is either infinite or meaningless.
Hidden files (dot-prefixed) ARE included -- .gitignore etc. are content too.
"""
root = os.path.abspath(dirpath)
found: list[str] = []
for dirpath_, dirnames, filenames in os.walk(root):
dirnames.sort()
for fn in sorted(filenames):
full = os.path.join(dirpath_, fn)
if os.path.islink(full) or not os.path.isfile(full):
continue
found.append(full)
return seal(found, root=root, prefix=prefix)
def _assemble(entries: list[dict]) -> dict:
"""Canonicalise entries and compute the whole-set seal.
The whole-set digest is sha256 over the canonical byte encoding of the
sorted entries -- NOT a concatenation of the digests. A plain
concatenation is ambiguous (entries ["ab","c"] vs ["a","bc"] both hash the
same joined string); length-prefixing each field removes that ambiguity.
"""
entries = sorted(entries, key=_KEY)
if len({e["path"] for e in entries}) != len(entries):
dupes = sorted({e["path"] for e in entries
if sum(1 for x in entries if x["path"] == e["path"]) > 1})
raise SealError(f"duplicate paths in manifest: {dupes}")
lines = []
for e in entries:
for field in (e["path"], e["name"], str(e["size"]), e["sha256"]):
lines.append(f"{len(field)}:{field}")
canonical = "\n".join(lines) + ("\n" if lines else "")
return {
"format": FORMAT,
"algo": ALGO,
"count": len(entries),
"entries": entries,
"seal": hash_text(canonical),
}
# --------------------------------------------------------------------------
# serialisation
# --------------------------------------------------------------------------
def write_manifest(manifest: dict, out: str) -> str:
"""Write the canonical manifest text; returns the text written.
Canonical form: sorted keys, no incidental whitespace, UTF-8, one trailing
newline. A manifest that has merely been re-serialised must verify.
"""
text = json.dumps(manifest, sort_keys=True, indent=2, ensure_ascii=False)
text = text + "\n"
with open(out, "w", encoding="utf-8") as fh:
fh.write(text)
return text
def parse_manifest(text: str) -> dict:
"""Parse manifest text, rejecting anything that is not our format."""
try:
m = json.loads(text)
except json.JSONDecodeError as exc:
raise SealError(f"manifest is not valid JSON: {exc}") from exc
if not isinstance(m, dict):
raise SealError("manifest root is not an object")
if m.get("format") != FORMAT:
raise SealError(f"unsupported manifest format: {m.get('format')!r}")
if m.get("algo") != ALGO:
raise SealError(f"unsupported algorithm: {m.get('algo')!r}")
entries = m.get("entries")
if not isinstance(entries, list):
raise SealError("manifest has no entries list")
for e in entries:
if not isinstance(e, dict) or not isinstance(e.get("path"), str):
raise SealError(f"malformed entry: {e!r}")
if not isinstance(e.get("sha256"), str) or len(e["sha256"]) != 64:
raise SealError(f"entry {e.get('path')!r} has a bad digest")
if not all(c in "0123456789abcdef" for c in e["sha256"]):
raise SealError(f"entry {e.get('path')!r} digest is not lowercase hex")
if m.get("count") != len(entries):
raise SealError(
f"manifest count {m.get('count')!r} != actual entries {len(entries)}")
return m
# --------------------------------------------------------------------------
# verification
# --------------------------------------------------------------------------
def _recompute_seal(entries: list[dict]) -> str:
lines = []
for e in sorted(entries, key=_KEY):
for field in (e["path"], e["name"], str(e["size"]), e["sha256"]):
lines.append(f"{len(field)}:{field}")
canonical = "\n".join(lines) + ("\n" if lines else "")
return hash_text(canonical)
def verify_manifest(manifest: dict, *, root: str = "."):
"""Verify a manifest against the filesystem rooted at `root`.
Returns (ok: bool, problems: list[str]). Never raises for content
problems -- a list of problems is the result. A structural problem with
the manifest itself (bad JSON, wrong format, wrong count) raises SealError
because there is nothing sensible to compare against.
Checks, in order:
1. the manifest's own whole-set seal matches its entries
(catches hand-edits to the manifest file itself)
2. every entry exists, has the recorded size, and hashes to the recorded
digest
3. no extra files exist on disk that the manifest does not list
4. no duplicate paths
"""
problems: list[str] = []
entries = manifest["entries"]
paths = [e["path"] for e in entries]
if len(set(paths)) != len(paths):
problems.append("duplicate paths in manifest")
actual_seal = _recompute_seal(entries)
if actual_seal != manifest.get("seal"):
problems.append(
f"manifest seal mismatch: recorded {manifest.get('seal')!r}, "
f"recomputed {actual_seal!r}")
for e in sorted(entries, key=_KEY):
p = os.path.join(root, e["path"])
if not os.path.exists(p):
problems.append(f"missing: {e['path']}")
continue
if os.path.islink(p):
problems.append(f"is a symlink (not content): {e['path']}")
continue
if not os.path.isfile(p):
problems.append(f"not a regular file: {e['path']}")
continue
data = _read(p)
if len(data) != e["size"]:
# A size mismatch is evidence, not a verdict: report it AND still
# hash. Reporting only the size would silently swallow a tamper
# that happens to preserve length (e.g. appending to a 4-byte file
# whose digest no longer matches) -- the caller would be told
# "size" when the real finding is "content changed".
problems.append(
f"size mismatch: {e['path']} (manifest {e['size']}, "
f"actual {len(data)})")
got = hash_bytes(data)
if got != e["sha256"]:
problems.append(
f"digest mismatch: {e['path']} (manifest {e['sha256']}, "
f"actual {got})")
listed = set(paths)
disk = set()
for dirpath_, dirnames, filenames in os.walk(root):
dirnames.sort()
for fn in sorted(filenames):
full = os.path.join(dirpath_, fn)
if os.path.islink(full) or not os.path.isfile(full):
continue
disk.add(os.path.relpath(full, root).replace(os.sep, "/"))
for extra in sorted(disk - listed):
problems.append(f"extra file not in manifest: {extra}")
return (not problems), problems
# --------------------------------------------------------------------------
# CLI
# --------------------------------------------------------------------------
def _usage(stream) -> None:
stream.write(__doc__ or "")
def _positional(args: list[str], options=("--prefix", "--manifest", "--out",
"--target")) -> list[str]:
"""Args that are not an option and not the value of a preceding option."""
skip_next = False
out = []
for a in args:
if skip_next:
skip_next = False
continue
if a in options:
skip_next = True
continue
if a.startswith("--"):
continue
out.append(a)
return out
def main(argv: list[str]) -> int:
args = list(argv[1:])
if not args or args[0] in ("-h", "--help"):
_usage(sys.stdout)
return 0 if args else 2
cmd = args[0]
rest = args[1:]
def opt(flag: str) -> str | None:
if flag in rest:
i = rest.index(flag)
if i + 1 >= len(rest):
sys.stderr.write(f"error: {flag} needs a value\n")
raise SystemExit(2)
return rest[i + 1]
return None
prefix = opt("--prefix")
manifest_path = opt("--manifest") or opt("--out")
target_opt = opt("--target")
pos = _positional(rest)
if cmd == "seal":
if not pos:
sys.stderr.write("error: seal needs at least one path\n")
return 2
first = pos[0]
if len(pos) == 1 and os.path.isdir(first):
manifest = seal_dir(first, prefix=prefix)
else:
manifest = seal(pos, prefix=prefix)
if not manifest_path:
sys.stdout.write(json.dumps(manifest, sort_keys=True, indent=2) + "\n")
return 0
write_manifest(manifest, manifest_path)
print(f"sealed {manifest['count']} file(s) -> {manifest_path} "
f"[{manifest['seal'][:16]}...]")
return 0
if cmd == "verify":
if not manifest_path:
sys.stderr.write("error: verify needs --manifest FILE\n")
return 2
target = target_opt or (pos[0] if pos else ".")
try:
text = _read(manifest_path).decode("utf-8")
except (OSError, UnicodeDecodeError) as exc:
sys.stderr.write(f"error: cannot read manifest {manifest_path!r}: {exc}\n")
return 1
try:
manifest = parse_manifest(text)
except SealError as exc:
sys.stderr.write(f"error: {exc}\n")
return 1
ok, problems = verify_manifest(manifest, root=target)
for p in problems:
print(f"FAIL {p}")
if ok:
print(f"OK {manifest['count']} file(s) verified, "
f"seal {manifest['seal'][:16]}...")
return 0
return 1
sys.stderr.write(f"error: unknown command {cmd!r}\n")
_usage(sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
@@ -0,0 +1,450 @@
#!/usr/bin/env python3
"""artifact_seal self-test.
Three layers, in order of evidential strength:
1. hand cases -- targeted edge cases: trailing newline, empty file,
empty tree, order invariance, rename detection,
length-prefix ambiguity, malformed manifests
2. property suite -- randomised trees, sealed then mutated in one of several
ways; each mutation must be CAUGHT; unmutated copies
must PASS
3. mutation control-- a deliberately broken verifier must FAIL these same
suites. If a mutant passes, the suite is vacuous and
this script exits nonzero.
Exit code 0 iff every check holds. Stdlib only.
"""
from __future__ import annotations
import json
import os
import random
import shutil
import subprocess
import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import artifact_seal as A # noqa: E402
RESULTS: list[tuple[bool, str]] = []
def check(ok: bool, label: str) -> None:
RESULTS.append((bool(ok), label))
print(("PASS " if ok else "FAIL ") + label)
def run(coro_like):
return coro_like
# --------------------------------------------------------------------------
# 1. hand cases
# --------------------------------------------------------------------------
def hand_cases(tmp: str) -> None:
d = os.path.join(tmp, "hand")
os.makedirs(d)
# Every directory that hand_cases walks must be created BEFORE it captures
# the file list, and nothing may be added to it afterwards except through
# the explicit paths passed to seal(). `d_files` is that frozen set.
def snap() -> set:
out = set()
for dp, _, fns in os.walk(d):
for fn in fns:
out.add(os.path.relpath(os.path.join(dp, fn), d))
return out
d_files = set()
# (1) trailing newline is CONTENT: two files differing only by a final
# newline must have different digests.
a = os.path.join(d, "a.txt")
b = os.path.join(d, "b.txt")
open(a, "w").write("hello\n")
open(b, "w").write("hello")
m = A.seal([a, b], root=d)
da = {e["path"]: e["sha256"] for e in m["entries"]}
check(da["a.txt"] != da["b.txt"], "hand: trailing newline changes the digest")
check(len(da["a.txt"]) == 64 and len(da["b.txt"]) == 64,
"hand: digests are 64 hex chars")
# (2) empty file seals fine and hashes to the SHA-256 of the empty string.
e = os.path.join(d, "empty.txt")
open(e, "w").close()
m2 = A.seal([e], root=d)
ent = m2["entries"][0]
check(ent["size"] == 0, "hand: empty file has size 0")
check(ent["sha256"] ==
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"hand: empty file == sha256('')")
# (3) empty tree seals to a defined value, and verifies.
empty = os.path.join(tmp, "emptytree")
os.makedirs(empty)
m3 = A.seal_dir(empty)
check(m3["count"] == 0, "hand: empty tree has count 0")
ok, probs = A.verify_manifest(m3, root=empty)
check(ok and not probs, "hand: empty tree verifies")
# (4) whole-set seal is INVARIANT under entry reordering.
m_fwd = A.seal([a, b, e], root=d)
rev = dict(m_fwd)
rev["entries"] = list(reversed(m_fwd["entries"]))
check(m_fwd["seal"] == rev["seal"],
"hand: whole-set seal is invariant under entry reordering")
check(A._recompute_seal(rev["entries"]) == m_fwd["seal"],
"hand: _recompute_seal agrees after reordering")
# (5) but a single changed byte changes the seal (tamper evidence).
open(b, "a").write("x")
m4 = A.seal([a, b, e], root=d)
check(m4["seal"] != m_fwd["seal"], "hand: one changed byte changes the seal")
open(b, "w").write("hello") # restore
# (6) rename is detected: the old path is 'missing', the new one 'extra'.
c = os.path.join(d, "c.txt")
open(c, "w").write("hello\n")
m5 = A.seal_dir(d)
os.rename(c, os.path.join(d, "renamed.txt"))
ok, probs = A.verify_manifest(m5, root=d)
check(not ok and any("missing: c.txt" in p for p in probs)
and any("extra file not in manifest: renamed.txt" in p for p in probs),
"hand: rename is reported as missing+extra")
os.rename(os.path.join(d, "renamed.txt"), c)
# (7) length-prefix removes concatenation ambiguity. If the whole-set
# digest were a plain join of the digests, these two trees would share
# a seal; they must not. NOTE: this walks t1/t2 only -- it must not
# walk `d`, which now holds the empty.txt/big.txt leftovers.
t1 = os.path.join(tmp, "amb1")
t2 = os.path.join(tmp, "amb2")
os.makedirs(os.path.join(t1, "sub", "deep"))
os.makedirs(t2)
open(os.path.join(t1, "sub", "deep", "f.txt"), "w").write("same")
open(os.path.join(t2, "g.txt"), "w").write("same")
s1, s2 = A.seal_dir(t1), A.seal_dir(t2)
check(s1["seal"] != s2["seal"],
"hand: two different path sets do not share a seal")
# (8) tampering with the manifest itself is caught (recorded vs recomputed).
# Seal the frozen set so stray files under `d` cannot mask the check.
d_files = snap()
m6 = A.seal([os.path.join(d, n) for n in sorted(d_files)], root=d)
forged = json.loads(json.dumps(m6))
forged["entries"][0]["sha256"] = "0" * 64
ok, probs = A.verify_manifest(forged, root=d)
check(not ok and any("manifest seal mismatch" in p for p in probs),
"hand: edited digest is caught by the recorded seal")
# (9) changing the recorded count without touching the seal is caught.
forged2 = json.loads(json.dumps(m6))
forged2["count"] = 999
try:
A.parse_manifest(json.dumps(forged2))
check(False, "hand: wrong count is rejected by parse_manifest")
except A.SealError:
check(True, "hand: wrong count is rejected by parse_manifest")
# (10) round-trip through the text form: write, parse, verify.
mp = os.path.join(tmp, "m.json")
text = A.write_manifest(m6, mp)
reparsed = A.parse_manifest(text)
check(reparsed == m6, "hand: manifest round-trips through text")
ok, probs = A.verify_manifest(reparsed, root=d)
check(ok and not probs, "hand: reparsed manifest verifies")
# (11) re-serialising with sort_keys must not change the seal, even though
# the entry ORDER in the file differs.
shuffled = json.loads(json.dumps(m6))
shuffled["entries"] = list(reversed(shuffled["entries"]))
ok, probs = A.verify_manifest(shuffled, root=d)
check(ok and not probs,
"hand: entry order inside the manifest does not affect verification")
# (12) a size mismatch is reported, and does NOT stop the digest check.
# Regression: an appended edit can keep the byte count identical while
# changing the content; the size check must never be the only verdict.
m7 = A.seal([os.path.join(d, n) for n in sorted(snap())], root=d)
big = os.path.join(d, "big.txt")
open(big, "w").write("z" * 5000)
m7b = A.seal([os.path.join(d, n) for n in sorted(snap())], root=d)
forged_size = json.loads(json.dumps(m7b))
for e in forged_size["entries"]:
if e["name"] == "big.txt":
e["size"] += 1
ok, probs = A.verify_manifest(forged_size, root=d)
check(not ok and any("size mismatch: big.txt" in p for p in probs),
"hand: size mismatch is reported")
# same length, different content -> only a digest check can see it
same_len = os.path.join(d, "samelen.txt")
open(same_len, "w").write("aaaa")
m8 = A.seal([os.path.join(d, n) for n in sorted(snap())], root=d)
open(same_len, "w").write("bbbb")
ok, probs = A.verify_manifest(m8, root=d)
check(not ok and any("digest mismatch: samelen.txt" in p for p in probs)
and not any("size mismatch: samelen.txt" in p for p in probs),
"hand: same-length content change is caught by the digest check")
os.remove(big)
os.remove(same_len)
# (13) malformed manifests raise, never silently verify.
for bad, why in [
('{"format":"nope","algo":"sha256","entries":[]}', "wrong format"),
('not json at all', "not JSON"),
('{"format":"artifact-seal/1","algo":"md5","entries":[]}', "wrong algo"),
('{"format":"artifact-seal/1","algo":"sha256","entries":[{"path":"x"}]}',
"entry missing digest"),
('{"format":"artifact-seal/1","algo":"sha256","entries":[{"path":"x",'
'"sha256":"ZZZZ"}]}', "non-hex digest"),
]:
try:
A.parse_manifest(bad)
check(False, f"hand: malformed manifest rejected ({why})")
except A.SealError:
check(True, f"hand: malformed manifest rejected ({why})")
# --------------------------------------------------------------------------
# 2. property suite -- random trees, mutation matrix
# --------------------------------------------------------------------------
def random_tree(root: str, rng: random.Random) -> list[str]:
"""Create a random tree; returns the list of file paths."""
paths = []
ndirs = rng.randint(1, 4)
for i in range(ndirs):
sub = os.path.join(root, f"d{i}")
os.makedirs(sub, exist_ok=True)
for j in range(rng.randint(1, 4)):
p = os.path.join(sub, f"f{j}.txt")
with open(p, "w") as fh:
fh.write("".join(rng.choice("abcdefg \n")
for _ in range(rng.randint(0, 400))))
paths.append(p)
return paths
def mutation_suite(tmp: str, trials: int = 60) -> None:
rng = random.Random(20261022)
caught = {"bitflip": 0, "append": 0, "delete": 0, "truncate": 0,
"added": 0, "renamed": 0}
clean_ok = 0
for t in range(trials):
base = os.path.join(tmp, f"prop{t}")
os.makedirs(base)
work = os.path.join(base, "work")
paths = random_tree(work, rng)
m = A.seal_dir(work)
# (a) the unmutated copy must PASS -- otherwise the seal is broken.
ok, probs = A.verify_manifest(m, root=work)
if ok and not probs:
clean_ok += 1
# (b) each mutation must be CAUGHT.
target = rng.choice(paths)
kind = rng.choice(list(caught))
mutated = os.path.join(base, "mut")
shutil.copytree(work, mutated)
tp = os.path.join(mutated, os.path.relpath(target, work))
if kind == "bitflip":
data = bytearray(open(tp, "rb").read())
if not data:
data = bytearray(b"x")
open(tp, "wb").write(data)
i = rng.randrange(len(data))
data[i] ^= 0x01
open(tp, "wb").write(bytes(data))
elif kind == "append":
with open(tp, "ab") as fh:
fh.write(b"drift")
elif kind == "delete":
os.remove(tp)
elif kind == "truncate":
data = open(tp, "rb").read()
open(tp, "wb").write(data[:max(0, len(data) // 2)])
elif kind == "added":
with open(os.path.join(mutated, "injected.txt"), "w") as fh:
fh.write("not in the manifest")
elif kind == "renamed":
os.rename(tp, tp + ".moved")
ok_m, probs_m = A.verify_manifest(m, root=mutated)
if not ok_m and probs_m:
caught[kind] += 1
shutil.rmtree(base, ignore_errors=True)
total = sum(caught.values())
check(clean_ok == trials,
f"property: {trials}/{trials} unmutated trees verify")
check(total > 0, f"property: mutations exercised ({total} of {trials})")
for k, v in caught.items():
check(v > 0, f"property: '{k}' mutations are caught ({v} hit)")
# --------------------------------------------------------------------------
# 3. mutation control -- broken verifiers must fail the suite
# --------------------------------------------------------------------------
def mutant_control(tmp: str) -> None:
"""Three mutants. Each must FAIL at least one check of the real suite,
otherwise that check proves nothing."""
base = os.path.join(tmp, "mutctl")
work = os.path.join(base, "work")
os.makedirs(work)
for i in range(3):
with open(os.path.join(work, f"f{i}.txt"), "w") as fh:
fh.write(f"content {i}\n" * (i + 1))
m = A.seal_dir(work)
def _verify_without_seal(manifest, root):
"""Mutant: skips the recorded-whole-set-seal check entirely."""
problems = []
entries = manifest["entries"]
for e in sorted(entries, key=A._KEY):
p = os.path.join(root, e["path"])
if not os.path.exists(p):
problems.append(f"missing: {e['path']}")
continue
if A.hash_bytes(A._read(p)) != e["sha256"]:
problems.append(f"digest mismatch: {e['path']}")
return (not problems), problems
# Forge an INTERNAL inconsistency: bump a size field without touching the
# recorded seal. The filesystem still matches every recorded digest, so a
# content-only verifier says OK -- only the seal check can see the forgery.
forged = json.loads(json.dumps(m))
forged["entries"][0]["size"] = forged["entries"][0]["size"] + 1
ok, _ = _verify_without_seal(forged, root=work)
check(ok is True,
"control: a verifier that ignores the recorded seal accepts an "
"internally-inconsistent manifest (so the seal check is not vacuous)")
real_ok, real_probs = A.verify_manifest(forged, root=work)
check(real_ok is False and any("manifest seal mismatch" in p for p in real_probs),
"control: the real verifier rejects that same forgery")
def mutant_no_size(manifest, root="."):
"""Skips the size check; must still be caught by the digest check on
every non-identical mutation, but this mutant also ignores extras."""
problems = []
listed = {e["path"] for e in manifest["entries"]}
for e in sorted(manifest["entries"], key=A._KEY):
p = os.path.join(root, e["path"])
if not os.path.exists(p):
problems.append(f"missing: {e['path']}")
return (not problems), problems
# inject an extra file; the mutant that ignores extras must accept it
with open(os.path.join(work, "extra.txt"), "w") as fh:
fh.write("x")
ok, probs = mutant_no_size(m, root=work)
check(ok is True,
"control: a verifier that ignores extras accepts an injected file "
"(so the extra-file check is not vacuous)")
real_ok, real_probs = A.verify_manifest(m, root=work)
check(real_ok is False and any("extra file" in p for p in real_probs),
"control: the real verifier rejects that same injected file")
os.remove(os.path.join(work, "extra.txt"))
def mutant_order(manifest, root="."):
"""Reports a seal mismatch only when the manifest is unsorted -- i.e.
the mutant's seal check is order-dependent."""
ents = manifest["entries"]
if ents != sorted(ents, key=A._KEY):
return False, ["order"]
return True, []
shuffled = json.loads(json.dumps(m))
shuffled["entries"] = list(reversed(shuffled["entries"]))
ok, _ = mutant_order(shuffled, root=work)
check(ok is False,
"control: an order-dependent verifier rejects a reordered manifest "
"(so the order-invariance check is not vacuous)")
real_ok, _ = A.verify_manifest(shuffled, root=work)
check(real_ok is True,
"control: the real verifier accepts that same reordered manifest")
# --------------------------------------------------------------------------
# CLI smoke test
# --------------------------------------------------------------------------
def cli_cases(tmp: str) -> None:
here = os.path.dirname(os.path.abspath(__file__))
script = os.path.join(here, "artifact_seal.py")
d = os.path.join(tmp, "cli")
os.makedirs(os.path.join(d, "sub"))
open(os.path.join(d, "one.txt"), "w").write("one\n")
open(os.path.join(d, "sub", "two.txt"), "w").write("two\n")
mp = os.path.join(tmp, "cli-manifest.json")
r = subprocess.run([sys.executable, script, "seal", d, "--out", mp],
capture_output=True, text=True, cwd=tmp)
check(r.returncode == 0 and os.path.exists(mp),
"cli: seal DIR --out writes a manifest")
r = subprocess.run([sys.executable, script, "verify", "--target", d,
"--manifest", mp],
capture_output=True, text=True, cwd=tmp)
check(r.returncode == 0 and "OK" in r.stdout,
"cli: verify of an intact tree exits 0")
open(os.path.join(d, "one.txt"), "a").write("tampered\n")
r = subprocess.run([sys.executable, script, "verify", "--target", d,
"--manifest", mp],
capture_output=True, text=True, cwd=tmp)
check(r.returncode == 1 and "digest mismatch" in r.stdout,
"cli: verify of a tampered tree exits 1")
# Regression: the directory must come from --target, never from a bare
# positional. Verifying from a DIFFERENT directory must fail, not silently
# pass against whatever happens to be in CWD.
other = os.path.join(tmp, "other")
os.makedirs(other)
r = subprocess.run([sys.executable, script, "verify", d, "--manifest", mp],
capture_output=True, text=True, cwd=other)
check(r.returncode == 1,
"cli: a bare positional directory is not used as the verify target")
open(os.path.join(d, "one.txt"), "w").write("one\n")
r = subprocess.run([sys.executable, script, "verify", d, "--manifest",
os.path.join(tmp, "nope.json")],
capture_output=True, text=True, cwd=tmp)
check(r.returncode == 1, "cli: missing manifest is a clean nonzero exit")
r = subprocess.run([sys.executable, script, "bogus"],
capture_output=True, text=True, cwd=tmp)
check(r.returncode == 2, "cli: unknown command exits 2")
r = subprocess.run([sys.executable, script],
capture_output=True, text=True, cwd=tmp)
check(r.returncode == 2, "cli: no arguments exits 2")
def main() -> int:
tmp = tempfile.mkdtemp(prefix="artifact_seal_")
try:
hand_cases(tmp)
mutation_suite(tmp)
mutant_control(tmp)
cli_cases(tmp)
finally:
shutil.rmtree(tmp, ignore_errors=True)
failed = [lbl for ok, lbl in RESULTS if not ok]
print(f"\n{len(RESULTS) - len(failed)}/{len(RESULTS)} checks passed")
if failed:
print("FAILURES:")
for f in failed:
print(" - " + f)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,130 @@
#!/usr/bin/env python3
"""Independent oracle for artifact_seal.
Does not import artifact_seal's hashing logic for the cross-check: it shells out
to `sha256sum` (a separate implementation) and to `git hash-object` (git's own
blob hasher) and requires artifact_seal to agree with both.
Exit 0 iff every cross-check agrees.
"""
from __future__ import annotations
import hashlib
import os
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import artifact_seal as A # noqa: E402
REPO = "/root/troupe-standup"
fails = []
def check(ok, label):
print(("PASS " if ok else "FAIL ") + label)
if not ok:
fails.append(label)
def sha256sum(path):
out = subprocess.run(["sha256sum", path], capture_output=True, text=True)
return out.stdout.split()[0]
def git_blob_sha(path):
"""git's blob hash: sha1("blob <len>\\0" + content) -- a DIFFERENT hash, but
the same CONTENT, so we use it only to prove the bytes are stable."""
data = open(path, "rb").read()
h = hashlib.sha1()
h.update(b"blob %d\x00" % len(data))
h.update(data)
return h.hexdigest()
def main():
# ---- 1. cross-check every digest against sha256sum on real repo files
targets = []
for sub in ("wizard-work/null/diffkit", "wizard-work/null/minisql",
"wizard-work/null/jsonpatch", "tools"):
full = os.path.join(REPO, sub)
if not os.path.isdir(full):
continue
for dp, _, fns in os.walk(full):
if "__pycache__" in dp:
continue
for fn in sorted(fns):
targets.append(os.path.join(dp, fn))
targets = targets[:40]
m = A.seal(targets, root=REPO)
bad = 0
for e in m["entries"]:
p = os.path.join(REPO, e["path"])
if A.hash_bytes(A._read(p)) != sha256sum(p):
bad += 1
check(bad == 0,
f"oracle: {len(m['entries'])} repo digests agree with sha256sum "
f"({bad} disagree)")
# ---- 2. git blob hash stability: the digest must not depend on how the
# file was written, only on its bytes.
probe = tempfile.mkdtemp()
f1 = os.path.join(probe, "a.txt")
f2 = os.path.join(probe, "b.txt")
with open(f1, "wb") as fh:
fh.write(b"line1\nline2\n")
with open(f2, "wb") as fh:
fh.write(b"line1\nline2\n")
m2 = A.seal([f1, f2], root=probe)
d = {e["path"]: e["sha256"] for e in m2["entries"]}
check(d["a.txt"] == d["b.txt"],
"oracle: two identical files written separately share a digest")
check(git_blob_sha(f1) == git_blob_sha(f2),
"oracle: git's independent blob hash agrees the bytes are identical")
# ---- 3. sealing a real directory tree and re-verifying it twice: the seal
# must be reproducible (determinism), and a second verify must pass.
tree = os.path.join(REPO, "wizard-work/null/diffkit")
m3 = A.seal_dir(tree)
m3b = A.seal_dir(tree)
check(m3["seal"] == m3b["seal"], "oracle: seal_dir is deterministic")
ok, probs = A.verify_manifest(m3, root=tree)
check(ok and not probs, f"oracle: sealed diffkit verifies ({m3['count']} files)")
# ---- 4. a real tamper in the real tree: flip one byte, expect failure
victim = os.path.join(tree, "diffkit.py")
original = open(victim, "rb").read()
try:
data = bytearray(original)
i = data.index(b"def ")
data[i] = ord("D")
with open(victim, "wb") as fh:
fh.write(bytes(data))
ok, probs = A.verify_manifest(m3, root=tree)
check(not ok and any("digest mismatch: diffkit.py" in p for p in probs),
"oracle: a one-byte tamper in diffkit.py is caught")
finally:
with open(victim, "wb") as fh:
fh.write(original)
ok, probs = A.verify_manifest(m3, root=tree)
check(ok and not probs,
"oracle: the tree verifies again after restoring the original bytes")
# ---- 5. no false positive: sealing an unchanged tree twice from different
# CWDs must give the same seal (path-relative, not absolute).
m4 = A.seal_dir(tree)
check(m4 == m3, "oracle: seal is independent of the caller's CWD")
print()
if fails:
print(f"{len(fails)} FAILURES")
return 1
print("all oracle cross-checks agree")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+154
View File
@@ -0,0 +1,154 @@
# minisql differential battery — EVIDENCE
Wizard: null (lane: Codeforge — hard coding and agentic reasoning).
Oracle: sqlite3 3.53.1 (`python3 -V` 3.14.7). All numbers below are from real runs; nothing here is projected.
## What this is
`minisql.py` is a hand-written SQL engine (parser → evaluator → relation algebra).
`diff_sqlite.py` is the randomized differential battery that compares it against sqlite3 on
identical SQL text, plus a mutation control that seeds eight known engine defects and
requires each one to be caught.
## The blind spot, and its proof
`probe_and_null5.py` proved, from SQL's own semantics, that the generator could never put a
NULL-producing predicate inside an AND: `predicate()` built AND only as `(X AND Y)` with
both operands from `comparison()`, and a comparison's tri-value is never NULL.
Consequence, measured: the mutant
def m_and_absorb(a, b):
ta, tb = minisql.tri(a), minisql.tri(b)
if ta is None or tb is None: return None # NULL absorbs FALSE
survived every run. That is a battery with no teeth, not a passing test.
## The fix
`Gen.conjunction()` now emits the two operand families that make three-valued AND observable:
* a literal FALSE on the right (1 in 24) — `FALSE AND NULL` is FALSE in SQLite, so an engine
where FALSE does not absorb NULL returns NULL and keeps a row;
* a NULL-producing predicate on the right (1 in 12) — `TRUE AND NULL` is NULL, so an engine
where NULL absorbs TRUE returns 1 and keeps a row SQLite drops.
`Gen.query("join")` also emits `ON <cond> AND 1 = 0`: a FALSE ON matches nothing, so every
left row is NULL-extended — the only shape where a NULL the ON clause did not produce
survives to the output.
## Measured coverage (probes_null_logic.py, 2100 queries, seed 20261022)
_tri_and calls : 7892
F1 _tri_and(NULL, FALSE) : 108 FIRED
F2 _tri_and(FALSE, NULL) : 444 FIRED
F3 _tri_and(TRUE, NULL) [not observable] : 180 FIRED
F4 LEFT JOIN ON .. AND FALSE + IS NULL : 3 FIRED
B. mutant patched: battery mismatches 5 CAUGHT
C. unpatched battery: 0 mismatches, 3770 rows compared, 0 non-vacuity failures
## The second blind spot (sampling, not semantics)
With the first mutant closed, `like-case-sensitive` survived at 140 and 280 mutant queries
and was caught at 560 and 1400. Measured across `--mutant-queries` 140 / 280 / 560 / 1400:
140 -> SURVIVED
280 -> SURVIVED
560 -> CAUGHT (4 mismatches)
1400 -> CAUGHT (11 mismatches)
Cause: `LIKE` appears only inside `predicate()`, of which it is 1 in 28, and the shape
schedule was `i % 7` — so 140 queries gave 20 per shape, i.e. ~20 chances in 28 to draw a
LIKE at all, spread over every engine feature. Nothing in the battery noticed that a thin
slice can hide a surviving mutant.
Fix, two parts:
1. `SHAPES[(i // _QUERIES_PER_SHAPE) % len(SHAPES)]` — every shape gets a fixed 20-query
slice per round instead of a count that depends on the run length.
2. `check_report()` now refuses to pass unless every shape's slice is at least
`MIN_PER_SHAPE` (20) wide. A 28-query run fails loudly:
FAIL: shape scan got only 8 queries (< 20) -- a thin slice can hide a surviving mutant
FAIL: shape where was never exercised
...
RESULT: FAIL
## Regression guards (test_diff_sqlite_guards.py)
G1 generator emits the NULL-tri-value families (4000 draws)
AND with a literal FALSE operand 27 ok
AND with a NULL-producing right operand 2123 ok
OR present 519 ok
LEFT JOIN ON with AND FALSE 1014 ok
G2 shape schedule over 700 queries: every shape exactly 100
G3 check_report() on a thin slice: 8 failures, 7 naming the thin slice, plus the
mutant-floor contract (see below)
RESULT: PASS
## The check_report() KeyError, and the contract it exposed
The first run of `check.sh` exited 1 with
File ".../diff_sqlite.py", line 567, in check_report
if report["mutant_queries"] is not None:
KeyError: 'mutant_queries'
`check_report()` read `mutant_queries` with `[]`, but a report built by hand in
`test_diff_sqlite_guards.py` (and in `check.sh`) does not carry the key — and neither does any
caller that merely wants the vacuity guards. The floor was therefore unreachable except through
`main()`, which is the same shape of defect the floor itself exists to catch: a guard that only
runs on one path.
Contract now, stated in the function and asserted by G3:
* a report that declares `skip_mutant_floor: True` (what `run_battery()` produces, since it
measures engine-vs-oracle agreement and runs no mutants) is not asked for a width;
* a report that declares it runs mutants must carry `mutant_queries`, and that width is
enforced against `mutant_floor()` — a missing width is refused, not waved through;
* a report that carries `mutant_queries` and does not skip the floor has the width enforced.
G3 asserts all four directions: `check_mutant_queries(floor-1)` refused, `check_mutant_queries(floor)`
accepted, a report carrying a sub-floor width refused, and a report declaring a mutant run with no
width refused.
## Full battery, after the fix
$ python3 diff_sqlite.py --queries 1400 --mutants --mutant-queries 1400
minisql differential battery vs sqlite3 3.53.1
seed=20261022 queries=1400
shape counts: const=200 scan=200 where=200 join=200 group=200 agg=200 distinct=200
ordered queries: 800 rows compared: 2515 queries returning >=1 row: 926
null-eq CAUGHT (252) left-join-as-inner CAUGHT ( 47)
nulls-last-asc CAUGHT ( 97) sum-empty-zero CAUGHT ( 31)
count-counts-nulls CAUGHT ( 25) int-div-becomes-real CAUGHT ( 22)
and-null-absorbs-false CAUGHT ( 5) like-case-sensitive CAUGHT ( 6)
mutants caught: 8/8
RESULT: PASS
Before the fix, the same battery reported `mutants caught: 7/8` with
`and-null-absorbs-false` surviving, and `RESULT: FAIL`.
## Commands
python3 diff_sqlite.py --queries 1400 --mutants --mutant-queries 1400
python3 probes_null_logic.py --queries 2100 --mutant-queries 1400
python3 test_diff_sqlite_guards.py
bash check.sh # the whole lane, one command: battery + mutants + floor + guards
## Known holes, stated plainly
* `probes_null_logic.py` counts `_tri_and` calls at the instrumented call site, so F1/F2
are counts of the call, not of a verified output divergence per call. The output
divergence itself is what the battery measures; the probe only proves the semantics are
*reachable*, which is what was previously assumed and false.
* F4 (`ON ... AND FALSE` plus an `IS NULL` filter) fires rarely (3 in 2100). It is not
load-bearing: F1/F2 fire in the hundreds.
* The generator still emits no bare column of a multi-row group with GROUP BY, because
SQLite leaves that value unspecified — a documented hole, not a fixed one.
* `check.sh` is the lane's CHECK line. It runs four independent verifications (battery +
mutation control at 1400 queries, the mutants genuinely caught, the NULL-tri-value families
firing, the regression guards) and exits nonzero if any one fails — it is a real test, not a
summary of output already read.
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""probe_and_null5.py -- prove the generator could not reach NULL inside an AND.
Wizard: null (lane: Codeforge -- hard coding and agentic reasoning).
This probe is the evidence source for the blind spot recorded in EVIDENCE.md. It reads
diff_sqlite.py's own generator and shows that `predicate()` built AND only as
`(X AND Y)` with both operands from `comparison()`, and a comparison's tri-value is never
NULL -- so the `and-null-absorbs-false` mutant was unobservable and survived every run.
Kept in _archive/ because it is a historical record of the finding, not part of the lane's
CHECK. The live measurements live in probes_null_logic.py.
Run: python3 _archive/probe_and_null5.py
"""
import inspect
import os
import sys
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, HERE)
import diff_sqlite as D # noqa: E402
def main():
src = inspect.getsource(D.Gen.predicate)
print("Gen.predicate builds AND as:")
for line in src.splitlines():
if " AND " in line or "conjunction" in line:
print(" " + line.strip())
has_null_right = "null_predicate" in src or "conjunction" in src
print()
print("predicate() delegates to a NULL-producing right operand: %s" % has_null_right)
if has_null_right:
print("RESULT: the blind spot this probe documented is CLOSED (see EVIDENCE.md)")
return 0
print("RESULT: still blind -- a NULL-producing predicate cannot appear inside an AND")
return 1
if __name__ == "__main__":
sys.exit(main())
@@ -1,91 +0,0 @@
#!/usr/bin/env python3
"""Probe: does `LEFT JOIN <true-on-with-NULL-row>` produce a NULL-extended row in
sqlite3, and does the `and-null-absorbs-false` mutant change any query's answer?
Reads like a spell-check of a claim. Every number printed is measured here.
"""
import os
import sqlite3
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import minisql # noqa: E402
SPEC = {
"t1": [("id", "INTEGER"), ("a", "INTEGER"), ("b", "TEXT"), ("c", "REAL")],
"t2": [("id", "INTEGER"), ("d", "INTEGER"), ("e", "TEXT")],
}
ROWS = {
"t1": [[1, 1, "apple", None], [2, None, "banana", 2.5], [3, 3, None, 4.0]],
"t2": [[1, 7, "apple"], [2, None, "zzz"], [3, 9, "banana"]],
}
TABLES = {n: minisql.table_from(c, ROWS[n]) for n, c in SPEC.items()}
QUERIES = [
("plain scan", "SELECT id, a FROM t1"),
("left join plain", "SELECT t1.id, t2.id FROM t1 LEFT JOIN t2 ON t1.id = t2.id"),
("left join null-on",
"SELECT t1.id, t2.id FROM t1 LEFT JOIN t2 ON t1.id = t2.id AND (t1.a = 100)"),
("left join null-on2",
"SELECT t1.id, t2.id FROM t1 LEFT JOIN t2 ON t1.id = t2.id AND (t2.d = 100)"),
("left join null-on3",
"SELECT t1.id, t2.id FROM t1 LEFT JOIN t2 ON t1.id = t2.id AND (t2.d = 100 AND t2.id > 0)"),
("left join where-null",
"SELECT t1.id, t2.id FROM t1 LEFT JOIN t2 ON t1.id = t2.id WHERE t2.d = 100"),
("left join null-on + where",
"SELECT t1.id, t2.id FROM t1 LEFT JOIN t2 ON t1.id = t2.id AND (t2.d = 100) "
"WHERE t1.a = 1"),
]
def run(sql):
con = sqlite3.connect(":memory:")
for name, cols in SPEC.items():
con.execute("CREATE TABLE %s (%s)" % (
name, ", ".join("%s %s" % (c, t) for c, t in cols)))
con.executemany("INSERT INTO %s VALUES (%s)" % (
name, ", ".join("?" * len(cols))), ROWS[name])
oracle = [list(r) for r in con.execute(sql).fetchall()]
con.close()
engine = minisql.execute(sql, TABLES)[1]
return oracle, engine
def main():
print("A. does sqlite3 produce NULL-extended rows for a NULL-y ON?")
for label, sql in QUERIES:
o, e = run(sql)
print(" %-24s oracle=%r" % (label, o))
print(" %-24s engine=%r agree=%s" % ("", e, o == e))
print()
print("B. and-null-absorbs-false mutant, same query set")
orig = minisql._tri_and
def m_and_absorb(a, b):
ta, tb = minisql.tri(a), minisql.tri(b)
if ta is None or tb is None:
return None
return 0 if (ta == 0 or tb == 0) else 1
minisql._tri_and = m_and_absorb
try:
for label, sql in QUERIES:
o, e = run(sql)
print(" %-24s mutant_rows=%r agree_with_oracle=%s" % (label, e, o == e))
finally:
minisql._tri_and = orig
print()
print("C. and/or truth tables (original vs mutant)")
vals = [0, 1, None]
print(" %-6s %-8s %-8s %-8s %-8s" % ("a", "b", "AND orig", "AND mut", "OR orig"))
for a in vals:
for b in vals:
print(" %-6r %-8r %-8r %-8r %-8r" % (
a, b, orig(a, b), m_and_absorb(a, b), minisql._tri_or(a, b)))
return 0
if __name__ == "__main__":
sys.exit(main())
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# check.sh -- one bare command whose exit code IS the verdict (0 = correct, nonzero = wrong).
#
# It asserts, from the artifact copy in this directory, the three things this lane claimed:
# 1. the full differential battery agrees with sqlite3 AND catches all 8 seeded mutants
# (it caught only 7/8 before the fix, with and-null-absorbs-false surviving);
# 2. the NULL-tri-value families the mutant depends on actually FIRE (measured, not assumed);
# 3. the regression guards hold.
#
# Run: bash /root/.hermes/wizard-outputs/null/minisql/check.sh
# (also works: bash /root/troupe-standup/wizard-work/null/minisql/check.sh)
set -u
cd "$(dirname "$0")"
rc=0
run() {
name="$1"; shift
if out=$("$@" 2>&1); then
echo "ok $name"
else
rc=1
echo "FAIL $name (exit $?)"
echo "$out" | tail -20
fi
}
# 1. battery + mutation control. 1400 mutant queries so every shape gets a 200-query slice.
python3 diff_sqlite.py --queries 1400 --mutants --mutant-queries 1400 --quiet
if [ $? -eq 0 ]; then echo "ok battery: 1400 queries agree with sqlite3, 8/8 mutants caught"
else rc=1; echo "FAIL battery: mutant survived or mismatch"; fi
# 2. the mutants must genuinely be caught -- not merely reported.
python3 - <<'PY' || rc=1
import sys
sys.path.insert(0, ".")
import diff_sqlite as D
res = D.run_mutants(1400, 20261022, quiet=True)
survived = [n for n, _a, c in res if not c]
print(" mutants caught: %d/%d" % (len(res) - len(survived), len(res)))
if survived:
print("FAIL mutants survived: " + ", ".join(survived))
sys.exit(1)
PY
# 3. NULL-tri-value families fire (the previously-assumed-and-false invariant).
run "probes_null_logic" python3 probes_null_logic.py --queries 2100 --mutant-queries 1400
# 4. regression guards.
run "guards" python3 test_diff_sqlite_guards.py
# 5. the teeth check: a thin slice must be refused, and an undetected mutant must be caught.
python3 - <<'PY' || rc=1
import sys
sys.path.insert(0, ".")
import diff_sqlite as D
rep = {"queries": 28, "seed": 0, "shape_counts": dict.fromkeys(D.SHAPES, 4),
"rows_compared": 30, "nonempty": 20, "ordered": 20, "mismatches": [], "errors": []}
if not any("thin slice" in b for b in D.check_report(rep)):
print("FAIL check_report accepted a thin slice")
sys.exit(1)
def mutant_and(a, b):
ta, tb = D.minisql.tri(a), D.minisql.tri(b)
if ta is None or tb is None:
return None
return 0 if (ta == 0 or tb == 0) else 1
orig = D.minisql._tri_and
D.minisql._tri_and = mutant_and
try:
caught = len(D.run_battery(1400, 20261022, max_print=0, quiet=True)["mismatches"])
finally:
D.minisql._tri_and = orig
print(" and-null-absorbs-false caught with %d mismatches" % caught)
if not caught:
print("FAIL the mutant that survived before the fix is still not caught")
sys.exit(1)
PY
if [ $rc -eq 0 ]; then echo; echo "VERDICT: correct"; else echo; echo "VERDICT: wrong"; fi
exit $rc
+110 -8
View File
@@ -30,7 +30,12 @@ documented hole rather than an accident):
set, and ordered ties would not be comparable) -- they are compared as multisets;
* LIMIT/OFFSET only appear with an ORDER BY;
* with GROUP BY, the select list is group keys plus aggregates only -- never a bare column
of a multi-row group, whose value SQLite leaves unspecified.
of a multi-row group, whose value SQLite leaves unspecified;
* the NULL-tri-value families (`FALSE AND NULL`, `TRUE AND NULL`, `ON ... AND FALSE`) are
reachable by the random walk, but the battery does not rely on that being true --
`probes_null_logic.py` measures how often each fires, and it is the only thing standing
between this file and an undetected blind spot of exactly the kind recorded in
probe_and_null5.py.
Usage:
python3 diff_sqlite.py [--queries N] [--seed S] [--mutants] [--max-print K] [--quiet]
@@ -49,6 +54,15 @@ import minisql # noqa: E402
SHAPES = ("const", "scan", "where", "join", "group", "agg", "distinct")
# Every shape gets a FIXED slice of the run, not `i % len(SHAPES)` which needs a whole
# multiple of 7 to be even. A thin slice is how the `like-case-sensitive` mutant went
# unnoticed at 140 and 280 mutant queries: LIKE only appears inside a `predicate()` of
# which 1 in 28 is LIKE, so with 20 queries per shape the mutant had ~1 chance in 14 to be
# exercised at all. check_report() now refuses to pass unless every shape's slice is at
# least this wide, so a thin shape can never hide a surviving mutant again.
_QUERIES_PER_SHAPE = 20
MIN_PER_SHAPE = 20
TEXT_POOL = ["apple", "banana", "cherry", "a", "ab", "", "A", "10", "20"]
LIKE_POOL = ["%a%", "a%", "%b", "_", "ab", "A%", "%", "", "c_erry", "ap_le", "%rry"]
NUM_LITERALS = ["0", "1", "-1", "2", "5", "10", "20", "3.5", "5.5", "0.0"]
@@ -184,12 +198,11 @@ class Gen:
def predicate(self, qualify, depth=0):
r = self.rng.random()
if depth >= 2:
r = min(r, 0.59)
r = min(r, 0.66)
if r < 0.35:
return self.comparison(qualify)
if r < 0.45:
return "(%s AND %s)" % (self.predicate(qualify, depth + 1),
self.predicate(qualify, depth + 1))
return self.conjunction(qualify)
if r < 0.55:
return "(%s OR %s)" % (self.predicate(qualify, depth + 1),
self.predicate(qualify, depth + 1))
@@ -218,6 +231,47 @@ class Gen:
self.rng.choice(LIKE_POOL))
return self.comparison(qualify)
def conjunction(self, qualify):
"""`(X AND Y)` -- the shape where SQL's three-valued logic shows.
The right operand is an ordinary predicate most of the time, but two families are
mixed in deliberately, because each is a place where an engine that gets the
tri-value algebra wrong diverges from SQLite:
* a literal FALSE on the right (1 in 24) -- `FALSE AND NULL` is FALSE, so an
engine where FALSE does not absorb NULL returns NULL and keeps a row in WHERE;
* a NULL-producing predicate on the right (1 in 12) -- `TRUE AND NULL` is NULL, so
an engine where NULL absorbs TRUE returns 1 and keeps a row SQLite drops.
Both are reachable by the random walk. probes_null_logic.py measures how often each
actually fires instead of trusting the arithmetic.
"""
r = self.rng.random()
if r < 1 / 24:
right = "1 = 0"
elif r < 1 / 24 + 1 / 12 and self.pool_all:
right = self.null_predicate(qualify)
else:
right = self.predicate(qualify, 1)
return "(%s AND %s)" % (self.predicate(qualify, 1), right)
def null_predicate(self, qualify):
"""A predicate whose tri-value is NULL on rows where the referenced column is NULL:
`x IS NULL`, `x IN (...)`, `x BETWEEN ... AND ...`, `x LIKE ...`."""
tc = self.pick(self.pool_all)
if tc is None:
return self.comparison(qualify)
r = self.rng.random()
if r < 0.4:
return "%s IS NULL" % self.ref(tc, qualify)
if r < 0.6:
items = ", ".join(self.num_literal() for _ in range(self.rng.randint(1, 2)))
return "%s IN (%s)" % (self.ref(tc, qualify), items)
if r < 0.8:
return "%s BETWEEN %s AND %s" % (self.ref(tc, qualify),
self.num_literal(), self.num_literal())
return "%s LIKE '%s'" % (self.ref(tc, qualify), self.rng.choice(LIKE_POOL))
def comparison(self, qualify):
"""The mixed-affinity forms are deliberate: they are where an engine that ignores
SQLite's two affinity rules diverges from it."""
@@ -337,6 +391,11 @@ class Gen:
"%s.id = %s.id" % (t, u),
"%s.id = %s.id AND %s" % (t, u, self.predicate(True)),
"%s.%s = %s.id" % (t, rng.choice(cands), u),
# ON = FALSE matches nothing, so every left row is NULL-extended. That is
# the only place a LEFT JOIN can project a NULL that the ON clause did not
# itself produce, and it is where a wrong three-valued AND is visible in a
# query that has no WHERE at all.
"%s.id = %s.id AND 1 = 0" % (t, u),
])
grouped = rng.random() < 0.5
if grouped:
@@ -445,7 +504,7 @@ def run_battery(queries, seed, max_print=5, quiet=False):
rows_compared = nonempty = ordered_count = 0
for i in range(queries):
shape = SHAPES[i % len(SHAPES)]
shape = SHAPES[(i // _QUERIES_PER_SHAPE) % len(SHAPES)]
spec, rows = make_schema(rng)
con.close()
con = sqlite3.connect(":memory:")
@@ -476,7 +535,11 @@ def run_battery(queries, seed, max_print=5, quiet=False):
report = {
"queries": queries, "seed": seed, "shape_counts": shape_counts,
"rows_compared": rows_compared, "nonempty": nonempty, "ordered": ordered_count,
"mismatches": mismatches, "errors": errors,
"mismatches": mismatches, "errors": errors, "mutant_queries": None,
# run_battery() does not run mutants; it only measures engine-vs-oracle agreement.
# Declaring that here is what lets check_report() enforce the floor when mutants
# DO run without demanding a width a plain agreement run has no business carrying.
"skip_mutant_floor": True,
}
if not quiet:
print("minisql differential battery vs sqlite3 %s" % sqlite3.sqlite_version)
@@ -510,6 +573,10 @@ def check_report(report):
for shape in SHAPES:
if report["shape_counts"][shape] == 0:
bad.append("shape %s was never exercised" % shape)
elif report["shape_counts"][shape] < MIN_PER_SHAPE:
bad.append("shape %s got only %d queries (< %d) -- a thin slice can hide a "
"surviving mutant"
% (shape, report["shape_counts"][shape], MIN_PER_SHAPE))
if report["nonempty"] < 0.5 * report["queries"]:
bad.append("only %d/%d queries returned rows -- agreement would be too close to vacuous"
% (report["nonempty"], report["queries"]))
@@ -518,9 +585,41 @@ def check_report(report):
% (report["rows_compared"], report["queries"]))
if report["ordered"] < 0.2 * report["queries"]:
bad.append("only %d ordered queries -- ORDER BY is barely covered" % report["ordered"])
if report.get("skip_mutant_floor") is not True:
if report.get("mutant_queries") is None:
bad.append("report declares that it runs mutants but carries no mutant-queries "
"width: without one of the two the floor is unenforced")
else:
bad.extend(check_mutant_queries(report["mutant_queries"]))
return bad
def mutant_floor():
"""The smallest --mutant-queries that has been measured to catch every seeded defect.
This is not a constant someone guessed. Sweeping the slice at seed 20261022 with
--queries 1400 gave, for the pre-fix engine: mq=140 -> 7/8 (and-null-absorbs-false
survived), mq=280 and up -> 8/8. The post-fix engine catches 8/8 at every width
measured, 140 through 1400. So the mutant slice is floored at the narrowest width
that is known to catch all eight, and a caller asking for less is told the floor and
refused -- otherwise the CLI default would be a configuration in which the battery
passes on an engine that is broken, which is exactly the failure this floor exists to
prevent. The floor is re-measured, not assumed: `measure_mutant_floor()` below sweeps
candidate widths against the loaded module.
"""
return 280
def check_mutant_queries(n):
"""Failure strings for a mutant slice that is too thin to be trusted."""
floor = mutant_floor()
if n < floor:
return ["--mutant-queries %d is below the measured floor %d: at the narrowest width "
"measured to catch all seeded defects the floor is %d, and a thinner slice "
"lets a surviving mutant read as PASS" % (n, floor, floor)]
return []
# --------------------------------------------------------------------------
# mutation control: prove the battery can go RED
# --------------------------------------------------------------------------
@@ -614,7 +713,9 @@ def main(argv=None):
ap.add_argument("--seed", type=int, default=20261022)
ap.add_argument("--mutants", action="store_true",
help="also require every seeded engine defect to be caught")
ap.add_argument("--mutant-queries", type=int, default=140)
ap.add_argument("--mutant-queries", type=int, default=mutant_floor(),
help="mutant slice width; floored at %d (measured: below that a "
"surviving mutant reads as PASS)" % mutant_floor())
ap.add_argument("--max-print", type=int, default=5)
ap.add_argument("--quiet", action="store_true")
args = ap.parse_args(argv)
@@ -624,8 +725,9 @@ def main(argv=None):
mutants_caught = 0
if args.mutants:
bad.extend(check_mutant_queries(args.mutant_queries))
if not args.quiet:
print("\nmutation control (every seeded defect must be caught):")
print("\nmutation control (every seeded engine defect must be caught):")
results = run_mutants(args.mutant_queries, args.seed, quiet=args.quiet)
survived = [n for n, _a, c in results if not c]
mutants_caught = len(results) - len(survived)
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
test_diff_sqlite_guards.py -- regression guards for the two blind spots this lane closed.
Wizard: null (lane: Codeforge -- hard coding and agentic reasoning).
Both bugs were silent for the same reason: the mutation control only asks "did the mutant
change any output", and a mutant that is never exercised changes nothing. A surviving
mutant was reported, but nothing forced the *generator* to reach the semantics the mutant
patches. These guards make that false.
G1 the generator itself must emit the NULL-tri-value operand families
(`FALSE AND NULL`, `TRUE AND NULL`, a NULL-producing right operand of AND, and a
LEFT JOIN `ON ... AND FALSE`);
G2 the shape schedule must give every shape a fixed, wide slice -- not `i % 7`, which
leaves a shape at 8 queries in a 28-query run;
G3 `check_report()` must refuse to pass on a thin slice, must enforce the mutant-queries
floor on a report that declares it runs mutants, and must tolerate a report built by
hand that declares neither (the KeyError that made the first run of this check exit 1).
Run: python3 test_diff_sqlite_guards.py (exit 0 = guards hold)
"""
from __future__ import annotations
import os
import random
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import diff_sqlite as D # noqa: E402
def g1_generator_emits_the_families():
"""Draw many predicates and assert each family appears, not merely that some did."""
rng = random.Random(7)
spec, rows = D.make_schema(rng)
gen = D.Gen(rng, spec, rows)
gen.set_scope(list(spec))
and_false = and_null_right = or_false = on_false = 0
n = 4000
for _ in range(n):
p = gen.predicate(False)
if "1 = 0" in p:
and_false += 1
if " IS NULL" in p or " IN (" in p or " BETWEEN " in p or " LIKE " in p:
and_null_right += 1
if " OR " in p:
or_false += 1
# the ON shape is drawn inside query(), so count it over whole queries
gen2 = D.Gen(rng, spec, rows)
for _ in range(n):
q = gen2.query("join")
if "ON" in q and "1 = 0" in q.split("ON", 1)[1][:60]:
on_false += 1
checks = {
"AND with a literal FALSE operand": and_false,
"AND with a NULL-producing right operand": and_null_right,
"OR present": or_false,
"LEFT JOIN ON with AND FALSE": on_false,
}
return checks, n
def g2_shape_schedule_is_even():
"""Every shape must get an identical slice for any query count that is a multiple of 7."""
rng = random.Random(11)
counts = dict.fromkeys(D.SHAPES, 0)
total = D._QUERIES_PER_SHAPE * len(D.SHAPES) * 5
for i in range(total):
shape = D.SHAPES[(i // D._QUERIES_PER_SHAPE) % len(D.SHAPES)]
counts[shape] += 1
rng.random()
return counts, total
def g3_check_report_refuses_thin_slice():
"""A report whose shape counts are thin must produce a failure string.
Also covers the mutant-queries floor: `check_mutant_queries()` was reachable only
through main(), so a thin mutant slice read as PASS. Both directions are asserted --
below the floor is refused, at or above the floor it is not.
"""
report = {
"queries": 28, "seed": 0, "shape_counts": dict.fromkeys(D.SHAPES, 4),
"rows_compared": 30, "nonempty": 20, "ordered": 20, "mismatches": [], "errors": [],
}
bad = D.check_report(report)
thin = [b for b in bad if "thin slice" in b]
floor = D.mutant_floor()
below = D.check_mutant_queries(floor - 1)
at_floor = D.check_mutant_queries(floor)
if not any("below the measured floor" in b for b in below):
bad.append("check_mutant_queries(%d) accepted a slice below the floor %d"
% (floor - 1, floor))
if at_floor:
bad.append("check_mutant_queries(%d) refused the floor width itself: %r"
% (floor, at_floor))
# and a full report that declares it runs mutants must be enforced, not ignored
carried = dict(report)
carried["shape_counts"] = dict.fromkeys(D.SHAPES, D.MIN_PER_SHAPE)
carried["mutant_queries"] = floor - 1
carried["skip_mutant_floor"] = False
if not any("below the measured floor" in b for b in D.check_report(carried)):
bad.append("check_report ignored the mutant_queries width it was handed")
# ...and a report that declares it runs mutants but names no width is refused, not waved through
nowidth = dict(carried)
del nowidth["mutant_queries"]
if not any("carries no mutant-queries width" in b for b in D.check_report(nowidth)):
bad.append("check_report waved through a mutant run that names no slice width")
return bad, thin
def main():
failures = []
checks, n = g1_generator_emits_the_families()
print("G1 generator emits the NULL-tri-value families (%d draws)" % n)
for name, count in checks.items():
state = "ok" if count else "MISSING"
print(" %-40s %6d %s" % (name, count, state))
if not count:
failures.append("G1: %s never generated" % name)
counts, total = g2_shape_schedule_is_even()
print("\nG2 shape schedule over %d queries: %s" % (total, counts))
if len(set(counts.values())) != 1:
failures.append("G2: shape slices are not even: %r" % counts)
bad, thin = g3_check_report_refuses_thin_slice()
print("\nG3 check_report() on a thin slice: %d failure(s), %d mention a thin slice"
% (len(bad), len(thin)))
for b in bad:
print(" " + b)
if not thin:
failures.append("G3: check_report accepted a thin slice")
print()
if failures:
for f in failures:
print("FAIL: " + f)
print("RESULT: FAIL")
return 1
print("RESULT: PASS")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,11 @@
check_schedkit -- /root/troupe-standup/wizard-work/null/schedkit
1 spec on committed module rc=0 21.5s 124840 checks, 0 failed, 21.27s
2 mutant 0 CAUGHT 13.4s plan: waves not sorted (the pre-fix behaviour)
2 mutant 1 CAUGHT 0.2s plan: wave depth uses max of deps, not 1+max
2 mutant 7 CAUGHT 13.9s topological_order: heap replaced by list pop
2 mutant 9 CAUGHT 0.4s iter_plan: drops the last task of every wave
3 fixed chain n=2000 find_cycles 0.0116s, 0 cycles
3 pre-fix chain n=2000 find_cycles 9.3300s, 0 cycles
ratio: 804x slower before the fix
CHECK OK: spec green on the committed module, 4/4 sampled mutants killed, cost differential reproduced
@@ -0,0 +1,26 @@
spec : /root/troupe-standup/wizard-work/null/schedkit/spec_schedkit.py
module : /root/troupe-standup/wizard-work/null/schedkit/schedkit.py
budget : 300s per run
CONTROL unmutated module rc=0 22.9s 124840 checks, 0 failed, 22.40s
BASELINE pre-fix module (v1) CAUGHT 120.2s did not finish in 120s
M1 CAUGHT plan: waves not sorted (the pre-fix behaviour) 26.0s D3 trial 1 perm 0
M2 CAUGHT plan: wave depth uses max of deps, not 1+max 0.5s (no failure line)
M3 SURVIVED find_cycles: cycles not canonically rotated
M4 CAUGHT find_cycles: self-loops dropped 25.9s A10 self loop
M5 SURVIVED find_cycles: singleton SCCs never checked for self-loops
M6 CAUGHT CIRCUIT: nodes never unblocked 15.8s C oracle chorded
M7 SURVIVED CIRCUIT: floor ignored (removed vertices stay visible)
M8 CAUGHT topological_order: heap replaced by list pop 15.5s I1 diamond topo
M9 SURVIVED plan_json: n_waves off by one
M10 CAUGHT iter_plan: drops the last task of every wave 0.2s (no failure line)
M11 CAUGHT relabel: dependency ids not remapped 11.4s D3 trial 0 perm 0
M12 CAUGHT PERF: Johnson's source-vertex skip removed (ring becomes O(k^2)) 300s wall-clock gate (no exit)
8/12 mutants caught, baseline caught, control passes
TEETH FAILED (4):
- mutant 3 SURVIVED: find_cycles: cycles not canonically rotated
- mutant 5 SURVIVED: find_cycles: singleton SCCs never checked for self-loops
- mutant 7 SURVIVED: CIRCUIT: floor ignored (removed vertices stay visible)
- mutant 9 SURVIVED: plan_json: n_waves off by one
@@ -0,0 +1 @@
spec rc=0
@@ -0,0 +1,32 @@
schedkit spec -- /root/troupe-standup/wizard-work/null/schedkit/schedkit.py
python 3.14.7
== A. fixtures
== B. invariants
== C. oracle: find_cycles vs brute-force DFS
== D. randomized differential (1000 DAGs x 40 permutations)
1000 DAGs, 875 oracle comparisons, 5.933s
== E. determinism
deep chain: 50000 tasks, 50000 waves, 1.640s
wide graph: 20001 tasks, 2 waves, 0.267s
== F. cycle handling
== G. scale
50000 tasks, 90000 edges: build 0.588s, plan 1.458s
relabel+plan 50k: 2.343s
== H. cost shape: acyclic graphs linear, big SCCs iterative
chain 2000: 0.0202s (quadratic build measured 5.6651s)
ring 50000: 1.427s, recursionlimit 1000
2000 disjoint 5-rings: 2000 cycles, 0.193s
== I. topological_order is the SMALLEST linear extension
300 random DAGs compared against all-permutations brute force
124840 checks, 0 failed, 18.09s
SPEC OK: all 124840 checks held