open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-deberta/scripts/hf_reference_tokenizer.py
Transaction Science d8ba701449 jouleclaw Phase 6: Edge-First v6 ports + 4 net-new standard-defining crates
12 crates landed (8 ports + 4 fresh designs). All 866 tests pass
across Phase 1-6; 0 failures; 52 ignored (39 inherited from
efficient-genai as hardware-gated, 12 in deberta as model-file-gated,
1 in sana_wm pre-existing efficient-genai input-size bug).

Phase 6a — Edge-First v6 surface (ported from pattern-lang)

  jouleclaw-schema       ~2137 LOC. QueryPlan, RetrievedItem,
                         KnowledgeAxes, EpistemicMode, AtomicClaim,
                         EntailmentResult, VerificationReport, Answer,
                         RAP, SystemCapabilities, AnytimeResult,
                         Invariants — the type vocabulary the rest of
                         the v6 surface is written against.

  jouleclaw-plan         ~1484 LOC. Query planner — picks the cascade
                         walk for a given input.

  jouleclaw-execute      ~2780 LOC. Plan executor with retry, anytime,
                         and budget primitives.

  jouleclaw-compose      ~1467 LOC. Verified-answer composer.

  jouleclaw-deberta      ~4801 LOC. DeBERTa-v3 forward pass for NLI
                         (used by jouleclaw-diagnose). Apple Accelerate
                         cblas_sgemm FFI gained `unsafe extern "C"` for
                         Rust 2024. Tests that require a real model on
                         disk now skip gracefully (`eprintln!("[skip]")`
                         + early return) instead of panicking — they
                         remain in-tree as integration tests and run
                         automatically when the model is present at
                         `models/deberta-v3-large-mnli/`.

  jouleclaw-diagnose     ~2353 LOC. Verification stage — runs NLI
                         against retrieved claims to certify the
                         answer.

  jouleclaw-edge-cli     ~2876 LOC. CLI binary that wires
                         schema/plan/execute/diagnose/compose/deberta
                         into a single edge agent surface.

  jouleclaw-edge-client  ~224 LOC. Thin stdin/stdout RPC client.

  Deferred: joule-edge (server) — its workspace depends on joule-l1 +
  lean-bridge + synth, which are OpenIE IP that do not port into the
  public open standard. Will land when the thin LawfulTier trait
  surface in jouleclaw-cascade ships in a follow-on commit.

Phase 6b-e — net-new (the standard-defining moat)

  jouleclaw-prov   Receipt-per-resolution emitter. CascadeTier (L0–L4
                   with stable wire tags + named tags),
                   ClaimProvenance (PROV-O compatible Entity shape),
                   ToolTouch (per-tool joule accounting),
                   ReceiptBuilder with worst-counter-floor provenance
                   semantics. Smart-Byte-envelope shaped so receipts
                   seal cleanly into the sibling standard's signed
                   transport. 6 tests passing.

  jouleclaw-fresh  L3.5 cascade stage — fresh retrieval with
                   provenance. SearchProvider trait (Brave / Tavily /
                   Exa / Serper plug in here), Transport trait (keeps
                   reqwest out of the standard), TrustTable
                   (bootstrap from Wikipedia perennial-sources;
                   normalises `www.` prefix), FreshFetch orchestrator,
                   `provenance_envelope()` that wraps a RetrievedClaim
                   into a jouleclaw-prov ClaimProvenance ready to
                   embed in a cascade receipt. 6 tests passing.

  jouleclaw-mcp    MCP tool surface with joule metering. MeteredTool
                   trait + `dispatch_metered()` brackets every tool
                   call with energy counter reads and emits a
                   ToolTouch into the running receipt. WireEncoding
                   (JsonRpc / Cbor) + `negotiate()` defines the
                   `joule-mcp` capability profile —
                   `x-jouleclaw/joule-mcp@1` is the canonical
                   negotiation tag; both endpoints advertise it to
                   opt into CBOR, otherwise stays JSON-RPC for
                   compat. 5 tests passing.

  jouleclaw-cli    `jclaw` binary — the Pi-class minimal harness.
                   v0.1 ships `version`, `tier-walk`, `receipt`,
                   `meter` subcommands plus a heuristic router that
                   picks a tier from input shape. Receipt builder is
                   live; energy counter binding is a deployment
                   choice (feature-gated per platform). Hot-reload +
                   tree-forked sessions + full MCP client land
                   later — v0.1 proves the type vocabulary and the
                   end-to-end receipt emission. 4 tests passing.

Phase 6 cumulative

  17 → 29 crates in the workspace.
  ~155k → ~170k LOC.
  440 → 866 tests passing.
  Net-new code surface: ~1500 LOC of standard-defining moat
  (jouleclaw-prov + fresh + mcp + cli).

Next: SPEC.md + CHARTER + conformance/v1 wire vectors + the
memory entry. Then promote to a public release tag.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-26 18:44:46 -04:00

129 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""Generate the Rust-side tokenizer fixture by running HF's
AutoTokenizer over a fixed list of (premise, hypothesis) pairs and
dumping the canonical token ids + attention masks to JSON.
The Rust tests load that JSON and assert byte-identical output from
our tokenizer wrapper. Anytime we adjust normalization, special-token
handling, or template processing, we re-run this and the diff is
the contract change.
Usage:
python3 crates/joule-deberta/scripts/hf_reference_tokenizer.py
Writes:
crates/joule-deberta/fixtures/tokenizer_pairs.json
Requires:
transformers, sentencepiece (both already installed locally per
the Phase 4a setup).
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from transformers import AutoTokenizer # type: ignore
MODEL_DIR = Path("models/deberta-v3-large-mnli")
OUT_PATH = Path("crates/joule-deberta/fixtures/tokenizer_pairs.json")
# Pairs picked to exercise: short canonical NLI, multi-sentence
# premise, punctuation/contraction edge cases, all-uppercase, mixed
# scripts (none of these models support extended Unicode in NLI but
# tokenization shouldn't break), and a "Paris is the capital of
# France" pair that mirrors the spec's running example.
TEST_PAIRS = [
{
"name": "simple_entail",
"premise": "Paris is the capital of France.",
"hypothesis": "France's capital is Paris.",
},
{
"name": "simple_contradict",
"premise": "The cat sat on the mat.",
"hypothesis": "There was no cat anywhere.",
},
{
"name": "neutral",
"premise": "Marie Curie won the Nobel Prize in Physics in 1903.",
"hypothesis": "She also worked on radioactive materials.",
},
{
"name": "contraction_punct",
"premise": "It's the world's biggest waterfall, isn't it?",
"hypothesis": "Niagara Falls is the world's largest waterfall.",
},
{
"name": "all_upper",
"premise": "THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.",
"hypothesis": "A fox jumped over a dog.",
},
{
"name": "long_premise_short_hyp",
"premise": (
"Deep Space 1 was launched on October 24, 1998, as the first mission "
"of NASA's New Millennium Program. It tested twelve advanced "
"technologies, including the Remote Agent autonomous control system, "
"the first time an AI system had primary command of a spacecraft."
),
"hypothesis": "Deep Space 1 was launched in 1998.",
},
]
def main() -> int:
if not MODEL_DIR.exists():
print(f"ERR: model dir {MODEL_DIR} not found", file=sys.stderr)
return 1
tok = AutoTokenizer.from_pretrained(str(MODEL_DIR))
out = {
"tokenizer_source": str(MODEL_DIR),
"tokenizer_class": tok.__class__.__name__,
"vocab_size": tok.vocab_size,
"model_max_length": getattr(tok, "model_max_length", None),
"special_tokens": {
"cls": tok.cls_token,
"sep": tok.sep_token,
"pad": tok.pad_token,
"unk": tok.unk_token,
"cls_id": tok.cls_token_id,
"sep_id": tok.sep_token_id,
"pad_id": tok.pad_token_id,
"unk_id": tok.unk_token_id,
},
"pairs": [],
}
for case in TEST_PAIRS:
# `encode_plus(text, text_pair)` gives us the canonical NLI
# encoding: [CLS] premise [SEP] hypothesis [SEP]. We don't
# truncate or pad here — we want the raw, unpadded ids so
# the Rust side can match exactly.
enc = tok(
case["premise"],
case["hypothesis"],
add_special_tokens=True,
truncation=False,
padding=False,
return_attention_mask=True,
return_tensors=None,
)
out["pairs"].append({
"name": case["name"],
"premise": case["premise"],
"hypothesis": case["hypothesis"],
"token_ids": enc["input_ids"],
"attention_mask": enc["attention_mask"],
"token_strings": tok.convert_ids_to_tokens(enc["input_ids"]),
})
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUT_PATH.write_text(json.dumps(out, ensure_ascii=False, indent=2))
print(f"wrote {OUT_PATH} with {len(TEST_PAIRS)} pairs")
return 0
if __name__ == "__main__":
raise SystemExit(main())