Appendix F — Divergences from the spec

Where this list is silent, the spec governs. Every entry states: what the spec requires, what this implementation does instead, and why.

Live divergences

1. Repository naming

Spec: Meridian-Canon (one n). This repo: Meridian-Cannon (two n’s) for historical reasons. The inconsistency is unresolved. Why: The textbook preserves both spellings: Meridian-Cannon for the repository, Meridian-Canon for the specification. No change to conformance.

2. Embedding model defaults

Spec: nomic-embed-text-v1.5 (768-d) on SQLite; bge-large-en-v1.5 (1024-d) on Postgres. This textbook: Chapter 9 introduces BGE-M3 and Nomic v2-MoE as defaults for new builds; spec defaults remain documented for backward compatibility. Why: Per dossier 03, the spec defaults are the 2024 generation; BGE-M3 and Nomic v2-MoE are the 2026 successors. No change to attestation byte-shape.

3. Cryptopals-equivalent for canonicalization

Spec: No specific lab prescribed. This textbook: Lab 7 — a six-problem set walking from naïve canonicalization to the parser-mismatch attack, identified in dossier 01 as a pedagogical gap. Why: Canonicalization byte-exactness is best learned by attacking it. The lab is supplementary; it is not a Canon conformance requirement.

4. Second-language verifier

Spec: Implementation-neutral; no second language required. This textbook: Lab 25 requires implementing the verifier in Go (or Rust) and proving byte-for-byte agreement with the Python reference. Why: Re-implementation in a second language is the only reliable way to verify that canonicalization is byte-exact. This is pedagogy, not a Canon conformance requirement.

5. EU AI Act compliance overlay

Spec: Silent on EU AI Act compliance. This textbook: Appendix C and Chapter 26 treat it as an optional overlay for systems deployed in EU jurisdictions. No effect on R1–R9 conformance.

6. Status of FRE 707

Spec: Frames FRE 707 as an open question. This textbook: Explicit that FRE 707 is proposed but not law as of May 2026 (see Chapter 3, Appendix C, dossier 05). The spec is silent on the timeline.

7. Key rotation implemented; formal revocation not implemented

What diverges. The spec (§ 6.4) describes a key lifecycle that includes revocation: a previously-issued key can be invalidated so that attestations signed under it are no longer considered valid. The meridian-canon rotate-key CLI command (Chapter 23, meridian/canon/keys.py) implements rotation — generating a new Ed25519 keypair, recording the prior public key and rotation timestamp in the audit log, and updating the active key in the Keychain — but it does not implement revocation. Existing attestations signed under the rotated-out key remain verifiable; there is no mechanism to mark them invalid or to publish a revocation list.

Why. Revocation requires a distribution channel (a revocation endpoint or a signed revocation list that verifiers can fetch). That infrastructure is out of scope for the current implementation; the textbook does not yet cover deployment of the attestation signing service as a network-accessible component.

Implication. In the current implementation a compromised key cannot be actively invalidated. The mitigation is procedural: rotate promptly on suspicion of compromise, record the rotation event in the audit log with a reason field, and disclose the rotation to any relying party that holds attestations signed under the prior key. Chapter 23 notes this limitation explicitly and defers formal revocation to a future Phase C implementation.

8. DSSE adoption — breaking change in signature format

What changed. Canon v0.2.0 adopts DSSE per the CNCF in-toto specification. Prior to v0.2.0, Canon used bare Ed25519 over JCS bytes — sign(SHA-256(JCS(obj))). v0.2.0 uses sign(PAE(payload_type, JCS(obj))). This is a breaking change in the signature format: a v0.1.x signature over an attestation body is not a valid v0.2.0 DSSE signature over the same body, and vice versa.

Implication. Attestations sealed with emit() (v0.1.x) verify via the seven-step legacy path. Attestations sealed with emit_dsse() (v0.2.0) verify via the DSSE three-step path. Both paths are supported by conformant verifiers. The dsse_envelope key is the discriminator.

Migration. Existing v0.1.x attestations are not invalidated. No re-sealing is required. For new attestations, emit_dsse() is preferred. emit() is retained for backward compatibility and documented as legacy.

9. chain_hash as Canon extension to DSSE

What diverges. The DSSE base specification does not include a chain_hash field in the envelope. Canon v0.2.0 adds chain_hash as an extension field containing SHA-256(base64url_decode(payload)). This allows field-level integrity checking without implementing the full PAE verification procedure.

Why. The chain_hash field is a convenience for recipients who want to confirm payload integrity quickly (a single SHA-256) before committing to the PAE computation and Ed25519 verification. It is not a substitute for signature verification but provides a fast pre-check.

Implication. A verifier that only checks chain_hash is not conformant — it must also verify the Ed25519 signature over the PAE. The chain_hash field is documented as a convenience; the PAE signature is authoritative.

10. payload_type version encoding — Canon convention

What diverges. DSSE specifies that payload_type is a URI identifying the content type of the payload. It does not require version information in the URI. Canon v0.2.0 encodes the spec version in the payload_type string: application/vnd.nora.canon.attestation+json; version=0.2.0. This is a Canon convention, not a DSSE requirement.

Why. Encoding the version in payload_type ensures that the version is part of the signed message (via PAE). A verifier can detect version mismatches from the payload_type alone, without parsing the payload. This makes version detection fast and tamper-evident.

Implication. A DSSE verifier that does not understand Canon’s version parameter will still verify the signature correctly — the parameter is part of the type string, not a separate field. It is a Canon extension to DSSE naming conventions, not a change to the DSSE protocol.

12. Epistemic Neutrality Masking uses a rule-based masker, not a trained classifier

What diverges. The spec (§ 5.2, R5 — Epistemic Neutrality) calls for a masking component that suppresses author-identifying signals using a model trained to recognize those signals across diverse text styles. The current implementation in meridian/witness/local_chunker.py and part3_architecture/18_epistemic_masking.md uses a rule-based masker: a configurable list of named-entity patterns, pronoun substitution rules, and stopword filters defined in YAML (see docs/textbook divergence note in EDITORIAL.md). The masker does not learn from data and does not generalize beyond its configured pattern set.

Why. A trained classifier requires a labeled corpus of author-attributed text, a training pipeline, and periodic retraining as writing styles shift — infrastructure that is not yet in place. The rule-based approach is deterministic, auditable, and sufficient for the current evidence types (email headers, SMS metadata, structured records) where author signals are largely confined to known fields.

Implication. The rule-based masker may fail to suppress subtle stylometric signals in free-text fields (narrative case notes, long-form correspondence). Any consumer of Meridian-Cannon attestations relying on R5 for anonymization should treat the current ENM as a best-effort mitigation, not a guarantee. Chapter 18 documents the pattern set in full and includes a ✻ Try This exercise that demonstrates the attack surface. Replacing the rule-based masker with a trained classifier is listed as a Phase C deliverable.

Resolved divergences

(none yet)

Open questions

  • Should the textbook include a Rust reference verifier as a separate published artifact? Decision pending.
  • Should the textbook ship a Quarto build pipeline by default, or remain Markdown-only? Decision pending; per dossier 06 Quarto is the recommendation.
  • Should the standalone nora-canon-verifier PyPI package ship from this repository or from a separate one? Decision pending.

Status: living document. Update on every divergence noticed.