--- Status: v0.1 Working Draft — open for external review (review window to be scheduled) Spec name: Typed Standards Specification Version: v0.1.9 Tag: v0.1.9-typed-standards-spec License: CC BY 4.0 Last updated: 2026-09-21 Maintainer: Nathan Storey (current; see reviewer-orientation document for stewardship and contact details) Canonical URL: "[TK: typedstandards.org/specs/v0.1/ once typedstandards.org is registered and the spec is published there]" --- # Typed Standards Specification **Abstract.** Typed Standards is a content-agnostic open standard for **production-process attestation** of analytical artifacts: a cryptographically signed, content-addressed, capture-method-labeled record of *how* an artifact was produced, verifiable by a third party who does not trust the publisher. The standard specifies a cryptographic envelope (Ed25519ph signature over RFC 8785 JCS-canonicalized JSON; RFC 3161 trusted timestamp from a public TSA; Sigstore Rekor inclusion proof; trust registry under the publisher's own well-known path), a typed-node taxonomy of standalone assertions (`content/*`) and assertions about other nodes (`attestation/*`), a signature-covered capture-method discipline, and an extensible content-profile mechanism. The standard is deliberately silent about truth, editorial policy, and topology: corroboration is not truth; the system surfaces signals, the consumer applies judgment. This document consolidates the project's standards work — formerly the Open Evidence Standard (OES) at the envelope layer and the Civic Claim Vocabulary (CCV) at the typed-claims layer — under a single umbrella name and the `ts:` JSON-LD prefix, resolved to `https://typedstandards.org/ns/ts#`. The consolidation is recorded; the historical OES and CCV drafts remain in this directory as frozen snapshots for cross-reference accuracy. --- ## 2. Status of This Document **Status:** v0.1 Working Draft — open for external review (review window to be scheduled) **Spec name:** Typed Standards Specification **Version:** v0.1.9 (patch revision of the v0.1 Working Draft; tag: `v0.1.9-typed-standards-spec`) **License:** CC BY 4.0 (see §3; canonical citation form in Appendix A) **Maintainer:** Nathan Storey (current; see reviewer-orientation document for stewardship and contact details) **Canonical URL:** [TK: typedstandards.org/specs/v0.1/ once typedstandards.org is registered and the spec is published there] This document is an open-for-external-review working draft of the Typed Standards Specification. The substantive specification stabilized through the G1-G4 MVP cohort; this document consolidates the prior OES + CCV drafts under a single umbrella. The v0.1 designation reflects that the specification is open for substantive external feedback; review-window dates and a comment-deadline are populated once initial conversations with reviewer organizations have set expectations. Until then, the specification lives at HEAD of `main`. Reviewers and integrators embedding a version identifier cite against the **current patch-revision tag** (`v0.1.9-typed-standards-spec`), which names the precise revision of this document; the consolidation-milestone tag (`v0.2-typed-standards-rfc`) remains for citing the consolidation milestone itself, not the current text. The document's own version metadata — this status block's Version line, and the title block's separate `Version` and `Tag` keys — carries the same patch version and tag name, so an integrator embedding a version identifier from the document itself gets the precise revision rather than the coarse draft number. Since v0.1.7 that agreement is mechanically enforced: a repository check parses the title block on every change and fails when a value is unparseable, when `Version` is not a full patch version, or when `Tag` and `Version` name different revisions. Conformance language follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) keywords (MUST, MUST NOT, SHOULD, MAY) when applied to normative requirements. Every normative requirement in this document corresponds either to a check enforced in the reference implementation at `civic-ai-tools-website` code today or to a settled Architectural Decision Record in `civic-ai-tools/docs/adr/`. Where neither holds, the section is labeled with a callout pointing at the relevant open question in [`open-questions.md`](open-questions.md). A snapshot of which sections are built / specified / reserved in the reference implementation appears in Appendix E. Open questions affecting the specification are tracked in [`open-questions.md`](open-questions.md) per the working method's "registry is the front door" discipline. Offline verifiability — once the specification's most load-bearing aspirational claim — is, as of the offline-crypto-hardening arc documented in §9.4, a **demonstrated property of the self-contained commitment bundle** (resolving [Q15](open-questions.md#q15--external-verification-testing)); the still-open [Q1](open-questions.md#q1--package-format) (package format) now governs the narrower question of whether the single-blob *package alone* should embed its own proofs rather than carrying them in an accompanying bundle. --- ## 3. License This specification is licensed under the **Creative Commons Attribution 4.0 International License (CC BY 4.0)**. You are free to share and adapt this material for any purpose, including commercially, provided you give appropriate credit, provide a link to the license, and indicate if changes were made. Full license text: . Patent posture is separate from this copyright license: see `PATENTS.md` at the repository root — the maintainer's royalty-free non-assertion statement over Essential Claims for conformant implementations, adopted per ADR-0017. Citation conventions for this specification appear in Appendix A. --- ## 4. Table of Contents 1. Title block and abstract 2. Status of This Document 3. License 4. Table of Contents 5. Introduction - 5.1 Normative preamble - 5.2 The problem - 5.3 What Typed Standards is - 5.4 Project posture - 5.5 Relationship to adjacent standards 6. Conventions and Terminology - 6.1 Conformance keywords - 6.2 Glossary - 6.3 Disambiguated terms 7. Architecture - 7.1 Layered shape - 7.2 Envelope - 7.3 Verification flow - 7.4 Two-family taxonomy — one structural primitive, content/* and attestation/* - 7.5 The QEC sub-ontology within content/* 8. Normative specification - 8.1 Record package structure - 8.2 Canonical JSON, envelope hash, content hash - 8.3 Cryptographic envelope - 8.4 Trace capture - 8.5 Identity binding - 8.6 captureMethod - 8.7 Profile requirements: datHere and scripted-recomputation - 8.8 Cross-host publication: commitment-view schema - 8.9 Embed-vs-reference policy - 8.10 Lifecycle and location attestations - 8.11 Typed Claims - 8.12 The attestation/* namespace - 8.13 Federation and discoverability 9. Conformance 10. Security Considerations 11. Privacy Considerations 12. IANA Considerations 13. Internationalization Considerations 14. References - 14.1 Normative References - 14.2 Informative References 15. Appendices - A. Citation conventions - B. Worked example: typed claim - C. Adjacent-standards comparison table - D. C2PA-to-Typed-Standards term-translation table - E. Implementation status snapshot (as of 2026-05-26) - F. Open questions pointer - G. Revision history - H. Related documents - I. Acknowledgments - J. Vocabulary settlement (Q50/Q66): old→new mapping and dual-era rules --- ## 5. Introduction ### 5.1 Normative preamble Every product surface that renders signed nodes under this specification, every downstream consumer that processes them, every derived publication that cites them, and every third-party implementation of this specification MUST carry the following preamble or a clearly-equivalent statement, surfaced where readers will encounter it before forming conclusions about a node's content: > **Corroboration ≠ truth.** Consensus can be wrong. > > **Contradiction ≠ falsity.** The heretic is sometimes right. > > **Identity strength ≠ topic authority.** A credentialed outsider can be wrong; a pseudonymous insider can be right. > > **The system surfaces signals; the consumer applies judgment.** The intent is to prevent the architecture from drifting into automated truth-scoring — a regime that has historically gone badly (content moderation, credit scoring, citation metrics). Implementers MUST NOT use signed-node signals to compute platform-issued correctness verdicts, rank-by-trust scores, or any equivalent consensus collapse. Consumer-side aggregation (citation graphs, contradiction surfacing, meta-analysis) is permitted and encouraged, provided the preamble's framing accompanies the surfaced result. This is the only normative requirement in this specification that is not enforced by code. **Enforcement of this requirement is currently editorial and reputational only.** No standards body, no certification regime, no audit, and no automated check exists today to verify that downstream implementations honor it. This is a v0.1 limitation. Future versions of the specification may define a stewardship process — a public consultation forum, a conformance self-attestation, or a reference test corpus — through which implementers demonstrate the preamble is surfaced. Until such a process exists, the requirement holds normatively, and breaches are addressed in conversation with the implementer rather than through enforcement infrastructure. ### 5.2 The problem Trust in analytical claims today is mediated by brand. A reader who encounters a chart, a number, or a synthesis decides whether to believe it based on the institution behind it — investigative journalism, academic publishing, civic-data analysis from a government agency, consumer-rights research, regulatory submissions, audit work product. The artifact itself usually carries no machine-verifiable record of how it was produced. This was workable while production was an implicit labor attestation. A serious data analysis took weeks of skilled work; the institution staking its name on it had presumably done that work. AI-assisted analysis breaks the implicit-labor-attestation assumption. The same chart that took an analyst a week can now be produced in minutes by a journalist with no statistical training, a community member with no institutional affiliation, or an adversary fabricating a plausible-looking story. Brand-mediated trust becomes increasingly orthogonal to whether the analysis is sound — and brands themselves are now consumers of AI-assisted production they cannot internally verify. The specification's response is to make the **production process itself** the unit of attestation. Not "is this true," but "here is, in cryptographic detail, how this was produced — judge for yourself." ### 5.3 What Typed Standards is Typed Standards is **opinionated about three things** and **deliberately silent about three others**. **Opinionated about:** 1. **The envelope.** Every conformant package carries an Ed25519ph signature over a SHA-256 content-addressed canonical JSON, an RFC 3161 trusted timestamp from a public TSA, and an inclusion proof on a public transparency log (Sigstore Rekor). The signing key is bound to a published trust registry under the publisher's own well-known path. 2. **Capture-method discipline.** Every package declares, in a field covered by the canonical-JSON hash and therefore by the signature, *how* its content was captured. The label is structural and tamper-evident: a verifier can tell a verbatim wire-layer capture from a JSONL-layer readback from a paraphrased self-report. Future capture methods extend the vocabulary; the discipline holds. 3. **The typed-node ontology** *(specified; operationalization per sub-type via downstream ADRs).* Every conformant signed node is a signed envelope over a typed payload, drawn from **two top-level type families** that share a single structural primitive: **`content/*`** (standalone assertions; no `targetNodeId` on the payload — analyses, typed claims, questions, evidence records, host self-declarations, tool author declarations) and **`attestation/*`** (assertions about another node; `targetNodeId` required — lifecycle, reference, claim-to-claim, and authority-bearing relations). Hosts, tools, and certifying bodies fold in as sub-types of one of the two families per the [Q36](open-questions.md#q36--attestation-sub-type-collapse-regular-family-or-structured-hierarchy) ratified table — not as peer families (the prior four-families framing is demoted). Within `content/*`, the QEC sub-ontology — `metadata.contentType` set-valued across `claim` / `question` / `evidence` / `untyped` — is the most-developed sub-area today. Nodes can carry signatures from different parties — individuals, hosts, certifying bodies, third-party attesters — and these signatures layer rather than collapse into a single trust authority. A small relations vocabulary (`supportedBy`, `opposedBy`, `answersQuestion`, `corroborates`, `contradicts`, `supersedes`, `wasDerivedFrom`, etc.) is carried by `attestation/*` sub-types. The **normative preamble** (§5.1) applies across all three commitments and across every implementation: corroboration ≠ truth, contradiction ≠ falsity, identity strength ≠ topic authority, the system surfaces signals and the consumer applies judgment. The preamble is the architectural guardrail against drift toward automated truth-scoring; every product surface, downstream consumer, and third-party implementation MUST carry it. **Deliberately silent about:** 1. **Truth.** The signature attests that the package was published and has not been altered. It does not attest that the content is correct. Editorial review, fact-checking, replication, and adversarial evaluation are *separately-signed attestations* carried in the network around the envelope, never enforced by it. 2. **Editorial policy.** Publishers set their own filters, audiences, and review processes. The standard does not gate publication on topic, viewpoint, or sign-off. 3. **Topology.** Publishers publish at their own domains. The standard does not require — and is structurally indifferent to — any central host, federation substrate, or coordination protocol beyond an optional indexing registry that does not host or gatekeep. Four adjacent categories this specification is sometimes mistaken for, and is not: - **Not fact-checking.** Nothing here scores whether content is true; production-process attestation is orthogonal to fact-check tagging (Schema.org ClaimReview covers that; see §5.5). - **Not credentialing.** Identity binding surfaces *who signed, at what binding strength*; it does not accredit signers, certify expertise, or issue credentials. - **Not a knowledge-graph format.** Typed claims are JSON-LD and compose with RDF tooling, but the specification standardizes the signed envelope and capture-method discipline, not a graph data model. - **Not a CMS or a SaaS product.** Publishers host their own content at their own domains; the reference implementation is one instance, not a hosted service offering. ### 5.4 Project posture - **Permissionless publishing.** Publishers publish at their own domains. An institutional publisher's domain is its sovereignty boundary; an independent publisher's domain is theirs. The standard specifies the envelope; it does not host content. - **Indexing, not gatekeeping.** The reserved publisher registry indexes declared publishers — it does not approve them, host them, rank them, or vouch for their content. Inclusion is informational. - **Graded identity surfaced, not computed.** Identity binding tiers (pseudonymous → OAuth-bound → academic-bound → institution-DNS-bound → notarized) are surfaced as signals consumers can filter on. The standard never computes a platform-issued trust verdict from them. - **Don't build until an adopter needs it.** Project discipline per [`xanadu-doctrine.md`](xanadu-doctrine.md): items move from reserved → specified → built only when a real adopter or package concretely needs the change. This specification sketches reserved layers; it does not promote any of them. Promotions happen separately, with the motivating adopter named in the work that promotes the item. ### 5.5 Relationship to adjacent standards Typed Standards occupies cryptographic-provenance terrain alongside several adjacent standards. Two of those — C2PA and W3C Verifiable Credentials — carry "claim" terminology that overlaps with Typed Standards' usage in ways that matter to first-impression reading. Those disambiguations appear inline in this section; smaller cosmetic disambiguations (project name vs. TypeScript / ISO/IEC TS document class; prefix choice vs. RFC 3161 TSS) appear in §6.3. **Relationship to C2PA.** The Coalition for Content Provenance and Authenticity (C2PA) defines a cryptographic provenance standard for images, videos, and other media — capture-device authenticity, edit history, and signed assertions about media assets, serialized as COSE-signed JUMBF boxes. Typed Standards is structurally the same idea applied to a different artifact class: cryptographically signed records of how an *analytical* artifact was produced, serialized as JCS-canonicalized JSON. C2PA reads "claim" as a signed bundle of assertions over a media asset; "assertion" as a single typed statement inside that bundle; "manifest" as the signed package as a whole. Typed Standards reads "claim" in the W3C-PROV / Verifiable-Credentials sense — a single first-class signed assertion about a subject (`content/claim/v1`); "envelope" as the structural primitive that carries any signed node; and "attestation" as a separately-signed assertion about another node. The two specs are equivalent at the role level (both produce signed, content-addressed, transparency-logged records); they differ at the term level. Where this specification refers to C2PA constructs, it spells them as "C2PA claim" and "C2PA assertion" to keep the cross-spec vocabulary unambiguous. A C2PA-to-Typed-Standards term-translation table appears in Appendix D. **Relationship to W3C Verifiable Credentials.** The W3C Verifiable Credentials Data Model is a general signed-claim format for credentials issued in a three-party (issuer / holder / verifier) ecosystem. A VC's "claim" is a property about a subject inside a credential (e.g., `dateOfBirth: 2010-01-01`); a "credential" is a set of such claims with the issuer's signature; a "presentation" is a holder's selective disclosure to a verifier. Typed Standards' "claim" (`content/claim/v1`) is closer in granularity to a complete VC than to a single VC claim: it is a standalone, separately-signed analytical assertion with its own envelope, provenance graph, confidence statement, and identity binding. The structures are JSON-LD compatible; a Typed Standards claim can in principle be expressed as a VC whose subject is the analytical artifact. The three-party VC model (issuer asserts about subject; holder presents to verifier) does not cleanly map to Typed Standards' publisher-and-verifier-only model. Where this specification refers to a property inside a VC credential, it uses "VC claim" explicitly; the unqualified term "claim" in this specification always means a Typed Standards `content/claim/v1` node. **Comparison table.** A summary of Typed Standards' relationship to the broader cryptographic-provenance and signed-claim ecosystem appears below; an extended version with structural mapping notes appears in Appendix C. | Standard / framework | Relationship to Typed Standards | |---|---| | **Discourse Graphs** | Source of the QEC pattern. Typed Standards adopts the claim-question-evidence content types and the `supportedBy` / `opposedBy` / `answersQuestion` relations with attribution to **Joel Chan** and the Discourse Graphs community. | | **Nanopublications** | Closest semantic match for atomic signed claims with provenance. Nanopubs use an RDF-named-graph format for the assertion + provenance + publication info triplet. Typed Standards is envelope-first and content-addressable with capture-method discipline; consuming Typed Standards content as nanopublications is a plausible bridge but a separate effort. | | **W3C PROV-O** | Used directly. Every package's provenance graph is PROV-O JSON-LD; the envelope does not redefine derivation, attribution, or generation. | | **W3C Verifiable Credentials** | Adjacent. See disambiguation paragraph above. VC-over-MCP-tool-call receipts are a candidate trace-capture layer for the envelope's trace slot. | | **Schema.org Claim / ClaimReview** | Different problem. Schema.org's fact-check vocabulary tags claims with human fact-check reviews. Typed Standards attests to *how the artifact was produced*, not whether a fact-checker endorsed it. The two can coexist. | | **C2PA** | Closest structural analogue in a different domain. See disambiguation paragraph above. | | **in-toto / DSSE** | Direct alignment at the structural level. Typed Standards adopts in-toto's multihash DigestSet convention and a predicate-type-URI pattern modeled on in-toto's `predicateType`. Divergence: in-toto attestations are consumed by automated policy engines; Typed Standards envelopes are consumed by readers exercising judgment. | | **SLSA** | Adjacent but disjoint. SLSA Provenance is a specific in-toto predicate type for software builds. Typed Standards covers production-process attestation for analytical artifacts (charts, syntheses, claims). Conceptual alignment, different artifact class. | | **Sigstore (Cosign, Fulcio, Rekor)** | Foundational infrastructure, not a competitor. Typed Standards uses Sigstore Rekor for transparency log inclusion per §8.3.2; Sigstore Fulcio keyless OIDC is a candidate identity tier per [Q3](open-questions.md#q3--first-non-github-identity-provider). | | **RO-Crate / WRROC** | Candidate package container. The end-state direction for the package format is a multi-file directory with an RO-Crate / WRROC compatibility profile per [Q1](open-questions.md#q1--package-format). The cryptographic mechanics are independent of the container choice. | | **DCAT / open-data catalogs** | Different layer. DCAT describes datasets for catalog discovery. Typed Standards describes *analyses produced from those datasets*; data-source references inside a package may cite DCAT-described datasets. | --- ## 6. Conventions and Terminology ### 6.1 Conformance keywords Conformance language follows [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) keywords (MUST, MUST NOT, SHOULD, MAY) when applied to normative requirements. Every normative requirement in this document corresponds either to a check enforced in the reference implementation at `civic-ai-tools-website` code today or to a settled Architectural Decision Record in `civic-ai-tools/docs/adr/`. Where neither holds, the section is labeled with a callout pointing at the relevant open question in [`open-questions.md`](open-questions.md). ### 6.2 Glossary Terms below are used with the meanings given. **Normative** terms have specific meaning when used in conformance language; **informative** terms are used descriptively. - **Record package** *(normative)*: A signed node in the system, carrying the structural-primitive fields plus sub-type-specific payload fields. Today's reference implementation produces `content/analysis/v1` nodes (the default content sub-type — legacy and `datHere` shapes both map to it, as does `scripted-recomputation` output, §8.7.5); the broader concept is "signed node." Identified by its envelope hash (see `nodeId`) and stored at a content-addressable URL. See §8.1 for structure. Named "evidence package" before the 2026-08-19 vocabulary settlement (Appendix J): the prior-era name remains valid wherever it is frozen inside already-published records and documentation, and is distinct from the retained epistemic sense of "evidence" (the QEC role — `content/evidence/v1`, the `contentType` value `"evidence"`; see §6.3). - **Signed node** *(normative)*: Any conformant signed object in the system — a `content/*` node (standalone assertion; no `targetNodeId`) or an `attestation/*` node (assertion about another node; `targetNodeId` required). - **`type`** *(normative)*: The URI declaring a signed node's family + sub-type. Required v0.1; pre-v0.1 packages are interpreted as `content/analysis/v1` by construction. Form: `content//v` or `attestation//v`. - **`content/*` namespace** *(normative)*: The top-level type family for **standalone assertions** — nodes whose payloads do NOT carry `targetNodeId`. Sub-types include `content/analysis/v1` (built; the content type of the AI-Assisted Analysis Producer Profile's output and of the `scripted-recomputation` Producer Profile's output, §8.7.5), `content/claim/v1`, `content/question/v1`, `content/evidence/v1`, `content/host/v1`, `content/hostPolicy/v1`, `content/hostTermsOfUse/v1`, `content/tool/v1` (reserved name-only). - **`attestation/*` namespace** *(normative)*: The top-level type family for **assertions about another node** — nodes whose payloads carry at least one `targetNodeId`. The v0.1 sub-type table — `attestation/withdraws/v1`, `attestation/reinstates/v1`, `attestation/supersedes/v1`, `attestation/revises/v1`, `attestation/publishes/v1`, `attestation/locatedAt/v1`, `attestation/corroborates/v1`, `attestation/contradicts/v1`, `attestation/endorses/v1`, `attestation/wasDerivedFrom/v1`, `attestation/answersQuestion/v1`, `attestation/supportedBy/v1`, `attestation/opposedBy/v1`, `attestation/certifies/v1`, `attestation/evaluates/v1`, `attestation/conforms/v1` — is ratified. Operationalization per sub-type lands via downstream ADRs. - **`nodeId`** *(normative)*: A signed node's stable identity in the system — the envelope hash, by construction. Derived (not a separately-stored field). `attestation/*` payloads carry `targetNodeId` referencing the target's `nodeId`. Verifier semantics: cross-check the recomputed envelope hash matches the URL slug, any stored envelope hash, and (for any referencing attestation) the `targetNodeId` field. - **`signer`** *(normative)*: An object on the canonical JSON top level carrying identity binding for the party that signed the node — `bindingTier` (one of `pseudonymous`, `oauth`, `orcid`, `did-web`, `notarized` per the §8.5 graded identity ladder, or `platform`, the reference implementation's operator value used in the §8.3.3 registry example; extensible), `identifier` (provider-prefixed string, or a key-derived `did:key` identifier per §8.5.1), `displayName`, optional `verifiedAt`. Recommended v0.1; pre-v0.1 packages derive `signer` from the trust registry's `signerIdentity` entry for the envelope's `kid`. Distinct from the `sig` (signature envelope); the verifier MUST cross-check `sig.kid → trust-registry signerIdentity` against `signer.identifier`, or, when `signer.identifier` is key-derived, recompute the identifier from the envelope's `publicKey` and compare (§8.5.1). - **Content hash** *(normative)*: The multihash digest set fingerprinting the package's off-log content, canonicalized per the rule named in `contentCanonicalization`. Serialized as a JSON object keyed by lowercase algorithm name (e.g., `{"sha256": "...", "blake3": "..."}`); v0.1 vocabulary is `sha256` (required default), `sha3-256` (registered alternate), `blake3` (registered alternate). Embedded in the canonical JSON as the top-level `contentHash` field. pre-v0.1 packages emit a single SHA-256 hex string externally (URL slug + DB row) instead of an embedded field; verifiers interpret the legacy form as `{"sha256": }`. - **Envelope hash** *(normative)*: The SHA-256 hex digest of the [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) JSON Canonicalization Scheme (JCS) canonicalization of the unsigned envelope. The unsigned envelope is the canonical-JSON package object with the signature envelope removed. pre-v0.1 packages used Node.js `JSON.stringify` insertion-order serialization; verifiers handle them under that legacy rule. - **Content canonicalization** *(normative)*: The URI naming the rule by which off-log content reduces to bytes that `contentHash` fingerprints. Carried as the top-level `contentCanonicalization` field on the canonical JSON; covered by the envelope hash and the platform signature. v0.1 reserved URIs: `https://typedstandards.org/canonicalization/dathere-ag-jupyter/v1` (datHere A-G/Jupyter content profile), `https://typedstandards.org/canonicalization/legacy-json/v1` (legacy default content profile), and `https://typedstandards.org/canonicalization/raw-bytes/v1` (a file's bytes as it holds them; §8.2). - **Signed envelope** *(normative)*: The envelope-hash hex string, signed with Ed25519ph by a key in the trust registry. The envelope JSON object stored alongside the package also carries `publicKey`, `algorithm`, and `kid` fields for verifier convenience; signature math binds only the hash bytes. The envelope hash is computed over the JCS canonicalization of the unsigned envelope; pre-v0.1 packages used `JSON.stringify` insertion-order canonicalization, and verifiers handle them under that legacy rule. - **Trust registry** *(normative)*: The JSON document at `${baseUrl}/.well-known/typed-publisher.json` that lists authorized signing keys with lifecycle metadata. Reference implementations SHOULD also serve the same JSON at the legacy path `${baseUrl}/.well-known/evidence-public-keys.json` for backwards-compatibility with pre-v0.1 fetchers. See §8.3.3. - **`kid` (key identifier)** *(normative)*: A stable string identifying a signing key (e.g. `platform:evidence-2026-04`), present in both the signed envelope and the trust registry. The `kid` is part of the canonical package JSON via `metadata.signingKeyId`, so it is covered by the envelope hash and therefore by the platform signature. The example is the reference implementation's live kid, which is **exempt-frozen** under the 2026-08-19 vocabulary settlement (Appendix J): it appears inside every already-signed envelope, so it is never renamed; the next key rotation names its key under the new vocabulary, and no rotation is forced. - **BlobRef** *(normative)*: A four-field JSON object `{ ref, url, contentType, size }` that names a content-addressable Vercel Blob (or equivalent content-addressable storage) in place of inline content for selected fields. See §8.1.5. - **`captureMethod`** *(normative)*: The label identifying *how* the package's content was captured — the integrity-of-pipeline property. The field is required, signed, and tamper-evident. Its **value space is open at the core level**; the vocabulary of valid values is declared by the package's `producerProfile`'s guidance bundle. For the `ai-assisted-analysis` Producer Profile, the v0.1 vocabulary is `chat-flow-stream`, `claude-code-jsonl-readback`, `claude-code-self-report` — the three values originally enumerated in core by ADR-0003 and relocated to this profile's guidance bundle. For the `scripted-recomputation` Producer Profile, the vocabulary is `script-run`, `tool-emitted`. See §8.6. - **`contentProfile`** *(normative)*: The label identifying *what shape* the package's content is in — the content-shape property. Orthogonal to `captureMethod`. Values: `"default"` (legacy shape; absence treated as default) or `"datHere"` (A-G envelope content profile per §8.7). Carried inside the `metadata` object (`metadata.contentProfile`, §8.1.2). See §8.1 and §8.7. - **`producerProfile`** *(normative)*: The Producer Profile the package conforms to. Compound-string value of the form `/`. v0.1 vocabulary includes `"ai-assisted-analysis/datHere"` (first realized subtype; refactor of the `datHere` content profile) and the profile type `scripted-recomputation` (deterministic tool output, §8.7.5), whose subtypes the producer names. Other profile types (`human`, `hybrid`, `sandbox-only`) and subtypes are reserved name-only. Consistency invariant, compared only when both fields are present: `metadata.contentProfile === "datHere"` iff `producerProfile.startsWith("ai-assisted-analysis/datHere")`. - **Trace** *(informative)*: An OpenTelemetry-shaped JSON object (or BlobRef) describing the spans of the analysis. See §8.4. - **PROV-O graph** *(informative)*: A W3C PROV-O JSON-LD graph derived from the trace at publish time. See §8.1.4. - **Withdrawal / reinstatement** *(normative)*: Signed, public, append-only lifecycle events on a published node, expressed as separately-signed `attestation/withdraws/v1` / `attestation/reinstates/v1` nodes referencing the target by `nodeId`. See §8.10. - **Verifier** *(informative)*: Any party performing the checks described in §9 against a fetched node. A broader vocabulary covering the surrounding architectural standards (PROV-O, Croissant, RO-Crate, atproto, KOI, nanopublications, etc.) is defined in [`end-state-vision.md`](end-state-vision.md) Glossary and not duplicated here. ### 6.3 Disambiguated terms **Project name.** "Typed Standards" is unrelated to TypeScript (the Microsoft-stewarded typed superset of JavaScript) or to the `.ts` / `.tsx` file extensions. The `ts:` JSON-LD prefix resolves to `https://typedstandards.org/ns/ts#`; it does not refer to TypeScript types, type-definition files, or the ECMA-262 / TC39 ecosystem. Separately, ISO/IEC and W3C use "TS" as a document-class abbreviation in publication identifiers (e.g., `ISO/IEC TS 22237`, `W3C TS-…`); a "Typed Standards specification" is a project name in this specification's sense, not an ISO/IEC or W3C document-class marker. **Prefix choice — relationship to RFC 3161 timestamping.** This specification uses RFC 3161 trusted timestamps as a cryptographic-envelope component (§8.3.2). The shorthand "TSS" is sometimes used in the timestamping literature for "Time-Stamping Server" or "Time-Stamping Service" — a different concept (an external service that issues `TimeStampToken`s) from "Typed Standards." This specification reserves "Typed Standards" and the `ts:` prefix for the project itself, and uses the spelled-out terms "Time-Stamping Authority (TSA)" and "TimeStampToken (TST)" per RFC 3161 §1 for the timestamping subsystem to keep the two concepts unambiguous. The `tss:` prefix was considered and rejected on these grounds. **`contentProfile` — one name, two documented senses.** In the §8.1.2 `metadata` field table and §8.7, `contentProfile` is the package **field** naming the content *shape* (`"default"` | `"datHere"`). In the §7.1 architecture diagram, "Content profiles" names the typed-content **carrier axis** (the Typed Claims / Typed Evidence / Typed Questions profiles). The two are related but not the same thing, and the collision is known and deliberate: [ADR-0006](../adr/0006-producer-profile-architecture.md) records the clean split — `contentProfile` reserved for typed-content carriers, `producerProfile` for the production-shape axis — as **deferred**, because renaming the live field is a breaking change tied to the [Q27](open-questions.md#q27--schema-version-bump-trigger-for-the-oes-spec) version bump. Until that bump, read `contentProfile` (code font, the field) in the shape sense, and "content profiles" (prose, the axis) in the carrier sense. **"Evidence" — two roles; one excised, one retained (the 2026-08-19 vocabulary settlement, Q50/Q66).** The word "evidence" historically played two unrelated roles in this specification and its surrounding infrastructure, and the vocabulary settlement recorded in Appendix J separates them on principle: - **As the artifact / infrastructure brand** — the package noun ("evidence package"), the reference implementation's route segments, environment-variable prefix, wire version key, exported type and function names, OAuth scope, skill name, and sidecar filename — the word **overclaims**. A signed record under this specification shows *how* an answer was produced; it does not show that the answer is correct, and "evidence" in the brand position invites exactly the corroboration-equals-truth reading the §5.1 normative preamble exists to prevent. This role is **excised**: the artifact is a **record package**, and the infrastructure surfaces migrate to `record`/`records` vocabulary under the per-surface migration classes in Appendix J. Prior-era names remain valid per the dual-era rules there — nothing already published, linked, or signed stops resolving. - **As the epistemic role in the typed-claims triad** — Question / Evidence / Claim (`content/evidence/v1`; the `contentType` value `"evidence"`; the `supportedBy` / `opposedBy` relations whose targets are evidence nodes) — the word is **precise and retained**. Content serving as *evidence for a claim* is the legitimate, Discourse-Graphs-derived use (§7.5), and it is unaffected by the settlement. The unqualified word "evidence" in this specification therefore always means the epistemic QEC role. The prior-era artifact-brand usages survive only where they are frozen (inside signed artifacts, published URLs, and historical records) and are enumerated, with their migration classes, in Appendix J. --- ## 7. Architecture ### 7.1 Layered shape The layered shape the umbrella sits over. Color encoding follows the convention in [`end-state-vision.md`](end-state-vision.md): **green = built**, **yellow = partial**, **orange = reserved (designed but not implemented; or proposed in this document and not yet in the existing drafts)**. ```mermaid flowchart TB REG["Publisher registry
(coordination index — names publishers,
does not host or gatekeep)"]:::reserved DX["Domain extensions
(specialize a content profile for a domain)
civic data analysis = first domain extension"]:::reserved subgraph PROF["Profiles — orthogonal axes"] direction LR CP["Content profiles
(Typed Claims / Typed Evidence /
Typed Questions)"]:::partial PP["Producer profiles
(AI-Assisted Analysis specified
with subtype/flavor model per ADR-0006;
'ai-assisted-analysis/datHere' subtype = built;
'civicaitools-default' subtype = reserved;
human / hybrid / sandbox-only types = reserved)"]:::partial end subgraph TYPED["Two-family taxonomy — semantic distinction (per ADR-0009)"] direction LR CONTENT["content/* family
(standalone assertions —
no targetNodeId on payload;
'content/analysis/v1' = built;
typed-content sub-types — claim / question / evidence — reserved;
host / hostPolicy / hostTermsOfUse / tool sub-types reserved per Q22)"]:::partial ATTESTATION["attestation/* family
(assertions about another node —
targetNodeId required on payload;
v0.1 Q36 sub-type table ratified by ADR-0009 §7;
operationalization per sub-type via downstream ADRs)"]:::partial end ENV["Structural primitive
(content-agnostic cryptographic core:
type URI · nodeId (≡ envelope hash) · contentHash multihash · contentCanonicalization URI ·
sig (publicKey + algorithm + kid) · signer (identity binding) · timestamp ·
transparency log · trust registry · captureMethod · withdrawal lifecycle)"]:::built REG -.indexes.-> PROF DX -.specializes.-> CP PROF --> TYPED TYPED --> ENV classDef built fill:#86efac,stroke:#166534,color:#000 classDef partial fill:#fde68a,stroke:#92400e,color:#000 classDef reserved fill:#fdba74,stroke:#9a3412,color:#000 ``` **How to read.** The structural primitive at the bottom is the cryptographic core: every signed node carries the same envelope shape. The two-family taxonomy — `content/*` (standalone assertions) and `attestation/*` (assertions about another node) — sits over that primitive; sub-types within each family are an open enum extensible per the [Q36](open-questions.md#q36--attestation-sub-type-collapse-regular-family-or-structured-hierarchy) ratified table. Producer profiles and content profiles are two independent axes describing *who/how produced the package* and *what shape the content takes*. v0.1 specifies the `ai-assisted-analysis/datHere` subtype; Human / Hybrid / Sandbox-only profile types are reserved pending real adopters. Domain extensions specialize a content profile for a subject domain; civic data analysis is the first. ### 7.2 Envelope The envelope mechanics are content-agnostic; the content slot is swappable per content profile. Canonicalization comes in two kinds: **envelope-level** canonicalization is a single fixed rule (RFC 8785 JCS) committed to by the spec; **content-level** canonicalization legitimately varies per content shape and is named by the envelope's `contentCanonicalization` URI field. The envelope-hash (SHA-256 over JCS-canonicalized unsigned envelope) is what the signature covers; the multihash `contentHash` field fingerprints the off-log content per the named rule and is itself embedded in (and therefore covered by) the envelope. ```mermaid flowchart TB subgraph ENV["Envelope (content-agnostic)"] ENVH["Envelope hash
(SHA-256 of JCS-canonicalized
unsigned envelope)"]:::partial HASH["Multihash contentHash
(sha256 required + sha3-256 / blake3 alts;
over canonical content per
contentCanonicalization rule)"]:::partial CANON["contentCanonicalization URI
(names content-level
canonicalization rule)"]:::reserved SIG["Ed25519ph signature
over envelope-hash hex string"]:::built TS["RFC 3161 trusted timestamp
from public TSA"]:::built REK["Sigstore Rekor entry
+ inclusion proof"]:::built CM["captureMethod label
(in canonical JSON, signed)"]:::built CTL["contentType label
(set of QEC values,
in canonical JSON, signed)"]:::reserved PROV["W3C PROV-O graph
(derived from trace at publish)"]:::built TR["Execution trace
(OTel-shaped, hand-rolled)"]:::partial end subgraph CONT["Typed nodes (content shown; host / tool / attestation reserved)"] CL["Claim"]:::reserved QU["Question"]:::reserved EV["Evidence"]:::reserved UN["Untyped
(raw, mutually exclusive
with the others)"]:::reserved OTHER["Host · Tool/method · Attestation
(reserved node families)"]:::reserved end CONT -.canonicalized per.-> CANON CONT --> HASH HASH -.embedded in.-> ENVH CANON -.embedded in.-> ENVH CM -.embedded in.-> ENVH CTL -.embedded in.-> ENVH SIG -.covers.-> ENVH TS -.covers.-> ENVH REK -.indexes.-> ENVH PROV -.about.-> CONT TR -.captures.-> CONT classDef built fill:#86efac,stroke:#166534,color:#000 classDef partial fill:#fde68a,stroke:#92400e,color:#000 classDef reserved fill:#fdba74,stroke:#9a3412,color:#000 ``` Today the content slot carries an AI-assisted civic-data analysis (prompt, queries, outputs, costs, skill metadata, optional notebook under the `datHere` content profile). The proposed restructure treats that content as **a set of typed content blocks**, with a new `metadata.contentType` field carrying the set of QEC values present — drawn from `claim`, `question`, `evidence`, or `untyped`. The most common shape is `["claim"]`; a claim that explicitly carries the question it answers is `["claim", "question"]`; raw assistant output not yet processed against any content profile is `["untyped"]`. `untyped` is mutually exclusive with the typed values. Per-block requirements (provenance, confidence, scope, AnalyticalDerivation for claims) do not relax when the set has more than one member — a multi-type package is several conformant typed blocks side-by-side, not a looser format. The envelope's hash, signature, timestamp, transparency-log entry, capture-method label, contentType label, contentCanonicalization URI, multihash content hash, provenance graph, and trace bind whatever typed node is inside; the envelope mechanics do not change when the node type or content shape changes (host, tool/method, and attestation node families are reserved alongside the content family). Different content shapes vary the `contentCanonicalization` URI; the envelope-level JCS commitment is invariant. ### 7.3 Verification flow A verifier can complete every check using only public infrastructure plus the publisher's own trust registry. No central authority is required, and no `typedstandards.org` lookup appears in the verification path. ```mermaid sequenceDiagram autonumber participant V as Verifier participant Pub as Publisher's domain
(hosts package + registry) participant TSA as Public TSA
(e.g. FreeTSA) participant Rkr as Sigstore Rekor V->>Pub: GET package + envelope
(at publisher's content-addressable URL) Pub-->>V: package JSON + signature + timestamp + Rekor entry id V->>V: recompute SHA-256 over canonical JSON
compare with package URL slug V->>Pub: GET /.well-known/typed-publisher.json
(publisher's trust registry) Pub-->>V: public keys + lifecycle status
(active / deprecated / revoked) V->>V: verify Ed25519ph signature against
registry-listed public key V->>TSA: verify RFC 3161 timestamp token
against TSA's CA chain V->>Rkr: verify Rekor inclusion proof V-->>V: render verdict
(signature ✓, timestamp ✓, log entry ✓,
captureMethod label, identity tier) Note over V,Pub: Decentralized publishing.
Publisher hosts its own packages and its own trust registry. Note over V,Rkr: No central authority.
The verifier never trusts the publishing platform
and never trusts the Typed Standards body. ``` **The decentralized-publishing / central-indexing split.** Each publisher hosts its own packages and serves its own trust registry at a well-known path on its own domain. The reserved publisher registry at `typedstandards.org` indexes declared publishers (a directory function) but is not in the verification path: a verifier never queries `typedstandards.org` to verify a package, and the index has no authority to vouch for or reject any publisher's content. This is the deliberate inversion of the brand-mediated-trust model: trust is in the cryptography and the publisher's identity binding, not in the standards body or any host. > **What's reserved vs. what's built in this flow.** The trust-registry well-known path shown above — `/.well-known/typed-publisher.json` — is the canonical v0.1 path; reference implementations SHOULD also serve the same JSON at the legacy path `/.well-known/evidence-public-keys.json` for backwards-compatibility with pre-v0.1 fetchers (parallel-serve indefinitely; no forced cutover). Offline verification — package content plus the publisher's trust registry plus the TSA and Rekor, with no dependency on the publishing platform — is a **demonstrated property of the self-contained commitment bundle** (§8.8 `?inline=1`; see §9.4). The single-blob package alone does not embed its proofs, so a bare package still relies on a proof carrier — either its §8.8 commitment view / bundle, or the reference implementation's server-composed verify endpoint, which assembles the signature envelope, RFC 3161 token, and Rekor proof from a database row. Whether the package format itself should embed those proofs is [Q1](open-questions.md#q1--package-format). ### 7.4 Two-family taxonomy — one structural primitive, content/* and attestation/* **The structural primitive.** Every conformant signed node is a signed envelope over a typed payload, carrying the structural-primitive fields specified: a `type` URI (identifying the node's family + sub-type), a derived `nodeId` (the envelope hash by construction), a multihash `contentHash` (fingerprinting the off-log payload), a `contentCanonicalization` URI (naming the off-log content's canonicalization rule), a signature envelope (`sig` per §8.3.1 — public key + algorithm + kid), a `signer` object (identity binding), an RFC 3161 timestamp, a Sigstore Rekor inclusion proof, and the `metadata` object. Sub-type-specific payload fields live alongside the primitive at the canonical-JSON top level (for `content/analysis/v1`, that means `prompt` / `queries` / `output` / `trace` / etc.; for `attestation/*` sub-types, that means `targetNodeId` plus sub-type-specific fields). **Two top-level type families.** Every conformant signed node belongs to exactly one of two families, distinguished by the `type` URI's first path segment: - **`content/*`** — *standalone assertion.* The node asserts something the signer takes responsibility for (an analysis, a typed claim, a question, an evidence record, a host's own identity, a tool author's tool declaration). It does NOT carry `targetNodeId` on its payload; it MAY cite or reference other nodes via PROV-O-style `wasDerivedFrom` provenance, but those references are upstream provenance, not the assertion's subject. - **`attestation/*`** — *assertion about another node.* The node carries at least one `targetNodeId` referencing the node it asserts about. It does not stand alone — without its target the assertion has no subject. Sub-types cover lifecycle (withdraws / reinstates / supersedes / publishes), reference (locatedAt / wasDerivedFrom / answersQuestion / supportedBy / opposedBy), claim-to-claim (corroborates / contradicts / endorses), and authority-bearing (certifies / evaluates / conforms) relations. The presence (or absence) of `targetNodeId` on the payload is the structural rule that decides which family a node belongs to. Hosts are not a separate family — host self-declarations are `content/host/v1` or `content/hostPolicy/v1` (the host is asserting something about itself, no other node referenced); host endorsements of others' content are `attestation/endorses/v1`. Tools / certifying bodies are not a separate family — a tool author's declaration is `content/tool/v1`; a certifying body's attestation about that tool is `attestation/certifies/v1`. **Sub-type URI format.** Sub-type URIs use the form `content//v` for `content/*` sub-types and `attestation//v` for `attestation/*` sub-types. Sub-types are an open enum; new sub-types arrive via subsequent ADRs that name the motivating adopter per the Xanadu doctrine. The registry mechanism (how sub-type URIs are documented, versioned, mirrored, deprecated, governed across implementations) stays Xanadu-gated per [Q37](open-questions.md#q37--type-registry-mechanism-and-governance-for-the-content-and-attestation-namespaces) — specifying the registry mechanism prematurely is the foundational-layer version of the over-design the Xanadu doctrine exists to prevent. **Q36 ratified sub-type table.** The v0.1 attestation sub-type table — `attestation/withdraws/v1`, `attestation/reinstates/v1`, `attestation/supersedes/v1`, `attestation/publishes/v1`, `attestation/locatedAt/v1`, `attestation/corroborates/v1`, `attestation/contradicts/v1`, `attestation/endorses/v1`, `attestation/wasDerivedFrom/v1`, `attestation/answersQuestion/v1`, `attestation/supportedBy/v1`, `attestation/opposedBy/v1`, `attestation/certifies/v1`, `attestation/evaluates/v1`, `attestation/conforms/v1` — is ratified with three explicit refinements: `extractsTo` merges into `wasDerivedFrom` (with `AnalyticalDerivation` as the content-shape variant when source is untyped and target is typed); `endorses` and `corroborates` stay distinct sub-types (peer attestation vs. institutional endorsement carries meaningfully different signal); and Q38 resolves with `locatedAt` suffices, no `copyOf` sub-type. Each sub-type declares its authorization rule (`publisher-only`, `any-with-binding`, or `specific-role-required`) and its payload shape; the full table lives and §8.12 of this specification. Corresponding ratified `content/*` sub-types are `content/analysis/v1` (built — the legacy and datHere content shapes both map to it), `content/claim/v1` / `content/question/v1` / `content/evidence/v1` (reserved name-only — promotion gated on first typed-content producer), `content/host/v1` / `content/hostPolicy/v1` / `content/hostTermsOfUse/v1` (reserved name-only per [Q22](open-questions.md#q22--host-as-typeable-subject--host-self-attestation-shape)), and `content/tool/v1` (reserved name-only). **Layered signatures across typed nodes.** A package's nodes may carry signatures from different parties at different scopes — the producer who created the `content/*` node, a host that endorses it via `attestation/endorses/v1`, a certifying body that attests to a tool's conformance via `attestation/certifies/v1`, third parties that corroborate or contradict via `attestation/corroborates/v1` / `attestation/contradicts/v1`. These signatures **layer** rather than collapse: a verifier sees who signed what and at what scope, never a single composite verdict. The specification specifies how multiple signers and node types compose verifiably without forcing a single trust authority — and the `signer.identifier` ↔ `sig.kid → trust-registry signerIdentity` cross-check makes the layering tamper-evident at the verifier level. ### 7.5 The QEC sub-ontology within content/* Within the `content/*` family, the QEC sub-ontology specifies the typed-content sub-types — `content/claim/v1`, `content/question/v1`, `content/evidence/v1` — alongside `content/analysis/v1` (the default for AI-Assisted Analysis Producer Profile output,). A `content/analysis/v1` node's `metadata.contentType` is a set drawn from four values: - `claim` — one or more conformant claims (assertions the producer is making) - `question` — one or more conformant questions (asked but not-yet-answered queries) - `evidence` — one or more conformant evidence records (captured observations or analytical artifacts) - `untyped` — the envelope is valid, but the content has not been processed against any content profile yet (raw output pending extraction) `untyped` is **mutually exclusive** with the typed values. There is no `mixed` value; multiplicity is expressed by the set having more than one member. When `contentType` has more than one member, the `content` field carries an array of individually-typed blocks, each conformant to its profile and each retaining its own provenance, confidence, scope, and AnalyticalDerivation (for claims). The AI-Assisted Analysis Producer Profile output is `content/analysis/v1` with `metadata.contentType: ["untyped"]`; subsequent typed-content extraction produces separately-signed `content/claim/v1` / `content/question/v1` / `content/evidence/v1` nodes referencing the source `content/analysis/v1` via `attestation/wasDerivedFrom/v1` (see below). Relations among typed-content sub-types and the untyped source draw from the small set the `attestation/*` family already provides, plus the structural-primitive references inside `content/*` nodes. ```mermaid flowchart LR U["content/analysis/v1
(untyped — raw output)"]:::partial subgraph T["Typed content/* sub-types (reserved name-only)"] Q["content/question/v1"]:::reserved C["content/claim/v1"]:::reserved E["content/evidence/v1"]:::reserved end U -.->|"attestation/wasDerivedFrom/v1
+ AnalyticalDerivation derivationMethod
(classification-laundering guard)"| T C -->|"attestation/answersQuestion/v1"| Q C -->|"attestation/supportedBy/v1 / opposedBy/v1"| E C -.->|"attestation/corroborates/v1 / contradicts/v1 / supersedes/v1
(separately-signed claim → claim)"| C E -.->|"attestation/wasDerivedFrom/v1
(separately-signed evidence → evidence)"| E classDef partial fill:#fde68a,stroke:#92400e,color:#000 classDef reserved fill:#fdba74,stroke:#9a3412,color:#000 ``` **Attribution.** The QEC pattern — claim, question, evidence as the three first-class content types of a discourse representation — is from **Joel Chan's Discourse Graphs work**. The Discourse Graphs community has developed and used QEC for several years as a structural representation of scholarly discourse. Typed Standards' adoption is structurally similar: QEC nodes are `content/*` sub-types; relations among them are separately-signed `attestation/*` nodes between content-addressed packages. The relations vocabulary is intentionally minimal — `wasDerivedFrom` is inherited from W3C PROV-O; `supportedBy` / `opposedBy` are the QEC primitives; `answersQuestion` ties a claim back to a question; `corroborates` / `contradicts` carry the existing claim-to-claim relations forward; `supersedes` carries claim versioning; all of them are `attestation/*` sub-types Domain extensions and producer profiles add domain-specific relations on top; the small core holds. --- ## 8. Normative specification ### 8.1 Record package structure A conformant record package is a single JSON object whose canonical-JSON serialization is the input to the SHA-256 envelope hash. The field table in §8.1.1 mixes two kinds of fields: the **structural-primitive fields** shared by every signed node per §7.4 (`type`, `contentHash`, `contentCanonicalization`, `signer`, plus the envelope-side `sig` object of §8.3.1 and the derived `nodeId`) and the **payload fields** specific to `content/analysis/v1` (`prompt`, `queries`, `dataSources`, `cost`, `skillMetadata`, `output`, `trace`, …). Sub-type-specific payload fields live alongside the primitive at the canonical-JSON top level; see §7.4 for the general rule. > ⚠ **Subject to [Q1](open-questions.md#q1--package-format) — package format.** The current implementation is a single canonical JSON object plus a database-resident envelope. The current direction is a multi-file directory with an RO-Crate / WRROC compatibility profile, in which the canonical JSON object would become one artifact in a larger package. This v0.1 normalizes the single-blob form because that is what the publish path produces today; this section will be revised when the format decision lands. #### 8.1.1 Top-level fields A conformant record package MUST carry every field in the following list. Fields marked optional MAY be omitted; when present, they MUST conform to the type given. | Field | Type | Required | Description | |---|---|---|---| | `metadata` | object | yes | See §8.1.2. | | `prompt` | object | yes | See §8.1.3. | | `queries` | array of objects | yes | One entry per tool call observed during the analysis. May be empty when the analysis made no tool calls. An entry MAY record that the source rejected the call rather than answering it; see §8.1.8, which also states what the absence of that record does and does not mean. | | `dataSources` | array of objects | yes | One entry per data source touched by the analysis, derived from `queries[]` and the trace. A `queries[]` entry recorded as failed contributes no entry here (§8.1.8). May be empty when `queries[]` is empty, and when every entry in `queries[]` records a rejection. | | `cost` | object | yes | Token-usage and timing summary. See §8.1.7. | | `skillMetadata` | object | yes | Skill-guidance hash, MCP server URL, and skill text or BlobRef. | | `output` | string \| BlobRef | yes | The assistant's final response text, or a BlobRef. See §8.1.5. | | `trace` | object \| BlobRef | yes | OpenTelemetry-shaped trace, or a BlobRef to the same. | | `summary` | string | optional | Short, indexable, citation-ready summary of the analysis. Required when `metadata.contentProfile == "datHere"` (see §8.7). When present, part of canonical JSON and therefore covered by the envelope hash and signature. | | `producerProfile` | string | optional | The Producer Profile the package conforms to. Compound-string value of the form `/`. v0.1 vocabulary includes `"ai-assisted-analysis/datHere"` and the profile type `scripted-recomputation` (`scripted-recomputation/`, subtype non-empty and named by the producer; §8.7.5). Other profile types (`human`, `hybrid`, `sandbox-only`) and subtypes are reserved name-only. A top-level envelope field; its grandfathered legacy alias `contentProfile` lives inside the `metadata` object (§8.1.2). Consistency invariant, compared only when both fields are present ([ADR-0006](../adr/0006-producer-profile-architecture.md) §2): `metadata.contentProfile === "datHere"` iff `producerProfile.startsWith("ai-assisted-analysis/datHere")`. | | `contentHash` | object | yes (v0.1) | Multihash digest set fingerprinting the package's off-log content, canonicalized per the rule named in `contentCanonicalization`. Object keyed by lowercase algorithm name (`sha256`, `sha3-256`, `blake3`) with hex digest values; at least one entry required, `sha256` required by default. Verifier semantics: at least one of the listed algorithms' digests MUST match. Pre-v0.1 packages omit the field; the legacy single-SHA-256 hash lives externally (URL slug + DB row) and is interpreted as `contentHash: {"sha256": }` at verify time. | | `contentCanonicalization` | string (URI) | recommended (v0.1) | URI naming the canonicalization rule by which off-log content reduces to bytes that `contentHash` fingerprints. v0.1 reserved values: `https://typedstandards.org/canonicalization/dathere-ag-jupyter/v1`, `https://typedstandards.org/canonicalization/legacy-json/v1`, and `https://typedstandards.org/canonicalization/raw-bytes/v1` (§8.2). Resolution semantics out of scope (URI is an identifier, not a fetch target); verifiers resolve via a local rule registry. Pre-v0.1 packages omit the field; verifiers infer the rule from `contentProfile` / `producerProfile`. | | `type` | string (URI) | yes (v0.1) | The node's family + sub-type identifier per the two-family taxonomy. Form: `content//v` or `attestation//v`. Pre-v0.1 packages omit the field and are interpreted as `content/analysis/v1`. | | `signer` | object | recommended (v0.1) | Identity binding for the party that signed the node. Fields: `bindingTier` (required), `identifier` (required; provider-prefixed string, or a key-derived `did:key` identifier per §8.5.1), `displayName` (required), `verifiedAt` (optional; ISO-8601). Distinct from the `sig` envelope (publicKey + algorithm + kid per §8.3.1); verifier MUST cross-check that `sig.kid` resolves via the trust registry's `signerIdentity` to the same identity `signer.identifier` claims, or, when `signer.identifier` is key-derived, that it equals the identifier recomputed from the envelope's `publicKey` (§8.5.1). | | `vcsRef` | object | optional | Version-control reference recording the source revision the analysis was generated from ([ADR-0016](../adr/0016-vcs-native-lifecycle-mapping.md) §B). Sub-fields: `repoUrl` (required-if-present), `commitSha` (required-if-present; the full immutable revision object id), `path` (optional; path to the source artifact within the repository), `ref` (optional; branch or tag name — a mutable pointer, informative only). A content-family signed self-declaration: a top-level envelope field alongside `producerProfile` / `type` / `signer` (not inside `metadata`), covered by the envelope hash and signature, so the binding is tamper-evident and attributable. The signature attests the *assertion*, not the *fact*: a signed `vcsRef` proves the signer asserted the analysis corresponds to this revision — not that the revision exists, is reachable, or contains what is claimed. Verification is **verify-on-fetch**: a verifier MAY resolve `repoUrl` + `commitSha` and check the artifact at `path` against the node's `contentHash`; a mismatch or unreachable revision is **informative, not a hard failure** (surfaced as a signal per §5.1), mirroring the `attestation/locatedAt/v1` treatment (§8.10.2). The weight a consumer places on an *unverified* `vcsRef` is `captureMethod`-contextualized (§8.6). Distinct from `attestation/locatedAt/v1`: `locatedAt` says where the published artifact is fetchable; `vcsRef` says which source revision the analysis derives from — both MAY be present and point at different things. Threat-model row: §10.1. | | `targetNodeId` | string | conditional | Required for `attestation/*` nodes (the node referenced by the attestation); MUST NOT appear on `content/*` nodes. Some `attestation/*` sub-types carry multiple target references — see the v0.1 sub-type table in §8.12.1 for per-sub-type payload shape. | | `provenance` | object | optional | W3C PROV-O JSON-LD graph derived from `trace` at publish time. Present when the trace was inspectable inline; omitted when `trace` is a BlobRef and no override is supplied. | | `extensions` | object | optional | Reverse-DNS-keyed implementation-specific artifacts (e.g. `org.civicaitools.notebook`, `org.civicaitools.environment`). Included in the canonical JSON and therefore covered by the envelope hash. | #### 8.1.2 `metadata` object | Field | Type | Required | Description | |---|---|---|---| | `schemaVersion` | string | yes | Currently `0.1.0`. | | `packageId` | string (UUID) | yes | A UUID generated at publish time. Distinct from the envelope hash. | | `createdAt` | string (ISO 8601) | yes | UTC timestamp set at packager time. | | `signingKeyId` | string | yes | The `kid` of the signing key. Present in the canonical JSON; therefore covered by the envelope hash. | | `captureMethod` | string | yes (v0.1) | A value in the captureMethod vocabulary of the package's `producerProfile`'s guidance bundle. For the `ai-assisted-analysis` Producer Profile (the v0.1 default), the vocabulary is `chat-flow-stream`, `claude-code-jsonl-readback`, `claude-code-self-report`; for the `scripted-recomputation` Producer Profile, `script-run`, `tool-emitted` (§8.6). Required at the publish route since 2026-04-29. pre-v0.1 packages persist with a `null` capture method on the database row and render with an "Unknown (pre-v0.1)" label. | | `contentProfile` | string | optional | The content profile the package conforms to. Known values: `"default"` (legacy shape; absence treated as default) or `"datHere"` (A-G envelope content profile per §8.7). Orthogonal to `captureMethod`. Extensible — future profiles add ADRs; a new Producer Profile says what it is through `producerProfile` and its subtype, not through a new value here, and the `scripted-recomputation` profile adds none (§8.7.5). A value that is neither known value is reported by §9.2 check #16, not rejected. **Retained as legacy alias v0.1**: a package MAY carry both `metadata.contentProfile` and the top-level `producerProfile`, or only one of them — the signed [ADR-0028](../adr/0028-eval-run-worked-example.md) packages carry only `producerProfile`; verifiers SHOULD prefer `producerProfile` when present; when both are present, the two MUST be consistent (see the `producerProfile` row in §8.1.1). | > **Placement note (2026-08-03, spec-text alignment — no wire change).** Earlier revisions of this document described `contentProfile` as a top-level envelope field. The shipped wire format has always nested it as `metadata.contentProfile` — alongside `metadata.signingKeyId` and `metadata.captureMethod` — with `producerProfile` at the top level as the successor axis (see the produce-core envelope builder, which carries the grandfathered-alias note). The formalization collaborator's formal model documents the wire correctly; this revision aligns the spec text to the shipped format. Packages are byte-identical before and after this alignment. #### 8.1.3 `prompt` object | Field | Type | Required | Description | |---|---|---|---| | `hash` | string (hex) | yes | SHA-256 hex digest of the prompt text. | | `visibility` | string | yes | One of `full_text` or `hash_only`. Enforced at the publish route. | | `text` | string | conditional | The prompt text in clear, present iff `visibility == "full_text"`. MUST be omitted when `visibility == "hash_only"`. | #### 8.1.4 `provenance` object (informative on shape) When present, `provenance` is a W3C PROV-O JSON-LD graph with `@context` mapping the `prov`, `xsd`, `civic`, and `dcterms` prefixes, and an `@graph` array of typed entities, activities, and agents derived from the trace. The graph reflects per-source attribution: each MCP server appears as a `prov:Agent` with a `civic:sourceId`, and each data response is a `prov:Entity` tagged with the same `civic:sourceId`. Skill guidance, the LLM model, and the platform are also represented as agents. In new-era emissions the platform is `urn:civic-record:platform:civic-ai-tools`; graphs emitted before the 2026-08-19 vocabulary settlement carry `urn:civic-evidence:platform:civic-ai-tools`, which remains valid forever — identifiers inside already-signed packages are frozen, and verifiers treat both eras as valid (Appendix J). The `civic:` prefix in existing PROV-O graphs predates the consolidation; new packages MAY emit `ts:` in place of `civic:`, but the `civic:` prefix in already-published packages remains valid (vocabulary URIs are identifiers, not fetch targets, and a future migration is gated on adopter need per [Q10](open-questions.md#q10--civic-claim-vocabulary-as-a-full-ontology) / the OWL ontology promotion). The provenance graph is deterministically derivable from `trace` at the time of packaging. A verifier MAY recompute and compare; the field exists primarily for downstream consumption rather than as a separate verification primitive. #### 8.1.5 BlobRef substitution The fields `output`, `trace`, and `skillMetadata.skillText` MAY be supplied as a BlobRef in place of inline content. A BlobRef is the JSON object: ```json { "ref": "blob:sha256:<64-hex-char SHA-256 of the content bytes>", "url": "https://.public.blob.vercel-storage.com/evidence-refs/.", "contentType": "text/markdown", "size": 4194304 } ``` The `evidence-refs/` storage prefix in the example URL is an **exempt-frozen storage literal** (Appendix J): blob addresses are hash-frozen inside signed fields, so the prefix is recorded under the prior-era vocabulary rather than renamed. A verifier encountering a BlobRef MUST: 1. Fetch the URL over HTTPS without authentication. 2. Recompute SHA-256 over the fetched bytes; the result MUST equal the hex part of `ref`. 3. Confirm the fetched byte length equals `size`. A BlobRef whose fetch fails, whose hash mismatches, or whose size mismatches MUST cause the verifier to report `ok: false` for that reference. A package MAY remain otherwise verifiable when one of its BlobRefs fails, but downstream consumers SHOULD treat a package with a failed BlobRef as missing the corresponding content. **Relationship to `attestation/locatedAt/v1` (v0.1).** BlobRef is the **single-signer implicit case** of location-as-attestation framing. The verification mechanics are structurally identical: a verifier fetches a URI, recomputes a content hash over the fetched bytes, compares against the signed fingerprint, and confirms size. The differences are placement and signing surface: - **BlobRef** is an in-envelope four-field shape on a `content/*` node's sub-content fields. The parent node's signature covers the BlobRef object as part of its canonical JSON, so the publisher *implicitly* asserts "this sub-content lives at this URL with this fingerprint and this size" as part of their own signed package. The assertion piggybacks on the parent node's signature. - **`attestation/locatedAt/v1`** is a separately-signed envelope with its own `nodeId`, signer, timestamp, and (optionally) Rekor inclusion proof. It can be emitted later than the target content node, by parties other than the target's publisher, and references the target by `nodeId` rather than living inside it. Multiple `locatedAt` attestations from different `(signer.identifier, uri-authority)` pairs express durable independent copies. BlobRef-shaped sub-content references remain conformant under v0.1 for in-envelope use. New cross-host location declarations made by parties other than the parent node's signer SHOULD use `attestation/locatedAt/v1` instead of BlobRef. > ⚠ **Subject to [Q2](open-questions.md#q2--federation-substrate) — federation substrate.** BlobRef URLs in the reference implementation point at the deployment's Vercel Blob storage. Generalizing to multi-host or multi-registry blob storage — including content-addressable storage that does not require an HTTPS-fetchable URL at all (e.g. IPFS-style addressing) — depends on which federation substrate Q2 selects. The `attestation/locatedAt/v1` framing is the natural carrier for cross-host or federation-substrate-native location declarations once Q2 resolves. #### 8.1.6 `extensions` (optional) Implementations MAY add fields under `extensions` keyed by reverse-DNS identifiers (`org.civicaitools.notebook`, `org.civicaitools.environment`, `org..`). All extension content is part of the canonical JSON and is therefore covered by the envelope hash and the platform signature. Extensions are advisory — they MUST NOT change the meaning of fields defined in this specification, and a verifier MAY ignore unknown extensions without breaking conformance. The `org.civicaitools.notebook` extension is a content-format marker (a Jupyter-style cell list) emitted by the canonical reference implementation. Under `contentProfile: datHere`, this extension is promoted from informative to normatively required and carries the deterministic notebook of section E (see §8.7). The `org.civicaitools.environment` extension carries environment metadata (model version, temperature, sampling parameters, tool definitions, publishing-host identifier) required by the `datHere` content profile. See §8.7 for its required shape. #### 8.1.7 `cost` object framing The `cost` object's current schema (`promptTokens`, `completionTokens`, `totalTokens`, `model`, `durationMs`) is AI-LLM-specific. It presupposes that the analysis was produced by a token-billed language model. Under the `scripted-recomputation` Producer Profile (§8.7.5) no token-billed model is in the path, and `cost` is `{"model": "none"}` with the token fields omitted: zeros would state that a model ran and used nothing. `durationMs` MAY be given. > ⚠ **Subject to [Q7](open-questions.md#q7--producer-type-scope) — producer-type scope** and [Q9](open-questions.md#q9--ai-specific-commitments-and-producer-type-generalization) (AI-specific commitments inventory). The `cost` object's schema is currently AI-specific. If Q7 resolves toward generalization to human-authored or hybrid-authored packages, this object will need a producer-type-aware shape (human time, compute time, third-party API costs, etc.) per the pattern. The current shape stays normative for AI-produced packages in v0.1; downstream generalization will land as its own ADR when an adopter blocks. #### 8.1.8 Recorded tool-call failure in `queries[]`, and what it withholds from `dataSources[]` A `queries[]` entry MAY record that the call it describes was **rejected by the source** rather than answered by it. Two optional fields carry that record. This section defines those two fields and nothing else about the entry: v0.1 does not enumerate the remaining `queries[]` entry fields, which a producer derives from its own tool-call capture. | Field | Type | Required | Description | |---|---|---|---| | `failed` | boolean | optional | Present and `true` when the producer recorded this call as rejected by the source. | | `failureKind` | string | optional | The producer's own short label for why the call was rejected. Open value space; no vocabulary is defined or reserved for it. | **Absence is absence.** An entry that omits `failed` records *nothing* about how the call ended, and a consumer MUST NOT read the omission as an assertion that the call succeeded. Every package produced before these fields existed omits both; a producer MAY omit them for any call, and one that never records an outcome is conformant. The three states are distinct and a consumer MUST keep them distinct: - **`failed: true`** — the producer recorded this call as rejected by the source. - **`failed: false`** — the producer recorded this call and did not record it as rejected. Permitted; never required, and no producer is obliged to emit it. - **`failed` absent** — the record states nothing about this call's outcome. A rejection nobody recorded and a call that was answered are indistinguishable here, and the package asserts neither. A consumer that needs "this call succeeded" needs a positive statement to that effect. This specification defines no such statement in v0.1, and a missing `failed` is not one. **`failureKind` is a producer label, not a vocabulary.** This specification defines no value set for `failureKind` and reserves none. Its value is producer-supplied and opaque: a consumer MAY display it, group by it, or ignore it, and MUST NOT switch on it exhaustively or treat an unrecognized value as invalid. A producer whose classification grows widens this field rather than putting prose in it. `failed` is the assertion and `failureKind` only a label on one, so an entry carrying `failureKind` without `failed: true` does not record a rejection, and a producer SHOULD NOT emit the label without the assertion. *(Informative: the reference implementation emits one of `timeout`, `unavailable`, `not_configured`, `unknown`. That closed set is that producer's choice, not this specification's, and no conformant consumer may depend on it.)* **A rejected call contributes no `dataSources[]` entry.** A call recorded as `failed: true` MUST NOT, on its own, give rise to a `dataSources[]` entry — neither one carrying a per-dataset identifier for a dataset the call did not read, nor one recording access to a source without a per-dataset surface. Where the same analysis reached a source through at least one call *not* recorded as failed, that source's entry stands and is derived from those calls. A `dataSources[]` array MAY therefore be empty even when `queries[]` is not, when every entry in `queries[]` records a rejection. This constraint is **normative for the standard, not a description of one producer.** The reason is the meaning §8.1.1 already gives the field — one entry per data source *touched by the analysis* — rather than any producer's convention: a source that refused a call delivered nothing to the analysis, so a call it refused does not by itself establish the touch the entry records. Deriving `dataSources[]` is producer work (this specification names what the array means, not the walk that populates it), which is exactly why the constraint on what may be derived belongs to the standard: without it, two conformant producers reading the same run would disagree about whether a signed package asserts that a source was reached, and a consumer could not tell which it was holding. The entry shape in use at the reference implementation makes the cost concrete — it carries an access timestamp and no field in which an attempt that obtained nothing could be recorded, so a producer that mints one for a refused call has no way to disclose the attempt without asserting the access, inside bytes a publisher signs. The constraint is vacuous for any package in which no entry carries `failed: true`, which is every package produced before these fields existed: no already-published package becomes non-conformant, and no package's bytes change. ### 8.2 Canonical JSON, the envelope hash, and the content hash Canonicalization comes in two kinds,: - **Envelope-level canonicalization** is a single fixed rule committed to by the spec. Every package's unsigned envelope (the canonical-JSON record package object with the signature envelope removed) is canonicalized via [RFC 8785 JSON Canonicalization Scheme (JCS)](https://www.rfc-editor.org/rfc/rfc8785) to produce envelope bytes. The **envelope hash** is the SHA-256 hex digest of those JCS bytes. This rule applies to every envelope shape; there is no envelope-level URI. - **Content-level canonicalization** varies per content shape. Off-log content (whatever the package's `contentHash` fingerprints) is canonicalized per the rule named by the package's `contentCanonicalization` field (§8.1.1). The **content hash** is the multihash digest set fingerprinting those canonicalized bytes — an object keyed by lowercase algorithm name (`sha256` required default, `sha3-256` + `blake3` registered alternates). v0.1 reserved canonicalization-rule URIs are `https://typedstandards.org/canonicalization/dathere-ag-jupyter/v1` (datHere A-G/Jupyter), `https://typedstandards.org/canonicalization/legacy-json/v1` (legacy default), and `https://typedstandards.org/canonicalization/raw-bytes/v1` (a file's bytes as it holds them; [ADR-0029](../adr/0029-scripted-recomputation-producer-profile.md) §4). **What each v0.1 canonicalization rule fingerprints.** - `dathere-ag-jupyter/v1` fingerprints the executed notebook (the `extensions["org.civicaitools.notebook"]` object and its rendered outputs, per §8.7.2) — the content the datHere profile is *about*; the A-G envelope's other sections are envelope metadata. - `legacy-json/v1` (default) fingerprints the JCS canonicalization of the canonical-JSON record package object with the `contentHash` field and the signature envelope both omitted. Excluding `contentHash` keeps the rule non-circular (a fingerprint cannot include itself); excluding the signature envelope matches the unsigned-envelope boundary used for the envelope hash (step 2 below). For a package whose content is fully inline — the legacy default — the content hash and the envelope hash therefore fingerprint nearly identical byte sets, differing only by the `contentHash` field; here the content hash satisfies the §8.1.1 requirement rather than fingerprinting genuinely-separate off-log content. The off-log-content concept becomes load-bearing only when content is supplied by reference (BlobRef) rather than inline. - `raw-bytes/v1` fingerprints exactly one byte sequence: the bytes of the package's `output`. The canonicalization is the identity — no newline, encoding, byte-order-mark or whitespace change — so `contentHash.sha256` is the SHA-256 of those bytes, the same hex string `sha256sum` prints for the file, and a holder of the file can match the signed digest without any Typed Standards code. The rule is non-circular without an exclusion, because `output` never contains the package. Where the bytes live: - **Inline.** `output` is a JSON string, and the fingerprinted bytes are its UTF-8 encoding. A producer MAY inline a file only when the file is valid UTF-8, since valid UTF-8 round-trips through a JSON string byte for byte and nothing else does. A file that is not valid UTF-8 MUST be supplied by BlobRef. An inline package verifies from the package alone: §9.2 check #4 hashes the UTF-8 bytes of `output` and compares. - **BlobRef (§8.1.5).** `output` is a BlobRef, and the fingerprinted bytes are the bytes it names. `contentHash.sha256` MUST equal the hex part of `output.ref`, which §8.1.5 already defines as the SHA-256 of the same bytes. Check #4 obtains the file's bytes through the verifier's fetcher — the route check #9 uses — and an offline verifier supplies the file as a local copy through that route. Check #4 then hashes the bytes and compares the SHA-256 with `contentHash.sha256`. Without the bytes, check #4 reports that the bytes were not checked (§9.2). - **A named external file** outside a BlobRef is not admitted. The file is `output`, and a BlobRef is the one in-envelope way to point at bytes the package does not hold. A raw-bytes package meant to verify with no side file SHOULD inline its output. A BlobRef package's checks #4 and #9 run offline only when the file travels with the §8.8 bundle; carrying file bytes inside the commitment bundle is not specified here and is package-format work under [Q1](open-questions.md#q1--package-format). The rule fingerprints one file: a rule over a set of files is not specified, and a producer with several inputs fingerprints the primary file and pins the others by SHA-256 in `queries[].arguments` or `extensions` (signed assertions that check #4 does not recompute), emits one package per file linked by `prov:wasDerivedFrom`, or archives the set into one file. The rule's definition does not depend on the profile; this revision admits it under the `scripted-recomputation` Producer Profile (§8.7.5), and whether other profiles may name it is not decided. The two rules are nested: 1. Off-log content → `contentCanonicalization` rule → bytes → multihash → `contentHash` field embedded in the envelope. 2. Unsigned envelope (containing `contentHash`, `contentCanonicalization`, and the other top-level fields) → RFC 8785 JCS → bytes → SHA-256 → envelope hash. 3. The envelope hash hex string is what the platform Ed25519ph signature covers (§8.3.1). Because the signature covers the envelope JCS bytes, and the envelope contains `contentHash` and `contentCanonicalization`, both the off-log content's fingerprint AND the rule by which it was canonicalized are signature-covered. A bad actor cannot rewrite the canonicalization rule, the content hash, or any other in-envelope field after publication without invalidating the signature. > **Hash framing (informative).** The content-addressing claims in this section rest on **collision resistance under a named, upgradeable algorithm** — never on uniqueness. An envelope hash or content hash identifies its bytes only as strongly as the named digest algorithm resists collisions, and the multihash digest set exists precisely so the algorithm can be upgraded (registered alternates today; future algorithms via subsequent ADRs per [ADR-0008](../adr/0008-multihash-content-hash.md)). This specification deliberately avoids stronger framings such as "hashes never repeat." All field values defined in this specification — including `metadata.captureMethod`, `metadata.signingKeyId`, `metadata.contentProfile`, `producerProfile`, `contentCanonicalization`, `contentHash`, `vcsRef`, every `extensions` entry, and BlobRef objects — are part of the canonical JSON, part of the JCS-canonicalized envelope bytes, and therefore part of the envelope hash and signature. Fields that live on the database row but not in the canonical package object (such as `title`, `verificationStatus`, `creatorId`) are NOT part of the canonical JSON and are NOT covered by the envelope hash. The `summary` field is optionally part of the canonical JSON per §8.1.1 (required for packages with `metadata.contentProfile === "datHere"`, optional for others); when present in the package, it IS covered by the envelope hash. A change to any in-package field — including a single character in `output`, a different `kid`, a different `captureMethod`, a different `contentCanonicalization` URI, or a different `contentHash` digest — produces a different envelope hash, which produces a different content-addressable URL and a different signature. > **Backwards-compatibility (normative).** Pre-v0.1 packages were canonicalized via Node.js `JSON.stringify` with insertion-order key preservation. They remain verifiable under that legacy rule; verifiers detect which rule applies by whether `contentHash` is embedded as a multihash object (v0.1 + JCS) or supplied as an external single-SHA-256 hex string (pre-v0.1 + `JSON.stringify`). For pre-v0.1 packages, the external single-SHA-256 (the package's historical slug hash) is **relabeled** as `contentHash: {"sha256": }` at verify time — not recomputed under the `legacy-json/v1` rule. The two regimes (pre-v0.1 relabeling, v0.1 `legacy-json/v1` computation) coexist because the detection rule routes each package to its own verification chain. The reference implementation's switch from `JSON.stringify` to JCS is a Phase 3 implementation item. ### 8.3 Cryptographic envelope This section describes the signing, timestamping, and transparency-log mechanisms applied to the envelope hash. #### 8.3.1 Signature **Every package is cryptographically signed so that any modification of its content invalidates the signature, and so that the signature ties the bytes to a publisher whose key is named in the trust registry.** A conformant record package MUST be signed with **Ed25519ph** (the pre-hashed Ed25519 variant, RFC 8032 §5.1.2). The signature is computed over the UTF-8 bytes of the **envelope-hash** hex string, NOT over the raw 32-byte hash bytes. The envelope hash is the SHA-256 hex digest of the RFC 8785 JCS canonicalization of the unsigned envelope (§8.2; [ADR-0008](../adr/0008-multihash-content-hash.md) §6-§7). Implementations using `@noble/curves/ed25519` apply Ed25519ph's internal SHA-512 prehash automatically; implementations using primitives that expose only Ed25519 MUST NOT pre-hash on the application side. The full signing chain v0.1: 1. Unsigned envelope (the canonical JSON with the signature envelope removed) → RFC 8785 JCS → envelope bytes. 2. Envelope bytes → SHA-256 → 32-byte envelope hash. 3. Envelope hash → hex encode → envelope-hash hex string. 4. Envelope-hash hex string → UTF-8 bytes → Ed25519ph → signature. The envelope JSON contains `contentHash` (multihash form) and `contentCanonicalization` (URI naming the off-log content's canonicalization rule) as fields; both are therefore covered by the envelope hash and the signature. The off-log content's bytes are independently fingerprinted by `contentHash` per §8.2's two-kinds split. The signed envelope persisted alongside the package is the JSON object: ```json { "signature": "", "publicKey": "", "algorithm": "Ed25519ph", "kid": "" } ``` The `kid` and `publicKey` in the envelope MUST match the `kid` and `publicKey` of an entry in the trust registry, except when the package's `signer.identifier` is key-derived (§8.5.1): the identifier, recomputed from `publicKey`, then names the key, and no registry entry is required. Under a key-derived identifier `kid` is optional; when present it MUST equal `metadata.signingKeyId`, and it SHOULD be the identifier string itself, so that the key is named the same way in the envelope and in the claim. A `kid` that resolves nowhere is not an error under a key-derived identifier. The `metadata.signingKeyId` field inside the package's canonical JSON MUST equal the envelope's `kid`. A `kid` swap on the envelope after publication therefore changes neither the envelope hash nor the package itself — the canonical JSON is unchanged — but is detectable as an envelope-vs-canonical mismatch by any verifier. The signature envelope (`sig` — publicKey + algorithm + kid + signature bytes) is structurally distinct from the envelope-side identity claim (`signer` — bindingTier + identifier + displayName per §6.2 and §8.5). The signature envelope answers *what was signed and by what key*; the `signer` object answers *who claims to have signed it*. A verifier MUST cross-check that the envelope's `kid` resolves via the trust registry's `signerIdentity` (per §8.3.3) to the same identity the package's `signer.identifier` claims. A mismatch MUST cause the verifier to report `signer_identity_mismatch` and reject the node. When `signer.identifier` is key-derived (§8.5.1), the verifier MUST instead recompute the identifier from the envelope's `publicKey` and compare it with `signer.identifier` byte for byte; a mismatch MUST cause the verifier to report `key_derived_mismatch` and reject the node, whatever the `bindingTier` and whether or not a registry is supplied (§9.2 check #14). pre-v0.1 packages do not carry an envelope-side `signer` claim; verifiers derive an implicit `signer` from the trust-registry `signerIdentity` entry and apply no mismatch check (there is no envelope-side claim to cross-check against). This split prevents an attacker from attaching a valid-by-key signature with a mismatched identity claim. Signing is best-effort at publish time. If the signing leg fails, the database row persists with a `null` signature column; the package and its envelope hash remain valid but it does not satisfy this specification's signed-package conformance. #### 8.3.2 Timestamp and transparency log **A publicly-verifiable timestamp proves the package existed by a particular time; an inclusion proof in a public transparency log makes the signing event itself auditable.** A conformant record package SHOULD also carry an RFC 3161 trusted timestamp from a public TSA and a Sigstore Rekor inclusion proof. The reference implementation uses `freetsa.org` for the timestamp and Rekor's `hashedrekord` v0.0.1 entry type for the transparency log. Both are best-effort: failures persist as `null` columns and the package remains queryable. A verifier checks the RFC 3161 token against FreeTSA's CA chain — validated to a pinned root anchor (§10.3) — and Rekor inclusion against the pinned `rekor.sigstore.dev` log key (§10.3), once it has obtained the timestamp token and the Rekor entry. The cryptographic *check* of these proofs requires only public infrastructure plus the pinned anchors. The *retrieval* of the proofs is carried by the §8.8 commitment view / self-contained bundle (`?inline=1` inlines them, enabling the zero-network verification demonstrated in §9.4); the canonical single-blob package JSON does not itself embed them, so a bare package without its bundle falls back to the reference implementation's verify endpoint. See §9 for the full verification surface and the [Q1](open-questions.md#q1--package-format) callout in §8.1 for the package-format question of whether the proofs should move into the package itself. **Privacy-disclosure note.** Publishing a node's commitment to a transparency log is itself a public act: the envelope hash (`nodeId`), the envelope timestamp, and (via the trust registry's `signerIdentity`) the signer's identity all become public records the moment the inclusion proof is obtained. This is the intended property for published analyses where transparency is a feature, and it is part of the trust contract this specification offers. It is not a neutral act for sensitive or pre-publication content. The architecture therefore PERMITS private transparency logs — an organizational-internal Rekor-equivalent log, a recipient-distributed inclusion-proof protocol, or a deferred-publication pattern where the public log entry is created only when the publication transition lands — for maximally-sensitive cases. No private-log substrate is built in v0.1; the design-permission is named and Xanadu-gated for implementation. Adopters reasoning about whether to publish should treat the public log entry as part of the disclosure surface, not as opaque cryptographic plumbing. See §11 for a fuller treatment of privacy implications. #### 8.3.3 Trust registry **The trust registry lets a verifier confirm that the key which signed a given package is one the publisher claims as authorized — and which lifecycle status that key currently holds (active, deprecated, revoked).** The trust registry is published at `${baseUrl}/.well-known/typed-publisher.json`. Reference implementations SHOULD also serve the same JSON content at the legacy path `${baseUrl}/.well-known/evidence-public-keys.json` for backwards-compatibility with pre-v0.1 fetchers; both URLs return byte-identical content, and verifiers MAY fetch either. The new path is the **canonical** path going forward; new external clients SHOULD fetch the new path. The parallel-serve pattern is permanent (no forced cutover); a future ADR may deprecate the legacy path if no live consumer depends on it. The trust registry is a JSON object with a `keys` array of entries: ```json { "kid": "platform:evidence-2026-04", "publicKey": "", "signerIdentity": { "bindingTier": "platform", "identifier": "platform:civic-ai-tools", "displayName": "Civic AI Tools Platform" }, "status": "active", "activatedAt": "2026-04-15T00:00:00.000Z", "deprecatedAt": null, "revokedAt": null } ``` The `kid` in the example above is the reference implementation's live key identifier — exempt-frozen under the 2026-08-19 vocabulary settlement (it is embedded in every envelope the key has signed; the next key rotation names its successor under the new vocabulary, with no forced rotation; Appendix J). Each entry MAY carry a `signerIdentity` object documenting which identity the `kid` is bound to. The verifier uses this to cross-check the envelope's `signer.identifier` claim against the registry-recorded identity for the envelope's `kid`. pre-v0.1 registry entries omit `signerIdentity`; verifiers treat absence as `signerIdentity: { bindingTier: "legacy_embedded", identifier: "", displayName: "" }` and apply no mismatch check. That `legacy_embedded` value is a placeholder a verifier synthesizes, not a rung of the §8.5 ladder. Post-ADR-0009 registries SHOULD populate `signerIdentity` for every active key. Status values: - `active` — current authorized signing key. - `deprecated` — no longer used to sign new packages; packages signed before `deprecatedAt` remain trusted; packages signed after `deprecatedAt` are not trusted. - `revoked` — never trusted, regardless of integration time. A verifier MUST: 1. Match the envelope's `(kid, publicKey)` pair against an entry in the registry. 2. Apply the status semantics. The reference implementation's verify endpoint reports the verdict via a `keyTrust` field with values `active`, `deprecated_valid`, `deprecated_invalid`, `revoked`, `unknown_key`, `registry_unavailable`, or `legacy_embedded`. This revision adds an eighth value, `self_certified` (below), which the reference verifier implements from `@typedstandards/verify-core` 0.10.0. The `legacy_embedded` value covers packages predating the trust registry: their signature still verifies mathematically against the embedded public key, but the registry cannot vouch for the key. Surfaces SHOULD render this as a neutral status, not as a failure. **A key-derived identifier ([ADR-0030](../adr/0030-self-certifying-signer-did-key.md) §3-§4).** The `self_certified` value is the verdict for a signer whose `signer.identifier` is key-derived (§8.5.1), whose `bindingTier` is `pseudonymous`, and whose identifier equals the one recomputed from the envelope's `publicKey`. It says the identifier and the signing key agree; it does not say that a registry vouched for the key, and like `legacy_embedded` it is not a verified status. A key-derived identifier under any other `bindingTier` is never `self_certified`: it is checked for the match all the same (§9.2 check #14), and its verdict follows the registry path above. The rules below need both the package's `signer.identifier` and the envelope's `publicKey`; when the package is absent (a commitment view verified without its package), no key-derived check runs and the verdict is what the envelope alone yields. With the package present and a key-derived identifier, a verifier MUST apply the following: 1. **Mismatch.** When the recomputed identifier differs from `signer.identifier`, check #14 reports `key_derived_mismatch` and the node is rejected; the verdict here follows the registry path unchanged, because the signature may verify under a key that is not the one the identifier names. 2. **Match, and no registry entry for the key.** When no registry is supplied, or the supplied registry has no entry for the envelope's `(kid, publicKey)`, the verdict is `self_certified`, in place of `registry_unavailable`, `unknown_key` or `legacy_embedded`. 3. **Match, and a registry lists the key.** What the registry's verdict may do depends on where the registry came from. Only a registry obtained from a declared `trustRegistryUrl` can change the status; a registry carried in the bundle can report `revoked` but never raise the status above `self_certified`. Only an `https:` URL is a declared registry URL: a registry obtained from a URL with any other scheme counts as carried in the bundle, and can only lower the status. - **Fetched from a declared `trustRegistryUrl`** (the view's `trustRegistryUrl` or `trustRegistryUrlLegacy`, resolved by the verifier): the registry's verdict — `active`, `deprecated_valid`, `deprecated_invalid` or `revoked` — is reported, and `self_certified` is not. - **Carried in the bundle** (the `?inline=1` form's `trustRegistry`, or any registry the caller did not fetch from the declared URL): a verdict that lowers — `revoked`, or `deprecated_invalid`, which likewise says the key must not be trusted for this signature — is reported. A verdict that would raise — `active` or `deprecated_valid` — leaves the verdict at `self_certified`. The registry's provenance is the caller's to state, as an input beside the registry: fetched from the declared `trustRegistryUrl`, or carried in the bundle. A registry supplied without a stated provenance is treated as carried in the bundle. Rule 3 is the only way a registry can raise a self-certified signer's status, and it raises it only to what that registry says. None of these rules changes the verdict for a signer whose identifier is not key-derived. The rotation runbook for the reference implementation is at `civic-ai-tools-website/docs/key-rotation.md`. ### 8.4 Trace capture > ⚠ **Subject to [Q4](open-questions.md#q4--trace-capture) — trace capture.** The reference implementation uses hand-rolled OTel-shaped JSON. Adopting a real OpenTelemetry SDK or layering Agent Receipts (W3C Verifiable Credentials over MCP tool calls) over or under the OTel layer is the resolution surface. The reference implementation captures a hand-rolled OpenTelemetry-shaped JSON document covering five span kinds: `analysis` (root), `skill_fetch`, `llm_inference`, `mcp_tool_call`, and `synthesis`. The trace is embedded in the package as the `trace` field (or a BlobRef to the same). The PROV-O graph in `provenance` is derived from this trace at publish time. The hand-rolled builder is OTel-schema-compliant for the spans it emits but is **not** a real OpenTelemetry SDK. Adopters that bring their own OTel infrastructure cannot drop into the publish path without adapter work. The current direction is to either (a) adopt a real OTel SDK with the GenAI and MCP semantic conventions, or (b) layer Agent Receipts (W3C Verifiable Credentials over MCP tool calls) over or under the OTel layer. Both directions are tracked under [Q4](open-questions.md#q4--trace-capture). This v0.1 draft normalizes the current span-kind set as conformant; the resolution of Q4 will revise this section. ### 8.5 Identity binding > ⚠ **Subject to [Q3](open-questions.md#q3--first-non-github-identity-provider) — first non-GitHub identity provider.** GitHub OAuth is the only currently-implemented binding. The graded ladder (pseudonymous → GitHub → ORCID → DNS-bound `did:web` → notarized) is informative direction. The reference implementation binds package authorship to a GitHub OAuth account. The DB columns recording authorship (`github_id`, `display_name`, `github_profile_url`) are GitHub-specific. The signing key is platform-held; the user does not currently sign their own packages. The current direction is a graded identity ladder: pseudonymous → weak (GitHub OIDC / sigstore keyless) → moderate (ORCID) → institutional (DNS-bound `did:web`) → strong (notarized). The ladder is informative for now; only the GitHub tier is implemented. [Q3](open-questions.md#q3--first-non-github-identity-provider) will resolve which non-GitHub provider lands first. The `signer` object (§8.1.1, §6.2) carries the identity claim on the envelope side; the trust registry's `signerIdentity` entry (§8.3.3) carries the identity binding on the registry side; the verifier cross-checks the two. The fully-fleshed-out per-tier identity-binding schemas (what `signer.identifier` looks like for `orcid`, `did-web`, `notarized`) are out of scope for v0.1 and stay tied to Q3. This v0.1 draft documents the GitHub binding as the only currently-conformant identity binding; the standard will gain richer identity-binding shapes once Q3 lands. §8.5.1 specifies a self-certifying form of the ladder's first rung, `pseudonymous`. It binds no identity and is not an identity provider, so it leaves Q3 open. #### 8.5.1 Self-certifying signer: the key-derived identifier A signer with no domain has no trust registry to be checked against (§8.3.3 places the registry at `${baseUrl}/.well-known/typed-publisher.json`). The self-certifying form gives such a signer an identifier derived from its signing key, which a verifier checks against the signature's public key with no registry and no network ([ADR-0030](../adr/0030-self-certifying-signer-did-key.md)). **The identifier form.** The key-derived identifier is `did:key` in its base58btc (`z`) form for an Ed25519 public key, per *The did:key Method v0.9* (W3C Credentials Community Group draft, ), the multicodec table (`ed25519-pub`, code `0xed`) and the multibase table (`z`, base58btc). This specification fixes the `z` form only. **The trigger is the identifier, never the tier.** A `signer.identifier` that begins `did:key:` is a **key-derived identifier**. The self-certifying check runs if and only if the identifier is key-derived; `bindingTier` does not enter that decision. The `self_certified` trust status additionally requires `bindingTier: "pseudonymous"` (§8.3.3). **Derivation** from the signature envelope's `publicKey` (§8.3.1: standard base64 of the 44-byte DER SubjectPublicKeyInfo; RFC 8410 §3-§4, OID 1.3.101.112, the BIT STRING holding the raw RFC 8032 §5.1.2 public key): 1. Base64-decode `publicKey` (RFC 4648 §4) to `der`. The result MUST be 44 bytes whose first 12 are `30 2a 30 05 06 03 2b 65 70 03 21 00`; anything else is a malformed key, and the derivation fails. 2. `raw` = `der[12..44]`, 32 bytes. 3. `mc` = `ed 01` ‖ `raw`, 34 bytes. `ed 01` is the unsigned varint encoding of multicodec `0xed`. 4. `s` = base58btc(`mc`): Bitcoin alphabet `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`, big-endian base conversion, one leading `1` per leading zero byte (never, here, since `mc[0]` is `0xed`). 5. identifier = `did:key:z` ‖ `s`. For an Ed25519 key, `s` is 47 characters and begins `6Mk`. A verifier checks a key-derived identifier by recomputing steps 1-5 from the envelope's `publicKey` and comparing the result with `signer.identifier` byte for byte: case-sensitive, with no normalization and no decoding of the claimed string. A mismatch is fatal (§8.3.1; §9.2 check #14). A `did:key:u…` identifier, or a `did:key:z…` identifier for a key type other than Ed25519, is key-derived by the prefix rule and never equals an Ed25519 derivation, so it is a mismatch: one key has one string, and a verifier never decodes a claim to decide whether it might have been truthful under another spelling. *Worked example (informative).* For the committed public key of the eval-run example repository of [ADR-0028](../adr/0028-eval-run-worked-example.md) (`package/public-key.txt`), the derivation yields ``` did:key:z6MktZfoG3M2Navo8Pvbn8DXZAQrYTHk95QbZ7W5gnU9sK1u ``` The derivation above is normative; this example is not. **What it proves.** A key-derived identifier proves that the same key signed everything under that identifier. It does not prove who holds the key, and no registry vouches for it. **No rotation and no revocation.** A self-certifying signer has no rotation and no revocation. The key lifecycle of §8.3.3 (`active`, `deprecated`, `revoked`) belongs to a registry, and a self-certified signer has none. A compromised self-certifying key can only be abandoned; a new key is a new identifier with no link to the old one, and nothing in a package signed under the old key says it was abandoned (§10.2). The one exception is §8.3.3 rule 3: a registry that lists such a key can mark it revoked, and a verifier given that registry sees the revocation, whether the registry was fetched from a declared `trustRegistryUrl` or carried in the bundle. **The upgrade path** is an attestation in which a domain-bound signer vouches for a key: a signed `attestation/*` node whose signer is on the ladder's `did-web` or higher rung and whose target is the self-certifying key's identifier. It is named as the path and is undecided; this revision does not specify it. ### 8.6 captureMethod **Readers see exactly which mechanism produced the bytes they're verifying — and that mechanism is itself tamper-evident** (*tamper-evident:* any change to the labeled value invalidates the signature) **because the label is part of what the signature covers.** `captureMethod` is the label identifying how the package's content was captured. Its presence, its required-and-signed discipline, and its verbatim-by-construction labeling apply to every conformant package. The value space is open at the core level; valid values are declared per Producer Profile. A conformant record package published after 2026-04-29 MUST carry exactly one of the values declared by the captureMethod vocabulary of the package's `producerProfile`'s guidance bundle. The vocabulary lookup follows the rule: 1. Read the package's `producerProfile`. When absent and `metadata.contentProfile === "datHere"`, treat producerProfile as `ai-assisted-analysis/datHere` legacy alias. When both fields are absent (pre-v0.1 packages), treat producerProfile as `ai-assisted-analysis` — the implicit profile-type for pre-existing packages, all of which were AI-mediated by construction. 2. Resolve the producerProfile's guidance bundle via the local rule registry mechanism [Q32](open-questions.md#q32--producer-profile-guidance-doc-routing-convention) anticipates. v0.1 verifiers resolve to a hardcoded fallback table; the bundle distribution mechanism is a follow-on per Q32. 3. Confirm `metadata.captureMethod` is in the captureMethod vocabulary declared by that bundle. For the `ai-assisted-analysis` Producer Profile, the v0.1 vocabulary — relocated from core — is: - **`chat-flow-stream`** — the publishing platform captured the bytes as the model streamed to the calling client. Verbatim by construction at the wire layer. - **`claude-code-jsonl-readback`** — the publishing client (typically a Claude Code skill) read each turn's content and per-invocation usage from the session JSONL on disk, filtering to text-typed content blocks only. Verbatim by construction at the JSONL layer. - **`claude-code-self-report`** — legacy. The publishing model paraphrased the original session from in-context memory. Deprecated as of 2026-04-28; retained as a vocabulary value so packages predating the capture-method discipline can be re-rendered with their actual capture method labeled rather than silently re-described as something they were not. The vocabulary applies to all subtypes of `ai-assisted-analysis` (the existing `datHere` subtype, the reserved `civicaitools-default` subtype, and any future subtypes) unless a subtype's guidance bundle explicitly constrains or extends the parent vocabulary; v0.1 has no subtype-level overrides. For the `scripted-recomputation` Producer Profile (§8.7.5; [ADR-0029](../adr/0029-scripted-recomputation-producer-profile.md) §2), the v0.1 vocabulary is: - **`script-run`** — a packaging program, run after the content's files already existed on disk, read their bytes into the package; the files are pinned by SHA-256 and no AI conversation is in the path. Verbatim by construction at the file layer. How each file came to exist (a program's run, or a retrieval) is recorded in `queries[]`, not by the label. - **`tool-emitted`** — the program that computed the content wrote the package itself, in the same process, hashing the bytes as it wrote them. Verbatim by construction at emission; no step reads the content back from disk between computation and packaging. No package carries `tool-emitted` yet. The two differ as `chat-flow-stream` and `claude-code-jsonl-readback` do: under `script-run` a file sits on disk between the run and the packaging, and nothing but the package's own signed assertions binds the file to the run that wrote it; under `tool-emitted` that interval does not exist. v0.1 verifiers resolve the vocabulary by profile type and do not check the subtype. The hardcoded fallback table of step 2 above holds one entry per built profile type, `ai-assisted-analysis` and `scripted-recomputation`; it remains a stand-in until the bundle routing convention of [Q32](open-questions.md#q32--producer-profile-guidance-doc-routing-convention) is settled. The reference publish route enforces the field at request validation; a missing or invalid value returns `400`. The field is part of `metadata.captureMethod` in the canonical JSON and is therefore covered by the envelope hash and the platform signature: the capture method itself is tamper-evident. A package's signature attests that the package was published and has not been altered since. It does NOT attest that the package's content matches what was actually generated in the original session — that property is structural and follows from the capture method. Surfaces SHOULD render the `captureMethod` label near the signature-verification verdict so readers do not conflate "signed" with "verbatim." pre-v0.1 packages persist with a `null` capture-method column on the database row. Surfaces SHOULD render these as `Unknown (pre-v0.1)` rather than defaulting to one of the listed values. Future AI-publishing surfaces (a hook-based path that records bytes at message-emission time, a third-party signed self-attestation, an MCP-host-agnostic capture protocol) extend the `ai-assisted-analysis` Producer Profile's vocabulary by amending **that profile's guidance bundle**, not by amending the specification's core. The bundle's amendment surface — versioning, distribution, content-addressing — is governed by [Q32](open-questions.md#q32--producer-profile-guidance-doc-routing-convention). Non-AI Producer Profiles (Human, Hybrid, Sandbox-only, future adopter profiles) declare their own captureMethod vocabularies in their respective guidance bundles when promoted from reserved to built. ### 8.7 Profile requirements: datHere and scripted-recomputation Content profiles specify the normative requirements for packages produced under a particular Producer Profile subtype. They sit below the captureMethod layer (§8.6) — captureMethod names *how* content was captured; a content profile names *what additional fields the package must carry and how its content is structured*. The cross-host publication mechanism (§8.8) is the bridge that lets a content profile's packages travel to hosts other than the producing host. v0.1 specifies requirements for two profiles: the `datHere` content profile, the first realized subtype of the AI-Assisted Analysis Producer Profile (§8.7.1-§8.7.4), and the `scripted-recomputation` Producer Profile for deterministic tool output (§8.7.5). The `scripted-recomputation` profile is not the `datHere` content profile, does not extend it, and does not use its canonicalization rule. Other Producer Profile types (Human, Hybrid, Sandbox-only) are reserved; their requirements will be specified when promoted from reserved to built. §8.7.1-§8.7.4 specify the `datHere` content profile. A `datHere`-content-profile package organizes its content as the **A-G envelope**, a profile over the existing top-level fields specified in §8.1. The envelope is a content profile, not a new container: the package remains the single canonical JSON object whose SHA-256-over-JCS-canonicalization is the envelope hash. A-G is the way a `datHere`-content-profile package's content is *organized for readers and cross-host publishing*; the top-level fields are still what gets hashed and signed. The A-G section-to-field mapping: | Section | Content | Top-level field | |---|---|---| | A | Initial prompt — the user's question, verbatim | `prompt.text` (with `prompt.visibility == "full_text"`) | | B | System prompt(s) active for the model | `skillMetadata.skillText` | | C | Model card + environment metadata: model ID/version, temperature, sampling parameters, MCP server URLs, tool definitions, publishing-host identifier | `cost.model` + `skillMetadata.mcpServerUrl` + `extensions["org.civicaitools.environment"]` (§8.7.1) | | D | Deliberative trace: thinking, tool calls, and tool results in order | `trace` (OTel-shaped, or BlobRef) + `queries[]` | | E | Answer notebook — a notebook that, when executed against the documented runtime, produces F | `extensions["org.civicaitools.notebook"]` (§8.7.2) | | F | The rendered answer | `output` (string or BlobRef) | | G | Short, indexable, citation-ready summary | `summary` (§8.1.1) | #### 8.7.1 Normative requirements A conformant `datHere`-content-profile package MUST satisfy *every* requirement below, in addition to the standard's existing requirements for conformant packages (§8.1, §8.2, §8.3, §8.6). 1. **Prompt visibility.** `prompt.visibility` MUST be `"full_text"`. The hash-only mode is incompatible with the A-G envelope, which requires section A to be readable. 2. **System prompt(s) present.** `skillMetadata.skillText` MUST be non-empty (inline string or BlobRef) and MUST reflect the composed system prompt set the model was operating under at the time of the analysis. 3. **Environment metadata present.** The `extensions["org.civicaitools.environment"]` object MUST be present and MUST contain at least: `modelVersion` (string), `temperature` (number), `mcpServers` (array of objects with `url` and optional `name`), `toolDefinitions` (array of tool-schema objects, OR a BlobRef when large), `host` (string identifying the publishing host, e.g. `"civicaitools.org"` or an external publisher's host identifier). Additional fields are permitted under reverse-DNS sub-namespacing. 4. **Notebook present.** The `extensions["org.civicaitools.notebook"]` object MUST be present, MUST conform to a notebook format admitted by §8.7.2, and MUST satisfy the determinism property in §8.7.3. Where the notebook is too large to inline, it MAY be supplied as a BlobRef. The notebook MAY be either skeleton or executed at protocol level; §8.7.4 specifies the discriminator and the corresponding reproducibility-property strength for each. Both forms are conformant `datHere` notebooks. 5. **Rendered answer present.** `output` MUST be present (inline or BlobRef) and MUST be the rendered output of executing the notebook against the documented runtime at publish time. 6. **Summary present.** `summary` (§8.1.1) MUST be present, MUST be non-empty, and SHOULD be short enough to surface in citation contexts (recommended ≤ 280 characters; not enforced numerically). 7. **Content-profile label.** `metadata.contentProfile` MUST be `"datHere"`. The label is itself covered by the canonical-JSON hash and the platform signature per §8.2. `captureMethod` (per §8.6) continues to carry one of the values declared by the package's `producerProfile`'s guidance bundle; for a `datHere`-content-profile package — which v0.1 also carries `producerProfile: "ai-assisted-analysis/datHere"` — that resolves to the `ai-assisted-analysis` Producer Profile's v0.1 vocabulary (`chat-flow-stream`, `claude-code-jsonl-readback`, `claude-code-self-report`). `contentProfile` is an independent axis describing what shape the content takes. A verifier encountering a `datHere`-content-profile-labeled package that fails any of the requirements above MUST report the package as malformed-for-`datHere` while still being able to perform the standard envelope-integrity checks (§9). Non-datHere content profiles continue to use their existing requirements; the requirements above apply only when `metadata.contentProfile == "datHere"`. #### 8.7.2 Notebook format A conformant `datHere`-content-profile package's section E (the notebook) MUST conform to **Jupyter Notebook Format v4.5 or later** (nbformat 4), expressed as the JSON cell structure with a top-level `cells` array, per the public nbformat specification. Jupyter is the v1 default because it matches the pattern in use at the pilot integration partner and has the broadest ecosystem support (rendering, diffing, archival, citation tooling). This specification admits alternative notebook formats — most notably Marimo, which has stronger determinism properties via reactive evaluation and no hidden state — as conforming notebook formats for `datHere`-content-profile packages, provided they: 1. Produce a self-contained executable representation whose execution against the documented runtime is reproducible (no hidden inputs, no cell-order-dependent state that is not re-evaluable); 2. Carry an explicit content-type marker on the `extensions["org.civicaitools.notebook"]` entry indicating which format is in use (e.g., a `"format"` sub-field with values like `"jupyter-v4.5"` or `"marimo-v0.x"`); 3. Are accompanied by a renderer that produces section F (the rendered answer) from section E. The protocol-level property the specification locks is **deterministic reproducibility**, not the choice of notebook engine. A future ADR may promote Marimo (or another format) to a second normative default without superseding this one if a real adopter requires it. Until then, `datHere`-content-profile packages SHOULD default to Jupyter v4.5+. #### 8.7.3 Determinism property A `datHere`-content-profile package's section E (the notebook) is **deterministic against a documented runtime environment plus stable upstream data**. The specification articulates this property explicitly because conflating "verifiable" with "the same answer forever" is the predictable failure mode. 1. The notebook MUST record its runtime requirements (language version, library versions, MCP server URLs) either in its first cell or in a sidecar `requirements` field on the `extensions["org.civicaitools.environment"]` object. 2. Re-execution of the notebook against the documented runtime, with the same MCP server endpoints reachable and the same upstream data unchanged since publication, SHOULD reproduce section F (the rendered answer) byte-for-byte modulo non-deterministic formatting (timestamps in tool-call results, floating-point representations that depend on platform libc, etc.). 3. The determinism property is **best-effort**, not absolute. Civic data is live; an upstream dataset updated since publication will produce different tool-call results on re-execution, which will produce a different rendered answer. This is expected behavior, not a verification failure. 4. Verifiers and surfaces SHOULD render the determinism property as *"reproducible against the documented runtime AND the upstream-data state at publish time,"* not as *"the same answer forever."* The signature attests that the notebook in section E has not been altered since publication. It does NOT attest that re-executing it tomorrow produces the same answer as today; the upstream data may have changed. This is the `datHere` analog of the chat-flow-stream / JSONL-readback "verbatim-by-construction at *some* layer, with the layer named" property: the layer named is *the documented runtime against the upstream-data state at publish time*, and the property promised is *reproducibility against that layer*, not invariance. Skeleton and executed notebooks (§8.7.4) deliver the reproducibility property with different strengths: skeleton notebooks re-execute the data-fetch cells reproducibly but the answer-synthesis cell carries a hardcoded markdown answer that is not re-derived from cell outputs; executed notebooks deliver the property materially because every cell's output (including the synthesis cell) is computed at publish time, and the comparison-cell convention (§8.7.4) makes original-vs-current values legible to verifiers. Surfaces SHOULD render the property strength honestly per §8.7.4's labeling convention. #### 8.7.4 Notebook execution provenance and metadata This section adds two protocol-level fields that discriminate how the notebook in section E was produced and, when the notebook was executed by the publisher's pipeline, what runtime environment produced its outputs. The two fields are independent of `captureMethod` (§8.6) and `contentProfile` (§8.1.2, §8.7) — they describe the *notebook authoring path*, a third orthogonal axis. The fields apply only when `metadata.contentProfile == "datHere"`; non-datHere content profiles ignore them. **`extensions["org.civicaitools.notebook"].provenance`** A new sub-field on the existing notebook extension distinguishing how the notebook in section E was authored: | Value | Meaning | |---|---| | `"skeleton"` | The notebook structure wraps an answer authored elsewhere (typically the chat-flow LLM output). Data-fetch cells are re-executable and reproducible; the answer-synthesis cell carries a hardcoded markdown answer that is NOT re-derived from cell outputs above. The reproducibility property in §8.7.3 is satisfied partially: data-fetch reproducibility holds; answer-synthesis reproducibility does not. | | `"executed"` | The notebook was executed end-to-end by the publisher's pipeline before signing; every cell's output (including the synthesis cell) is computed from real cell execution against the documented runtime and live upstream data at publish time. The reproducibility property in §8.7.3 is satisfied materially; the comparison-cell convention below makes original-vs-current values legible to re-executors. | When absent, verifiers SHOULD treat the field as `"skeleton"` (the pre-v0.1 default). The field is auto-emitted by conformant packagers from ADR-0005 forward; pre-v0.1 `datHere`-profile packages omit it and remain conformant. **`extensions["org.civicaitools.execution"]`** A new reverse-DNS-keyed extension recording the execution telemetry needed for verifiers to reason about the determinism property. The extension MUST be present when `provenance == "executed"` and MUST be absent when `provenance == "skeleton"` (or absent). Field set: | Field | Type | Required | Description | |---|---|---|---| | `executedAt` | string (ISO-8601 UTC) | yes | Timestamp at which the notebook execution completed. | | `environment` | object | yes | Runtime the notebook actually executed against. MUST contain at least: `python` (string version) and `libraries` (object mapping library name to pinned version string). Additional sub-fields permitted under reverse-DNS sub-namespacing. | | `executionDuration_ms` | integer | yes | Wall-clock duration of the sandbox execution, milliseconds. Informational; not part of the trust property. | | `sandboxId` | string | optional | Opaque identifier for the execution substrate run. Informational; not part of the trust property. The specification does NOT specify the sandbox provider; this field carries provider-specific telemetry without naming a provider in mandatory shape (see [Q28](open-questions.md#q28--sandbox-provider-lock-in-vs-portability-for-the-executed-notebook-path) on the portability question). | | `comparisonCellPresent` | boolean | optional | Defaults to `true` for new executions. When `true`, the executed notebook includes the appended "Comparison: original vs. current" cell described below. | The `extensions["org.civicaitools.environment"]` extension from §8.7.1 describes the runtime the package was *authored under*; `extensions["org.civicaitools.execution"]` describes the runtime an execution *actually ran in*. They coexist; an executed-path package carries both. A re-executor matches both blocks against their own environment to reason about whether re-execution outputs should match. **Comparison-cell convention (executed notebooks, SHOULD)** When `provenance == "executed"` and `comparisonCellPresent != false`, the executed notebook SHOULD include a final cell appended by the publisher's pipeline after sandbox execution and before signing. The cell embeds the prominent numeric/dataframe values from the original execution as Python literals and re-computes the same values on re-execution against live data. The intent is that a re-executor of the notebook tomorrow sees both the original values (as constants in source code) and the current values (computed at re-execution time) and a printed delta, without any introspection of the notebook's own .ipynb file structure. The canonical shape is: ```python # ORIGINAL VALUES (captured at executedAt = ) original = { "": , ... } # CURRENT VALUES (re-computed against live data using the same helpers + queries above) current = recompute_key_metrics() # DELTAS for k in original: delta = (current[k] - original[k]) if isinstance(original[k], (int, float)) else (original[k], current[k]) print(f"{k}: original={original[k]}, current={current[k]}, delta={delta}") ``` The "prominent metrics to capture" selection is at the publisher's discretion. Conformant publishers SHOULD use a deterministic heuristic or an LLM-selected metric set, documented in their reference implementation. The cell is part of the signed notebook artifact and is covered by the envelope hash and signature. **Reproducibility-property labeling convention (rendering surfaces, SHOULD)** Rendering surfaces (the publisher's detail page, third-party renderers of the cross-host publication artifact, archive views) SHOULD frame the reproducibility property a package actually delivers using labels that name the property, not the internal versioning. Recommended labels: - `provenance == "executed"` → *"Executed notebook — answer derived from computed data; full re-execution reproducible against the documented runtime + upstream-data state at publish time."* - `provenance == "skeleton"` (or absent) → *"Skeleton notebook — answer authored in chat; data fetch reproducible but answer synthesis is not."* **Backwards compatibility.** Pre-v0.1 `datHere` packages without these notebook-extension fields remain conformant; verifiers treat the omission as `provenance == "skeleton"`. #### 8.7.5 The `scripted-recomputation` Producer Profile The `scripted-recomputation` Producer Profile type ([ADR-0029](../adr/0029-scripted-recomputation-producer-profile.md)) covers a record whose content a program computed from inputs pinned by hash, so that running the same program over the same inputs recomputes the same bytes, with no AI conversation in the path. The profile states what produced the content. It makes no claim about an execution environment, which separates it from the reserved `sandbox-only` type. `producerProfile` takes the form `scripted-recomputation/`, with a non-empty subtype the producer names; subtype names are open and not pre-allocated, and this revision registers none (`eval-run`, carried by the [ADR-0028](../adr/0028-eval-run-worked-example.md) packages, is the first in use). A package whose `producerProfile` begins `scripted-recomputation/` MUST satisfy §8.1 and §8.6, and in addition: 1. **`producerProfile`** is `scripted-recomputation/`, subtype non-empty. 2. **`metadata.captureMethod`** is `script-run` or `tool-emitted` (§8.6). 3. **`type`** is `content/analysis/v1`. A later content type for this profile needs its own decision record. 4. **`contentCanonicalization`** is present (§8.1.1 recommends the field; this profile requires it) and names `legacy-json/v1` or `raw-bytes/v1` (§8.2). `dathere-ag-jupyter/v1` is not admitted: this profile carries no notebook extension. 5. **`contentHash`** carries a `sha256` digest. A producer MAY list `sha3-256` or `blake3` alongside it; v0.1 verifiers recompute `sha256` only, and `blake3` is not implemented. 6. **`metadata.contentProfile`** SHOULD be omitted; when present it MUST be `"default"`. The consistency invariant of §8.1.1 then means `metadata.contentProfile` is never `"datHere"` under this profile. 7. **The AI-shaped required fields** of §8.1.1 are present, populated as follows: | Field | Under `scripted-recomputation` | |---|---| | `prompt` | Required as §8.1.3 defines it. `prompt.text` states the task the program performed, or the command that ran it; `visibility` is either value. | | `queries` | At least one entry, recording a step that produced or obtained the content: a program run, or a retrieval. Each entry SHOULD name the program in `tool` and SHOULD pin each input it read by SHA-256 in `arguments`. A step that failed is recorded under §8.1.8. | | `cost` | `{"model": "none"}`, with the token fields omitted (§8.1.7). `durationMs` MAY be given. | | `skillMetadata` | Present and MAY be empty (`{}`): no skill guidance and no MCP server was in the path. | | `output` | The program's output: an inline string or a BlobRef (§8.1.5). Under `raw-bytes/v1` it is the fingerprinted file (§8.2). | | `trace` | `{"resourceSpans": []}` when no trace was captured, which is the conformant empty OTel-shaped trace. A producer MAY carry real spans. | 8. **`summary`** SHOULD be present. **`signer`** follows §8.1.1 and §8.5; this profile adds no identity requirement. **Which checks test these.** Requirements 1 and 2 are §9.2 check #15. Requirement 6 is check #16. Requirement 4 is not tested by a check: check #3 resolves whatever rule URI a package names, under any profile — a known URI, `dathere-ag-jupyter/v1` included, reports `ok`, an unknown URI reports `unknown_canonicalization_rule`, and an absent field reports `implicit`, with the rule inferred as for a pre-v0.1 package — and check #4 then recomputes the content hash under the rule #3 resolved. For requirement 5, check #4 recomputes `sha256` when `contentHash` lists it and reports `content_hash_mismatch` when it differs; a `contentHash` that lists no `sha256` reports `contentHash_no_supported_algorithm`, which is reported and does not reject the node. Requirements 3, 7 and 8, and requirement 4's presence of `contentCanonicalization` and its restriction to `legacy-json/v1` or `raw-bytes/v1`, are producer obligations that no v0.1 check tests, as the §8.7.1 requirements for `datHere` are. ### 8.8 Cross-host publication: commitment-view schema A `datHere`-content-profile package MAY be published cross-host as a Jupyter notebook on a git host, as a multi-file commit with a sibling metadata file, or as future analogous content-addressable surfaces. In every case the published artifact carries the package's **commitment view** — enough fields for any reader to independently verify the package against the publisher's trust registry without fetching the canonical-JSON package object. This section defines the commitment view as a **logical schema** (§8.8.1 — field definitions) and specifies two **concrete serializations** of that schema: notebook-embedded (§8.8.2, for `.ipynb` outputs) and sibling YAML file (§8.8.3, for non-notebook outputs and as a sidecar option). §8.8.4 describes a reader-affordance rendering convention for notebook outputs. Both serializations carry the same field set with the same semantics; they are byte-different but semantically identical for verification. A conformant publisher MAY emit either serialization; a conformant verifier MUST accept either. A reader holding only the self-contained bundle — this commitment view in its `?inline=1` serialization, with the package, the publisher's trust registry, and the RFC 3161 token, Rekor entry body and inclusion proof, and lifecycle chain all carried inline (`GET …/commitment?inline=1`) — can verify the package's full cryptographic envelope with **no network access at all**; this is the demonstrated offline property of §9.4. The lighter (non-`inline`) commitment view references the trust registry by URL and so still fetches the publisher's registry (and, for embeds, the package and attestations) over public infrastructure — offline *from the originating host*, but not zero-network. Either way the cross-host publication pattern *makes the package's content* independent of the originating host as long as the trust registry remains independently reachable. What stays gated on [Q1](open-questions.md#q1--package-format) is only whether the single-blob *package* should itself embed the proofs rather than carrying them in this commitment view. Bundle-export endpoints on conformant publishers produce the published artifact (the notebook with its embedded metadata, or the multi-file set including any sibling YAML) as a single response; the reference implementation's contract is in `civic-ai-tools-website/docs/api/evidence-publish.md` (renaming to `records-publish.md` under the vocabulary settlement, with the old filename remaining as a stub — Appendix J). Bundle endpoints are advisory — a publisher MAY support cross-host publication by manual artifact construction without offering a bundle endpoint. #### 8.8.1 Field definitions A conformant commitment view carries the following fields. The field set is the same regardless of serialization; §8.8.2 and §8.8.3 specify how the fields are arranged in their respective serializations. The required/optional marks reflect the served reference shape (the produce-core commitment-view builder), ratified into this table 2026-08-03; absent optional fields are omitted, never emitted as `null` (the two explicitly-nullable exceptions are marked). | Field | Type | Required | Description | |---|---|---|---| | `protocolVersion` | string | yes (dual-era key) | The Typed Standards schema version this commitment view was published against (currently `0.1.0`). **Dual-era key** (Appendix J): commitment views minted before a publisher's settlement cutover carry the same value under the prior-era key `evidenceProtocolVersion`, which remains valid forever — the key is frozen inside already-signed artifacts. A conformant verifier MUST accept either key; post-cutover emissions use `protocolVersion`. The reference implementation's cutover is coordinated with the one live external adopter (which serves this field); until a publisher's cutover lands, its prior-era emissions remain conformant. | | `packageHash` | string (hex SHA-256) | yes | The SHA-256 hex digest of the canonical-JSON package object. The package's content-addressable identifier. | | `packageUrl` | string (URL) | conditional | The content-addressable URL where the canonical-JSON package is fetchable. Reference implementation: Vercel Blob URL. Other hosts MAY serve from their own content-addressable storage. Omitted when unknown and on redacted (sealed-visibility) views — a non-derivable capability URL MUST NOT be disclosed for a sealed-visibility record (see the redaction rule below). | | `visibility` | string | yes | The content node's visibility state ([ADR-0016](../adr/0016-vcs-native-lifecycle-mapping.md) §A vocabulary: `sealed` / `public`). Lets a verifier render a sealed / not-publicly-located state honestly instead of treating a missing `packageUrl` as an error. Reference implementations predating the value-rename sweep emit the legacy values `committed` / `published`; consumers SHOULD accept the legacy values as aliases (`committed` → `sealed`, `published` → `public`). | | `captureMethod` | string \| null | yes | One of the values declared by the package's `producerProfile`'s captureMethod vocabulary (§8.6). Mirrors `metadata.captureMethod` from the canonical-JSON package. Emitted as an explicit `null` when the record predates the capture-method discipline. | | `contentProfile` | string | yes | Mirrors `metadata.contentProfile` (§8.1.2); `"default"` when the package carries none. | | `producerProfile` | string | optional | Mirrors the package's top-level `producerProfile` (§8.1.1). Absent on packages predating it. | | `type` | string (URI) | optional | Mirrors the package's `type` (§8.1.1). Absence is interpreted as `content/analysis/v1`. | | `signer` | object | optional | The envelope-side §8.5 identity claim `{ bindingTier, identifier, displayName, verifiedAt? }`, mirrored verbatim from the package — **the subject of §9.2 check #14**. `identifier` may be a key-derived `did:key` identifier (§8.5.1). Distinct from `signerIdentity` below; see the naming note. Absent on pre-v0.1 packages, which carry no envelope-side claim. | | `contentHash` | object | optional | The package's multihash digest set (§8.2), mirrored from the canonical JSON. Absent on pre-v0.1 packages. | | `contentCanonicalization` | string (URI) | optional | The package's content-canonicalization rule URI (§8.2). Absent on pre-v0.1 packages. | | `signature` | object | conditional | Signed-envelope object carried **verbatim**: `{ signature, publicKey, algorithm, kid }` matching §8.3.1. `algorithm` is load-bearing — an independent verifier MUST dispatch Ed25519 vs. Ed25519ph on it; `algorithm` and `kid` MAY be absent on packages signed via older paths. Omitted when the package is unsigned (§8.3.1 best-effort signing). | | `signerIdentity` | object | optional | Informational identity block about the record's creator (reference implementation: the publishing user's public GitHub identity `{ provider, providerId, displayName, profileUrl }`). Surfaced for human context only — **NOT the verification subject**; a verifier MUST NOT use it as the signature subject (that is the `signer` claim above). | | `rfc3161Timestamp` | string (base64) | optional | RFC 3161 trusted timestamp token. Present when the publisher's pipeline obtains one. | | `rekorEntryId` | string | optional | Sigstore Rekor entry identifier. Present when the publisher's pipeline obtains one. | | `rekorInclusionProof` | string (JSON) | optional | Sigstore Rekor Merkle inclusion proof. Present when the publisher's pipeline obtains one. | | `rekorEntryBody` | string (base64) | optional | The canonical Rekor leaf body, carried so Merkle inclusion verifies offline (§9.4) without fetching the log entry. | | `lifecycle` | object | optional | Informational lifecycle summary `{ status: "active" \| "withdrawn", withdrawnAt?, withdrawnReason?, reinstatedAt?, reinstatedReason? }`. Omitted when the package has no lifecycle history. Informational only — the authoritative lifecycle state is the signed attestation chain (§8.10); a withdrawn package's base signature still verifies. | | `lifecycleAttestations` | array | optional | Signed lifecycle `attestation/*` envelopes carried inline (embed form) so an independent verifier resolves the §9.2 check-#10 lifecycle chain offline. Omitted when empty. | | `attestations` | array | optional | Non-lifecycle attestation entries (corroborations, contradictions, evaluations, …), each either a reference or an embed per §8.9. The reference implementation's served sidecar does not currently emit this array — its lifecycle chain travels under `lifecycleAttestations` — but cross-host serializations MAY carry it. | | `trustRegistryUrl` | string (URL) | conditional | The `.well-known/typed-publisher.json` URL where the publisher's trust registry is served (the **canonical** path per §8.3.3). Lets a reader resolve `signature.kid` independently of the publishing host. Per-publisher configuration — never a constant. Required, except that it MAY be absent when `signer.identifier` is key-derived (§8.5.1) and `signer.bindingTier` is `pseudonymous`; the key is then omitted, not `null`. It is required in every other case: no `signer`, a `signer` whose identifier is not key-derived, or any other `bindingTier`. | | `trustRegistryUrlLegacy` | string (URL) | optional | Secondary registry URL served byte-identical to the canonical one (the §8.3.3 legacy path `.well-known/evidence-public-keys.json`), emitted for clients that only know the older path. The legacy path is the **exempt-frozen leg of the already-completed trust-registry rename** (settlement ruling D2; Appendix J) — recorded, not renamed again. | | `subjectTitle` | string \| null | conditional | Human-readable title of the analysis. Matches the publisher's database `title` field. Omitted on redacted (sealed-visibility) views; MAY be an explicit `null` when the publisher has no title. | | `subjectSummary` | string \| null | conditional | The G-section summary. Matches the canonical-JSON `summary` field (§8.7.1 requirement 6). Omitted on redacted (sealed-visibility) views. | > **`signer` vs. `signerIdentity` — naming note (ratified 2026-08-03; codebase-wins, zero wire change).** Earlier revisions of this table used `signerIdentity` for the §8.5-shaped identity claim. The served sidecar has always carried the §8.5-shaped claim under **`signer`** — matching the §8.1.1 envelope field it mirrors — and uses **`signerIdentity`** for an optional informational provider-identity block that is never the verification subject. This revision ratifies the served split. Three `signerIdentity` surfaces are now disambiguated: (1) a **trust-registry entry's** `signerIdentity` (§8.3.3) — §8.5-shaped, resolved via the envelope's `kid`, one side of check #14; (2) the commitment view's **`signer`** — the §8.5-shaped claim mirrored from the package, the other side of check #14; (3) the commitment view's **`signerIdentity`** — informational, MUST NOT be used as the signature subject. > **Redaction rule (sealed-visibility records).** The commitment itself is public by design — the envelope hash is already on the transparency log — but a sealed record's content surface is not. A redacted view omits `packageUrl`, `subjectTitle`, and `subjectSummary`; the proof-side fields are served unredacted — they ARE the commitment. > **Self-contained (`?inline=1`) serialization.** The self-contained bundle (§8.8 intro, §9.4) is this same commitment view plus two inlined fields: `package` (the full canonical package JSON, otherwise fetched from `packageUrl`) and `trustRegistry` (the publisher's trust-registry document with its `generatedAt` as-of date, otherwise fetched from `trustRegistryUrl`). Under a key-derived identifier with no `trustRegistryUrl`, `trustRegistry` is absent too, and a verifier follows §8.3.3 rule 2; a self-certified signer's bundle MAY carry a `trustRegistry` it wrote itself, which a verifier treats as carried in the bundle (§8.3.3 rule 3: it can lower the status and never raise it). The RFC 3161 token, the Rekor entry body + inclusion proof, and the lifecycle chain are already inline in the default view, so the inline form verifies with zero network access. #### 8.8.2 Notebook-embedded serialization (`.ipynb` outputs) A `datHere`-content-profile package published as a Jupyter notebook (§8.7.2) MUST carry the commitment view in the notebook's root `metadata` object under the commitment-view reverse-DNS namespace. The namespace is **dual-era** (settlement ruling D3; Appendix J): new emissions mint **`org.civicaitools.record`**; notebooks published under the prior-era namespace **`org.civicaitools.evidence`** are accepted forever. A conformant verifier MUST read either namespace (preferring `org.civicaitools.record` when both are present); a publisher's pre-cutover emissions under the prior-era namespace remain conformant. ```json { "cells": [ ... ], "metadata": { "org.civicaitools.record": { "protocolVersion": "0.1.0", "packageHash": "", "packageUrl": "", "visibility": "public", "captureMethod": "chat-flow-stream", "contentProfile": "datHere", "signature": { "signature": "...", "publicKey": "...", "algorithm": "Ed25519ph", "kid": "..." }, "signer": { "bindingTier": "...", "identifier": "...", "displayName": "..." }, "trustRegistryUrl": "", "subjectTitle": "...", "subjectSummary": "...", "lifecycleAttestations": [ ... ] }, "kernelspec": { ... }, "language_info": { ... } }, "nbformat": 4, "nbformat_minor": 5 } ``` The commitment-view namespace (`org.civicaitools.record`; `org.civicaitools.evidence` in prior-era notebooks) lives at the notebook's root `metadata` level — the location the Jupyter notebook format reserves for opaque metadata that conformant tooling MUST preserve on round-trip. Sibling namespaces (publisher-specific identifiers, `kernelspec`, `language_info`, future namespaces) coexist with the commitment-view namespace and are unaffected by it; a conformant verifier MUST ignore unknown sibling namespaces. All field names and semantics from §8.8.1 map directly. Nested objects (`signature`, `signer`) flatten naturally into the JSON shape Jupyter expects. Optional fields (`rfc3161Timestamp`, `rekorEntryId`, `rekorInclusionProof`, `rekorEntryBody`, `lifecycle`, `lifecycleAttestations`, `attestations`, `signerIdentity`) MAY be omitted; when present they carry the §8.8.1-defined shape. A conformant publisher MUST ensure the commitment-view metadata block (`org.civicaitools.record`, or `org.civicaitools.evidence` in prior-era notebooks) survives notebook tooling round-trip (executing the notebook in Jupyter, Colab, VS Code, or analogous environments MUST NOT clobber the namespace). The Jupyter notebook format spec is explicit that root-level metadata under unrecognized keys is preserved by conformant tooling, which makes this serialization durable in practice. Notebook-embedded is the recommended default serialization for `.ipynb` outputs. #### 8.8.3 Sibling YAML file serialization (non-notebook outputs and sidecar) A `datHere`-content-profile package published as a non-notebook artifact, or as a notebook alongside an explicit sidecar, MAY carry the commitment view as a sibling YAML file with the conventional filename `.record.yaml`. The filename convention is **dual-era** (Appendix J): sidecars published under the prior-era convention `.evidence.yaml` keep that filename valid indefinitely, and a conformant verifier MUST accept either filename; new sidecars use `.record.yaml`. (No emitting code existed for this convention at any main when the settlement landed, so the rename carries no emitter cutover.) The file's content is the §8.8.1 field set serialized as YAML at the top level: ```yaml protocolVersion: "0.1.0" packageHash: "" packageUrl: "" visibility: "public" captureMethod: "chat-flow-stream" contentProfile: "datHere" signature: signature: "" publicKey: "" algorithm: "Ed25519ph" kid: "" signer: # ... identity-binding fields per §8.5 (bindingTier, identifier, displayName) trustRegistryUrl: "" subjectTitle: "..." subjectSummary: "..." lifecycleAttestations: # ... signed lifecycle envelopes per §8.8.1 attestations: # ... non-lifecycle entries per §8.9 ``` A conformant verifier MUST accept either YAML or JSON shapes at this filename (YAML is a superset of JSON; either form is valid). Where the published artifact is itself a markdown document, publishers MAY ALTERNATIVELY embed the commitment view as YAML frontmatter at the top of the markdown file between `---` delimiters (the Jekyll / GitHub Pages frontmatter convention); the field set is identical. For non-notebook artifacts that are markdown documents, the document body (everything after the YAML frontmatter or alongside the sibling YAML file) SHOULD render A-G content as markdown sections. The exact layout is at the publisher's discretion as long as A through G are unambiguously identifiable. When a sibling YAML file accompanies a notebook, the notebook-embedded serialization (§8.8.2) and the sibling YAML serialization MUST carry the same field values for any field they both express. A verifier encountering a mismatch SHOULD prefer the serialization whose signature recomputes correctly against the envelope hash and SHOULD report the mismatch. This serialization is the primary path for non-notebook outputs and a valid choice for notebook outputs that prefer separation of concerns over embedded metadata. #### 8.8.4 Cell 0 rendering convention (notebook outputs, SHOULD) A `datHere`-content-profile package published as a Jupyter notebook SHOULD render a human-readable metadata table in the notebook's first markdown cell. The cell is a reader affordance — verification does NOT depend on its presence — but it materially improves the experience of opening the notebook in any renderer (Jupyter, Colab, VS Code, GitHub's `.ipynb` viewer, nbviewer). Recommended fields to surface in the table: - **Signer identity + binding tier.** Who signed the package, at what identity-binding strength (per §8.5). - **Package hash (truncated).** First 8-12 hex characters of the canonical SHA-256, sufficient for at-a-glance identification. - **Trust seal / captureMethod + contentProfile.** The captureMethod label and the contentProfile label and a short verification status indicator suitable for static rendering. The table itself does not perform verification. - **Attestation summary count.** Number of attestations carried in §8.8.1's `attestations` field, optionally broken out by kind. - **Publishing host + timestamp.** `host` and publish-time information from the environment metadata (§8.7). The rendered cell is purely a reader affordance. A reader who needs to verify the package MUST work from the commitment-view namespace metadata (`org.civicaitools.record`, or the prior-era `org.civicaitools.evidence`; §8.8.2) or the sibling YAML (§8.8.3); the rendered table is not authoritative and SHOULD NOT be trusted on its own. ### 8.9 Embed-vs-reference policy for cross-host publication The `attestations` array in the commitment view (§8.8) MAY contain entries in either of two forms. The same rules apply to both serializations defined in §8.8. (The lifecycle chain travels separately, under the commitment view's `lifecycleAttestations` field per §8.8.1, always in embed form; entries under `attestations` are the non-lifecycle kinds.) **Reference form** is the default. A reference entry is a JSON object with the following fields: ```yaml - kind: targetHash: attestationHash: attestationUrl: ``` A reader processing a reference entry fetches the attestation from `attestationUrl`, recomputes its SHA-256 against `attestationHash`, and verifies its signature against the publisher's trust registry (the same `trustRegistryUrl` from the commitment view, or a different registry URL carried inside the attestation itself). **Embed form** is an optimization for attestations that are stable and load-bearing for trust evaluation. An embed entry is the complete signed attestation envelope, inline: ```yaml - kind: targetHash: attestationHash: attestation: signature: ``` A reader processing an embed entry verifies the embedded envelope's signature directly without fetching anything. Both forms preserve independent verifiability: an embedded attestation carries its own signature, so a reader can verify it even if the surrounding commitment view has been altered (the alteration would break the package-hash check anyway, but the embed-vs-reference distinction is orthogonal to the package signature). **Default-to-reference rule.** Implementations SHOULD prefer reference form for routine attestations (corroborations from other authors, contradictions, citations) and SHOULD use embed form only when an attestation is structurally tied to the published claim's trust state — for example, an admin-approve attestation that establishes a corroboration relationship between an original sealed claim and a publication-record, or a host-policy attestation that gates publication on adversarial-evaluation presence. A reader encountering an embedded attestation MUST verify its signature against the publisher's trust registry just like any other attestation; the embed/reference distinction is a fetch-time vs. commitment-view-size trade, not a trust trade. The attestation-kind vocabulary itself is governed by §8.12 (the `attestation/*` namespace) and is not normatively closed by this section. The cross-host publication schema accepts any attestation kind the publisher emits; readers and downstream consumers apply their own filters. ### 8.10 Lifecycle and location attestations Lifecycle events (withdrawals, reinstatements, supersessions, publications) are separately-signed `attestation/*` nodes referencing the target content node by `nodeId`, not DB-row columns on the target's storage. Location pointers (the publisher's URL where the content lives, plus any backup-host pointers) are separately-signed `attestation/locatedAt/v1` nodes. Pre-v0.1 packages whose lifecycle state lives in the legacy DB columns remain verifiable; the reference implementation honors both representations for pre-v0.1 packages and emits attestation nodes for new packages. #### 8.10.1 Lifecycle as a chain of attestation nodes A package author MAY withdraw, reinstate, or supersede a previously-emitted content node at any time by emitting the corresponding `attestation/*` node, signed under the same trust-registry key as the target (or a delegated-publisher key per [Q20](open-questions.md#q20--visibility-lifecycle-and-attestpublish-semantics)). The lifecycle is a **chain of separately-signed attestation nodes**, each referencing the target (and, for reinstatements, the prior withdrawal) by `nodeId`. Sub-types per the v0.1 sub-type table: - **`attestation/withdraws/v1`** — references target by `nodeId`; carries `reason` (required, non-empty) and `effectiveAt` (defaults to envelope timestamp). Authorization rule: `publisher-only`. - **`attestation/reinstates/v1`** — references target by `nodeId` and the prior withdrawal by `priorWithdrawalNodeId`; optionally carries `reason`. Authorization rule: `publisher-only`. - **`attestation/supersedes/v1`** — references the prior node by `targetNodeId` and the successor node by `successorNodeId`. Authorization rule: `publisher-only`. - **`attestation/publishes/v1`** — references target by `nodeId`; carries `publicationHost` and `releasedAt`. Authorization rule: `publisher-only` OR `delegated-publisher` per [Q20](open-questions.md#q20--visibility-lifecycle-and-attestpublish-semantics) (delegated-publisher mechanics are a future ADR). Each attestation envelope is Ed25519ph-signed and SHOULD be RFC 3161-timestamped + Sigstore Rekor-included per §8.3.2, exactly like a `content/*` envelope. The signature attests that the lifecycle event occurred at the asserted time, by the asserted signer, referencing the asserted target; it does not modify the target's own signature, which remains valid as published. **Multi-cycle support is free by construction.** A `public → withdrawn → reinstated → withdrawn → reinstated → ...` sequence is a longer chain of attestation nodes referencing the same target, with each subsequent reinstatement pointing back to its immediately-prior withdrawal via `priorWithdrawalNodeId`. There is no cycle counter, no DB-shape mutation per cycle, and no spec-level cycle limit. A verifier processing a content node MUST surface the **chain of signer-matched lifecycle attestations** referencing the node, in envelope-timestamp order (ties broken by `nodeId` lexicographic). The verifier reports the current lifecycle status as derived from the latest signer-matched lifecycle attestation per the retention-asymmetry rule in §8.10.3 below. #### 8.10.2 Location as attestation The publisher's own URL where the content lives, plus any backup-host or mirror URL, are each expressed as a signed `attestation/locatedAt/v1` referencing the content node by `nodeId`. Payload fields: `targetNodeId`, `uri`, `targetContentHash` (multihash; SHOULD match the target's `contentHash` — mismatch is informative, indicating content drift between the location and the target's signed fingerprint), optional `contentLength`, optional `availability`. Authorization rule: `any-with-binding`. The target-fingerprint payload field is named **`targetContentHash`** (not `contentHash`) because sub-type payload fields live flat at the canonical-JSON top level alongside the structural primitive (§7.4, §8.1), and the structural primitive already claims `contentHash` for the attestation node's *own* off-log fingerprint (§8.2) — the two meanings cannot share one key. The name follows the same disambiguation pattern as `targetNodeId` (vs. the attestation's own `nodeId`). Ratified 2026-08-03, resolving [Q48](open-questions.md#q48--attestationlocatedatv1-payload-contenthash-name-collision-with-the-structural-primitive); the reference implementation already emits this name. **Multiple `attestation/locatedAt/v1` attestations from different `(signer.identifier, uri-authority)` pairs express that the content has independent durable copies** per [Q38](open-questions.md#q38--dedicated-copyof-relation-vs-multiple-locatedat-attestations) (resolved by the v0.1 sub-type table in §8.12.1, refinement (c)). A dedicated `copyOf` sub-type is not minted; the multi-`locatedAt` pattern carries the durability signal sufficiently. Consumer-side weighting (publisher's own pointer vs. third-party mirror vs. recognized archive) lives in the verifier's surface logic, not in the attestation envelope. A content node with **zero `attestation/locatedAt/v1` attestations** is the valid private/draft/enterprise base case: the signer holds the bytes; no public location is asserted. See [ADR-0010](../adr/0010-visibility-lifecycle-location-attestations.md) §5 for the use cases (draft / pre-publication, enterprise-private, recipient-distributed). The BlobRef pattern in §8.1.5 is the **single-signer implicit case** of `attestation/locatedAt/v1`'s verification rule (fetch the URI, recompute the content hash over the fetched bytes, confirm size). BlobRef-shaped sub-content references remain conformant for in-envelope use; new cross-host location declarations made by parties other than the parent node's signer SHOULD use `attestation/locatedAt/v1` instead. #### 8.10.3 Retention asymmetry (normative property) A publisher's withdrawal authority is **bounded to their own pointer and status label**. Specifically: 1. A `attestation/withdraws/v1` from publisher P referencing target T removes P's own visibility commitment. It does NOT invalidate `attestation/locatedAt/v1` attestations signed by other parties pointing at T's content hosted on other hosts. 2. A backup-host's `attestation/locatedAt/v1` remains independently verifiable after P's withdrawal: the backup host's signature still verifies; the content at their URI still hashes correctly against the target's `contentHash`; the backup host's location attestation has not been retracted (only P's own status has). Whether the backup host honors P's withdrawal at the application layer is a host-policy question (the future host self-attestation work per [Q22](open-questions.md#q22--host-as-typeable-subject--host-self-attestation-shape)), not a protocol question. 3. A verifier MUST surface the **latest signer-matched lifecycle attestation alongside any verified copy**. When rendering a content node with an `attestation/withdraws/v1` from P AND an `attestation/locatedAt/v1` from backup-host B, the verifier displays both — "withdrawn by publisher; copy still available at B's host" — rather than treating the withdrawal as global content erasure. This is the deliberate civic-accountability feature the prior OES §10 implied but did not name. Silent erasure of civic-data claims is a worse failure mode than asymmetric retention; the specification surfaces the asymmetry honestly and lets consumers apply judgment per §5.1's normative preamble. A permanent record that a civic-data claim was made and later retracted is more honest than silent deletion. Implementations MUST NOT remove withdrawn content nodes from storage or registry-side listings except through explicit administrative action with an audit trail. The retention-asymmetry rule above scopes this MUST NOT to the publisher's own infrastructure; it does not extend to a publisher's authority over content others host. #### 8.10.4 Backwards compatibility for pre-v0.1 packages Pre-v0.1 packages whose lifecycle state lives in legacy DB columns (`withdrawnAt` / `reinstatedAt` and related) remain verifiable; verifiers MUST honor the legacy columns when no `attestation/withdraws/v1` / `attestation/reinstates/v1` envelopes are present. A one-time migration to attestation envelopes is a Phase 3 implementation item; the schema version stays at `0.1.0` per [Q27](open-questions.md#q27--schema-version-bump-trigger-for-the-oes-spec). #### 8.10.5 Lineage: revision succession vs. corrective replacement A chain of revisions of the same analysis (typically mirroring version-control history) is expressed as a chain of **`attestation/revises/v1`** nodes, one per revision edge, each linking a prior node to its successor ([ADR-0016](../adr/0016-vcs-native-lifecycle-mapping.md) §C). Payload: `targetNodeId` (the prior revision — the parent) and `successorNodeId` (this revision — the child). Authorization rule: `publisher-only` (the lineage owner), consistent with the other lifecycle sub-types. Single-parent only at v0.1 (linear lineage); multi-parent / merge-DAG lineage is deferred to the registry. `revises` and the existing `attestation/supersedes/v1` encode meaningfully different consumer signals, and conflating them loses one — the same rationale that keeps `endorses` and `corroborates` distinct ([ADR-0009](../adr/0009-unified-typed-attestation-primitive.md) §7 refinement (b)): - **`attestation/revises/v1` — neutral version succession.** "This is the *next version* in a lineage." Carries **no** deprecation signal: the prior revision remains a valid point-in-time snapshot, matching both version-control history (commit N+1 does not declare N "wrong") and the retention model (§8.10.3 — a prior node is not withdrawn merely because a successor exists). - **`attestation/supersedes/v1` — corrective replacement.** "This new node *replaces* the old one; treat the old as obsoleted." Carries a deprecation signal — a consumer should stop relying on the superseded node. **The diff between two revisions is a derivable human view, not a signed object.** It is computed on demand from the two content-addressed nodes (both immutable, both retrievable) — not a node, not an attestation, not separately signed. The `attestation/revises/v1` edge plus the two nodes it links are sufficient to render the diff; its integrity is already implied by the two nodes' own signatures. A future adopter needing a signed, attributable diff (e.g., a reviewer attesting "I reviewed exactly this diff") would express that as an `attestation/*` over the two nodes, registered when that need is real. #### 8.10.6 Visibility, lifecycle status, and host display are orthogonal dimensions Three dimensions of a content node's disposition are deliberately kept distinct ([ADR-0016](../adr/0016-vcs-native-lifecycle-mapping.md) §A.1); conflating them — most commonly by reading `withdrawn` as a visibility value — loses a signal the standard is built to surface: | Dimension | Question | Values | Derived from | |---|---|---|---| | **Visibility** | Is the content disclosed? | `sealed` / `public` | presence of `attestation/publishes/v1` + `attestation/locatedAt/v1` (§8.10.1; zero-`locatedAt` = the sealed base case per §8.10.2) | | **Lifecycle status** | What is the publisher's current standing? | `active` / `withdrawn` / `superseded` (reinstate → active) | the latest signer-matched lifecycle attestation in the chain (§8.10.1) | | **Host display** | Does *this* host show it? | host's choice | host policy (`content/hostPolicy/v1`, [Q22](open-questions.md#q22--host-as-typeable-subject--host-self-attestation-shape)) reading the two signals above | - **`withdrawn` / `reinstated` / `superseded` are lifecycle status, NOT visibility values.** A withdrawn-formerly-public node is **`public` + `withdrawn`**, not `sealed`: withdrawal retracts the publisher's *standing*, not the content's *disclosure* (the retention-asymmetry property, §8.10.3). A publisher cannot un-disclose content already on the public log and on other hosts; folding `withdrawn` into visibility would falsely assert that it can. - **Host display is host policy, not a node property.** The protocol emits the `withdraws` signal but mandates no host behavior; "public but withdrawn, and host X still shows it" must remain expressible — which it cannot be if `withdrawn` is a visibility value. - **Availability** — whether a `locatedAt` URI currently resolves — rides the optional `availability` sub-field on `attestation/locatedAt/v1` (§8.10.2), distinct again from visibility, which records *asserted* disclosure rather than current reachability. The visibility values are `sealed` / `public` per [ADR-0016](../adr/0016-vcs-native-lifecycle-mapping.md) §A (`committed` → `sealed`, `published` → `public`; "Publish"/"Seal" remain the verbs and `attestation/publishes/v1` the relationship — only the state labels moved). The cryptographic "commitment" noun (the §8.8 commitment view) is unaffected. Both states carry a public transparency-log commitment when Rekor inclusion is obtained; visibility describes whether the *content* is disclosed, never whether the node is on the ledger (§8.3.2, §11). ### 8.11 Typed Claims Typed claims are `content/*` sub-types — `content/claim/v1`, `content/question/v1`, `content/evidence/v1` — under the two-family taxonomy ratified. The claim shapes specified in this section (TrendClaim, ComparisonClaim, ObservationClaim, CompositionClaim, RelationshipClaim, QualitativeClaim) and their conformance requirements remain authoritative; the carrier is the signed node itself, not a `claims.jsonld` companion file. The historical CCV draft at [`civic-claim-vocabulary-draft-spec.md`](civic-claim-vocabulary-draft-spec.md) is preserved as a frozen snapshot. > ⚠ **Subject to [Q5](open-questions.md#q5--claimsjsonld-and-upstream-evidencejson-implementation-timing) — typed-claim build-out timing.** Promotion of typed claims from "specified" to "built" is gated on a real adopter package whose verification or claim queries are blocked without the layer per the [Xanadu doctrine](xanadu-doctrine.md). The v0.1 `content/claim/v1` / `content/question/v1` / `content/evidence/v1` sub-types are reserved name-only until that adopter is identified; the spec text below is forward-compatible with the new framing. #### 8.11.1 Purpose and scope The envelope (§8.1-§8.10) captures *how* an analysis was produced. The Typed Claims layer additionally captures *what the analysis claims*, in a form that is machine-comparable across packages. A **typed claim** is a structured assertion derived from analysis output and serialized as JSON-LD against the published Typed Standards vocabulary. Typed claims are intended to enable corpus-level operations — corroboration, contradiction detection, citation graphs, meta-analysis, drift surveillance — that are impractical against unstructured prose. This specification defines the structure of a typed claim, the Typed Standards Claim Vocabulary, the extension mechanism for domain-specific vocabularies, the relationship between typed claims and the rest of a package, and the translation provenance requirements for LLM-emitted claims. It does **not** define domain vocabularies (those live in extensions; see §8.11.6), network-layer ranking/surfacing of claims, or whether any specific claim is true — the specification surfaces structure, not truth, per the §5.1 preamble. #### 8.11.2 Design principles 1. **Build on existing standards, do not replace them.** Vocabulary fragmentation harms cross-package operations — when two packages assert the same metric in different vocabularies they can't be compared without translation. Where W3C, OGC, ISO, or domain-specific vocabularies and ontologies exist (PROV-O, SDMX, Data Cube, Schema.org, GeoSPARQL, FHIR, etc.), the Typed Claims layer references and reuses them rather than inventing parallel terms. 2. **Common core, modular extensions.** A small mandatory core ensures every claim has provenance, geographic and temporal scope, and a quantified confidence statement. Domain extensions add typed claim shapes for crime, transit, housing, public health, budgets, procurement, and so on, without requiring the core to know about them. 3. **Optional in v1, incentivized.** A typed-claim node is OPTIONAL alongside `content/analysis/v1`-shaped packages in v0.1 of this specification. Packages whose extracted typed claims are emitted as conformant `content/claim/v1` nodes SHOULD receive richer treatment in network-layer processors (citation graph inclusion, contradiction surfacing, meta-analysis discovery). Packages without typed claims remain fully valid. 4. **Translation is itself an analytical step.** Structured form does not confer truth: when an LLM extracts a precise-looking claim from vague prose, the precision of the structure is mistaken for the precision of the underlying analysis. When a typed claim is generated by extracting structure from an LLM's prose output, the extraction MUST be captured as a separately-signed `attestation/wasDerivedFrom/v1` node carrying an `ts:AnalyticalDerivation` payload (per refinement (a) §4 — the classification-laundering guard). The specification is explicit that structured form does not confer truth. 5. **Falsifiable by construction.** Every claim type MUST be defined such that a counter-claim can be expressed in the same vocabulary. If a claim cannot be contradicted by another well-formed claim, it is not a claim — it is decoration. 6. **Confidence MUST be derivable, not asserted.** Any confidence value attached to a claim MUST trace to a method (sample size, statistical test, model log probability, human review) recorded in the package. Free-form LLM confidence judgments are not permitted at the protocol level. #### 8.11.3 Package integration Under the unified-primitive framing of §7.4, a typed claim is a **first-class signed node** — a `content/claim/v1` envelope with its own `nodeId`, signature, timestamp, transparency-log inclusion proof, and identity binding. The carrier is the envelope itself, not a companion file alongside a containing analysis. A typical pattern for an AI-assisted analysis that emits typed claims: 1. The publisher produces a `content/analysis/v1` node carrying the raw analysis output (the A-G envelope per §8.7 for `ai-assisted-analysis/datHere`; or another shape for other Producer Profiles). The node's `metadata.contentType` is `["untyped"]` — the rendered answer is unstructured prose. 2. The publisher (or a downstream extractor) processes the untyped output to produce one or more typed claims. For each extracted claim, the publisher: - Signs a separate `content/claim/v1` envelope carrying the claim payload (per §8.11.4). - Signs a separate `attestation/wasDerivedFrom/v1` envelope referencing the source `content/analysis/v1` node by `nodeId` and the derived claim by a paired identifier; the attestation's `derivationMethod` MUST carry a `ts:AnalyticalDerivation` describing which model performed the extraction, against what prompt, over which source span (per refinement (a) — the classification-laundering guard). 3. Each `content/claim/v1` node MUST cite its supporting evidence within its own envelope via `prov:wasDerivedFrom` references to entities derivable from the source `content/analysis/v1` node's provenance graph (§8.1.4). 4. Typed claims MAY reference data sources from the source analysis's `dataSources[]` entries via the source's stable identifier. 5. Cross-package corroboration, contradiction, citation, or supersession is expressed as separately-signed `attestation/*` nodes (`attestation/corroborates/v1`, `attestation/contradicts/v1`, `attestation/supersedes/v1`, `attestation/answersQuestion/v1`, `attestation/supportedBy/v1`, `attestation/opposedBy/v1`) referencing the target `content/claim/v1` node by `nodeId`. No separate companion file is needed. A typed claim's signature is independent of any containing analysis's signature: a typed claim can be verified, withdrawn, reinstated, superseded, corroborated, or contradicted independently of the source analysis. Lifecycle attestations targeting a typed-claim node follow the same rules as lifecycle attestations targeting any other content node (§8.10). A `content/claim/v1` node conforms to this specification's typed-claims layer if: 1. It is a valid JSON-LD 1.1 document carried inside the canonical-JSON envelope per §8.1-§8.2. 2. Its claim payload's `@context` includes the Typed Standards Claim Vocabulary context (`https://typedstandards.org/ns/ts#`). 3. Every top-level claim object validates against the SHACL shapes published with the Typed Standards Claim Vocabulary. 4. Every confidence value is traceable to a method recorded in the source analysis's trace (or in the claim node's own structured payload when the claim was generated outside an AI synthesis loop). 5. Every claim is falsifiable in the sense of §8.11.2 principle 5. #### 8.11.4 Vocabulary The Typed Standards Claim Vocabulary is a controlled set of typed claim shapes expressed in JSON-LD. It references W3C ontologies (PROV-O, OWL-Time, RDF Data Cube), OGC GeoSPARQL, Schema.org, and SDMX-RDF for concepts those vocabularies already cover; the Typed Standards Claim Vocabulary contributes only the structural conventions that analytical claims need on top. **Namespace.** ``` @prefix ts: ``` This is the single normative prefix for the vocabulary. The legacy `ccv:` prefix continues to resolve as an alias for backwards-compatibility; a future deprecation is gated on adopter need per [Q10](open-questions.md#q10--civic-claim-vocabulary-as-a-full-ontology). **Reused vocabularies.** The Typed Standards Claim Vocabulary does not redefine concepts that exist in widely-adopted vocabularies. It imports and references: | Prefix | Vocabulary | Used for | |---|---|---| | `prov:` | W3C PROV-O | Provenance of every claim | | `qb:` | W3C RDF Data Cube | Multidimensional observations | | `sdmx:` | W3C SDMX-RDF | Statistical metadata, time periods | | `schema:` | Schema.org | Datasets, organizations, persons | | `geo:` | OGC GeoSPARQL | Geographic geometries | | `time:` | W3C OWL-Time | Temporal entities and intervals | | `dcterms:` | Dublin Core Terms | Identifiers, titles, descriptions | | `xsd:` | XML Schema datatypes | Primitive value types | The Typed Standards Claim Vocabulary contributes only what these external vocabularies do not already provide: the *Claim* shape, the *ConfidenceStatement* pattern, civic-specific scope types (e.g. `ts:NeighborhoodTabulationArea`), and the *AnalyticalDerivation* link from claim to LLM extraction trace. **Core class: `ts:Claim`.** Every typed claim is an instance of `ts:Claim` or a subclass thereof. The base class defines the minimum required structure; subclasses (`ts:TrendClaim`, `ts:ComparisonClaim`, etc.) refine it. **Required properties** on every Claim: | Property | Type | Description | |---|---|---| | `dcterms:identifier` | `xsd:string` | Stable claim ID, unique within the package | | `ts:subject` | URI | What the claim is about (typically a metric, indicator, or observable) | | `ts:scope` | `ts:Scope` | Geographic and temporal bounds of the claim | | `ts:confidence` | `ts:ConfidenceStatement` | Method-derived confidence (see below) | | `prov:wasDerivedFrom` | URI[] | At least one entity from the source analysis's provenance graph | | `ts:derivedVia` | `ts:AnalyticalDerivation` | Link to the analytical step that produced the claim | **Optional properties:** | Property | Type | Description | |---|---|---| | `dcterms:description` | `xsd:string` | Human-readable claim summary | | `ts:contradicts` | URI[] | Identifiers of other claims (in this or other packages) the author is explicitly contesting (also expressible as separately-signed `attestation/contradicts/v1` nodes) | | `ts:corroborates` | URI[] | Identifiers of other claims the author is explicitly affirming (also expressible as separately-signed `attestation/corroborates/v1` nodes) | | `ts:supersedes` | URI | A previous claim this one replaces (also expressible as `attestation/supersedes/v1`) | | `ts:limitations` | `xsd:string` | Author-acknowledged limitations of the claim | **Scope: `ts:Scope`.** Every claim has a scope expressing where and when it applies. ```json { "@type": "ts:Scope", "ts:geographicScope": { "@type": "ts:NeighborhoodTabulationArea", "dcterms:identifier": "BK0801", "schema:name": "Bushwick North", "geo:hasGeometry": { "@id": "..." } }, "ts:temporalScope": { "@type": "time:Interval", "time:hasBeginning": { "time:inXSDDate": "2024-01-01" }, "time:hasEnd": { "time:inXSDDate": "2025-12-31" } } } ``` The Typed Standards Claim Vocabulary defines a small taxonomy of `ts:GeographicScope` subtypes for common civic units. Each subtype names the canonical reference standard or authority where one exists. | Subtype | Reference standard / authority | |---|---| | `ts:CensusTract` | US Census Bureau [TIGER/Line](https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.html) (`tract` shapefile, GEOID encoding) | | `ts:CensusBlock` | US Census Bureau TIGER/Line (`tabblock` shapefile) | | `ts:CensusBlockGroup` | US Census Bureau TIGER/Line (`bg` shapefile) | | `ts:ZIPCodeTabulationArea` | US Census Bureau TIGER/Line ZCTA (`zcta` shapefile) | | `ts:SchoolDistrict` | US Census Bureau Education Demographic and Geographic Estimates (EDGE) plus TIGER/Line school-district shapefiles | | `ts:MunicipalBoundary` | US Census Bureau TIGER/Line `place` shapefile | | `ts:CountyBoundary` | US Census Bureau TIGER/Line `county` shapefile (FIPS county code) | | `ts:StateBoundary` | US Census Bureau TIGER/Line `state` shapefile (FIPS state code); ISO 3166-2 for international subdivisions | | `ts:NeighborhoodTabulationArea` | NYC Department of City Planning NTA (NYC-specific) | | `ts:CommunityBoardDistrict` | NYC Department of City Planning Community District boundary file (NYC-specific) | | `ts:CityCouncilDistrict` | Jurisdiction-specific. Reference the publishing city's council-district authority. No single cross-city canonical authority. | | `ts:PolicePrecinct` | Jurisdiction-specific. NYC: NYPD precinct boundaries via NYC OpenData. **Flagged for follow-up per [Q14](open-questions.md#q14--geographic-and-temporal-scope-nullability):** the namespace name `PolicePrecinct` may be too NYC-coded; some jurisdictions use "district," "division," "ward." | Domain extensions MAY add more. International equivalents (statistical-area-1 in Australia, OA in the UK, etc.) should be added as domain extensions rather than core subtypes. For arbitrary geometries that do not fit any of the named subtypes, claims MAY use `ts:GeographicScope` directly with a `geo:hasGeometry` value pointing at an OGC GeoSPARQL geometry literal (WKT, GML, or GeoJSON-LD). Temporal scope uses W3C OWL-Time directly. No additions to the vocabulary are needed. **Confidence: `ts:ConfidenceStatement`.** Confidence is structured, not free-form. Every confidence statement MUST identify its method. ```json { "@type": "ts:ConfidenceStatement", "ts:method": "ts:FrequentistInterval", "ts:level": 0.95, "ts:lowerBound": 18.4, "ts:upperBound": 27.6, "ts:methodReference": "trace.json#step-stat-test-3" } ``` The Typed Standards Claim Vocabulary defines a starting set of confidence methods: | Method URI | Use when | |---|---| | `ts:FrequentistInterval` | Confidence interval from a statistical test | | `ts:BayesianCredibleInterval` | Bayesian posterior interval | | `ts:SampleSizeBased` | Confidence implied by sample size against a known population | | `ts:LLMReportedLogProbability` | Token-level log probabilities from the model (limited applicability) | | `ts:HumanReview` | Reviewer-asserted confidence with reviewer identity | | `ts:NotApplicable` | The claim is qualitative or definitional; numeric confidence does not apply | `ts:NotApplicable` is permitted but MUST be accompanied by a description explaining why a quantified confidence is not appropriate. **The list is extensible by domain extensions.** Domain extensions MAY define additional confidence methods under their own namespace (for example, `nyc-housing:RentRollSampleConfidence` or `transit:ScheduleAdherenceConfidence`), in the same way they MAY add to the `ts:GeographicScope` subtype taxonomy. New methods MUST satisfy the §8.11.2 principle 6 requirement (confidence is method-derived, traceable to a recorded calculation in the package). **AnalyticalDerivation.** Every claim MUST link to the analytical step that produced it. This is what makes the LLM-to-claim translation auditable. ```json { "@type": "ts:AnalyticalDerivation", "ts:traceReference": "trace.json#step-claim-extraction-1", "ts:translationModel": { "@type": "schema:SoftwareApplication", "schema:name": "claude-opus-4-7", "schema:softwareVersion": "claude-opus-4-7" }, "ts:translationPrompt": { "@id": "prompt.json#claim-extraction" }, "ts:sourceOutputSpan": { "ts:outputFile": "output.md", "ts:byteRange": [1240, 1487] } } ``` This serves three purposes: 1. Makes clear that the structured claim is a *translation* of LLM prose, not the prose itself. 2. Makes the translation prompt inspectable (and re-runnable for verification). 3. Pins the claim to a specific span of source output for human review. Under refinement (a), an `AnalyticalDerivation` payload is also a MUST-carry component of the separately-signed `attestation/wasDerivedFrom/v1` node that documents the extraction step (per §8.11.3 step 2). The same data MAY appear in both places — on the claim's own `ts:derivedVia` property and on the attestation's `derivationMethod` payload — provided the two are consistent. #### 8.11.5 Core claim types The vocabulary ships with a small starter set of claim shapes. All extend `ts:Claim`; each adds typed properties beyond the core. | Sub-type | Asserts | Additional required properties (beyond `ts:Claim`'s core) | |---|---|---| | `ts:TrendClaim` | a metric increased / decreased / remained stable across two time periods within a scope | `ts:metric` (URI; ideally referencing an external vocabulary); `ts:baselinePeriod` (`time:Interval`); `ts:comparisonPeriod` (`time:Interval`); `ts:direction` (`Increase` / `Decrease` / `NoSignificantChange`); `ts:magnitude` (`ts:Magnitude`; absolute or percent) | | `ts:ComparisonClaim` | two scopes differ on a metric within a single time period | `ts:metric` (URI); `ts:scopeA` + `ts:scopeB` (`ts:Scope`); `ts:relation` (`GreaterThan` / `LessThan` / `ApproximatelyEqual`); `ts:magnitude` (`ts:Magnitude`) | | `ts:ObservationClaim` | a metric had a specific value within a scope at a point in time; maps onto `qb:Observation` from the W3C Data Cube vocabulary | `ts:metric` (URI); `ts:value` (numeric or `qb:Observation`); `ts:unit` (URI; QUDT or UCUM reference) | | `ts:CompositionClaim` | a breakdown of a population, budget, or count into components (budget analyses, demographic breakdowns, etc.) | `ts:whole` (`ts:Scope`); `ts:components` (`ts:Component`[]); `ts:totalsTo` (`xsd:decimal`; 1.0 for proportions, total $ for budgets, etc.) | | `ts:RelationshipClaim` | a statistical relationship (correlation, regression coefficient) between two metrics within a scope; distinguished from causal claims (not in v1) | `ts:metricA` + `ts:metricB` (URI); `ts:relationshipType` (URI: `Correlation` / `RegressionCoefficient` / `RankOrderAgreement` / etc.); `ts:strength` (`xsd:decimal`) | | `ts:QualitativeClaim` | a claim that doesn't reduce to a single numeric assertion (pattern, procedural finding, typology) — permitted but flagged; downstream processors may treat differently | `ts:assertion` (`xsd:string`); `ts:groundingMethod` (URI: `Pattern Recognition` / `Document Analysis` / `Comparative Synthesis` / etc.). `ts:confidence` MAY be `ts:NotApplicable` with a required description. | #### 8.11.6 Extension mechanism Domain extensions extend the Typed Standards Claim Vocabulary by: 1. Declaring their own namespace (e.g., `nyc-housing:`, `transit:`, `health:`). 2. Subclassing one or more core claim types or defining new ones that extend `ts:Claim`. 3. Adding domain-specific properties. 4. Optionally referencing or aligning with external domain vocabularies (FHIR for health, GTFS for transit, DCAT for catalogs, etc.). 5. Publishing SHACL shapes for validation. **Example extension fragment.** A hypothetical NYC Housing extension might define: ```turtle nyc-housing:RentStabilizationClaim rdfs:subClassOf ts:ObservationClaim ; rdfs:label "Claim about rent-stabilized unit counts" ; sh:property [ sh:path nyc-housing:buildingClassification ; sh:class nyc-housing:BuildingClass ; sh:minCount 1 ; ] . ``` **Extension registry.** A lightweight registry at the Typed Standards project's canonical URL lists known extension vocabularies, their maintainers, their version status, and their SHACL shape files. Inclusion is informational; it does not confer endorsement. The registry URL is reserved pending domain registration; consumers should consult this specification's canonical URL (§2) for the current registry location. Governance of the vocabulary itself (versioning discipline, breaking-change protocol) is deferred to [Q10](open-questions.md#q10--civic-claim-vocabulary-as-a-full-ontology) until the full-ontology promotion lands. #### 8.11.7 Anti-patterns and prohibitions **Things this specification deliberately does not enable.** - **Causal claims.** The Typed Standards Claim Vocabulary v1 includes `ts:RelationshipClaim` for statistical relationships but no `ts:CausalClaim`. Causal inference requires either experimental design or strong identifying assumptions, neither of which the specification can verify. Domain extensions may add causal claim types if they include explicit identification-strategy fields. - **Free-form confidence.** `ts:confidence` MUST reference a method; `"high"`, `"medium"`, `"low"` strings without method backing are non-conforming. - **Implicit scope.** Every claim MUST have an explicit scope. "Crime is up" without geographic and temporal scope is non-conforming. **Translation laundering.** A common failure mode: an LLM produces vague prose, a translator extracts a precise-looking structured claim, and the precision of the claim is taken as the precision of the underlying analysis. The specification guards against this through: - The `ts:derivedVia` requirement, which exposes the translation prompt and source span (per §8.11.4). - The separately-signed `attestation/wasDerivedFrom/v1` extraction-attestation MUST-carry-`AnalyticalDerivation` rule refinement (a) §4 (per §8.11.3 step 2). - The requirement that confidence be method-derived. - The expectation that downstream processors surface the source span alongside the structured claim. **Vocabulary shopping.** Where multiple external vocabularies cover the same concept, claim authors SHOULD prefer the most widely adopted one for the domain. The extension registry lists recommended choices per domain. Authors who use a less-common vocabulary SHOULD include `owl:sameAs` or `skos:exactMatch` references to the canonical alternative. ### 8.12 The attestation/* namespace An **attestation** is one of two top-level type families: a signed node whose payload carries `targetNodeId` referencing the node it asserts about. Attestations cover lifecycle (withdraws / reinstates / supersedes / revises / publishes — operationalized per §8.10), reference (locatedAt — operationalized per §8.10 — / wasDerivedFrom / answersQuestion / supportedBy / opposedBy), claim-to-claim (corroborates / contradicts / endorses), and authority-bearing (certifies / evaluates / conforms) relations. The v0.1 sub-type table is ratified; sub-types declare their authorization rule (`publisher-only`, `any-with-binding`, or `specific-role-required`) and payload shape per the table. #### 8.12.1 Sub-type table (v0.1 ratified) | Sub-type | Relation kind | Authorization rule | Payload (beyond structural primitive) | |---|---|---|---| | `attestation/withdraws/v1` | lifecycle (publisher → status: withdrawn) | publisher-only | `targetNodeId`, `reason` (required, non-empty), `effectiveAt` (defaults to envelope timestamp) | | `attestation/reinstates/v1` | lifecycle (publisher → status: active after withdrawn) | publisher-only | `targetNodeId`, `priorWithdrawalNodeId`, `reason` (optional) | | `attestation/supersedes/v1` | lifecycle + claim-to-claim (old → new; corrective replacement) | publisher-only (typically same publisher) | `targetNodeId` (old), `successorNodeId` (new) | | `attestation/revises/v1` | lifecycle (version succession; prior revision stays valid) | publisher-only | `targetNodeId` (prior revision), `successorNodeId` (this revision) | | `attestation/publishes/v1` | lifecycle (sealed → public; transitions visibility) | publisher-only OR delegated-publisher per Q20 | `targetNodeId`, `publicationHost`, `releasedAt` | | `attestation/locatedAt/v1` | location pointer (content available at URI) | any-with-binding | `targetNodeId`, `uri`, `targetContentHash` (multihash; SHOULD match target's `contentHash`; mismatch is informative — content drift; named `targetContentHash` because the structural primitive claims `contentHash` for the attestation's own fingerprint — §8.10.2, Q48-resolved), optional `contentLength`, optional `availability` | | `attestation/corroborates/v1` | claim-to-claim agreement | any-with-binding | `targetNodeId`, `scope`, `reasoning` (optional) | | `attestation/contradicts/v1` | claim-to-claim disagreement | any-with-binding | `targetNodeId`, `scope`, `reasoning` (optional) | | `attestation/endorses/v1` | claim acceptance by authority-bearing party | specific-role-required (authority-bearing) | `targetNodeId`, `scope` | | `attestation/wasDerivedFrom/v1` | derivation pointer (PROV-O semantics) | any-with-binding (the deriver) | `targetNodeId` (source), `derivationMethod` (object; when source is `content/analysis/v1` with `untyped` content and target is a typed content sub-type, `derivationMethod` MUST carry a `ts:AnalyticalDerivation` per the classification-laundering guard, see refinement (a)) | | `attestation/answersQuestion/v1` | claim → question pointer | any-with-binding (the asserter) | `targetNodeId` (question) | | `attestation/supportedBy/v1` | claim → evidence pointer | any-with-binding (the asserting publisher) | `targetNodeId` (evidence) | | `attestation/opposedBy/v1` | claim → evidence pointer | any-with-binding (the asserting publisher) | `targetNodeId` (evidence) | | `attestation/certifies/v1` | tool / method authority-bearing | specific-role-required (certifying body) | `targetNodeId` (tool / method), `certificationScheme`, `validityWindow` | | `attestation/evaluates/v1` | claim evaluation | specific-role-required (evaluator with methodology + bindingTier per [Q26](open-questions.md#q26--valid-evaluator-definition-identity-binding--methodology-declaration)) | `targetNodeId`, `methodology`, `scoringRubric`, `results` | | `attestation/conforms/v1` | claim conformance pointer | self-attestation OR specific-role-required (third-party) | `targetNodeId`, `standardId` | **Succession vs. correction.** `attestation/revises/v1` (added per [ADR-0016](../adr/0016-vcs-native-lifecycle-mapping.md) §C) and `attestation/supersedes/v1` are deliberately distinct: `revises` records neutral version succession — the next version in a lineage, with no deprecation signal, the prior revision remaining a valid point-in-time snapshot — while `supersedes` records corrective replacement, signaling that consumers should stop relying on the superseded node. Conflating them would mislabel every routine revision as a correction (or launder a correction as a routine revision); see §8.10.5. #### 8.12.2 Existing attestation kinds map to sub-types The reference implementation has long supported a separate type of artifact — an attestation — that comments on a previously-published package without modifying it. The pre-v0.1 attestation kinds map to specific sub-types in the ratified table: - **`consistency`** (repeat-publish runs of the same prompt to surface determinism / drift) → emitted as a separately-signed `content/analysis/v1` node (the variance test itself) plus an `attestation/corroborates/v1` or `attestation/contradicts/v1` (depending on whether the result agrees with the original); the variance methodology + result delta live in the attestation's `reasoning` payload. - **`evaluation`** (adversarial review by an LLM-as-judge against a rubric) → `attestation/evaluates/v1` with declared methodology per [Q26](open-questions.md#q26--valid-evaluator-definition-identity-binding--methodology-declaration). Authorization rule: `specific-role-required` (evaluator with declared methodology + bindingTier). - **`expert_attestation`** (review by a named human expert with stated relationship to the original package) → `attestation/evaluates/v1` when the expert is producing a critique, or `attestation/endorses/v1` when the expert is vouching. Authorization rule: `specific-role-required` (authority-bearing party). A future migration will rewrite existing pre-v0.1 attestation records on `evidence_records` to emit conforming `attestation/*` nodes; the migration is scoped separately from this spec change. Until that migration ships, the existing attestation surface continues to work, and verifiers SHOULD treat existing records under the legacy attestation-kind vocabulary while emitting them under the new sub-type URIs for any new attestations. #### 8.12.3 Conformance requirements for attestation/* nodes A conformant `attestation/*` node: - MUST satisfy the structural-primitive requirements per §8.1 (envelope hash, content hash, content canonicalization, signature, optional timestamp + Rekor proof, `type`, `signer`, `metadata`). - MUST carry `type` matching a registered `attestation/*` sub-type URI (or a future sub-type minted by an ADR naming its motivating adopter). - MUST carry `targetNodeId` referencing at least one target node by `nodeId`. The target node need not be retrievable for the attestation to be verifiable — the attestation's signature is independent of the target's availability — but a verifier MAY report `unknown_target_node` per §9 when the target cannot be resolved. - MUST satisfy the sub-type's authorization rule. For `publisher-only` sub-types, the verifier MUST confirm that the attestation's `signer.identifier` matches the target node's `signer.identifier` (or that a delegated-publisher relationship is in effect per a future ADR). For `specific-role-required` sub-types, the role declaration lives in the sub-type's own normative section. For `any-with-binding` sub-types, the attestation's `signer.bindingTier` need only be at least `pseudonymous`. - MUST carry the sub-type's required payload fields per the §8.12.1 table. - SHOULD use the default content-canonicalization rule (`https://typedstandards.org/canonicalization/legacy-json/v1`; §8.2, §12.3). Attestation payloads are small structured JSON and rarely warrant a bespoke rule; a sub-type MAY name a different rule in its own normative section, and the envelope mechanics are unchanged either way. #### 8.12.4 Verifier expectations when an original node has one or more attestations A verifier rendering a `content/*` node with referencing `attestation/*` nodes SHOULD: - Surface aggregate attestation stats ("N corroborations, M contradictions, K endorsements") near the node's signature verdict. - Allow drill-down to individual attestations, each rendered with its own `signer`, signature verdict, and payload. - Distinguish sub-types visually so consumers can apply judgment per the §5.1 normative preamble (`corroborates` vs. `endorses` carry different signals; high-bindingTier vs. pseudonymous signers carry different signals; etc.). - For lifecycle attestations (`withdraws`, `reinstates`, `publishes`, `supersedes`), surface the lifecycle state per §8.10. Operationalization of specific sub-types lands per-ADR on its own timeline. The withdrawal/reinstatement/supersession/publication lifecycle sub-types and the `attestation/locatedAt/v1` location sub-type are operationalized; the adversarial-eval requirement model lands in a future ADR anticipated from [civic-ai-tools#72](https://github.com/npstorey/civic-ai-tools/issues/72) per [Q25](open-questions.md#q25--adversarial-evaluation-requirement-strength-on-publication-records) + [Q26](open-questions.md#q26--valid-evaluator-definition-identity-binding--methodology-declaration); the host self-attestation pattern (host endorsements + host-policy-required gating) lands when a host-self-attestation adopter blocks per [Q22](open-questions.md#q22--host-as-typeable-subject--host-self-attestation-shape). The ratified Q36 sub-type table is the taxonomy backbone; individual sub-types operationalize on their own per-ADR timelines. ### 8.13 Federation and discoverability > ⚠ **Subject to [Q2](open-questions.md#q2--federation-substrate) — federation substrate.** Also subject to [Q8](open-questions.md#q8--croissant-outbound-metadata) — Croissant outbound metadata. Both are independent of the package format (§8.1) and the cryptographic envelope (§8.3). The reference implementation publishes packages as stable URLs on `civicaitools.org`. There is currently no federation transport: a package's canonical home is the URL on the publisher's registry; cross-registry discovery is manual. The current direction names three candidate federation substrates (atproto firehose / labelers, KOI net with sensor nodes, nanopub network) and an orthogonal discoverability mechanism (outbound Croissant metadata at a well-known location on each record page, making packages discoverable via Hugging Face / Kaggle / CKAN / Schema.org-aware crawlers). This v0.1 draft makes no normative claim about federation transport or outbound metadata. Adopters running their own registries SHOULD publish to a stable, content-addressable URL and SHOULD honor the trust-registry contract for their own signing keys (§8.3.3), but no specific federation protocol is required. As [Q2](open-questions.md#q2--federation-substrate) resolves toward a specific substrate, this section will gain normative content describing how packages propagate across registries; until then, single-registry deployments and hand-replication between registries are the only patterns the specification contemplates. The publisher-registry indexing function reserved at `typedstandards.org` is an **indexing-only** coordination surface: it names declared publishers but is not in the verification path (per §7.3). A verifier never queries `typedstandards.org` to verify a package; the index has no authority to vouch for or reject any publisher's content. Inclusion in the index is informational. --- ## 9. Conformance > ⚠ **Offline verifiability is now a demonstrated property of the self-contained bundle** (§9.4, resolving [Q15](open-questions.md#q15--external-verification-testing) — external verification testing); **[formal conformance criteria](open-questions.md#q16--formal-conformance-criteria) (Q16) remain open.** The specification documents the operational understanding of conformance as it stands today, and stays honest about what is a demonstrated property versus a deferred target. ### 9.1 Package conformance A **conformant package** is a JSON object satisfying §8.1 that, when canonicalized per §8.2 and SHA-256-hashed, produces an envelope hash matching the URL slug and matching a successful Ed25519ph signature verification under the trust-registry contract (§8.3). A **conformant publisher implementation** is one that: 1. Validates the publish-route required-field set (§8.1.1, §8.6). 2. Builds canonical JSON with the JCS rule and produces the envelope hash per §8.3.1. 3. Signs the envelope-hash hex string with Ed25519ph using a key listed in the publisher's published trust registry (§8.3.3). 4. Persists the package at a content-addressable URL. 5. Honors the withdrawal / reinstatement lifecycle as signed, public, append-only `attestation/*` events per §8.10. 6. Carries the §5.1 normative preamble on every product surface that renders nodes. A **conformant verifier implementation** is one that performs every check in §9.2 against any node it processes, surfaces lifecycle state per §8.10 when present, and refuses to compute platform-issued correctness verdicts as constrained by §5.1. ### 9.2 Verification check list Per the v0.1 envelope + canonicalization + identity-binding rules, a verifier processing a node performs the following checks. The numbering reflects the v0.1 reference-implementation check order; future revisions may extend the list. 1. **Envelope integrity** *(verifies the package's exact byte content has not been altered since signing).* Recompute the envelope hash over the canonical JSON of the fetched package per §8.2's chain (v0.1: RFC 8785 JCS canonicalization of the unsigned envelope + SHA-256; pre-v0.1: `JSON.stringify` insertion-order + SHA-256). The result MUST equal the envelope-hash hex string the verify endpoint reports. 2. **Signature mathematics** *(verifies the signature was made by the named signing key over the package's exact bytes).* Verify the Ed25519ph signature over the envelope-hash hex string against the embedded `publicKey`. pre-v0.1 packages were signed over the legacy `JSON.stringify`-derived hex string; verifiers handle them under that legacy chain per §8.3.1. 3. **Content canonicalization rule resolution** *(verifies the verifier knows how to canonicalize the off-log content for hashing).* Read `contentCanonicalization` from the package (v0.1). Resolve the URI via the verifier's local rule registry, which holds the three v0.1 rules of §8.2 (`dathere-ag-jupyter/v1`, `legacy-json/v1`, `raw-bytes/v1`). An unknown URI reports `unknown_canonicalization_rule`; an absent field on a pre-v0.1 package implies the rule per the package's `contentProfile`. 4. **Content hash verification** *(verifies the off-log content bytes match the fingerprint the signature covers).* Apply the resolved canonicalization rule to the off-log content; multi-hash the canonicalized bytes per the algorithms in `contentHash` (v0.1 multihash form). Confirm at least one of the listed algorithms' digests matches. Under `raw-bytes/v1` (§8.2) the bytes are those of `output`: for an inline `output`, its UTF-8 encoding, verified from the package alone; for a BlobRef `output`, the bytes it names, obtained through the verifier's fetcher (the route check #9 uses). Check #4 then reports `ok` when the bytes are held and their SHA-256 equals `contentHash.sha256`; `content_hash_mismatch` when the bytes are held and their SHA-256 differs, and also when `contentHash.sha256` differs from the hex part of `output.ref`, which is decided from the package alone because no file can hash to two different signed digests; and `content_bytes_unavailable` when the bytes cannot be obtained. `content_bytes_unavailable` says the bytes were not checked: it is never reported as `ok`, and it is not a finding that the content was altered. Check #9 is unchanged and still checks the same bytes against `ref` and `size`. 5. **Trust-registry verdict** *(verifies the signing key is one the publisher currently authorizes — and reports its lifecycle status: active, deprecated, revoked).* Look up the envelope's `(kid, publicKey)` pair in the trust registry; apply the status semantics from §8.3.3. Under a key-derived `signer.identifier` (§8.5.1), apply §8.3.3's rules for a key-derived identifier, whose verdict may be `self_certified`; `self_certified` is not a verified status. 6. **`metadata.signingKeyId` consistency** *(verifies the envelope's `kid` matches the key id embedded in the package's canonical JSON, ruling out an envelope swap).* Confirm the `kid` from the signature envelope equals `metadata.signingKeyId` in the package. A mismatch indicates an envelope-vs-canonical drift. 7. **Timestamp validity** *(verifies the package existed at or before the asserted time, per the public TSA's signature).* When the package's proof carrier (the §8.8 commitment view / self-contained bundle, or the reference implementation's verify endpoint) supplies a non-null RFC 3161 token, a conformant verifier **cryptographically verifies** the TSA's signature over the token AND validates the token's embedded signing-certificate chain to a pinned TSA root anchor (§10.3) — full X.509 cert-chain validation, not token-presence or a parity check. ENFORCED in `@typedstandards/verify-core@0.6.0`; demonstrated offline by the §9.4 Q15 harness. 8. **Transparency-log inclusion** *(verifies the signing event is recorded in a public append-only log).* When the proof carrier supplies a non-null Rekor entry, a conformant verifier **cryptographically verifies** RFC 6962 Merkle inclusion of the entry against a pinned Rekor log public key and the entry's signed checkpoint (§10.3) — not entry-id presence, and not a parity check against the log's self-reported root. ENFORCED in `@typedstandards/verify-core@0.6.0`; demonstrated offline by the §9.4 Q15 harness. 9. **BlobRef integrity** *(verifies that blob-referenced sub-content matches the fingerprint embedded in the package).* For every BlobRef in the package, fetch the URL over HTTPS, recompute SHA-256, and confirm size. See §8.1.5. 10. **Lifecycle state** *(verifies the package's current published / withdrawn / superseded status via the publisher's signature on each lifecycle transition).* Detect withdrawal, reinstatement, supersession, or publication via the chain of signer-matched `attestation/*` lifecycle nodes referencing the target by `nodeId` per §8.10; verify the corresponding lifecycle signatures and timestamps for each attestation independently. Apply the retention-asymmetry rule per §8.10.3. 11. **`captureMethod` label** *(reads how the bytes were captured — the label is signature-covered, so it cannot be silently re-described).* Read `metadata.captureMethod`; render it alongside the signature verdict. The label is covered by the signature. 12. **`type` resolution** *(reads the node's family + sub-type so the verifier can apply per-sub-type rules; unknown sub-types render as `unknown_type` rather than failing verification).* Read the `type` field (v0.1). Resolve to one of the v0.1 sub-types (or future ADRs minting new sub-types). pre-v0.1 packages omit the field; verifiers interpret implicit `content/analysis/v1`. A `type` URI not in the verifier's local sub-type registry reports `unknown_type` rather than failing verification. 13. **`nodeId` cross-check** *(verifies that any attestation pointing at this node references the right envelope hash).* Recompute the envelope hash per §8.2 + §8.3.1. The result IS the `nodeId` by construction. For nodes referencing this one (an attestation whose `targetNodeId` points here), the `targetNodeId` MUST resolve to this envelope hash; a `targetNodeId` that does not resolve to any known envelope hash reports `unknown_target_node`. 14. **`signer.identifier` ↔ `sig.kid → trust-registry signerIdentity` cross-check** *(verifies the package's signer-identity claim matches the registry's record for the signing key, ruling out a kid-swap-with-mismatched-identity attack).* Read the package's `signer.identifier` (v0.1). Look up the envelope's `kid` in the trust registry. Compare `signer.identifier` against the registry entry's `signerIdentity.identifier`. A mismatch MUST cause the verifier to report `signer_identity_mismatch` and reject the node. pre-v0.1 packages have no envelope-side `signer.identifier`; verifiers derive `signer` from the registry entry and skip the cross-check. **Under a key-derived identifier** (§8.5.1), the verifier recomputes the identifier from the envelope's `publicKey` and compares it with `signer.identifier` byte for byte. A mismatch MUST cause the verifier to report `key_derived_mismatch`, carrying the `claimed` and `derived` values, and reject the node, whatever the `bindingTier` and whether or not a registry is supplied. A match reports `key_derived_match`. Check #14 never reports `ok` under a key-derived identifier, whatever the registry's source: `ok` means the identity matches a registry's record. When a supplied registry lists the envelope's `(kid, publicKey)` with a `signerIdentity.identifier` that differs from the claimed identifier, the verifier reports `signer_identity_mismatch` and rejects the node, whether the registry was fetched or carried in the bundle. When the package itself is absent, no key-derived check runs and check #14 reports nothing. 15. **`captureMethod` per-profile vocabulary conformance** *(verifies the `captureMethod` value is one the package's Producer Profile actually authorizes).* Resolve the package's `producerProfile` (or its legacy-alias fallback; or the implicit `ai-assisted-analysis` profile-type for pre-v0.1 packages). Confirm `metadata.captureMethod` is in the declared vocabulary. A value not in the declared vocabulary reports `captureMethod_unknown` and rejects the node. A producerProfile whose bundle cannot be resolved reports `producerProfile_bundle_unresolved` and degrades gracefully — the value is preserved verbatim, the structural integrity check (#11) still passes, only the vocabulary-conformance assertion is unverified. v0.1 verifiers resolve two profile types, `ai-assisted-analysis` and `scripted-recomputation` (§8.6); the subtype is not checked. 16. **Content-profile label** *(reads `metadata.contentProfile` and checks it against the known values and against `producerProfile`).* Read `metadata.contentProfile` and report one of four statuses: `ok` when the value is `"default"` or `"datHere"` and consistent with `producerProfile` under the §8.1.1 invariant, or `producerProfile` is absent; `contentProfile_absent` when the key is absent, read as `"default"` (§8.1.2); `contentProfile_unknown` when the value is neither known value, in which case consistency is not judged; `contentProfile_inconsistent` when the value is known, `producerProfile` is present, and the invariant fails. No status of this check rejects the node or alters the envelope-integrity or signature verdicts: both labels are inside the signed bytes, so an unknown value is an unrecognized identifier and a contradiction between the two is a producer error, not evidence of tampering. A package reporting `contentProfile_inconsistent` is reported malformed ([ADR-0006](../adr/0006-producer-profile-architecture.md) §2). ### 9.3 What a verifier cannot check today A verifier cannot determine, from any combination of public sources, any of: 1. Whether the captured analysis matches what was actually generated in the original session. The `captureMethod` label is the structural answer; verbatim guarantees follow from the labeled mechanism, not from the signature. 2. Whether the assistant's prose or numerical outputs are correct. Correctness review is a separate, separately-signed attestation (see §8.12). 3. Whether the analysis was authored under coercion, paid promotion, or other conflicts of interest. The specification surfaces identity and provenance; the consumer applies judgment. 4. Whether the package's claims have been corroborated or contradicted by other packages without iterating the attestation graph. Cross-package operations require resolving the relevant `attestation/*` nodes targeting this package's `nodeId`. ### 9.4 Offline verifiability and honest-status notes Offline verifiability has graduated from an aspirational target to a **demonstrated property of the self-contained commitment bundle** (§8.8). The reference verifier `@typedstandards/verify-core@0.6.0` — a portable, browser-safe library carrying no `civicaitools.org` dependency — cryptographically completes every check in §9.2 against a self-contained bundle with **zero network access** (for the two cases v0.1.9 adds, a key-derived signer identifier and a `raw-bytes/v1` BlobRef `output`, see the #5 entry below and the note after the list). The bundle inlines both the proofs that previously required the reference implementation's server-composed verify endpoint (#2, #7, #8, #10) and the publisher trust registry that the verdict check (#5) previously fetched live: - **#2 signature** and **#5 trust-registry verdict** — verified against the trust registry inlined in the bundle (no live `/.well-known` fetch). For a key-derived `signer.identifier` (§8.5.1), #5 is decided from the signing key: a registry inlined in the bundle can report only a lowering verdict (`revoked`, `deprecated_invalid`) and never raises the status above `self_certified` (§8.3.3), and so can a registry obtained from a URL whose scheme is not `https:`, since only an `https:` URL is a declared registry URL (§8.3.3 rule 3); - **#7 timestamp** — full RFC 3161 TSA-signature verification plus X.509 cert-chain validation to a pinned trust anchor (§10.3); - **#8 transparency-log inclusion** — RFC 6962 Merkle-inclusion proof verified against a pinned Rekor log key and the entry's signed checkpoint (§10.3); - **#10 lifecycle state** — resolved from the signed `attestation/*` chain carried inline, each node's signature verified in-process. **Raw-bytes content by BlobRef (v0.1.9).** Under `raw-bytes/v1` with a BlobRef `output` (§8.2), check #4 needs the file's bytes, as check #9 does, and the bundle does not carry them (whether it should is [Q1](open-questions.md#q1--package-format)). An offline verifier completes #4 and #9 only when the file travels with the bundle and is supplied through the verifier's fetcher. Without the bytes, check #4 reports `content_bytes_unavailable` — the bytes were not checked, which is neither a pass nor a finding of alteration — and check #9 reports the blob as it reports any BlobRef whose fetch fails (§8.1.5). An inline `raw-bytes/v1` `output` verifies from the package alone. The demonstration is the **Q15 offline-bundle harness** (`apps/web/src/lib/q15-offline-bundle.test.ts` in the verifier monorepo): it runs the full verify flow over real production bundles — captured from `GET /api/evidence//commitment?inline=1` (§8.8, the self-contained serialization: package + stamped trust registry + RFC 3161 token + Rekor entry body and inclusion proof + lifecycle chain, all inline; the route segment as captured — under the 2026-08-19 settlement the canonical segment becomes `/api/records/`, with the prior-era `/api/evidence/` segment a permanent alias per Appendix J) — behind a `fetch` stub that **throws on any call**, asserting zero fetches and a `fullyOffline` resolution. The fixtures span the verification matrix: a full-depth package (active key, TSA cert-chain, Merkle inclusion, and #10 resolved through a `withdraws → reinstates` attestation chain), a prod-parity package, a legacy/calm package (no Rekor entry, `legacy_embedded` key, withdrawn — verified offline without a false alarm), and a synthetic in-process-minted bundle that keeps the offline-plumbing regression independent of drift in captured production material. This is the hermetic external verification that [Q15](open-questions.md#q15--external-verification-testing) called for: real packages, full depth, no `civicaitools.org` endpoint in the verification path. **Scope discipline — what is demonstrated and what stays open.** The demonstrated property is offline verifiability **of the self-contained bundle serialization**, not of the single-blob package alone. The bundle achieves it by carrying the proofs and the trust registry inline (the §8.8 commitment view in its `?inline=1` form); the canonical single-blob **package** still does not embed its own proofs, so a bare package handed to a verifier without its accompanying commitment view still depends on an out-of-band proof carrier. Whether the package format itself should embed the proofs — collapsing the package and its bundle into one artifact — remains [Q1](open-questions.md#q1--package-format) (package format), which the offline-hardening arc did **not** resolve: it decoupled offline verification from the package-format question rather than answering it. The lighter (non-`inline`) §8.8 commitment-view sidecar references the trust registry by URL and so still performs public-infrastructure fetches (the publisher's registry, the package blob); it is offline *from the publishing platform* but not zero-network. Zero-network full-depth verification is the property of the `?inline=1` self-contained bundle specifically. **Revocation-staleness — an honest-status note (offline physics, not a gap to fix).** A self-contained bundle carries the trust registry as of the bundle's `generatedAt` (the stamped `/.well-known` snapshot). An offline verifier therefore cannot observe a key revoked *after* that snapshot: it renders a verdict "verified against the publisher's trust registry as of ``," not "verified against the registry as it stands now." This is the same forensic boundary §10.2 names for a compromised signing key — the distinction between a pre- and post-revocation signature lives in the timestamp's relationship to the revocation event, which an offline snapshot cannot see. A conformant verifier SHOULD render the as-of date alongside the trust verdict and SHOULD offer an online-recheck affordance that re-fetches the live registry. This is a property of offline operation, not a defect: documented, not "fixed." Formal conformance criteria, a reference test corpus, and a conformance-claims registration mechanism remain deferred per [Q16](open-questions.md#q16--formal-conformance-criteria). The Q15 harness sharpens what that work would formalize — the required §9.2 check-list, the pinned trust anchors (§10.3), and the self-contained-bundle serialization (§8.8) together describe the conformance shape an external implementation would target — but a formal suite, versioned corpus, and registration mechanism are not yet specified. The current de-facto corpus is the set of packages published on civicaitools.org plus the real fixtures captured into the Q15 harness; verifying any of them against this specification is the closest thing to a conformance test today. --- ## 10. Security Considerations This section names the threats the specification's cryptographic envelope, identity-binding, and trust-registry mechanisms address — and the threats they explicitly do not address. ### 10.1 Threat model The specification's primary adversary is a **post-publication tamperer**: a party who attempts to modify a published package, its capture-method label, its content-hash, its `type` or `signer` field, its identity binding, or its lifecycle state after the original publisher has signed it. The envelope hash + Ed25519ph signature + RFC 3161 timestamp + Sigstore Rekor inclusion proof composition is designed to detect any such modification. Secondary adversaries: - A **kid-swap attacker** who attempts to attach a valid signature under one key while claiming a different identity binding. Mitigated by the §8.3.1 `sig` vs. `signer` split + the §9.2 check 14 cross-check between `sig.kid` (via trust-registry `signerIdentity`) and envelope-side `signer.identifier`. - A **trust-registry impersonator** who attempts to substitute the publisher's trust-registry response with a different key list. Mitigated by the publisher hosting their own trust registry at a well-known path on their own domain (§8.3.3); a verifier who fetches the registry over HTTPS against the publisher's domain has the same trust assumption as TLS to that domain — no additional trust dependency on a central registry. - A **transparency-log replacement attacker** who attempts to retroactively change the Rekor entry. Mitigated by Rekor's own transparency-log mechanics (append-only Merkle-tree-backed log; inclusion proofs are verifiable against the log's signed checkpoint). - A **timestamp-grinding attacker** who attempts to substitute a past timestamp for a present one. Mitigated by the RFC 3161 TSA's signed-token mechanism (the token includes the TSA's own signature over a hash + timestamp; the verifier checks the TSA's CA chain). - A **false-VCS-binding publisher** who self-asserts a `vcsRef` (§8.1.1) to a commit or repository that does not exist, is unreachable, or does not contain the referenced source. An instance of the pre-signing-fabrication class: the signature attests the *assertion*, not the *fact* — it establishes that this signer asserted this binding, tamper-evidently; it does **not** establish that the referenced git state exists or matches. Mitigated per [ADR-0016](../adr/0016-vcs-native-lifecycle-mapping.md) §B by (a) **verify-on-fetch** — a verifier MAY resolve `repoUrl` + `commitSha` and check the artifact at `path` against the node's `contentHash`, with a mismatch or unreachable commit surfaced as an informative signal rather than a hard failure (per the §5.1 preamble, mirroring the `attestation/locatedAt/v1` treatment in §8.10.2) — and (b) **`captureMethod`-weighting** — the weight a consumer places on an *unverified* `vcsRef` is contextualized by the package's capture method (a platform-attested capture lends a self-asserted reference more weight than a user-attested, locally-editable one). No new `captureMethod` value is needed. ### 10.2 Threats the specification does not address - **Content correctness.** The signature attests that the package was published and has not been altered; it does not attest that the content is correct. Editorial review, fact-checking, replication, and adversarial evaluation ride alongside as separately-signed `attestation/*` nodes (§8.12), never enforced by the envelope. - **Compromised signing key.** A signing key disclosed to an adversary can produce valid signatures under that `kid` until the registry entry is moved to `revoked` status. The specification mitigates the impact via the trust registry's `revoked` status semantics (§8.3.3); pre-revocation signatures may have been produced by the legitimate signer or by the adversary, and the distinction lives in the timestamp's relationship to the disclosure event — a forensic question, not a protocol guarantee. - **Compromised self-certifying key.** A self-certifying signer (§8.5.1) has no registry, so its key cannot be rotated or revoked. A compromised self-certifying key can only be abandoned: a new key is a new identifier with no link to the old one, nothing in a package signed under the old key says it was abandoned, and a verifier working from a bundle can never learn that the key was compromised. The one exception is a registry that lists the key and marks it revoked (§8.3.3 rule 3), which a verifier given that registry sees. - **Offline-snapshot revocation staleness.** A verifier working from a self-contained bundle (§8.8 `?inline=1`) checks the signing key against the trust registry *as of the bundle's `generatedAt` snapshot*; it cannot see a key revoked after that moment. This is the offline-operation corollary of the compromised-key boundary above: the verdict is honestly "verified against the registry as of ``," and a conformant verifier SHOULD surface that as-of date plus an online-recheck affordance (§9.4). This is a property of offline verification, not a threat the envelope fails to detect. - **Coerced signing.** A publisher signing under duress produces valid-by-protocol packages. The specification surfaces identity and provenance; consumers apply judgment per §5.1's normative preamble. - **Adversarial AI output.** An AI assistant that fabricates plausible-looking but incorrect output produces packages whose envelope verifies cleanly. The `captureMethod` label tells a reader the bytes were faithfully captured; it does not tell them the bytes are correct. This is the same property the specification names in §5.2 — production-process attestation, not truth. ### 10.3 Per-mechanism notes - **Ed25519ph signature** (§8.3.1) — RFC 8032 §5.1.2. The pre-hashed variant is used because the signing chain hashes the envelope at SHA-256; Ed25519ph's internal SHA-512 prehash composes correctly with that input. Implementations using primitives that expose only Ed25519 (not Ed25519ph) MUST NOT pre-hash on the application side, since Ed25519's internal handling differs. - **RFC 3161 trusted timestamp** (§8.3.2) — the reference implementation uses FreeTSA (`freetsa.org`); alternative public TSAs are acceptable. **Pinned trust anchor.** A conformant offline verifier validates the token's embedded signing certificate (an ECDSA P-384 leaf) up a full X.509 chain to a *pinned* FreeTSA root: the self-signed `O=Free TSA, OU=Root CA` RSA-4096 CA, SHA-256 fingerprint `A6:37:9E:7C:EC:C0:5F:AA:3C:BF:07:60:13:D7:45:E3:27:BB:BA:A3:8C:0B:9A:F2:24:69:D4:70:1D:18:AA:BC`, captured 2026-06-07 from `https://freetsa.org/files/cacert.pem`. `@typedstandards/verify-core@0.6.0` carries this anchor as a documented constant (`FREETSA_ROOT_ANCHORS`); the cert chain is validated RSASSA-PKCS1-v1.5/SHA-512 and the leaf's ECDSA-P384 signature over the timestamp is checked under the chain-validated leaf key. Pinning the long-lived root (not the signing cert) lets the verifier trust any signing cert the root issues. Adopters changing TSAs MUST document their root anchor — fingerprint and provenance — here, so verifiers can resolve it. - **Sigstore Rekor inclusion proof** (§8.3.2) — `rekor.sigstore.dev` is the reference. **Pinned trust anchor.** A conformant offline verifier verifies RFC 6962 Merkle inclusion against the public-good Rekor shard's *pinned* log public key (ECDSA P-256; log id `c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d`, which is the SHA-256 of the key's SPKI DER and is independently corroborated against Sigstore's TUF `trusted_root.json`) and the entry's signed checkpoint (a transparency-dev Go signed-note, matched to the anchor by the note's 4-byte key hint). `@typedstandards/verify-core@0.6.0` carries this anchor as a documented constant (`REKOR_LOG_ANCHORS`). Private-Rekor substrates are design-permitted per §8.3.2 and Xanadu-gated for implementation; adopters needing pre-publication content commitments should plan for the substrate question per [Q2](open-questions.md#q2--federation-substrate), and a private-log adopter documents its own log anchor here. - **Identity binding** (§8.5) — graded ladder is informative; only GitHub OAuth is built. Higher-tier binding (ORCID, did:web, notarized) carries stronger signals but is no substitute for editorial judgment; the §5.1 preamble's "identity strength ≠ topic authority" line is the protocol's stance. --- ## 11. Privacy Considerations The specification's transparency-log inclusion + trust-registry contract make the *fact* of a publication public the moment a package is signed. This section names the privacy implications and the design-permitted patterns for sensitive material. ### 11.1 Transparency-log inclusion is a disclosure Publishing a node's commitment to a transparency log (Sigstore Rekor by default per §8.3.2) is itself a public act: - The envelope hash (`nodeId`) becomes a public record the moment the inclusion proof is obtained. - The envelope timestamp (the moment of publication) becomes public. - The signer's identity — via the trust registry's `signerIdentity` for the envelope's `kid` (§8.3.3, §8.5) — becomes resolvable. This is the intended property for **published analyses where transparency is a feature**: civic-data analyses published for public consumption, regulatory submissions intended for the record, academic-style preprints, audit work product. It is part of the trust contract this specification offers — a verifier holding only the package + public infrastructure can confirm the publication is genuine. It is **not a neutral act** for sensitive or pre-publication content: - A draft analysis the publisher is still editing — once it has been signed and Rekor-included, the existence of the publication and the publisher's identity are public, even if the content URL has not been advertised. - Recipient-distributed content (a publisher sharing an analysis with a specific recipient over a private channel) — once Rekor-included, the publication is public. - Enterprise-internal analyses an organization wants signed for integrity but kept off the public log. ### 11.2 Visibility-lifecycle semantics The specification PERMITS truly-no-public-footprint content via the zero-`attestation/locatedAt/v1` base case: a content node MAY be signed without any `attestation/publishes/v1` or `attestation/locatedAt/v1` references. In this base case, the signer holds the bytes; no public location is asserted; the package is verifiable by the signer (who has the bytes) but not retrievable by anyone else. Even in this base case, **the Rekor inclusion proof (if obtained) reveals the publication's existence and the signer's identity**. Truly-no-public-footprint mode requires the publisher to either: 1. Skip Rekor inclusion for sensitive content (and accept the loss of transparency-log timestamp credibility), or 2. Use a **private transparency-log substrate** — an organizational-internal Rekor-equivalent log, a recipient-distributed inclusion-proof protocol, or a deferred-publication pattern where the public log entry is created only when an `attestation/publishes/v1` lifecycle event lands. Private-log substrates are design-permitted per §8.3.2 §4 but Xanadu-gated for implementation: no built private-log substrate exists in v0.1, and the specification does not normatively specify the substrate's shape. Adopters needing private-log support today should treat it as an open question and plan accordingly per [Q2](open-questions.md#q2--federation-substrate). ### 11.3 Identity-binding privacy Higher-tier identity binding (§8.5) carries privacy implications: a publication signed under an ORCID or DNS-bound `did:web` identity is publicly attributable to the named individual or organization. Publishers SHOULD consider the identity-tier choice as part of the publication's disclosure surface, not as a separate technical decision. The graded identity ladder permits pseudonymous publication: a publisher MAY sign under a `bindingTier: "pseudonymous"` identity (e.g., a Sigstore-keyless ephemeral identity, a one-time signing key with no registry-recorded human binding). Pseudonymous signers SHOULD expect their consumers to apply weaker trust signals — `identity strength ≠ topic authority` cuts both ways per §5.1. ### 11.4 Withdrawal does not erase Per §8.10.3, a publisher's withdrawal authority is bounded to their own pointer and status label. A withdrawn content node remains: - **In the transparency log.** The Rekor entry is append-only. - **At any third-party `attestation/locatedAt/v1` URL** that asserted the location before or independent of the withdrawal. - **In any backup-host's signed location attestations** that have not themselves been retracted. Adopters publishing material with privacy-sensitive content SHOULD treat publication as a one-way operation: anything that goes into the transparency log is permanently public; withdrawal removes the publisher's own visibility commitment but does not retract the public record of publication. The retention-asymmetry rule per §8.10.3 is the deliberate civic-accountability feature; it is also the protocol's privacy limitation. --- ## 12. IANA Considerations ### 12.1 Well-known URI registration This specification requests **provisional registration** of `/.well-known/typed-publisher.json` in the IANA Well-Known URI Registry per [RFC 8615](https://www.rfc-editor.org/rfc/rfc8615). The registration request is filed at ; the citing document is this specification at its canonical URL (§2). The registration procedure per RFC 8615 is Specification Required; the designated expert is Mark Nottingham. The legacy path `/.well-known/evidence-public-keys.json` is NOT IANA-registered (and never was); reference implementations serving the legacy path do so as a backwards-compatibility convenience, not as an IANA-recognized path. Under the 2026-08-19 vocabulary settlement the legacy path is the exempt-frozen leg of the already-completed trust-registry rename (ruling D2; Appendix J) — recorded, not renamed again. Promotion to a permanent registration is deferred until a stable v1.0 of this specification is published at a permanent URL and at least one external implementation has demonstrated conformance. Provisional status is appropriate for v0.1. ### 12.2 Vocabulary URI This specification reserves the JSON-LD / RDF prefix `ts:` resolved to `https://typedstandards.org/ns/ts#`. The vocabulary URI is an identifier, not a fetch target; this specification does not request IANA action for the vocabulary URI. Registration on the prefix.cc de facto JSON-LD/RDF prefix registry is a coordination item (not IANA-administered). ### 12.3 Canonicalization-rule URIs This specification reserves the following content-canonicalization rule URIs: - `https://typedstandards.org/canonicalization/dathere-ag-jupyter/v1` — the datHere A-G/Jupyter content profile (§8.7). - `https://typedstandards.org/canonicalization/legacy-json/v1` — the legacy default content profile. - `https://typedstandards.org/canonicalization/raw-bytes/v1` — the bytes of a package's `output` as the file holds them, with the identity canonicalization (§8.2). These URIs are identifiers; IANA action is not requested. --- ## 13. Internationalization Considerations The specification's envelope, signature mechanics, hash construction, and transparency-log mechanics are byte-oriented and culture-independent. JSON-LD's multi-language support applies to the content layer: `rdfs:label`, `schema:name`, and similar properties on typed claims MAY carry `@language` tags for translated forms. The `dcterms:identifier` and `dcterms:description` properties on `ts:Claim` (§8.11.4) are JSON-LD strings and MAY carry language tags. The `summary` field on `datHere`-content-profile packages (§8.7.1 requirement 6) and the `subjectTitle` / `subjectSummary` fields on the commitment view (§8.8.1) are single-language strings in v0.1. A future revision may add explicit multi-language support if a real adopter requires it; the current scope is single-language strings with the publisher's choice of language. Civic geographic-scope subtypes (§8.11.4) are jurisdiction-specific. International equivalents (statistical-area-1 in Australia, OA in the UK, etc.) should be added as domain extensions rather than core subtypes. The base `ts:GeographicScope` shape with `geo:hasGeometry` accommodates arbitrary geometries for jurisdictions outside the named taxonomy. Authors of typed claims in non-English jurisdictions MAY use domain extensions that publish vocabulary terms in their local language; the SHACL shapes published with the extension SHOULD validate against the local-language property names. --- ## 14. References ### 14.1 Normative References - **RFC 2119** — *Key words for use in RFCs to Indicate Requirement Levels* (S. Bradner, 1997). - **RFC 3161** — *Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP)* (Adams et al., 2001). Used by §8.3.2. - **RFC 8032** — *Edwards-Curve Digital Signature Algorithm (EdDSA)* (Josefsson & Liusvaara, 2017). Ed25519ph variant per §5.1.2; used by §8.3.1. - **RFC 8615** — *Well-Known Uniform Resource Identifiers (URIs)* (Nottingham, 2019). Used by §8.3.3 and §12.1. - **RFC 8785** — *JSON Canonicalization Scheme (JCS)* (Rundgren, Jordan, Erdtman, 2020). Used by §8.2 and §8.3.1. - **W3C PROV-O** — *PROV-O: The PROV Ontology* (Lebo et al., W3C Recommendation 2013). Used by §8.1.4. - **Multihash** — *Multihash format specification* (Multiformats community). Used by §8.2 + §8.1.1's `contentHash` field. - **Sigstore Rekor** — *Rekor: A transparency log for software artifacts* (Sigstore community). Used by §8.3.2. - **in-toto Attestation Framework** — *in-toto Attestation Framework specification* (in-toto / SLSA community). Used as structural alignment for multihash + `predicateType`-style URIs. ### 14.2 Informative References - **C2PA Technical Specification** — *Content Authenticity Initiative / Coalition for Content Provenance and Authenticity*. Used for §5.5 disambiguation paragraph. - **W3C Verifiable Credentials Data Model 2.0** — *VC Data Model 2.0* (W3C Recommendation). Used for §5.5 disambiguation paragraph. - **SLSA** — *Supply-chain Levels for Software Artifacts*. Used as structural analogue per §5.5. - **Sigstore (Cosign / Fulcio)** — *Sigstore: a free, ephemeral-key signing infrastructure*. Used as foundational infrastructure per §5.5. - **W3C OWL-Time** — *Time Ontology in OWL* (W3C Recommendation). Used by §8.11.4. - **W3C RDF Data Cube** — *The RDF Data Cube Vocabulary* (W3C Recommendation). Used by §8.11.4. - **OGC GeoSPARQL 1.1** — *OGC GeoSPARQL: A geographic query language for RDF data*. Used by §8.11.4. - **Schema.org** — *Schema.org vocabulary*. Used by §8.11.4. - **Dublin Core Terms** — *DCMI Metadata Terms*. Used by §8.11.4. - **W3C SDMX-RDF** — *SDMX-RDF specification*. Used by §8.11.4. - **Discourse Graphs** — *Discourse Graphs: A novel notation for capturing scholarly discourse* (Joel Chan and collaborators). Source of the QEC pattern per §5.5 + §7.5. - **Nanopublications** — *Nanopublications: A novel format for the dissemination of scientific data*. Adjacent specification per §5.5. - **RO-Crate / WRROC** — *Research Object Crate*. Candidate package-container per §5.5 and [Q1](open-questions.md#q1--package-format). - **DCAT** — *Data Catalog Vocabulary*. Adjacent specification per §5.5. - **2026-05-26 deep-research memo** (`/temp/q39_40_memo`, workspace-local) — pre-consolidation research memo executing the [Q40](open-questions.md#q40--pre-adoption-research-for-typedstandardsorg-rename--namespace-prefix-selection-ts-vs-tss-vs-alternatives) seven-item research checklist; not committed under the workspace stakeholder/relationship-content boundary; cited Context as the research basis for the consolidation's pre-decided items. - **2026-05-25 strategic memo** (`civic-ai-tools/docs/architecture-incorporation-memo-2026-05-25.md`, workspace-local) — the G1-G4 sequencing memo whose MVP-cohort gate the consolidation trigger fires after. --- ## 15. Appendices ### Appendix A. Citation conventions The canonical citation form for this specification follows the convention named in the 2026-05-26 deep-research memo §6.4: > *Typed Standards. Specification Name, Version. Status. Published at URL. Date.* Example (for a future v0.1-frozen envelope-section citation): *Typed Standards. Envelope Specification v0.1. Working Draft. Published at https://typedstandards.org/specs/v0.1/. 2026-06-XX.* Academic citation follows the prevailing pattern across SLSA / in-toto / C2PA papers: cite the specification by its stable URL with an accessed-date footnote. Once v0.1 is frozen, a Zenodo DOI is applied for (the standard mechanism for academic citability of grey literature); the DOI takes precedence over the URL form for academic citations once issued. DOI application is deferred to v0.2 — v0.1 may need substantive revisions before freeze, and DOIs are permanent. For citation of a specific section of this specification, append the section number (e.g., `Typed Standards Specification v0.1, §8.3.1 Signature`). Section **numbers** are the stable citation unit and are preserved across patch revisions; major-revision renumbering will preserve aliases. Rendered heading anchors (e.g., GitHub's auto-generated heading slugs) derive from heading *text* and can change when a heading is retitled within a patch revision — the v0.1.5 retitle of §8.1 ("Evidence package structure" → "Record package structure") is the case on record — so durable citations should use section numbers plus the patch-revision tag, not heading-text anchors. ### Appendix B. Worked example: typed claim A worked example of a `content/claim/v1` node carrying a `ts:TrendClaim` for noise complaints in Bushwick, 2024 vs. 2025. The example is illustrative; it does NOT represent a real published package. ```json { "@context": [ "https://typedstandards.org/ns/ts#", { "ex": "https://example.gov/datasets/311/" } ], "@graph": [ { "@id": "claim-001", "@type": "ts:TrendClaim", "dcterms:identifier": "claim-001", "dcterms:description": "Noise complaints rose materially in Bushwick North between 2024 and 2025.", "ts:subject": { "@id": "ex:complaint-count" }, "ts:metric": { "@id": "ex:complaint-count", "schema:name": "311 noise complaint count" }, "ts:scope": { "@type": "ts:Scope", "ts:geographicScope": { "@type": "ts:NeighborhoodTabulationArea", "dcterms:identifier": "BK0801", "schema:name": "Bushwick North" }, "ts:temporalScope": { "@type": "time:Interval", "time:hasBeginning": { "time:inXSDDate": "2024-01-01" }, "time:hasEnd": { "time:inXSDDate": "2025-12-31" } } }, "ts:baselinePeriod": { "time:hasBeginning": { "time:inXSDDate": "2024-01-01" }, "time:hasEnd": { "time:inXSDDate": "2024-12-31" } }, "ts:comparisonPeriod": { "time:hasBeginning": { "time:inXSDDate": "2025-01-01" }, "time:hasEnd": { "time:inXSDDate": "2025-12-31" } }, "ts:direction": "ts:Increase", "ts:magnitude": { "@type": "ts:Magnitude", "ts:percentChange": 23.0, "ts:absoluteChange": 1842 }, "ts:confidence": { "@type": "ts:ConfidenceStatement", "ts:method": "ts:FrequentistInterval", "ts:level": 0.95, "ts:lowerBound": 18.4, "ts:upperBound": 27.6, "ts:methodReference": "trace.json#step-stat-test-3" }, "prov:wasDerivedFrom": [ { "@id": "source-analysis-nodeId#query-result-2" } ], "ts:derivedVia": { "@type": "ts:AnalyticalDerivation", "ts:traceReference": "trace.json#step-claim-extraction-1", "ts:translationModel": { "@type": "schema:SoftwareApplication", "schema:name": "claude-opus-4-7" }, "ts:translationPrompt": { "@id": "prompt.json#claim-extraction" }, "ts:sourceOutputSpan": { "ts:outputFile": "output.md", "ts:byteRange": [1240, 1487] } }, "ts:limitations": "Excludes complaints recorded against addresses without geocoded NTA assignment (~3.1% of records)." } ] } ``` The example carries both the core-required `ts:subject` (what the claim is about — here the same metric URI the TrendClaim's `ts:metric` names; §8.11.4 required-properties table) and the TrendClaim-specific `ts:metric`. *(Correction 2026-08-03: earlier revisions of this example omitted `ts:subject` and therefore failed the vocabulary's own required-property set — surfaced by the formalization collaborator's validation pass.)* The accompanying `attestation/wasDerivedFrom/v1` envelope (per §8.11.3 step 2; not shown in full) carries the same `ts:AnalyticalDerivation` payload on its `derivationMethod` field, referencing the source `content/analysis/v1` node by `nodeId` and the derived `content/claim/v1` node by paired identifier. ### Appendix C. Adjacent-standards comparison table Extended comparison with structural mapping notes (the short version is in §5.5). The table below covers terminological overlap, structural compatibility, alignment opportunities, and divergences. | Standard / framework | Term overlap with Typed Standards | Structural compatibility | Alignment opportunities | Notes | |---|---|---|---|---| | **C2PA** | High — "claim," "assertion," "manifest" overlap with Typed Standards "content/claim/v1," "envelope," "attestation/*" | Structurally compatible; not byte-compatible. C2PA uses COSE/CBOR + JUMBF; Typed Standards uses Ed25519ph over JCS-canonical JSON | Term-translation table (Appendix D); C2PA-compatible terminology in cross-references; consumers reading both standards see qualified "C2PA claim" / "C2PA assertion" | Different artifact class (media vs. analytical); same role for production-process attestation | | **in-toto / DSSE** | Low overlap; aligned on `predicateType` URI pattern | High — Typed Standards adopts in-toto's multihash DigestSet and predicate-type-URI pattern | DSSE as candidate envelope-serialization profile for in-toto-native consumers; multihash convention already adopted | Different consumer model (automated policy engines vs. readers exercising judgment) | | **W3C Verifiable Credentials** | High — "claim" overlap, different granularity | Compatible at the JSON-LD level; Typed Standards claim can be expressed as a VC whose subject is the analytical artifact | "VC claim" used explicitly for VC-internal claims; "claim" without qualifier means Typed Standards `content/claim/v1` | Three-party VC model (issuer / holder / verifier) does not map to two-party Typed Standards model | | **SLSA** | None — "Provenance" attestation type, "Build Type" | Adjacent; SLSA-Provenance is a specific in-toto predicate type for software builds | Typed Standards is to analytical artifacts what SLSA is to software builds — production-process attestation framework | Typed Standards does not define maturity levels; trust signals are graded identity-binding + captureMethod + transparency-log inclusion | | **Sigstore (Cosign / Fulcio / Rekor)** | None — Cosign / Fulcio / Rekor are tooling | Active dependency; Typed Standards uses Rekor for transparency-log inclusion (§8.3.2) | Continue using Rekor; consider Fulcio keyless OIDC as identity tier per [Q3](open-questions.md#q3--first-non-github-identity-provider); track Rekor v2 migration | Foundational infrastructure, not a competing standard | | **Discourse Graphs** | High — "claim," "question," "evidence" (the QEC trio) | Source pattern; Typed Standards adopts QEC nodes with attribution to Joel Chan and the Discourse Graphs community | QEC nodes as `content/*` sub-types; relations vocabulary (`supportedBy`, `opposedBy`, `answersQuestion`) | Discourse Graphs operates at the content-representation layer; Typed Standards wraps QEC content in cryptographic envelopes | | **Nanopublications** | Moderate — atomic signed claims with provenance | Compatible at the assertion-with-provenance level; carrier format differs (RDF named graphs vs. JSON-LD envelopes) | Consuming Typed Standards claims as nanopublications is a plausible bridge but requires a separate effort | Nanopubs are RDF-native; Typed Standards is envelope-first JSON-LD | | **W3C PROV-O** | Direct dependency | Direct adoption — every package's provenance graph is PROV-O JSON-LD (§8.1.4) | Continued direct usage; no replacement candidate | PROV-O is normative; this specification does not redefine derivation, attribution, or generation | | **Schema.org Claim / ClaimReview** | Low — "Claim" / "ClaimReview" target fact-check pipelines | Different problem (fact-check tagging vs. production-process attestation) | ClaimReview-style attestations CAN coexist alongside Typed Standards packages | Schema.org's Claim is human-verifier-oriented; Typed Standards' claim is machine-verifier-oriented | | **RO-Crate / WRROC** | None | Candidate package container for the multi-file end-state per [Q1](open-questions.md#q1--package-format) | RO-Crate / WRROC compatibility profile is the current direction for Typed Standards' package format | Cryptographic mechanics independent of the container choice | | **DCAT / open-data catalogs** | Low — DCAT describes datasets; Typed Standards describes analyses | Different layer; data-source references inside a package may cite DCAT-described datasets | DCAT-described data-source references in Typed Standards packages; outbound Croissant metadata per [Q8](open-questions.md#q8--croissant-outbound-metadata) | Typed Standards is not a catalog vocabulary | ### Appendix D. C2PA-to-Typed-Standards term-translation table For readers moving between C2PA and Typed Standards, the term-translation table below maps the cross-spec roles. The table is informative; cross-spec interoperability requires byte-level mapping work that is out of scope for this specification. | C2PA term | C2PA role | Typed Standards equivalent | Notes | |---|---|---|---| | **C2PA claim** | Signed bundle of assertions over a media asset | Typed Standards **envelope** (§7.4) | Both are signed, content-addressed, transparency-log-included primary units | | **C2PA assertion** | A single typed statement about the asset (capture device, edit history, copyright, etc.) | Typed Standards **content/claim/v1**, **content/evidence/v1**, or `attestation/*` sub-type (depending on the assertion's semantics) | A C2PA capture-device assertion is closer to a Typed Standards `content/evidence/v1`; a C2PA edit-history assertion is closer to a chain of `attestation/wasDerivedFrom/v1` nodes | | **C2PA manifest** | The signed package as a whole, embedded in or alongside the media asset | Typed Standards **signed node** (§7.4) | Both are the cryptographic container | | **C2PA hard binding** | Cryptographic binding of the manifest to the media asset's bytes | Typed Standards **envelope-hash + content-hash chain** (§8.2) | Same property, different mechanism; C2PA uses COSE/CBOR + JUMBF, Typed Standards uses JCS + multihash | | **C2PA soft binding** | Identifier-based binding of the manifest to the asset | Typed Standards has no exact analogue — soft binding is not used in v0.1 | Adopters needing soft-binding-like semantics should treat it as a content-profile-specific concern | | **C2PA actor / signer** | The entity who signed the C2PA manifest | Typed Standards **signer** (§8.5) + trust-registry **signerIdentity** (§8.3.3) | Same role, different mechanism | | **C2PA trust list** | The list of trusted C2PA signing identities (often a CA chain) | Typed Standards **trust registry** (§8.3.3) | Both serve the same role; Typed Standards uses a publisher-hosted well-known JSON document rather than a CA-issued chain | The mapping is provided for cross-spec readers' orientation. Implementations that need to consume both standards should treat the mapping as a starting point and verify byte-level compatibility against the C2PA Technical Specification and this document independently. ### Appendix E. Implementation status snapshot (as of 2026-05-26) This appendix is a point-in-time snapshot of which spec sections are realized in the reference implementation at `civic-ai-tools-website`. The specification's normative content is independent of this status; the snapshot supports reviewer triage. - **Built:** the envelope (signing, timestamping, public transparency log, publisher-hosted trust registry); the capture-method discipline; the `datHere` content profile and its executed-notebook architecture; the withdrawal lifecycle (DB-column form today); provenance graphs; one identity tier (GitHub OAuth). - **Specified, not built:** the unified typed-attestation primitive + two-family taxonomy (`type` URI, `nodeId`, `signer` cross-check, the v0.1 `attestation/*` sub-type table); lifecycle/location attestation envelopes (reference-impl migration is a Phase 3 item); the Typed Claims layer at §8.11 — claim shapes, confidence-method discipline, AnalyticalDerivation, the civic-data geographic-scope taxonomy — gated on a first typed-content producer. - **Reserved:** other `content/*` sub-types (host / tool sub-shapes); other Producer Profile types (Human, Hybrid, Sandbox-only); the publisher registry as indexing-only coordination surface at typedstandards.org; richer identity tiers beyond GitHub (ORCID, did:web, notarized). (Status updated 2026-06-08: offline verification, listed here as not-yet-a-property in the 2026-05-26 snapshot, is now a demonstrated property of the self-contained bundle per §9.4.) ### Appendix F. Open questions pointer This specification's open questions live in the canonical registry at [`civic-ai-tools/docs/architecture/open-questions.md`](open-questions.md), the front door of the project's working method. The registry is updated when questions surface, get promoted to GitHub issues, get resolved (with a pointer to the resolving ADR), or get deferred. Specification sections that depend on unresolved questions carry inline callouts pointing to the relevant Q-number. The most load-bearing open questions for v0.1 readers and reviewers: - [Q1](open-questions.md#q1--package-format) — Package format (multi-file directory with RO-Crate compatibility profile is the current direction). - [Q2](open-questions.md#q2--federation-substrate) — Federation substrate (atproto / KOI / nanopub network are the named candidates). - [Q3](open-questions.md#q3--first-non-github-identity-provider) — First non-GitHub identity provider (ORCID / Sigstore OIDC keyless / DNS-bound `did:web` are candidates). - [Q4](open-questions.md#q4--trace-capture) — Trace capture (OpenTelemetry SDK with GenAI semantic conventions; W3C-VC-over-MCP receipts). - [Q5](open-questions.md#q5--claimsjsonld-and-upstream-evidencejson-implementation-timing) — Typed-claim build-out timing. - [Q16](open-questions.md#q16--formal-conformance-criteria) — Formal conformance criteria (still open). [Q15](open-questions.md#q15--external-verification-testing) (external verification testing) resolved 2026-06-08 — offline verification is demonstrated for the self-contained bundle per §9.4. ### Appendix G. Revision history - **2026-09-21** — Deterministic-output profile and self-certifying signer patch revision (v0.1.9; decision records [ADR-0029](../adr/0029-scripted-recomputation-producer-profile.md) and [ADR-0030](../adr/0030-self-certifying-signer-did-key.md), both Accepted 2026-09-21; anchor [typedstandards#77](https://github.com/npstorey/typedstandards/issues/77)). **(1) The `scripted-recomputation` Producer Profile** for a record whose content a program computed from inputs pinned by hash, with no AI conversation in the path: new **§8.7.5** gives its requirements (§8.7 retitled "Profile requirements: datHere and scripted-recomputation"), §8.6 its capture vocabulary (`script-run`, `tool-emitted`), and §8.1.7 its `cost` rule (`{"model": "none"}`); the §6.2 glossary and §8.1.1-§8.1.2 rows gain the type and its vocabulary. **(2) The content canonicalization rule `raw-bytes/v1`** (§8.2, §12.3): the bytes of `output` as the file holds them, so `contentHash.sha256` is the file's ordinary SHA-256; inline for valid UTF-8, otherwise by BlobRef. §9.2 check #3's rule registry holds three rules, and check #4 gains the raw-bytes path and the status `content_bytes_unavailable`, which says the bytes were not checked and is never `ok`. **(3) A new §9.2 check #16** reads `metadata.contentProfile` and reports `ok`, `contentProfile_absent`, `contentProfile_unknown` or `contentProfile_inconsistent`; none rejects the node. Check #15 resolves the second profile type. **(4) A self-certifying signer for the `pseudonymous` rung:** new **§8.5.1** specifies the key-derived identifier — `did:key` in its base58btc (`z`) form for an Ed25519 key, derived from the envelope's `publicKey` and compared byte for byte — triggered by the identifier and never by the tier, with no rotation and no revocation, and with the domain-vouches-for-key attestation named as the upgrade path and undecided. §8.3.1 admits the self-certifying case beside the registry-match rule and states the `kid` rule under it; §8.3.3 gains the eighth trust status `self_certified` and the rules for a supplied registry (only a registry fetched from a declared `trustRegistryUrl` can raise the status; a registry carried in the bundle can lower it and never raise it); §9.2 check #5 can report `self_certified`, and check #14 gains `key_derived_match` and the fatal `key_derived_mismatch` and never reports `ok` under a key-derived identifier; §8.8.1's `trustRegistryUrl` becomes conditional; §10.2 states that a compromised self-certifying key cannot be revoked. §6.2 lists `platform`, the reference implementation's operator value already used in the §8.3.3 example, and §8.3.3 describes the synthesized `legacy_embedded` value as a placeholder, not a rung. **(5) Three corrections:** the `producerProfile` consistency invariant in §6.2 and §8.1.1 is compared only when both fields are present, as ADR-0006 §2 states it; §8.1.2 no longer says v0.1 packages emit both `metadata.contentProfile` and `producerProfile` (the signed ADR-0028 packages carry only `producerProfile`); and §6.2 no longer describes `content/analysis/v1` as the AI profile's alone. **(6) Before the tag:** §8.3.3 rule 3 and §9.4's #5 entry state that only an `https:` URL is a declared registry URL, so a registry obtained from a URL with any other scheme counts as carried in the bundle and can only lower a self-certified signer's status; this states what "declared URL" means and adds no rule beyond ADR-0030 §4. §8.3.3 and this entry name the implementing releases, `@typedstandards/verify-core` 0.10.0 and `@typedstandards/produce-core` 0.5.0. **Named under the Xanadu doctrine.** The packages blocked without the profile are the two signed ADR-0028 packages (envelopes `abb93f781ae71480bf8075474facbb272f3dcc38d50eeecda79be33427924a9c` and `3637b5952f6a44e050fa0ac84d2ab58454ab7d29507dbbaf09147da7f8b37d48`), whose `scripted-recomputation/eval-run` profile the reference verifier could not resolve before `@typedstandards/verify-core` 0.10.0; neither package changes, and neither is affected by the self-certifying mechanism. The adopter is the qsv project by datHere, through [dathere/qsv#4448](https://github.com/dathere/qsv/issues/4448), opened 2026-08-20 and labelled `revisit-later`, whose [comment of 2026-09-13](https://github.com/dathere/qsv/issues/4448#issuecomment-5649582321) reads "Revisit once TypedStandards supports something like Decentralized Identifiers" and links . The adopter's organization shares its name with this specification's `datHere` content profile (§8.7.1-§8.7.4, the A-G envelope for AI-assisted analysis); the `scripted-recomputation` profile is not that content profile. Per §6.1 the new requirements correspond to settled decision records; the reference verifier and producer core implement them in `@typedstandards/verify-core` 0.10.0 and `@typedstandards/produce-core` 0.5.0. No field is removed or retyped; `trustRegistryUrl` relaxes from required to conditional, and the `contentProfile`/`producerProfile` consistency rule applies only when both are present. The signed ADR-0028 packages are byte-identical, and the schema version is unchanged (`0.1.0`). The rendered HTML sidecar is regenerated from this revision. - **2026-09-04** — Rejected-call disclosure patch revision (v0.1.8; [civic-ai-tools#195](https://github.com/npstorey/civic-ai-tools/issues/195)). New **§8.1.8** states what a `queries[]` entry may carry about a call the source rejected, and what the absence of that record means. The two fields ship in `@typedstandards/produce-core@0.4.0` and were emitted by the reference implementation before this revision named them, so the specification is catching up to a wire shape rather than proposing one. **(1)** Two optional entry fields are documented: `failed` (boolean) and `failureKind` (string). **(2) Absence is absence** — an entry that omits `failed` records *nothing* about how the call ended, and a consumer MUST NOT read the omission as an assertion that the call succeeded. The three states (`true`, `false`, absent) are named and held distinct: every package produced before these fields existed omits both, so a rejection nobody recorded is indistinguishable there from a call that was answered, and the package asserts neither. **(3)** `failureKind` is a producer label with **no normative vocabulary** — no value set is defined or reserved, a consumer MUST NOT switch on it exhaustively or treat an unrecognized value as invalid, and the reference implementation's four values are recorded as informative rather than ratified. `failed` is the assertion and `failureKind` only a label on one. **(4)** A call recorded as `failed: true` **MUST NOT, on its own, give rise to a `dataSources[]` entry** — stated normatively for the standard rather than descriptively of one producer, on the meaning §8.1.1 already gives that field ("one entry per data source *touched by the analysis*"): without the rule, two conformant producers reading the same run would disagree about whether a signed package asserts that a source was reached, and a consumer could not tell which it was holding. The §8.1.1 `queries` and `dataSources` rows gain pointers to §8.1.8, and the `dataSources` row's emptiness condition widens to the case where every `queries[]` entry records a rejection. Per §6.1 the new requirement corresponds to a check enforced in the reference implementation today. Additive and consumer-facing: no field is removed or retyped, no requirement on an existing field changes, the new constraint is vacuous for any package in which no entry records a rejection — which is every package produced before the fields existed — and already-signed packages are byte-identical and unaffected. The rendered HTML sidecar is regenerated from this revision. - **2026-08-20** — Vocabulary-settlement close-out patch revision (v0.1.7). **(1)** Appendix J's §J.3 mapping table gains the `evpub_` publish-bearer-token prefix as an **exempt (recorded, not renamed)** row: the prefix sits inside every credential already issued, so renaming it would invalidate those credentials — the same frozen-in-live-artifacts test the live kid and the blob addresses are exempted under; future prefixes use the settlement vocabulary, and no rotation is forced. **(2)** The title block's `Canonical URL` value is quoted. Unquoted, its `TK:` marker was a second mapping indicator inside a plain scalar — the same defect class as v0.1.5's `Version` line, surviving the v0.1.6 fix because that fix addressed the one line it was found on: YAML read the value as a one-element sequence containing a nested mapping rather than as the intended string. It is now a quoted scalar, with the text unchanged. The class is no longer left to inspection: a repository check parses this block on every change and enforces the mapping shape, the `Version` pattern, and `Tag`/`Version` agreement, with the real v0.1.5 block pinned as its known-bad fixture. **(3)** The rendered HTML and PDF sidecars are regenerated from this revision, closing a render-staleness window that had been open since v0.1.2 (2026-05-26) — the sidecars had not been re-rendered across v0.1.3-v0.1.6, so the settlement revision's content reached readers only through the markdown. No normative change: no requirement, field, rule, or vocabulary term is added, removed, or altered by this revision, and already-signed packages are unaffected. - **2026-08-19** — Frontmatter YAML syntax fix (v0.1.6): the title block's Version line — which v0.1.5 had written as a single unquoted scalar containing a second `tag:` mapping, making the frontmatter unparseable as YAML — is split into two separate parseable keys, `Version` and `Tag`; the §2 status block and citation guidance update to the v0.1.6 tag. No normative change. - **2026-08-19** — Vocabulary-settlement patch revision (v0.1.5; registry [Q50](open-questions.md#q50--evidence-product-framing-vs-precise-typed-node-resource-naming)/[Q66](open-questions.md#q66--project-glossary-and-controlled-vocabulary-for-prose-across-the-four-repos), anchored at [civic-ai-tools#160](https://github.com/npstorey/civic-ai-tools/issues/160); decision record [ADR-0025](../adr/0025-vocabulary-settlement-evidence-excision.md)). "Evidence" is retired from the artifact and infrastructure surface and retained as the epistemic QEC role — the two-roles scoping principle, recorded at §6.3. §8.1 retitled "Record package structure"; the §6.2 glossary term becomes "Record package"; conformance prose swaps "evidence package" → "record package" throughout. New **Appendix J** carries the full old→new mapping table with per-surface migration classes, the exemption and retained rows, and the normative dual-era verification rules (old records keep old keys and identifiers forever; verifiers treat both eras as valid; new emissions use the new vocabulary; nothing already published stops resolving). §8.8.1's wire version key documented as the dual-era pair `protocolVersion` / `evidenceProtocolVersion` (frozen-in-signed-artifacts); §8.8.2's commitment-view notebook namespace becomes dual-era `org.civicaitools.record` / `org.civicaitools.evidence` (ruling D3); §8.8.3's sidecar filename convention renames to `.record.yaml` with the prior-era filename valid indefinitely; the §8.1.4 URN example gains the new-era `urn:civic-record:` form with the prior-era `urn:civic-evidence:` annotated valid-forever; kid examples annotated exempt-frozen; the `evidence-refs/` BlobRef storage prefix annotated exempt-frozen; the legacy trust-registry path rows annotated per ruling D2. **Header-semver rider** (requested by an external integrator embedding a version identifier): both version blocks now carry the full patch version and its tag name, and the §2 citation guidance names the patch-revision tag (`v0.1.5-typed-standards-spec`) as the precise citation target, with the milestone tag (`v0.2-typed-standards-rfc`) scoped to the consolidation milestone. Appendix A notes that heading-text anchors can change on retitle (the §8.1 case) while section numbers stay the citation unit. No wire change: already-signed packages are byte-identical and remain verifiable; schema version unchanged (`0.1.0` per [Q27](open-questions.md#q27--schema-version-bump-trigger-for-the-oes-spec)). - **2026-08-03** — Spec-reconciliation patch revision (v0.1.4): the ADR-0016 deferred amendments + served-surface reconciliation, consolidating the pre-RFC edit round's first installment. **(1) ADR-0016 execution** ([ADR-0016](../adr/0016-vcs-native-lifecycle-mapping.md), Accepted 2026-06-15): §8.1.1 gains the optional `vcsRef` field (attested content-family self-declaration; verify-on-fetch, mismatch-informative, `captureMethod`-weighted); §8.12.1 gains the `attestation/revises/v1` row + the succession-vs-correction contrast; §8.10.5 (lineage; diff = derivable view, not a signed object) and §8.10.6 (three orthogonal dimensions: visibility / lifecycle status / host display) added; §10.1 gains the false-VCS-binding adversary row (chartered via [civic-ai-tools#63](https://github.com/npstorey/civic-ai-tools/issues/63)); descriptive state labels updated to `sealed` / `public` (§8.9, §8.10.1, §8.12.1). **(2) Served-surface reconciliation (codebase-wins; zero wire change):** §8.8.1 ratifies the served commitment-view shape — the `signer` (§8.5-shaped claim, check-#14 subject) vs. `signerIdentity` (informational provider block) split, the `lifecycleAttestations` carrier, and the fields the served view had grown (`visibility`, `rekorEntryBody`, `lifecycle`, `contentHash`, `contentCanonicalization`, `producerProfile`, `type`, `trustRegistryUrlLegacy`), with required/optional marks, the sealed-record redaction rule, and the `?inline=1` field additions; §8.8.2/§8.8.3 examples updated. **(3) `metadata` placement alignment:** `contentProfile` documented at its shipped location `metadata.contentProfile` (§8.1.2; previously described as top-level — the wire never changed; the formalization collaborator's formal model documented the wire correctly). **(4) Q48 resolved:** the `attestation/locatedAt/v1` payload fingerprint field ratified as `targetContentHash` (§8.10.2, §8.12.1), matching the shipped emission and the `targetNodeId` disambiguation precedent. **(5) Appendix B correction:** the worked typed-claim example gains the core-required `ts:subject` it had omitted (surfaced by the formalization collaborator's validation pass). Schema version unchanged (`0.1.0` per [Q27](open-questions.md#q27--schema-version-bump-trigger-for-the-oes-spec)); all changes are spec-text-only or additive-optional; no package bytes change. - **2026-07-02** — §6.3 gains the `contentProfile` two-senses disambiguation note ([civic-ai-tools#100](https://github.com/npstorey/civic-ai-tools/issues/100); the ADR-0006 field split stays deferred per Q27). No normative mechanics changed. - **2026-07-01** — IPR posture adopted (ADR-0017): §3 gains the patent-posture pointer to `PATENTS.md` (maintainer royalty-free non-assertion statement; DCO inbound per `IPR.md`). No normative mechanics changed. - **2026-07-01** — Editorial pass from the chat-history extraction evaluation. §5.3 gains the "what this specification is not" boundary list; §8.1 gains the structural-primitive-vs-payload bridge note; §8.2 gains the hash-framing (collision resistance, not uniqueness) note; §8.12.3 gains default content-canonicalization guidance for `attestation/*` nodes; Appendix I expanded (Discourse Graphs / architecture-review / OKFN attribution). No normative mechanics changed. - **2026-06-08** — Offline-verifiability graduation (the offline-crypto-hardening arc, [civic-ai-tools-website#119](https://github.com/npstorey/civic-ai-tools-website/issues/119)). §9.4 rewritten: offline verifiability moves from an aspirational target to a demonstrated property of the self-contained commitment bundle (§8.8 `?inline=1`), evidenced by the Q15 offline-bundle harness verifying real production packages at full §9.2 depth with zero network; [Q15](open-questions.md#q15--external-verification-testing) resolved on that basis. §9.2 checks #7 (RFC 3161 TSA signature + X.509 cert-chain to a pinned FreeTSA RSA-4096 root) and #8 (RFC 6962 Rekor Merkle inclusion against a pinned log key + signed checkpoint) marked ENFORCED in `@typedstandards/verify-core@0.6.0`. §10.3 documents the pinned trust anchors with provenance. §9.4 / §10.2 add the offline-snapshot revocation-staleness honest-status note. Scope held precise: [Q1](open-questions.md#q1--package-format) (whether the single-blob package itself embeds its proofs) and [Q16](open-questions.md#q16--formal-conformance-criteria) (formal conformance) stay open. - **2026-05-26** — Phase 2 of the Q39 consolidation chat. File renamed from `typed-standards-proposal.md` to `typed-standards-specification.md`. Document title changed from "Typed Standards — proposal" to "Typed Standards Specification". OES §3-§16 and CCV §1-§6 + §8 absorbed into the consolidated body following the 15-section RFC-conventional structure named in the 2026-05-26 deep-research memo §6.1. `ts:` namespace prefix replaces `ccv:` throughout the absorbed typed-claims body; `https://typedstandards.org/ns/ts#` replaces `https://civicaitools.org/ns/civic-claim-vocabulary/v1#` as the vocabulary URI. Well-known trust-registry path renamed from `/.well-known/evidence-public-keys.json` to `/.well-known/typed-publisher.json`; legacy path served in parallel indefinitely (no forced cutover). Status changed from "Internal working draft (pre-v0.1)" to "v0.1 Working Draft — open for external review (review window to be scheduled)". License: CC BY 4.0. New material: §5.5 C2PA + W3C VC disambiguation paragraphs; §6.3 project name + RFC 3161 prefix-choice disambiguation paragraphs; §10 Security Considerations; §11 Privacy Considerations; §12 IANA Considerations; §13 Internationalization Considerations; Appendix A Citation conventions; Appendix D C2PA-to-Typed-Standards term-translation table; Appendix I Acknowledgments. OES + CCV historical-snapshot status notes land in Phase 3 of the consolidation chat (separate commit on the same branch). - For earlier revision history of the absorbed OES envelope spec — through 2026-05-25 G4 captureMethod-generalization cohort, 2026-05-25 G3 lifecycle/location cohort, 2026-05-25 G2 unified-primitive cohort, 2026-05-25 G1 envelope-shape cohort, 2026-05-19 datHere reframe, 2026-05-18 datHere captureMethod variant — see [`open-evidence-standard.md`](open-evidence-standard.md) §17 (historical snapshot, frozen 2026-05-26). - For earlier revision history of the absorbed Civic Claim Vocabulary draft — see [`civic-claim-vocabulary-draft-spec.md`](civic-claim-vocabulary-draft-spec.md) (historical snapshot, frozen 2026-05-26). ### Appendix H. Related documents - [`open-evidence-standard.md`](open-evidence-standard.md) — historical snapshot of the OES envelope spec (frozen 2026-05-26). Section §3-§16 content absorbed into this consolidated spec per ADR-0012; the file is preserved for historical cross-reference accuracy in pre-2026-05-26 ADRs. - [`civic-claim-vocabulary-draft-spec.md`](civic-claim-vocabulary-draft-spec.md) — historical snapshot of the Civic Claim Vocabulary draft (frozen 2026-05-26). Section §1-§6 + §8 content absorbed into §8.11 of this consolidated spec per ADR-0012. - [`typed-standards-summary.md`](typed-standards-summary.md) — one-page summary / leave-behind for external readers. - [`end-state-vision.md`](end-state-vision.md) — architectural rationale, layered standards stack, network-signal model, full glossary, and the open-questions list this spec defers to. - [`xanadu-doctrine.md`](xanadu-doctrine.md) — project discipline governing how this spec is allowed to grow; gating criteria for promoting items from reserved to specified to built. - [`working-method.md`](working-method.md) — project discipline governing how content moves between the project's six coordination surfaces. - [`open-questions.md`](open-questions.md) — living registry of unresolved decisions; the front door per the working method. - [`chat-type-taxonomy.md`](chat-type-taxonomy.md) — workspace-level discipline for which conversational surface fits which kind of work. - [`../adr/0001-roadmap-governance.md`](../adr/0001-roadmap-governance.md) — public-roadmap governance and quarterly cadence. - [`../adr/0002-commitments-vs-targets.md`](../adr/0002-commitments-vs-targets.md) — distinction between absolute commitments and operational targets in ROADMAP.md §3. - [`../adr/0003-evidence-capture-method.md`](../adr/0003-evidence-capture-method.md) — the `captureMethod` field, vocabulary, and tamper-evident labeling. Authoritative for §8.6 (field shape). - [`../adr/0004-dathere-captureMethod-variant.md`](../adr/0004-dathere-captureMethod-variant.md) — the `datHere` content profile (originally framed as a captureMethod variant; reframed 2026-05-19), the A-G envelope shape, the notebook-format requirement, and the cross-host commitment-view publication schema. Authoritative for §8.7, §8.8, §8.9. - [`../adr/0005-executed-notebook-architecture.md`](../adr/0005-executed-notebook-architecture.md) — executed-notebook architecture per §8.7.4. - [`../adr/0006-producer-profile-architecture.md`](../adr/0006-producer-profile-architecture.md) — Producer Profile axis + per-subtype guidance bundles. - [`../adr/0007-content-canonicalization.md`](../adr/0007-content-canonicalization.md) — `contentCanonicalization` URI; the content-level half of §8.2. - [`../adr/0008-multihash-content-hash.md`](../adr/0008-multihash-content-hash.md) — multihash `contentHash` + RFC 8785 JCS envelope canonicalization + signature-chain. Authoritative for §8.2, §8.3.1. - [`../adr/0009-unified-typed-attestation-primitive.md`](../adr/0009-unified-typed-attestation-primitive.md) — unified typed-attestation primitive + two-family taxonomy. Authoritative for §7.4, §8.12. - [`../adr/0010-visibility-lifecycle-location-attestations.md`](../adr/0010-visibility-lifecycle-location-attestations.md) — lifecycle + location attestations. Authoritative for §8.10. - [`../adr/0011-capturemethod-generalization.md`](../adr/0011-capturemethod-generalization.md) — captureMethod value-space generalization + per-profile vocabulary. Authoritative for §8.6 (value-space half). - [`../adr/0012-typed-standards-consolidation.md`](../adr/0012-typed-standards-consolidation.md) — the consolidation that produced this document. - [`../docs/research/landscape-analysis.md`](../research/landscape-analysis.md) — relationship to existing standards. - `civic-ai-tools-website/docs/api/evidence-publish.md` (renaming to `records-publish.md` under the vocabulary settlement, with the old filename remaining as a stub — Appendix J) — request/response contract for the canonical reference implementation. This document and the API doc MUST stay aligned; where they diverge, this document is normative for the package shape and the API doc is normative for the request/response contract. - `civic-ai-tools-website/docs/key-rotation.md` — runbook for rotating the reference implementation's signing key. ### Appendix I. Acknowledgments This specification was developed in collaboration with **datHere** (Pittsburgh / WPRDC pilot integration partner; the captureMethod profile in §8.7 takes its name from their reference implementation, embedded with their consent), whose technical input shaped the notebook-format commitment-view schema (§8.8) and the captureMethod content-profile design (§8.6). The QEC content-type pattern (claim / question / evidence) and the `supportedBy` / `opposedBy` / `answersQuestion` relations are adopted from **Joel Chan's Discourse Graphs work** and the Discourse Graphs community, with attribution carried inline at §5.5 and §7.5. The layered artifact-layer / network-layer framing and several structural notes on the §7.1 stack were sharpened by an architecture-review conversation with **Michael Zargham** (Dynamical Systems Group, formerly BlockScience; see the Q41–Q45 origins in [`open-questions.md`](open-questions.md)). The influence is architectural; no endorsement of this specification by the reviewer or the Dynamical Systems Group is implied. The **Open Knowledge Foundation**'s open-data groundwork (Frictionless Data / the Data Package standard) informs the packaging and portability direction tracked under Q1 and Q18. Additional adopters and contributors will be acknowledged as their participation moves from private to publicly-named status. ### Appendix J. Vocabulary settlement (Q50/Q66): old→new mapping and dual-era rules Settled 2026-08-19 by the maintainer, resolving registry [Q50](open-questions.md#q50--evidence-product-framing-vs-precise-typed-node-resource-naming) and recording a settled term under [Q66](open-questions.md#q66--project-glossary-and-controlled-vocabulary-for-prose-across-the-four-repos). Decision record: [ADR-0025](../adr/0025-vocabulary-settlement-evidence-excision.md); execution anchor: [civic-ai-tools#160](https://github.com/npstorey/civic-ai-tools/issues/160). This appendix is the canonical old→new mapping every later migration phase cites. #### J.1 Scoping principle (Group F): two roles, one retained "Evidence" plays two unrelated roles (§6.3). As the **artifact / infrastructure brand** — the package noun, route segments, environment-variable prefix, wire version key, exported type and function names, OAuth scope, skill name, sidecar filename — it **overclaims**: the record shows how an answer was produced, not that the answer is correct. That role is excised. As the **epistemic role** in the Question / Evidence / Claim triad — `content/evidence/v1`, the `contentType` value `"evidence"`, the `supportedBy` / `opposedBy` relations whose targets are evidence nodes — it is precise and **retained**: content serving as evidence-for-a-claim is the legitimate use. #### J.2 Migration classes | Class | Meaning | |---|---| | **alias-permanent** | The new name is canonical; the old name is served as a **permanent** alias (published links exist in the wild — this is not a deprecation window). | | **expand-then-flip** | Readers accept the new name first, then the old, with a deprecation warning on old-name reads; documentation, examples, preflight, and deployment surfaces flip to the new names; old names drop at a later major version of the owning surface. | | **frozen-in-signed-artifacts (dual-era)** | The identifier is embedded in already-signed artifacts and therefore never changes there. Old records keep the old identifier forever; verifiers treat both eras as valid; post-cutover emissions use the new identifier. | | **alias-and-deprecate** | The new name is exported/served beside the deprecated old name; both work; the old name drops at the owning package's next major version. | | **dual-era, accepted forever** | Like frozen-in-signed-artifacts, but with no drop horizon on the acceptance side even for new artifacts: new emissions mint the new key; the old key is accepted indefinitely (ruling D3). | | **exempt (recorded, not renamed)** | Deliberately out of the rename: internal names that never cross the wire, or values hash-frozen inside signed fields. The exemption is a ruling, not an omission. | | **retained** | Not part of the rename at all — the epistemic QEC vocabulary. | #### J.3 Old→new mapping table | Surface | Prior-era name | Settlement name | Migration class | Notes | |---|---|---|---|---| | API routes (reference implementation) | `/api/evidence/*` | `/api/records/*` | alias-permanent | New canonical segments; old segments served forever. Publish responses emit new-form URLs. The neutral verifier's URL construction learns new-then-old resolution in the same settlement sprint. | | Public pages (reference implementation) | `/evidence/*` | `/records/*` | alias-permanent | Same rule as the API routes. | | Environment variables (reference implementation; 14 variables) | `EVIDENCE_*` | `PUBLISHER_*` | expand-then-flip | The full set: `EVIDENCE_SIGNING_KEY`, `EVIDENCE_KEY_ID`, `EVIDENCE_PUBLIC_KEY`, `EVIDENCE_SIGNER_BINDING_TIER`, `EVIDENCE_SIGNER_IDENTIFIER`, `EVIDENCE_SIGNER_DISPLAY_NAME`, `EVIDENCE_PLATFORM_AGENT_ID`, `EVIDENCE_PLATFORM_AGENT_TITLE`, `EVIDENCE_PLATFORM_AGENT_URL`, `EVIDENCE_PUBLICATION_HOST`, `EVIDENCE_SITE_ORIGIN`, `EVIDENCE_TRUST_REGISTRY_URL`, `EVIDENCE_TRUST_REGISTRY_CANONICAL_URL`, `EVIDENCE_TRUST_REGISTRY_LEGACY_URL` — each takes the `PUBLISHER_` prefix (e.g. `EVIDENCE_PUBLIC_KEY` → `PUBLISHER_PUBLIC_KEY`). `EVIDENCE_PUBLIC_KEY` is written-not-read (emitted by the key-generation script and documented; never read at runtime). Rider: the successor of `EVIDENCE_TRUST_REGISTRY_URL` (the verify-side consume override) gains a documentation home in the same sweep — the prior-era name was documented in neither deployment guide. | | Commitment-view wire key (§8.8.1) | `evidenceProtocolVersion` | `protocolVersion` | frozen-in-signed-artifacts | Old records keep the old key forever; verifiers MUST accept both keys for both eras; post-cutover emissions use the new key. Cutover coordinated with the one live external adopter, which serves this field. | | URN scheme (§8.1.4) | `urn:civic-evidence:` | `urn:civic-record:` | frozen-in-signed-artifacts | Existing provenance graphs keep their identifiers; verifiers treat both eras as valid; golden fixtures re-freeze in the affected repos. The `typedstandards.org/ns/ts#` alignment question stays with the [Q10](open-questions.md#q10--civic-claim-vocabulary-as-a-full-ontology) ontology adoption round — explicitly not pre-judged here. | | Vocabulary URI (civic harness emitter) | `https://civicaitools.org/ns/evidence/` | `https://civicaitools.org/ns/civic/` | frozen-in-signed-artifacts | Same rules as the URN scheme row; single emitter (the civic harness vocabulary module). | | Exported type (produce-core; mirrored in the harness signature) | `EvidencePackage` | `RecordPackage` | alias-and-deprecate | New name exported beside the deprecated old one; old drops at the package's next major. | | Exported function (verify-core) | `verifyEvidence` | `verifyRecord` | alias-and-deprecate | Same rule. | | OAuth scope (reference implementation) | `evidence:publish` | `records:publish` | alias-and-deprecate | Token endpoints accept both scopes and mint the new one. | | Claude Code skill | `publish-evidence` | `publish-record` | alias-and-deprecate | Old invocation aliased; the skill's User-Agent literal and client display name move with the rename. | | Commitment-view sidecar filename (§8.8.3) | `.evidence.yaml` | `.record.yaml` | alias-and-deprecate | Existing sidecars keep the old filename valid; verifiers accept either; no emitting code existed at any main at settlement time. | | Reference-implementation API docs | `evidence-publish.md`, `evidence-commitment.md` | `records-publish.md`, `records-commitment.md` | alias-and-deprecate | Old-name stubs remain at the old paths. | | Extension namespace (§8.1.6, §8.8.2) | `org.civicaitools.evidence` | `org.civicaitools.record` | dual-era, accepted forever | Ruling D3 (deliberate, against the default recommendation): new emissions mint the new key; the old key is accepted forever; verifiers read either, preferring the new when both are present. `org.civicaitools.notebook` / `.environment` / `.execution` / `.summary` are unaffected (no excised word). | | Database names (reference implementation) | `evidence_records` table + its constraints and indexes | — | exempt (recorded, not renamed) | DB names never cross the wire. | | Blob storage prefixes | `evidence-packages/`, `evidence-refs/`, `evidence-packages/committed/` | — | exempt (recorded, not renamed) | Blob addresses are hash-frozen inside signed `packageUrl` and BlobRef fields; `committed/` is a declared frozen storage literal. | | Live signing-key kid | `platform:evidence-2026-04` | — | exempt (recorded, not renamed) | Inside every signed envelope. The next key rotation names its key under the new vocabulary; no forced rotation. | | Publish bearer-token prefix (reference implementation) | `evpub_` | — | exempt (recorded, not renamed) | Added 2026-08-20 (v0.1.7), on the same frozen-in-live-artifacts test as the kid and the blob addresses: the prefix is inside every credential already issued to an integrator, and renaming it would invalidate those credentials. A token prefix minted in future — a second token family, or a re-issuing of this one — uses the settlement vocabulary; no rotation is forced, and the prior-era prefix stays accepted for as long as the tokens carrying it are. | | OTel scope names inside already-signed traces | (as signed) | — | exempt (recorded, not renamed) | New emissions MAY change. | | Legacy trust-registry path (§8.3.3, §12.1) | `/.well-known/evidence-public-keys.json` | — | exempt (recorded, not renamed) | Ruling D2: the exempt-frozen leg of the **already-completed** trust-registry rename (`→ /.well-known/typed-publisher.json`, [ADR-0012](../adr/0012-typed-standards-consolidation.md)); not renamed again. | | Dated talk deck (2026-07) | (as published) | — | exempt (frozen dated record) | Ruling D4. | | Epistemic QEC vocabulary | `content/evidence/v1`; `contentType: "evidence"`; `supportedBy` / `opposedBy` evidence targets | (unchanged) | retained | The Group F ruling (§J.1): the epistemic role is the legitimate use and is untouched. | #### J.4 Dual-era verification rules (normative) 1. **Old records keep their old keys and identifiers forever.** An identifier frozen inside an already-signed artifact (wire keys, URN identifiers, vocabulary URIs, kids, storage URLs, extension-namespace keys) is never rewritten: rewriting would change the envelope hash and invalidate the signature. Prior-era artifacts remain byte-identical and remain verifiable exactly as published. 2. **Verifiers treat both eras as valid.** A conformant verifier MUST accept the prior-era and settlement-era forms of every dual-era surface in the §J.3 table (wire key, extension namespace, sidecar filename, URN scheme, vocabulary URI) without warning-as-error semantics; era is not a trust signal. 3. **New emissions use the new vocabulary.** Post-cutover emissions mint the settlement-era names. Cutovers on externally-served surfaces (routes, the wire key) are coordinated with the one live external adopter; until a publisher's cutover lands, its prior-era emissions remain conformant. 4. **Expand before any flip; nothing stops resolving.** Every accept-both / alias mechanism lands and is verified before any default or emission changes. No migration phase may create a state where an existing verifier, link, token, or record stops resolving.