Reorganize paperspec commands and pin dependency versions #2
No files matched your search
@@ -18,4 +18,4 @@ jobs:
|
||||
- name: Check the repository
|
||||
run: pixi run check
|
||||
- name: Stress the reference implementation
|
||||
run: pixi run python tools/stress.py
|
||||
run: pixi run stress
|
||||
@@ -0,0 +1,66 @@
|
||||
# Publishes the paperspec Python package to this instance's PyPI registry when a version tag (v0.1.1) is
|
||||
# pushed; see deploy/README.md for the secrets it needs and how to install from the registry.
|
||||
# The package version comes from the tag through setuptools-scm, so the job refuses to upload unless the
|
||||
# built version equals the tag. Run it by hand only on a tag, for example after adding the secrets.
|
||||
name: publish
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: publish-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: linux-base
|
||||
container:
|
||||
image: git.scient.ing/infra/linux-base:26.10.7
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
REGISTRY: ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/pypi
|
||||
steps:
|
||||
- name: Require a version tag
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" != refs/tags/v* ]]; then
|
||||
echo "Publish runs on a version tag such as v0.1.1; $GITHUB_REF is not one."
|
||||
exit 1
|
||||
fi
|
||||
# The whole history, so setuptools-scm finds the tag and computes the release version.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Check the repository
|
||||
run: pixi run --locked check
|
||||
- name: Build the sdist and wheel
|
||||
run: pixi run --locked -e dev dist
|
||||
- name: Confirm the built version is the tag
|
||||
run: |
|
||||
tag="${GITHUB_REF_NAME#v}"
|
||||
wheels=(dist/paperspec-*.whl)
|
||||
built="$(basename "${wheels[0]}" | cut -d- -f2)"
|
||||
echo "tag $GITHUB_REF_NAME, built $built"
|
||||
if [ "${#wheels[@]}" -ne 1 ] || [ "$built" != "$tag" ]; then
|
||||
echo "The built version is not the tag's; is the tag on the commit this job checked out?"
|
||||
exit 1
|
||||
fi
|
||||
- name: Check the package metadata
|
||||
run: pixi run --locked -e dev twine check --strict dist/*
|
||||
# --skip-existing makes a rerun on the same tag succeed instead of failing on the files already there.
|
||||
- name: Upload to the package registry
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PACKAGE_USER }}
|
||||
TWINE_PASSWORD: ${{ secrets.PACKAGE_TOKEN }}
|
||||
TWINE_REPOSITORY_URL: ${{ env.REGISTRY }}
|
||||
run: |
|
||||
if [ -z "$TWINE_USERNAME" ] || [ -z "$TWINE_PASSWORD" ]; then
|
||||
echo "The secrets PACKAGE_USER and PACKAGE_TOKEN are not set; deploy/README.md says how."
|
||||
exit 1
|
||||
fi
|
||||
pixi run --locked -e dev twine upload --non-interactive --skip-existing dist/*
|
||||
echo "Published to $REGISTRY: pip install --index-url $REGISTRY/simple/ paperspec"
|
||||
+5
-2
@@ -3,7 +3,10 @@ __pycache__/
|
||||
*.db
|
||||
*.db.assets/
|
||||
|
||||
# the website, built by tools/site.py
|
||||
# the package version, written by setuptools-scm when the package is built or installed
|
||||
/src/paperspec/_version.py
|
||||
|
||||
# the website, built by `paperspec site`
|
||||
/_site/
|
||||
|
||||
# pixi environments
|
||||
@@ -758,7 +761,7 @@ FodyWeavers.xsd
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/osx,python,pycharm,windows,visualstudio,visualstudiocode,zsh,jetbrains,jupyternotebooks
|
||||
|
||||
# The Visual Studio rules above ignore every packages/ and log/ folder; these hold fixtures that tools/check.py
|
||||
# The Visual Studio rules above ignore every packages/ and log/ folder; these hold fixtures that `paperspec check`
|
||||
# compares byte for byte, so keep them.
|
||||
!/fixtures/packages/*
|
||||
!/fixtures/records/log/
|
||||
@@ -19,3 +19,19 @@ The first release.
|
||||
- Guides for newcomers: an overview, the concepts, a walkthrough of the example paper, runnable examples, the rationale, a FAQ and a glossary.
|
||||
- Tools that build and check everything, validate packages and zipped packages and run proposed corrections through the gate, load packages into SQLite or a bundle, and stress the reference implementation with random inputs.
|
||||
- A pixi workspace pinned to Python 3.14, with tasks for checking, building, linting, validating and loading.
|
||||
|
||||
### 0.1.1
|
||||
|
||||
Packaging, commands and lazy imports only; the formats stay at 0.1.0, and no computed value, vector, fixture, schema or SQL file changed.
|
||||
|
||||
- The oracle moves from `tools/oracle/` to `src/paperspec/` and installs as the Python package `paperspec` (`pyproject.toml`, setuptools), with `schemas/`, `sql/` and `vectors/` as the data subpackages `paperspec.schemas`, `paperspec.sql` and `paperspec.vectors`, so other repositories import the reference implementation instead of reimplementing it.
|
||||
- The package version comes from git tags through setuptools-scm.
|
||||
- The base package has no dependencies and requires Python 3.14, the only version with the Unicode 16.0 character data the specification pins, and refuses to import under any other Unicode version; the extras `paperspec[schema]` (`jsonschema`, `referencing`), `paperspec[crypto]` (`cryptography`) and `paperspec[tools]` (both) cover `schema` and the HKDF and Ed25519 functions of `crypto`, which now import their packages on first use.
|
||||
- `paperspec.resources.path` finds the schemas, SQL and vectors in an installed wheel and in an editable install alike.
|
||||
- The programs in `tools/` become subcommands of one command, `paperspec`, in `src/paperspec/cli/`: `paperspec validate`, `load`, `check`, `build`, `stress` and `site`, which the pixi tasks and the workflows run, and which `python -m paperspec` also runs.
|
||||
The scripts `tools/*.py` are gone, so `python3 tools/check.py` and the like no longer work; `tools/` keeps the page that documents the commands.
|
||||
- `paperspec --help` says which commands work anywhere and which need a checkout, each command's `--help` gives advice and examples, a misspelled command gets a suggestion, and `paperspec --version` prints the package version and the Unicode version it pins.
|
||||
- `paperspec load` refuses `--built-from` and `--store` without `--bundle`, where it used to ignore them.
|
||||
- `src/` is Apache-2.0, as `tools/` was, and the package metadata states `Apache-2.0 AND CC0-1.0` for the code and the data it carries.
|
||||
- The pixi workspace installs the package editable, and a `dev` environment builds the sdist and the wheel with `pixi run -e dev dist`.
|
||||
- Pushing a version tag publishes the package to the Gitea package registry through `.gitea/workflows/publish.yml` ([deploy/](deploy/README.md#publish-the-python-package)); a release tag versions the package, and [CONTRIBUTING.md](CONTRIBUTING.md#releasing) says how that relates to the format version.
|
||||
+10
-7
@@ -13,11 +13,11 @@ The schemas, SQL, registries, vectors and fixtures then follow the chapter ([spe
|
||||
2. **Change the schemas, SQL and registries** that the rule shapes.
|
||||
A new refusal gets a rule ID in [registry/rules.json](registry/rules.json).
|
||||
3. **Compute, never type, the expected values.**
|
||||
Change the oracle in [tools/oracle/](tools/oracle/) to apply the rule, and add the case that shows it to [tools/build.py](tools/build.py): a vector, a fixture or a broken package in `fixtures/invalid/`.
|
||||
Change the oracle in [src/paperspec/](src/paperspec/) to apply the rule, and add the case that shows it to [src/paperspec/cli/build.py](src/paperspec/cli/build.py): a vector, a fixture or a broken package in `fixtures/invalid/`.
|
||||
Then run `pixi run build`, which rewrites `fixtures/` and `vectors/`.
|
||||
4. **Run the checks.**
|
||||
`pixi run check` must end with `All checks pass.`
|
||||
Without pixi, run `python3 tools/check.py`, with Python 3.14 and two packages, `jsonschema` and `cryptography` installed.
|
||||
Without pixi, install the checkout with `pip install -e ".[tools]"` on Python 3.14, then run `paperspec check`.
|
||||
5. **Record the change** in [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
## Versions
|
||||
@@ -29,17 +29,20 @@ A rebuild that changes any file under `vectors/` or any hash in `fixtures/` is a
|
||||
## New node kinds and run kinds
|
||||
|
||||
Propose a kind with the vector or fixture that shows it.
|
||||
A node kind needs its row in [chapter 3](spec/03-nodes.md), the node schema, and its entry in the `CHILDREN` table of `tools/oracle/model.py`.
|
||||
A node kind needs its row in [chapter 3](spec/03-nodes.md), the node schema, and its entry in the `CHILDREN` table of `src/paperspec/model.py`.
|
||||
A run kind needs its row in [section 3.5](spec/03-nodes.md#35-inline-runs) and the node schema, and its text in the plain projection ([3.6](spec/03-nodes.md#36-the-plain-projection)) and in the strict and lossy projections ([5.3](spec/05-hashes.md#53-the-strict-projection), [5.5](spec/05-hashes.md#55-the-lossy-projection)).
|
||||
|
||||
## Releasing
|
||||
|
||||
A release is a tag on `main` whose version is the top heading of [CHANGELOG.md](CHANGELOG.md).
|
||||
A release is a tag `vX.Y.Z` on `main`, and its version is the version of the Python package `paperspec`, which setuptools-scm reads from the tag.
|
||||
A release that changes the formats takes the new format version, which becomes the top heading of [CHANGELOG.md](CHANGELOG.md) and the version that `pixi.toml` and the registries state.
|
||||
A release that changes only the code takes the next patch version and records it as a subsection under the current format version, as `### 0.1.1` sits under `## 0.1.0`.
|
||||
Pushing the tag publishes the package to the Gitea package registry ([deploy/](deploy/README.md#publish-the-python-package)).
|
||||
Before tagging:
|
||||
|
||||
1. `pixi run check` passes on Linux and macOS, which the workflow in `.gitea/workflows/` runs on every push.
|
||||
The check also confirms that every count the prose states (formats, schemas, vectors, broken packages) matches the repository, and that `pixi.toml`, the registries and `tools/build.py` state the same version.
|
||||
2. `python3 tools/stress.py` passes with its default seed, and with one or two others.
|
||||
The check also confirms that every count the prose states (formats, schemas, vectors, broken packages) matches the repository, and that `pixi.toml`, the registries and `src/paperspec/cli/build.py` state the same version.
|
||||
2. `paperspec stress` passes with its default seed, and with one or two others.
|
||||
3. The website is published from the release ([deploy/](deploy/README.md)), and the JSON-LD context at `https://paperspec.scient.ing/context/scienting-1.jsonld` and a schema at its `$id` both answer with their media types.
|
||||
Layers claim to be W3C Web Annotations, and a JSON-LD processor that cannot fetch the context cannot read them.
|
||||
4. The decisions that freeze on release are made: enumeration values and format identifiers never change meaning afterwards ([spec/22 §22.5](spec/22-conformance.md#225-stability)), so a name worth changing is changed now.
|
||||
@@ -56,4 +59,4 @@ The keys in `fixtures/keys/` are public test keys and must never sign anything e
|
||||
|
||||
## Licenses
|
||||
|
||||
Each path keeps the license that [LICENSE.md](LICENSE.md) gives it: prose CC-BY-4.0, the normative data CC0-1.0, and `tools/` Apache-2.0.
|
||||
Each path keeps the license that [LICENSE.md](LICENSE.md) gives it: prose CC-BY-4.0, the normative data CC0-1.0, and the code in `src/` and `tools/` Apache-2.0.
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
| --- | --- | --- |
|
||||
| `spec/`, `docs/`, `README.md`, `CONTRIBUTING.md`, `CHANGELOG.md` and the other prose | Creative Commons Attribution 4.0 International | `CC-BY-4.0` |
|
||||
| `schemas/`, `sql/`, `context/`, `registry/`, `vectors/`, `fixtures/` | Creative Commons Zero 1.0 Universal | `CC0-1.0` |
|
||||
| `tools/` | Apache License 2.0 | `Apache-2.0` |
|
||||
| `src/` (the `paperspec` package and its commands), `tools/` | Apache License 2.0 | `Apache-2.0` |
|
||||
|
||||
The legal texts are at <https://creativecommons.org/licenses/by/4.0/legalcode>, <https://creativecommons.org/publicdomain/zero/1.0/legalcode> and <https://www.apache.org/licenses/LICENSE-2.0>.
|
||||
|
||||
|
||||
@@ -61,7 +61,8 @@ The address grammar is in [spec/06 §6.1](spec/06-anchors.md#61-addresses), and
|
||||
| [registry/](registry/README.md) | The list of formats, the rule IDs validators report, download-stamp rules, and the default rights each processing stage requires. |
|
||||
| [vectors/](vectors/README.md) | Test vectors: inputs and the exact outputs every implementation must compute. |
|
||||
| [fixtures/](fixtures/README.md) | Example packages and records, deliberately broken packages, and the test keys that signed them. |
|
||||
| [tools/](tools/README.md) | The programs that build and check everything above. |
|
||||
| [tools/](tools/README.md) | How to run the commands that build and check everything above. |
|
||||
| [src/paperspec/](src/paperspec/) | The reference implementation of every rule and those commands, installable as the Python package `paperspec`. |
|
||||
| [docs/](docs/README.md) | Guides for people getting to know the project. |
|
||||
| [deploy/](deploy/README.md) | How the website at <https://paperspec.scient.ing> is built and served. |
|
||||
|
||||
@@ -80,10 +81,11 @@ Last, it loads every package into SQLite and back, and checks the prose and the
|
||||
Pixi lints the Markdown first.
|
||||
The run ends with `All checks pass.`
|
||||
`pixi task list` shows the other tasks: `quick`, `build`, `lint`, `load`, `validate` and `stress`.
|
||||
`pixi run python tools/site.py` builds the website into `_site/` ([deploy/](deploy/README.md)).
|
||||
`pixi run site` builds the website into `_site/` ([deploy/](deploy/README.md)).
|
||||
|
||||
The tools need Python 3.14 and two packages, `jsonschema` and `cryptography`, which pixi installs along with markdownlint for the Markdown.
|
||||
Without pixi, install those two with pip and run `python3 tools/check.py`.
|
||||
The commands need Python 3.14 and the package's `tools` extra (`jsonschema`, `referencing` and `cryptography`), which pixi installs along with markdownlint for the Markdown.
|
||||
Without pixi, install the checkout as the package with `pip install -e ".[tools]"` on Python 3.14, then run `paperspec check`.
|
||||
`paperspec --help` lists the commands, and each command's `--help` gives advice and examples ([tools/](tools/README.md)).
|
||||
|
||||
To explore a worked package, load the example paper (a synthetic chemistry paper with 12 corrections) into SQLite and query it; [docs/examples.md](docs/examples.md) has queries to start from.
|
||||
|
||||
@@ -131,5 +133,5 @@ Table columns and figure-text items have IDs and are not nodes ([spec/04 §4.2](
|
||||
|
||||
The specification and other prose are CC BY 4.0, so anyone may implement the formats.
|
||||
Schemas, SQL, the JSON-LD context, the registries, the vectors and the fixtures are CC0 1.0.
|
||||
The tools are Apache-2.0.
|
||||
The code, the `paperspec` package in `src/` with its commands, is Apache-2.0.
|
||||
See [LICENSE.md](LICENSE.md).
|
||||
+3
-2
@@ -96,11 +96,12 @@ Three of the nine fixture annotations have only an `AnchorSelector` (the section
|
||||
- Reader: Follow [chapter 6](../spec/06-anchors.md) to resolve an anchor.
|
||||
The file `vectors/resolve.jsonl` resolves seventeen anchors against `fixtures/packages/hale2026harmonic-damb` and gives every outcome.
|
||||
- JSON-LD processor: Use a JSON-LD 1.1 processor; the context relies on `@json` and `@version` 1.1.
|
||||
- Checker: `schemas/annotation.schema.json` and `schemas/layer.schema.json` validate the files. `tools/check.py` also checks that each annotation's ID derives from its content and that the receipt at `fixtures/records/layer/receipt.json` covers each `confirmed` annotation.
|
||||
- Checker: `schemas/annotation.schema.json` and `schemas/layer.schema.json` validate the files.
|
||||
`paperspec check` also checks that each annotation's ID derives from its content and that the receipt at `fixtures/records/layer/receipt.json` covers each `confirmed` annotation.
|
||||
|
||||
The traps:
|
||||
|
||||
- No test vector pins the context, and nothing in `tools/` reads `scienting-1.jsonld`.
|
||||
- No test vector pins the context, and nothing here runs a JSON-LD processor on it; `paperspec check` and `paperspec site` read only its term IRIs.
|
||||
The schemas require only that `@context` be an array of at least two non-empty strings; they do not check the URLs.
|
||||
A file with a wrong context can pass the checker and still expand wrongly.
|
||||
- `@context` is part of an annotation's content hash (the hash covers everything except `status`, `receipt` and `modified`).
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Serves the paperspec website built by `python3 tools/site.py` (the `_site/` folder), copied to /srv/www/paperspec.
|
||||
# Serves the paperspec website built by `paperspec site` (`pixi run site`, the `_site/` folder), copied to /srv/www/paperspec.
|
||||
# The site mirrors the repository, so every schema is served at its $id and the JSON-LD context at the address
|
||||
# layers name; this file sets the media types those addresses need and lets any page fetch them.
|
||||
paperspec.scient.ing {
|
||||
|
||||
+68
-5
@@ -1,7 +1,8 @@
|
||||
# Deploying the website
|
||||
# Deploying the website and the Python package
|
||||
|
||||
This folder holds what it takes to serve the paperspec website at <https://paperspec.scient.ing>.
|
||||
The site is static: `tools/site.py` builds it into `_site/`, and any web server can serve that folder.
|
||||
The last sections say how the Python package `paperspec` is published to the Gitea package registry, and how other repositories install it from there.
|
||||
The site is static: `paperspec site` builds it into `_site/`, and any web server can serve that folder.
|
||||
|
||||
## What the site serves
|
||||
|
||||
@@ -21,8 +22,8 @@ The pages need no server-side code; search runs in the browser over a small inde
|
||||
## Build it
|
||||
|
||||
```sh
|
||||
pixi run python tools/site.py # writes _site/
|
||||
pixi run python tools/site.py --out /tmp/paperspec-site
|
||||
pixi run site # writes _site/
|
||||
pixi run site --out /tmp/paperspec-site
|
||||
```
|
||||
|
||||
`pixi run check` builds the site into a temporary folder and checks that every Markdown file has a page, that every internal link and fragment resolves, that no Markdown is left unrendered, and that each schema's `$id`, the context's address and the vocabulary's IRIs are files of the site.
|
||||
@@ -52,7 +53,69 @@ On a runner without that mount the workflow still builds and checks the site, an
|
||||
Only changed files are copied, so every other file keeps its `Last-Modified` and `ETag` and caches stay valid across releases.
|
||||
The environment in `.pixi/envs` is cached between runs under the hash of `pixi.lock`, so a fresh container installs it only when the lock file changes.
|
||||
|
||||
## Distributing packages
|
||||
## Publish the Python package
|
||||
|
||||
The Python package `paperspec` holds the reference implementation and the `paperspec` command.
|
||||
The workflow `.gitea/workflows/publish.yml` publishes it to the PyPI registry of the Gitea instance, `https://git.scient.ing/api/packages/infra/pypi`, when a version tag such as `v0.1.1` is pushed.
|
||||
It checks the repository, builds the sdist and the wheel with `pixi run -e dev dist`, confirms that the version setuptools-scm computed is the tag's, checks the metadata with twine, and uploads both files.
|
||||
The package version is the version of this code; the formats keep the version that `CHANGELOG.md` and `pixi.toml` state.
|
||||
|
||||
### Set it up once
|
||||
|
||||
1. Pick the account the workflow publishes as, a bot account or your own, and make it a member of a team in the `infra` organization with write access to packages.
|
||||
2. Signed in as that account, create an access token under Settings, Applications, with the `write:package` scope.
|
||||
3. In this repository's Settings, Actions, Secrets, add `PACKAGE_USER` with the account's user name and `PACKAGE_TOKEN` with the token.
|
||||
Organization secrets work as well, for every repository in `infra`.
|
||||
Gitea refuses secret names that start with `GITEA_` or `GITHUB_`.
|
||||
4. After the first upload, link the package to this repository in the package's settings, so it shows on the repository's Packages tab.
|
||||
|
||||
### Release a version
|
||||
|
||||
Tag a commit on `main` and push the tag:
|
||||
|
||||
```sh
|
||||
git tag -a v0.1.1 -m "Release the paperspec package, version 0.1.1"
|
||||
git push origin v0.1.1
|
||||
```
|
||||
|
||||
The job refuses to upload when the built version differs from the tag, for example when the tag is not on the commit it checked out.
|
||||
The registry never replaces a version, so to publish a fix, push a new tag.
|
||||
A rerun on the same tag skips the files already uploaded, which makes it safe to rerun a job that failed on its secrets: open the workflow under Actions and run it on the tag.
|
||||
|
||||
### Install from the registry
|
||||
|
||||
The registry does not mirror PyPI, so an installer needs two indexes: the registry for `paperspec`, and PyPI for `jsonschema`, `referencing` and `cryptography`.
|
||||
Tie `paperspec` to the registry wherever the installer allows it, so that a package of the same name on PyPI is never chosen instead.
|
||||
|
||||
With pixi, in `pixi.toml`:
|
||||
|
||||
```toml
|
||||
[pypi-dependencies]
|
||||
paperspec = { version = ">=0.1.1", extras = ["tools"], index = "https://git.scient.ing/api/packages/infra/pypi/simple" }
|
||||
```
|
||||
|
||||
With uv, in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[[tool.uv.index]]
|
||||
name = "scienting"
|
||||
url = "https://git.scient.ing/api/packages/infra/pypi/simple"
|
||||
explicit = true
|
||||
|
||||
[tool.uv.sources]
|
||||
paperspec = { index = "scienting" }
|
||||
```
|
||||
|
||||
With pip, which cannot tie a package to an index and takes the highest version from either:
|
||||
|
||||
```sh
|
||||
pip install --index-url https://git.scient.ing/api/packages/infra/pypi/simple/ --extra-index-url https://pypi.org/simple/ "paperspec[tools]"
|
||||
```
|
||||
|
||||
If the organization or the package is private, the installer also needs a token with the `read:package` scope, for example in a `~/.netrc` entry for `git.scient.ing`.
|
||||
A git pin to a commit, as `pixi.toml` and `pyproject.toml` can also express, keeps working and needs no registry.
|
||||
|
||||
## Distributing document packages
|
||||
|
||||
A zipped package is an ordinary file, and the site serves the fixtures as such.
|
||||
For a team's own packages, the Gitea instance speaks the OCI distribution protocol, so packages can be pushed to and pulled from its package registry as OCI artifacts with ORAS, and signed there with cosign; the media type of the artifact is the package's, `application/vnd.scienting.doc+zip`.
|
||||
|
||||
+2
-2
@@ -428,8 +428,8 @@ Keep using it as a reading and distribution format.
|
||||
*Status.*
|
||||
The specification is version 0.1.0.
|
||||
`registry/formats.json` lists 32 formats: 19 are `candidate` (complete, with vectors and fixtures, ready to implement) and 13 are `provisional` (complete; expected to change as implementations gain experience): project, bundle, dataset, digitization, view, snapshot, draft-ids, exercise, submission, grade, checklist, log-entry and checkpoint ([spec/00 §0.4](../spec/00-introduction.md#04-status)).
|
||||
The repository holds 24 chapters, 42 JSON Schemas, 25 vector files, five valid fixture packages, one of them also zipped, and 31 invalid ones, each naming the rule it breaks, and a reference checker, `tools/check.py`.
|
||||
Licenses: prose CC BY 4.0; schemas, SQL, the JSON-LD context, registries, vectors and fixtures CC0 1.0; tools Apache-2.0 ([README.md](../README.md#licenses)).
|
||||
The repository holds 24 chapters, 42 JSON Schemas, 25 vector files, five valid fixture packages, one of them also zipped, and 31 invalid ones, each naming the rule it breaks, and a reference checker, `paperspec check`.
|
||||
Licenses: prose CC BY 4.0; schemas, SQL, the JSON-LD context, registries, vectors and fixtures CC0 1.0; the code in `src/` Apache-2.0 ([README.md](../README.md#licenses)).
|
||||
|
||||
*No adoption claim.*
|
||||
The repository names no outside adopter and no independent implementation, and this page claims none.
|
||||
|
||||
+6
-6
@@ -9,7 +9,7 @@ The formats are version 0.1.0, and each is `candidate` or `provisional` in `regi
|
||||
|
||||
Run every command from the root of the repository, <https://git.scient.ing/infra/paperspec>.
|
||||
With [pixi](https://pixi.sh), `pixi run <task>` sets up the environment first.
|
||||
Every command also works without pixi, with Python 3.14 and two packages, `jsonschema` and `cryptography` installed and standard command-line tools (the `sed` command in the last section is the GNU version of `sed`).
|
||||
Every command also works without pixi once the checkout is installed with `pip install -e ".[tools]"` on Python 3.14, given standard command-line tools (the `sed` command in the last section is the GNU version of `sed`).
|
||||
|
||||
## Run the checks
|
||||
|
||||
@@ -19,10 +19,10 @@ This shows how to confirm that the repository's specification, fixtures and test
|
||||
pixi run check
|
||||
```
|
||||
|
||||
`pixi run check` lints the Markdown, then runs `python tools/check.py`, the repository's reference checker.
|
||||
`pixi run check` lints the Markdown, then runs `paperspec check`, the repository's reference checker.
|
||||
Each section prints how many checks it ran, and the last line is `All checks pass.`
|
||||
`pixi run quick`, or `python3 tools/check.py --quick`, skips the slowest part, which rebuilds everything and compares it byte for byte.
|
||||
Without pixi, the Markdown lint is a separate step (`markdownlint-cli2`); `tools/check.py` does not run it.
|
||||
`pixi run quick`, or `paperspec check --quick`, skips the slowest part, which rebuilds everything and compares it byte for byte.
|
||||
Without pixi, the Markdown lint is a separate step (`markdownlint-cli2`); `paperspec check` does not run it.
|
||||
|
||||
## Recompute a hash by hand
|
||||
|
||||
@@ -146,7 +146,7 @@ This shows that the same packages can live in a database and be queried with SQL
|
||||
pixi run load --sqlite lab.db fixtures/packages/hale2026harmonic-damb fixtures/packages/okafor2025solvent-acth fixtures/packages/writer2027fixed-ynr7.v3
|
||||
```
|
||||
|
||||
Without pixi, run `python3 tools/load.py` with the same arguments.
|
||||
Without pixi, run `paperspec load` with the same arguments.
|
||||
|
||||
```text
|
||||
hale2026harmonic-damb: 75 nodes
|
||||
@@ -154,7 +154,7 @@ okafor2025solvent-acth: 15 nodes
|
||||
writer2027fixed-ynr7.v3: 15 nodes
|
||||
```
|
||||
|
||||
The tables are those of [sql/doc.sql](../sql/doc.sql) and [sql/store.sql](../sql/store.sql), which run unchanged on Dolt and PostgreSQL as well ([chapter 13](../spec/13-database.md)); `tools/check.py` exercises them on SQLite only, and this repository has not yet run them on Dolt or PostgreSQL.
|
||||
The tables are those of [sql/doc.sql](../sql/doc.sql) and [sql/store.sql](../sql/store.sql), which run unchanged on Dolt and PostgreSQL as well ([chapter 13](../spec/13-database.md)); `paperspec check` exercises them on SQLite only, and this repository has not yet run them on Dolt or PostgreSQL.
|
||||
Here are four queries, in Python's `sqlite3` so that nothing else is needed.
|
||||
|
||||
```python
|
||||
|
||||
+6
-5
@@ -107,7 +107,7 @@ A `candidate` format is complete, with vectors and fixtures, and ready to implem
|
||||
A `provisional` one is complete and expected to change as implementations gain experience.
|
||||
The provisional formats are dataset, digitization, view, project, bundle, snapshot, draft-ids, exercise, submission, grade, checklist, log-entry and checkpoint.
|
||||
|
||||
The repository holds a reference checker (`tools/check.py`), 25 vector files, five valid packages, one of them also as a zip, and 31 deliberately broken packages ([spec/00 §0.4](../spec/00-introduction.md#04-status), [spec/22](../spec/22-conformance.md)).
|
||||
The repository holds a reference checker (`paperspec check`), 25 vector files, five valid packages, one of them also as a zip, and 31 deliberately broken packages ([spec/00 §0.4](../spec/00-introduction.md#04-status), [spec/22](../spec/22-conformance.md)).
|
||||
|
||||
## What does it not do?
|
||||
|
||||
@@ -124,8 +124,9 @@ Propose either with the vector or fixture that shows it.
|
||||
A new kind is a minor version ([spec/00 §0.3](../spec/00-introduction.md#03-format-identifiers-and-versions)).
|
||||
See [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
||||
## What are the files in `tools/` for?
|
||||
## What are the `paperspec` commands for?
|
||||
|
||||
`build.py` builds the fixtures and vectors. `check.py` recomputes and checks every one, and `load.py` loads packages into SQLite.
|
||||
The `oracle/` modules compute every expected value, so that none is typed by hand.
|
||||
They are not a library; implementations follow the specification ([tools/README.md](../tools/README.md)).
|
||||
`paperspec build` builds the fixtures and vectors.
|
||||
`paperspec check` recomputes and checks every one, and `paperspec load` loads packages into SQLite.
|
||||
The oracle's modules in `src/paperspec/` compute every expected value, so that none is typed by hand.
|
||||
The other repositories of the project install them as the Python package `paperspec`; independent implementations follow the specification ([tools/README.md](../tools/README.md)).
|
||||
+4
-3
@@ -54,7 +54,8 @@ It appears inside hashed data and test vectors, and it is separate from the repo
|
||||
| [spec/](../spec/README.md) | The specification, in 24 chapters (00 to 23). Normative. |
|
||||
| [schemas/](../schemas/README.md) | 42 JSON (plain-text data format) Schemas for the files and records. |
|
||||
| [registry/](../registry/README.md) | The list of formats, the rule IDs a checker reports, download-stamp rules, and default rights requirements. |
|
||||
| [tools/](../tools/README.md) | Reference programs: `check.py` checks everything, `build.py` rebuilds the fixtures and vectors, `load.py` loads packages into SQLite, and `oracle/` is a compact implementation of every rule. |
|
||||
| [tools/](../tools/README.md) | How to run the reference commands: `paperspec check` checks everything, `paperspec build` rebuilds the fixtures and vectors, and `paperspec load` loads packages into SQLite. |
|
||||
| [src/paperspec/](../src/paperspec/) | The oracle, a compact implementation of every rule, which the other repositories of the project install as the Python package `paperspec`. |
|
||||
| [vectors/](../vectors/README.md) | 25 files of test vectors: inputs and the exact outputs every implementation must compute. |
|
||||
| [fixtures/](../fixtures/README.md) | Five valid example packages, one of them also as a zip, 31 deliberately broken ones that each break one named rule, example records, and the test keys that signed them. All text is synthetic. |
|
||||
| [sql/](../sql/README.md) | Tables for SQLite, Dolt and PostgreSQL, and a SQLite bundle with full-text search. |
|
||||
@@ -72,8 +73,8 @@ The repository ships reference tools only for the checker and store roles.
|
||||
| Reader | Opens a package, resolves addresses and anchors, and shows text, annotations and each passage's fidelity level. |
|
||||
| Writer | Produces files in these formats: nodes, corrections, receipts, annotations, datasets. |
|
||||
| Converter | Turns a witness (a PDF, JATS XML, the article format publishers use, a LaTeX source archive, an HTML page, an EPUB or a Typst project) into a package and raises doubtful readings as suspects (flagged disagreements awaiting review). |
|
||||
| Checker | Recomputes hashes, validates files against their schemas and reports broken rules by rule ID. `tools/check.py` does this for the repository. |
|
||||
| Store | Keeps packages and records in a database a team can clone. `tools/load.py` loads packages into SQLite. |
|
||||
| Checker | Recomputes hashes, validates files against their schemas and reports broken rules by rule ID. `paperspec check` does this for the repository. |
|
||||
| Store | Keeps packages and records in a database a team can clone. `paperspec load` loads packages into SQLite. |
|
||||
|
||||
The [specification's reading paths](../spec/README.md) say which chapters each role needs.
|
||||
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ Only the three text values, `text_hash`, `match_hash` and `align_key`, normalize
|
||||
### The character data is pinned to one Unicode version
|
||||
|
||||
Normalization and General Categories change between Unicode versions, and both enter every hash.
|
||||
The chapters pin Unicode 16.0, the reference tools refuse to run on another version, and moving to a later one is a change to every text-hashing format ([spec/01 §1.4](../spec/01-conventions.md#14-text-normalization-for-hashing)).
|
||||
The chapters pin Unicode 16.0, the reference implementation refuses to import on another version, and moving to a later one is a change to every text-hashing format ([spec/01 §1.4](../spec/01-conventions.md#14-text-normalization-for-hashing)).
|
||||
|
||||
### Hashes that leave the team are keyed
|
||||
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ It has nothing to do with the repository's name.
|
||||
|
||||
Open [fixtures/packages/hale2026harmonic-damb/](../fixtures/packages/hale2026harmonic-damb/) beside this page, because every ID below is in it.
|
||||
A package is a folder of files, mostly JSON Lines (`.jsonl`: one JSON record per line).
|
||||
The hand-written source of the first reading is [fixtures/src/hale2026harmonic.json](../fixtures/src/hale2026harmonic.json); no converter ran on it, and [`tools/build.py`](../tools/build.py) expands it into the package.
|
||||
The hand-written source of the first reading is [fixtures/src/hale2026harmonic.json](../fixtures/src/hale2026harmonic.json); no converter ran on it, and [`paperspec build`](../src/paperspec/cli/build.py) expands it into the package.
|
||||
Each step says what a converter, a person or a checker does.
|
||||
|
||||
## 1. A PDF arrives
|
||||
@@ -166,7 +166,7 @@ It is a [Frictionless Data Package](https://datapackage.org/standard/data-packag
|
||||
Its CSV keeps every digit as printed (`1.526` and `1.5260` stay different), and every value has an anchor to the cell that prints it, so a claim that rests on a value can show the cell.
|
||||
The descriptor types the fields as numbers; how a generic Data Package tool treats `1.5260` was not verified.
|
||||
All twelve value anchors resolve as `current`.
|
||||
The reference checker, [`tools/check.py`](../tools/check.py), confirms each CSV value equals its cell's text.
|
||||
The reference checker, [`paperspec check`](../src/paperspec/cli/check.py), confirms each CSV value equals its cell's text.
|
||||
|
||||
## 8. Sharing without the text
|
||||
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ A conforming reader accepts each one and recomputes every hash it contains.
|
||||
## Records
|
||||
|
||||
`records/` holds a valid example of every other format that is a JSON file of its own.
|
||||
Three formats have their example elsewhere: the stage requirements (`scienting-stages/1`) and the stamp rules (`scienting-stamps/1`) are in [registry/](../registry/README.md), and a bundle (`scienting-bundle/1`) is a SQLite file that `tools/load.py --bundle` builds from the packages.
|
||||
Three formats have their example elsewhere: the stage requirements (`scienting-stages/1`) and the stamp rules (`scienting-stamps/1`) are in [registry/](../registry/README.md), and a bundle (`scienting-bundle/1`) is a SQLite file that `paperspec load --bundle` builds from the packages.
|
||||
Each record is a single file, or a folder where a format needs several files.
|
||||
|
||||
| Files | What they are for |
|
||||
@@ -109,4 +109,4 @@ Use them for nothing else.
|
||||
## Sources
|
||||
|
||||
`src/` holds the hand-written descriptions of the three main documents, as first read; see [src/README.md](src/README.md).
|
||||
Everything else here is generated by `python3 tools/build.py`, so change the sources or the builder, never the generated files.
|
||||
Everything else here is generated by `paperspec build`, so change the sources or the builder, never the generated files.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Fixture sources
|
||||
|
||||
Each source describes a document as a converter first read it, mistakes included, in a shorthand that `tools/build.py` expands.
|
||||
Each source describes a document as a converter first read it, mistakes included, in a shorthand that `paperspec build` expands.
|
||||
The build mints IDs, applies the listed corrections, lays out synthetic geometry, signs the listed receipts and writes the package.
|
||||
The shorthand is tooling, not a format.
|
||||
|
||||
|
||||
@@ -20,14 +20,34 @@ markdownlint-cli2 = ">=0.23.3"
|
||||
nodejs = ">=22"
|
||||
cryptography = ">=46"
|
||||
|
||||
# The reference implementation in src/paperspec, installed editable so that `import paperspec` and the
|
||||
# `paperspec` command work in every pixi environment; without pixi, `pip install -e ".[tools]"` does the same.
|
||||
[pypi-dependencies]
|
||||
paperspec = { path = ".", editable = true }
|
||||
|
||||
# What building and publishing the package needs: pixi run -e dev dist; see deploy/README.md.
|
||||
# twine 6.1 is the first that reads the license metadata (Metadata-Version 2.4) setuptools writes.
|
||||
[feature.dev.dependencies]
|
||||
setuptools = ">=80"
|
||||
setuptools-scm = ">=8"
|
||||
python-build = ">=1.2"
|
||||
twine = ">=6.1"
|
||||
|
||||
[feature.dev.tasks]
|
||||
dist = { cmd = "python -m build --sdist --wheel --no-isolation --outdir dist", description = "Build the paperspec sdist and wheel into dist/" }
|
||||
|
||||
[environments]
|
||||
default = { solve-group = "default" }
|
||||
dev = { features = ["dev"], solve-group = "default" }
|
||||
|
||||
[tasks]
|
||||
check = { cmd = "python tools/check.py", depends-on = [
|
||||
check = { cmd = "paperspec check", depends-on = [
|
||||
"lint",
|
||||
], description = "Check schemas, records, packages, vectors, a byte-for-byte rebuild, the SQL round trip, prose and links" }
|
||||
quick = { cmd = "python tools/check.py --quick", description = "Check everything except the byte-for-byte rebuild" }
|
||||
build = { cmd = "python tools/build.py", description = "Rebuild fixtures/ and vectors/ from fixtures/src/ and the oracle" }
|
||||
quick = { cmd = "paperspec check --quick", description = "Check everything except the byte-for-byte rebuild" }
|
||||
build = { cmd = "paperspec build", description = "Rebuild fixtures/ and vectors/ from fixtures/src/ and the oracle" }
|
||||
lint = { cmd = "markdownlint-cli2", description = "Lint every Markdown file with markdownlint" }
|
||||
load = { cmd = "python tools/load.py", description = "Load packages into SQLite: pixi run load --sqlite lab.db PACKAGE..." }
|
||||
validate = { cmd = "python tools/validate.py", description = "Validate packages, or run proposed corrections through the gate" }
|
||||
stress = { cmd = "python tools/stress.py", description = "Stress the oracle with random inputs: pixi run stress -- --seed N" }
|
||||
site = { cmd = "python tools/site.py", description = "Build the website into _site/" }
|
||||
load = { cmd = "paperspec load", description = "Load packages into SQLite: pixi run load --sqlite lab.db PACKAGE..." }
|
||||
validate = { cmd = "paperspec validate", description = "Validate packages, or run proposed corrections through the gate" }
|
||||
stress = { cmd = "paperspec stress", description = "Stress the oracle with random inputs: pixi run stress --seed N" }
|
||||
site = { cmd = "paperspec site", description = "Build the website into _site/" }
|
||||
@@ -0,0 +1,68 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=80", "setuptools-scm>=8"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "paperspec"
|
||||
dynamic = ["version"]
|
||||
description = "The scienting formats: the reference implementation of every rule, with the JSON Schemas, SQL and test vectors as package data"
|
||||
readme = "README.md"
|
||||
# The code (src/) is Apache-2.0 and the schemas, SQL and vectors it carries are CC0-1.0, as LICENSE.md says.
|
||||
license = "Apache-2.0 AND CC0-1.0"
|
||||
license-files = ["LICENSE.md"]
|
||||
authors = [{ name = "Scientific Computing Studio", email = "maintainer@scient.ing" }]
|
||||
# Python 3.14 is the only version with the Unicode 16.0 character data the specification pins (spec/01 §1.4);
|
||||
# 3.13 has 15.1 and 3.15 has 17.0, and the package refuses to import on either.
|
||||
requires-python = ">=3.14,<3.15"
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Science/Research",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Topic :: Scientific/Engineering",
|
||||
]
|
||||
# The base package is standard library only. `schema.py` needs jsonschema and
|
||||
# `crypto.py` needs cryptography; the repository's own tools need both.
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
schema = ["jsonschema>=4.26", "referencing>=0.35"]
|
||||
crypto = ["cryptography>=46"]
|
||||
tools = ["paperspec[crypto,schema]"]
|
||||
|
||||
# One command with subcommands: paperspec validate, load, check, build, stress and site; `paperspec --help`
|
||||
# says which work anywhere and which need a checkout.
|
||||
[project.scripts]
|
||||
paperspec = "paperspec.cli.main:main"
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://paperspec.scient.ing"
|
||||
Repository = "https://git.scient.ing/infra/paperspec"
|
||||
Documentation = "https://paperspec.scient.ing/docs/"
|
||||
|
||||
# The code is `src/paperspec`. The schemas, SQL and vectors stay at the repository root, where the
|
||||
# specification links to them and the website serves the schemas at their `$id` URLs, and travel with the
|
||||
# package as data subpackages, so a consumer reads them with importlib.resources. That mapping is why the
|
||||
# packages are listed rather than found.
|
||||
[tool.setuptools]
|
||||
packages = ["paperspec", "paperspec.cli", "paperspec.schemas", "paperspec.sql", "paperspec.vectors"]
|
||||
|
||||
[tool.setuptools.package-dir]
|
||||
"" = "src"
|
||||
"paperspec.schemas" = "schemas"
|
||||
"paperspec.sql" = "sql"
|
||||
"paperspec.vectors" = "vectors"
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"paperspec.schemas" = ["*.json"]
|
||||
"paperspec.sql" = ["*.sql"]
|
||||
"paperspec.vectors" = ["*.jsonl"]
|
||||
|
||||
# The package version comes from git tags (v0.1.1 builds as 0.1.1, later commits as 0.1.2.devN); CONTRIBUTING.md
|
||||
# says how it relates to the format version that CHANGELOG.md and pixi.toml carry. A source tree without git
|
||||
# history, such as a downloaded archive, builds as 0+unknown instead of failing.
|
||||
[tool.setuptools_scm]
|
||||
fallback_version = "0+unknown"
|
||||
version_file = "src/paperspec/_version.py"
|
||||
version_scheme = "guess-next-dev"
|
||||
local_scheme = "node-and-timestamp"
|
||||
+1
-1
@@ -43,7 +43,7 @@ A rule's meaning never changes, and a retired rule's ID is never reused, so tool
|
||||
A tool can match on the `rule` field, whose meaning is looked up in `rules.json`, and treat the message as text for people.
|
||||
[Chapter 22, section 22.4](../spec/22-conformance.md#224-rules) lists the ID prefixes.
|
||||
Each directory in `fixtures/invalid/` has an `expect.json` that names the rule it breaks.
|
||||
The reference checker (`tools/check.py`) fails if that rule is not in `rules.json`.
|
||||
The reference checker (`paperspec check`) fails if that rule is not in `rules.json`.
|
||||
|
||||
## Adding stamp rules
|
||||
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ The `Registry` maps that name to the file's contents, so nothing is fetched over
|
||||
Never load a schema alone.
|
||||
|
||||
Any draft 2020-12 validator works the same way: give it every schema under its `$id`, then validate against the one you need.
|
||||
`python3 tools/check.py` validates every fixture with `jsonschema` this way, and checks that every schema keeps to the keyword subset.
|
||||
`paperspec check` validates every fixture with `jsonschema` this way, and checks that every schema keeps to the keyword subset.
|
||||
|
||||
## What a schema does not check
|
||||
|
||||
|
||||
@@ -82,8 +82,9 @@ The other folders help people use the specification.
|
||||
| `registry/` | Formats, rule IDs, stamp rules and the default stage requirements. | Yes |
|
||||
| `vectors/` | Test vectors, one case per line. | Yes |
|
||||
| `fixtures/` | Valid packages and records, invalid packages, and the keys that signed them. | Yes |
|
||||
| `tools/` | The programs that compute and check the vectors and fixtures, and load packages into SQLite. | No |
|
||||
| `src/` | The Python package `paperspec`: the reference implementation, and the `paperspec` command that computes and checks the vectors and fixtures and loads packages into SQLite. | No |
|
||||
| `tools/` | How to run those commands. | No |
|
||||
| `docs/` | Guides for people getting to know the formats. | No |
|
||||
|
||||
The programs in `tools/` compute every expected value, so that none is typed by hand.
|
||||
They are not a library, and implementations conform to the chapters and the vectors.
|
||||
The reference implementation in `src/paperspec/` computes every expected value, so that none is typed by hand.
|
||||
Other repositories of the project import it, but implementations conform to the chapters and the vectors, not to it.
|
||||
@@ -46,7 +46,7 @@ Stored text is kept exactly as witnessed; normalization applies only when a valu
|
||||
The vectors in `normalize.jsonl` cover the cases, including the angstrom sign, which NFC turns into Å.
|
||||
|
||||
Normalization forms, General Categories and the `White_Space` property are those of Unicode 16.0.
|
||||
A later Unicode version can change a decomposition or a category, and with it a hash, so an implementation states the Unicode version its character data comes from, the reference tools refuse to run on another, and moving the formats to a later version is a change to every format that hashes text ([0.3](00-introduction.md#03-format-identifiers-and-versions)).
|
||||
A later Unicode version can change a decomposition or a category, and with it a hash, so an implementation states the Unicode version its character data comes from, the reference implementation refuses to import on another, and moving the formats to a later version is a change to every format that hashes text ([0.3](00-introduction.md#03-format-identifiers-and-versions)).
|
||||
|
||||
## 1.5 Character offsets
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ A correction is refused, naming the rule ([22](22-conformance.md)), when:
|
||||
11. the result breaks a rule of [3](03-nodes.md).
|
||||
|
||||
The gate needs only the document's current state, so a tool can run it on a proposal before applying anything, and get back the record the correction would append or the rule it breaks.
|
||||
`tools/validate.py --correction` does this against a package.
|
||||
`paperspec validate --correction` does this against a package.
|
||||
|
||||
Digits are characters of General Category Nd in the plain projection.
|
||||
A package's corrections are also checked as a whole: indexes run from 1 without gaps, each `id` derives from its record, `rev` equals their number, every retired ID is in `ids.jsonl`, and the last correction of each leaf ends at its current `node_hash`.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
| --- | --- | --- |
|
||||
| `vectors/*.jsonl` | Inputs and expected outputs, one case per line. | Computes every expected output from its input. |
|
||||
| `fixtures/packages/` | Valid packages: a converted chemistry paper with corrections, receipts, a multi-level-header table and a dataset; its checks-only export; an open-access paper with two witnesses; a Typst draft; a tiny document, as a folder and as the zip of [12.2](12-package.md#122-paths). | Accepts each, and recomputes every value it contains. |
|
||||
| `fixtures/records/` | A valid record of every other format that is a JSON file of its own; the stage requirements and stamp rules are in `registry/`, and a bundle is built by `tools/load.py --bundle`. | Accepts each. |
|
||||
| `fixtures/records/` | A valid record of every other format that is a JSON file of its own; the stage requirements and stamp rules are in `registry/`, and a bundle is built by `paperspec load --bundle`. | Accepts each. |
|
||||
| `fixtures/invalid/` | Invalid packages, each with `expect.json` naming the rule it breaks. | Refuses each, and SHOULD name that rule. |
|
||||
| `fixtures/keys/` | The test keys that signed the fixtures and the team secret of the checks-only export. | Never uses them for anything else. |
|
||||
|
||||
|
||||
+3
-2
@@ -31,7 +31,7 @@ dolt sql < sql/doc.sql && dolt sql < sql/store.sql
|
||||
The PostgreSQL database `lab` and the Dolt repository must already exist.
|
||||
The `sqlite3` line runs as shown; the `psql` and `dolt` lines follow the portability rules of [chapter 13](../spec/13-database.md#131-portability) but have not been run on those engines in this repository.
|
||||
|
||||
To load packages into SQLite without pixi, run `python3 tools/load.py --sqlite lab.db PACKAGE…` with Python 3.14 and two packages, `jsonschema` and `cryptography` installed.
|
||||
To load packages into SQLite without pixi, install the package with its `tools` extra on Python 3.14, then run `paperspec load --sqlite lab.db PACKAGE…`.
|
||||
The file [docs/examples.md](../docs/examples.md#load-packages-into-sqlite) has queries to try.
|
||||
The loader copies files that have no table (assets, dataset files, derived files) by SHA-256 into `lab.db.assets/`.
|
||||
|
||||
@@ -57,7 +57,8 @@ On `fixtures/packages/hale2026harmonic-damb` it returns one row, `hca6dc5`: the
|
||||
- Names: no table, column or index name is reserved in SQLite, PostgreSQL or MySQL (Dolt is MySQL-compatible).
|
||||
The formats' `key`, `index`, `at`, `by`, `before` and `after` become `doc_key`, `idx`, `made_at`, `signer`, `before_hash` and `after_hash`.
|
||||
- Round trip: each row keeps every property of its record, so unloading restores the same bytes and the same manifest hash.
|
||||
Unloading copies the files with no table from the asset store, which must travel with the database. `tools/check.py` tests this for every fixture package.
|
||||
Unloading copies the files with no table from the asset store, which must travel with the database.
|
||||
`paperspec check` tests this for every fixture package.
|
||||
- Derived tables: `citations`, `receipt_nodes` and `annotation_targets` are built on load for queries and are not part of the package.
|
||||
|
||||
## Status
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""The reference implementation of the scienting formats: the computations behind every expected value
|
||||
in the paperspec repository; see https://paperspec.scient.ing/tools/.
|
||||
|
||||
Every module imports with the standard library alone. Schema validation (`schema`, and with it
|
||||
`package.validate` and `corrections.gate`) needs `paperspec[schema]`, and HKDF and Ed25519 in `crypto`, which
|
||||
the signature and receipt checks use, need `paperspec[crypto]`; both import their packages on first use, and
|
||||
`paperspec[tools]` installs both.
|
||||
The schemas, SQL and vectors travel with the package: `paperspec.resources.path("sql")`.
|
||||
The commands are in `paperspec.cli`.
|
||||
|
||||
The specification pins the Unicode character data that normalization and the character classes come from
|
||||
(spec/01 §1.4), so the package refuses to import on a Python with any other version: a different version
|
||||
would compute different values for text with the characters it adds or changes.
|
||||
"""
|
||||
|
||||
import unicodedata
|
||||
|
||||
UNICODE_VERSION = "16.0.0"
|
||||
|
||||
if unicodedata.unidata_version != UNICODE_VERSION:
|
||||
raise ImportError("paperspec needs the Unicode %s character data the specification pins, which Python 3.14 "
|
||||
"ships; this Python has Unicode %s." % (UNICODE_VERSION, unicodedata.unidata_version))
|
||||
|
||||
try:
|
||||
from ._version import __version__
|
||||
except ImportError: # a checkout that was never built or installed, as `python -m` from src/ runs it
|
||||
__version__ = "0+unknown"
|
||||
@@ -0,0 +1,7 @@
|
||||
"""`python -m paperspec COMMAND`: the same as the `paperspec` command."""
|
||||
|
||||
import sys
|
||||
|
||||
from paperspec.cli.main import main
|
||||
|
||||
sys.exit(main())
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,55 @@
|
||||
"""The `paperspec` command's subcommands, one module each; the parser and the help are in `main.py`.
|
||||
|
||||
`paperspec validate` and `paperspec load` work anywhere the package is installed with `paperspec[tools]`.
|
||||
`paperspec check`, `build`, `stress` and `site` work on a checkout of the paperspec repository: the one an
|
||||
editable install points at, or else the one around the current folder.
|
||||
Each module has `run(args)`, which takes the parsed arguments and returns the exit status.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
REPOSITORY = "https://git.scient.ing/infra/paperspec.git"
|
||||
INSTALL = "https://paperspec.scient.ing/deploy/#install-from-the-registry"
|
||||
|
||||
|
||||
class UsageError(Exception):
|
||||
"""Arguments that parse but do not fit together; `paperspec` prints the command's usage and exits 2."""
|
||||
|
||||
|
||||
def require(*modules):
|
||||
"""Exit with what to install when a package a command needs is missing."""
|
||||
missing = [m for m in modules if importlib.util.find_spec(m) is None]
|
||||
if missing:
|
||||
sys.exit("This command needs %s %s, which the package's tools extra installs.\n"
|
||||
"Install paperspec[tools] from the package registry: %s\n"
|
||||
"In a checkout of the repository, pip install -e \".[tools]\" or pixi run COMMAND installs them."
|
||||
% ("the package" if len(missing) == 1 else "the packages", " and ".join(missing), INSTALL))
|
||||
|
||||
|
||||
def _is_checkout(d):
|
||||
return all(os.path.exists(os.path.join(d, m)) for m in ("pyproject.toml", "spec", "registry", "fixtures/src"))
|
||||
|
||||
|
||||
def checkout():
|
||||
"""The root of the paperspec checkout a repository command works on, or exit with what to do.
|
||||
|
||||
An editable install (and `python -m paperspec` from `src/`) runs the checkout this file is in, so the code
|
||||
and the files it checks always match; an installed wheel uses the nearest checkout above the current folder.
|
||||
"""
|
||||
own = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
if _is_checkout(own):
|
||||
return own
|
||||
d = os.getcwd()
|
||||
while True:
|
||||
if _is_checkout(d):
|
||||
return d
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
sys.exit("This command works on a checkout of the paperspec repository, and %s is not inside one.\n"
|
||||
"Clone it and run the command there:\n"
|
||||
" git clone %s\n"
|
||||
" cd paperspec && pixi run check\n"
|
||||
"paperspec validate and paperspec load work anywhere." % (os.getcwd(), REPOSITORY))
|
||||
d = parent
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build every fixture and test vector from fixtures/src and the cases below.
|
||||
|
||||
Non-normative tooling.
|
||||
The build is deterministic: run it twice and the output is byte-identical; `tools/check.py` verifies that.
|
||||
Usage: python3 tools/build.py [--out DIR] (default: the repository itself)
|
||||
The build is deterministic: run it twice and the output is byte-identical; `paperspec check` verifies that.
|
||||
Usage: paperspec build [--out DIR] (default: the repository itself)
|
||||
"""
|
||||
|
||||
import base64
|
||||
@@ -18,20 +17,22 @@ import zipfile
|
||||
import zlib
|
||||
from fractions import Fraction
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
from paperspec.cli import checkout, require
|
||||
|
||||
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 keys as K, log as LG, mldsa, 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, 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
|
||||
require("cryptography", "jsonschema")
|
||||
ROOT = checkout()
|
||||
|
||||
from paperspec import align as AL # noqa: E402
|
||||
from paperspec import anchors as AN # noqa: E402
|
||||
from paperspec import corrections as CO # noqa: E402
|
||||
from paperspec import crypto, fingerprint as FP, grade as GR, hashing, jcs, receipts as RC, rights as RI # noqa: E402
|
||||
from paperspec import keys as K, log as LG, mldsa, targets as TG, vrf # noqa: E402
|
||||
from paperspec.model import LEAVES, MATH_KINDS, RUN_KINDS, VERBATIM_KINDS, Doc # noqa: E402
|
||||
from paperspec.notation import fold_math # noqa: E402
|
||||
from paperspec.package import ZIP_EXTENSION, history_items, jsonl, line, witness_id, write, write_zip # noqa: E402
|
||||
from paperspec.schema import Store # noqa: E402
|
||||
from paperspec.text import letter_first, mint_node_id, norm, record_id, sha256_hex # noqa: E402
|
||||
from paperspec.tree import Refusal, _flatten, mint_ids, validate_structure # noqa: E402
|
||||
|
||||
VERSION = "0.1.0"
|
||||
STORE = "smithlab"
|
||||
@@ -544,6 +545,8 @@ def package_files(src, key, names, side, final, records, receipts=None, roster_r
|
||||
if roster_rec:
|
||||
files["roster.json"] = (jcs.dumps(roster_rec) + "\n").encode()
|
||||
files["witnesses.json"] = (jcs.dumps({"witnesses": witnesses}) + "\n").encode()
|
||||
# The tool name is the label the fixtures were built and signed under, not this file's path; changing it
|
||||
# would change every fixture's provenance, manifest hash and receipts.
|
||||
files["provenance.json"] = (jcs.dumps({
|
||||
"tools": [{"name": "paperspec tools/build.py", "version": VERSION}],
|
||||
"stages": [{"stage": "hand", "processor": "local", "tool": "paperspec tools/build.py"}],
|
||||
@@ -903,7 +906,7 @@ def vectors(out, hale, tiny):
|
||||
("astral", [T("Let "), M("\U0001d465"), T(" vary with "), M("\U0001d466"), T(".")], [4, 7])]:
|
||||
doc, s = sentence_doc(runs)
|
||||
p = hashing.plain(doc, s)
|
||||
from tools.oracle.text import cp_to_utf16
|
||||
from paperspec.text import cp_to_utf16
|
||||
off.append({"id": cid, "runs": runs, "plain": p, "span": span, "exact": p[span[0]:span[1]],
|
||||
"utf16_span": [cp_to_utf16(p, span[0]), cp_to_utf16(p, span[1])], "code_points": len(p),
|
||||
"utf16_units": cp_to_utf16(p, len(p))})
|
||||
@@ -1849,21 +1852,16 @@ def main(out):
|
||||
invalid(out, tiny)
|
||||
|
||||
|
||||
def cli():
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Build every fixture and test vector from fixtures/src and the oracle. The build is "
|
||||
"deterministic, and tools/check.py rebuilds into a temporary folder to prove it.")
|
||||
ap.add_argument("--out", metavar="DIR", default=ROOT,
|
||||
help="build into DIR instead of rewriting fixtures/ and vectors/ in this repository")
|
||||
out = os.path.abspath(ap.parse_args().out)
|
||||
def run(args):
|
||||
out = os.path.abspath(args.out or ROOT)
|
||||
if out != ROOT:
|
||||
for sub in ("registry", "fixtures/src"):
|
||||
dst = os.path.join(out, sub)
|
||||
if not os.path.exists(dst):
|
||||
shutil.copytree(os.path.join(ROOT, sub), dst)
|
||||
main(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
if out == ROOT:
|
||||
print("Rebuilt fixtures/ and vectors/ in %s.\n"
|
||||
"Review the changes with git status and git diff, then commit them with the change that caused them."
|
||||
% ROOT)
|
||||
return 0
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the whole repository. Exit status 0 means every check passed.
|
||||
|
||||
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)
|
||||
Non-normative tooling; it needs the packages `jsonschema` and `cryptography`, which pixi and paperspec[tools] install.
|
||||
Usage: paperspec check [--quick] (--quick skips the determinism rebuild)
|
||||
"""
|
||||
|
||||
import base64
|
||||
@@ -18,23 +17,25 @@ import tempfile
|
||||
import zipfile
|
||||
from fractions import Fraction
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
from paperspec.cli import checkout, require
|
||||
|
||||
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, db, fingerprint as FP, grade as GR, hashing, jcs, receipts as RC, rights as RI # noqa: E402
|
||||
from tools.oracle import package as PK # noqa: E402
|
||||
from tools.oracle import keys as K, log as LG, mldsa, targets as TG, vrf # noqa: E402
|
||||
require("cryptography", "jsonschema")
|
||||
ROOT = checkout()
|
||||
|
||||
from paperspec import align as AL # noqa: E402
|
||||
from paperspec import anchors as AN # noqa: E402
|
||||
from paperspec import corrections as CO # noqa: E402
|
||||
from paperspec import crypto, db, fingerprint as FP, grade as GR, hashing, jcs, receipts as RC, rights as RI # noqa: E402
|
||||
from paperspec import package as PK # noqa: E402
|
||||
from paperspec import keys as K, log as LG, mldsa, targets as TG, vrf # noqa: E402
|
||||
import unicodedata # 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 paperspec.model import Doc # noqa: E402
|
||||
from paperspec.notation import fold_math # noqa: E402
|
||||
from paperspec.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
|
||||
from paperspec.text import cp_to_utf16, letter_first, mint_node_id, norm, record_id, sha256_hex # noqa: E402
|
||||
from paperspec.tree import Refusal # noqa: E402
|
||||
|
||||
FAILS = []
|
||||
# Folders that are not part of the repository: version control, environments and caches.
|
||||
@@ -68,7 +69,7 @@ STORE = Store(p("schemas"))
|
||||
def fixture_key(stem):
|
||||
"""The key the rule of spec/02 §2.4 mints for a fixture source, which names its package folder."""
|
||||
if stem == "tiny2026example":
|
||||
from tools.build import TINY
|
||||
from paperspec.cli.build import TINY
|
||||
d = TINY["document"]
|
||||
else:
|
||||
d = js(p("fixtures", "src", stem + ".json"))["document"]
|
||||
@@ -111,16 +112,19 @@ 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"))):
|
||||
for f in sorted(os.listdir(p("src", "paperspec"))):
|
||||
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()):
|
||||
for rule in re.findall(r'(?:Refusal|rep\.add)\(\s*"([a-z][a-z0-9.-]+)"', open(p("src", "paperspec", f)).read()):
|
||||
if not rule.endswith("."):
|
||||
ok("registry", rule in rules, "tools/oracle/%s raises %s, which is not in rules.json" % (f, rule))
|
||||
ok("registry", rule in rules, "src/paperspec/%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)
|
||||
# The character data the hashes depend on is pinned to one Unicode version (spec/01 §1.4).
|
||||
ok("registry", unicodedata.unidata_version == "16.0.0",
|
||||
"the tools run on Unicode %s, but the specification pins 16.0" % unicodedata.unidata_version)
|
||||
# The character data the hashes depend on is pinned to one Unicode version (spec/01 §1.4); the package
|
||||
# refuses to import on any other, so this guards the pin itself against changing without the chapters.
|
||||
import paperspec
|
||||
ok("registry", paperspec.UNICODE_VERSION == "16.0.0" == unicodedata.unidata_version,
|
||||
"the package pins Unicode %s and runs on %s, but the specification pins 16.0"
|
||||
% (paperspec.UNICODE_VERSION, unicodedata.unidata_version))
|
||||
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)
|
||||
|
||||
@@ -431,7 +435,7 @@ def reference_values(doc, hasher=hashing.PLAIN):
|
||||
|
||||
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
|
||||
from paperspec.cli.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()
|
||||
@@ -509,7 +513,7 @@ ROSTER_BY_ID_CHECK = {k["id"]: k for k in js(p("fixtures", "records", "roster.js
|
||||
|
||||
|
||||
def check_vectors():
|
||||
from tools.build import parse_doubles, projection_case, sentence_doc
|
||||
from paperspec.cli.build import parse_doubles, projection_case, sentence_doc
|
||||
for c in V("canonical-json.jsonl"):
|
||||
ok("vectors", jcs.dumps(parse_doubles(c["input_json"])) == c["output"], "canonical-json " + c["id"])
|
||||
for c in V("normalize.jsonl"):
|
||||
@@ -693,7 +697,7 @@ def check_vectors():
|
||||
ok("vectors", any(c["id"] == "rfc9381-example-16" and c["proof"].startswith("8657106690b5526245a92b003bb079cc")
|
||||
and c["beta"].startswith("90cf1df3b703cce59e2a35b925d41116") for c in V("vrf.jsonl")),
|
||||
"the RFC 9381 example is not reproduced")
|
||||
from tools.build import secret
|
||||
from paperspec.cli.build import secret
|
||||
for c in V("checkpoint.jsonl"):
|
||||
ok("vectors", LG.root(c["entries"]) == c["root"], "checkpoint %s: root" % c["id"])
|
||||
signers = [(n, secret(n)) for n in c["signers"]]
|
||||
@@ -708,7 +712,7 @@ def check_vectors():
|
||||
|
||||
def check_determinism():
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
subprocess.run([sys.executable, p("tools", "build.py"), "--out", tmp], check=True)
|
||||
subprocess.run([sys.executable, "-m", "paperspec", "build", "--out", tmp], check=True)
|
||||
for sub in ("fixtures", "vectors"):
|
||||
# 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"])
|
||||
@@ -981,7 +985,7 @@ def check_counts():
|
||||
}
|
||||
top = re.search(r"^## (\S+)", open(p("CHANGELOG.md"), encoding="utf-8").read(), re.M).group(1)
|
||||
pixi = re.search(r'^version = "([^"]+)"', open(p("pixi.toml"), encoding="utf-8").read(), re.M).group(1)
|
||||
from tools.build import VERSION
|
||||
from paperspec.cli.build import VERSION
|
||||
ok("counts", top == pixi == formats["version"] == rules["version"] == VERSION,
|
||||
"versions differ: CHANGELOG %s, pixi.toml %s, formats %s, rules %s, build.py %s"
|
||||
% (top, pixi, formats["version"], rules["version"], VERSION))
|
||||
@@ -1037,7 +1041,7 @@ def check_log():
|
||||
ok("log", LG.accept_checkpoint(note, roster, settings) == (True, []), "the project does not accept its checkpoint")
|
||||
ok("log", not LG.accept_checkpoint(note, roster, dict(settings, threshold=2))[0], "two witnesses were not required")
|
||||
ok("log", not LG.accept_checkpoint(note, roster, dict(settings, origin="other"))[0], "another origin was accepted")
|
||||
alone = LG.sign_checkpoint(settings["origin"], entries, [("wit", __import__("tools.build", fromlist=["secret"]).secret("wit"))])
|
||||
alone = LG.sign_checkpoint(settings["origin"], entries, [("wit", __import__("paperspec.cli.build", fromlist=["secret"]).secret("wit"))])
|
||||
ok("log", not LG.accept_checkpoint(alone, roster, settings)[0], "a checkpoint without the team's key was accepted")
|
||||
# The ML-DSA-65 receipt verifies under dana's key, and fails under a tampered payload.
|
||||
rec = js(os.path.join(R, "receipt-ml-dsa.json"))
|
||||
@@ -1086,7 +1090,7 @@ def check_site():
|
||||
"""The website builds, every Markdown file has a page, every internal link and fragment resolves, no Markdown
|
||||
is left unrendered, and the namespace addresses the formats name are files the site serves."""
|
||||
from html.parser import HTMLParser
|
||||
from tools import site
|
||||
from paperspec.cli import site
|
||||
|
||||
class Page(HTMLParser):
|
||||
def __init__(self):
|
||||
@@ -1164,14 +1168,7 @@ def check_site():
|
||||
ok("site", t[4:] in pages["vocab/index.html"].ids, "the vocabulary page has no #%s" % t[4:])
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(
|
||||
description="Check the whole repository: schemas, records, packages, zipped packages, broken packages, "
|
||||
"vectors, a byte-for-byte rebuild, the SQL round trip and the bundle, counts, 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()
|
||||
def run(args):
|
||||
check_schemas()
|
||||
check_registry()
|
||||
check_records()
|
||||
@@ -1198,9 +1195,11 @@ def main():
|
||||
print("\n%d failures:" % len(FAILS))
|
||||
for f in FAILS[:60]:
|
||||
print(" " + f)
|
||||
sys.exit(1)
|
||||
if len(FAILS) > 60:
|
||||
print(" and %d more" % (len(FAILS) - 60))
|
||||
if any(f.startswith("determinism") for f in FAILS):
|
||||
print("\nThe rebuild differs from fixtures/ or vectors/: if you changed a rule, a fixture source or a"
|
||||
"\nbuild case, run paperspec build, review the diff and commit the regenerated files.")
|
||||
return 1
|
||||
print("\nAll checks pass.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
return 0
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Load packages into a SQLite database with the tables of sql/doc.sql and sql/store.sql.
|
||||
|
||||
Non-normative tooling.
|
||||
Usage: python3 tools/load.py --sqlite lab.db PACKAGE [PACKAGE ...]
|
||||
python3 tools/load.py --sqlite lab.db --bundle --built-from REV PACKAGE [PACKAGE ...]
|
||||
Usage: paperspec load --sqlite lab.db PACKAGE [PACKAGE ...]
|
||||
paperspec load --sqlite lab.db --bundle --built-from REV PACKAGE [PACKAGE ...]
|
||||
A package is a folder, or a zipped package (`.sct.zip`), unpacked under the rules of spec/12 §12.2 first.
|
||||
With --bundle the new file is a scienting-bundle/1: it also gets the tables of sql/bundle-sqlite.sql,
|
||||
the full-text search index and bundle_meta (spec/13 §13.5).
|
||||
@@ -12,32 +11,27 @@ Files without tables of their own (assets, datasets, derived files) are copied b
|
||||
asset store folder beside the database, named <database>.assets/.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
from paperspec.cli import UsageError, require
|
||||
|
||||
from tools.oracle import db, package # noqa: E402
|
||||
from tools.oracle.schema import Store # noqa: E402
|
||||
require("cryptography", "jsonschema")
|
||||
|
||||
from paperspec import db, package, resources # noqa: E402
|
||||
from paperspec.schema import Store # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--sqlite", required=True, help="the database file; created with the tables if new")
|
||||
ap.add_argument("--bundle", action="store_true", help="build a new scienting-bundle/1 file with search")
|
||||
ap.add_argument("--built-from", help="with --bundle: the store revision the packages come from")
|
||||
ap.add_argument("--store", help="with --bundle: the store's name (default: the packages' common store)")
|
||||
ap.add_argument("--max-size", metavar="BYTES", type=int, default=package.ZIP_LIMIT,
|
||||
help="refuse a zipped package that expands past this many bytes (default %d)" % package.ZIP_LIMIT)
|
||||
ap.add_argument("packages", nargs="+", help="package folders or zipped packages")
|
||||
a = ap.parse_args()
|
||||
if a.bundle and (os.path.exists(a.sqlite) or not a.built_from):
|
||||
ap.error("--bundle needs --built-from and a database file that does not exist yet")
|
||||
store, bad = Store(os.path.join(ROOT, "schemas")), False
|
||||
def run(a):
|
||||
if a.bundle and not a.built_from:
|
||||
raise UsageError("--bundle needs --built-from REV, the store revision the packages come from")
|
||||
if a.bundle and os.path.exists(a.sqlite):
|
||||
raise UsageError("--bundle builds a new file, and %s exists; give a new name or remove it" % a.sqlite)
|
||||
if not a.bundle and (a.built_from or a.store):
|
||||
raise UsageError("--built-from and --store go with --bundle")
|
||||
store, bad = Store(resources.path("schemas")), False
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
folders = []
|
||||
for i, p in enumerate(a.packages):
|
||||
@@ -54,13 +48,13 @@ def main():
|
||||
print("%s: %s: %s: %s" % (p, e["where"], e["rule"], e["message"]), file=sys.stderr)
|
||||
bad = True
|
||||
if bad:
|
||||
sys.exit("Nothing was loaded.")
|
||||
sys.exit("Nothing was loaded. Fix the packages above; paperspec validate checks one without loading it.")
|
||||
new = not os.path.exists(a.sqlite)
|
||||
conn = sqlite3.connect(a.sqlite)
|
||||
if a.bundle:
|
||||
db.create_bundle(conn, os.path.join(ROOT, "sql"))
|
||||
db.create_bundle(conn, resources.path("sql"))
|
||||
elif new:
|
||||
db.create(conn, os.path.join(ROOT, "sql"))
|
||||
db.create(conn, resources.path("sql"))
|
||||
for p in folders:
|
||||
key = db.load(conn, p, a.sqlite + ".assets")
|
||||
n = conn.execute("SELECT COUNT(*) FROM nodes WHERE doc_key = ?", (key,)).fetchone()[0]
|
||||
@@ -75,7 +69,4 @@ def main():
|
||||
sys.exit("The packages name no single store; give one with --store.")
|
||||
db.write_meta(conn, store_name, a.built_from)
|
||||
conn.commit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
return 0
|
||||
@@ -0,0 +1,248 @@
|
||||
"""The `paperspec` command: one entry point, with a subcommand for each tool.
|
||||
|
||||
The parser lives here, apart from the commands, so that `paperspec --help` and `paperspec COMMAND --help` work
|
||||
without a checkout or the tools extra; a command's module is imported only when the command runs.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
from paperspec import UNICODE_VERSION, __version__
|
||||
from paperspec.cli import UsageError
|
||||
from paperspec.package import ZIP_LIMIT
|
||||
|
||||
SITE = "https://paperspec.scient.ing"
|
||||
|
||||
DESCRIPTION = """\
|
||||
The reference implementation of the scienting formats: validate packages, load them into SQLite,
|
||||
and check, rebuild and stress the paperspec repository itself.
|
||||
"""
|
||||
|
||||
EPILOG = """\
|
||||
examples:
|
||||
paperspec validate paper.sct.zip check a package against every rule
|
||||
paperspec validate --correction fix.json paper/ try a proposed correction without writing it
|
||||
paperspec load --sqlite lab.db paper/ other/ load packages into a SQLite file to query
|
||||
paperspec check --quick in a checkout: check everything but the rebuild
|
||||
|
||||
where each command works:
|
||||
validate and load work wherever paperspec is installed with its tools extra, paperspec[tools];
|
||||
installing from the package registry: %s/deploy/#install-from-the-registry
|
||||
check, build, stress and site work on a checkout of the paperspec repository,
|
||||
where pixi also runs them as tasks: pixi run check
|
||||
|
||||
Run "paperspec COMMAND --help" for a command's options, advice and examples.
|
||||
""" % SITE
|
||||
|
||||
PACKAGE_ADVICE = """\
|
||||
A package is a folder, or a zipped package (.sct.zip) that is unpacked under the rules of
|
||||
spec/12 §12.2 before anything else happens.
|
||||
"""
|
||||
|
||||
|
||||
def _validate(sub):
|
||||
p = sub.add_parser(
|
||||
"validate", help="check packages against every rule, or try proposed corrections",
|
||||
description="Validate packages, or run proposed corrections through the gate against a package.\n\n"
|
||||
+ PACKAGE_ADVICE + """
|
||||
Each package is checked as spec/22 §22.2 describes, and each failure is printed as
|
||||
`package: where: rule: message`. The rule ID is listed in """ + SITE + """/registry/rules.json
|
||||
with the chapter that states the rule, which is where to read why it failed.
|
||||
|
||||
With --correction, PROPOSALS is a JSON file holding one proposed correction, or a JSON Lines
|
||||
file of several, applied in order. Each runs through the gate (spec/11 §11.4) against the
|
||||
package's current state, and the record it would append to corrections.jsonl is printed,
|
||||
until one is refused. Nothing is written, so it is safe to try a correction before making it.
|
||||
|
||||
Exit status: 0 when everything is valid, 1 when something is refused, 2 for a usage error.""",
|
||||
epilog="""\
|
||||
examples:
|
||||
paperspec validate fixtures/packages/hale2026harmonic-damb
|
||||
paperspec validate paper/ other.sct.zip several at once; each failure names its package
|
||||
paperspec validate --correction fix.json paper/ one proposal; prints the record it would append
|
||||
paperspec validate --correction fixes.jsonl paper.sct.zip
|
||||
several, in order, stopping at the first refusal""")
|
||||
p.add_argument("--correction", metavar="PROPOSALS",
|
||||
help="a JSON or JSON Lines file of proposed corrections to run through the gate")
|
||||
p.add_argument("--max-size", metavar="BYTES", type=int, default=ZIP_LIMIT,
|
||||
help="refuse a zipped package that expands past this many bytes (default %d, 1 GiB)" % ZIP_LIMIT)
|
||||
p.add_argument("packages", nargs="+", metavar="PACKAGE", help="a package folder or a zipped package")
|
||||
return p
|
||||
|
||||
|
||||
def _load(sub):
|
||||
p = sub.add_parser(
|
||||
"load", help="validate packages and load them into a SQLite database",
|
||||
description="Load packages into a SQLite database with the tables of sql/doc.sql and sql/store.sql.\n\n"
|
||||
+ PACKAGE_ADVICE + """
|
||||
Every package is validated first, and nothing is loaded if any fails; each failure is printed as
|
||||
`package: where: rule: message`, as `paperspec validate` prints it. A new file gets the tables;
|
||||
an existing one takes more packages. Files with no table of their own (assets, datasets, derived
|
||||
files) are copied by SHA-256 into an asset store folder beside the database, DATABASE.assets/,
|
||||
which must travel with it. The full and checks-only forms of one document share its key, so
|
||||
load them into separate databases.
|
||||
|
||||
With --bundle the new file is a scienting-bundle/1 (spec/13 §13.5): it also gets the tables of
|
||||
sql/bundle-sqlite.sql, the full-text search index and bundle_meta. A bundle is built once, into
|
||||
a file that does not exist yet, and names the store revision it was built from.""",
|
||||
epilog="""\
|
||||
examples:
|
||||
paperspec load --sqlite lab.db fixtures/packages/hale2026harmonic-damb
|
||||
paperspec load --sqlite lab.db more/ other.sct.zip add packages to the same database later
|
||||
paperspec load --sqlite search.db --bundle --built-from 4fa326d paper/ other/
|
||||
a new bundle with full-text search
|
||||
sqlite3 lab.db "SELECT doc_key, COUNT(*) FROM nodes GROUP BY doc_key"
|
||||
|
||||
Queries to start from: %s/docs/examples.html""" % SITE)
|
||||
p.add_argument("--sqlite", required=True, metavar="DATABASE",
|
||||
help="the database file; created with the tables if it does not exist")
|
||||
p.add_argument("--bundle", action="store_true", help="build a new scienting-bundle/1 file with search")
|
||||
p.add_argument("--built-from", metavar="REV", help="with --bundle: the store revision the packages come from")
|
||||
p.add_argument("--store", help="with --bundle: the store's name (default: the packages' common store)")
|
||||
p.add_argument("--max-size", metavar="BYTES", type=int, default=ZIP_LIMIT,
|
||||
help="refuse a zipped package that expands past this many bytes (default %d, 1 GiB)" % ZIP_LIMIT)
|
||||
p.add_argument("packages", nargs="+", metavar="PACKAGE", help="a package folder or a zipped package")
|
||||
return p
|
||||
|
||||
|
||||
def _check(sub):
|
||||
p = sub.add_parser(
|
||||
"check", help="check the whole repository (in a checkout)",
|
||||
description="""\
|
||||
Check the whole paperspec repository: schemas, records, packages and the zipped package, the
|
||||
broken packages, every vector, a byte-for-byte rebuild, the SQL round trip and the bundle, the
|
||||
counts the prose states, the prose, the links and the website.
|
||||
|
||||
Run it before every commit. Each section prints how many checks it ran; a failure names its
|
||||
section and the file and line, and the run ends with `All checks pass.` or the failures.
|
||||
After you change a rule, a fixture source or a case in the builder, run `paperspec build` and
|
||||
commit the regenerated files, or the rebuild check fails on the out-of-date ones.
|
||||
`pixi run check` also lints the Markdown first, which this command does not.
|
||||
|
||||
It works on a checkout of the repository and needs the tools extra.
|
||||
Exit status: 0 when every check passed, 1 otherwise.""",
|
||||
epilog="""\
|
||||
examples:
|
||||
paperspec check everything
|
||||
paperspec check --quick everything except the rebuild, the slowest part
|
||||
pixi run check lint the Markdown, then everything""")
|
||||
p.add_argument("--quick", action="store_true", help="skip the byte-for-byte rebuild, the slowest check")
|
||||
return p
|
||||
|
||||
|
||||
def _build(sub):
|
||||
p = sub.add_parser(
|
||||
"build", help="rebuild fixtures/ and vectors/ from their sources (in a checkout)",
|
||||
description="""\
|
||||
Build every fixture and test vector from the hand-written sources in fixtures/src/ and the
|
||||
cases the builder defines, computing each expected value with the oracle.
|
||||
|
||||
Without --out it rewrites fixtures/ and vectors/ in place: run it after you change a rule, a
|
||||
fixture source or a case, review the diff, and commit the regenerated files with the change.
|
||||
The build is deterministic, and `paperspec check` rebuilds into a temporary folder and compares
|
||||
every file byte for byte to prove it.
|
||||
|
||||
It works on a checkout of the repository and needs the tools extra.""",
|
||||
epilog="""\
|
||||
examples:
|
||||
paperspec build rewrite fixtures/ and vectors/ in this checkout
|
||||
paperspec build --out /tmp/rebuilt build elsewhere, then compare:
|
||||
diff -r /tmp/rebuilt/vectors vectors""")
|
||||
p.add_argument("--out", metavar="DIR",
|
||||
help="build into DIR instead of rewriting fixtures/ and vectors/ in the checkout")
|
||||
return p
|
||||
|
||||
|
||||
def _stress(sub):
|
||||
p = sub.add_parser(
|
||||
"stress", help="stress the oracle with random inputs (in a checkout)",
|
||||
description="""\
|
||||
Stress the reference implementation with random inputs from a seeded generator. The vectors pin
|
||||
values; this checks the invariants the chapters state, such as canonical JSON round trips,
|
||||
corrections keeping the five values equal to a full recomputation, anchors always resolving to
|
||||
one of the five outcomes, the zip reader never writing outside its folder, and a mutated package
|
||||
always being refused.
|
||||
|
||||
A failure prints the seed, so it reproduces exactly: run it again with that --seed.
|
||||
Before a release, run it with the default seed and with one or two others.
|
||||
|
||||
It works on a checkout of the repository and needs the tools extra.
|
||||
Exit status: 0 when every property held, 1 otherwise.""",
|
||||
epilog="""\
|
||||
examples:
|
||||
paperspec stress the default seed, 200 cases per property
|
||||
paperspec stress --seed 7 --rounds 1000 another seed, more cases
|
||||
paperspec stress --rounds 5 a quick smoke test""")
|
||||
p.add_argument("--seed", type=int, default=20261014, help="the random seed (default 20261014)")
|
||||
p.add_argument("--rounds", type=int, default=200, help="cases per property (default 200)")
|
||||
return p
|
||||
|
||||
|
||||
def _site(sub):
|
||||
p = sub.add_parser(
|
||||
"site", help="build the website into _site/ (in a checkout)",
|
||||
description="""\
|
||||
Build the website. The output mirrors the repository, so each file is served at its own path:
|
||||
a schema at the URL in its `$id`, the JSON-LD context at the address layers name, and every
|
||||
vector, fixture and table as it is. Beside each Markdown file it writes a page, with a vocabulary
|
||||
page, folder listings, a search index and a 404 page; deploy/README.md says how it is served.
|
||||
|
||||
The output folder must not be the repository or contain it. It needs only the standard library
|
||||
and works on a checkout of the repository.""",
|
||||
epilog="""\
|
||||
examples:
|
||||
paperspec site write _site/
|
||||
paperspec site --out /tmp/site --base-url http://localhost:8000
|
||||
python -m http.server --directory /tmp/site 8000 preview it at http://localhost:8000""")
|
||||
p.add_argument("--out", metavar="DIR", help="the output folder (default _site/ in the checkout)")
|
||||
p.add_argument("--base-url", metavar="URL",
|
||||
help="the site's address, for canonical links (default https://paperspec.scient.ing)")
|
||||
return p
|
||||
|
||||
|
||||
COMMANDS = {"validate": _validate, "load": _load, "check": _check, "build": _build, "stress": _stress,
|
||||
"site": _site}
|
||||
|
||||
|
||||
def parser():
|
||||
"""The `paperspec` parser and a map from each command to its own parser."""
|
||||
fmt = argparse.RawDescriptionHelpFormatter
|
||||
top = argparse.ArgumentParser(prog="paperspec", description=DESCRIPTION, epilog=EPILOG, formatter_class=fmt,
|
||||
suggest_on_error=True)
|
||||
top.add_argument("--version", action="version",
|
||||
version="paperspec %s (Unicode %s)" % (__version__, UNICODE_VERSION))
|
||||
sub = top.add_subparsers(dest="command", title="commands", metavar="COMMAND")
|
||||
subs = {}
|
||||
for name, add in COMMANDS.items():
|
||||
subs[name] = add(_Sub(sub, fmt))
|
||||
return top, subs
|
||||
|
||||
|
||||
class _Sub:
|
||||
"""add_parser with the formatter and suggestions every command shares."""
|
||||
|
||||
def __init__(self, sub, fmt):
|
||||
self.sub, self.fmt = sub, fmt
|
||||
|
||||
def add_parser(self, name, **kw):
|
||||
return self.sub.add_parser(name, formatter_class=self.fmt, suggest_on_error=True, **kw)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
top, subs = parser()
|
||||
args = top.parse_args(argv)
|
||||
if args.command is None:
|
||||
top.print_help()
|
||||
return 2
|
||||
module = importlib.import_module("paperspec.cli." + args.command)
|
||||
try:
|
||||
return module.run(args) or 0
|
||||
except UsageError as e:
|
||||
subs[args.command].error(str(e))
|
||||
except KeyboardInterrupt:
|
||||
return 130
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the paperspec website: every file of the repository, and its Markdown rendered as documentation.
|
||||
|
||||
Non-normative tooling, standard library only.
|
||||
Usage: python3 tools/site.py [--out DIR] [--base-url URL]
|
||||
Usage: paperspec site [--out DIR] [--base-url URL]
|
||||
The output folder (default `_site/`) mirrors the repository, so each file is served at its own path: a schema at
|
||||
the URL in its `$id`, the JSON-LD context at the address layers name, and every vector, fixture and table as it is.
|
||||
Beside each Markdown file the builder writes a page, `README.md` becoming its folder's `index.html` and `X.md`
|
||||
@@ -11,16 +10,16 @@ term IRIs point at, a search index, a 404 page and one stylesheet. Links between
|
||||
the pages, so the site works from any host, and `deploy/Caddyfile` serves it with the right media types.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SKIP_DIRS = {".git", ".pixi", ".claude", "_site", "__pycache__", "node_modules", ".gitea", ".vscode", ".idea"}
|
||||
from paperspec.cli import UsageError, checkout
|
||||
|
||||
ROOT = checkout()
|
||||
SKIP_DIRS = {".git", ".pixi", ".claude", "_site", "__pycache__", "node_modules", ".gitea", ".vscode", ".idea", "build", "dist"}
|
||||
SKIP_FILES = {".DS_Store"}
|
||||
NAME = "paperspec"
|
||||
REPO = "https://git.scient.ing/infra/paperspec"
|
||||
@@ -36,7 +35,7 @@ def version():
|
||||
# --- inline Markdown ------------------------------------------------------------------------------------------
|
||||
|
||||
def slug(heading):
|
||||
"""A heading's anchor as GitHub, Gitea and Forgejo compute it (the same rule tools/check.py uses)."""
|
||||
"""A heading's anchor as GitHub, Gitea and Forgejo compute it (the same rule paperspec check uses)."""
|
||||
t = re.sub(r"[`*]", "", heading.strip().lower())
|
||||
t = "".join(ch for ch in t if ch.isalnum() or ch in " -_")
|
||||
return t.replace(" ", "-")
|
||||
@@ -572,7 +571,8 @@ def build(out, base_url=BASE_URL):
|
||||
os.makedirs(out)
|
||||
mds = []
|
||||
for d, dirs, fs in os.walk(ROOT):
|
||||
dirs[:] = sorted(x for x in dirs if x not in SKIP_DIRS and not (x.startswith(".") and x != ".well-known"))
|
||||
dirs[:] = sorted(x for x in dirs if x not in SKIP_DIRS and not x.endswith(".egg-info")
|
||||
and not (x.startswith(".") and x != ".well-known"))
|
||||
reld = os.path.relpath(d, ROOT)
|
||||
reld = "" if reld == "." else reld.replace(os.sep, "/")
|
||||
if os.path.abspath(d) == os.path.abspath(out):
|
||||
@@ -620,17 +620,10 @@ def build(out, base_url=BASE_URL):
|
||||
return len(mds)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--out", default=os.path.join(ROOT, "_site"), help="the output folder (default _site/)")
|
||||
ap.add_argument("--base-url", default=BASE_URL, help="the site's address, for canonical links (default %s)" % BASE_URL)
|
||||
a = ap.parse_args()
|
||||
out = os.path.abspath(a.out)
|
||||
def run(a):
|
||||
out = os.path.abspath(a.out or os.path.join(ROOT, "_site"))
|
||||
if out == ROOT or ROOT.startswith(out + os.sep):
|
||||
ap.error("the output folder must not be the repository or contain it")
|
||||
n = build(out, a.base_url)
|
||||
raise UsageError("the output folder must not be the repository or contain it; the default is _site/")
|
||||
n = build(out, a.base_url or BASE_URL)
|
||||
print("Wrote %d pages and every file of the repository to %s" % (n, out))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
return 0
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stress the reference implementation with random inputs. Exit status 0 means every property held.
|
||||
|
||||
Non-normative tooling; it needs Python 3.14 and the packages pixi installs (`jsonschema`, `cryptography`).
|
||||
Usage: python3 tools/stress.py [--seed N] [--rounds N]
|
||||
Non-normative tooling; it needs the packages `jsonschema` and `cryptography`, which pixi and paperspec[tools] install.
|
||||
Usage: paperspec stress [--seed N] [--rounds N]
|
||||
The vectors pin values; this program checks the invariants the chapters state, over inputs drawn from a seeded
|
||||
random generator, so a failure is reproducible from the seed it prints. Canonical JSON round-trips and sorts keys
|
||||
by UTF-16 units; normalization is idempotent; notation folding never raises; random corrections keep the five
|
||||
@@ -14,7 +13,6 @@ input without raising and never write outside their folders; rights combine the
|
||||
spot-check chains hundreds deep settle without recursion.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
@@ -27,21 +25,23 @@ import zipfile
|
||||
from fractions import Fraction
|
||||
from math import comb
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
from paperspec.cli import checkout, require
|
||||
|
||||
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, hashing, jcs, receipts as RC, rights as RI, targets as TG # noqa: E402
|
||||
from tools.oracle import keys as K, log as LG, mldsa, vrf # noqa: E402
|
||||
from tools.oracle import package as PK # noqa: E402
|
||||
from tools.oracle.model import LABELLED, LEAVES, Doc # noqa: E402
|
||||
from tools.oracle.notation import fold_math # noqa: E402
|
||||
from tools.oracle.text import NODE_ID_RE, RECORD_ID_RE, letter_first, norm, record_id, sha256_hex, is_ws # noqa: E402
|
||||
from tools.oracle.tree import Refusal, validate_structure # noqa: E402
|
||||
from tools.build import ROSTER_BY_ID, SCHEMAS, secret, sentence_doc, tiny_package # noqa: E402
|
||||
from tools.check import HALE, STORE, TINY_KEY, reference_values # noqa: E402
|
||||
require("cryptography", "jsonschema")
|
||||
ROOT = checkout()
|
||||
|
||||
from paperspec import align as AL # noqa: E402
|
||||
from paperspec import anchors as AN # noqa: E402
|
||||
from paperspec import corrections as CO # noqa: E402
|
||||
from paperspec import crypto, fingerprint as FP, hashing, jcs, receipts as RC, rights as RI, targets as TG # noqa: E402
|
||||
from paperspec import keys as K, log as LG, mldsa, vrf # noqa: E402
|
||||
from paperspec import package as PK # noqa: E402
|
||||
from paperspec.model import LABELLED, LEAVES, Doc # noqa: E402
|
||||
from paperspec.notation import fold_math # noqa: E402
|
||||
from paperspec.text import NODE_ID_RE, RECORD_ID_RE, letter_first, norm, record_id, sha256_hex, is_ws # noqa: E402
|
||||
from paperspec.tree import Refusal, validate_structure # noqa: E402
|
||||
from paperspec.cli.build import ROSTER_BY_ID, SCHEMAS, secret, sentence_doc, tiny_package # noqa: E402
|
||||
from paperspec.cli.check import HALE, STORE, TINY_KEY, reference_values # noqa: E402
|
||||
|
||||
FAILS = []
|
||||
COUNTS = {}
|
||||
@@ -690,11 +690,7 @@ def stress_mldsa(rng, rounds):
|
||||
"garbage verifies")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--seed", type=int, default=20261014, help="the random seed (default 20261014)")
|
||||
ap.add_argument("--rounds", type=int, default=200, help="cases per property (default 200)")
|
||||
a = ap.parse_args()
|
||||
def run(a):
|
||||
rng = random.Random(a.seed)
|
||||
print("seed %d, %d rounds" % (a.seed, a.rounds))
|
||||
stress_canonical_json(rng, a.rounds)
|
||||
@@ -724,9 +720,9 @@ def main():
|
||||
print("\n%d failures (seed %d):" % (len(FAILS), a.seed))
|
||||
for f in FAILS[:60]:
|
||||
print(" " + f)
|
||||
sys.exit(1)
|
||||
if len(FAILS) > 60:
|
||||
print(" and %d more" % (len(FAILS) - 60))
|
||||
print("\nReproduce them with: paperspec stress --seed %d --rounds %d" % (a.seed, a.rounds))
|
||||
return 1
|
||||
print("\nEvery property held.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
return 0
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate packages, or run the correction gate on proposals against a package.
|
||||
|
||||
Non-normative tooling.
|
||||
Usage: python3 tools/validate.py PACKAGE [PACKAGE ...]
|
||||
python3 tools/validate.py --correction PROPOSALS PACKAGE
|
||||
Usage: paperspec validate PACKAGE [PACKAGE ...]
|
||||
paperspec validate --correction PROPOSALS PACKAGE
|
||||
A package is a folder, or a zipped package (`.sct.zip`), which is unpacked under the rules of spec/12 §12.2
|
||||
before it is checked. Each is checked as spec/22 §22.2 describes, and each failure printed as
|
||||
`package: where: rule: message`.
|
||||
@@ -13,18 +12,18 @@ it would append to corrections.jsonl is printed, until one is refused. Nothing i
|
||||
Exit status: 0 when everything is valid, 1 when something is refused, 2 for a usage error.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
from paperspec.cli import UsageError, require
|
||||
|
||||
from tools.oracle import corrections, jcs, package # noqa: E402
|
||||
from tools.oracle.schema import Store # noqa: E402
|
||||
from tools.oracle.tree import Refusal # noqa: E402
|
||||
require("cryptography", "jsonschema")
|
||||
|
||||
from paperspec import corrections, jcs, package, resources # noqa: E402
|
||||
from paperspec.schema import Store # noqa: E402
|
||||
from paperspec.tree import Refusal # noqa: E402
|
||||
|
||||
|
||||
def read_proposals(path):
|
||||
@@ -43,19 +42,10 @@ def validate_one(path, store, limit):
|
||||
return package.validate(path, store)
|
||||
|
||||
|
||||
def main():
|
||||
lines = __doc__.splitlines()
|
||||
ap = argparse.ArgumentParser(description=lines[0], epilog="\n".join(lines[5:]),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--correction", metavar="PROPOSALS",
|
||||
help="a JSON or JSON Lines file of proposed corrections to run through the gate")
|
||||
ap.add_argument("--max-size", metavar="BYTES", type=int, default=package.ZIP_LIMIT,
|
||||
help="refuse a zipped package that expands past this many bytes (default %d)" % package.ZIP_LIMIT)
|
||||
ap.add_argument("packages", nargs="+", metavar="PACKAGE", help="package folders or zipped packages")
|
||||
a = ap.parse_args()
|
||||
def run(a):
|
||||
if a.correction and len(a.packages) != 1:
|
||||
ap.error("--correction checks proposals against exactly one package")
|
||||
store, bad = Store(os.path.join(ROOT, "schemas")), False
|
||||
raise UsageError("--correction checks proposals against exactly one package")
|
||||
store, bad = Store(resources.path("schemas")), False
|
||||
for p in a.packages:
|
||||
for e in validate_one(p, store, a.max_size).errors:
|
||||
print("%s: %s: %s: %s" % (p, e["where"], e["rule"], e["message"]), file=sys.stderr)
|
||||
@@ -84,7 +74,3 @@ def main():
|
||||
return 1
|
||||
print(jcs.dumps(record))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File renamed without changes.
@@ -1,22 +1,31 @@
|
||||
"""Keyed hashing, key derivation, Merkle roots and Ed25519.
|
||||
|
||||
Non-normative; see spec/05-hashes.md and spec/08-fidelity.md.
|
||||
HKDF and Ed25519 come from the `cryptography` package.
|
||||
HKDF and Ed25519 come from the `cryptography` package, imported on first use so that the hashing, Merkle
|
||||
and did:key functions work without it.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
|
||||
from . import jcs
|
||||
|
||||
|
||||
def _cryptography():
|
||||
"""The `cryptography` modules HKDF and Ed25519 use: exceptions, hashes, ed25519 and hkdf."""
|
||||
try:
|
||||
from cryptography import exceptions
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives.kdf import hkdf
|
||||
except ImportError:
|
||||
raise ImportError("HKDF and Ed25519 need the cryptography package: install paperspec[crypto]") from None
|
||||
return exceptions, hashes, ed25519, hkdf
|
||||
|
||||
|
||||
def hkdf(ikm, salt, info, length=32):
|
||||
"""HKDF-SHA-256, RFC 5869."""
|
||||
return HKDF(algorithm=hashes.SHA256(), length=length, salt=salt, info=info).derive(ikm)
|
||||
_, hashes, _, kdf = _cryptography()
|
||||
return kdf.HKDF(algorithm=hashes.SHA256(), length=length, salt=salt, info=info).derive(ikm)
|
||||
|
||||
|
||||
def team_key(secret):
|
||||
@@ -199,20 +208,23 @@ def verify_consistency(root_m, root_n, m, n, proof):
|
||||
|
||||
def ed25519_public(secret):
|
||||
"""The 32-byte public key of a 32-byte Ed25519 secret."""
|
||||
return Ed25519PrivateKey.from_private_bytes(secret).public_key().public_bytes_raw()
|
||||
_, _, ed25519, _ = _cryptography()
|
||||
return ed25519.Ed25519PrivateKey.from_private_bytes(secret).public_key().public_bytes_raw()
|
||||
|
||||
|
||||
def ed25519_sign(secret, msg):
|
||||
return Ed25519PrivateKey.from_private_bytes(secret).sign(msg)
|
||||
_, _, ed25519, _ = _cryptography()
|
||||
return ed25519.Ed25519PrivateKey.from_private_bytes(secret).sign(msg)
|
||||
|
||||
|
||||
def ed25519_verify(public, msg, signature):
|
||||
"""True when `signature` is a valid Ed25519 signature of `msg` under `public`; never raises."""
|
||||
if len(public) != 32 or len(signature) != 64:
|
||||
return False
|
||||
exceptions, _, ed25519, _ = _cryptography()
|
||||
try:
|
||||
Ed25519PublicKey.from_public_bytes(public).verify(signature, msg)
|
||||
except (InvalidSignature, ValueError):
|
||||
ed25519.Ed25519PublicKey.from_public_bytes(public).verify(signature, msg)
|
||||
except (exceptions.InvalidSignature, ValueError):
|
||||
return False
|
||||
return True
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,21 @@
|
||||
"""Where the schemas, SQL and vectors are, as filesystem paths.
|
||||
|
||||
An installed wheel carries them inside the package (`paperspec/schemas`, `paperspec/sql`, `paperspec/vectors`).
|
||||
An editable install or a checkout keeps them at the repository root, where the specification links to them,
|
||||
so `importlib.resources.files("paperspec") / "sql"` finds nothing there; `path("sql")` finds both.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
FOLDERS = ("schemas", "sql", "vectors")
|
||||
_PACKAGE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def path(name):
|
||||
"""The folder `schemas`, `sql` or `vectors`."""
|
||||
if name not in FOLDERS:
|
||||
raise ValueError("no data folder %r; there are %s" % (name, ", ".join(FOLDERS)))
|
||||
for d in (os.path.join(_PACKAGE, name), os.path.join(os.path.dirname(os.path.dirname(_PACKAGE)), name)):
|
||||
if os.path.isdir(d):
|
||||
return d
|
||||
raise FileNotFoundError("the %s folder is neither in the package nor at the root of its checkout" % name)
|
||||
File renamed without changes.
@@ -3,14 +3,12 @@
|
||||
Non-normative.
|
||||
The spec's schemas are restricted to the keywords below (spec/22-conformance.md) so that every 2020-12
|
||||
validator reads them alike; `lint` refuses any schema that leaves the subset.
|
||||
`jsonschema` and `referencing` are imported on first use, so `Store.load`, `deref` and `lint` work without them.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
from referencing import Registry, Resource
|
||||
|
||||
KEYWORDS = {
|
||||
"$schema", "$id", "$ref", "$defs", "$comment", "title", "description", "examples", "default", "deprecated",
|
||||
"type", "enum", "const", "properties", "required", "additionalProperties", "patternProperties",
|
||||
@@ -25,6 +23,17 @@ class SchemaError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _validation():
|
||||
"""The validator and registry classes from `jsonschema` and `referencing`, imported on first use."""
|
||||
try:
|
||||
from jsonschema import Draft202012Validator
|
||||
from referencing import Registry, Resource
|
||||
except ImportError:
|
||||
raise ImportError("JSON Schema validation needs the jsonschema and referencing packages: "
|
||||
"install paperspec[schema]") from None
|
||||
return Draft202012Validator, Registry, Resource
|
||||
|
||||
|
||||
class Store:
|
||||
"""The schemas of one folder, linted on load, with a compiled validator per schema."""
|
||||
|
||||
@@ -36,6 +45,7 @@ class Store:
|
||||
|
||||
def registry(self):
|
||||
if self._registry is None:
|
||||
_, Registry, Resource = _validation()
|
||||
names = sorted(f for f in os.listdir(self.root) if f.endswith(".schema.json"))
|
||||
self._registry = Registry().with_resources(
|
||||
[(self.load(f)["$id"], Resource.from_contents(self.load(f))) for f in names])
|
||||
@@ -43,6 +53,7 @@ class Store:
|
||||
|
||||
def validator(self, name):
|
||||
if name not in self._validators:
|
||||
Draft202012Validator, _, _ = _validation()
|
||||
self._validators[name] = Draft202012Validator(self.load(name), registry=self.registry())
|
||||
return self._validators[name]
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+44
-29
@@ -4,51 +4,58 @@ This page is for anyone who wants to run the repository's checks, rebuild its ex
|
||||
Newcomers get each term defined where it first appears.
|
||||
Implementers get the commands and the traps.
|
||||
|
||||
These programs are the repository's reference implementation.
|
||||
These commands are the repository's reference implementation.
|
||||
They apply every rule of the specification, so the rest of the repository can be checked against them.
|
||||
Two build and check the repository, one validates packages and proposed corrections, and one loads packages into SQLite, a database stored in a single file.
|
||||
They need Python 3.14 and two packages, `jsonschema` and `cryptography`, and are licensed Apache-2.0.
|
||||
Two build and check the repository, one validates packages and proposed corrections, one loads packages into SQLite, a database stored in a single file, one stresses the reference implementation with random inputs, and one builds the website.
|
||||
They are subcommands of one command, `paperspec`, which the Python package `paperspec` installs; its code is in [src/paperspec/](../src/paperspec/), with the commands in `src/paperspec/cli/`.
|
||||
`paperspec --help` lists them and says where each works, and `paperspec COMMAND --help` gives a command's options, advice and examples.
|
||||
They need Python 3.14 and the package's `tools` extra (`jsonschema`, `referencing` and `cryptography`), except `paperspec site`, which needs only the standard library, and they are licensed Apache-2.0.
|
||||
|
||||
A package is the folder of files that holds one document, such as `fixtures/packages/hale2026harmonic`.
|
||||
A package is the folder of files that holds one document, such as `fixtures/packages/hale2026harmonic-damb`.
|
||||
A vector is a test case: an input and the exact output every implementation must compute (see [the vectors](../vectors/README.md)).
|
||||
A fixture is an example file or package under `fixtures/`.
|
||||
|
||||
| Program | Does |
|
||||
| Command | Does |
|
||||
| --- | --- |
|
||||
| `check.py` | Checks everything: schemas, records, packages and the zipped package, the broken packages, every vector, a byte-for-byte rebuild, the SQL (a database query language) round trip, the counts the prose states, and the prose. `--quick` skips the rebuild. |
|
||||
| `build.py` | Builds `fixtures/` and `vectors/` from the hand-written sources in `fixtures/src/` and the cases it defines. `--out DIR` builds into another folder. |
|
||||
| `validate.py` | Validates packages, as folders or as zipped packages (`.sct.zip`, unpacked under the rules of [spec/12 §12.2](../spec/12-package.md#122-paths)): `python3 tools/validate.py PACKAGE [PACKAGE ...]`. With `--correction PROPOSALS` it runs proposed corrections through the gate against one package's current state, and prints the record each would append or the rule it breaks, without writing anything ([spec/11 §11.4](../spec/11-corrections.md#114-the-gate)). |
|
||||
| `load.py` | Validates packages, folders or zipped, then loads them into a SQLite database with the tables of `sql/`: `python3 tools/load.py --sqlite lab.db PACKAGE [PACKAGE ...]`. With `--bundle --built-from REV` it builds a new bundle instead, with the full-text search index ([spec/13 §13.5](../spec/13-database.md#135-the-bundle)). |
|
||||
| `site.py` | Builds the website into `_site/`: every file of the repository at its own path, and each Markdown file rendered as a page beside it, with a vocabulary page, folder listings and a search index. It uses only the standard library. `pixi run check` builds it into a temporary folder and checks every link ([deploy/](../deploy/README.md)). |
|
||||
| `stress.py` | Stresses the oracle with random inputs from a seeded generator: `python3 tools/stress.py [--seed N] [--rounds N]`. It checks the invariants the chapters state rather than the vectors' values, such as canonical JSON round trips, corrections keeping the five values consistent with a full recomputation, anchors always resolving to one of the five outcomes, the zip reader never writing outside its folder, and a mutated package always being refused. |
|
||||
| `oracle/` | A compact implementation of every rule. `build.py` uses it to compute expected values, and `check.py` uses it to recompute them. |
|
||||
| `paperspec check` | Checks everything: schemas, records, packages and the zipped package, the broken packages, every vector, a byte-for-byte rebuild, the SQL (a database query language) round trip, the counts the prose states, and the prose. `--quick` skips the rebuild. |
|
||||
| `paperspec build` | Builds `fixtures/` and `vectors/` from the hand-written sources in `fixtures/src/` and the cases it defines. `--out DIR` builds into another folder. |
|
||||
| `paperspec validate` | Validates packages, as folders or as zipped packages (`.sct.zip`, unpacked under the rules of [spec/12 §12.2](../spec/12-package.md#122-paths)): `paperspec validate PACKAGE [PACKAGE ...]`. With `--correction PROPOSALS` it runs proposed corrections through the gate against one package's current state, and prints the record each would append or the rule it breaks, without writing anything ([spec/11 §11.4](../spec/11-corrections.md#114-the-gate)). |
|
||||
| `paperspec load` | Validates packages, folders or zipped, then loads them into a SQLite database with the tables of `sql/`: `paperspec load --sqlite lab.db PACKAGE [PACKAGE ...]`. With `--bundle --built-from REV` it builds a new bundle instead, with the full-text search index ([spec/13 §13.5](../spec/13-database.md#135-the-bundle)). |
|
||||
| `paperspec site` | Builds the website into `_site/`: every file of the repository at its own path, and each Markdown file rendered as a page beside it, with a vocabulary page, folder listings and a search index. It uses only the standard library. `pixi run check` builds it into a temporary folder and checks every link ([deploy/](../deploy/README.md)). |
|
||||
| `paperspec stress` | Stresses the oracle with random inputs from a seeded generator: `paperspec stress [--seed N] [--rounds N]`. It checks the invariants the chapters state rather than the vectors' values, such as canonical JSON round trips, corrections keeping the five values consistent with a full recomputation, anchors always resolving to one of the five outcomes, the zip reader never writing outside its folder, and a mutated package always being refused. |
|
||||
|
||||
`paperspec validate` and `paperspec load` work anywhere the package is installed with its `tools` extra, using the schemas and SQL it carries.
|
||||
The other four work on a checkout of this repository: the one an editable install points at, or else the one around the current folder.
|
||||
|
||||
## When to run what
|
||||
|
||||
Each program is also a pixi task.
|
||||
Pixi sets up a pinned Python and the other packages the repository uses.
|
||||
Each command is also a pixi task.
|
||||
Pixi sets up a pinned Python and the other packages the repository uses, and installs this checkout as the package, editable, so a change to the code takes effect at once.
|
||||
The commands `pixi run check`, `pixi run quick`, `pixi run build` and `pixi run load` then work from anywhere in the repository.
|
||||
`pixi run check` lints the Markdown first.
|
||||
|
||||
Run the programs directly from the repository root if you do not use pixi:
|
||||
If you do not use pixi, install the checkout editable with its extras on Python 3.14, then run the commands from the repository root:
|
||||
|
||||
```sh
|
||||
python3 tools/check.py # everything
|
||||
python3 tools/check.py --quick # everything except the rebuild
|
||||
python3 tools/build.py --out /tmp/rebuilt
|
||||
python3 tools/validate.py fixtures/packages/hale2026harmonic
|
||||
python3 tools/load.py --sqlite lab.db fixtures/packages/hale2026harmonic
|
||||
pip install -e ".[tools]"
|
||||
paperspec check # everything
|
||||
paperspec check --quick # everything except the rebuild
|
||||
paperspec build --out /tmp/rebuilt
|
||||
paperspec validate fixtures/packages/hale2026harmonic-damb
|
||||
paperspec load --sqlite lab.db fixtures/packages/hale2026harmonic-damb
|
||||
```
|
||||
|
||||
`python -m paperspec check` runs the same program as `paperspec check`, which helps where the installed command is not on the path.
|
||||
|
||||
A passing check ends with `All checks pass.`
|
||||
The load command prints `hale2026harmonic: 75 nodes`, one line per package, and writes `lab.db` with a folder `lab.db.assets/` beside it for files that have no table of their own.
|
||||
It validates every package first and loads nothing if one fails, printing each failure as `package: where: rule: message`.
|
||||
The load command prints `hale2026harmonic-damb: 75 nodes`, one line per package, and writes `lab.db` with a folder `lab.db.assets/` beside it for files that have no table of their own.
|
||||
It validates every package first and loads nothing if one fails, printing each failure as `package: where: rule: message`, or `package: rule: message` for a zipped package that cannot be unpacked.
|
||||
The full and checks-only forms of one document share its key, so they go into separate databases.
|
||||
Each program prints its options with `--help` and refuses options it does not know.
|
||||
`build.py` rebuilds `fixtures/` and `vectors/` in place unless you pass `--out`.
|
||||
Each command prints its options with `--help` and refuses options it does not know.
|
||||
`paperspec build` rebuilds `fixtures/` and `vectors/` in place unless you pass `--out`.
|
||||
|
||||
Run `pixi run check` before every commit.
|
||||
Run `pixi run build` after you change a fixture source, a rule in `oracle/` or a case in `build.py`, and commit the regenerated files with the change.
|
||||
Run `pixi run build` after you change a fixture source, a rule in `src/paperspec/` or a case in `src/paperspec/cli/build.py`, and commit the regenerated files with the change.
|
||||
The check rebuilds into a temporary folder and compares every file byte for byte, so it fails on out-of-date files.
|
||||
|
||||
## About the oracle
|
||||
@@ -59,13 +66,21 @@ The oracle exists so that no expected value in this repository is typed by hand.
|
||||
|
||||
It favors clarity, and leaves the cryptography and schema validation to established packages.
|
||||
Ed25519 (a digital signature scheme) and HKDF come from `cryptography`, and JSON Schema (a format that describes the shape of JSON data) validation from `jsonschema`.
|
||||
The VRF of RFC 9381 (`oracle/vrf.py`) and ML-DSA-65 of FIPS 204 (`oracle/mldsa.py`) are written out in plain Python, slow and clear, and checked against the standards' own test vectors.
|
||||
The VRF of RFC 9381 (`src/paperspec/vrf.py`) and ML-DSA-65 of FIPS 204 (`src/paperspec/mldsa.py`) are written out in plain Python, slow and clear, and checked against the standards' own test vectors.
|
||||
The oracle still refuses any schema that uses a keyword outside the subset the schemas are restricted to.
|
||||
Canonical JSON goes through Python's built-in JSON encoder, and falls back to an exact RFC 8785 serializer for the values that encoder writes differently: floating-point numbers, integers beyond 2^53 and some non-ASCII member names.
|
||||
|
||||
Hashes are kept incrementally.
|
||||
A node's five values depend only on its own subtree, so after a correction the oracle rehashes only the nodes that changed and their ancestors, and a container's projections are its children's cached projections joined.
|
||||
`check.py` compares the result after every correction with a slow computation that projects each node from its whole subtree.
|
||||
`paperspec check` compares the result after every correction with a slow computation that projects each node from its whole subtree.
|
||||
|
||||
Do not build on it as a library.
|
||||
Implementations follow the [specification](../spec/README.md) and test themselves against the [vectors](../vectors/README.md).
|
||||
Other repositories of the project do not reimplement the specification: they install this repository as the Python package `paperspec`, from the Gitea package registry or as a git pin ([deploy/](../deploy/README.md#install-from-the-registry)), and import the oracle from it, so that every key, identifier, canonical JSON string, package check and database form comes from one piece of code.
|
||||
The library surface they use is `keys`, `text`, `jcs`, `package`, `db`, `schema` and `rights`, with the schemas, SQL and vectors as package data: `paperspec.resources.path("sql")` finds a folder in an installed wheel and in an editable install alike.
|
||||
The vectors travel with the package, but `resolve.jsonl`, `grading.jsonl` and `targets.jsonl` name a fixture package, and the fixtures stay in the repository.
|
||||
The package needs Python 3.14, the only version with the Unicode 16.0 character data the specification pins, and refuses to import on any other.
|
||||
Importing any of its modules needs only the standard library.
|
||||
Schema validation (`schema`, and with it `package.validate` and `corrections.gate`) needs `paperspec[schema]` (`jsonschema` and `referencing`), and HKDF and Ed25519 in `crypto`, which the signature and receipt checks use, need `paperspec[crypto]`.
|
||||
`paperspec[tools]` installs both, and is what a program that validates packages needs.
|
||||
The package version comes from git tags through setuptools-scm, and `pixi run -e dev dist` builds the sdist and the wheel into `dist/`.
|
||||
When `scienting-core` exists it takes this role behind the same vectors, and the import changes without any computed value changing.
|
||||
Independent implementations follow the [specification](../spec/README.md) and test themselves against the [vectors](../vectors/README.md).
|
||||
Whitespace-only changes.
@@ -1,13 +0,0 @@
|
||||
"""The reference computations behind every expected value in this repository; see tools/README.md."""
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
if sys.version_info < (3, 14):
|
||||
sys.exit("The paperspec tools need Python 3.14 or later; this is Python %d.%d." % sys.version_info[:2])
|
||||
|
||||
_missing = [m for m in ("cryptography", "jsonschema") if importlib.util.find_spec(m) is None]
|
||||
if _missing:
|
||||
sys.exit("The paperspec tools need %s %s; run them through pixi, or install %s with pip."
|
||||
% ("the package" if len(_missing) == 1 else "the packages", " and ".join(_missing),
|
||||
"it" if len(_missing) == 1 else "them"))
|
||||
+6
-6
@@ -73,19 +73,19 @@ Some files change the procedure:
|
||||
Compute the field for each listed case and compare across the group.
|
||||
- `fingerprints.jsonl` needs the download-stamp rules in [registry/stamps/scienting-stamps-1.json](../registry/stamps/scienting-stamps-1.json), which each case names in `rules`.
|
||||
- `resolve.jsonl`, `grading.jsonl` and `targets.jsonl` name a package by a path from the repository root.
|
||||
All three use `fixtures/packages/hale2026harmonic`.
|
||||
All three use `fixtures/packages/hale2026harmonic-damb`.
|
||||
|
||||
Several files also mix more than one kind of case, told apart by their fields.
|
||||
`ids.jsonl` has minting cases and letter-first cases, `keyed.jsonl` and `signatures.jsonl` have a case for each derivation or record, `spotcheck.jsonl` has sample-size cases and one full sample, and `rights.jsonl` has stage checks (a `stage` field), backend checks (a `backend` field) and one combining case (neither).
|
||||
|
||||
The reference checker in [tools/](../tools/README.md) runs all twenty-five files, together with the rest of the repository's checks:
|
||||
The reference checker, `paperspec check` ([tools/](../tools/README.md)), runs all twenty-five files, together with the rest of the repository's checks:
|
||||
|
||||
```sh
|
||||
python3 tools/check.py --quick
|
||||
paperspec check --quick
|
||||
```
|
||||
|
||||
It needs Python 3.14 and two packages, `jsonschema` and `cryptography`, and has no option to run one file.
|
||||
It tests the reference implementation in `tools/oracle/` against the vectors.
|
||||
It runs in a checkout, through pixi (`pixi run quick`) or after `pip install -e ".[tools]"` on Python 3.14, and has no option to run one file.
|
||||
It tests the reference implementation in `src/paperspec/` against the vectors.
|
||||
It does not test yours.
|
||||
|
||||
## What each file tests
|
||||
@@ -136,7 +136,7 @@ The specification, and the vectors with it, are at version 0.1.0 ([0.3](../spec/
|
||||
The specification has 32 formats: 19 are `candidate` and 13 are `provisional` in [registry/formats.json](../registry/formats.json).
|
||||
A candidate format changes only with a recorded reason and a new or changed vector ([22.5](../spec/22-conformance.md#225-stability)).
|
||||
|
||||
A passing run shows one thing: for these inputs, your implementation computes the same values as the reference implementation in `tools/oracle/`.
|
||||
A passing run shows one thing: for these inputs, your implementation computes the same values as the reference implementation in `src/paperspec/`.
|
||||
That code computed the expected outputs, and `pixi run build` regenerates them ([tools/](../tools/README.md)).
|
||||
The vectors hold 311 cases in all, some of them small: `hashes.jsonl` has one document, `alignment.jsonl` one pair of readings, `offsets.jsonl` two cases.
|
||||
|
||||
|
||||
Reference in new issue
Block a user