open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-jcr1/docs/JCR-1-v1.1-intent.md
Transaction Science 2dc5b0c4cb jouleclaw-jcr1: JCR-1 reference + 10 vectors + Rust producer + Python/Go verifiers
JCR-1 — the **JouleClaw Receipt Specification** — is the propagation
wedge the 5-lens cross-talk audit named as the single highest-leverage
positioning move. A signed, per-resolution thermodynamic receipt
third-party-verifiable with the spec, the public key, and standard
CBOR + COSE tooling. No JouleClaw dependency required to *check* a
receipt; this crate is needed only to *mint* one.

This commit lands the complete propagation package — spec, suite,
producer, two independent verifiers — as a single workspace crate.

## What's in the crate

```
crates/jouleclaw-jcr1/
├── README.md
├── run.sh                       ← Rust mints, Python + Go each verify
├── docs/
│   ├── JCR-1-spec.md            ← the receipt standard (CC-BY-4.0)
│   └── JCR-1-v1.1-intent.md     ← additive proposal: intent in signed core
├── conformance/
│   ├── README.md
│   ├── vectors.json             ← 10 signed canonical vectors
│   ├── test-keys.json           ← published TEST keys
│   ├── verify.py                ← reference verifier (cbor2 + cryptography)
│   └── verify.go                ← independent Go verifier (stdlib only)
├── examples/
│   └── jcr1_mint.rs             ← Rust producer: read energy → sign → emit
└── src/lib.rs                   ← det-CBOR encoder, COSE_Sign1, HMAC, projection
```

## Surface (Rust)

- `canonical_cbor(&Value)` — RFC 8949 §4.2.1 length-first-then-bytewise
  key ordering, no floats/bools/nulls. Matches the Python
  `cbor2.dumps(canonical=True)` and the Go from-scratch encoder
  byte-for-byte across all 10 conformance vectors.
- `to_jcr1_wire(&Receipt) -> Value` — projects a Rust `Receipt` to
  the JCR-1 wire form: `CascadeTier::L0Cache` becomes wire tag `"L0"`,
  `Provenance` renders PascalCase, wave-8 fields `model_id` + `intent`
  are intentionally dropped (deferred to JCR-1 v1.1 — see
  `docs/JCR-1-v1.1-intent.md`).
- `signed_core_bytes(&Value)` — drops `token_attributions` + null
  fields per JCR-1 §3.1, then canonically encodes.
- `canonical_signed_core(&Receipt)` — convenience wrapping both.
- `cose_sign1_envelope` / `verify_cose_sign1` — COSE_Sign1 (RFC 9052)
  envelope with EdDSA / Ed25519 only. Build the protected header,
  Sig_structure, signature; verify by parsing the tagged CBOR,
  re-building Sig_structure, checking kid == expected_steward, and
  Ed25519-verifying.
- `hmac_tool_hex` — gateway tool-receipt HMAC per JCR-1 §5
  (NUL-delimited canonical payload, HMAC-SHA-256, lowercase hex).
  HMAC implemented inline (30 LOC) since `hmac = 0.12` doesn't
  satisfy the workspace's `sha2 = 0.11` CoreProxy bound.
- `check_honesty(&Value)` — JCR-1 §6 step 5: worst-counter rule,
  joule sum vs tools_touched, mJ resolution floor for non-HwShunt.
- `VECTORS_JSON` / `TEST_KEYS_JSON` — the 10 vectors and test keys
  embedded via `include_str!` so `cargo test` re-verifies the suite
  on every workspace run.

## The decisive test: byte-equal re-encode across three encoders

`vector_signed_cores_re_encode_byte_identical` re-encodes each of
the 10 vectors' receipts via the Rust canonical encoder and asserts
the bytes equal the suite's `signed_core_cbor_hex` — which was
produced by Python's `cbor2.dumps(canonical=True)` and independently
re-produced by the Go verifier's hand-written encoder.

Three encoders, three languages, byte-for-byte identical output on
all 10 vectors — that is what makes the spec the source of truth.

## V5 ↔ V10 the promotion-collapse demo

`vector_5_and_10_share_input_hash_promotion_collapse` asserts the
two vectors share the same `input_hash` and that V5 (L3, 2,500,000
µJ) → V10 (L1, 9 µJ) is a ≥100,000× collapse. The "yang→yin"
turnover claim made auditable in a single diff.

## Tamper detection

`tampering_with_joules_uj_is_caught_by_re_encode` proves an
understated-joules forgery produces different canonical bytes —
matching the negative-test surface documented in the Python and Go
README. The 4 failure modes (re-encode mismatch, payload mismatch,
HMAC mismatch, Ed25519 mismatch) all surface different errors.

## Quickstart

`./crates/jouleclaw-jcr1/run.sh` runs the full demo: Rust mints,
Python verifier accepts the 10 canonical vectors, Go verifier
accepts the same 10 vectors, then BOTH verifiers accept the
freshly Rust-minted receipt.

## Honest scope (v0.1)

- JCR-1 v1 wire form only. Wave-8 `model_id` + `intent` are NOT in
  the signed core; the additive v1.1 proposal in
  `docs/JCR-1-v1.1-intent.md` describes the path.
- Ed25519 (`alg = -8`) only. ES256 (`alg = -7`) permitted by the
  spec but not implemented here — `verify_cose_sign1` rejects other
  algs with `UnsupportedAlg`.
- Key transparency / rotation is out of scope; KERI AID rotation is
  recommended by §4 but resolution is the consumer's job.
- The 10 vectors are TOY joule values exercising every spec path.
  Real evidence requires RAPL/NVML/IOReport cross-validated against
  Zeus — see strategy-and-landscape.md from the source drop.

## Lineage

This commit ingests a May 31 conversation drop at
`joule-claw-web/notes-05312026/` into the open-standards workspace,
restructured per the 4-move plan: (1) adopt the 10 vectors as
fixtures, (2) decide on `steward` + adopt det-CBOR, (3) propose
v1.1 with `intent`, (4) move the producer + verifiers into the
workspace. All four closed in two commits (b39fe21 + this one).

Tests: 25 lib + 1 example smoke + 10 Python + 10 Go = the full suite
across three languages with byte-identical canonical CBOR.
Workspace regression: 2,427 lib tests pass, 0 failures.
2026-06-01 03:32:44 -04:00

206 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# JCR-1 v1.1 — Proposal: query-intent in the signed core
**Status:** Proposal (draft).
**Target:** Additive bump of [JCR-1](./JCR-1-spec.md), `jc_receipt: "1"` →
optional `intent` field MAY appear in the signed core. v1.0 verifiers
ignore it (per §8 additive evolution).
---
## 1. Why
Priscilla (Sentry) ran 116 of her own Claude sessions through a
post-hoc classifier and found **67% comprehension / 2% generation** —
the rest split across modification / process / review / other. The
finding generalises: *every workload that uses an LLM-shaped runtime
distributes across the same six categories, and the comprehension
slice is consistently dominant.*
JouleClaw already classifies queries at runtime
(`jouleclaw-intent::HeuristicClassifier` + the `LlmFallbackClassifier`
seam) and tags `Receipt.intent` on the way to the ledger
(`jouleclaw-ledger::CostSummary::by_intent`). What's missing for a
deployment to reproduce Priscilla's analysis on its OWN signed
receipts is: **the intent verdict needs to sit inside the signed core
so it survives across third-party audit.** An external grader can
then run
> *"prove to me that ≥60% of your `Receipt` records carried
> intent=`comprehension` and a tier in {L0, L0.x, L1, L2}, signed by
> the steward I trust."*
without trusting the runtime's separate dashboard.
This proposal pins how the intent verdict is canonicalised so the
audit is mechanical, not interpretive.
---
## 2. What's added
One optional top-level field on the JCR-1 receipt:
```
intent: { intent: enum, classifier_id: string } // OPTIONAL
```
### 2.1 `intent.intent` — taxonomy (frozen for v1.x)
A stable string enum mirroring `jouleclaw_intent::QueryIntent`:
| Wire form | Meaning | Verifiable-zone? |
|-------------------|---------------------------------------------------|------------------|
| `comprehension` | Reading / understanding existing code or state | yes |
| `generation` | Producing new artefacts | no |
| `modification` | Editing existing artefacts in place | partial |
| `process` | Operational / workflow tasks | partial |
| `review` | Reviewing a candidate artefact | yes |
| `other` | Unclear / refused | n/a |
Names are `snake_case` text strings — *not* small-int enums — so the
det-CBOR form stays human-inspectable and matches the Rust
`#[serde(rename_all = "snake_case")]` already shipping.
### 2.2 `intent.classifier_id` — provenance for drift
A stable string identifying the classifier that produced the verdict
(`"heuristic:v1"`, `"llm:claude-haiku-4-5"`, `"override:operator"`,
…). Required when `intent` is present.
A receipt grader that sees a dashboard shift from 67% → 50%
`comprehension` over a week needs to know whether the workload moved
or the classifier got changed. The `classifier_id` is that signal.
The grader correlates the shift against a `classifier_id` change in
the audit log; if it tracks, the workload didn't change.
### 2.3 What is NOT added
- **`confidence` is intentionally excluded.** `IntentVerdict.confidence`
is a `f64`; JCR-1 §3.1 already excludes floats from the signed core
because they're not reproducible across encoders, and confidence is
classifier-specific (no shared scale). Confidence stays on the
full `Receipt` JSON outside the signed core, alongside
`token_attributions`.
- **No new `intent_categories` map or sub-types.** The six-variant
taxonomy is the wire form. Subdivisions are caller-side concerns.
---
## 3. Canonical encoding
The `intent` field, when present, is a 2-entry CBOR map under the top-
level receipt map. Both subfields are required when `intent` is
present (omitting either is a structural violation).
Order under RFC 8949 §4.2.1 (length-first then bytewise) places the
keys as:
```
intent (6) → comes between "claims" (6) and "tier" (4)? No — length-first.
4-byte keys: tier
6-byte keys: claims, intent, ...
```
`"intent"` and `"claims"` both have length 6; bytewise: `c` (0x63) <
`i` (0x69), so `claims` comes before `intent`. The inner map
`{intent, classifier_id}` sorts to `intent` (6) before
`classifier_id` (13) by length.
Wire example (canonical CBOR, hex):
```
A2 # map(2)
66 69 6E 74 65 6E 74 # text(6) "intent"
6D 63 6F 6D 70 72 65 68 65 # text(13) "comprehension"
65 6E 73 69 6F 6E
6D 63 6C 61 73 73 69 66 69 # text(13) "classifier_id"
65 72 5F 69 64
6C 68 65 75 72 69 73 74 69 # text(12) "heuristic:v1"
63 3A 76 31
```
(Hand-wrapped above; an encoder MUST sort the inner map's keys
length-first → `intent` before `classifier_id`.)
---
## 4. Verification flow change
JCR-1 §6 gains one optional step between step 4 (tool HMACs) and step 5
(honesty consistency):
> **4.5 — Intent structural.** If `intent` is present, both
> `intent.intent` and `intent.classifier_id` MUST be present;
> `intent.intent` MUST be one of the six taxonomy values. Unknown
> intent values are rejected (the spec is `#[non_exhaustive]` at the
> Rust level but the wire enum is frozen for v1.x).
The drift bands of §7.1 are unchanged — intent doesn't enter joule
re-validation.
---
## 5. Backward-compatibility
- v1.0 verifiers: per §8 ("Unknown fields MUST be ignored, never
rejected"), v1.0 verifiers see the extra `intent` field and skip
it. The 10 canonical vectors continue to verify under v1.0 verifiers.
- v1.1 receipts produced without `intent` are valid v1.0 receipts.
- The canonical CBOR re-encode test continues to pass byte-for-byte —
`intent` just adds a new (key, value) pair to the map, with its
position determined by RFC 8949 §4.2.1.
This is an additive change. `jc_receipt` stays `"1"`.
---
## 6. Conformance vectors
A v1.1-ratifying conformance suite SHOULD add three vectors on top of
the existing ten:
11. **L0 cache hit + intent=comprehension** — verifies the canonical
map order with `intent` present and no `tools_touched`.
12. **L3 model + intent=generation + ModelBased** — verifies intent
coexists with `tools_touched` and ModelBased honesty floor.
13. **Promoted skill (collapses V5) + intent=comprehension** — the
Priscilla 67% case: comprehension intent now resolves at L1.
A v1.1 verifier that round-trips vectors 1–13 byte-identically is
**v1.1 vector-conformant**.
---
## 7. Why not add `model_id` too?
`model_id` (also a wave-8 `Receipt` field) is a candidate but raises
distinct questions JCR-1 v1.1 deliberately defers:
- Where does `model_id` resolve? KERI AID? OCI subject digest? A
registry the spec has to pin? A stable identifier scheme is a
wider call than adding the intent taxonomy.
- Per-receipt `model_id` overlaps with the wave-6 `jouleclaw-oci` +
`jouleclaw-slsa` provenance chain — those already pin the model
binary's identity via in-toto Statement subject + ORAS referrer.
Carrying it on the cascade receipt may duplicate that surface.
A separate v1.2 proposal (or a sidecar binding) would resolve
`model_id` once the wider model-identity question is settled.
---
## 8. Implementation in jouleclaw-jcr1
The Rust reference (this crate) implements v1.1 behind an opt-in
constructor. The current `to_jcr1_wire` projection drops `intent`
(v1.0 behaviour). When v1.1 ratifies:
```rust
// proposed v1.1 surface
pub fn to_jcr1_wire_v1_1(receipt: &Receipt) -> Result<Value, JcrError>;
pub fn canonical_signed_core_v1_1(receipt: &Receipt) -> Result<Vec<u8>, JcrError>;
```
Both paths share the canonical CBOR encoder; v1.1 just stops dropping
`intent` during the projection. Roughly five additional lines of code
plus three vectors.