feat: validator, bundle builder and release checks for 0.1.0

- Add tools/validate.py: validates packages, and with --correction runs
  proposed corrections through the gate against a package without
  writing anything (spec/11 §11.4). The gate now refuses a proposal that
  fails the correction schema, rule correction.schema.
- tools/load.py validates every package before loading, and with
  --bundle --built-from builds a scienting-bundle/1 with the FTS5 search
  index and bundle_meta (spec/13 §13.5). Search rows are defined per
  unit (sentence, cell, equation outside a sentence, figure-text) with
  the parent kind as context.
- Roster keys carry a role, and a key with the role owner may revoke any
  receipt (spec/08 §8.7, spec/20); roster schema, store table and the
  fixture roster updated. The checker signs a trial revocation to prove
  the rule both ways.
- A qa: receipt raises the nodes it sampled except those in its errors
  (spec/08 §8.2); the example paper's fidelity counts become 27 F3,
  13 F2, 4 F1 and 1 F0.
- The destination chooses the keyed scheme, not the text policy:
  hmac-team between the team's machines, hkdf-pages outside the team
  (spec/05 §5.8, spec/12 §12.5).
- A TextQuoteSelector must repeat the anchor's plain quote and is absent
  for hashed or keyed anchors (spec/18 §18.3).
- Add fixtures/records/suspect.json, a scienting-suspect/1 record.
- Oracle: hashes are kept incrementally and checked after every
  correction against a full recomputation; load and write refuse unsafe
  manifest paths; Ed25519, HKDF and schema validation come from the
  cryptography and jsonschema packages; canonical JSON uses the built-in
  encoder with an exact RFC 8785 fallback.
- The tools need Python 3.14 with cryptography and jsonschema; the py310
  CI environment is dropped and the workflow runs the default only.
- Resolve every [CHECK] note in the docs, READMEs and context; add
  CONTRIBUTING.md; every tool answers --help and refuses unknown options.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
alexmandClaude committed 2026-10-14 18:58:00 -04:00
1 parent b357af84d8
commit 8c8b51cc3f
89 files changed
+1141 -513

No files matched your search

+237 -49
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Check the whole repository. Exit status 0 means every check passed.
Non-normative tooling; standard library only (it uses `jsonschema` as a second validator when installed).
Non-normative tooling; it needs Python 3.14 and the packages pixi installs (`jsonschema`, `cryptography`).
Usage: python3 tools/check.py [--quick] (--quick skips the determinism rebuild)
"""
@@ -13,6 +13,7 @@ import re
import sqlite3
import subprocess
import sys
import shutil
import tempfile
from fractions import Fraction
@@ -27,6 +28,8 @@ from tools.oracle import package as PK # noqa: E402
from tools.oracle.model import Doc # noqa: E402
from tools.oracle.notation import fold_math # noqa: E402
from tools.oracle.schema import Store, validate as sv # noqa: E402
from jsonschema import Draft202012Validator # noqa: E402
from jsonschema.exceptions import SchemaError # noqa: E402
from tools.oracle.text import cp_to_utf16, letter_first, mint_node_id, norm, record_id, sha256_hex # noqa: E402
from tools.oracle.tree import Refusal # noqa: E402
@@ -69,35 +72,20 @@ def check_schemas():
for f in SCHEMA_FILES:
s = STORE.load(f)
ok("schemas", s.get("$id", "").endswith("/" + f), "%s: $id must end with its file name" % f)
try:
Draft202012Validator.check_schema(s)
meta = True
except SchemaError:
meta = False
ok("schemas", meta, "%s is not a valid draft 2020-12 schema" % f)
for f in js(p("registry", "formats.json"))["formats"]:
ok("schemas", os.path.exists(p("schemas", f["schema"])) or os.path.exists(p(f["schema"])),
"%s names a missing schema %s" % (f["format"], f["schema"]))
try:
import jsonschema # noqa: F401
from referencing import Registry, Resource
reg = Registry().with_resources(
[(STORE.load(f)["$id"], Resource.from_contents(STORE.load(f))) for f in SCHEMA_FILES])
COUNTS["second-validator"] = 0
globals()["SECOND"] = (jsonschema, reg)
except ImportError:
globals()["SECOND"] = None
def second(name, rec):
if not SECOND:
return
jsonschema, reg = SECOND
v = jsonschema.Draft202012Validator(STORE.load(name), registry=reg)
errs = list(v.iter_errors(rec))
COUNTS["second-validator"] += 1
if errs:
FAILS.append("second-validator: %s: %s" % (name, errs[0].message))
def valid(section, name, rec, where):
e = schema_errors(name, rec)
ok(section, not e, "%s: %s" % (where, e[:3]))
second(name, rec)
def check_registry():
@@ -106,6 +94,13 @@ def check_registry():
for rule in js(p("registry", "stamps", "scienting-stamps-1.json"))["rules"]:
re.compile(rule["pattern"])
rules = {r["id"] for r in js(p("registry", "rules.json"))["rules"]}
for f in sorted(os.listdir(p("tools", "oracle"))):
if f.endswith(".py"):
for rule in re.findall(r'(?:Refusal|rep\.add)\(\s*"([a-z][a-z0-9.-]+)"', open(p("tools", "oracle", f)).read()):
if not rule.endswith("."):
ok("registry", rule in rules, "tools/oracle/%s raises %s, which is not in rules.json" % (f, rule))
for v in ("text-hash", "match-hash", "align-key", "own-hash", "node-hash"):
ok("registry", "doc.hash." + v in rules, "doc.hash.%s is not in rules.json" % v)
for d in sorted(os.listdir(p("fixtures", "invalid"))):
ok("registry", js(p("fixtures", "invalid", d, "expect.json"))["rule"] in rules, "%s: rule is not in rules.json" % d)
@@ -133,13 +128,28 @@ def check_records():
else:
sch = FORMAT_SCHEMA[rec["format"]]
valid("records", sch, rec, rel)
if rec.get("format") == "scienting-suspect/1":
ok("records", rec["id"] == record_id(sha256_hex(jcs.dumpb({k: v for k, v in rec.items() if k != "id"}))),
"%s: id does not derive" % rel)
if rec.get("signature"):
ok("records", RC.verify_signature(rec, roster), "%s: signature does not verify" % rel)
ok("records", rec["id"] == record_id(RC.record_hash(rec)), "%s: id does not derive" % rel)
# A project's settings.stages holds the id of a stage file.
stage_ids = {js(p("registry", "stages", f))["id"] for f in os.listdir(p("registry", "stages")) if f.endswith(".json")}
stages = js(os.path.join(R, "project.json")).get("settings", {}).get("stages")
ok("records", stages is None or stages in stage_ids, "project settings.stages names no stage file: %s" % stages)
# Confirmed annotations are covered by the layer receipt at their content hash.
lr = js(os.path.join(R, "layer", "receipt.json"))
items = {(k, i): h for k, i, h in lr["records"]}
for a in jl(os.path.join(R, "layer", "annotations.jsonl")):
sel = a["target"].get("selector", [])
anchors = [x["anchor"] for x in sel if x.get("type") == "AnchorSelector"]
for x in sel:
if x.get("type") == "TextQuoteSelector":
q = anchors[0].get("quote", {}) if anchors else {}
same = all(x.get(k, "") == q.get(k, "") for k in ("exact", "prefix", "suffix"))
ok("records", "exact" in q and same and set(x) <= {"type", "exact", "prefix", "suffix"},
"a TextQuoteSelector must repeat the anchor's plain quote: " + a["id"])
body = {k: v for k, v in a.items() if k not in ("status", "receipt", "modified")}
ok("records", a["id"].rsplit("/", 1)[1] == record_id(sha256_hex(jcs.dumpb(
{k: v for k, v in body.items() if k != "id"}))), "annotation id does not derive: " + a["id"])
@@ -150,12 +160,7 @@ def check_records():
def state_of(pkg):
nodes = jl(os.path.join(pkg, "nodes.jsonl"))
key = js(os.path.join(pkg, "document.json"))["key"]
ids = jl(os.path.join(pkg, "ids.jsonl")) if os.path.exists(os.path.join(pkg, "ids.jsonl")) else []
cors = jl(os.path.join(pkg, "corrections.jsonl")) if os.path.exists(os.path.join(pkg, "corrections.jsonl")) else []
strip = ("text_hash", "match_hash", "align_key", "own_hash", "node_hash")
return CO.State(key, [{k: v for k, v in n.items() if k not in strip} for n in nodes], ids, cors)
return PK.read_state(pkg)
def check_packages():
@@ -192,6 +197,142 @@ def check_packages():
valid("packages", "mathml.schema.json", m, "derived/mathml.jsonl")
def check_owner_revocation():
"""A key the roster makes an owner may revoke anyone's receipt (spec/08 §8.7); any other key may not.
No fixture has such a revocation, so this signs one: Ben revokes a receipt of Ana's, once with the
roster role owner and once as the editor he is.
"""
hp = p("fixtures", "packages", "hale2026harmonic")
revoker = "ben"
secret = bytes.fromhex(open(p("fixtures", "keys", revoker + ".ed25519.hex")).read().strip())
roster_rec = js(os.path.join(hp, "roster.json"))
as_owner = dict(roster_rec, keys=[dict(k, role="owner") if k["id"] == revoker else k for k in roster_rec["keys"]])
owner_roster = {k["id"]: k for k in as_owner["keys"]}
nodes = jl(os.path.join(hp, "nodes.jsonl"))
vals = {n["id"]: n for n in nodes}
document = js(os.path.join(hp, "document.json"))
recs, fidelity = jl(os.path.join(hp, "receipts.jsonl")), None
for target in recs:
if target["format"] != "scienting-receipt/1" or target["by"] == revoker:
continue
rev = RC.seal({"format": "scienting-revocation/1", "receipt": target["id"], "reason": "Owner check.",
"by": revoker, "at": "2026-10-13T00:00:00Z"}, secret, revoker)
trial = sorted(recs + [rev], key=lambda r: (r["at"], r["id"]))
fidelity = RC.distribution(RC.effective_levels(nodes, vals, trial, owner_roster))
if fidelity != document["fidelity"]:
recs = trial
break
ok("packages", fidelity != document["fidelity"], "no receipt of the hale package matters to its levels")
document["fidelity"] = fidelity
manifest = js(os.path.join(hp, "MANIFEST.json"))
files, absent = {}, [f for f in manifest["files"] if f.get("present", True) is False]
for f in manifest["files"]:
if f.get("present", True):
with open(os.path.join(hp, f["path"]), "rb") as fh:
files[f["path"]] = fh.read()
files["receipts.jsonl"] = PK.jsonl(recs)
files["document.json"] = PK.line(document).encode("utf-8")
for roster, passes in ((as_owner, True), (roster_rec, False)):
files["roster.json"] = PK.line(roster).encode("utf-8")
with tempfile.TemporaryDirectory() as tmp:
PK.write(tmp, files, absent)
rules = {e["rule"] for e in PK.validate(tmp, STORE).errors}
role = next(k["role"] for k in roster["keys"] if k["id"] == revoker)
ok("packages", ("doc.fidelity.report" not in rules) == passes and rules <= {"doc.fidelity.report"},
"a revocation by a key with the role %s: %s" % (role, sorted(rules)))
def check_unsafe_paths():
"""Loading and writing refuse manifest paths that leave the package, even without a prior validation."""
with tempfile.TemporaryDirectory() as tmp:
pkg = os.path.join(tmp, "pkg")
shutil.copytree(p("fixtures", "packages", "tiny2026example"), pkg)
manifest = js(os.path.join(pkg, "MANIFEST.json"))
for bad in ("../outside", "/etc/outside", "assets/../../outside"):
m = dict(manifest, files=[{"path": bad, "sha256": "0" * 64, "size": 1}] + manifest["files"])
with open(os.path.join(pkg, "MANIFEST.json"), "w", encoding="utf-8") as f:
f.write(PK.line(m))
conn = sqlite3.connect(":memory:")
db.create(conn, p("sql"))
try:
db.load(conn, pkg, os.path.join(tmp, "assets"))
rule = None
except Refusal as e:
rule = e.rule
ok("packages", rule == "package.path.unsafe", "load accepted the manifest path %r" % bad)
try:
PK.write(os.path.join(tmp, "out"), {bad: b"x", "document.json": b"{}"})
rule = None
except Refusal as e:
rule = e.rule
ok("packages", rule == "package.path.unsafe", "write accepted the path %r" % bad)
ok("packages", not os.path.exists(os.path.join(tmp, "outside")), "a file was written outside the package")
def reference_values(doc, hasher=hashing.PLAIN):
"""The five values the slow way: every node projected from its whole subtree, as spec/05 §5.1 reads.
The oracle's hashing.Index joins its children's cached projections and rehashes only what a correction
changed; this independent computation must always agree with it.
"""
values = {}
for nid in reversed(doc.preorder()):
st, _ = hashing.strict(doc, nid)
own = hasher(jcs.dumpb(hashing.own_content(doc, nid, lambda x: values[x]["text_hash"])))
values[nid] = {"text_hash": hasher(norm(hashing.plain(doc, nid))), "match_hash": hasher(st),
"align_key": hasher(hashing.lossy_from_strict(st)), "own_hash": own,
"node_hash": hasher(jcs.dumpb({"own": own, "children": [
values[c]["node_hash"] for c in doc.kids(nid)]}))}
return values
def check_incremental():
"""After every correction of every fixture source, the incrementally kept values equal the slow ones."""
from tools.build import Names, build_document
team = hashing.Hasher(crypto.team_key(bytes.fromhex(
open(p("fixtures", "keys", "smithlab-team-secret.hex")).read().strip())))
Names.registry.clear()
# Sorted, the main example comes first, so the draft's references to its sentences resolve.
for f in sorted(os.listdir(p("fixtures", "src"))):
if not f.endswith(".json"):
continue
_, _, _, _, states, _ = build_document(js(p("fixtures", "src", f)))
keyed = None
for rev in sorted(states):
st = states[rev]
ok("packages", st.index().values == reference_values(st.doc()),
"%s rev %d: incremental values differ from the full computation" % (f, rev))
# The keyed values, carried forward from revision 0 by updates alone.
keyed = hashing.Index.build(st.doc(), team) if keyed is None else keyed.update(st.doc())
ok("packages", keyed.values == reference_values(st.doc(), team),
"%s rev %d: incremental keyed values differ from the full computation" % (f, rev))
Names.registry.clear()
def check_gate():
"""The gate runs on a proposal against a package's current state and leaves that state unchanged."""
st = state_of(p("fixtures", "packages", "hale2026harmonic"))
before = json.dumps(st.nodes, sort_keys=True)
target = next(n for n in st.nodes if n["kind"] == "sentence" and "Each bond" in hashing.plain(st.doc(), n["id"]))
base = {"format": "scienting-correction/1", "evidence": [{"kind": "person", "by": "ana"}], "author": {"key": "ana"},
"reason": "Gate check.", "at": "2026-10-14T09:00:00Z", "status": "proposed"}
st1, rec = CO.gate(st, dict(base, op="replace", target=target["id"], find="Each bond", replace="Every bond"), STORE)
ok("packages", rec["index"] == st.rev + 1 and st1.rev == st.rev + 1, "the gate's record has the next index")
ok("packages", json.dumps(st.nodes, sort_keys=True) == before, "the gate changed the state it was given")
valid("packages", "correction.schema.json", rec, "gate record")
for bad, rule in (({"op": "replace", "target": target["id"]}, "correction.schema"),
(dict(base, op="replace", target=target["id"], find="0.01", replace="0.02"), "correction.digits"),
(dict(base, op="insert", target=target["id"], run=9, offset=0, text="x",
evidence=[{"kind": "crop", "sha256": "0" * 64}]), "correction.insert")):
try:
CO.gate(st, bad, STORE)
got = None
except Refusal as e:
got = e.rule
ok("packages", got == rule, "the gate gave %s for a proposal that breaks %s" % (got, rule))
def check_invalid():
base = p("fixtures", "invalid")
for d in sorted(os.listdir(base)):
@@ -299,6 +440,8 @@ def check_vectors():
try:
st1, rec = CO.apply(st, c["correction"])
v1 = hashing.compute(st1.doc())
ok("vectors", st1.index().values == reference_values(st1.doc()),
"correction %s: incremental values differ from the full computation" % c["id"])
got = {"record": rec, "nodes": [dict(n, **v1[n["id"]]) for n in st1.nodes], "ids": st1.ids}
except Refusal as e:
got = {"refusal": e.rule}
@@ -330,7 +473,8 @@ def check_determinism():
with tempfile.TemporaryDirectory() as tmp:
subprocess.run([sys.executable, p("tools", "build.py"), "--out", tmp], check=True)
for sub in ("fixtures", "vectors"):
cmp = filecmp.dircmp(p(sub), os.path.join(tmp, sub))
# Files the repository ignores, such as macOS's .DS_Store, are not part of the build.
cmp = filecmp.dircmp(p(sub), os.path.join(tmp, sub), ignore=filecmp.DEFAULT_IGNORES + [".DS_Store"])
diffs = []
def walk(c, prefix):
@@ -384,8 +528,7 @@ def check_sql():
ok("sql", "FOREIGN KEY" not in ddl and "REFERENCES" not in ddl, "no foreign keys")
with tempfile.TemporaryDirectory() as tmp:
conn = sqlite3.connect(os.path.join(tmp, "bundle.db"))
db.create(conn, p("sql"))
conn.executescript(open(p("sql", "bundle-sqlite.sql"), encoding="utf-8").read())
db.create_bundle(conn, p("sql"))
assets = os.path.join(tmp, "assets")
pk = p("fixtures", "packages")
for d in sorted(os.listdir(pk)):
@@ -393,29 +536,18 @@ def check_sql():
if key is None:
# A second load of the same key goes to its own database.
c2 = sqlite3.connect(":memory:")
db.create(c2, p("sql"))
db.create_bundle(c2, p("sql"))
key = db.load(c2, os.path.join(pk, d), assets)
ok("sql", db.index(c2, key) == 0, "%s: withheld text was indexed" % d)
h = db.unload(c2, key, os.path.join(tmp, "out", d), assets)
else:
db.index(conn, key)
h = db.unload(conn, key, os.path.join(tmp, "out", d), assets)
cmp = filecmp.dircmp(os.path.join(pk, d), os.path.join(tmp, "out", d))
same = _same_tree(os.path.join(pk, d), os.path.join(tmp, "out", d))
ok("sql", same, "%s: unload is not byte-identical" % d)
with open(os.path.join(pk, d, "MANIFEST.json"), "rb") as f:
ok("sql", sha256_hex(f.read()) == h, "%s: package hash differs after unload" % d)
# The search index over sentences, captions, headings and cells.
for doc_key, nid, kind, parent in conn.execute(
"SELECT n.doc_key, n.id, n.kind, p.kind FROM nodes n JOIN nodes p ON p.doc_key = n.doc_key AND p.id = n.parent "
"WHERE n.kind IN ('sentence', 'cell', 'equation') AND n.withheld IS NULL").fetchall():
st = None
rows = conn.execute("SELECT doc_key, id, kind, content FROM nodes WHERE kind IN ('sentence','cell')").fetchall()
for doc_key, nid, kind, content in rows:
runs = json.loads(content)
text = "".join(r.get("text", "") if r["kind"] != "math" else "$" + r["source"] + "$" for r in runs
if r["kind"] != "equation")
conn.execute("INSERT INTO search VALUES (?,?,?,?,?)", (doc_key, nid, kind, "", norm(text)))
hits = conn.execute("SELECT node FROM search WHERE search MATCH 'scanned'").fetchall()
ok("sql", len(hits) == 1, "full-text search finds the merged sentence")
check_bundle(conn)
# Store records satisfy the store tables' CHECK constraints.
R = p("fixtures", "records")
lay = js(os.path.join(R, "layer", "layer.json"))
@@ -433,8 +565,8 @@ def check_sql():
for m in prj["members"]:
conn.execute("INSERT INTO members VALUES (?,?,?,?)", (prj["key"], m["person"], m["role"], jcs.dumps(m["keys"])))
for k in js(os.path.join(R, "roster.json"))["keys"]:
conn.execute("INSERT INTO roster_keys VALUES (?,?,?,?,?,?,?,?)",
("smithlab", k["id"], jcs.dumps(k["person"]), k["scheme"], k["public"],
conn.execute("INSERT INTO roster_keys VALUES (?,?,?,?,?,?,?,?,?)",
("smithlab", k["id"], jcs.dumps(k["person"]), k["role"], k["scheme"], k["public"],
jcs.dumps(k["purposes"]), k["registered_at"], k["revoked_at"]))
dg = js(os.path.join(R, "digitization.json"))
conn.execute("INSERT INTO digitizations VALUES (?,?,?,?,?,?,?,?)",
@@ -444,6 +576,51 @@ def check_sql():
ok("sql", True, "store records loaded")
def check_bundle(conn):
"""The bundle's search index and metadata (spec/13 §13.5), built by the oracle's db.index and db.write_meta."""
units = dict(conn.execute("SELECT unit, COUNT(*) FROM search GROUP BY unit").fetchall())
for u in ("sentence", "cell", "figure-text"):
ok("sql", units.get(u, 0) > 0, "the search index has no %s rows" % u)
# No fixture has an equation outside a sentence, so a small document shows both kinds of equation.
nodes = [{"id": "d", "kind": "document", "parent": None, "ord": 0},
{"id": "p", "kind": "paragraph", "parent": "d", "ord": 0},
{"id": "s", "kind": "sentence", "parent": "p", "ord": 0,
"content": [{"kind": "text", "text": "Then "}, {"kind": "equation", "node": "e1"}]},
{"id": "e1", "kind": "equation", "parent": "s", "ord": 0, "content": {"notation": "tex", "source": "a=b"}},
{"id": "e2", "kind": "equation", "parent": "p", "ord": 1, "content": {"notation": "tex", "source": "c=d"}}]
got = [(n, u, c) for _, n, u, c, _ in db.search_rows("k", nodes)]
ok("sql", got == [("s", "sentence", "paragraph"), ("e2", "equation", "paragraph")],
"search rows for a sentence with an inline equation and a display equation: %s" % got)
ok("sql", set(units) <= {"sentence", "cell", "equation", "figure-text"}, "unknown search units %s" % sorted(units))
expect = conn.execute(
"SELECT COUNT(*) FROM nodes n LEFT JOIN nodes q ON q.doc_key = n.doc_key AND q.id = n.parent "
"WHERE n.withheld IS NULL AND (n.kind IN ('sentence', 'cell') OR (n.kind = 'equation' AND q.kind <> 'sentence'))"
).fetchone()[0]
ok("sql", sum(v for u, v in units.items() if u != "figure-text") == expect,
"the search index does not have one row per sentence, cell and equation outside a sentence")
inner = conn.execute(
"SELECT COUNT(*) FROM search s JOIN nodes n ON n.doc_key = s.doc_key AND n.id = s.node "
"JOIN nodes q ON q.doc_key = n.doc_key AND q.id = n.parent WHERE n.kind = 'equation' AND q.kind = 'sentence'"
).fetchone()[0]
ok("sql", inner == 0, "an equation inside a sentence is indexed apart from its sentence")
bad = conn.execute(
"SELECT COUNT(*) FROM search s JOIN nodes n ON n.doc_key = s.doc_key AND n.id = s.node "
"LEFT JOIN nodes q ON q.doc_key = n.doc_key AND q.id = n.parent "
"WHERE s.unit <> 'figure-text' AND s.context IS NOT q.kind").fetchone()[0]
ok("sql", bad == 0, "a search row's context is not the kind of its node's parent")
hits = conn.execute("SELECT node FROM search WHERE search MATCH 'scanned'").fetchall()
ok("sql", len(hits) == 1, "full-text search finds the merged sentence")
hits = conn.execute("SELECT context FROM search WHERE unit = 'sentence' AND search MATCH 'Methods'").fetchall()
ok("sql", ("heading",) in hits, "a heading sentence is indexed with the context heading")
stores = [r[0] for r in conn.execute("SELECT DISTINCT store FROM documents WHERE store IS NOT NULL")]
db.write_meta(conn, stores[0] if len(stores) == 1 else "", "fixtures/packages")
meta = dict(conn.execute("SELECT name, value FROM bundle_meta").fetchall())
ok("sql", {"format", "store", "built_from", "spec"} <= set(meta), "bundle_meta lacks a required name")
ok("sql", meta["format"] in FORMAT_SCHEMA, "bundle_meta names an unregistered format")
top = re.search(r"^## (\S+)", open(p("CHANGELOG.md"), encoding="utf-8").read(), re.M).group(1)
ok("sql", meta["spec"] == top, "bundle_meta spec %s is not the changelog's latest version %s" % (meta["spec"], top))
def _same_tree(a, b):
fa = sorted(os.path.relpath(os.path.join(d, f), a) for d, _, fs in os.walk(a) for f in fs)
fb = sorted(os.path.relpath(os.path.join(d, f), b) for d, _, fs in os.walk(b) for f in fs)
@@ -525,13 +702,24 @@ def check_links():
def main():
import argparse
ap = argparse.ArgumentParser(
description="Check the whole repository: schemas, records, packages, broken packages, vectors, a "
"byte-for-byte rebuild, the SQL round trip and the bundle, prose and links. "
"Exit status 0 means every check passed.")
ap.add_argument("--quick", action="store_true", help="skip the byte-for-byte rebuild, the slowest check")
args = ap.parse_args()
check_schemas()
check_registry()
check_records()
check_packages()
check_owner_revocation()
check_unsafe_paths()
check_incremental()
check_gate()
check_invalid()
check_vectors()
if "--quick" not in sys.argv:
if not args.quick:
check_determinism()
check_sql()
check_prose()