Cite2Site v1.0 Stable Contract
Status: v1.0 stabilization authority. Defines every stable interface, every experimental/deferred surface, and the compatibility rules for future releases.
This document is a required Phase 7 deliverable. It is referenced by the v1 gate report, the compatibility test corpus, and the user and agent guides.
1. Stable Schema Identifiers
| Identifier | Meaning | Frozen |
|---|---|---|
c2s.event.v0.3 |
Event envelope and citation-created/handle-bound/workflow event schema_version | Yes |
c2s.project.v0.3 |
Project file schema_version | Yes |
c2s.citations.v0.3 |
c2s citations response schema_version |
Yes |
c2s.status.v0.3 |
c2s status response schema_version |
Yes |
c2s.lookup-actions.v0.3 |
c2s lookup-actions response schema_version (defined in schema; not yet emitted at runtime) |
Yes |
c2s.artifact-index.v0.3 |
Derived artifact-index cache schema_version (internal) | Yes |
Note: the v0.3 in the schema identifier is the protocol version, not the Cite2Site release version. v1.0.0 of Cite2Site implements protocol v0.3. Future protocol versions must maintain a migration path from v0.3.
2. Stable CLI Commands
| Command | Stability | Notes |
|---|---|---|
c2s init |
Stable | --force flag |
c2s cite-selection |
Stable | --artifact, --start, --end, --adapter, --expected-content-hash, --handle, --label, --tag, --note, --handle-from-first-line |
c2s cite-batch |
Stable | --request (JSON file path) |
c2s set-handle |
Stable | --citation-id, --handle, --action (bind/rename/alias/retire), --previous-handle |
c2s preflight-selection |
Stable | Same flags as cite-selection; read-only |
c2s accept-current |
Stable | --citation-id, --expected-content-hash |
c2s retract |
Stable | --citation-id |
c2s restore |
Stable | --citation-id |
c2s relocate |
Stable | --citation-id, --artifact, --start, --end, --adapter, --expected-content-hash |
c2s note |
Stable | --citation-id, --note |
c2s lookup-actions |
Stable | --artifact, --start, --end, --privacy |
c2s status |
Stable | --privacy |
c2s citations |
Stable | --artifact, --handle, --tag, --status, --batch, --privacy, --format |
c2s export |
Stable | --privacy |
c2s check |
Stable | No flags |
All commands above accept the global --repo REPO flag.
3. Stable Event Fields
Every event in .c2s/citation-history.jsonl carries:
| Field | Type | Required | Frozen |
|---|---|---|---|
schema_version |
const "c2s.event.v0.3" |
Yes | Yes |
event_type |
string | Yes | Yes |
event_id |
sha256 hex | Yes | Yes |
previous_event_hash |
sha256 hex | Yes | Yes |
repository_id |
sha256 hex | Yes | Yes |
batch_id |
sha256 hex or null | No | Yes |
idempotency_key |
string | No | Yes |
created_at |
ISO 8601 string | Yes | Yes |
actor.kind |
user/agent/machine | Yes | Yes |
actor.id |
string | No | Yes |
tool.name |
string | Yes | Yes |
tool.version |
string | Yes | Yes |
citation.created event (additional fields)
| Field | Type | Required | Frozen |
|---|---|---|---|
citation_id |
sha256 hex | Yes | Yes |
artifact.adapter |
filesystem-text/markdown | Yes | Yes |
artifact.uri |
string | Yes | Yes |
artifact.artifact_id |
sha256 hex | Yes | Yes |
locator.kind |
text-range | Yes | Yes |
locator.encoding |
unicode-scalar | Yes | Yes |
locator.start |
integer >= 0 | Yes | Yes |
locator.end |
integer >= 0 | Yes | Yes |
locator.start_line |
integer >= 1 | Yes | Yes |
locator.end_line |
integer >= 1 | Yes | Yes |
accepted_evidence.kind |
text | Yes | Yes |
accepted_evidence.canonicalization |
text-utf8-lf-v1 | Yes | Yes |
accepted_evidence.content_hash |
sha256 hex | Yes | Yes |
accepted_evidence.line_hashes |
array of sha256 hex | Yes | Yes |
accepted_evidence.byte_count |
integer | Yes | Yes |
accepted_evidence.line_count |
integer | Yes | Yes |
accepted_evidence.text |
string | No | Retained in authority; stripped by privacy |
metadata.label |
string or null | No | Yes |
metadata.tags |
array of strings | Yes | Yes |
metadata.note |
string or null | No | Yes |
handle.bound event (additional fields)
| Field | Type | Required | Frozen |
|---|---|---|---|
citation_id |
sha256 hex | Yes | Yes |
handle |
pattern ^[A-Za-z0-9_.:/-]{1,128}$ |
Yes | Yes |
action |
bind/rename/alias/retire | Yes | Yes |
previous_handle |
string or null | No | Yes |
policy.preserve_previous_as_alias |
boolean | Yes | Yes |
Workflow events (citation.accepted, retracted, restored, relocated, noted)
| Field | Type | Required | Frozen |
|---|---|---|---|
citation_id |
sha256 hex | Yes | Yes |
accepted_evidence |
object | Accept + relocate only | Yes |
artifact |
object | Relocate only | Yes |
locator |
object | Relocate only | Yes |
observed_content_hash |
sha256 hex | Accept only | No |
note |
string | Noted only | Yes |
4. Stable Projection Fields
Status report
| Field | Type | Frozen |
|---|---|---|
ok |
bool | Yes |
schema_version |
"c2s.status.v0.3" | Yes |
repository_id |
sha256 hex | Yes |
privacy_mode |
enum | Yes |
citations[] |
array | Yes |
indexes.by_artifact |
grouped index | Yes |
indexes.by_handle |
grouped index | Yes |
indexes.by_tag |
grouped index | Yes |
indexes.by_status |
grouped index | Yes |
indexes.by_batch |
grouped index | Yes |
summary |
object | Yes |
Citations query response
| Field | Type | Frozen |
|---|---|---|
ok |
bool (always true) | Yes |
schema_version |
"c2s.citations.v0.3" | Yes |
repository_id |
sha256 hex | Yes |
privacy_mode |
enum | Yes |
filters |
object | Yes |
citations[] |
array of citation projections | Yes |
Lookup-actions response
| Field | Type | Frozen |
|---|---|---|
ok |
bool (always true) | Yes |
requires_picker |
bool | Yes |
actions |
array of strings | Yes |
matches[] |
array | Yes |
matches[].citation_id |
sha256 hex | Yes |
matches[].preferred_handle |
string or null | Yes |
matches[].match_kind |
exact/contains/contained_by/overlaps | Yes |
matches[].range_summary |
string | Yes |
matches[].status |
enum | Yes |
matches[].actions |
array of strings | Yes |
Export indexes
| Index File | Frozen |
|---|---|
exports/index-by-artifact.json |
Yes |
exports/index-by-handle.json |
Yes |
exports/index-by-tag.json |
Yes |
exports/index-by-status.json |
Yes |
exports/index-by-batch.json |
Yes |
MkDocs site URL shape
| Path Pattern | Frozen |
|---|---|
docs/index.md |
Yes |
docs/citations.md |
Yes |
docs/artifacts/<key>.md |
Yes |
docs/handles/<key>.md |
Yes |
docs/tags/<key>.md |
Yes |
docs/status/<key>.md |
Yes |
docs/batches/<key>.md |
Yes |
Key format: url_safe_slug — lowercase alphanumeric + hyphens, no leading/trailing hyphens.
5. Stable Error Codes
| Code | Category | Frozen |
|---|---|---|
E_FILE_NOT_FOUND |
Input | Yes |
E_JSON_INVALID |
Input | Yes |
E_JSONL_INVALID |
Input | Yes |
E_ARTIFACT_MISSING |
Citation | Yes |
E_ARTIFACT_OUTSIDE_WORKSPACE |
Citation | Yes |
E_ARTIFACT_TEXT_DECODE |
Citation | Yes |
E_SELECTION_EMPTY |
Citation | Yes |
E_RANGE_INVALID |
Citation | Yes |
E_CONTENT_HASH_MISMATCH |
Citation | Yes |
E_FIRST_LINE_HANDLE |
Citation | Yes |
E_CITATION_NOT_FOUND |
Workflow | Yes |
E_CITATION_RETRACTED |
Workflow | Yes |
E_HANDLE_INVALID |
Handle | Yes |
E_HANDLE_COLLISION |
Handle | Yes |
E_HANDLE_ACTION_INVALID |
Handle | Yes |
E_HANDLE_MODE_CONFLICT |
Handle | Yes |
E_BATCH_EMPTY |
Batch | Yes |
E_BATCH_ITEM_INVALID |
Batch | Yes |
E_BATCH_MODE |
Batch | Yes |
E_NOTE_EMPTY |
Workflow | Yes |
E_PRIVACY_MODE |
Privacy | Yes |
E_PRIVACY_POLICY |
Privacy | Yes |
E_REPO_NOT_INITIALIZED |
Repository | Yes |
E_REPO_EXISTS |
Repository | Yes |
E_REPO_LOCKED |
Repository | Yes |
E_SCHEMA_UNKNOWN |
Schema | Yes |
E_SCHEMA_UNSUPPORTED |
Schema | Yes |
E_EVENT_CHAIN |
Chain | Yes |
E_EVENT_HASH |
Chain | Yes |
E_EVENT_ID_MISSING |
Chain | Yes |
E_PROJECTION_WRITE |
Export | Yes |
E_ARTIFACT_INDEX_CACHE |
Internal | Yes |
E_ADAPTER_UNSUPPORTED |
Adapter | Yes |
E_ADAPTER_UTF8 |
Adapter | Yes |
E_ADAPTER_MISSING |
Adapter | Yes |
E_ADAPTER_EMPTY_SELECTION |
Adapter | Yes |
E_ADAPTER_RANGE_INVALID |
Adapter | Yes |
E_ADAPTER_AMBIGUOUS |
Adapter | Yes |
38 stable C2SError codes. No error code may be removed or have its meaning changed without a protocol version bump.
6. Stable Privacy Modes
| Mode | Behavior | Frozen |
|---|---|---|
metadata_only |
Default. Strips accepted_evidence.text from all projections. |
Yes |
hash_only |
Strips accepted_evidence.text and accepted_evidence.line_hashes. |
Yes |
snippet |
Includes accepted_evidence.text only when allow_snippet: true is in project publication policy. |
Yes |
private_link |
Includes private-link URLs only when allow_private_link: true and private_link_base is an HTTPS URL in project publication policy. |
Yes |
7. Stable Canonicalization
| Canonicalization | Identifier | Frozen |
|---|---|---|
| Text line-ending normalization | text-utf8-lf-v1 |
Yes |
| Content hash | sha256 of canonicalized text | Yes |
| Line hashes | sha256 of each canonicalized line | Yes |
| Artifact identity | sha256 of {adapter, uri, artifact_id} dict |
Yes |
| Citation ID | sha256 of {artifact, locator, accepted_evidence} dict |
Yes |
| Event ID | sha256 of full event dict excluding event_id | Yes |
8. Stable Status Transitions
| Current Status | Trigger | Result |
|---|---|---|
| (none) | citation.created | resolved |
| resolved | citation.accepted | resolved (idempotent) |
| resolved | citation.retracted | retracted |
| retracted | citation.restored | resolved |
| resolved | citation.relocated | resolved (new evidence) |
| resolved | citation.noted | resolved (annotation added) |
| changed | citation.accepted | resolved (evidence accepted) |
| missing | citation.relocated | resolved (new location) |
| any | replay observation | resolved/changed/missing/unsupported/adapter_unavailable/retracted |
9. Experimental / Deferred Surfaces
| Surface | Status | Isolation |
|---|---|---|
| Overlap ordering by most-recent binding | Partial | Present ordering (handle presence) is documented; binding-order sort is a future enhancement |
| Block-aware Markdown adapter | Deferred | MarkdownAdapter currently uses text semantics; block-level locators not defined |
| PDF adapter | Deferred | No adapter exists |
| DOCX adapter | Deferred | No adapter exists |
| Browser extension integration | Deferred | Integration contract exists; no package |
| Editor plugin integration | Deferred | Editor mock example exists; no package |
| Runtime schema validation | Deferred | Migration fixture tests cover schema validation |
undo/redo command aliases |
Deferred | Recovery commands (retract/restore) provide the authoritative recovery model |
Citation-level retire |
Deferred | Handle retire exists; citation retire deferred |
| Signed/encrypted export transport | Deferred | Export is local filesystem only |
| User-defined grouping dimensions | Deferred | Five standard dimensions provided |
| Field-level projection profiles | Deferred | Privacy modes cover the available levels |
9a. Adapter Tiers
Adapters implement up to 8 contract methods. These split into two tiers with different implementation complexity and deployment risk:
Tier 1 — Creation (identify, canonicalize, evidence, locate)
Required for cite-selection and cite-batch. The adapter must independently
verify the caller's claimed selection against the actual artifact (the
mechanism behind E_CONTENT_HASH_MISMATCH).
Tier 1 adapters unblock creation. They are smaller, self-contained builds and can ship independently of Tier 2.
Tier 2 — Replay/Relocation (observe, compare, summarize, privacy)
Required for status, lookup-actions, citations, and export to report
current state. The adapter must re-read the artifact later and reconcile drift.
Tier 2 adapters can be deferred without blocking creation. A citation
created with a Tier-1-only adapter reports adapter_unavailable until the
Tier 2 methods are implemented.
Implementation Strategy: One ConverterAdapter, Not Three Per-Format Adapters
For local-file formats (DOCX, PDF, XLSX), Tier 1 and most of Tier 2 do not require format-specific code. Instead:
canonicalize()shells out to a registered external converter for the file extension (e.g.,mammoth/pandocfor DOCX,pdftotextfor PDF, a CSV-export path for XLSX) and treats its stdout as canonical text.evidence()/locate()/compare()reuse the exact same text-search reconciliation code already built and tested for the filesystem-text adapter — no format-specific parsing logic needed.- One generic
ConverterAdapter, configured with a{file extension → converter command}registry, replaces three separate per-format adapters.
Converter version pinning (required): The hash chain is only meaningful if
canonicalize() produces byte-identical output for byte-identical input on
every machine, every time. External converters are not guaranteed to do this
across versions. Therefore identify() MUST record the converter name and
version used per citation, so a converter upgrade that silently changes output
is detectable and attributable rather than a silent replay failure.
Determinism testing (required): Converter output must be tested for byte-identical determinism across two runs on the same fixture file. This test must run in CI — converter behavior can differ across OS and container base images. A version-pin test must also assert that a simulated version mismatch is surfaced, not silently ignored.
Browser Adapter: Creation vs. Re-Observation
Browser pages have no file to point a converter at. The strategy splits by lifecycle phase:
Creation (Tier 1, client-supplied text): The browser extension already has
the live DOM and the exact selected text at selection time. A thin browser
adapter accepts the client-supplied text and content hash directly —
canonicalize/evidence structure and hash what was handed to them without
independently re-fetching the live page. The selection MUST NOT be run through
an extractor (like Readability) at creation time, since extractors designed to
strip boilerplate could clip content that was genuinely part of the user's
selection (a sidebar quote, a table cell, a comment).
Re-observation (Tier 2, Readability-based): A background process fetches
the URL, runs @mozilla/readability server-side via jsdom (no live browser
session required) to strip navigation/ads/boilerplate, and produces
deterministic canonical text — the same role a converter plays for DOCX/PDF/
XLSX. The accepted evidence is searched for in the new extraction, reusing
the same resolved / changed / missing reconciliation logic.
Honesty requirement (critical): A plain HTTP fetch gets server-rendered
HTML, not JS-rendered SPA content, and nothing behind a login wall or paywall.
A Readability re-fetch that confidently compares against content the user
never actually saw is worse than an honest "can't verify" — it produces a
false resolved or missing. Therefore pages requiring JS rendering,
authentication, or likely to be personalized (heuristics: near-empty <body>,
login/paywall redirects, known SPA framework markers) MUST report
adapter_unavailable, not a comparison against unrelated content.
The Readability library version must be recorded per observation (same
discipline as converter version pinning). Full JS-rendering/headless support
(Playwright-class, browser binary in CI) is a separate, heavier build —
adapter_unavailable is the correct answer for that subset until it is built.
Adapter Development Priority (Revised)
- ConverterAdapter (DOCX/PDF/XLSX) — one adapter, three configurations, reusing existing evidence/locate/compare code. Highest-leverage: unblocks three journeys for the cost of one, plus determinism/version-pin tests.
- Browser Tier 1 (creation only) — client-supplied text, no converter. Small, independent build.
- Browser Tier 2 (Readability re-observation) — Readability-based fetch
for static/server-rendered pages, reusing same reconciliation logic as (1).
Honesty test (JS/auth pages →
adapter_unavailable) built FIRST. - Headless browser support — JS-rendered/auth pages. Separate build; requires browser binary in CI. Deferred.
Each new adapter must pass the relevant tier subset of the adapter conformance
harness (tests/test_adapter_conformance.py) before creation is accepted.
Isolation rule: experimental/deferred surfaces must not be referenced as available in status rows, guides, or release notes without explicit qualification.
10. Compatible Event Types
| event_type | Status |
|---|---|
citation.created |
Stable |
handle.bound |
Stable |
citation.accepted |
Stable |
citation.retracted |
Stable |
citation.restored |
Stable |
citation.relocated |
Stable |
citation.noted |
Stable |
7 stable event types.
11. Semantic Versioning Policy
Cite2Site uses semantic versioning. Given MAJOR.MINOR.PATCH:
- MAJOR — bumped when any frozen identifier, error code, event field, projection field, command flag, or URL shape is removed or changes meaning incompatibly. A major bump must include a migration path.
- MINOR — bumped when a new stable interface is added (new command, new event type, new flag, new error code) without breaking existing ones.
- PATCH — bumped for bug fixes, documentation corrections, and non-breaking internal changes that do not alter any stable interface.
Deprecation policy: a stable interface may be deprecated in a MINOR release with a documented replacement and must remain functional through the next MAJOR release.
12. Compatibility Test Corpus
See build-docs/internal/COMPATIBILITY_CORPUS.md for the concrete fixture
set and test matrix. The corpus must cover:
- Every event type against frozen fixtures.
- Every error code with a trigger fixture.
- Every privacy mode with deterministic output fixtures.
- Cross-version schema migration (v0.3 → v0.3 idempotent; unknown version refusal).
- Deterministic export comparison (two runs, same fixture, same output).
- Source-clean verification (artifact unchanged after citation workflows).
- Hash-chain integrity (tampered event rejection).