refactor: move the reference implementation to src/paperspec

The oracle had two import names, `tools.oracle` for the tools and
`paperspec` for other repositories, which could load the same code twice
in one process and needed a gate keyed on the module name. It now lives
in the standard src layout under its one name.

- git mv tools/oracle -> src/paperspec; the 22 rule modules are unchanged.
- The tools import `paperspec`. `tools/__init__.py` now holds the Python
  3.14 and cryptography/jsonschema check and puts src/ first on sys.path,
  so the tools run this checkout's code with or without pixi. The
  package's own __init__ checks nothing and exposes __version__.
- pyproject.toml: version from git tags via setuptools-scm
  (guess-next-dev, node-and-timestamp, src/paperspec/_version.py,
  gitignored). The packages stay listed rather than found because
  schemas/, sql/ and vectors/ stay at the root: the spec links to them and
  the site serves the schemas at their $id URLs.
- pixi.toml: a `dev` feature and environment with setuptools,
  setuptools-scm and python-build, and `pixi run -e dev wheel`.
- check.py scans src/paperspec for rule IDs; README, CONTRIBUTING, the
  FAQ, the overview, the vectors and tools READMEs and the changelog point
  at the new path. The FAQ no longer says the oracle is not a library.

pixi.lock is not re-solved in this commit: with a dynamic version, pixi
builds the package metadata to lock, which needs the conda environment
installed. Run `pixi lock` and commit the result before pushing; the site
workflow installs with --locked.

pixi run check passes; no computed value, vector, fixture, schema or SQL
file changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alexmandClaude committed 2026-10-15 09:00:40 -04:00
1 parent 2b4467d315
commit a4eecb4d5b
40 files changed
+134 -84

No files matched your search

-106
View File
@@ -1,106 +0,0 @@
"""Rights records, policies, stage checks and backend conditions.
Non-normative; see spec/14-rights.md.
"""
USES = ("read", "tdm", "redistribute", "quote-public", "accessible-copy")
PURPOSES = ("internal", "external")
PROCESSORS = ("local", "tenancy", "outside-no-retention", "outside")
AI_PAIRS = tuple(p + "/" + q for p in PURPOSES for q in PROCESSORS)
def _value(v):
return v if v in (True, False) else "unknown"
def flat(rights):
"""A rights record's uses as one flat map: the five uses plus ai/<purpose>/<processor>."""
uses = rights.get("uses", {})
out = {u: _value(uses.get(u, "unknown")) for u in USES}
ai = uses.get("ai", {})
for pair in AI_PAIRS:
out["ai/" + pair] = _value(ai.get(pair, "unknown"))
return out
def _policy_applies(policy, witness, now):
if policy.get("valid_until") and now >= policy["valid_until"]:
return False
if policy.get("at") and now < policy["at"]:
return False
a = policy.get("applies_to", {})
r = witness["rights"]
if "basis" in a and r.get("basis") not in a["basis"]:
return False
if "license" in a and (r.get("license") or {}).get("spdx") not in a["license"]:
return False
if "doi_prefixes" in a:
doi = (witness.get("origin") or {}).get("doi") or ""
if not any(doi.startswith(p) for p in a["doi_prefixes"]):
return False
if "witnesses" in a and witness["sha256"] not in a["witnesses"]:
return False
return True
def effective(witness, policies, now):
"""The witness's uses at `now`, with unknowns resolved by applicable policies, or all false if expired."""
r = witness["rights"]
f = flat(r)
if r.get("valid_until") and now >= r["valid_until"]:
return {k: False for k in f}, True
for p in policies:
if not _policy_applies(p, witness, now):
continue
pf = flat({"uses": p.get("uses", {})})
for k, v in pf.items():
if f[k] == "unknown" and v != "unknown":
f[k] = v
return f, False
def combine(flats):
"""The most restrictive of several witnesses' uses: false over unknown over true."""
out = {}
for k in flats[0]:
vals = [f[k] for f in flats]
out[k] = False if False in vals else ("unknown" if "unknown" in vals else True)
return out
def check(stage, processor, uses):
"""Whether a stage may run on a processor under `uses`. Returns (allowed, lacking)."""
lacking = []
for u in stage.get("uses", []):
if uses[u] is not True:
lacking.append(u if uses[u] is False else u + "?")
ai = stage.get("ai")
if ai:
allowed_here = ai.get("processors")
if allowed_here and processor not in allowed_here:
lacking.append("processor:" + processor)
k = "ai/" + ai["purpose"] + "/" + processor
if uses[k] is not True:
lacking.append(k if uses[k] is False else k + "?")
return (not lacking), lacking
def backend_allowed(backend, profile):
"""Whether a team profile meets every condition a backend's licenses impose. Returns (ok, failed)."""
failed = []
for cond in backend.get("conditions", []):
k = cond["kind"]
if k == "revenue-cap":
r = profile.get("revenue_or_funding_usd")
if r is None or r.get("at_most") is None or r["at_most"] > cond["usd"]:
failed.append(cond["id"])
elif k == "no-competition":
comp = profile.get("competes_with")
if comp is None or cond["vendor"] in comp:
failed.append(cond["id"])
elif k == "purpose":
if profile.get("purpose") not in cond["allowed"]:
failed.append(cond["id"])
else:
failed.append(cond["id"])
return (not failed), failed