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

-69
View File
@@ -1,69 +0,0 @@
"""The target set of targeted F3: the nodes a team's claims, data and quotations rest on.
Non-normative; see spec/08-fidelity.md §8.10.
"""
from .model import LEAVES, MATH_KINDS
from .text import is_digit
DIGIT_RULES = ("all", "anchored-sections", "none")
# The kinds that count as the section a node sits in: a section, or the front or back matter outside any.
SECTION_KINDS = frozenset(["section", "front", "back"])
DIGIT_KINDS = frozenset(["sentence", "cell"])
DIGIT_RUNS = frozenset(["text", "math"])
def has_digit_run(node):
"""Whether a `text` or `math` run of the node holds a digit (General Category Nd)."""
for r in node.get("content") or []:
if not isinstance(r, dict) or r.get("kind") not in DIGIT_RUNS:
continue
s = r.get("text") if r["kind"] == "text" else r.get("source")
if any(is_digit(ch) for ch in s or ""):
return True
return False
def enclosing_section(doc, nid):
"""The nearest ancestor-or-self that is a section, or the front or back matter, else the root."""
if doc.nodes[nid]["kind"] in SECTION_KINDS:
return nid
for a in reversed(doc.ancestors(nid)):
if doc.nodes[a]["kind"] in SECTION_KINDS:
return a
return doc.root
def target_set(doc, anchored, digit_rule="all"):
"""The target set as `[[node, reason], …]` in document order, with `reason` the first that applies of
`anchored`, `equation` and `digits`.
`anchored` lists the live nodes that extracts, dataset values, confirmed claim links, draft quotations or
pinned citations anchor; a container among them contributes its leaves. Every equation and equation line
is a target. Under `all`, so is every sentence or cell with a digit in a text or math run; under
`anchored-sections`, only those in a section that holds an anchored node; under `none`, none.
"""
if digit_rule not in DIGIT_RULES:
raise ValueError("digit rule must be one of %s" % ", ".join(DIGIT_RULES))
anchored = set(anchored)
unknown = sorted(a for a in anchored if a not in doc.nodes)
if unknown:
raise ValueError("anchored nodes are not live: %s" % ", ".join(unknown))
reasons = {}
for nid in sorted(anchored):
if doc.nodes[nid]["kind"] in LEAVES:
reasons[nid] = "anchored"
else:
for leaf in doc.leaves(nid):
reasons.setdefault(leaf, "anchored")
for nid in doc.preorder():
if doc.nodes[nid]["kind"] in MATH_KINDS:
reasons.setdefault(nid, "equation")
if digit_rule != "none":
sections = {enclosing_section(doc, a) for a in anchored}
for nid in doc.preorder():
n = doc.nodes[nid]
if n["kind"] in DIGIT_KINDS and has_digit_run(n):
if digit_rule == "all" or enclosing_section(doc, nid) in sections:
reasons.setdefault(nid, "digits")
return [[nid, reasons[nid]] for nid in doc.preorder() if nid in reasons]