joulespec: a standard for energy-accounted specification conformance #561

Merged
dcharlot merged 1 commit from joulespec/standard-v0.1 into main 2026-11-01 08:17:51 -05:00
17 changed files with 4639 additions and 2 deletions

View file

@ -85,6 +85,12 @@ jobs:
- { name: sandbox-rs, dir: sandbox/sandbox-rs, runner: linux, conformance: sandbox-conformance, feature_tests: "-p sandbox-tee --features amd-kds,intel-pcs" }
- { name: smart-byte-rs, dir: smart-byte/smart-byte-rs, runner: linux }
- { name: pool-rs, dir: pool/pool-rs, runner: linux }
# `clippy: true` adds the zero-warning gate below. It is opt-in per
# entry rather than repo-wide because several workspaces are not at
# zero yet, and a gate that fails on arrival teaches people to pass
# `--no-verify`. A workspace that reaches zero adds the flag and
# cannot regress; joulespec-rs starts there.
- { name: joulespec-rs, dir: joulespec/joulespec-rs, runner: linux, clippy: true }
- { name: openpay, dir: openpay, runner: linux-arm64 }
- { name: joule-code-rs, dir: joule-code/joule-code-rs, runner: linux-arm64, z3: true }
- { name: map-rs, dir: map/map-rs, runner: linux-arm64, conformance: map-conformance }
@ -155,7 +161,7 @@ jobs:
if: needs.plan.outputs.any == 'true'
# Spread across the fleet's runners rather than pinning every job to `linux`.
# For most of these the runner is a capacity choice, not a correctness one —
# and 17 jobs on a single runner queue behind whatever else is using it.
# and every job on one runner queues behind whatever else is using it.
# `jouleclaw-rs` is the exception: see its matrix entry.
runs-on: ${{ matrix.runner }}
strategy:
@ -275,6 +281,19 @@ jobs:
JOULE_FIX_EXPECT_Z3: ${{ matrix.z3 && '1' || '' }}
run: cargo test --workspace
# The zero-warning gate, on the workspace's own runner.
#
# Deliberately not in the macOS lint step: that one exists for code only
# a macOS host can compile, and the matrix comments above are about not
# sending platform-neutral work to the contended Mac. A workspace that
# needs no Apple anything is linted where it is built.
- name: Clippy (all targets, warnings denied)
if: matrix.clippy
working-directory: ${{ matrix.dir }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
run: cargo clippy --workspace --all-targets -- -D warnings
# The NVML backends sit behind features no workspace member enables
# (jcp-energy `nvml`, eoc-meter `cuda`, jouleclaw-energy `nvml`), so the
# step above never compiles them, let alone runs their tests. Here they

View file

@ -130,6 +130,49 @@ The **entitlement layer** in front of a contended pool: who may start, on whose
- **Licence:** Apache-2.0 for reference code (see [`pool/LICENSE`](pool/LICENSE)); CC-BY-4.0 for specification text (see [`pool/LICENSE-SPEC`](pool/LICENSE-SPEC)).
- **Status:** v0.1.0-draft.1. `SPEC.md` published; reference workspace `pool-rs/` — `pool-core` carries the entitlement tree with weighted sibling surplus, the normative §4 admission order (impossibility before pendency, guarantee, then borrowing), and the §5.2 preemption order. `cargo test --workspace` green.
### [joulespec/](joulespec/) — A carrier for intent
The **specification cargo class**: what software owes, and whether anything
established it. Specification-driven practice converged on prose an agent
interprets and a human reads, and the difficulties catalogued wherever it is
examined — specifications drifting from the code they describe, generated
artifacts trusted past their evidence, no method for checking conformance, a
review burden heavy enough that practitioners report preferring to read the
code — are measurement problems rather than methodology ones. They persist
because the specification is a *document*: a clause cannot be addressed, so
nothing binds to it; conformance is a sentence in a review, so it cannot be
graded or capped; the cost of the ceremony is an anecdote, so the argument about
whether it earns its overhead has no units. `joulespec/1` leaves the prose
alone and changes what the specification *is*. A clause is addressed by its
**obligation** — the content address of its kind and text, with the label
deliberately excluded — so renaming a clause loses no evidence while rewording
one invalidates exactly that clause's evidence, which is the inversion that
lets drift go unobserved under label-bound citations. Evidence climbs a ladder
ordered by one question, *how hard would it be for this obligation to be
violated without this evidence noticing*: `E0:Cited` (testimony — where
prevailing practice lands) → `E1:Reviewed` → `E2:Exercised` (a green check,
which flatters: a test with no assertion passes) → `E3:Falsifiable` (the same
check on record **failing** against a named counter-artifact — the first rung
where green means something) → `E4:Proved`, reached only when the formalization
binding statement to clause was cross-checked by a key other than the prover's,
because a proof under a declared formalization is machine-checked evidence that
the something is this clause on the prover's word. A specification's grade is
its **weakest** owed obligation; non-goals are first-class, and evidence that
one was implemented is a flag that blocks conformance however good the rest is.
Revising a specification yields the exact partition of which evidence survives,
and a witness bound to an earlier artifact state is reported **stale** rather
than counted — so neither direction of drift needs anyone to remember to check.
Every witness and every unit of work carries its joules, so the receipt seals
the **specified share** (how much energy went into work citing no clause at
all) and **microjoules per falsifiable obligation** — the denominator
deliberately the rung whose evidence can fail, because a figure per *attempted*
obligation rewards ceremony. Receipts are derived from the evidence and never
from a verdict handed in, so every claim in one is reproducible by anyone
holding the same inputs.
- **Licence:** Apache-2.0 for reference code (see [`joulespec/LICENSE`](joulespec/LICENSE)); CC-BY-4.0 for specification text (see [`joulespec/LICENSE-SPEC`](joulespec/LICENSE-SPEC)).
- **Status:** v0.1.0-draft.1. `SPEC.md` published; reference workspace `joulespec-rs/` — `joulespec-core` carries obligation-addressed clauses and signed revisions, the graded evidence ladder with independence computed from key disjointness, the deterministic evaluator with the weakest-obligation cap and its four flag conditions, the revision partition, and the `joulespec/1` JCR-1 receipt with its per-clause audit. `cargo test --workspace` green, `cargo clippy --workspace --all-targets` zero warnings and gated in CI.
## How this is organised
Each subdirectory is self-contained: it carries its own README, its own licence, and its own contribution guidance. Cross-protocol consistency lives at this level — in this README and in [`CHARTER.md`](CHARTER.md).
@ -152,7 +195,8 @@ open-standards/
├── jcp/ — Joule Context Protocol: unified energy-native capability-secured agentic substrate (metered grants + signed receipts, decided outside the model) (Apache-2.0 code / CC-BY-4.0 spec, Rust ref impl)
├── joulecontract/ — contract cargo class: five-obligation energy-accounted interface contracts, bound + sealed receipts, adversarial-diff provenance (Apache-2.0 code / CC-BY-4.0 spec, Rust ref impl)
├── joule-os/ — the UDA reference runtime: single-level store + refine-family cascade + capability ledger, lowered per coordinate across native / PWA / bare-metal (Apache-2.0 code / CC-BY-4.0 spec, Rust ref impl)
└── mesh/ — inference serving + routing fabric: joule-ceiling × fidelity-floor requests routed cheapest-sufficient, metered, sealed as µJ-per-accepted-token receipts (Apache-2.0 code / CC-BY-4.0 spec, Rust ref impl)```
├── mesh/ — inference serving + routing fabric: joule-ceiling × fidelity-floor requests routed cheapest-sufficient, metered, sealed as µJ-per-accepted-token receipts (Apache-2.0 code / CC-BY-4.0 spec, Rust ref impl)
└── joulespec/ — specification cargo class: obligation-addressed clauses, a graded evidence ladder from testimony to proof, deterministic drift, and the joules a specification cost to evidence (Apache-2.0 code / CC-BY-4.0 spec, Rust ref impl)```
The standards do not depend on one another. They share a steward and a doctrine — that the protocol is the public commitment and the operations are the offer. (Most also share a unit of accounting, joules; ARL is universal and tied to nothing, joules being one of its four measured axes rather than a shared substrate.)
@ -172,6 +216,8 @@ Contributions are welcome at the subdirectory level. Issues, discussions, and pu
- **JouleContract:** the in-toto/DSSE `JouleContract` predicate emission at the recognized edge; a signed conformance vector pack (`conformance/v1/`); adversarial negatives from the published diff rounds (a frame amendment racing its breach, a quorum whose members share a parent entity, an evidence bundle with one swapped attestation); meter-attestation backends (RAPL / NVML / substrate telemetry) feeding the `Metered` binding; and a fourth adversarial diff round.
- **Joule OS:** additional refine-family backends behind the cascade (real `LocalSearch` / `StateGraph` / `ConstrainedSearch` stages); per-platform measured-energy oracles feeding the work-class receipts; conformance negatives against the THEOREM I1–I20 invariants (a capability minted outside `Ledger::issue`, a single-level-store mutation that tears across an instruction boundary, an AOT/JIT divergence); and bare-metal surface drivers.
- **JouleSpec:** witness producers that emit `E3` evidence from suites already in use (a mutation runner recording the mutant each test was written against is the natural one), a bridge that turns the verification standard's discharged goals into `E4` witnesses with their formalizations carried through, adversarial negatives (a witness re-bound to a reworded obligation, a review countersigned by the citing key, a receipt sealed from a hand-built verdict), and measured-energy backends feeding the witness `HwShunt` path.
A `CONTRIBUTING.md` per standard captures the specifics where they diverge.
## Contact

202
joulespec/LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

30
joulespec/LICENSE-SPEC Normal file
View file

@ -0,0 +1,30 @@
Creative Commons Attribution 4.0 International
Copyright (c) 2026 Transaction Science, Inc.
This specification text and accompanying documents are licensed under the
Creative Commons Attribution 4.0 International License (CC BY 4.0).
You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material for any purpose,
even commercially
Under the following terms:
- Attribution — You must give appropriate credit, provide a link to the
license, and indicate if changes were made. You may do so in any
reasonable manner, but not in any way that suggests the licensor
endorses you or your use.
No additional restrictions — You may not apply legal terms or technological
measures that legally restrict others from doing anything the license
permits.
Full licence text: https://creativecommons.org/licenses/by/4.0/legalcode
────────────────────────────────────────────────────────────────────────
Reference implementations and software accompanying this specification, if
and when published in this directory, may be released under a separate
licence (typically Apache-2.0). Refer to the licence file alongside each
artefact.

213
joulespec/README.md Normal file
View file

@ -0,0 +1,213 @@
# JouleSpec — Energy-Accounted Specification Conformance
**Version:** 0.1.0-draft.1 · **Status:** Draft · **Profile:** `joulespec/1`
**Specification text:** CC-BY-4.0 · **Reference code (`joulespec-rs/`):** Apache-2.0
JouleSpec is the open standard for **stating what software owes and measuring
whether anything established it**. A clause is addressed by its obligation, so
renaming is free and rewording costs exactly that clause's evidence. Evidence
climbs a ladder from testimony to proof. A revision yields the exact set of
obligations that just lost their evidence. And the receipt says what the whole
exercise cost in joules.
JouleSpec is one of the [Transaction Science open standards](../README.md).
## The problem is measurement, not methodology
Specification-driven practice converged on a shared shape: write the intent
down, derive the plan and the implementation from it, review at each phase. The
artifact it produces is prose an agent interprets and a human reads, and the
difficulties that follow are catalogued consistently wherever the practice is
examined — specifications drifting from the code they describe, generated
artifacts trusted past their evidence, no method for checking that an
implementation conforms, and a review burden heavy enough that practitioners
report preferring to read the code.
Each of those is a measurement problem wearing a methodology costume, and each
persists for the same reason: **the specification is a document.** A clause
cannot be addressed, so nothing can bind to it. Conformance is a sentence in a
review, so it cannot be graded or capped. The cost of the ceremony is an
anecdote, so the argument about whether it is worth the effort has no units and
cannot be settled.
JouleSpec changes none of the methodology. It changes what the specification
*is*.
## Renaming is free; rewording is not
A clause carries a label and an **obligation**. The label — `C1`, `auth-2` — is
editorial. The obligation is the content address of the clause's kind and text,
with the label deliberately excluded (SPEC §1.2), and evidence binds to the
obligation.
```
C1 "The endpoint rejects unauthenticated requests." → b3:9f4c…
auth-2 "The endpoint rejects unauthenticated requests." → b3:9f4c… same obligation
C1 "The endpoint should reject unauthenticated requests." → b3:2e71… different obligation
```
Renumbering a whole specification moves nothing and loses no evidence.
Weakening one requirement by one word invalidates that clause's evidence and
nothing else's — automatically, with nobody having to notice.
Binding evidence to a label gets both halves backwards: it survives the edit
that changed the requirement and breaks on the edit that renamed it. That
inversion is the mechanism by which drift goes unobserved. It is an addressing
problem, not a discipline problem, and an address is the whole fix.
## A ladder ordered by one question
How hard would it be for this obligation to be violated **without this evidence
noticing?**
| | | |
|---|---|---|
| `E0` | `cited` | The implementer says it is done. Nothing noticed anything. |
| `E1` | `reviewed` | A second party read the artifact, once. |
| `E2` | `exercised` | A named check ran against the artifact and passed. |
| `E3` | `falsifiable` | The same check is on record *failing* against a named counter-artifact. |
| `E4` | `proved` | No input violates it, under a formalization someone independent checked. |
**`E0` is where prevailing practice lands**, and it is testimony from the party
being evaluated. Much of it is true; it is still testimony, and reporting it as
conformance is the error this standard exists to stop.
**`E2` flatters.** A green check establishes that something ran. A test with no
assertion passes, and so does one whose fixture never reaches the condition the
clause cares about.
**`E3` is the first rung where green means something** — the check has
demonstrated it can fail, so its passing carries information. It is also the
cheapest strong rung: most projects can reach it with tests they already have,
by recording the mutant the test was written against.
The ladder is ordered by **re-checkability, not diligence**. A review is bound
to the state a person looked at; a named check runs again against any later
state. That says nothing about how carefully anyone read the code — it says one
kind of evidence survives the next commit and the other does not.
**Independence is computed, never declared.** A review signed by the key that
made the citation is the citation again, and grades `E0` (SPEC §2.5).
## What an `E4` actually claims
A proof discharges a formal statement, and the statement is not the clause. The
binding between them is a human act and the weakest link in any claim of proved
conformance — so a proof reaches `E4` only when that binding was cross-checked
by a key other than the prover's. Under a merely declared formalization it
grades `E2`: real mechanical evidence about *something*, where that the
something is this clause remains the prover's word (SPEC §2.6).
The reference library never formalizes a clause on anyone's behalf. Demotion
rather than refusal is deliberate — the proof is not fake, and discarding it
would lose evidence that exists.
## The weakest obligation sets the grade
A specification's grade is the **minimum** over its requirements and
constraints, absent entirely when any of them has no surviving evidence (SPEC
§3.2). Averaging would let nine proved obligations conceal one nobody looked
at, which is the failure a grade exists to surface.
**Non-goals are first-class and handled differently.** There is nothing to
evidence about behaviour that should not exist, so an unwitnessed non-goal is
correct and carries no grade. Evidence that one *was* implemented is a flag: it
does not lower the grade of the owed obligations — they really are evidenced —
and it does block conformance. The specification said not to build this and
something says it exists.
**Nothing is dropped quietly.** A witness that does not verify, names another
specification, or names an obligation the specification does not state is a
reported finding, not a no-op. That last case is the normal aftermath of a
rewording and the clearest available signal that evidence and specification
have parted company.
## Drift is a partition, not a warning
Diffing two revisions by obligation address says exactly which evidence
survives (SPEC §4.1): obligations `kept`, `relabelled` (editorial — no evidence
lost), `reworded` (that clause's evidence invalidated, the new obligation
unevidenced), `added`, and `removed`. From those, the two sets a revision
review actually wants fall out: **what just lost its evidence**, and **what
starts unevidenced**.
A freed label reused by a new clause is an addition, not a rewording — the old
obligation still stands elsewhere, so no evidence is invalidated, and
conflating the two would report a loss that did not happen.
The other direction of drift is the artifact moving under evidence that stayed
still, and it needs no diff at all: a witness is bound to the artifact state it
observed, so evidence from an earlier state is reported stale rather than
counted (SPEC §3.1). Neither direction requires anyone to remember to check.
## The ceremony has a price
Every witness carries the energy it cost to produce, and every unit of work
carries the energy it cost to perform. From that the receipt seals two figures
nothing else in this space can state:
- **The specified share** — how much of the work energy went into work citing
an obligation this specification actually states, in parts-per-million (SPEC
§5.2). This is the measured form of an observation the field makes
qualitatively: that agents produce features nobody requested. A run where a
quarter of the energy implemented no clause has a number saying so.
- **Microjoules per falsifiable obligation** — total attributed energy over the
count of owed obligations evidenced at `E3` or better (SPEC §5.3). The
denominator is deliberately the rung whose evidence can fail: a figure per
*attempted* obligation rewards ceremony, a figure per obligation something
would have caught rewards evidence.
Whether specification-driven practice earns its overhead is currently settled
by anecdote in both directions. Two processes applied to the same
specification, reaching the same evidence, one spending an order of magnitude
more energy to get there — that is not a matter of taste.
Where nothing reaches the bar the figure is **absent**, not zero. A cost per
unit of nothing is not a small number.
## The receipt is derived, never signed on request
A receipt is sealed from the specification, the artifact, the witnesses and the
work — never from a verdict handed in (SPEC §6.1). A verdict is an ordinary
data structure, and an interface that signed one would let a producer construct
the grade it wanted. Because the receipt is derived from the evidence, every
claim in it is reproducible by anyone holding the same inputs, and a producer
who wants a better grade has to go and get better evidence.
Receipts seal as **`joulespec/1` JCR-1 envelopes** — canonical det-CBOR under
COSE_Sign1 with Ed25519, the wire format the rest of this family seals with, so
a receipt verifies under tooling that has never heard of JouleSpec. The core
admits no nulls and several figures are legitimately absent, so those fields
are **omitted** rather than zeroed: a reader who finds no grade knows none was
established, where a reader who found `E0` would believe one was.
Each receipt carries a **per-clause audit** — label, kind, obligation, grade,
the witness that carried it, evidence energy, stale count — because which
obligation is the weak one is the actionable fact, and it must be answerable
from the receipt alone (SPEC §6.3).
## What it does not do
JouleSpec does not write specifications, run checks, formalize clauses, or
judge whether a clause states a good requirement.
And it cannot tell whether a clause says what its author meant. An obligation
is addressed, signed and tracked, and may still be the wrong requirement.
Software can satisfy every clause of its specification and be unfit for
purpose, because the specification was incomplete — and an incomplete
specification is exactly the case where every clause is covered. A grade is a
property of evidence, not of software (SPEC §8).
## Reference
[`joulespec-rs/crates/joulespec-core`](joulespec-rs/crates/joulespec-core) —
obligation-addressed clauses and signed revisions (`spec`), the graded evidence
ladder with computed independence (`evidence`), the deterministic evaluator
with the weakest-obligation cap (`conform`), the revision partition (`drift`),
and the `joulespec/1` JCR-1 receipt derived from its inputs (`receipt`).
Prior art inside this family: `joule-code-spec` first shipped the signed,
content-addressed clause and the citation binding a coding operation to it.
Its citations bind by clause **label** and its audit has one notion of coverage
— a clause is covered when something claims it. That is the `E0` rung here, and
label binding is the drift hole this standard closes.

480
joulespec/SPEC.md Normal file
View file

@ -0,0 +1,480 @@
# JouleSpec v0.1 — A Standard for Energy-Accounted Specification Conformance
| | |
|---|---|
| **Standard** | joulespec |
| **Status** | v0.1.0 draft for review |
| **Profile** | `joulespec/1` |
| **Licence** | Specification text CC-BY-4.0 ([`LICENSE-SPEC`](LICENSE-SPEC)); reference code Apache-2.0 ([`LICENSE`](LICENSE)) |
| **Reference** | [`joulespec-rs/`](joulespec-rs/) |
A vendor-neutral standard for **stating what software owes and measuring
whether anything established it**. JouleSpec specifies an obligation address
that survives renaming and breaks on rewording, a five-rung evidence ladder
from testimony to proof, a deterministic drift partition over revisions, and a
receipt that seals what a specification cost to evidence.
JouleSpec is one of the [Transaction Science open standards](../README.md).
Transaction Science is the steward, not the proprietor.
---
## 1. Scope and doctrine
### 1.1 What JouleSpec decides
JouleSpec decides four questions and no others: **what does this software
owe**, **what is known about each obligation and how well**, **which of that
knowledge survives a change**, and **what did establishing it cost**.
It does not write specifications, does not run checks, does not formalize a
clause on anyone's behalf, and does not judge whether a clause states a good
requirement. Those belong to the author, the test runner, the prover, and the
reviewer respectively. JouleSpec is the part that holds their outputs together
and refuses to report them as interchangeable.
### 1.2 The specification is prose; the obligation is an address
A clause is a sentence a person wrote. That does not change under this
standard, and nothing here asks an author to write a requirement in a formal
notation.
What changes is that a clause carries two identifiers, not one:
- a **label** — `C1`, `auth-2` — which is editorial, exists so reports and
conversations have a short handle, and may be changed freely;
- an **obligation**, the content address of the clause's *kind and text*, with
the label deliberately excluded.
Evidence binds to the obligation. The consequence is the mechanism this
standard turns on:
> Renaming a clause moves no obligation and loses no evidence. Changing one
> word of a clause is a different obligation, and every piece of evidence bound
> to the old wording stops applying — with nobody having to notice.
Binding evidence to a label inverts both halves: it survives the edit that
changed the requirement and breaks on the edit that renamed it. Prevailing
practice binds to labels, or to nothing at all, which is why specification
drift is discussed as a discipline problem. It is an addressing problem, and an
address is the whole fix.
### 1.3 Conformance is a grade, not a claim
The prevailing artifact of specification-driven practice is prose an agent
interprets and a human reads, and its verdict is a sentence in a review. A
sentence cannot be compared across changes, cannot be capped by its weakest
part, and cannot distinguish "an agent said it was done" from "no input
violates it".
In `joulespec/1` every piece of evidence lands on a rung of an ordered ladder
(§2.5), and a specification's grade is the grade of its weakest owed obligation
(§3.2). An implementer's claim is a real rung — the bottom one — reported as
the testimony it is rather than as conformance.
### 1.4 A figure's grade is part of the verdict
Every energy figure carries how it was obtained: a counter read for this work,
a calibrated model, or a default guess. An aggregate is graded by its weakest
contributing figure, never by its best, and a receipt that summed one reading
and one guess reports a guess (§5.1). This is the same discipline the rest of
this family applies to every measured quantity it publishes.
---
## 2. Objects
### 2.1 The clause and its obligation
A clause is `{label, kind, text}` where `kind` is one of:
| Kind | Meaning |
|---|---|
| `requirement` | Behaviour the implementation must exhibit. |
| `constraint` | A bound it must stay inside — interface, dependency, style, energy. |
| `non-goal` | Behaviour deliberately out of scope. |
Non-goals are first-class. Writing down what a system must *not* do is half the
value of a design conversation, and a non-goal is checkable in a way the field
usually discards: evidence that something implements it is a finding (§3.3).
The **obligation** is the content address of the canonical document
`{kind, text}`. Text is compared exactly: an author who reflows a paragraph has
changed the obligation as far as this standard is concerned. That is the
conservative direction — evidence is dropped, never silently carried forward.
Because `kind` is addressed, moving a clause from `requirement` to `non-goal`
is a change of obligation. Deciding not to build something is not an editorial
act.
A conforming implementation MUST refuse a specification in which two clauses
state the same obligation. They would share an address, so evidence for one
would silently witness the other. The refusal MUST name both clauses: a report
naming only the second sends the author to the wrong place.
### 2.2 The specification
A specification is `{profile, alg, intent, clauses, parent?, author}`:
an intent in the author's words, ordered clauses with unique labels and
distinct obligations, an optional parent, and the author's key. Its address is
the content address of its canonical document; it is signed over
`spec_id ‖ parent`, so the signature binds a revision to its lineage.
A specification that does not validate has no address. There is no such thing
as the address of a document that does not hold together.
### 2.3 Revision
A revision names its predecessor by address, so the history of what was
approved is tamper-evident, and a reviser signs with their own key. A revision
is not required to be an improvement and the standard takes no position on who
may make one; it records who did.
### 2.4 The witness
A witness is one signed record binding four things:
| Field | Why it is bound |
|---|---|
| `spec` | Which specification this is about. |
| `obligation` | Which obligation — an address, never a label. |
| `artifact` | Which artifact state the evidence was observed against. |
| `evidence` | What was observed, with its full payload. |
| `energy` | What observing it cost, with provenance. |
The signature covers `witness_id ‖ spec ‖ obligation ‖ artifact`, so none of
those can be swapped under a valid signature.
The **artifact binding is what makes staleness computable.** Evidence gathered
against one state of the code says nothing about another, and a witness that
does not name the state it observed cannot be aged out. In practice this is a
tree digest, a build output address, or a commit — JouleSpec does not care
which, only that it is an address.
### 2.5 The evidence ladder
Every rung answers one question: **how hard would it be for this obligation to
be violated without this evidence noticing?**
| Grade | Name | What it establishes |
|---|---|---|
| `E0` | `cited` | The implementer claims it is done. Nothing noticed anything. |
| `E1` | `reviewed` | A second party read the artifact against the clause, once. |
| `E2` | `exercised` | A named check ran against the artifact and passed. |
| `E3` | `falsifiable` | The same check is on record *failing* against a named counter-artifact. |
| `E4` | `proved` | No input violates it, under a formalization someone independent checked. |
Three rungs need their reasons stated.
**`E0` is where prevailing practice lands.** A clause an agent claims to have
implemented, with a human skim at the end, is testimony from the party being
evaluated. Much of it is true. It is still testimony, and reporting it as
conformance is the error this standard exists to stop.
**`E2` flatters.** A green check establishes that something ran, not that the
obligation holds. A test with no assertion passes; so does one whose fixture
never reaches the condition the clause cares about. Nothing at `E2`
distinguishes those from a real check.
**`E3` is the first rung where green means something.** A check recorded as
failing against a named counter-artifact — a mutant, a reverted fix, a
deliberately broken build — has demonstrated that it *can* fail, so its passing
carries information. This is the cheapest strong rung, and the one most
projects can reach with tests they already have.
**The ladder is ordered by re-checkability, not by diligence.** A human review
is bound to the state a person looked at and cannot notice a violation
introduced afterwards; a named check can run again against any later state.
That ordering says nothing about how carefully anyone read the code — it says
that one kind of evidence survives the next commit and the other does not.
**Independence is computed, never declared.** A review signed by the key that
made the citation is the citation again, and grades `E0`. A conforming
implementation MUST compute this from key disjointness and MUST NOT read it
from a field the producer set.
### 2.6 Formalization, and what an `E4` claims
A proof discharges a formal statement. The statement is not the clause; the
binding between them is a human act, and it is the weakest link in any claim of
proved conformance. A conforming implementation MUST NOT formalize a clause on
its own initiative: a formalization is supplied, and it carries a grounding.
| Grounding | Reached grade |
|---|---|
| `cross-checked` by a key other than the witness author's | `E4` |
| `declared` | `E2` |
| `cross-checked` by the witness author's own key | `E2` |
A proof under a formalization nobody independent read is machine-checked
evidence about *something*; that the something is this clause is the prover's
word. It is therefore real mechanical evidence — `E2` — and not proof of the
clause. Demoting rather than refusing is deliberate: the proof is not fake, and
discarding it would lose evidence that exists.
This is the same boundary the family's verification standard draws from the
other side when it records that a proof is not a specification.
### 2.7 Work
Work is a unit of effort performed against a specification — a coding
operation, a build, a benchmark run — carrying its address, the obligations it
claims to advance, and its energy.
Work and witnesses are accounted separately. Work is what *cost* energy;
a witness is what *observed* something. Conflating them hides the question this
standard exists to answer, which is whether the evidence was worth what it
cost.
---
## 3. Evaluation
### 3.1 What counts
Evaluation takes a specification, one artifact state, the witnesses on record
and the work performed, and produces a verdict per clause plus a verdict for
the specification.
A witness contributes to a grade only when **all** of the following hold. Each
other outcome is reported, and none is discarded:
| Condition | Outcome when it fails |
|---|---|
| Its signature and structure verify | `unverifiable` flag, by input position |
| It names this specification | `foreign-spec` flag |
| It names an obligation this specification states | `unbound` flag |
| It was observed at this artifact state | recorded as **stale** on that clause |
A witness naming an obligation the specification does not state is the normal
aftermath of a rewording, and it MUST NOT be dropped quietly — it is the
clearest available signal that evidence and specification have parted company.
A clause's grade is the strongest grade among its contributing witnesses. Where
several witnesses tie, the one with the lowest address is named, so two
evaluations over the same inputs produce the same verdict.
### 3.2 The cap is a minimum
A specification's grade is the **minimum** over its requirements and
constraints, and is absent when any of them has no surviving evidence.
Averaging would let nine proved obligations conceal one nobody looked at, which
is precisely the failure a grade exists to surface. This family applies the
same rule wherever a composite claim meets a weak component.
### 3.3 Non-goals
Non-goals are excluded from the cap: there is nothing to evidence about
behaviour that should not exist, so an unwitnessed non-goal is correct and
carries no grade.
Evidence that a non-goal *was* implemented is a `non-goal-witnessed` flag. It
does not lower the grade of the owed obligations — they really are evidenced —
and it does block conformance (§3.4). The specification said not to build this
and something says it exists; no amount of evidence elsewhere settles that.
### 3.4 Flags block conformance
A flag is something wrong with the evidence set, as distinct from a weakly
evidenced obligation. An implementation reports conformance at a requested bar
only when every owed obligation reaches it **and** no flag is raised.
### 3.5 Determinism
Evaluation MUST be deterministic: the same specification, artifact, witnesses
and work in any order MUST produce an identical verdict. Collections in the
verdict are sorted, and ties are broken by address.
---
## 4. Drift
### 4.1 The partition
Diffing two revisions by obligation address yields an exact partition:
| Class | Meaning | Evidence |
|---|---|---|
| `kept` | Same obligation, same label | Survives |
| `relabelled` | Same obligation, new label | Survives |
| `reworded` | Same label, different obligation | Old evidence invalidated; new obligation unevidenced |
| `added` | Obligation only in the later revision | Unevidenced |
| `removed` | Obligation only in the earlier one | Orphaned |
From that, two sets follow directly: the obligations whose evidence no longer
applies, and the obligations that start the revision unevidenced. A revision
review wants those sets, not the fact that the specification changed.
### 4.2 Relabelling is free; rewording is not
A revision that changes only labels and ordering is **editorial** and costs no
evidence. Renumbering a whole specification lands there. This matters because a
standard that penalised renaming would teach authors not to rename, and
specifications whose labels cannot be tidied acquire labels nobody can read.
A revision that changes a clause's text costs exactly that clause's evidence,
and nothing else's.
A freed label reused by a new clause is an **addition**, not a rewording: the
old obligation still stands elsewhere in the specification, so no evidence is
invalidated. An implementation MUST distinguish these, because conflating them
would report a loss of evidence that did not happen.
### 4.3 Artifact drift
Specification drift is one of two directions. The other is the artifact moving
under evidence that stayed still, and it is handled at evaluation: a witness
bound to an earlier artifact state is reported stale rather than counted
(§3.1). Neither direction requires anyone to remember to check.
---
## 5. Cost
### 5.1 Work energy and evidence energy
A verdict accounts three figures in microjoules: energy spent on work citing at
least one of this specification's obligations, energy spent on work citing
none, and energy spent producing the surviving evidence. The provenance of the
total is the weakest provenance among every figure that fed it.
### 5.2 The specified share
The share of work energy that implements something the specification asked for
is reported in parts-per-million.
This is the measured form of an observation the field makes qualitatively —
that agents produce features nobody requested. A run in which a quarter of the
energy went into work citing no clause has a number saying so, and that number
is in the receipt.
Ratios are integers in parts-per-million because the canonical core admits no
floats (§6.2), and a ppm figure survives the round trip exactly where a rounded
percentage would not.
### 5.3 Microjoules per falsifiable obligation
The headline figure is total attributed energy divided by the number of owed
obligations evidenced at `E3` or better.
The denominator is deliberately the rung whose evidence can fail. A figure per
*attempted* obligation rewards ceremony; a figure per obligation that something
would actually have caught rewards evidence. The family's other per-unit
figures make the same choice, counting accepted output and verified proofs
rather than attempts.
Where nothing reaches the bar the figure is **absent**, not zero. A cost per
unit of nothing is not a small number.
This figure is what makes the field's central argument decidable. Whether
specification-driven practice earns its overhead is currently settled by
anecdote in both directions. Two processes applied to the same specification,
reaching the same evidence, with one spending an order of magnitude more energy
to get there, is not a matter of taste.
---
## 6. The receipt
### 6.1 Sealed from the inputs, never from a verdict
A conforming implementation MUST derive a receipt from the specification, the
artifact, the witnesses and the work — and MUST NOT seal a verdict handed to it.
A verdict is an ordinary data structure. An interface that signed one would let
a producer construct the grade it wanted and obtain a receipt saying so.
Because the receipt is derived from the evidence, any claim in it is
reproducible by anyone holding the same inputs, and a producer who wants a
better grade has to go and get better evidence.
### 6.2 Absent, not zero
Receipts seal as **JCR-1** envelopes — canonical det-CBOR (RFC 8949 §4.2.1)
inside a COSE_Sign1 envelope (RFC 9052, EdDSA/Ed25519) — the signed-receipt
format shared across this family, verifiable by tooling that has never heard of
JouleSpec.
That core admits no nulls, and several figures here are legitimately absent: a
specification with an unevidenced requirement has no grade; a run with no work
has no specified share. Such fields MUST be **omitted**, never sealed as zero. A
reader who finds no grade knows none was established; a reader who found `E0`
would believe one was.
### 6.3 The per-clause audit
A receipt carries one entry per clause — label, kind, obligation, grade where
established, the witness that carried it, evidence energy, and the count of
stale witnesses — alongside the specification-level summary.
Which obligation is the weak one is the actionable fact. It must be answerable
from the receipt alone, rather than by re-running an evaluation whose inputs
nobody kept.
---
## 7. Conformance
An implementation conforms to `joulespec/1` when:
1. **C1 — Addressing.** Obligation addresses are computed over `{kind, text}`
with the label excluded; relabelling preserves an address and rewording
changes it.
2. **C2 — Validation.** Specifications with a duplicate label, a duplicate
obligation, an empty intent, an empty clause, or no clauses are refused, and
a duplicate obligation names both clauses.
3. **C3 — Signatures.** Specifications and witnesses verify over the preimages
in §2.2 and §2.4, and altering any bound field invalidates the signature.
4. **C4 — The ladder.** Grades are assigned per §2.5 and §2.6, with
independence computed from key disjointness.
5. **C5 — Evaluation.** The weakest-owed-obligation cap, the non-goal rule, the
four flag conditions, staleness, and determinism under reordering all hold
per §3.
6. **C6 — Drift.** The §4.1 partition is computed by address, editorial
revisions invalidate nothing, and a reused label is distinguished from a
rewording.
7. **C7 — Cost.** Aggregates carry the weakest contributing provenance; ratios
are integer parts-per-million; absent figures are absent.
8. **C8 — Receipts.** Receipts are derived from inputs, seal as JCR-1, omit
absent fields, and carry the per-clause audit.
The reference implementation is conformant by construction. A third-party
implementation certifies itself by reproducing these behaviours; the reference
test suite in [`joulespec-rs/`](joulespec-rs/) is the executable form of the
list above.
---
## 8. The honest limit
**JouleSpec cannot tell whether a clause says what its author meant.** An
obligation is addressed, signed and tracked, and it may still be the wrong
requirement, badly worded, or quietly contradicted by another clause. This
standard measures evidence against stated obligations; it does not validate
obligations against intent, and no address makes a bad requirement good.
**A grade is a property of evidence, not of software.** `E4` across every clause
means no input violates the formalizations that were checked. Software can
satisfy every clause of its specification and be unfit for its purpose, because
the specification was incomplete — and an incomplete specification is exactly
the case where every clause is covered.
**Falsifiability is demonstrated, not guaranteed.** An `E3` witness records that
a check failed against one named counter-artifact. That establishes the check
can fail; it does not establish that the check would catch every way the
obligation could be broken.
**A specified share is not a quality measure.** Work citing no clause may be
necessary — a refactor, a build fix, a dependency bump. The figure says how
much energy went into work nobody asked for in this specification, which is a
question worth having an answer to, and not a verdict.
**Energy figures are as good as their meters.** A figure graded as a default
guess is a guess no matter how precisely it is reported, which is why the grade
travels with the number and why an aggregate never claims better than its
weakest part.
---
*JouleSpec is stewarded by [Transaction Science](https://transaction.science).
The protocol is owned by no one; the right to fork is public.*

1
joulespec/joulespec-rs/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

442
joulespec/joulespec-rs/Cargo.lock generated Normal file
View file

@ -0,0 +1,442 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "arrayvec"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "blake3"
version = "1.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
dependencies = [
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"cpufeatures 0.3.1",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cc"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"pkcs8",
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"rand_core",
"serde",
"sha2",
"subtle",
"zeroize",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "find-msvc-tools"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "joulespec-core"
version = "0.1.0"
dependencies = [
"blake3",
"ed25519-dalek",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "libc"
version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "serde_json"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "typenum"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-ident"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "zeroize"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"

View file

@ -0,0 +1,15 @@
[workspace]
resolver = "3"
members = ["crates/joulespec-core"]
[workspace.package]
edition = "2024"
license = "Apache-2.0"
repository = "https://git.transaction.science/Transaction-Science/open-standards"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
blake3 = "1"
ed25519-dalek = { version = "2", features = ["rand_core"] }
thiserror = "2"

View file

@ -0,0 +1,17 @@
[package]
name = "joulespec-core"
version = "0.1.0"
description = "JouleSpec — the reference conformance kernel for energy-accounted specifications (joulespec/1): obligation-addressed clauses, a graded evidence ladder from testimony to proof, deterministic drift, and the joules a specification cost to evidence."
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
blake3 = { workspace = true }
ed25519-dalek = { workspace = true }
thiserror = { workspace = true }
[lints.rust]
missing_docs = "warn"

View file

@ -0,0 +1,666 @@
//! Evaluation: the deterministic verdict over a specification, its evidence,
//! and the work done against it.
//!
//! ## What evaluation answers
//!
//! Given a specification, one artifact state, the witnesses on record, and the
//! work that was performed, evaluation answers four questions with no
//! judgement calls left in it:
//!
//! - **Per obligation** — what is the strongest evidence that survives at this
//! artifact state, and what did reaching it cost ([`ClauseVerdict`]).
//! - **For the specification** — the grade of its *weakest* owed obligation,
//! because a specification is no better evidenced than the requirement
//! nobody checked ([`Conformance::grade`]).
//! - **What is wrong with the evidence itself** — a non-goal somebody
//! implemented, a witness for an obligation this specification does not
//! state, a signature that does not verify ([`Flag`]). None of these are
//! dropped: a mis-bound witness is a finding, not a no-op.
//! - **What the energy bought** — how much went into work that cites an
//! obligation, and how much into work that cites none
//! ([`Conformance::specified_ppm`]).
//!
//! ## The cap, and why it is a minimum
//!
//! A specification's grade is the minimum over its requirements and
//! constraints. Averaging would let nine proved obligations hide one nobody
//! looked at, which is the failure the grade exists to surface. The family
//! applies the same rule wherever a composite claim meets a weak component —
//! a least-bound axis, a cross-axis gate, an aggregate graded by its worst
//! figure.
//!
//! Non-goals are excluded from the cap and handled as flags: there is nothing
//! to evidence about behaviour that should not exist, and evidence that it
//! *does* exist is a different kind of problem.
//!
//! ## Why the ratio is parts-per-million
//!
//! JCR-1's canonical core admits no floats, so every ratio this standard seals
//! is an integer. A fraction is reported in parts-per-million rather than as a
//! rounded percentage so it survives the round trip exactly.
//!
//! Normative: SPEC §3.1 (what counts), SPEC §3.2 (the cap), SPEC §3.3
//! (non-goals), SPEC §3.4 (flags), SPEC §3.5 (determinism), SPEC §5.1 and
//! SPEC §5.2 (cost).
use serde::{Deserialize, Serialize};
use crate::evidence::{Grade, SignedWitness};
use crate::spec::{ClauseKind, Obligation, SignedSpec};
use crate::{Address, Energy, Error, Provenance, worst_provenance};
/// One unit of work performed against a specification: a coding operation, a
/// build, a benchmark run.
///
/// Work is what *cost* energy; a witness is what *observed* something. The two
/// are accounted separately because conflating them hides the question this
/// standard exists to answer — whether the evidence was worth what it cost.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Work {
/// The work unit's address — an operation id, a build digest.
pub id: Address,
/// The obligations this work claims to advance. Empty means the work
/// implements nothing the specification asked for.
pub cites: Vec<Obligation>,
/// What the work cost.
pub energy: Energy,
}
impl Work {
/// A work unit.
pub fn new(id: Address, cites: Vec<Obligation>, energy: Energy) -> Self {
Self { id, cites, energy }
}
}
/// Something wrong with the evidence set, as distinct from a weakly evidenced
/// obligation.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "flag")]
pub enum Flag {
/// A non-goal has evidence that something implements it. The
/// specification says not to build this; a witness says it exists.
NonGoalWitnessed {
/// The non-goal's label, for the report.
label: String,
/// Its obligation address.
obligation: Obligation,
},
/// A witness names an obligation this specification does not state —
/// usually a clause that was reworded, sometimes one that was never
/// there. Never silently discarded.
Unbound {
/// The witness.
witness: Address,
/// The obligation it claims.
obligation: Obligation,
},
/// A witness is about a different specification.
ForeignSpec {
/// The witness.
witness: Address,
/// The specification it actually names.
spec_id: Address,
},
/// A witness failed structural validation or signature verification. Its
/// position in the input is reported because a witness that does not
/// verify has no trustworthy address.
Unverifiable {
/// Index in the witness slice as supplied.
index: u64,
},
}
/// The verdict for one clause.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClauseVerdict {
/// The clause's editorial label.
pub label: String,
/// Its obligation address.
pub obligation: Obligation,
/// Requirement, constraint, or non-goal.
pub kind: ClauseKind,
/// The strongest grade among witnesses that survive at this artifact
/// state. `None` when nothing witnesses it here.
pub grade: Option<Grade>,
/// The witness carrying that grade — lowest address among ties, so the
/// verdict is reproducible.
pub best: Option<Address>,
/// Witnesses for this obligation bound to some *other* artifact state.
/// Evidence that was true once and says nothing about now.
pub stale: Vec<Address>,
/// What the surviving evidence for this obligation cost to produce.
pub evidence_uj: u64,
/// The weakest provenance among those figures.
pub provenance: Option<Provenance>,
}
impl ClauseVerdict {
/// Whether this clause is owed and unmet at the requested bar.
#[must_use]
pub fn is_open(&self, min: Grade) -> bool {
self.kind.is_owed() && self.grade.is_none_or(|g| g < min)
}
}
/// The deterministic evaluation of a specification at one artifact state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Conformance {
/// The specification evaluated.
pub spec_id: Address,
/// The artifact state it was evaluated against.
pub artifact: Address,
/// One verdict per clause, in clause order.
pub clauses: Vec<ClauseVerdict>,
/// The grade of the weakest owed obligation — the specification's grade.
/// `None` when any requirement or constraint has no surviving evidence.
pub grade: Option<Grade>,
/// Problems with the evidence set itself, sorted.
pub flags: Vec<Flag>,
/// Energy spent on work that cites at least one of this specification's
/// obligations.
pub specified_uj: u64,
/// Energy spent on work that cites none of them.
pub unspecified_uj: u64,
/// Energy spent producing the surviving evidence.
pub evidence_uj: u64,
/// The weakest provenance across every figure contributing above.
pub provenance: Option<Provenance>,
}
impl Conformance {
/// Whether every owed obligation reaches `min` and the evidence set is
/// clean.
///
/// Flags block conformance regardless of grade: a specification whose
/// non-goal somebody implemented has a problem no amount of evidence
/// elsewhere settles.
#[must_use]
pub fn is_conformant(&self, min: Grade) -> bool {
self.flags.is_empty() && self.grade.is_some_and(|g| g >= min)
}
/// The owed obligations that do not reach `min`, in clause order.
#[must_use]
pub fn open(&self, min: Grade) -> Vec<&ClauseVerdict> {
self.clauses.iter().filter(|c| c.is_open(min)).collect()
}
/// The share of work energy that implements something this specification
/// asked for, in parts-per-million. `None` when no work was supplied.
///
/// This is the number that answers "how much of what the agent did was
/// asked for" — the measured form of the observation that agents produce
/// features nobody requested.
#[must_use]
pub fn specified_ppm(&self) -> Option<u64> {
let total = self.specified_uj.checked_add(self.unspecified_uj)?;
if total == 0 {
return None;
}
// u128 so a specified figure near u64::MAX cannot overflow the scale.
Some(((u128::from(self.specified_uj) * 1_000_000) / u128::from(total)) as u64)
}
/// Total energy attributed to this specification: the work plus the
/// evidence.
#[must_use]
pub fn total_uj(&self) -> u64 {
self.specified_uj
.saturating_add(self.unspecified_uj)
.saturating_add(self.evidence_uj)
}
/// Microjoules per owed obligation evidenced at `min` or better.
///
/// The headline figure takes `min = `[`Grade::Falsifiable`] — the first
/// rung whose evidence can fail — so the denominator counts obligations
/// something would actually have caught, the way this family's other
/// per-unit figures count accepted tokens and verified proofs rather than
/// attempts. `None` when nothing reaches the bar: a cost per unit of
/// nothing is not a small number, it is not a number.
#[must_use]
pub fn uj_per_evidenced_clause(&self, min: Grade) -> Option<u64> {
let n = self
.clauses
.iter()
.filter(|c| c.kind.is_owed() && c.grade.is_some_and(|g| g >= min))
.count();
if n == 0 {
return None;
}
Some(self.total_uj() / n as u64)
}
}
/// Evaluate a specification at one artifact state.
///
/// Every witness is verified here; none is taken on trust and none is
/// discarded quietly. A witness counts toward a grade only when it verifies,
/// names *this* specification, names an obligation this specification states,
/// and was observed at *this* artifact state. Each of the other outcomes is
/// reported: a different artifact makes it stale, anything else makes it a
/// flag.
pub fn evaluate(
spec: &SignedSpec,
artifact: &Address,
witnesses: &[SignedWitness],
work: &[Work],
) -> Result<Conformance, Error> {
let spec_id = spec.verify()?;
let obligations = spec.spec.obligations()?;
let mut verdicts: Vec<ClauseVerdict> = obligations
.iter()
.map(|(label, kind, obligation)| ClauseVerdict {
label: label.clone(),
obligation: obligation.clone(),
kind: *kind,
grade: None,
best: None,
stale: Vec::new(),
evidence_uj: 0,
provenance: None,
})
.collect();
let index: std::collections::BTreeMap<&Obligation, usize> =
obligations.iter().enumerate().map(|(i, (_, _, o))| (o, i)).collect();
let mut flags = Vec::new();
let mut evidence_provenances = Vec::new();
for (i, signed) in witnesses.iter().enumerate() {
let Ok(witness_id) = signed.verify() else {
flags.push(Flag::Unverifiable { index: i as u64 });
continue;
};
let w = &signed.witness;
if w.spec_id != spec_id {
flags.push(Flag::ForeignSpec { witness: witness_id, spec_id: w.spec_id.clone() });
continue;
}
let Some(&slot) = index.get(&w.obligation) else {
flags.push(Flag::Unbound {
witness: witness_id,
obligation: w.obligation.clone(),
});
continue;
};
if &w.artifact != artifact {
verdicts[slot].stale.push(witness_id);
continue;
}
let verdict = &mut verdicts[slot];
if verdict.kind == ClauseKind::NonGoal {
// Evidence that a non-goal was implemented is not coverage, and
// the energy that produced it is not evidence energy either.
flags.push(Flag::NonGoalWitnessed {
label: verdict.label.clone(),
obligation: verdict.obligation.clone(),
});
continue;
}
let grade = w.grade();
// Strongest grade wins; the lowest address breaks a tie so two runs
// over the same inputs name the same witness.
let replace = match (verdict.grade, &verdict.best) {
(None, _) => true,
(Some(current), Some(best)) => {
grade > current || (grade == current && &witness_id < best)
}
(Some(current), None) => grade > current,
};
if replace {
verdict.grade = Some(grade);
verdict.best = Some(witness_id);
}
verdict.evidence_uj = verdict.evidence_uj.saturating_add(w.energy.uj);
verdict.provenance = Some(match verdict.provenance {
Some(p) => p.min(w.energy.provenance),
None => w.energy.provenance,
});
evidence_provenances.push(w.energy.provenance);
}
for verdict in &mut verdicts {
verdict.stale.sort();
}
flags.sort();
let grade = verdicts
.iter()
.filter(|c| c.kind.is_owed())
.map(|c| c.grade)
.min()
.unwrap_or(None);
let mut specified_uj = 0u64;
let mut unspecified_uj = 0u64;
let mut work_provenances = Vec::new();
for unit in work {
let specified = unit.cites.iter().any(|o| index.contains_key(o));
if specified {
specified_uj = specified_uj.saturating_add(unit.energy.uj);
} else {
unspecified_uj = unspecified_uj.saturating_add(unit.energy.uj);
}
work_provenances.push(unit.energy.provenance);
}
let evidence_uj = verdicts.iter().map(|c| c.evidence_uj).fold(0u64, u64::saturating_add);
let provenance =
worst_provenance(evidence_provenances.into_iter().chain(work_provenances));
Ok(Conformance {
spec_id,
artifact: artifact.clone(),
clauses: verdicts,
grade,
flags,
specified_uj,
unspecified_uj,
evidence_uj,
provenance,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::evidence::{Evidence, Witness};
use crate::spec::fixtures::{key, spec};
use ed25519_dalek::SigningKey;
fn artifact() -> Address {
Address::of_bytes(b"artifact-v1")
}
fn witness_for(
spec: &SignedSpec,
label: &str,
artifact: &Address,
evidence: Evidence,
uj: u64,
author: &SigningKey,
) -> SignedWitness {
let obligation = spec
.spec
.clauses
.iter()
.find(|c| c.label == label)
.expect("label exists")
.obligation()
.unwrap();
Witness::new(
spec.spec.spec_id().unwrap(),
obligation,
artifact.clone(),
evidence,
Energy::new(uj, Provenance::HwShunt),
author.verifying_key().to_bytes(),
)
.sign(author)
.unwrap()
}
fn falsifiable(check: &str) -> Evidence {
Evidence::Falsifiable {
check: check.into(),
counter: Address::of_bytes(b"mutant"),
}
}
#[test]
fn a_specification_is_graded_by_its_weakest_owed_obligation() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let ws = vec![
witness_for(&s, "C1", &a, falsifiable("rejects_anon"), 900, &k),
witness_for(&s, "C2", &a, Evidence::Cited, 10, &k),
];
let c = evaluate(&s, &a, &ws, &[]).unwrap();
assert_eq!(c.grade, Some(Grade::Cited), "one cited clause caps the whole specification");
assert_eq!(c.clauses[0].grade, Some(Grade::Falsifiable));
assert_eq!(c.clauses[1].grade, Some(Grade::Cited));
assert!(!c.is_conformant(Grade::Exercised));
assert!(c.is_conformant(Grade::Cited));
assert_eq!(c.open(Grade::Exercised).len(), 1);
assert_eq!(c.open(Grade::Exercised)[0].label, "C2");
}
#[test]
fn an_unwitnessed_requirement_leaves_the_specification_ungraded() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let ws = vec![witness_for(&s, "C1", &a, falsifiable("rejects_anon"), 900, &k)];
let c = evaluate(&s, &a, &ws, &[]).unwrap();
assert_eq!(c.grade, None, "C2 has no evidence at all");
assert!(!c.is_conformant(Grade::Cited));
assert_eq!(c.open(Grade::Cited).len(), 1);
}
#[test]
fn a_non_goal_never_counts_as_coverage_and_never_blocks_the_grade() {
let k = key(1);
let s = spec(&k);
let a = artifact();
// C3 is the non-goal. Nothing witnesses it, and that is correct.
let ws = vec![
witness_for(&s, "C1", &a, falsifiable("rejects_anon"), 900, &k),
witness_for(&s, "C2", &a, falsifiable("decided_first"), 800, &k),
];
let c = evaluate(&s, &a, &ws, &[]).unwrap();
assert_eq!(c.grade, Some(Grade::Falsifiable));
assert!(c.flags.is_empty());
assert!(c.is_conformant(Grade::Falsifiable));
}
#[test]
fn evidence_that_a_non_goal_was_built_is_a_flag_that_blocks_conformance() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let ws = vec![
witness_for(&s, "C1", &a, falsifiable("rejects_anon"), 900, &k),
witness_for(&s, "C2", &a, falsifiable("decided_first"), 800, &k),
witness_for(&s, "C3", &a, Evidence::Exercised { check: "retry_backoff".into() }, 50, &k),
];
let c = evaluate(&s, &a, &ws, &[]).unwrap();
assert_eq!(c.grade, Some(Grade::Falsifiable), "the owed clauses are still well evidenced");
assert!(
matches!(c.flags.as_slice(), [Flag::NonGoalWitnessed { label, .. }] if label == "C3")
);
assert!(
!c.is_conformant(Grade::Falsifiable),
"a flag blocks conformance however good the rest of the evidence is"
);
}
#[test]
fn evidence_from_another_artifact_state_is_stale_not_counted() {
let k = key(1);
let s = spec(&k);
let old = Address::of_bytes(b"artifact-v0");
let new = artifact();
let ws = vec![
witness_for(&s, "C1", &old, falsifiable("rejects_anon"), 900, &k),
witness_for(&s, "C2", &new, falsifiable("decided_first"), 800, &k),
];
let c = evaluate(&s, &new, &ws, &[]).unwrap();
assert_eq!(c.clauses[0].grade, None);
assert_eq!(c.clauses[0].stale.len(), 1, "reported, not dropped");
assert_eq!(c.clauses[0].evidence_uj, 0, "stale evidence costs nothing here");
assert_eq!(c.grade, None);
}
#[test]
fn a_mis_bound_witness_is_a_finding_not_a_no_op() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let stray = Witness::new(
s.spec.spec_id().unwrap(),
crate::spec::Clause::requirement("C9", "A clause this specification never stated.")
.obligation()
.unwrap(),
a.clone(),
Evidence::Cited,
Energy::estimated(5),
k.verifying_key().to_bytes(),
)
.sign(&k)
.unwrap();
let c = evaluate(&s, &a, &[stray], &[]).unwrap();
assert!(matches!(c.flags.as_slice(), [Flag::Unbound { .. }]));
}
#[test]
fn a_witness_for_another_specification_is_flagged() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let mut foreign = witness_for(&s, "C1", &a, Evidence::Cited, 5, &k);
foreign.witness.spec_id = Address::of_bytes(b"some-other-spec");
let foreign = foreign.witness.clone().sign(&k).unwrap();
let c = evaluate(&s, &a, &[foreign], &[]).unwrap();
assert!(matches!(c.flags.as_slice(), [Flag::ForeignSpec { .. }]));
}
#[test]
fn an_unverifiable_witness_is_reported_by_position() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let mut tampered = witness_for(&s, "C1", &a, Evidence::Cited, 5, &k);
tampered.sig = vec![0u8; 64];
let c = evaluate(&s, &a, &[tampered], &[]).unwrap();
assert_eq!(c.flags, vec![Flag::Unverifiable { index: 0 }]);
}
#[test]
fn the_strongest_surviving_witness_sets_the_grade() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let ws = vec![
witness_for(&s, "C1", &a, Evidence::Cited, 5, &k),
witness_for(&s, "C1", &a, falsifiable("rejects_anon"), 900, &k),
witness_for(&s, "C2", &a, falsifiable("decided_first"), 800, &k),
];
let c = evaluate(&s, &a, &ws, &[]).unwrap();
assert_eq!(c.clauses[0].grade, Some(Grade::Falsifiable));
assert_eq!(c.clauses[0].evidence_uj, 905, "every surviving witness cost something");
}
#[test]
fn work_citing_nothing_is_counted_against_the_specified_share() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let c1 = s.spec.clauses[0].obligation().unwrap();
let work = vec![
Work::new(Address::of_bytes(b"op1"), vec![c1], Energy::new(750_000, Provenance::HwShunt)),
Work::new(Address::of_bytes(b"op2"), vec![], Energy::new(250_000, Provenance::HwShunt)),
];
let c = evaluate(&s, &a, &[], &work).unwrap();
assert_eq!(c.specified_uj, 750_000);
assert_eq!(c.unspecified_uj, 250_000);
assert_eq!(c.specified_ppm(), Some(750_000), "three quarters of the energy was asked for");
}
#[test]
fn work_citing_an_obligation_this_specification_lacks_is_unspecified() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let elsewhere = crate::spec::Clause::requirement("X", "Something else.")
.obligation()
.unwrap();
let work = vec![Work::new(
Address::of_bytes(b"op1"),
vec![elsewhere],
Energy::estimated(1_000),
)];
let c = evaluate(&s, &a, &[], &work).unwrap();
assert_eq!(c.specified_uj, 0);
assert_eq!(c.unspecified_uj, 1_000);
assert_eq!(c.specified_ppm(), Some(0));
}
#[test]
fn a_ratio_with_no_work_is_absent_rather_than_zero() {
let k = key(1);
let s = spec(&k);
let c = evaluate(&s, &artifact(), &[], &[]).unwrap();
assert_eq!(c.specified_ppm(), None, "no work is not zero specified work");
assert_eq!(c.uj_per_evidenced_clause(Grade::Falsifiable), None);
}
#[test]
fn the_wedge_figure_counts_only_obligations_whose_evidence_can_fail() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let ws = vec![
witness_for(&s, "C1", &a, falsifiable("rejects_anon"), 1_000, &k),
witness_for(&s, "C2", &a, Evidence::Exercised { check: "smoke".into() }, 1_000, &k),
];
let work =
vec![Work::new(Address::of_bytes(b"op"), vec![s.spec.clauses[0].obligation().unwrap()],
Energy::new(98_000, Provenance::HwShunt))];
let c = evaluate(&s, &a, &ws, &work).unwrap();
assert_eq!(c.total_uj(), 100_000);
assert_eq!(
c.uj_per_evidenced_clause(Grade::Falsifiable),
Some(100_000),
"one obligation reaches the bar, so the whole cost is attributed to it"
);
assert_eq!(
c.uj_per_evidenced_clause(Grade::Exercised),
Some(50_000),
"at a lower bar two obligations qualify"
);
}
#[test]
fn an_aggregate_is_graded_by_the_weakest_figure_that_fed_it() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let measured = witness_for(&s, "C1", &a, falsifiable("rejects_anon"), 900, &k);
let mut guessed = witness_for(&s, "C2", &a, falsifiable("decided_first"), 800, &k);
guessed.witness.energy = Energy::estimated(800);
let guessed = guessed.witness.clone().sign(&k).unwrap();
let c = evaluate(&s, &a, &[measured, guessed], &[]).unwrap();
assert_eq!(c.provenance, Some(Provenance::Estimator));
assert_eq!(c.clauses[0].provenance, Some(Provenance::HwShunt));
assert_eq!(c.clauses[1].provenance, Some(Provenance::Estimator));
}
#[test]
fn evaluation_is_deterministic_under_witness_reordering() {
let k = key(1);
let s = spec(&k);
let a = artifact();
let mut ws = vec![
witness_for(&s, "C1", &a, falsifiable("rejects_anon"), 900, &k),
witness_for(&s, "C1", &a, falsifiable("rejects_anon_too"), 900, &k),
witness_for(&s, "C2", &a, Evidence::Cited, 1, &k),
];
let first = evaluate(&s, &a, &ws, &[]).unwrap();
ws.reverse();
let second = evaluate(&s, &a, &ws, &[]).unwrap();
assert_eq!(first, second, "ties break on address, not on input order");
}
}

View file

@ -0,0 +1,333 @@
//! Drift: which evidence survives a revision, computed rather than warned
//! about.
//!
//! Specification drift is the first gap the field's own literature names, and
//! it is usually described as a discipline problem — someone forgot to update
//! the spec, someone edited the spec and forgot the code. It is really an
//! addressing problem. When evidence binds to a clause *label*, an editor can
//! change what a requirement says and every citation still points at it; the
//! evidence now attests to a requirement that no longer exists, and nothing in
//! the system can tell.
//!
//! Because evidence here binds to the obligation address ([`crate::spec`]),
//! revising a specification yields an exact partition:
//!
//! - **kept** — the obligation is in both revisions. Its evidence still
//! applies.
//! - **relabelled** — the same obligation under a new label. Editorial; no
//! evidence is lost. Renumbering a whole specification lands here.
//! - **reworded** — the same label, a different obligation. The evidence for
//! what it used to say no longer applies to what it now says, and the new
//! obligation starts unevidenced. This is the case that hides under
//! label-bound evidence.
//! - **added** / **removed** — clauses that appeared or disappeared outright.
//!
//! Drift against the *artifact* is the other half, and it is handled at
//! evaluation: a witness is bound to the artifact state it observed, so
//! evidence from an earlier state is reported stale rather than counted
//! ([`crate::conform`]).
//!
//! Normative: SPEC §4.1 (the partition), SPEC §4.2 (relabelling versus
//! rewording), SPEC §4.3 (artifact drift).
use serde::{Deserialize, Serialize};
use crate::spec::{Obligation, SignedSpec};
use crate::{Address, Error};
/// A clause whose label survived a revision while its obligation changed.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Reword {
/// The label both revisions use.
pub label: String,
/// What it used to oblige.
pub was: Obligation,
/// What it obliges now.
pub now: Obligation,
}
/// An obligation that moved from one label to another without changing.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct Relabel {
/// The label in the earlier revision.
pub was: String,
/// The label in the later one.
pub now: String,
/// The obligation, unchanged.
pub obligation: Obligation,
}
/// The exact difference between two revisions of a specification.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Drift {
/// The earlier revision.
pub from: Address,
/// The later revision.
pub to: Address,
/// Whether `to` names `from` as its parent. A diff between two
/// unrelated specifications is still computable and still useful; it is
/// just not a revision.
pub chained: bool,
/// Obligations present in both revisions under the same label.
pub kept: Vec<Obligation>,
/// Obligations carried over under a different label.
pub relabelled: Vec<Relabel>,
/// Labels carried over with a changed obligation.
pub reworded: Vec<Reword>,
/// Obligations that appear only in the later revision.
pub added: Vec<(String, Obligation)>,
/// Obligations that appear only in the earlier one.
pub removed: Vec<(String, Obligation)>,
}
impl Drift {
/// The obligations whose evidence no longer applies: everything removed,
/// plus the old side of every rewording.
///
/// This is the number a revision review wants — not "the spec changed"
/// but "these four obligations just lost their evidence".
#[must_use]
pub fn invalidated(&self) -> Vec<Obligation> {
let mut out: Vec<Obligation> = self
.removed
.iter()
.map(|(_, o)| o.clone())
.chain(self.reworded.iter().map(|r| r.was.clone()))
.collect();
out.sort();
out
}
/// The obligations that start the revision unevidenced: everything added,
/// plus the new side of every rewording.
#[must_use]
pub fn unevidenced(&self) -> Vec<Obligation> {
let mut out: Vec<Obligation> = self
.added
.iter()
.map(|(_, o)| o.clone())
.chain(self.reworded.iter().map(|r| r.now.clone()))
.collect();
out.sort();
out
}
/// Whether this revision changed no obligation — labels and ordering only.
/// An editorial revision costs nothing in evidence, and saying so is the
/// point: a specification should be renumberable without penalty.
#[must_use]
pub fn is_editorial(&self) -> bool {
self.added.is_empty() && self.removed.is_empty() && self.reworded.is_empty()
}
}
/// Diff two revisions by obligation address.
pub fn diff(from: &SignedSpec, to: &SignedSpec) -> Result<Drift, Error> {
let from_id = from.verify()?;
let to_id = to.verify()?;
// label -> obligation, and obligation -> label, on both sides. Both
// directions are needed: one finds rewording, the other finds relabelling.
let mut from_by_label = std::collections::BTreeMap::new();
let mut from_by_obligation = std::collections::BTreeMap::new();
for (label, _, obligation) in from.spec.obligations()? {
from_by_label.insert(label.clone(), obligation.clone());
from_by_obligation.insert(obligation, label);
}
let mut to_by_label = std::collections::BTreeMap::new();
let mut to_by_obligation = std::collections::BTreeMap::new();
for (label, _, obligation) in to.spec.obligations()? {
to_by_label.insert(label.clone(), obligation.clone());
to_by_obligation.insert(obligation, label);
}
let mut kept = Vec::new();
let mut relabelled = Vec::new();
let mut reworded = Vec::new();
let mut added = Vec::new();
let mut removed = Vec::new();
for (obligation, to_label) in &to_by_obligation {
match from_by_obligation.get(obligation) {
Some(from_label) if from_label == to_label => kept.push(obligation.clone()),
Some(from_label) => relabelled.push(Relabel {
was: from_label.clone(),
now: to_label.clone(),
obligation: obligation.clone(),
}),
None => {
// New obligation. If its label existed before, the clause was
// reworded rather than added.
match from_by_label.get(to_label) {
Some(was) if !to_by_obligation.contains_key(was) => reworded.push(Reword {
label: to_label.clone(),
was: was.clone(),
now: obligation.clone(),
}),
// The label existed, but its old obligation still lives
// under some other label — so this is a genuinely new
// clause that reused a freed label, not a rewording.
_ => added.push((to_label.clone(), obligation.clone())),
}
}
}
}
let reworded_old: std::collections::BTreeSet<&Obligation> =
reworded.iter().map(|r| &r.was).collect();
for (obligation, from_label) in &from_by_obligation {
if !to_by_obligation.contains_key(obligation) && !reworded_old.contains(obligation) {
removed.push((from_label.clone(), obligation.clone()));
}
}
kept.sort();
relabelled.sort();
reworded.sort();
added.sort();
removed.sort();
Ok(Drift {
chained: to.spec.parent.as_ref() == Some(&from_id),
from: from_id,
to: to_id,
kept,
relabelled,
reworded,
added,
removed,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec::{Clause, Spec, fixtures::key};
/// Revise `spec` by applying `edit` to its clauses, signed by the same
/// author.
fn revise(
base: &SignedSpec,
edit: impl FnOnce(&mut Vec<Clause>),
) -> SignedSpec {
let author = key(1);
let mut next = base.spec.revise(author.verifying_key().to_bytes()).unwrap();
edit(&mut next.clauses);
next.sign(&author).unwrap()
}
fn base() -> SignedSpec {
crate::spec::fixtures::spec(&key(1))
}
#[test]
fn renumbering_a_specification_costs_no_evidence() {
let from = base();
let to = revise(&from, |clauses| {
for (i, clause) in clauses.iter_mut().enumerate() {
clause.label = format!("R{}", i + 1);
}
});
let d = diff(&from, &to).unwrap();
assert!(d.is_editorial(), "{d:?}");
assert_eq!(d.relabelled.len(), 3);
assert!(d.kept.is_empty(), "every clause moved label");
assert!(d.invalidated().is_empty(), "renaming must never cost evidence");
assert!(d.chained);
}
#[test]
fn rewording_one_clause_invalidates_exactly_that_clauses_evidence() {
let from = base();
let to = revise(&from, |clauses| {
clauses[0].text = "The endpoint should reject unauthenticated requests.".into();
});
let d = diff(&from, &to).unwrap();
assert!(!d.is_editorial());
assert_eq!(d.reworded.len(), 1);
assert_eq!(d.reworded[0].label, "C1");
assert_eq!(d.kept.len(), 2, "the other two obligations are untouched");
assert_eq!(d.invalidated().len(), 1, "one obligation lost its evidence");
assert_eq!(d.unevidenced().len(), 1);
assert!(d.added.is_empty(), "a reworded clause is not an added one");
assert!(d.removed.is_empty(), "nor a removed one");
}
#[test]
fn a_weakened_requirement_is_not_mistaken_for_the_one_it_replaced() {
// The case the standard exists for: same label, same intent, one word
// changed. Under label-bound evidence this revision is invisible.
let from = base();
let to = revise(&from, |clauses| {
clauses[0].text = "The endpoint rejects most unauthenticated requests.".into();
});
let d = diff(&from, &to).unwrap();
assert_eq!(d.reworded.len(), 1);
assert_ne!(d.reworded[0].was, d.reworded[0].now);
}
#[test]
fn adding_and_removing_clauses_is_reported_as_such() {
let from = base();
let to = revise(&from, |clauses| {
clauses.remove(1);
clauses.push(Clause::requirement("C4", "Every decision seals a receipt."));
});
let d = diff(&from, &to).unwrap();
assert_eq!(d.added.len(), 1);
assert_eq!(d.added[0].0, "C4");
assert_eq!(d.removed.len(), 1);
assert_eq!(d.removed[0].0, "C2");
assert_eq!(d.invalidated().len(), 1);
}
#[test]
fn reusing_a_freed_label_is_an_addition_not_a_rewording() {
// C2's obligation survives under a new label, and the freed "C2" is
// given to a brand-new clause. Nothing was reworded: no evidence is
// invalidated, because every old obligation still stands.
let from = base();
let to = revise(&from, |clauses| {
clauses[1].label = "decide-first".into();
clauses.push(Clause::requirement("C2", "Every decision seals a receipt."));
});
let d = diff(&from, &to).unwrap();
assert!(d.reworded.is_empty(), "{d:?}");
assert_eq!(d.relabelled.len(), 1);
assert_eq!(d.added.len(), 1);
assert_eq!(d.added[0].0, "C2");
assert!(d.invalidated().is_empty(), "no obligation left the specification");
}
#[test]
fn an_unchanged_revision_keeps_everything() {
let from = base();
let to = revise(&from, |_| {});
let d = diff(&from, &to).unwrap();
assert!(d.is_editorial());
assert_eq!(d.kept.len(), 3);
assert!(d.relabelled.is_empty());
}
#[test]
fn a_diff_between_unrelated_specifications_is_computable_but_unchained() {
let author = key(1);
let from = base();
let unrelated = Spec::new(
"Something else entirely.",
vec![Clause::requirement("A1", "A different obligation.")],
author.verifying_key().to_bytes(),
)
.sign(&author)
.unwrap();
let d = diff(&from, &unrelated).unwrap();
assert!(!d.chained, "no parent link, and the diff says so");
assert_eq!(d.added.len(), 1);
assert_eq!(d.removed.len(), 3);
}
}

View file

@ -0,0 +1,569 @@
//! The evidence ladder: what is known about an obligation, and how well.
//!
//! ## The question the ladder asks
//!
//! Every rung is ordered by one question: **how hard would it be for this
//! obligation to be violated without this evidence noticing?**
//!
//! | Grade | Name | Noticing |
//! |---|---|---|
//! | `E0` | [`Grade::Cited`] | Not at all — the implementer said it was done. |
//! | `E1` | [`Grade::Reviewed`] | A second party read the artifact once. |
//! | `E2` | [`Grade::Exercised`] | A named check ran against the artifact and passed. |
//! | `E3` | [`Grade::Falsifiable`] | The same check is on record failing when the obligation is broken. |
//! | `E4` | [`Grade::Proved`] | No input violates it, under a formalization someone independent checked. |
//!
//! Three of those rungs are worth their own paragraph.
//!
//! **`E0` is where the state of the art lands.** A specification clause an
//! agent claims to have implemented, with a human skim at the end, is
//! testimony. It is not worthless — it is the most common evidence in software
//! and much of it is true — but it is a statement by the party being
//! evaluated, and reporting it as "conformant" is the error this standard
//! exists to stop.
//!
//! **`E2` is the rung that flatters.** A green check is evidence that
//! something ran, not that the obligation holds: a test with no assertion
//! passes, and so does a test whose fixture avoids the condition the clause
//! cares about. Nothing at `E2` distinguishes those from a real check.
//!
//! **`E3` is the first rung where green means something.** A check recorded as
//! *failing* against a named counter-artifact — a mutant, a reverted fix, a
//! deliberately broken build — has demonstrated that it can fail, so its
//! passing on the real artifact carries information. This is the cheapest
//! strong rung and the one most projects can reach today.
//!
//! ## Why a machine check outranks a human review
//!
//! The ladder is ordered by *re-checkability*, not by diligence. A review is
//! bound to the artifact state a person looked at and cannot notice a
//! violation introduced afterwards; a named check can be run again against any
//! later state. That ordering says nothing about how carefully anyone read the
//! code — it says that one of the two kinds of evidence survives the next
//! commit and the other does not.
//!
//! ## Independence is computed, never declared
//!
//! A review signed by the same key that made the citation is the citation
//! again, so it grades `E0`. A formalization "cross-checked" by its own author
//! is a declared formalization, so its proof grades `E2`. Neither is refused
//! and neither is accepted at face value: independence is key disjointness,
//! and it is computed here rather than read off a flag the producer set.
//!
//! Normative: SPEC §2.4 (the witness), SPEC §2.5 (the ladder), SPEC §2.6
//! (formalization).
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::spec::Obligation;
use crate::{ALG_ED25519, Address, Energy, Error};
/// The profile a witness declares.
pub const WITNESS_PROFILE: &str = "joulespec-witness/1";
/// How well an obligation is known. Ordered: `Cited < Reviewed < Exercised <
/// Falsifiable < Proved`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Grade {
/// `E0` — the implementer claims the obligation is met.
Cited,
/// `E1` — a party other than the claimant read the artifact.
Reviewed,
/// `E2` — a named check ran against the artifact and passed.
Exercised,
/// `E3` — the same check is on record failing against a counter-artifact,
/// so its passing carries information.
Falsifiable,
/// `E4` — discharged as a proof obligation under an independently
/// cross-checked formalization.
Proved,
}
impl Grade {
/// The short wire form (`"E0"` … `"E4"`).
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::Cited => "E0",
Self::Reviewed => "E1",
Self::Exercised => "E2",
Self::Falsifiable => "E3",
Self::Proved => "E4",
}
}
/// Whether evidence at this grade can fail when the obligation is broken.
/// Everything below [`Grade::Falsifiable`] is compatible with a check that
/// cannot fail, or with no check at all.
#[must_use]
pub const fn is_falsifiable(self) -> bool {
matches!(self, Self::Falsifiable | Self::Proved)
}
}
/// How a clause's text was turned into a formal statement.
///
/// This crate never formalizes a clause. A formalization is supplied by
/// whoever made the proof attempt, and the binding between the clause's prose
/// and the statement that was proved is the weakest link in any `E4` claim —
/// so the grade depends on whether anyone else checked it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Formalization {
/// The formal statement that was discharged.
pub statement: String,
/// Who, if anyone, independently confirmed the statement says what the
/// clause says.
pub grounding: Grounding,
}
/// Whether a formalization was checked by a second party.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Grounding {
/// Asserted by the party that produced the proof. The proof is real; that
/// it proves *this clause* is that party's word.
Declared,
/// Confirmed against the clause text by the named key.
CrossChecked {
/// The confirming party's Ed25519 verifying key.
by: [u8; 32],
},
}
/// What is known about an obligation, and by what means.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum Evidence {
/// The implementer claims this artifact meets the obligation.
Cited,
/// A second party read the artifact against the clause.
Reviewed {
/// The reviewing party's Ed25519 verifying key.
reviewer: [u8; 32],
},
/// A named check ran against the artifact and passed.
Exercised {
/// How the check is invoked — a test name, a lint, a conformance
/// vector id. Recorded so a reader can run it again.
check: String,
},
/// A named check passed against the artifact and is on record failing
/// against a counter-artifact.
Falsifiable {
/// The check, as in [`Evidence::Exercised`].
check: String,
/// The artifact state against which the same check failed — a mutant,
/// a reverted fix, a deliberately broken build.
counter: Address,
},
/// A proof obligation was discharged, bound to the clause by a
/// formalization.
Proved {
/// The discharged goal — a `proof/1` address where that standard
/// produced it.
goal: Address,
/// The tier that closed it, as the prover reported it.
tier: String,
/// The binding from clause prose to formal statement.
formalization: Formalization,
},
}
impl Evidence {
/// The grade this evidence reaches, given the key that signed for it.
///
/// Independence is computed here: a review or a cross-check by the
/// witness's own author establishes nothing beyond the citation, and
/// demotes rather than refuses.
#[must_use]
pub fn grade(&self, author: &[u8; 32]) -> Grade {
match self {
Self::Cited => Grade::Cited,
Self::Reviewed { reviewer } => {
if reviewer == author { Grade::Cited } else { Grade::Reviewed }
}
Self::Exercised { .. } => Grade::Exercised,
Self::Falsifiable { .. } => Grade::Falsifiable,
Self::Proved { formalization, .. } => match &formalization.grounding {
Grounding::CrossChecked { by } if by != author => Grade::Proved,
// A proof under a formalization nobody independent read is a
// machine-checked statement of an unverified reading of the
// clause: real, mechanical evidence about *something*.
_ => Grade::Exercised,
},
}
}
/// Structural validation of the payload.
pub fn validate(&self) -> Result<(), Error> {
match self {
Self::Cited | Self::Reviewed { .. } => Ok(()),
Self::Exercised { check } | Self::Falsifiable { check, .. } => {
if check.trim().is_empty() {
return Err(Error::IncompleteEvidence("a check must be named"));
}
Ok(())
}
Self::Proved { tier, formalization, .. } => {
if tier.trim().is_empty() {
return Err(Error::IncompleteEvidence("a proof must name its tier"));
}
if formalization.statement.trim().is_empty() {
return Err(Error::IncompleteEvidence(
"a proof must carry the statement it discharged",
));
}
Ok(())
}
}
}
/// The canonical document this evidence contributes to a witness's
/// address. Every payload field is present, so no part of the evidence can
/// be altered under a valid signature.
#[must_use]
pub fn canonical(&self) -> Value {
match self {
Self::Cited => json!({ "kind": self.label() }),
Self::Reviewed { reviewer } => {
json!({ "kind": self.label(), "reviewer": crate::hex32(reviewer) })
}
Self::Exercised { check } => json!({ "kind": self.label(), "check": check }),
Self::Falsifiable { check, counter } => json!({
"kind": self.label(),
"check": check,
"counter": counter.as_str(),
}),
Self::Proved { goal, tier, formalization } => {
let mut grounding = serde_json::Map::new();
match &formalization.grounding {
Grounding::Declared => {
grounding.insert("kind".into(), json!("declared"));
}
Grounding::CrossChecked { by } => {
grounding.insert("kind".into(), json!("cross-checked"));
grounding.insert("by".into(), json!(crate::hex32(by)));
}
}
json!({
"kind": self.label(),
"goal": goal.as_str(),
"tier": tier,
"statement": formalization.statement,
"grounding": Value::Object(grounding),
})
}
}
}
/// The wire label of the evidence kind.
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::Cited => "cited",
Self::Reviewed { .. } => "reviewed",
Self::Exercised { .. } => "exercised",
Self::Falsifiable { .. } => "falsifiable",
Self::Proved { .. } => "proved",
}
}
}
/// One signed record of evidence about one obligation at one artifact state.
///
/// A witness binds four things together: *which* obligation (by address, not
/// label), *which* artifact state it was observed against, *what* was
/// observed, and *what it cost* to observe. The artifact binding is what makes
/// staleness computable — evidence gathered against one state says nothing
/// about another.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Witness {
/// The profile (`joulespec-witness/1`).
pub profile: String,
/// Signature algorithm id (`0x01` = Ed25519).
pub alg: u8,
/// The specification this witness is about.
pub spec_id: Address,
/// The obligation witnessed — an address, so a relabelled clause keeps
/// its evidence and a reworded one does not.
pub obligation: Obligation,
/// The artifact state the evidence was observed against: a tree digest, a
/// build output address, a commit.
pub artifact: Address,
/// What was observed.
pub evidence: Evidence,
/// What producing this evidence cost, with its provenance.
pub energy: Energy,
/// The witnessing party's Ed25519 verifying key.
pub author: [u8; 32],
}
impl Witness {
/// A witness at an artifact state.
pub fn new(
spec_id: Address,
obligation: Obligation,
artifact: Address,
evidence: Evidence,
energy: Energy,
author: [u8; 32],
) -> Self {
Self {
profile: WITNESS_PROFILE.to_string(),
alg: ALG_ED25519,
spec_id,
obligation,
artifact,
evidence,
energy,
author,
}
}
/// Structural validation.
pub fn validate(&self) -> Result<(), Error> {
if self.profile != WITNESS_PROFILE {
return Err(Error::BadProfile(self.profile.clone()));
}
if self.alg != ALG_ED25519 {
return Err(Error::BadAlg(self.alg));
}
self.evidence.validate()
}
/// The canonical document the address and the signature commit to.
/// Written out for the same reasons as [`crate::spec::Spec::canonical`].
#[must_use]
pub fn canonical(&self) -> Value {
json!({
"profile": self.profile,
"alg": self.alg,
"spec": self.spec_id.as_str(),
"obligation": self.obligation.as_str(),
"artifact": self.artifact.as_str(),
"evidence": self.evidence.canonical(),
"energy": json!({
"uj": self.energy.uj,
"provenance": self.energy.provenance.label(),
}),
"author": crate::hex32(&self.author),
})
}
/// The witness's address.
pub fn witness_id(&self) -> Result<Address, Error> {
self.validate()?;
Address::of(&self.canonical())
}
/// The grade this witness reaches.
pub fn grade(&self) -> Grade {
self.evidence.grade(&self.author)
}
/// Preimage: `witness_id ‖ spec_id ‖ obligation ‖ artifact`. Signing binds
/// the evidence to the obligation and the state it was observed against,
/// so neither can be swapped under a valid signature.
fn preimage(&self) -> Result<Vec<u8>, Error> {
let id = self.witness_id()?;
let mut pre = id.as_str().as_bytes().to_vec();
pre.extend_from_slice(self.spec_id.as_str().as_bytes());
pre.extend_from_slice(self.obligation.as_str().as_bytes());
pre.extend_from_slice(self.artifact.as_str().as_bytes());
Ok(pre)
}
/// Sign with the witnessing party's key.
pub fn sign(self, key: &SigningKey) -> Result<SignedWitness, Error> {
let sig = key.sign(&self.preimage()?);
Ok(SignedWitness { witness: self, sig: sig.to_bytes().to_vec() })
}
}
/// A witness plus its signature.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedWitness {
/// The witness.
pub witness: Witness,
/// Ed25519 signature over `witness_id ‖ spec_id ‖ obligation ‖ artifact`.
pub sig: Vec<u8>,
}
impl SignedWitness {
/// Verify structure and signature, returning the witness address.
pub fn verify(&self) -> Result<Address, Error> {
let pre = self.witness.preimage()?;
let key = VerifyingKey::from_bytes(&self.witness.author).map_err(|_| Error::BadSignature)?;
let sig: [u8; 64] = self.sig.as_slice().try_into().map_err(|_| Error::BadSignature)?;
key.verify(&pre, &Signature::from_bytes(&sig)).map_err(|_| Error::BadSignature)?;
self.witness.witness_id()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Provenance;
use crate::spec::{Clause, fixtures::key};
fn obligation() -> Obligation {
Clause::requirement("C1", "The endpoint rejects unauthenticated requests.")
.obligation()
.unwrap()
}
fn witness(evidence: Evidence, author: &SigningKey) -> SignedWitness {
Witness::new(
Address::of_bytes(b"spec"),
obligation(),
Address::of_bytes(b"artifact-v1"),
evidence,
Energy::new(1_200, Provenance::HwShunt),
author.verifying_key().to_bytes(),
)
.sign(author)
.unwrap()
}
#[test]
fn the_ladder_is_ordered_from_testimony_to_proof() {
assert!(Grade::Cited < Grade::Reviewed);
assert!(Grade::Reviewed < Grade::Exercised);
assert!(Grade::Exercised < Grade::Falsifiable);
assert!(Grade::Falsifiable < Grade::Proved);
assert!(!Grade::Exercised.is_falsifiable(), "a green check may be vacuous");
assert!(Grade::Falsifiable.is_falsifiable());
}
#[test]
fn a_citation_is_testimony() {
let k = key(1);
let w = witness(Evidence::Cited, &k);
assert_eq!(w.witness.grade(), Grade::Cited);
w.verify().expect("verifies");
}
#[test]
fn a_review_by_someone_else_outranks_a_citation() {
let author = key(1);
let reviewer = key(2);
let w = witness(Evidence::Reviewed { reviewer: reviewer.verifying_key().to_bytes() }, &author);
assert_eq!(w.witness.grade(), Grade::Reviewed);
}
#[test]
fn a_self_review_is_the_citation_again() {
let author = key(1);
let w = witness(Evidence::Reviewed { reviewer: author.verifying_key().to_bytes() }, &author);
assert_eq!(
w.witness.grade(),
Grade::Cited,
"independence is key disjointness, computed here rather than claimed"
);
}
#[test]
fn a_check_that_can_fail_outranks_one_that_merely_passed() {
let k = key(1);
let passed = witness(Evidence::Exercised { check: "rejects_anon".into() }, &k);
let can_fail = witness(
Evidence::Falsifiable {
check: "rejects_anon".into(),
counter: Address::of_bytes(b"mutant-auth-removed"),
},
&k,
);
assert_eq!(passed.witness.grade(), Grade::Exercised);
assert_eq!(can_fail.witness.grade(), Grade::Falsifiable);
}
#[test]
fn a_proof_reaches_e4_only_when_its_formalization_was_cross_checked() {
let author = key(1);
let other = key(2);
let proved = |grounding| {
witness(
Evidence::Proved {
goal: Address::of_bytes(b"goal"),
tier: "T1".into(),
formalization: Formalization {
statement: "forall r. !authenticated(r) -> rejected(r)".into(),
grounding,
},
},
&author,
)
};
assert_eq!(
proved(Grounding::CrossChecked { by: other.verifying_key().to_bytes() })
.witness
.grade(),
Grade::Proved
);
assert_eq!(
proved(Grounding::Declared).witness.grade(),
Grade::Exercised,
"the proof is real; that it proves THIS clause is the prover's word"
);
assert_eq!(
proved(Grounding::CrossChecked { by: author.verifying_key().to_bytes() })
.witness
.grade(),
Grade::Exercised,
"cross-checking your own formalization is declaring it"
);
}
#[test]
fn swapping_the_obligation_under_a_signature_is_caught() {
let k = key(1);
let mut w = witness(Evidence::Cited, &k);
w.witness.obligation = Clause::requirement("C1", "Something else entirely.")
.obligation()
.unwrap();
assert!(matches!(w.verify(), Err(Error::BadSignature)));
}
#[test]
fn swapping_the_artifact_under_a_signature_is_caught() {
let k = key(1);
let mut w = witness(Evidence::Cited, &k);
w.witness.artifact = Address::of_bytes(b"artifact-v2");
assert!(matches!(w.verify(), Err(Error::BadSignature)));
}
#[test]
fn evidence_without_its_payload_is_refused() {
let k = key(1);
let w = Witness::new(
Address::of_bytes(b"spec"),
obligation(),
Address::of_bytes(b"a"),
Evidence::Exercised { check: " ".into() },
Energy::estimated(1),
k.verifying_key().to_bytes(),
);
assert!(matches!(w.validate(), Err(Error::IncompleteEvidence(_))));
let no_statement = Witness::new(
Address::of_bytes(b"spec"),
obligation(),
Address::of_bytes(b"a"),
Evidence::Proved {
goal: Address::of_bytes(b"g"),
tier: "T1".into(),
formalization: Formalization {
statement: String::new(),
grounding: Grounding::Declared,
},
},
Energy::estimated(1),
k.verifying_key().to_bytes(),
);
assert!(matches!(no_statement.validate(), Err(Error::IncompleteEvidence(_))));
}
}

View file

@ -0,0 +1,510 @@
//! JCR-1 wire primitives — canonical det-CBOR + COSE_Sign1.
//!
//! JouleSpec receipts seal with the **JouleClaw Receipt Specification (JCR-1)**
//! envelope, the signed-receipt wire format shared across the Transaction
//! Science family: canonical det-CBOR (RFC 8949 §4.2.1, length-first key order,
//! no floats/bools/nulls) inside a COSE_Sign1 envelope (RFC 9052, EdDSA/Ed25519,
//! `alg = -8`). JCR-1 is a published spec implementable with standard COSE/CBOR
//! tooling and no JouleSpec dependency; these primitives are that reference
//! algorithm, vendored so the JouleSpec reference library stays self-contained — the
//! same arrangement `mesh-receipt` documents for the same reason. They are
//! byte-compatible with JCR-1's cross-language (Python/Go) verifiers and with
//! the other family standards' receipts, which is the point of vendoring rather
//! than re-deriving: a JouleSpec receipt is verifiable by a tool that has never
//! heard of JouleSpec.
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde_json::Value;
/// Errors from JCR-1 canonical encoding or envelope handling.
#[derive(Debug, thiserror::Error)]
pub enum Jcr1Error {
/// A type not permitted in the canonical signed core (float, bool, null,
/// non-uint number).
#[error("disallowed type in canonical core: {0}")]
DisallowedType(&'static str),
/// The COSE_Sign1 envelope was malformed.
#[error("malformed COSE_Sign1 envelope: {0}")]
Envelope(String),
/// An unsupported COSE algorithm (only EdDSA `-8` is accepted).
#[error("unsupported COSE alg: {0} (only EdDSA -8)")]
UnsupportedAlg(i64),
/// The signature did not verify.
#[error("COSE_Sign1 signature did not verify")]
BadSignature,
/// The envelope's `kid` did not match the expected steward.
#[error("kid mismatch: expected {expected}, got {got}")]
KidMismatch {
/// The steward the verifier required.
expected: String,
/// The `kid` the envelope actually carried.
got: String,
},
/// The canonical core did not decode, or decoded to something that does
/// not re-encode to the bytes it came from.
#[error("malformed canonical core: {0}")]
MalformedCore(&'static str),
}
/// COSE algorithm tag for EdDSA (RFC 8152). The only `alg` JCR-1 v1 accepts.
pub const COSE_ALG_EDDSA: i64 = -8;
fn cbor_uint(out: &mut Vec<u8>, major: u8, n: u64) {
let mt = major << 5;
if n < 24 {
out.push(mt | n as u8);
} else if n < 1 << 8 {
out.push(mt | 24);
out.push(n as u8);
} else if n < 1 << 16 {
out.push(mt | 25);
out.extend_from_slice(&(n as u16).to_be_bytes());
} else if n < 1 << 32 {
out.push(mt | 26);
out.extend_from_slice(&(n as u32).to_be_bytes());
} else {
out.push(mt | 27);
out.extend_from_slice(&n.to_be_bytes());
}
}
fn cbor_text(out: &mut Vec<u8>, s: &str) {
cbor_uint(out, 3, s.len() as u64);
out.extend_from_slice(s.as_bytes());
}
fn cbor_bytes(out: &mut Vec<u8>, b: &[u8]) {
cbor_uint(out, 2, b.len() as u64);
out.extend_from_slice(b);
}
fn cbor_value(out: &mut Vec<u8>, v: &Value) -> Result<(), Jcr1Error> {
match v {
Value::String(s) => {
cbor_text(out, s);
Ok(())
}
Value::Number(n) => {
let u = n.as_u64().ok_or(Jcr1Error::DisallowedType("non-uint number"))?;
cbor_uint(out, 0, u);
Ok(())
}
Value::Array(a) => {
cbor_uint(out, 4, a.len() as u64);
for e in a {
cbor_value(out, e)?;
}
Ok(())
}
Value::Object(m) => {
// length-first, then bytewise on the key (RFC 8949 §4.2.1).
let mut keys: Vec<&String> = m.keys().collect();
keys.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.as_str().cmp(b.as_str())));
cbor_uint(out, 5, keys.len() as u64);
for k in keys {
cbor_text(out, k);
cbor_value(out, &m[k])?;
}
Ok(())
}
Value::Bool(_) => Err(Jcr1Error::DisallowedType("bool")),
Value::Null => Err(Jcr1Error::DisallowedType("null")),
}
}
/// Encode a JSON value as canonical det-CBOR (RFC 8949 §4.2.1): length-first key
/// ordering, shortest integer form, no floats/bools/nulls.
pub fn canonical_cbor(value: &Value) -> Result<Vec<u8>, Jcr1Error> {
let mut out = Vec::new();
cbor_value(&mut out, value)?;
Ok(out)
}
/// The canonical signed-core bytes for a JCR-1 wire document: drop null-valued
/// fields, then canonical-CBOR encode. This is exactly what the envelope signs
/// over.
pub fn signed_core_bytes(wire: &Value) -> Result<Vec<u8>, Jcr1Error> {
let obj = wire
.as_object()
.ok_or(Jcr1Error::DisallowedType("non-object root"))?;
let mut core = serde_json::Map::new();
for (k, v) in obj {
if v.is_null() {
continue;
}
core.insert(k.clone(), v.clone());
}
canonical_cbor(&Value::Object(core))
}
// COSE_Sign1 (RFC 9052), EdDSA / Ed25519 only.
fn build_protected_header(steward: &str) -> Vec<u8> {
// map(2) { 1: -8 (EdDSA), 4: bstr(steward) }
let mut p = vec![0xa2, 0x01, 0x27, 0x04];
cbor_bytes(&mut p, steward.as_bytes());
p
}
fn build_sig_structure(protected: &[u8], payload: &[u8]) -> Vec<u8> {
// ["Signature1", protected, external_aad (empty), payload]
let mut s = vec![0x84];
cbor_text(&mut s, "Signature1");
cbor_bytes(&mut s, protected);
cbor_bytes(&mut s, &[]);
cbor_bytes(&mut s, payload);
s
}
/// Build a tagged COSE_Sign1 envelope (CBOR tag 18) over the canonical signed
/// core. `steward` becomes the `kid` a verifier resolves.
pub fn cose_sign1_envelope(core: &[u8], signing_key: &SigningKey, steward: &str) -> Vec<u8> {
let protected = build_protected_header(steward);
let sig_struct = build_sig_structure(&protected, core);
let signature = signing_key.sign(&sig_struct).to_bytes();
let mut envelope = vec![0xd2, 0x84]; // tag(18), array(4)
cbor_bytes(&mut envelope, &protected);
envelope.push(0xa0); // unprotected = {}
cbor_bytes(&mut envelope, core);
cbor_bytes(&mut envelope, &signature);
envelope
}
struct ProtectedHeader {
alg: i64,
kid: Vec<u8>,
}
/// Verify a tagged COSE_Sign1 envelope against a public key, returning the inner
/// payload (the signed-core bytes) on success. Rejects unknown `alg`, a `kid`
/// other than `expected_steward`, and signature failure.
pub fn verify_cose_sign1(
envelope: &[u8],
expected_steward: &str,
pubkey: &[u8; 32],
) -> Result<Vec<u8>, Jcr1Error> {
let (protected, payload, signature) = parse_cose_sign1(envelope)?;
let header = parse_protected_header(&protected)?;
if header.alg != COSE_ALG_EDDSA {
return Err(Jcr1Error::UnsupportedAlg(header.alg));
}
let kid = std::str::from_utf8(&header.kid)
.map_err(|_| Jcr1Error::Envelope("kid is not UTF-8".into()))?;
if kid != expected_steward {
return Err(Jcr1Error::KidMismatch {
expected: expected_steward.to_string(),
got: kid.to_string(),
});
}
let sig_struct = build_sig_structure(&protected, &payload);
let vk = VerifyingKey::from_bytes(pubkey)
.map_err(|e| Jcr1Error::Envelope(format!("bad pubkey: {e}")))?;
let sig = Signature::from_slice(&signature)
.map_err(|e| Jcr1Error::Envelope(format!("bad signature shape: {e}")))?;
vk.verify(&sig_struct, &sig).map_err(|_| Jcr1Error::BadSignature)?;
Ok(payload)
}
// Minimal CBOR reader for the fixed COSE_Sign1 shape.
fn read_len(b: &[u8], pos: &mut usize, major: u8) -> Result<u64, Jcr1Error> {
let head = *b.get(*pos).ok_or(Jcr1Error::Envelope("truncated".into()))?;
*pos += 1;
if head >> 5 != major {
return Err(Jcr1Error::Envelope(format!(
"expected major {major}, got {}",
head >> 5
)));
}
let info = head & 0x1f;
let n = match info {
0..=23 => info as u64,
24 => {
let v = *b.get(*pos).ok_or(Jcr1Error::Envelope("truncated".into()))? as u64;
*pos += 1;
v
}
25 => {
let s = b.get(*pos..*pos + 2).ok_or(Jcr1Error::Envelope("truncated".into()))?;
*pos += 2;
u16::from_be_bytes([s[0], s[1]]) as u64
}
26 => {
let s = b.get(*pos..*pos + 4).ok_or(Jcr1Error::Envelope("truncated".into()))?;
*pos += 4;
u32::from_be_bytes([s[0], s[1], s[2], s[3]]) as u64
}
27 => {
let s = b.get(*pos..*pos + 8).ok_or(Jcr1Error::Envelope("truncated".into()))?;
*pos += 8;
u64::from_be_bytes(s.try_into().expect("8 bytes"))
}
_ => return Err(Jcr1Error::Envelope("indefinite length".into())),
};
Ok(n)
}
fn read_bstr(b: &[u8], pos: &mut usize) -> Result<Vec<u8>, Jcr1Error> {
let len = read_len(b, pos, 2)? as usize;
let s = b
.get(*pos..*pos + len)
.ok_or(Jcr1Error::Envelope("truncated bstr".into()))?;
*pos += len;
Ok(s.to_vec())
}
/// (protected header bytes, payload bytes, signature bytes) of a COSE_Sign1.
type CoseSign1Parts = (Vec<u8>, Vec<u8>, Vec<u8>);
fn parse_cose_sign1(envelope: &[u8]) -> Result<CoseSign1Parts, Jcr1Error> {
let mut pos = 0;
// tag(18) = 0xd2
if envelope.first() != Some(&0xd2) {
return Err(Jcr1Error::Envelope("missing COSE_Sign1 tag 18".into()));
}
pos += 1;
// array(4) = 0x84
if envelope.get(pos) != Some(&0x84) {
return Err(Jcr1Error::Envelope("expected array(4)".into()));
}
pos += 1;
let protected = read_bstr(envelope, &mut pos)?;
// unprotected map — expect empty map 0xa0
if envelope.get(pos) != Some(&0xa0) {
return Err(Jcr1Error::Envelope("expected empty unprotected map".into()));
}
pos += 1;
let payload = read_bstr(envelope, &mut pos)?;
let signature = read_bstr(envelope, &mut pos)?;
Ok((protected, payload, signature))
}
fn parse_protected_header(protected: &[u8]) -> Result<ProtectedHeader, Jcr1Error> {
let mut pos = 0;
let n = read_len(protected, &mut pos, 5)?; // map
let mut alg = None;
let mut kid = None;
for _ in 0..n {
let key = read_len(protected, &mut pos, 0)?; // small uint key
match key {
1 => {
// alg: negative int (major 1) → value = -1 - n
let head = *protected
.get(pos)
.ok_or(Jcr1Error::Envelope("truncated alg".into()))?;
if head >> 5 != 1 {
return Err(Jcr1Error::Envelope("alg not negative int".into()));
}
let v = read_len(protected, &mut pos, 1)?;
alg = Some(-1 - v as i64);
}
4 => kid = Some(read_bstr(protected, &mut pos)?),
_ => return Err(Jcr1Error::Envelope("unexpected protected header key".into())),
}
}
Ok(ProtectedHeader {
alg: alg.ok_or(Jcr1Error::Envelope("missing alg".into()))?,
kid: kid.ok_or(Jcr1Error::Envelope("missing kid".into()))?,
})
}
// ─────────────────────── canonical decoding ───────────────────────
//
// The encoder above admits exactly four types in a signed core: unsigned
// integers, text strings, arrays and maps (byte strings appear only in the
// COSE envelope around it). The decoder therefore accepts exactly those and
// refuses the rest, rather than accepting a wider grammar and hoping the
// encoder never meets it.
//
// Canonicity is then checked by construction: the decoded value is re-encoded
// and compared with the input. A core that decodes but does not re-encode to
// its own bytes is not canonical, and since the signature covers those exact
// bytes, accepting it would let two different documents share one signature.
fn read_head(b: &[u8], i: &mut usize) -> Result<(u8, u64), Jcr1Error> {
let ib = *b.get(*i).ok_or(Jcr1Error::MalformedCore("truncated head"))?;
*i += 1;
let major = ib >> 5;
let ai = ib & 0x1f;
let take = |i: &mut usize, n: usize| -> Result<u64, Jcr1Error> {
let end = i.checked_add(n).ok_or(Jcr1Error::MalformedCore("length overflow"))?;
let slice = b.get(*i..end).ok_or(Jcr1Error::MalformedCore("truncated argument"))?;
*i = end;
Ok(slice.iter().fold(0u64, |acc, byte| (acc << 8) | u64::from(*byte)))
};
let val = match ai {
0..=23 => u64::from(ai),
24 => take(i, 1)?,
25 => take(i, 2)?,
26 => take(i, 4)?,
27 => take(i, 8)?,
_ => return Err(Jcr1Error::MalformedCore("indefinite or reserved length")),
};
Ok((major, val))
}
fn read_value(b: &[u8], i: &mut usize) -> Result<Value, Jcr1Error> {
let (major, val) = read_head(b, i)?;
match major {
0 => Ok(Value::from(val)),
3 => {
let n = usize::try_from(val).map_err(|_| Jcr1Error::MalformedCore("huge string"))?;
let end = i.checked_add(n).ok_or(Jcr1Error::MalformedCore("length overflow"))?;
let raw = b.get(*i..end).ok_or(Jcr1Error::MalformedCore("truncated string"))?;
*i = end;
let s = core::str::from_utf8(raw)
.map_err(|_| Jcr1Error::MalformedCore("text is not UTF-8"))?;
Ok(Value::from(s))
}
4 => {
let n = usize::try_from(val).map_err(|_| Jcr1Error::MalformedCore("huge array"))?;
let mut out = Vec::with_capacity(n.min(1024));
for _ in 0..n {
out.push(read_value(b, i)?);
}
Ok(Value::Array(out))
}
5 => {
let n = usize::try_from(val).map_err(|_| Jcr1Error::MalformedCore("huge map"))?;
let mut m = serde_json::Map::new();
for _ in 0..n {
let Value::String(k) = read_value(b, i)? else {
return Err(Jcr1Error::MalformedCore("non-text map key"));
};
if m.insert(k, read_value(b, i)?).is_some() {
return Err(Jcr1Error::MalformedCore("duplicate map key"));
}
}
Ok(Value::Object(m))
}
_ => Err(Jcr1Error::MalformedCore("type not permitted in a signed core")),
}
}
/// Decode a canonical signed core back to JSON.
///
/// The inverse of [`canonical_cbor`] over the grammar a signed core may use.
/// The result is checked by re-encoding: a core that decodes but does not
/// re-encode to the bytes it came from is refused, because the signature
/// covers those exact bytes and a second encoding of the same value would
/// otherwise ride the same signature.
///
/// # Errors
/// [`Jcr1Error::MalformedCore`] if the bytes are truncated, use a type the
/// core grammar excludes, carry trailing data, or are not canonical.
pub fn from_canonical_cbor(bytes: &[u8]) -> Result<Value, Jcr1Error> {
let mut i = 0usize;
let v = read_value(bytes, &mut i)?;
if i != bytes.len() {
return Err(Jcr1Error::MalformedCore("trailing bytes after the core"));
}
if canonical_cbor(&v)? != bytes {
return Err(Jcr1Error::MalformedCore("core is not canonical"));
}
Ok(v)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
// ── the decoder (written here, not vendored, so tested here) ──
#[test]
fn the_decoder_round_trips_a_core() {
let v = json!({"a": 1, "bb": "x", "ccc": [1, 2], "d": {"e": 9}, "": "empty key"});
let bytes = canonical_cbor(&v).expect("encodes");
assert_eq!(from_canonical_cbor(&bytes).expect("decodes"), v);
}
/// A value that decodes but was not written canonically is refused: the
/// signature covers exact bytes, so a second encoding of the same value
/// must not ride the same signature.
#[test]
fn a_non_shortest_integer_is_not_canonical() {
// 5 the long way: major 0, additional-info 24, then the byte.
assert!(from_canonical_cbor(&[0x18, 0x05]).is_err());
// The same value, shortest form.
assert_eq!(from_canonical_cbor(&[0x05]).expect("decodes"), json!(5));
}
/// Length-first, then bytewise (RFC 8949 §4.2.1). `a` must precede `bb`.
#[test]
fn keys_out_of_canonical_order_are_refused() {
let good = canonical_cbor(&json!({"a": 2, "bb": 1})).expect("encodes");
assert!(from_canonical_cbor(&good).is_ok());
// The same map written "bb" first.
let bad = [0xa2, 0x62, b'b', b'b', 0x01, 0x61, b'a', 0x02];
assert!(from_canonical_cbor(&bad).is_err());
}
#[test]
fn trailing_bytes_are_refused() {
let mut b = canonical_cbor(&json!(1)).expect("encodes");
b.push(0x01);
assert!(
matches!(from_canonical_cbor(&b), Err(Jcr1Error::MalformedCore(m)) if m.contains("trailing")),
"refused for the trailing bytes specifically, not merely refused"
);
}
#[test]
fn a_type_outside_the_core_grammar_is_refused() {
assert!(from_canonical_cbor(&[0xf5]).is_err(), "true (major 7)");
assert!(from_canonical_cbor(&[0xf6]).is_err(), "null (major 7)");
assert!(from_canonical_cbor(&[0x20]).is_err(), "-1 (major 1)");
assert!(from_canonical_cbor(&[0x41, 0x00]).is_err(), "a byte string (major 2)");
}
#[test]
fn a_truncated_core_is_refused_rather_than_guessed() {
let full = canonical_cbor(&json!({"a": "xyz"})).expect("encodes");
for n in 1..full.len() {
assert!(from_canonical_cbor(&full[..n]).is_err(), "prefix of {n} bytes");
}
assert!(from_canonical_cbor(&full).is_ok());
}
#[test]
fn a_duplicate_map_key_is_refused() {
// {"a": 1, "a": 2} — well-formed CBOR, not a well-formed core.
let bad = [0xa2, 0x61, b'a', 0x01, 0x61, b'a', 0x02];
assert!(
matches!(from_canonical_cbor(&bad), Err(Jcr1Error::MalformedCore(m)) if m.contains("duplicate")),
"refused as a duplicate key specifically, not merely refused"
);
}
/// Every signed core this crate produces decodes back to what it encoded.
#[test]
fn a_signed_core_decodes_to_the_document_it_sealed() {
let wire = json!({"joulespec_receipt": "joulespec/1", "seq": 0, "prev": "00", "steward": "did:x"});
let core = signed_core_bytes(&wire).expect("canonical");
assert_eq!(from_canonical_cbor(&core).expect("decodes"), wire);
}
#[test]
fn envelope_roundtrip() {
let sk = SigningKey::from_bytes(&[7u8; 32]);
let steward = "did:example:endpoint1";
let core = canonical_cbor(&json!({"a": 1, "bb": "x"})).unwrap();
let env = cose_sign1_envelope(&core, &sk, steward);
let payload = verify_cose_sign1(&env, steward, &sk.verifying_key().to_bytes()).unwrap();
assert_eq!(payload, core);
}
#[test]
fn envelope_rejects_wrong_steward() {
let sk = SigningKey::from_bytes(&[7u8; 32]);
let core = canonical_cbor(&json!({"a": 1})).unwrap();
let env = cose_sign1_envelope(&core, &sk, "did:example:a");
assert!(verify_cose_sign1(&env, "did:example:b", &sk.verifying_key().to_bytes()).is_err());
}
#[test]
fn bool_is_rejected_in_core() {
// The signed core forbids bools — fidelity_met et al. must be uints.
assert!(canonical_cbor(&json!({"x": true})).is_err());
}
}

View file

@ -0,0 +1,302 @@
//! JouleSpec — the reference conformance kernel for energy-accounted
//! specifications (`joulespec/1`).
//!
//! ## What the field built, and what it left out
//!
//! Spec-driven development converged on a shared shape: write the intent
//! down, derive the plan and the implementation from it, review at each
//! phase. The artifact every framework in the field produces is **prose an
//! agent interprets and a human reads**, and the field's own literature names
//! the consequences — specifications drifting from the code they describe,
//! generated artifacts trusted beyond their evidence, no method for checking
//! that an implementation conforms, and a review burden heavy enough that
//! practitioners report preferring to read the code.
//!
//! Each of those is a measurement problem wearing a methodology costume. They
//! persist because the specification is a *document*: a clause cannot be
//! addressed, so nothing can bind to it; conformance is a sentence in a
//! review, so it cannot be graded; and the cost of the ceremony is an
//! anecdote, so the argument about whether it is worth the effort has no
//! units.
//!
//! ## What this crate makes true
//!
//! `joulespec/1` keeps the specification exactly as prose — a clause is still
//! a sentence a person wrote — and makes four things computable about it:
//!
//! 1. **A clause is addressed by its obligation, not its label.** The address
//! of a clause covers its kind and its text and *not* its label
//! ([`spec::Clause::obligation`]), so renaming `C1` to `auth-2` keeps every
//! piece of evidence bound to it, while changing one word of the
//! requirement invalidates that evidence. This is the distinction that lets
//! drift be computed instead of warned about — evidence bound to a label
//! survives a silent rewording, which is precisely how drift hides.
//!
//! 2. **Conformance is a grade, not a claim.** Evidence climbs a five-rung
//! ladder ([`evidence::Grade`]) ordered by one question: *how hard would it
//! be for this obligation to be violated without this evidence noticing?*
//! An implementer's citation — what the state of the art produces — is the
//! bottom rung, `E0`, and is reported as the testimony it is.
//!
//! 3. **Drift is a diff over addresses.** Revising a specification yields an
//! exact answer to "which evidence still applies" ([`drift::diff`]):
//! relabelling costs nothing, rewording costs that clause's evidence, and
//! the surviving set is computed rather than asserted.
//!
//! 4. **The ceremony has a price.** Every witness carries the energy it cost
//! to produce, so a sealed receipt states what a specification cost to
//! evidence, and what share of the energy spent went into work no clause
//! asked for ([`conform::Conformance::specified_fraction`]). The field's
//! central argument — whether spec-driven development earns its overhead —
//! becomes a measurement.
//!
//! ## What it does not do
//!
//! JouleSpec does not write specifications, does not run checks, does not
//! judge whether a clause is a *good* requirement, and never formalizes a
//! clause on anyone's behalf. It records which evidence exists, at what grade,
//! bound to which obligation, at what cost — and refuses to report testimony
//! and proof as the same thing.
//!
//! Prior art inside this family: `joule-code-spec` first shipped the signed,
//! content-addressed clause and the citation that binds a coding operation to
//! it. Its citations bind by clause **label** and its audit has a single
//! notion of coverage — a clause is covered when some operation claims it.
//! That is the `E0` rung here, and binding by label is the drift hole §3
//! closes.
//!
//! Normative: SPEC §1.1 (scope), SPEC §1.2 (addressing), SPEC §1.3
//! (grading), SPEC §1.4 (figure grades), SPEC §8 (the honest limit).
#![forbid(unsafe_code)]
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub mod conform;
pub mod drift;
pub mod evidence;
pub mod jcr1;
pub mod receipt;
pub mod spec;
/// Errors from specification handling, evidence verification, or evaluation.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The specification carries no intent.
#[error("intent must be non-empty")]
EmptyIntent,
/// The specification carries no clauses.
#[error("a specification must contain at least one clause")]
NoClauses,
/// Two clauses share a label.
#[error("clause labels must be unique: duplicate {0:?}")]
DuplicateLabel(String),
/// A label is empty or contains whitespace — it appears verbatim in
/// reports and must be a single token.
#[error("clause label must be non-empty and whitespace-free: {0:?}")]
BadLabel(String),
/// A clause has no text, so it states no obligation.
#[error("clause text must be non-empty: {0:?}")]
EmptyClause(String),
/// Two clauses state the same obligation at the same kind. They would
/// share an address, so evidence for one would silently witness the other.
#[error("clauses {0:?} and {1:?} state the same obligation")]
DuplicateObligation(String, String),
/// A signature did not verify.
#[error("signature verification failed")]
BadSignature,
/// An algorithm id this profile does not define.
#[error("unsupported algorithm id {0:#04x}")]
BadAlg(u8),
/// A witness names a profile this crate does not implement.
#[error("unsupported profile {0:?}")]
BadProfile(String),
/// A witness carries no evidence payload where its kind requires one.
#[error("evidence is incomplete: {0}")]
IncompleteEvidence(&'static str),
/// Canonical encoding failed.
#[error("canonical encoding: {0}")]
Canonical(#[from] jcr1::Jcr1Error),
/// A figure claimed a measured grade the contributing evidence cannot
/// support.
#[error("energy provenance {claimed} is not supported by its evidence ({actual})")]
DishonestProvenance {
/// The grade the caller asked to seal.
claimed: &'static str,
/// The worst grade among the contributing figures.
actual: &'static str,
},
}
/// Signature algorithm id for Ed25519, matching the rest of the family.
pub const ALG_ED25519: u8 = 0x01;
/// The profile a specification and its evaluation declare.
pub const PROFILE: &str = "joulespec/1";
// ────────────────────────── content addressing ──────────────────────────
/// A content address: `b3:` followed by the BLAKE3 digest of the canonical
/// det-CBOR encoding, in lowercase hex.
///
/// The address is taken over the *same* canonical bytes a receipt seals, so
/// an address and a signature commit to one encoding rather than two.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Address(String);
impl Address {
/// The address of a canonical document.
pub fn of(value: &Value) -> Result<Self, Error> {
let bytes = jcr1::canonical_cbor(value)?;
Ok(Self::of_bytes(&bytes))
}
/// The address of an opaque byte string — an artifact state, a build
/// output, a tree digest.
#[must_use]
pub fn of_bytes(bytes: &[u8]) -> Self {
let d = blake3::hash(bytes);
let mut out = String::with_capacity(67);
out.push_str("b3:");
for b in d.as_bytes() {
out.push_str(&format!("{b:02x}"));
}
Self(out)
}
/// Adopt an address produced elsewhere — an artifact digest from a build
/// system, a goal address from `proof/1`.
#[must_use]
pub fn from_string(s: String) -> Self {
Self(s)
}
/// The address as it appears on the wire.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Address {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
/// Lowercase hex for a 32-byte key, as canonical documents carry it.
///
/// Keys appear in addressed documents as hex rather than as byte arrays so the
/// wire form is readable and fixed-width, and so a document can be compared by
/// eye against a receipt.
#[must_use]
pub fn hex32(bytes: &[u8; 32]) -> String {
let mut out = String::with_capacity(64);
for b in bytes {
out.push_str(&format!("{b:02x}"));
}
out
}
// ────────────────────────── energy ──────────────────────────
/// How an energy figure was obtained. The grade travels with the number
/// because the number alone cannot say whether anyone looked.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Provenance {
/// Nobody measured or modelled this work: a default guess.
Estimator,
/// A model the host calibrated — a real curve, not a reading.
ModelBased,
/// A counter read for this work, at its honest resolution.
HwShunt,
}
impl Provenance {
/// Whether a figure at this grade rests on something the host observed.
#[must_use]
pub const fn is_observed(self) -> bool {
matches!(self, Self::ModelBased | Self::HwShunt)
}
/// The wire label.
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Estimator => "Estimator",
Self::ModelBased => "ModelBased",
Self::HwShunt => "HwShunt",
}
}
}
/// An energy figure and the grade of the thing that produced it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Energy {
/// Microjoules.
pub uj: u64,
/// How `uj` was obtained.
pub provenance: Provenance,
}
impl Energy {
/// A figure at a stated grade.
#[must_use]
pub const fn new(uj: u64, provenance: Provenance) -> Self {
Self { uj, provenance }
}
/// A figure nobody measured.
#[must_use]
pub const fn estimated(uj: u64) -> Self {
Self::new(uj, Provenance::Estimator)
}
}
/// The worst provenance across a set of figures — what a sum of them may
/// honestly claim. An aggregate is never better graded than its weakest part.
pub fn worst_provenance<I: IntoIterator<Item = Provenance>>(it: I) -> Option<Provenance> {
it.into_iter().min()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_address_covers_the_canonical_bytes_not_the_json_text() {
// Key order in the source literal must not change the address: the
// canonical encoder orders keys, so these are one document.
let a = Address::of(&serde_json::json!({"a": 1, "bb": 2})).unwrap();
let b = Address::of(&serde_json::json!({"bb": 2, "a": 1})).unwrap();
assert_eq!(a, b);
assert!(a.as_str().starts_with("b3:"), "{a}");
assert_eq!(a.as_str().len(), 67, "b3: + 64 hex chars");
}
#[test]
fn a_different_document_has_a_different_address() {
let a = Address::of(&serde_json::json!({"a": 1})).unwrap();
let b = Address::of(&serde_json::json!({"a": 2})).unwrap();
assert_ne!(a, b);
}
#[test]
fn an_aggregate_is_graded_by_its_weakest_figure() {
let worst = worst_provenance([Provenance::HwShunt, Provenance::Estimator]);
assert_eq!(worst, Some(Provenance::Estimator));
let all_measured = worst_provenance([Provenance::HwShunt, Provenance::HwShunt]);
assert_eq!(all_measured, Some(Provenance::HwShunt));
assert_eq!(worst_provenance([]), None);
}
#[test]
fn provenance_orders_from_guess_to_reading() {
assert!(Provenance::Estimator < Provenance::ModelBased);
assert!(Provenance::ModelBased < Provenance::HwShunt);
assert!(!Provenance::Estimator.is_observed());
assert!(Provenance::ModelBased.is_observed());
}
}

View file

@ -0,0 +1,326 @@
//! The `joulespec/1` receipt: what was evidenced, how well, and at what cost.
//!
//! ## Sealed from the inputs, never from a verdict
//!
//! [`seal`] takes the specification, the artifact state, the witnesses and the
//! work — and evaluates them itself. It does not accept a
//! [`Conformance`](crate::conform::Conformance) and sign it.
//!
//! That is deliberate and it is the whole integrity story. A verdict is an
//! ordinary struct with public fields; a function that signed one would let a
//! producer construct `grade: Proved`, hand it over, and get a receipt saying
//! so. Because the receipt is derived from the evidence, a claim in a sealed
//! receipt is reproducible by anyone holding the same inputs — and a producer
//! who wants a better grade has to go and get better evidence.
//!
//! ## What a figure may claim
//!
//! The provenance in the receipt is the **worst** across every figure that fed
//! it, computed during evaluation. An aggregate of one measured reading and
//! one default guess is a guess, and a receipt that reported it as a reading
//! would be the most misleading possible summary of the work.
//!
//! ## Absent, not zero
//!
//! JCR-1's canonical core admits no nulls, and several of these figures are
//! legitimately absent: a specification with an unevidenced requirement has no
//! grade, a run with no work has no specified share, a run where nothing
//! reaches the evidence bar has no cost-per-obligation. Those fields are
//! **omitted** from the core rather than sealed as zero. A reader that finds no
//! `grade` key knows the specification was not graded; a reader that found
//! `"grade": "E0"` would believe it was.
//!
//! Normative: SPEC §5.3 (the headline figure), SPEC §6.1 (derived, not
//! signed on request), SPEC §6.2 (absent, not zero), SPEC §6.3 (the
//! per-clause audit).
use ed25519_dalek::SigningKey;
use serde_json::{Map, Value, json};
use crate::conform::{Conformance, Work, evaluate};
use crate::evidence::{Grade, SignedWitness};
use crate::jcr1;
use crate::spec::SignedSpec;
use crate::{Address, Error, PROFILE};
/// The grade whose evidence can fail, and therefore the denominator of the
/// headline cost figure.
pub const HEADLINE_BAR: Grade = Grade::Falsifiable;
/// Build the canonical wire document for an evaluation.
///
/// Public so a verifier in another language can be checked against the same
/// field set without going through COSE.
#[must_use]
pub fn wire(conformance: &Conformance, steward: &str) -> Value {
let owed = conformance.clauses.iter().filter(|c| c.kind.is_owed()).count();
let open = conformance.open(HEADLINE_BAR).len();
// One entry per clause: which obligation is the weak one is the
// actionable fact, and it must be answerable from the receipt alone
// rather than by re-running an evaluation nobody kept the inputs for.
let clauses: Vec<Value> = conformance
.clauses
.iter()
.map(|c| {
let mut entry = Map::new();
entry.insert("label".into(), json!(c.label));
entry.insert("kind".into(), json!(c.kind.label()));
entry.insert("obligation".into(), json!(c.obligation.as_str()));
entry.insert("evidence_uj".into(), json!(c.evidence_uj));
entry.insert("stale".into(), json!(c.stale.len()));
if let Some(grade) = c.grade {
entry.insert("grade".into(), json!(grade.code()));
}
if let Some(best) = &c.best {
entry.insert("witness".into(), json!(best.as_str()));
}
Value::Object(entry)
})
.collect();
let mut map = Map::new();
map.insert("joulespec_receipt".into(), json!(PROFILE));
map.insert("clause_verdicts".into(), Value::Array(clauses));
map.insert("spec".into(), json!(conformance.spec_id.as_str()));
map.insert("artifact".into(), json!(conformance.artifact.as_str()));
map.insert("clauses".into(), json!(conformance.clauses.len()));
map.insert("owed".into(), json!(owed));
map.insert("open".into(), json!(open));
map.insert("flags".into(), json!(conformance.flags.len()));
map.insert("specified_uj".into(), json!(conformance.specified_uj));
map.insert("unspecified_uj".into(), json!(conformance.unspecified_uj));
map.insert("evidence_uj".into(), json!(conformance.evidence_uj));
map.insert("steward".into(), json!(steward));
// Omitted rather than zeroed — see the module docs.
if let Some(grade) = conformance.grade {
map.insert("grade".into(), json!(grade.code()));
}
if let Some(ppm) = conformance.specified_ppm() {
map.insert("specified_ppm".into(), json!(ppm));
}
if let Some(uj) = conformance.uj_per_evidenced_clause(HEADLINE_BAR) {
map.insert("uj_per_falsifiable_clause".into(), json!(uj));
}
if let Some(provenance) = conformance.provenance {
map.insert("provenance".into(), json!(provenance.label()));
}
Value::Object(map)
}
/// Evaluate and seal in one step.
///
/// Returns the COSE_Sign1 envelope and the evaluation it sealed, so a caller
/// can report the detail while publishing the receipt.
pub fn seal(
spec: &SignedSpec,
artifact: &Address,
witnesses: &[SignedWitness],
work: &[Work],
steward: &str,
key: &SigningKey,
) -> Result<(Vec<u8>, Conformance), Error> {
let conformance = evaluate(spec, artifact, witnesses, work)?;
let core = jcr1::signed_core_bytes(&wire(&conformance, steward))?;
Ok((jcr1::cose_sign1_envelope(&core, key, steward), conformance))
}
/// Verify a sealed receipt and return the document it sealed.
pub fn verify(envelope: &[u8], steward: &str, public_key: &[u8; 32]) -> Result<Value, Error> {
let core = jcr1::verify_cose_sign1(envelope, steward, public_key)?;
Ok(jcr1::from_canonical_cbor(&core)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::conform::Flag;
use crate::evidence::{Evidence, Witness};
use crate::spec::fixtures::{key, spec};
use crate::{Energy, Provenance};
const STEWARD: &str = "did:key:z6MkExample";
fn artifact() -> Address {
Address::of_bytes(b"artifact-v1")
}
fn witness(
s: &SignedSpec,
label: &str,
evidence: Evidence,
uj: u64,
provenance: Provenance,
) -> SignedWitness {
let k = key(1);
let obligation = s
.spec
.clauses
.iter()
.find(|c| c.label == label)
.unwrap()
.obligation()
.unwrap();
Witness::new(
s.spec.spec_id().unwrap(),
obligation,
artifact(),
evidence,
Energy::new(uj, provenance),
k.verifying_key().to_bytes(),
)
.sign(&k)
.unwrap()
}
fn falsifiable(check: &str) -> Evidence {
Evidence::Falsifiable { check: check.into(), counter: Address::of_bytes(b"mutant") }
}
fn fully_evidenced() -> (SignedSpec, Vec<SignedWitness>, Vec<Work>) {
let s = spec(&key(1));
let ws = vec![
witness(&s, "C1", falsifiable("rejects_anon"), 1_000, Provenance::HwShunt),
witness(&s, "C2", falsifiable("decided_first"), 1_000, Provenance::HwShunt),
];
let work = vec![Work::new(
Address::of_bytes(b"op1"),
vec![s.spec.clauses[0].obligation().unwrap()],
Energy::new(48_000, Provenance::HwShunt),
)];
(s, ws, work)
}
#[test]
fn a_sealed_receipt_verifies_and_carries_the_evaluation() {
let (s, ws, work) = fully_evidenced();
let k = key(9);
let (env, conformance) =
seal(&s, &artifact(), &ws, &work, STEWARD, &k).expect("seals");
assert!(conformance.is_conformant(Grade::Falsifiable));
let doc = verify(&env, STEWARD, &k.verifying_key().to_bytes()).expect("verifies");
assert_eq!(doc["joulespec_receipt"], json!("joulespec/1"));
assert_eq!(doc["grade"], json!("E3"));
assert_eq!(doc["owed"], json!(2));
assert_eq!(doc["open"], json!(0));
assert_eq!(doc["flags"], json!(0));
assert_eq!(doc["provenance"], json!("HwShunt"));
assert_eq!(doc["uj_per_falsifiable_clause"], json!(25_000));
assert_eq!(doc["specified_ppm"], json!(1_000_000));
// The per-clause audit: a reader finds the weak obligation here
// without holding the original evidence set.
let verdicts = doc["clause_verdicts"].as_array().expect("array");
assert_eq!(verdicts.len(), 3);
assert_eq!(verdicts[0]["label"], json!("C1"));
assert_eq!(verdicts[0]["grade"], json!("E3"));
assert_eq!(verdicts[0]["evidence_uj"], json!(1_000));
assert_eq!(verdicts[2]["kind"], json!("non-goal"));
assert!(
verdicts[2].get("grade").is_none(),
"an unwitnessed non-goal is correct, and carries no grade"
);
}
#[test]
fn a_receipt_is_byte_identical_for_identical_evidence() {
let (s, ws, work) = fully_evidenced();
let k = key(9);
let (first, _) = seal(&s, &artifact(), &ws, &work, STEWARD, &k).unwrap();
let (second, _) = seal(&s, &artifact(), &ws, &work, STEWARD, &k).unwrap();
assert_eq!(first, second, "the receipt is a function of the evidence");
}
#[test]
fn an_ungraded_specification_omits_the_grade_rather_than_sealing_a_floor() {
let s = spec(&key(1));
// C2 unevidenced.
let ws = vec![witness(&s, "C1", falsifiable("rejects_anon"), 10, Provenance::HwShunt)];
let k = key(9);
let (env, conformance) = seal(&s, &artifact(), &ws, &[], STEWARD, &k).unwrap();
assert_eq!(conformance.grade, None);
let doc = verify(&env, STEWARD, &k.verifying_key().to_bytes()).unwrap();
assert!(
doc.get("grade").is_none(),
"a reader must not find a grade where none was established: {doc}"
);
assert_eq!(doc["open"], json!(1));
let verdicts = doc["clause_verdicts"].as_array().unwrap();
assert_eq!(verdicts[0]["grade"], json!("E3"), "C1 is evidenced");
assert!(verdicts[1].get("grade").is_none(), "C2 is the open one, and says so");
}
#[test]
fn one_guessed_figure_degrades_the_whole_receipt() {
let s = spec(&key(1));
let ws = vec![
witness(&s, "C1", falsifiable("rejects_anon"), 1_000, Provenance::HwShunt),
witness(&s, "C2", falsifiable("decided_first"), 1_000, Provenance::Estimator),
];
let k = key(9);
let (env, _) = seal(&s, &artifact(), &ws, &[], STEWARD, &k).unwrap();
let doc = verify(&env, STEWARD, &k.verifying_key().to_bytes()).unwrap();
assert_eq!(
doc["provenance"],
json!("Estimator"),
"a sum containing a guess is a guess"
);
}
#[test]
fn a_flagged_evidence_set_seals_the_flag_rather_than_refusing() {
let s = spec(&key(1));
let ws = vec![
witness(&s, "C1", falsifiable("rejects_anon"), 1_000, Provenance::HwShunt),
witness(&s, "C2", falsifiable("decided_first"), 1_000, Provenance::HwShunt),
// Something implements the non-goal.
witness(&s, "C3", Evidence::Exercised { check: "retry".into() }, 10, Provenance::HwShunt),
];
let k = key(9);
let (env, conformance) = seal(&s, &artifact(), &ws, &[], STEWARD, &k).unwrap();
assert!(matches!(conformance.flags.as_slice(), [Flag::NonGoalWitnessed { .. }]));
assert!(!conformance.is_conformant(Grade::Falsifiable));
let doc = verify(&env, STEWARD, &k.verifying_key().to_bytes()).unwrap();
assert_eq!(doc["flags"], json!(1), "disclosed in the sealed artifact");
assert_eq!(doc["grade"], json!("E3"), "the owed clauses really are evidenced");
}
#[test]
fn a_receipt_cannot_be_sealed_for_an_unsigned_specification() {
let (mut s, ws, work) = fully_evidenced();
s.sig = vec![0u8; 64];
let k = key(9);
assert!(matches!(
seal(&s, &artifact(), &ws, &work, STEWARD, &k),
Err(Error::BadSignature)
));
}
#[test]
fn a_receipt_does_not_verify_under_another_steward_or_key() {
let (s, ws, work) = fully_evidenced();
let k = key(9);
let (env, _) = seal(&s, &artifact(), &ws, &work, STEWARD, &k).unwrap();
assert!(verify(&env, "did:key:someone-else", &k.verifying_key().to_bytes()).is_err());
assert!(verify(&env, STEWARD, &key(8).verifying_key().to_bytes()).is_err());
}
#[test]
fn evidence_from_a_stale_artifact_state_cannot_be_sealed_as_current() {
let (s, ws, work) = fully_evidenced();
let k = key(9);
// The same witnesses, evaluated against a later artifact state.
let (env, conformance) =
seal(&s, &Address::of_bytes(b"artifact-v2"), &ws, &work, STEWARD, &k).unwrap();
assert_eq!(conformance.grade, None, "nothing witnesses the new state");
let doc = verify(&env, STEWARD, &k.verifying_key().to_bytes()).unwrap();
assert!(doc.get("grade").is_none());
assert_eq!(doc["open"], json!(2));
}
}

View file

@ -0,0 +1,466 @@
//! The specification: prose, addressed by obligation.
//!
//! A specification is an intent plus ordered clauses, signed by its author
//! and content-addressed, with revisions chained by `parent` so the history
//! of what was approved is tamper-evident. That much this family already had.
//!
//! The load-bearing decision here is **what a clause's address covers**.
//!
//! A clause carries a label (`"C1"`, `"auth-2"`) because reports and
//! conversations need a short handle, and an *obligation* — the address of its
//! kind and its text, with the label deliberately excluded
//! ([`Clause::obligation`]). Evidence binds to the obligation. The
//! consequences are the point:
//!
//! - **Relabelling is free.** Renumbering a specification, or renaming `C3`
//! to `rate-limit`, moves no obligation and loses no evidence.
//! - **Rewording is not.** Changing "the endpoint must reject unauthenticated
//! requests" to "…should reject…" is a different obligation with a
//! different address, and every piece of evidence bound to the old wording
//! stops applying to the new one — automatically, with nobody having to
//! notice.
//!
//! Binding by label gets this exactly backwards: evidence survives the edit
//! that changed the requirement and breaks on the edit that renamed it. That
//! is not a tooling gap, it is the mechanism by which specification drift goes
//! unobserved, and an address is the whole fix.
//!
//! Normative: SPEC §2.1 (the clause and its obligation), SPEC §2.2 (the
//! specification), SPEC §2.3 (revision).
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::{ALG_ED25519, Address, Error, PROFILE};
/// What kind of statement a clause makes.
///
/// Non-goals are first-class: writing down what the system must *not* do is
/// half the value of a design conversation, and evidence that something
/// implements a non-goal is a flag rather than a pass.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ClauseKind {
/// Behaviour the implementation must exhibit.
Requirement,
/// A bound the implementation must stay inside — interface, dependency,
/// style, or energy.
Constraint,
/// Behaviour deliberately out of scope.
NonGoal,
}
impl ClauseKind {
/// Whether this kind states something the implementation owes. Non-goals
/// do not: nothing is open because nothing should be built.
#[must_use]
pub const fn is_owed(self) -> bool {
matches!(self, Self::Requirement | Self::Constraint)
}
/// The wire label.
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Requirement => "requirement",
Self::Constraint => "constraint",
Self::NonGoal => "non-goal",
}
}
}
/// The address of one obligation: what evidence binds to.
///
/// Derived from a clause's kind and text. Two clauses with the same kind and
/// the same text state one obligation and have one address — which is why
/// [`Spec::validate`] refuses a specification containing both.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Obligation(Address);
impl Obligation {
/// The underlying address.
#[must_use]
pub const fn address(&self) -> &Address {
&self.0
}
/// The address as it appears on the wire.
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
/// Adopt an obligation address from the wire.
#[must_use]
pub const fn from_address(address: Address) -> Self {
Self(address)
}
}
impl std::fmt::Display for Obligation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0.as_str())
}
}
/// One individually addressable statement of the specification.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Clause {
/// The editorial handle — short, unique within the specification, and
/// deliberately **not** part of the obligation address.
pub label: String,
/// Requirement, constraint, or non-goal.
pub kind: ClauseKind,
/// The statement itself, in the author's words.
pub text: String,
}
impl Clause {
/// A clause at a kind.
pub fn new(label: impl Into<String>, kind: ClauseKind, text: impl Into<String>) -> Self {
Self { label: label.into(), kind, text: text.into() }
}
/// A requirement.
pub fn requirement(label: impl Into<String>, text: impl Into<String>) -> Self {
Self::new(label, ClauseKind::Requirement, text)
}
/// A constraint.
pub fn constraint(label: impl Into<String>, text: impl Into<String>) -> Self {
Self::new(label, ClauseKind::Constraint, text)
}
/// A non-goal.
pub fn non_goal(label: impl Into<String>, text: impl Into<String>) -> Self {
Self::new(label, ClauseKind::NonGoal, text)
}
/// The obligation this clause states: the address of `{kind, text}`.
///
/// The label is excluded, so the address answers "is this the same
/// obligation" and not "is this the same clause entry". Text is compared
/// exactly — a specification author who reflows a paragraph has changed
/// the obligation as far as this standard is concerned, which is the
/// conservative direction: evidence is dropped, never silently carried.
pub fn obligation(&self) -> Result<Obligation, Error> {
let doc = json!({ "kind": self.kind.label(), "text": self.text });
Ok(Obligation(Address::of(&doc)?))
}
}
/// The canonical specification artifact, unsigned.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Spec {
/// The profile this specification declares (`joulespec/1`).
pub profile: String,
/// Signature algorithm id (`0x01` = Ed25519).
pub alg: u8,
/// The distilled intent — *why* this exists, in the author's words.
pub intent: String,
/// Ordered, non-empty clauses with unique labels and distinct
/// obligations.
pub clauses: Vec<Clause>,
/// The specification this one revises, if any. Revisions chain by
/// address, so what was approved is tamper-evident history.
pub parent: Option<Address>,
/// The author's Ed25519 verifying key.
pub author: [u8; 32],
}
impl Spec {
/// A root specification.
pub fn new(intent: impl Into<String>, clauses: Vec<Clause>, author: [u8; 32]) -> Self {
Self {
profile: PROFILE.to_string(),
alg: ALG_ED25519,
intent: intent.into(),
clauses,
parent: None,
author,
}
}
/// Structural validation: a known profile and algorithm, a non-empty
/// intent, at least one clause, unique well-formed labels, non-empty
/// text, and no two clauses stating one obligation.
pub fn validate(&self) -> Result<(), Error> {
if self.profile != PROFILE {
return Err(Error::BadProfile(self.profile.clone()));
}
if self.alg != ALG_ED25519 {
return Err(Error::BadAlg(self.alg));
}
if self.intent.trim().is_empty() {
return Err(Error::EmptyIntent);
}
if self.clauses.is_empty() {
return Err(Error::NoClauses);
}
let mut labels = std::collections::BTreeSet::new();
// Obligation -> the label that first stated it, so the error can name
// both halves of a collision rather than just the second one.
let mut obligations = std::collections::BTreeMap::new();
for clause in &self.clauses {
if clause.label.is_empty() || clause.label.chars().any(char::is_whitespace) {
return Err(Error::BadLabel(clause.label.clone()));
}
if clause.text.trim().is_empty() {
return Err(Error::EmptyClause(clause.label.clone()));
}
if !labels.insert(clause.label.as_str()) {
return Err(Error::DuplicateLabel(clause.label.clone()));
}
let obligation = clause.obligation()?;
if let Some(first) = obligations.insert(obligation, clause.label.clone()) {
return Err(Error::DuplicateObligation(first, clause.label.clone()));
}
}
Ok(())
}
/// The canonical document the address and the signature commit to.
///
/// Written out rather than derived from the struct, for two reasons: the
/// canonical core admits no nulls, so an absent `parent` must be *omitted*
/// rather than serialized; and the wire form is then a declared part of the
/// standard instead of a consequence of Rust field names.
pub fn canonical(&self) -> Value {
let clauses: Vec<Value> = self
.clauses
.iter()
.map(|c| json!({ "label": c.label, "kind": c.kind.label(), "text": c.text }))
.collect();
let mut map = serde_json::Map::new();
map.insert("profile".into(), json!(self.profile));
map.insert("alg".into(), json!(self.alg));
map.insert("intent".into(), json!(self.intent));
map.insert("clauses".into(), Value::Array(clauses));
map.insert("author".into(), json!(crate::hex32(&self.author)));
if let Some(parent) = &self.parent {
map.insert("parent".into(), json!(parent.as_str()));
}
Value::Object(map)
}
/// The specification's address. Validates first — a specification that
/// does not hold together has no address.
pub fn spec_id(&self) -> Result<Address, Error> {
self.validate()?;
Address::of(&self.canonical())
}
/// The obligations this specification states, in clause order.
pub fn obligations(&self) -> Result<Vec<(String, ClauseKind, Obligation)>, Error> {
self.validate()?;
self.clauses
.iter()
.map(|c| Ok((c.label.clone(), c.kind, c.obligation()?)))
.collect()
}
/// The signature preimage: `spec_id ‖ parent`, so signing binds the
/// revision to its lineage — the same shape the family's operations use.
fn preimage(&self) -> Result<Vec<u8>, Error> {
let spec_id = self.spec_id()?;
let mut pre = spec_id.as_str().as_bytes().to_vec();
if let Some(parent) = &self.parent {
pre.extend_from_slice(parent.as_str().as_bytes());
}
Ok(pre)
}
/// Sign with the author's key.
pub fn sign(self, key: &SigningKey) -> Result<SignedSpec, Error> {
let sig = key.sign(&self.preimage()?);
Ok(SignedSpec { spec: self, sig: sig.to_bytes().to_vec() })
}
/// Begin a revision: same intent and clauses — edit them before signing —
/// with `parent` set to this specification's address and the author
/// replaced by the reviser.
pub fn revise(&self, author: [u8; 32]) -> Result<Self, Error> {
Ok(Self {
profile: self.profile.clone(),
alg: self.alg,
intent: self.intent.clone(),
clauses: self.clauses.clone(),
parent: Some(self.spec_id()?),
author,
})
}
}
/// A specification plus its author's signature.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedSpec {
/// The specification.
pub spec: Spec,
/// Ed25519 signature over `spec_id ‖ parent`.
pub sig: Vec<u8>,
}
impl SignedSpec {
/// Verify structure, address, and signature, returning the address.
pub fn verify(&self) -> Result<Address, Error> {
let pre = self.spec.preimage()?;
let key = VerifyingKey::from_bytes(&self.spec.author).map_err(|_| Error::BadSignature)?;
let sig: [u8; 64] = self.sig.as_slice().try_into().map_err(|_| Error::BadSignature)?;
key.verify(&pre, &Signature::from_bytes(&sig)).map_err(|_| Error::BadSignature)?;
self.spec.spec_id()
}
}
#[cfg(test)]
pub(crate) mod fixtures {
use super::*;
pub fn key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
/// Three clauses: two owed, one non-goal.
pub fn spec(author: &SigningKey) -> SignedSpec {
Spec::new(
"A metered endpoint that refuses what it cannot charge for.",
vec![
Clause::requirement("C1", "The endpoint rejects unauthenticated requests."),
Clause::constraint("C2", "A request is decided before any side effect runs."),
Clause::non_goal("C3", "The endpoint does not implement its own retry policy."),
],
author.verifying_key().to_bytes(),
)
.sign(author)
.expect("fixture signs")
}
}
#[cfg(test)]
mod tests {
use super::fixtures::{key, spec};
use super::*;
#[test]
fn a_signed_spec_verifies_and_addresses() {
let k = key(1);
let s = spec(&k);
let id = s.verify().expect("verifies");
assert!(id.as_str().starts_with("b3:"));
assert_eq!(id, s.spec.spec_id().unwrap(), "address is stable");
}
#[test]
fn a_tampered_clause_breaks_the_signature() {
let k = key(1);
let mut s = spec(&k);
s.spec.clauses[0].text = "The endpoint rejects nothing.".into();
assert!(matches!(s.verify(), Err(Error::BadSignature)));
}
// ── the load-bearing pair ───────────────────────────────────────────
#[test]
fn relabelling_a_clause_moves_no_obligation() {
let original = Clause::requirement("C1", "The endpoint rejects unauthenticated requests.");
let renamed =
Clause::requirement("auth-2", "The endpoint rejects unauthenticated requests.");
assert_eq!(
original.obligation().unwrap(),
renamed.obligation().unwrap(),
"a label is editorial: evidence must survive renaming"
);
}
#[test]
fn rewording_a_clause_moves_its_obligation() {
let strict = Clause::requirement("C1", "The endpoint rejects unauthenticated requests.");
let weakened =
Clause::requirement("C1", "The endpoint should reject unauthenticated requests.");
assert_ne!(
strict.obligation().unwrap(),
weakened.obligation().unwrap(),
"same label, different requirement: evidence must not carry over"
);
}
#[test]
fn changing_a_clauses_kind_moves_its_obligation() {
let owed = Clause::requirement("C1", "Retries are handled upstream.");
let disowned = Clause::non_goal("C1", "Retries are handled upstream.");
assert_ne!(owed.obligation().unwrap(), disowned.obligation().unwrap());
}
// ── validation ──────────────────────────────────────────────────────
#[test]
fn two_clauses_stating_one_obligation_are_refused() {
let s = Spec::new(
"intent",
vec![
Clause::requirement("C1", "The endpoint is metered."),
Clause::requirement("C2", "The endpoint is metered."),
],
key(1).verifying_key().to_bytes(),
);
// Both halves of the collision are named: a report that said only
// "C2" would send the author looking at the wrong clause.
assert!(
matches!(s.validate(), Err(Error::DuplicateObligation(a, b)) if a == "C1" && b == "C2")
);
}
#[test]
fn the_same_text_at_different_kinds_is_not_a_collision() {
let s = Spec::new(
"intent",
vec![
Clause::requirement("C1", "Retries are handled upstream."),
Clause::non_goal("C2", "Retries are handled upstream."),
],
key(1).verifying_key().to_bytes(),
);
assert!(s.validate().is_ok(), "kind is part of the obligation");
}
#[test]
fn structural_refusals() {
let author = key(1).verifying_key().to_bytes();
assert!(matches!(Spec::new("", vec![Clause::requirement("C1", "x")], author).validate(),
Err(Error::EmptyIntent)));
assert!(matches!(Spec::new("i", vec![], author).validate(), Err(Error::NoClauses)));
assert!(matches!(
Spec::new("i", vec![Clause::requirement("C 1", "x")], author).validate(),
Err(Error::BadLabel(l)) if l == "C 1"
));
assert!(matches!(
Spec::new("i", vec![Clause::requirement("C1", " ")], author).validate(),
Err(Error::EmptyClause(l)) if l == "C1"
));
assert!(matches!(
Spec::new("i", vec![Clause::requirement("C1", "x"), Clause::constraint("C1", "y")], author).validate(),
Err(Error::DuplicateLabel(l)) if l == "C1"
));
}
#[test]
fn a_revision_chains_to_its_parent_and_can_be_signed_by_someone_else() {
let author = key(1);
let reviser = key(2);
let root = spec(&author);
let root_id = root.verify().unwrap();
let mut next = root.spec.revise(reviser.verifying_key().to_bytes()).unwrap();
next.clauses.push(Clause::requirement("C4", "Every decision seals a receipt."));
let next = next.sign(&reviser).unwrap();
assert_eq!(next.spec.parent.as_ref(), Some(&root_id));
assert_ne!(next.verify().unwrap(), root_id);
}
#[test]
fn a_specification_that_does_not_validate_has_no_address() {
let s = Spec::new("", vec![], key(1).verifying_key().to_bytes());
assert!(s.spec_id().is_err());
}
}