Section: Core Specification Version: 0.1
Codex documents have explicit lifecycle states that govern their mutability and signature requirements. This state machine addresses a fundamental limitation of PDF and other formats: the lack of clear semantics around document finalization.
When a document is signed, it should be clear:
- What content is covered by the signature
- Whether the document can still be modified
- What modifications (if any) are permitted
The document state is a contract between author and reader:
| State | Author's Intent | Reader's Expectation |
|---|---|---|
| draft | "Work in progress" | "Content may change" |
| review | "Ready for feedback" | "Seeking input" |
| frozen | "This is final" | "Content is fixed" |
| published | "This is authoritative" | "Official version" |
┌─────────────────────────────────────────────────────────────────┐
│ STATE MACHINE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ review ┌─────────┐ sign ┌─────────┐ │
│ │ DRAFT │ ───────────▶ │ REVIEW │ ────────▶ │ FROZEN │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ │ │ │ │
│ │ fork │ fork │ │
│ ◀─────────────────────────┴──────────────────────┘ │
│ │
│ publish │
│ ┌─────────┐ │ ┌───────────┐ │
│ │ FROZEN │ ───┴──▶ │ PUBLISHED │ │
│ └─────────┘ └───────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
The initial state for new documents.
Characteristics:
- Fully editable
- No signature required
- Document ID may be "pending"
- Content hash may be outdated
Permitted Operations:
- Edit content
- Add/remove assets
- Modify presentation
- Modify metadata
- Add/edit/remove phantom clusters
- Transition to REVIEW
Documents ready for feedback and approval.
Characteristics:
- Content editable (with tracking)
- Document ID computed
- Comments/annotations encouraged
- Signature optional
Permitted Operations:
- Edit content (changes tracked)
- Add comments/annotations
- Add/edit/remove phantom clusters
- Transition to FROZEN (with signature)
- Transition back to DRAFT (if unsigned)
- Fork to new DRAFT
Documents that have been signed and locked.
Characteristics:
- Content immutable
- At least one valid signature required
- Document ID is final
- Hash verified on load
Permitted Operations:
- Add annotation layer (separate from content)
- Add additional signatures
- Add/edit/remove phantom clusters (outside hashing boundary)
- Transition to PUBLISHED
- Fork to new DRAFT
Prohibited Operations:
- Edit content
- Modify presentation layer
- Change metadata (except security metadata)
Documents officially released for distribution.
Characteristics:
- All FROZEN characteristics
- Indicates official/authoritative status
- May have distribution metadata
Permitted Operations:
- Same as FROZEN (including phantom cluster operations)
- Fork to new DRAFT
| From | To | Trigger | Requirements |
|---|---|---|---|
| DRAFT | REVIEW | submitForReview() |
None |
| REVIEW | DRAFT | revertToDraft() |
No signatures |
| REVIEW | FROZEN | sign() |
Valid signature |
| FROZEN | PUBLISHED | publish() |
None |
| Any | DRAFT (new) | fork() |
Creates new document |
{
"state": "draft",
"id": "pending"
}Becomes:
{
"state": "review",
"id": "sha256:computed..."
}Actions:
- Compute document ID (hash canonical content)
- Update state to "review"
- Update modified timestamp
{
"state": "review",
"id": "sha256:abc123...",
"security": null
}Becomes:
{
"state": "frozen",
"id": "sha256:abc123...",
"security": {
"signatures": "security/signatures.json"
}
}Actions:
- Verify document ID matches current content
- Create signature over document ID
- Store signature in security layer
- Update state to "frozen"
Forking creates a new document derived from the current one:
// Original (frozen)
{
"state": "frozen",
"id": "sha256:original..."
}Produces new document:
{
"state": "draft",
"id": "pending",
"lineage": {
"parent": "sha256:original...",
"version": 2,
"note": "Forked for revisions"
}
}The fork operation:
- Copies content, assets, and metadata
- Removes security layer
- Sets state to "draft"
- Records lineage to parent
Implementations MUST enforce state semantics when reading:
| State | Enforcement |
|---|---|
| draft | Allow editing UI |
| review | Allow editing, show tracking |
| frozen | Read-only content, verify signatures |
| published | Read-only content, verify signatures |
Implementations MUST enforce when saving:
| State | Enforcement |
|---|---|
| draft | Allow content changes |
| review | Track content changes |
| frozen | Reject content changes |
| published | Reject content changes |
When loading a frozen/published document:
- Verify document state is "frozen" or "published"
- Verify at least one signature exists
- Verify at least one signature is valid
- Verify document ID matches content hash
- If any verification fails, warn user
Signatures bind to the document ID, which is computed from content:
Signature = Sign(PrivateKey, DocumentID)
This means:
- Any content change invalidates all signatures
- Signature verification proves content unchanged
- Multiple parties can sign the same content
| State | Signature Requirement |
|---|---|
| draft | None |
| review | Optional |
| frozen | At least one valid signature |
| published | At least one valid signature |
Frozen documents can accumulate signatures without changing content:
{
"signatures": [
{
"signedAt": "2025-01-10T08:00:00Z",
"signer": "Alice",
"documentId": "sha256:abc123..."
},
{
"signedAt": "2025-01-11T14:00:00Z",
"signer": "Bob",
"documentId": "sha256:abc123..."
}
]
}All signatures reference the same document ID.
Frozen documents distinguish between:
| Type | Mutability | Part of Hash |
|---|---|---|
| Content | Immutable | Yes |
| Annotations | Mutable | No |
On frozen documents:
- Comments on specific content blocks
- Highlights
- Sticky notes
- Reactions/approvals
Annotations are stored separately:
security/
├── signatures.json # Document signatures
└── annotations.json # User annotations
Annotations do NOT affect the document ID or signatures.
{
"annotations": [
{
"id": "annot-1",
"type": "comment",
"anchor": { "blockId": "block-456" },
"author": "Jane Doe",
"created": "2025-01-15T10:00:00Z",
"content": "This section needs a citation."
}
]
}The anchor field uses a ContentAnchor object from the Anchors and References specification. For range-specific annotations, include start and end:
{
"anchor": { "blockId": "block-456", "start": 10, "end": 25 }
}There are three annotation storage locations, each serving a different purpose:
| Layer | Location | Purpose | Extension Required |
|---|---|---|---|
| Core annotations | security/annotations.json |
Minimal annotation support for frozen/published documents. Lightweight format for implementations that don't support extensions. | No (core) |
| Collaboration | collaboration/comments.json |
Full-featured comments, suggestions, change tracking, presence. Supersedes core annotations when active. | codex.collaboration |
| Phantoms | phantoms/clusters.json |
Spatially-organized off-page annotation clusters. Orthogonal to inline annotations. | codex.phantoms |
When the collaboration extension is active, implementations SHOULD use collaboration/comments.json rather than security/annotations.json for new annotations. Core annotations exist as a fallback for minimal implementations.
Phantoms are a separate concept from inline annotations — they provide spatially-organized, off-page content that is anchored to document content but rendered outside the page plane.
The state is stored in the manifest:
{
"codex": "0.1",
"state": "frozen",
"id": "sha256:..."
}Optionally, state transitions can be logged:
{
"stateHistory": [
{ "state": "draft", "at": "2025-01-10T08:00:00Z" },
{ "state": "review", "at": "2025-01-12T14:00:00Z" },
{ "state": "frozen", "at": "2025-01-15T10:00:00Z", "signature": "sig-1" }
]
}If a frozen document's signature is invalid:
- Warn user: "Document integrity cannot be verified"
- Offer options: View anyway, Reject, Report
- Do NOT allow editing (state is still frozen)
- Log the verification failure
If a frozen document has no signatures:
- Treat as integrity violation
- Same handling as invalid signature
- This indicates tampering (state changed without signature)
When multiple forks exist from the same parent:
- Each fork is a separate document
- Lineage shows common parent
- No automatic merge (out of scope)
If signatures expire (based on certificate validity):
- Document remains frozen
- Signature marked as expired
- New signature can be added if content unchanged
- Original signature retained for audit trail
Implementations SHOULD log state transitions for audit:
{
"transition": "review->frozen",
"at": "2025-01-15T10:00:00Z",
"actor": "alice@example.com",
"signature": "sig-1"
}Implementations SHOULD clearly indicate document state:
| State | Suggested Indicator |
|---|---|
| draft | Yellow/amber badge, "Draft" label |
| review | Blue badge, "In Review" label |
| frozen | Green badge + lock icon, "Signed" label |
| published | Green badge + globe icon, "Published" label |
For frozen documents:
- Disable edit controls
- Show "View Only" mode
- Require explicit "Fork" action to enable editing
{
"codex": "0.1",
"id": "pending",
"state": "draft",
"created": "2025-01-10T08:00:00Z",
"modified": "2025-01-14T16:30:00Z",
"content": {
"path": "content/document.json",
"hash": "sha256:..."
},
"metadata": {
"dublinCore": "metadata/dublin-core.json"
}
}{
"codex": "0.1",
"id": "sha256:3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b",
"state": "frozen",
"created": "2025-01-10T08:00:00Z",
"modified": "2025-01-15T10:00:00Z",
"content": {
"path": "content/document.json",
"hash": "sha256:abc123..."
},
"security": {
"signatures": "security/signatures.json"
},
"extensions": [
{ "id": "codex.security", "version": "0.1", "required": true }
],
"metadata": {
"dublinCore": "metadata/dublin-core.json"
},
"lineage": {
"parent": null,
"version": 1
}
}{
"codex": "0.1",
"id": "pending",
"state": "draft",
"created": "2025-01-16T09:00:00Z",
"modified": "2025-01-16T09:00:00Z",
"content": {
"path": "content/document.json",
"hash": "sha256:def456..."
},
"metadata": {
"dublinCore": "metadata/dublin-core.json"
},
"lineage": {
"parent": "sha256:3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b",
"version": 2,
"branch": "main",
"note": "Forked for Q2 updates"
}
}