feat: minted keys, VRF seeds, RFC 9162 proofs, history roots, tag URIs and a transparency log
Keys (spec/02 §2.4), the rule paperstack settled on:
- A work key is a Scholar-style citation key, a hyphen and the first four
Crockford base32 characters of SHA-256("scienting-key/1:" + identity),
where the identity is the first of doi, arxiv, isbn, pmid, pmcid and
openalex in same_as, else the folded title, year and family name. A
collision lengthens the later key by two characters; a key is minted
once and never recomputed. A document key adds .si, .v<seq> or .<type>
for versions other than the version of record.
- Every fixture key is now minted (hale2026harmonic-damb,
okafor2025solvent-acth, writer2027fixed-ynr7.v3, tiny2026tiny-j4qm), so
every node and record ID was re-minted and the docs rewritten to match.
The validator checks the document key against the work key and version
(doc.key.work) and the hash part against the identifier (doc.key.suffix).
vectors/keys.jsonl pins the cases paperstack's tests pin.
Review:
- Spot-check seeds are ECVRF-EDWARDS25519-SHA512-TAI proofs (RFC 9381),
implemented in tools/oracle/vrf.py in plain Python and reproduced
against the RFC's Example 16; the sample ranks by the VRF output. The
earlier Ed25519 seed could be ground by a dishonest checker.
- RFC 9162 inclusion and consistency proofs (crypto.inclusion_path,
verify_inclusion, consistency_proof, verify_consistency) with
scienting-proof/1, vectors/proofs.jsonl and two fixture records.
- document.json carries `history`, the root over the corrections, with
the empty-tree hash for none (spec/11 §11.5, rule doc.history).
- A provisional transparency log (spec/23): log entries, signed-note
checkpoints with key hashes as transparency logs share them, the rule
that a logged revocation beats a self-asserted time, and vectors.
- Reserved signature schemes cose and ml-dsa-65 (spec/08 §8.4).
Identifiers and text:
- urn:scienting: becomes tag:scient.ing,2026: (RFC 4151), since the URN
namespace was never registered.
- Unicode 16.0 is pinned for normalization and categories (spec/01 §1.4);
the checker refuses another version.
Checking:
- check.py: keys, proofs, VRF and checkpoint vectors; the proof and
checkpoint records; a check that every ID and hash the prose quotes
exists in the fixtures or vectors; three more broken packages
(history-wrong, key-suffix, key-work).
- stress.py: key, VRF, proof and checkpoint properties.
- Docs: a keys section in the concepts and FAQ, rationale entries for
keys, VRF, proofs, tag URIs and the Unicode pin, a "Looking ahead"
section, media-type registration and OCI distribution notes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
307 files changed
+2698
-1497
No files matched your search
+201
-62
@@ -25,10 +25,10 @@ from tools.oracle import align as AL # noqa: E402
|
||||
from tools.oracle import anchors as AN # noqa: E402
|
||||
from tools.oracle import corrections as CO # noqa: E402
|
||||
from tools.oracle import crypto, fingerprint as FP, grade as GR, hashing, jcs, receipts as RC, rights as RI # noqa: E402
|
||||
from tools.oracle import targets as TG # noqa: E402
|
||||
from tools.oracle import keys as K, log as LG, targets as TG, vrf # noqa: E402
|
||||
from tools.oracle.model import LEAVES, MATH_KINDS, RUN_KINDS, VERBATIM_KINDS, Doc # noqa: E402
|
||||
from tools.oracle.notation import fold_math # noqa: E402
|
||||
from tools.oracle.package import ZIP_EXTENSION, jsonl, line, witness_id, write, write_zip # noqa: E402
|
||||
from tools.oracle.package import ZIP_EXTENSION, history_items, jsonl, line, witness_id, write, write_zip # noqa: E402
|
||||
from tools.oracle.schema import Store # noqa: E402
|
||||
from tools.oracle.text import letter_first, mint_node_id, norm, record_id, sha256_hex # noqa: E402
|
||||
from tools.oracle.tree import Refusal, _flatten, mint_ids, validate_structure # noqa: E402
|
||||
@@ -175,19 +175,23 @@ def expand(s, page=None):
|
||||
|
||||
|
||||
class Names:
|
||||
"""Local names (`@x`) and the IDs minted for them, per document."""
|
||||
"""Local names (`@x`) and the IDs minted for them, per document, registered under the source's name."""
|
||||
|
||||
registry = {}
|
||||
|
||||
def __init__(self, key):
|
||||
def __init__(self, name, key):
|
||||
self.name = name
|
||||
self.key = key
|
||||
self.ids = {}
|
||||
Names.registry[key] = self
|
||||
Names.registry[name] = self
|
||||
|
||||
def resolve(self, v):
|
||||
if isinstance(v, str) and v.startswith("ext:"):
|
||||
_, doc, name = v.split(":", 2)
|
||||
return doc + "#" + Names.registry[doc].ids[name]
|
||||
parts = v.split(":", 2)
|
||||
other = Names.registry[parts[1]]
|
||||
if len(parts) == 2: # `ext:<source>`: the document, qualified by the store
|
||||
return STORE + "/" + other.key
|
||||
return other.key + "#" + other.ids[parts[2]]
|
||||
if isinstance(v, str) and v.startswith("@"):
|
||||
return self.ids[v]
|
||||
if isinstance(v, list):
|
||||
@@ -412,8 +416,13 @@ def seal_receipts(spec, names, state_by_rev, final_state, key, wit_sha):
|
||||
|
||||
|
||||
def build_document(src, used=None, extra_files=None):
|
||||
key = src["document"]["key"]
|
||||
names = Names(key)
|
||||
# The keys are minted, never written by hand (spec/02 §2.4): the work key from the work's identity and its
|
||||
# metadata, the document key from the work key and the version.
|
||||
d = src["document"]
|
||||
wkey = K.work_key(d["work"].get("same_as"), d["meta"])
|
||||
key = K.document_key(wkey, d["version"])
|
||||
d["key"], d["work"]["key"] = key, wkey
|
||||
names = Names(src.get("name", key), key)
|
||||
used = used if used is not None else set()
|
||||
spec = expand(src["tree"])
|
||||
nodes, side = establish(key, spec, names, used)
|
||||
@@ -502,7 +511,7 @@ def package_files(src, key, names, side, final, records, receipts=None, roster_r
|
||||
if "_csl" in s:
|
||||
r["csl"] = s["_csl"]
|
||||
res = s.get("_resolution", {"type": "none"})
|
||||
r["resolution"] = res
|
||||
r["resolution"] = {k: names.resolve(v) for k, v in res.items()}
|
||||
refs.append({k: v for k, v in r.items() if v is not None})
|
||||
if refs:
|
||||
files["references.jsonl"] = jsonl(refs)
|
||||
@@ -524,6 +533,7 @@ def package_files(src, key, names, side, final, records, receipts=None, roster_r
|
||||
d = dict(src["document"])
|
||||
d = {"format": "scienting-doc/1", **d}
|
||||
d.update({"hashing": {"scheme": "plain"}, "root": vals[doc.root]["node_hash"], "rev": final.rev,
|
||||
"history": crypto.history_root(history_items(records or [])),
|
||||
"counts": {"nodes": len(nodes), "leaves": sum(1 for n in nodes if n["kind"] in LEAVES)},
|
||||
"fidelity": RC.distribution(levels)})
|
||||
d["meta_hash"] = hashing.meta_hash(d)
|
||||
@@ -535,6 +545,7 @@ def package_files(src, key, names, side, final, records, receipts=None, roster_r
|
||||
|
||||
def build_hale(out):
|
||||
src = json.load(open(os.path.join(ROOT, "fixtures", "src", "hale2026harmonic.json")))
|
||||
src["name"] = "hale2026harmonic"
|
||||
image = png(4, 3, (40, 90, 160))
|
||||
image_sha = sha256_hex(image)
|
||||
key, names, side, final, states, records = build_document(src)
|
||||
@@ -636,14 +647,15 @@ def build_checks_only(out, hale):
|
||||
f["references.jsonl"] = jsonl(refs)
|
||||
d = dict(hale["document"])
|
||||
d.update({"policy": "checks-only", "hashing": {"scheme": "hmac-team", "key_id": crypto.key_id(k)},
|
||||
"root": keyed[doc.root]["node_hash"]})
|
||||
"root": keyed[doc.root]["node_hash"], "history": crypto.EMPTY_ROOT})
|
||||
d["meta_hash"] = hashing.meta_hash(d, hashing.Hasher(k))
|
||||
f["document.json"] = (jcs.dumps(d) + "\n").encode()
|
||||
write(os.path.join(out, "fixtures", "packages", "hale2026harmonic.checks-only"), f, hale["absent"])
|
||||
write(os.path.join(out, "fixtures", "packages", hale["key"] + ".checks-only"), f, hale["absent"])
|
||||
|
||||
|
||||
def build_simple(out, name, used=None):
|
||||
src = json.load(open(os.path.join(ROOT, "fixtures", "src", name + ".json")))
|
||||
src["name"] = name
|
||||
key, names, side, final, states, records = build_document(src)
|
||||
witnesses = []
|
||||
pages = boxes = None
|
||||
@@ -680,8 +692,11 @@ def build_simple(out, name, used=None):
|
||||
|
||||
# --- the tiny document used for corrections vectors and invalid packages -----------------
|
||||
|
||||
TINY = {"document": {"key": "tiny2026example", "kind": "article", "work": {"key": "tiny2026example", "same_as": []},
|
||||
"version": {"type": "version-of-record"}, "relations": [], "meta": {"title": "A tiny example"},
|
||||
TINY = {"name": "tiny2026example",
|
||||
"document": {"kind": "article", "work": {"same_as": ["doi:10.5555/example.tiny.2026"]},
|
||||
"version": {"type": "version-of-record"}, "relations": [],
|
||||
"meta": {"title": "A tiny example", "author": [{"family": "Tiny", "given": "T."}],
|
||||
"issued": {"date-parts": [[2026]]}},
|
||||
"language": "en", "license": {"spdx": "CC0-1.0"}, "policy": "full"},
|
||||
"tree": {"kind": "document", "children": [
|
||||
{"kind": "section", "name": "@sec", "label": "1", "level": 1, "title": "Results", "children": [
|
||||
@@ -707,7 +722,8 @@ TINY = {"document": {"key": "tiny2026example", "kind": "article", "work": {"key"
|
||||
|
||||
|
||||
def tiny_package(out_dir, receipts=True):
|
||||
key, names, side, final, states, records = build_document(copy.deepcopy(TINY))
|
||||
src = copy.deepcopy(TINY)
|
||||
key, names, side, final, states, records = build_document(src)
|
||||
wsha = H("tiny2026example: a file (synthetic)")
|
||||
wit = [{"id": witness_id(wsha), "sha256": wsha, "size": 1000, "kind": "pdf", "media_type": "application/pdf",
|
||||
"pages": 1, "origin": {"source": "author"}, "rights": rights_record("own")}]
|
||||
@@ -716,7 +732,7 @@ def tiny_package(out_dir, receipts=True):
|
||||
recs, _ = seal_receipts([{"name": "t1", "by": "ana", "level": "F3", "scope": "all", "at": "2026-10-11T12:00:00Z",
|
||||
"nodes": ["@a", "@b", "@c", "@d", "@x1", "@y1", "@hx", "@hy", "@r"]}],
|
||||
names, states, final, key, wsha)
|
||||
files, nodes, vals, d = package_files(TINY, key, names, side, final, records, recs, ROSTER if receipts else None,
|
||||
files, nodes, vals, d = package_files(src, key, names, side, final, records, recs, ROSTER if receipts else None,
|
||||
wit)
|
||||
if out_dir:
|
||||
write(out_dir, files)
|
||||
@@ -783,8 +799,8 @@ def vectors(out, hale, tiny):
|
||||
for doc_key, kind, th, occ in [("cornell1995second", "sentence", H("A sentence."), 0),
|
||||
("cornell1995second", "sentence", H("A sentence."), 1),
|
||||
("cornell1995second.si", "sentence", H("A sentence."), 0),
|
||||
("hale2026harmonic", "cell", H("310.0"), 0),
|
||||
("hale2026harmonic", "column", H("Set A $K_r$ (kcal mol⁻¹ Å⁻²)"), 0)]:
|
||||
(hale["key"], "cell", H("310.0"), 0),
|
||||
(hale["key"], "column", H("Set A $K_r$ (kcal mol⁻¹ Å⁻²)"), 0)]:
|
||||
payload = jcs.dumps(["scienting-id/1", doc_key, kind, th, occ])
|
||||
idc.append({"id": "mint-%s-%s-%d" % (doc_key, kind, occ), "doc": doc_key, "kind": kind, "text_hash": th,
|
||||
"occurrence": occ, "payload": payload, "sha256": H(payload),
|
||||
@@ -876,19 +892,23 @@ def vectors(out, hale, tiny):
|
||||
# Five values on a small tree: the tiny document.
|
||||
tnodes = [{k: v for k, v in n.items() if k not in ("text_hash", "match_hash", "align_key", "own_hash", "node_hash")}
|
||||
for n in map(json.loads, tiny["files"]["nodes.jsonl"].decode().splitlines())]
|
||||
tdoc = Doc("tiny2026example", tnodes)
|
||||
tdoc = Doc(tiny["key"], tnodes)
|
||||
tv = hashing.compute(tdoc)
|
||||
own = {nid: hashing.own_content(tdoc, nid, lambda x: tv[x]["text_hash"]) for nid in tdoc.order}
|
||||
wv(out, "hashes.jsonl", [{"id": "tiny2026example", "doc": "tiny2026example", "nodes": tnodes,
|
||||
wv(out, "hashes.jsonl", [{"id": "tiny2026example", "doc": tiny["key"], "nodes": tnodes,
|
||||
"own_content": own, "values": tv}])
|
||||
# Addresses.
|
||||
ad = [("plain", "cornell1995second#k3m9q2a"), ("pinned", "cornell1995second#k3m9q2a@4f1a0c"),
|
||||
("span", "cornell1995second#k3m9q2a@4f1a0c[12:57]"), ("store", "smithlab/cornell1995second#k3m9q2a"),
|
||||
("span-no-pin", "dong2026machine.v2#k3m9q2a[0:0]"), ("si", "cornell1995second.si#m2x8p4d"),
|
||||
("alias", "cornell1995second#b-p03-text-004"), ("bad-reversed", "cornell1995second#k3m9q2a[57:12]"),
|
||||
("bad-pin", "cornell1995second#k3m9q2a@4F1A0C"), ("bad-leading-zero", "k#k3m9q2a[01:2]"),
|
||||
("bad-no-node", "cornell1995second#"), ("bad-upper-key", "Cornell1995#k3m9q2a"),
|
||||
("bad-two-stores", "a/b/c#k3m9q2a"), ("bad-pin-length", "k#k3m9q2a@4f1a")]
|
||||
# Addresses. The example keys are real work keys: Cornell et al. (1995) by its DOI, a fictional preprint.
|
||||
cornell = "cornell1995second-" + K.suffix("doi:10.1021/ja00124a002")
|
||||
dong = "dong2026machine-" + K.suffix("doi:10.5555/example.dong.2026")
|
||||
ad = [("plain", cornell + "#k3m9q2a"), ("pinned", cornell + "#k3m9q2a@4f1a0c"),
|
||||
("span", cornell + "#k3m9q2a@4f1a0c[12:57]"), ("store", "smithlab/" + cornell + "#k3m9q2a"),
|
||||
("span-no-pin", dong + ".v2#k3m9q2a[0:0]"), ("si", cornell + ".si#m2x8p4d"),
|
||||
("alias", cornell + "#b-p03-text-004"), ("bad-reversed", cornell + "#k3m9q2a[57:12]"),
|
||||
("bad-pin", cornell + "#k3m9q2a@4F1A0C"), ("bad-leading-zero", "k-0000#k3m9q2a[01:2]"),
|
||||
("bad-no-node", cornell + "#"), ("bad-upper-key", "Cornell1995-k3f9#k3m9q2a"),
|
||||
("bad-two-stores", "a/b/c-0000#k3m9q2a"), ("bad-pin-length", "k-0000#k3m9q2a@4f1a"),
|
||||
("bad-no-hash", "cornell1995second#k3m9q2a"), ("bad-hash-alphabet", "cornell1995second-k3fi#k3m9q2a"),
|
||||
("bad-short-hash", "cornell1995second-k3f#k3m9q2a")]
|
||||
wv(out, "addresses.jsonl", [{"id": i, "address": a, "parsed": AN.parse_address(a)} for i, a in ad])
|
||||
# Keyed hashes.
|
||||
k = crypto.team_key(TEAM_SECRET)
|
||||
@@ -960,6 +980,7 @@ def vectors(out, hale, tiny):
|
||||
q1 = hale["by_name"]["q1"]
|
||||
wv(out, "spotcheck.jsonl", [{"id": "q1", "receipt": r1, "seed_payload": RC.seed_payload(r1).decode(),
|
||||
"checker_public": ROSTER_BY_ID["ben-spot"]["public"], "seed": q1["qa"]["seed"],
|
||||
"beta": vrf.proof_to_hash(base64.b64decode(q1["qa"]["seed"])).hex(),
|
||||
"n": len(r1["nodes"]), "rate": "0.05", "minimum": 10,
|
||||
"size": RC.sample_size(len(r1["nodes"])), "sample": q1["qa"]["sample"]}]
|
||||
+ [{"id": "size-%d" % n, "n": n, "rate": "0.05", "minimum": 10, "size": RC.sample_size(n)}
|
||||
@@ -968,6 +989,89 @@ def vectors(out, hale, tiny):
|
||||
wv(out, "bounds.jsonl", [{"id": "k%d-n%d" % (kk, nn), "errors": kk, "sampled": nn, "confidence": "0.95",
|
||||
"bound": RC.bound_string(kk, nn), "approx": round(RC.upper_bound(kk, nn), 9)}
|
||||
for kk, nn in [(0, 10), (0, 300), (1, 10), (1, 300), (2, 50), (5, 100), (0, 1), (10, 10)]])
|
||||
# Keys (spec/02 §2.4): Scholar parts, identities and suffixes, including the cases paperstack pins.
|
||||
attn = {"title": "Attention is all you need", "author": [{"family": "Vaswani", "given": "Ashish"}],
|
||||
"issued": {"date-parts": [[2017]]}}
|
||||
kc = []
|
||||
for cid, same_as, meta in [
|
||||
("doi", ["doi:10.48550/arXiv.1706.03762"], attn),
|
||||
("openalex", ["openalex:W2963403868"], attn),
|
||||
("doi-before-arxiv", ["arxiv:1706.03762", "doi:10.48550/arXiv.1706.03762"], attn),
|
||||
("arxiv-only", ["arxiv:1706.03762"], attn),
|
||||
("url-is-not-an-identity", ["url:https://example.org/paper"], attn),
|
||||
("meta", [], {"title": "On the Origin of Species", "author": [{"family": "Darwin", "given": "Charles"}],
|
||||
"issued": {"date-parts": [[1859]]}}),
|
||||
("doi-case", ["doi:10.1109/CVPR.2016.90"], {"title": "Deep Residual Learning for Image Recognition",
|
||||
"author": [{"family": "He", "given": "Kaiming"}],
|
||||
"issued": {"date-parts": [[2016]]}}),
|
||||
("accents", [], {"title": "A new method", "author": [{"literal": "M\u00fcller, J\u00fcrgen"}],
|
||||
"issued": {"date-parts": [[2001]]}}),
|
||||
("apostrophe", [], {"title": "Don't stop", "author": [{"literal": "Conan O'Brien"}],
|
||||
"issued": {"date-parts": [[2020]]}}),
|
||||
("name-suffix", [], {"title": "3D printing of bone", "author": [{"literal": "A. Smith Jr."}],
|
||||
"issued": {"date-parts": [[2015]]}}),
|
||||
("only-stop-words", [], {"title": "The of a", "author": [{"family": "Lee", "given": "Jo"}],
|
||||
"issued": {"date-parts": [[2019]]}}),
|
||||
("no-year", [], {"title": "Untitled", "author": [{"family": "Lee"}]}),
|
||||
("no-author", [], {"title": "Fluids", "issued": {"date-parts": [[2020]]}}),
|
||||
("non-latin-author", [], {"title": "Fluids", "author": [{"family": "\u5f20\u4f1f"}], "issued": {"date-parts": [[2020]]}}),
|
||||
("hale", hale["src"]["document"]["work"]["same_as"], hale["src"]["document"]["meta"]),
|
||||
]:
|
||||
ident = K.identity(same_as, meta)
|
||||
kc.append({"id": cid, "same_as": same_as, "meta": meta, "scholar": K.scholar_part(meta), "identity": ident,
|
||||
"payload": K.SCHEME + ":" + ident, "sha256": H(K.SCHEME + ":" + ident), "suffix": K.suffix(ident),
|
||||
"suffix_8": K.suffix(ident, 8), "key": K.work_key(same_as, meta)})
|
||||
hk = hale["document"]["work"]["key"]
|
||||
for cid, version in [("version-of-record", {"type": "version-of-record"}), ("supplement", {"type": "supplement"}),
|
||||
("preprint-v2", {"type": "preprint", "seq": 2}), ("draft-v3", {"type": "draft", "seq": 3}),
|
||||
("erratum", {"type": "erratum"}), ("accepted-no-seq", {"type": "accepted"})]:
|
||||
kc.append({"id": "document-" + cid, "work_key": hk, "version": version, "document_key": K.document_key(hk, version)})
|
||||
kc.append({"id": "collision", "same_as": attn and ["doi:10.48550/arXiv.1706.03762"], "meta": attn,
|
||||
"taken": [K.work_key(["doi:10.48550/arXiv.1706.03762"], attn)],
|
||||
"key": K.unique_work_key(["doi:10.48550/arXiv.1706.03762"], attn, {K.work_key(["doi:10.48550/arXiv.1706.03762"], attn)})})
|
||||
wv(out, "keys.jsonl", kc)
|
||||
# Proofs (RFC 9162): inclusion for every item of the Merkle vectors, consistency for every prefix, and the
|
||||
# example paper's correction history.
|
||||
pf = [{"id": "empty-history", "kind": "root", "items": [], "root": crypto.history_root([])}]
|
||||
for n in (1, 2, 3, 4, 5, 7):
|
||||
items = [["node", mint_node_id("m", "sentence", H(str(i)), 0), H("h%d" % i)] for i in range(n)]
|
||||
root = crypto.merkle_root(items)
|
||||
for i in range(n):
|
||||
pf.append({"id": "inclusion-%d-of-%d" % (i, n), "kind": "inclusion", "items": items, "index": i, "size": n,
|
||||
"root": root, "leaf": crypto.leaf_hash(items[i]), "path": crypto.inclusion_path(items, i)})
|
||||
for m in range(n + 1):
|
||||
pf.append({"id": "consistency-%d-to-%d" % (m, n), "kind": "consistency", "items": items, "from": m, "to": n,
|
||||
"from_root": crypto.history_root(items[:m]), "to_root": root,
|
||||
"path": crypto.consistency_proof(items, m)})
|
||||
hist = history_items(hale["records"])
|
||||
pf.append({"id": "hale-history-8-to-12", "kind": "consistency", "items": hist, "from": 8, "to": len(hist),
|
||||
"from_root": crypto.history_root(hist[:8]), "to_root": crypto.history_root(hist),
|
||||
"path": crypto.consistency_proof(hist, 8)})
|
||||
pf.append({"id": "hale-history-correction-5", "kind": "inclusion", "items": hist, "index": 4, "size": len(hist),
|
||||
"root": crypto.history_root(hist), "leaf": crypto.leaf_hash(hist[4]), "path": crypto.inclusion_path(hist, 4)})
|
||||
wv(out, "proofs.jsonl", pf)
|
||||
# The VRF (RFC 9381, ECVRF-EDWARDS25519-SHA512-TAI): the RFC's Example 16, two more inputs for its key, and
|
||||
# the spot-check seed of the example paper's first receipt.
|
||||
vv = []
|
||||
for cid, alpha in (("rfc9381-example-16", b""), ("rfc9381-key-alpha-72", b"\x72"), ("rfc9381-key-alpha-af82", b"\xaf\x82")):
|
||||
pi = vrf.prove(sk, alpha)
|
||||
vv.append({"id": cid, "secret": sk.hex(), "public": vrf.public_key(sk).hex(), "alpha": alpha.hex(),
|
||||
"proof": pi.hex(), "beta": vrf.proof_to_hash(pi).hex()})
|
||||
seed = base64.b64decode(q1["qa"]["seed"])
|
||||
vv.append({"id": "ben-spot-seed-q1", "public": base64.b64decode(ROSTER_BY_ID["ben-spot"]["public"]).hex(),
|
||||
"alpha": RC.seed_payload(r1).hex(), "proof": seed.hex(), "beta": vrf.proof_to_hash(seed).hex()})
|
||||
bad = bytearray(seed)
|
||||
bad[40] ^= 1
|
||||
vv.append({"id": "tampered-challenge", "public": base64.b64decode(ROSTER_BY_ID["ben-spot"]["public"]).hex(),
|
||||
"alpha": RC.seed_payload(r1).hex(), "proof": bytes(bad).hex(), "beta": None})
|
||||
vv.append({"id": "other-input", "public": base64.b64decode(ROSTER_BY_ID["ben-spot"]["public"]).hex(),
|
||||
"alpha": (RC.seed_payload(r1) + b"x").hex(), "proof": seed.hex(), "beta": None})
|
||||
wv(out, "vrf.jsonl", vv)
|
||||
# A checkpoint of the team's log (spec/23): every receipt and revocation of the example paper, then the roster.
|
||||
entries = [LG.entry(r) for r in hale["receipts"]] + [LG.entry(ROSTER)]
|
||||
note = LG.sign_checkpoint("scient.ing/log/smithlab", entries, [("ana", secret("ana"))])
|
||||
wv(out, "checkpoint.jsonl", [{"id": "smithlab", "origin": "scient.ing/log/smithlab", "entries": entries,
|
||||
"root": LG.root(entries), "signers": ["ana"], "note": note}])
|
||||
# Alignment.
|
||||
def item(i, runs, container, context="paragraph"):
|
||||
doc, s = sentence_doc(runs)
|
||||
@@ -1077,7 +1181,7 @@ def resolve_vectors(hale):
|
||||
if "alias" in a:
|
||||
anchor["node"] = a["alias"]
|
||||
res = AN.resolve(final, anchor)
|
||||
out.append({"id": cid, "package": "fixtures/packages/hale2026harmonic", "anchor": anchor, "expect": res})
|
||||
out.append({"id": cid, "package": "fixtures/packages/" + hale["key"], "anchor": anchor, "expect": res})
|
||||
return out
|
||||
|
||||
|
||||
@@ -1134,12 +1238,19 @@ def ann_hash(a):
|
||||
return H(jcs.dumpb({k: v for k, v in a.items() if k not in ("status", "receipt", "modified")}))
|
||||
|
||||
|
||||
TAG = "tag:scient.ing,2026:"
|
||||
|
||||
|
||||
def records(out, hale):
|
||||
names, final = hale["names"], hale["final"]
|
||||
doc = final.doc()
|
||||
vals = hashing.compute(doc)
|
||||
rev = final.rev
|
||||
I = names.ids
|
||||
HALE = hale["key"]
|
||||
QHALE = STORE + "/" + HALE
|
||||
OKAFOR = Names.registry["okafor2025open"].key
|
||||
DRAFT = Names.registry["smithlab2027charges"].key
|
||||
|
||||
def anchor(name, quote=None):
|
||||
nid = I[name]
|
||||
@@ -1164,7 +1275,7 @@ def records(out, hale):
|
||||
wr(out, "anchor-leaf.json", long_anchor)
|
||||
# A public layer's anchor on a paywalled paper: keyed by the pages, no text.
|
||||
fps = [p["page_fp"] for p, _ in hale["pages"]]
|
||||
layer_id = "urn:scienting:layer:smithlab/kp4m2x7q"
|
||||
layer_id = TAG + "layer:smithlab/kp4m2x7q"
|
||||
pk = crypto.page_key(fps, "layer:smithlab/kp4m2x7q")
|
||||
hk = hashing.Hasher(pk)
|
||||
ka = anchor("@cc_ak")
|
||||
@@ -1211,11 +1322,11 @@ def records(out, hale):
|
||||
"revenue_or_funding_usd": {"at_most": None}, "competes_with": None,
|
||||
"recorded_by": "acme-admin", "at": "2026-10-01T00:00:00Z"})
|
||||
# A layer with one annotation of each common kind.
|
||||
creator = {"id": "urn:scienting:key:ana", "type": "Person", "name": "Ana Example"}
|
||||
model = {"id": "urn:scienting:software:example-model", "type": "Software", "name": "A model (illustrative)"}
|
||||
creator = {"id": TAG + "key:ana", "type": "Person", "name": "Ana Example"}
|
||||
model = {"id": TAG + "software:example-model", "type": "Software", "name": "A model (illustrative)"}
|
||||
|
||||
def target(a):
|
||||
return {"type": "SpecificResource", "source": "urn:scienting:doc:smithlab/hale2026harmonic",
|
||||
return {"type": "SpecificResource", "source": TAG + "doc:" + QHALE,
|
||||
"selector": [{"type": "AnchorSelector", "anchor": a}]
|
||||
+ ([{"type": "TextQuoteSelector", "exact": a["quote"]["exact"],
|
||||
"prefix": a["quote"].get("prefix", ""), "suffix": a["quote"].get("suffix", "")}]
|
||||
@@ -1224,7 +1335,7 @@ def records(out, hale):
|
||||
def ann(body, tgt, motivation, status="proposed", who=creator, created="2026-10-11T19:00:00Z"):
|
||||
a = {"@context": CTX, "type": "Annotation", "layer": layer_id, "motivation": motivation, "creator": who,
|
||||
"created": created, "body": body, "target": tgt}
|
||||
a["id"] = "urn:scienting:annotation:smithlab/" + record_id(H(jcs.dumpb(a)))
|
||||
a["id"] = TAG + "annotation:smithlab/" + record_id(H(jcs.dumpb(a)))
|
||||
a["status"] = status
|
||||
return a
|
||||
|
||||
@@ -1267,7 +1378,7 @@ def records(out, hale):
|
||||
anns.append(ann({"type": "TextualBody", "kind": "comment", "value": "Agreed; see the residual panel."},
|
||||
{"type": "SpecificResource", "source": anns[2]["id"]}, "replying", created="2026-10-11T19:05:00Z"))
|
||||
confirmed = [a for a in anns if a["status"] == "confirmed"]
|
||||
lr = RC.seal({"format": "scienting-receipt/1", "doc": "hale2026harmonic", "rev": rev, "level": "F3",
|
||||
lr = RC.seal({"format": "scienting-receipt/1", "doc": HALE, "rev": rev, "level": "F3",
|
||||
"scope": "targeted:layer", "nodes": [],
|
||||
"records": sorted([["annotation", a["id"].rsplit("/", 1)[1], ann_hash(a)] for a in confirmed]),
|
||||
"evidence": [], "by": "ana", "at": "2026-10-11T19:30:00Z"}, secret("ana"), "ana")
|
||||
@@ -1278,12 +1389,12 @@ def records(out, hale):
|
||||
"purpose": "Glosses, claim links and exercises for new students.",
|
||||
"audience": "student", "visibility": "team", "license": {"spdx": "CC-BY-4.0"},
|
||||
"creator": creator, "created": "2026-10-11T19:00:00Z", "project": "smithlab-charges",
|
||||
"docs": ["smithlab/hale2026harmonic"], "total": len(anns)})
|
||||
"docs": [QHALE], "total": len(anns)})
|
||||
wr(out, "layer/annotations.jsonl", jsonl(anns))
|
||||
wr(out, "layer/receipt.json", lr)
|
||||
# Submissions and grades.
|
||||
ex_ann = anns[6]
|
||||
sub_ok = {"format": "scienting-submission/1", "exercise": ex_ann["id"], "doc": "smithlab/hale2026harmonic",
|
||||
sub_ok = {"format": "scienting-submission/1", "exercise": ex_ann["id"], "doc": QHALE,
|
||||
"rev": rev, "student": "s-7k2m", "answers": {"anchors": [anchor("@cc_ak")]},
|
||||
"submitted_at": "2026-10-20T10:00:00Z"}
|
||||
sub_ok["id"] = record_id(H(jcs.dumpb(sub_ok)))
|
||||
@@ -1291,7 +1402,7 @@ def records(out, hale):
|
||||
sub_miss.pop("id")
|
||||
sub_miss["id"] = record_id(H(jcs.dumpb(sub_miss)))
|
||||
ch_ann = anns[7]
|
||||
sub_check = {"format": "scienting-submission/1", "exercise": ch_ann["id"], "doc": "smithlab/hale2026harmonic",
|
||||
sub_check = {"format": "scienting-submission/1", "exercise": ch_ann["id"], "doc": QHALE,
|
||||
"rev": rev, "student": "s-7k2m",
|
||||
"answers": {"verdict": "supports", "anchors": [anchor("@s1", "most common description of covalent")]},
|
||||
"submitted_at": "2026-10-20T10:05:00Z"}
|
||||
@@ -1302,7 +1413,7 @@ def records(out, hale):
|
||||
g = GR.grade(body, sub, final)
|
||||
wr(out, "submissions/%s.json" % name, sub)
|
||||
wr(out, "grades/%s.json" % name, g)
|
||||
grades.append({"id": name, "package": "fixtures/packages/hale2026harmonic", "exercise": body,
|
||||
grades.append({"id": name, "package": "fixtures/packages/" + hale["key"], "exercise": body,
|
||||
"submission": sub, "expect": g})
|
||||
wv(out, "grading.jsonl", grades)
|
||||
# Project.
|
||||
@@ -1311,15 +1422,15 @@ def records(out, hale):
|
||||
"members": [{"person": "Ana Example", "role": "owner", "keys": ["ana"]},
|
||||
{"person": "Ben Example", "role": "editor", "keys": ["ben", "ben-spot"]},
|
||||
{"person": "Cara Example", "role": "reader", "keys": ["cara"]}],
|
||||
"documents": [{"doc": "smithlab/hale2026harmonic", "reason": "The bond constants we start from.",
|
||||
"documents": [{"doc": QHALE, "reason": "The bond constants we start from.",
|
||||
"tags": ["parameters"], "status": {"Ben Example": "read", "Cara Example": "reading"}},
|
||||
{"doc": "okafor2025open", "reason": "Solvent shifts for the validation set.", "tags": ["spectra"]}],
|
||||
{"doc": OKAFOR, "reason": "Solvent shifts for the validation set.", "tags": ["spectra"]}],
|
||||
"paths": [{"id": "onboarding", "title": "First week", "steps": [
|
||||
{"type": "document", "ref": "smithlab/hale2026harmonic", "note": "Read the introduction."},
|
||||
{"type": "node", "ref": "smithlab/hale2026harmonic#" + I["@tab1"], "note": "Then the table."},
|
||||
{"type": "document", "ref": QHALE, "note": "Read the introduction."},
|
||||
{"type": "node", "ref": QHALE + "#" + I["@tab1"], "note": "Then the table."},
|
||||
{"type": "claim", "ref": "lode:amber/k-bond"}]}],
|
||||
"layers": [layer_id], "datasets": ["ds-hale-bond-params", "ds-hale-ctct-scan"], "views": ["ctct-scan"],
|
||||
"drafts": ["smithlab/smithlab2027charges"], "claims": {"lode": "amber"},
|
||||
"drafts": [STORE + "/" + DRAFT], "claims": {"lode": "amber"},
|
||||
"settings": {"digit_rule": "anchored-sections", "drift_threshold": "0.25",
|
||||
"spot_check": {"rate": "0.05", "minimum": 10}, "rights_unknown": "refuse",
|
||||
"stages": "scienting-stages-default"}})
|
||||
@@ -1328,7 +1439,7 @@ def records(out, hale):
|
||||
pa = I["@pa"]
|
||||
ft = {t["text"]: t["id"] for t in doc.nodes[pa]["attrs"]["figure_text"]}
|
||||
pts = [((110.0, 360.0), ("1.45", "3.10")), ((150.0, 330.0), ("1.50", "0.40")), ((190.0, 300.0), ("1.55", "2.80"))]
|
||||
dg = {"format": "scienting-digitization/1", "doc": "smithlab/hale2026harmonic",
|
||||
dg = {"format": "scienting-digitization/1", "doc": QHALE,
|
||||
"target": {"anchor": AN.make_anchor(doc, vals, pa, rev, store=STORE), "page_fp": p2["page_fp"],
|
||||
"region": [72.0, 260.0, 300.0, 410.0], "witness": hale["witness"]["sha256"]},
|
||||
"axes": [{"id": "x", "scale": "linear", "unit": "angstrom",
|
||||
@@ -1348,7 +1459,7 @@ def records(out, hale):
|
||||
dg["id"] = record_id(H(jcs.dumpb(dg)))
|
||||
wr(out, "digitization.json", dg)
|
||||
body = {k: v for k, v in dg.items() if k != "confirmation"}
|
||||
conf = RC.seal({"format": "scienting-receipt/1", "doc": "hale2026harmonic", "rev": rev, "level": "F3",
|
||||
conf = RC.seal({"format": "scienting-receipt/1", "doc": HALE, "rev": rev, "level": "F3",
|
||||
"scope": "targeted:digitization", "nodes": [],
|
||||
"records": [["digitization", dg["id"], H(jcs.dumpb(body))]],
|
||||
"evidence": [{"kind": "crop", "node": pa, "sha256": crop_sha("overlay of panel a")}],
|
||||
@@ -1390,14 +1501,14 @@ def records(out, hale):
|
||||
k = len(q1["qa"]["errors"]) + len(q2["qa"]["errors"])
|
||||
wr(out, "qa-report.json", RC.seal({
|
||||
"format": "scienting-qa-report/1", "delivery": "smithlab evidence pilot (fictional)",
|
||||
"docs": ["smithlab/hale2026harmonic"], "receipts": [hale["by_name"]["r1"]["id"], hale["by_name"]["r4"]["id"]],
|
||||
"docs": [QHALE], "receipts": [hale["by_name"]["r1"]["id"], hale["by_name"]["r4"]["id"]],
|
||||
"reviewers": [{"key": "ana", "sampled": n, "errors": k, "confidence": "0.95", "method": "clopper-pearson",
|
||||
"bound": RC.bound_string(k, n)}],
|
||||
"by": "ana", "at": "2026-10-12T10:00:00Z"}, secret("ana"), "ana"))
|
||||
# A checklist for an accessible HTML export of the paper, and the receipt that covers it (spec/08 §8.11).
|
||||
html_sha = H("accessible HTML export of hale2026harmonic (synthetic)")
|
||||
html_sha = H("accessible HTML export of %s (synthetic)" % HALE)
|
||||
checker = {"name": "example-checker", "version": "1.0"}
|
||||
checklist = {"format": "scienting-checklist/1", "doc": "smithlab/hale2026harmonic", "rev": rev,
|
||||
checklist = {"format": "scienting-checklist/1", "doc": QHALE, "rev": rev,
|
||||
"standard": {"name": "WCAG 2.1", "level": "AA", "url": "https://www.w3.org/TR/WCAG21/"},
|
||||
"export": {"kind": "html", "sha256": html_sha, "size": 48213},
|
||||
"items": [
|
||||
@@ -1417,7 +1528,7 @@ def records(out, hale):
|
||||
checklist["id"] = record_id(checklist_hash)
|
||||
wr(out, "checklist/checklist.json", checklist)
|
||||
wr(out, "checklist/receipt.json", RC.seal({
|
||||
"format": "scienting-receipt/1", "doc": "hale2026harmonic", "rev": rev, "level": "F3",
|
||||
"format": "scienting-receipt/1", "doc": HALE, "rev": rev, "level": "F3",
|
||||
"scope": "targeted:checklist", "nodes": [], "records": [["checklist", checklist["id"], checklist_hash]],
|
||||
"evidence": [{"kind": "source-file", "sha256": html_sha, "note": "The exported HTML."}],
|
||||
"by": "ana", "at": "2026-10-12T11:30:00Z"}, secret("ana"), "ana"))
|
||||
@@ -1426,23 +1537,41 @@ def records(out, hale):
|
||||
dataset_anchors = [json.loads(x) for x in
|
||||
hale["files"]["datasets/ds-hale-bond-params/anchors.jsonl"].decode().splitlines()]
|
||||
anchored = sorted({a["anchor"]["node"] for a in dataset_anchors} | {I["@s2"], pa})
|
||||
wv(out, "targets.jsonl", [{"id": "hale-" + rule, "package": "fixtures/packages/hale2026harmonic",
|
||||
wv(out, "targets.jsonl", [{"id": "hale-" + rule, "package": "fixtures/packages/" + hale["key"],
|
||||
"anchored": anchored, "digit_rule": rule,
|
||||
"expect": {"nodes": TG.target_set(doc, anchored, rule)}}
|
||||
for rule in TG.DIGIT_RULES])
|
||||
# Proofs (spec/08 §8.5, spec/11 §11.5): one node of the first receipt was covered, and revision 12 extends
|
||||
# revision 8 of the correction history.
|
||||
r1 = hale["by_name"]["r1"]
|
||||
items = crypto.receipt_items(r1)
|
||||
wr(out, "proof-inclusion.json", {"format": "scienting-proof/1", "kind": "inclusion", "receipt": r1["id"],
|
||||
"item": items[3], "index": 3, "size": len(items), "root": r1["root"],
|
||||
"path": crypto.inclusion_path(items, 3)})
|
||||
hist = history_items(hale["records"])
|
||||
wr(out, "proof-consistency.json", {"format": "scienting-proof/1", "kind": "consistency", "doc": QHALE,
|
||||
"from_rev": 8, "to_rev": len(hist), "from_root": crypto.history_root(hist[:8]),
|
||||
"to_root": hale["document"]["history"], "path": crypto.consistency_proof(hist, 8)})
|
||||
# A checkpoint of the team's log (spec/23), as the parsed form of its signed note.
|
||||
entries = [LG.entry(r) for r in hale["receipts"]] + [LG.entry(ROSTER)]
|
||||
note = LG.sign_checkpoint("scient.ing/log/smithlab", entries, [("ana", secret("ana"))])
|
||||
parsed = LG.parse_checkpoint(note)
|
||||
wr(out, "checkpoint.json", {"format": LG.FORMAT, "origin": parsed["origin"], "size": parsed["size"],
|
||||
"root": parsed["root"], "note": note,
|
||||
"signatures": [{"name": n, "key_hash": kh.hex(), "value": base64.b64encode(sig).decode()}
|
||||
for n, kh, sig in parsed["signatures"]]})
|
||||
# Draft snapshot and sentence IDs.
|
||||
s5 = I["@s5"]
|
||||
snap_anchor = AN.make_anchor(doc, vals, s5, rev, store=STORE)
|
||||
wr(out, "snapshot.json", {"format": "scienting-snapshot/1", "store": STORE, "made_at": "2026-10-11T21:00:00Z",
|
||||
"quotes": [{"address": AN.format_address(STORE, "hale2026harmonic", s5, vals[s5]["node_hash"]),
|
||||
"quotes": [{"address": AN.format_address(STORE, HALE, s5, vals[s5]["node_hash"]),
|
||||
"anchor": snap_anchor, "text": norm(hashing.plain(doc, s5)),
|
||||
"fidelity": "F3", "result": "current"}],
|
||||
"claims": [{"ref": "amber/k-bond", "pin": "illustrative-pin", "level": "illustrative",
|
||||
"typst": "K_r"}],
|
||||
"datasets": [{"id": "ds-hale-ctct-scan", "hash": ds_hash}]})
|
||||
draft = Names.registry["smithlab2027charges"]
|
||||
dd = Doc("smithlab2027charges", [json.loads(x) for x in open(os.path.join(
|
||||
out, "fixtures", "packages", "smithlab2027charges", "nodes.jsonl"))])
|
||||
dd = Doc(DRAFT, [json.loads(x) for x in open(os.path.join(out, "fixtures", "packages", DRAFT, "nodes.jsonl"))])
|
||||
lines = []
|
||||
for nid in dd.preorder():
|
||||
if dd.nodes[nid]["kind"] == "sentence":
|
||||
@@ -1520,12 +1649,12 @@ def invalid(out, tiny):
|
||||
content=[{"kind": "text", "text": "A second paragraph.", "style": ["sub", "sup"]}]))},
|
||||
"A run cannot be both a subscript and a superscript.")
|
||||
# Zipped packages that break spec/12 §12.2: the tiny package's files plus one bad entry.
|
||||
tiny_dir = os.path.join(out, "fixtures", "packages", "tiny2026example")
|
||||
tiny_dir = os.path.join(out, "fixtures", "packages", tiny["key"])
|
||||
entries = []
|
||||
for rel in sorted(os.path.relpath(os.path.join(d, f), tiny_dir).replace(os.sep, "/")
|
||||
for d, _, fs in os.walk(tiny_dir) for f in fs):
|
||||
with open(os.path.join(tiny_dir, rel), "rb") as fh:
|
||||
entries.append(("tiny2026example/" + rel, fh.read(), 0o644))
|
||||
entries.append((tiny["key"] + "/" + rel, fh.read(), 0o644))
|
||||
|
||||
def zip_case(name, rule, note, extra):
|
||||
d = os.path.join(base, name)
|
||||
@@ -1541,9 +1670,9 @@ def invalid(out, tiny):
|
||||
fh.write((jcs.dumps({"rule": rule, "note": note}) + "\n").encode())
|
||||
|
||||
zip_case("zip-traversal", "package.zip.entry", "An archive entry whose path leaves the package folder.",
|
||||
[("tiny2026example/../escape.txt", b"outside\n", 0o644)])
|
||||
[(tiny["key"] + "/../escape.txt", b"outside\n", 0o644)])
|
||||
zip_case("zip-symlink", "package.zip.entry", "An archive entry that is a symbolic link.",
|
||||
[("tiny2026example/assets/link", b"document.json", 0o120777)])
|
||||
[(tiny["key"] + "/assets/link", b"document.json", 0o120777)])
|
||||
# Rules the audit found no broken package for.
|
||||
case("missing-required-file", "package.file.required", {"provenance.json": None}, "A required file is missing.")
|
||||
case("counts-wrong", "doc.counts", {"document.json": _doc_edit(files, counts={"nodes": 1, "leaves": 1})},
|
||||
@@ -1557,7 +1686,7 @@ def invalid(out, tiny):
|
||||
{"id": nm["@a"], "kind": "retired", "successor": None, "node_kind": "sentence", "rev": 1}])},
|
||||
"An ID that is both live and retired.")
|
||||
case("manifest-key", "package.manifest.key", {}, "The manifest names another key than document.json.",
|
||||
raw=lambda d: _manifest_edit(d, key="other2026example"))
|
||||
raw=lambda d: _manifest_edit(d, key="other2026example-0000"))
|
||||
case("unsafe-path", "package.path.unsafe", {}, "The manifest lists a path that leaves the package.",
|
||||
raw=lambda d: _manifest_edit(d, files=[{"path": "../escape.txt", "sha256": "0" * 64, "size": 1}]
|
||||
+ json.load(open(os.path.join(d, "MANIFEST.json")))["files"]))
|
||||
@@ -1567,6 +1696,15 @@ def invalid(out, tiny):
|
||||
case("signer-mismatch", "doc.receipt.signature", {"receipts.jsonl": jsonl([
|
||||
RC.seal(dict(signer, by="cara"), secret("ana"), "ana")])},
|
||||
"A receipt whose signature was made with another key than the signer it names.")
|
||||
case("history-wrong", "doc.history", {"document.json": _doc_edit(files, history="4" * 64)},
|
||||
"document.json's history is not the root over the corrections.")
|
||||
d0 = json.loads(files["document.json"])
|
||||
wk = d0["work"]["key"]
|
||||
bad = wk[:-4] + "zzzz"
|
||||
case("key-suffix", "doc.key.suffix", {"document.json": _doc_edit(files, key=bad, work=dict(d0["work"], key=bad))},
|
||||
"A work key whose hash part is not the hash of the work's DOI.")
|
||||
case("key-work", "doc.key.work", {"document.json": _doc_edit(files, key=wk + ".si")},
|
||||
"A document key whose suffix is not the version's.")
|
||||
|
||||
|
||||
def _manifest_edit(d, **kw):
|
||||
@@ -1655,10 +1793,11 @@ def main(out):
|
||||
build_checks_only(out, hale)
|
||||
build_simple(out, "okafor2025open")
|
||||
build_simple(out, "smithlab2027charges")
|
||||
tiny = tiny_package(os.path.join(out, "fixtures", "packages", "tiny2026example"))
|
||||
tiny = tiny_package(None)
|
||||
tiny_dir = os.path.join(out, "fixtures", "packages", tiny["key"])
|
||||
write(tiny_dir, tiny["files"])
|
||||
# The same package zipped as spec/12 §12.2 says a producer writes a byte-identical archive.
|
||||
write_zip(os.path.join(out, "fixtures", "packages", "tiny2026example"),
|
||||
os.path.join(out, "fixtures", "packages", "tiny2026example" + ZIP_EXTENSION))
|
||||
write_zip(tiny_dir, tiny_dir + ZIP_EXTENSION)
|
||||
records(out, hale)
|
||||
vectors(out, hale, tiny)
|
||||
invalid(out, tiny)
|
||||
|
||||
Reference in new issue
Block a user