+38
-38
.beads/issues.jsonl
+38
-38
.beads/issues.jsonl
···
1
1
{"id":"atproto-1","title":"AT Protocol OCaml Library Suite","description":"Implement a comprehensive suite of OCaml libraries for the AT Protocol (Authenticated Transfer Protocol), enabling developers to build decentralized social networking applications. The implementation should be I/O engine agnostic using OCaml 5.4 effects, pass all public conformance tests, and leverage the OCaml ecosystem effectively.","design":"## Architecture Overview\n\nThe library suite follows the AT Protocol's layered architecture:\n\n1. **Foundation Layer** - Core primitives (syntax, crypto, encoding)\n2. **Data Layer** - IPLD, repositories, MST (Merkle Search Tree)\n3. **Identity Layer** - DIDs, handles, resolution\n4. **Network Layer** - XRPC transport, event streams, sync\n5. **Application Layer** - Lexicon schemas, high-level API\n\n## Design Principles\n\n- **Effects-based I/O**: Use OCaml 5.4 algebraic effects for I/O abstraction\n- **Functional-first**: Immutable data structures, pure functions where possible\n- **Separate packages**: Each component as independent opam package\n- **Test-driven**: Pass all AT Protocol interop tests\n- **Spec-compliant**: Follow atproto.com/specs exactly\n- **No regex**: All syntax validation uses hand-written parsers/codecs\n- **jsont for JSON**: Use jsont library for all JSON serialization\n\n## Package Structure\n\n```\natproto-syntax - Identifier parsing/validation (parser-based)\natproto-crypto - P-256/K-256 cryptography\natproto-multibase - Base encoding (base32, base58btc)\natproto-ipld - DAG-CBOR, CIDs, CAR files\natproto-mst - Merkle Search Tree\natproto-repo - Repository operations\natproto-identity - DID/Handle resolution\natproto-xrpc - HTTP API client/server\natproto-sync - Repository synchronization\natproto-lexicon - Schema language\natproto-api - High-level client API\n```\n\n## Core Dependencies\n\n| Purpose | Library |\n|---------|---------|\n| JSON | jsont |\n| Crypto (P-256) | mirage-crypto-ec |\n| Crypto (K-256) | hacl-star |\n| Hashing | digestif |\n| Time | ptime |\n| I/O (testing) | eio |","acceptance_criteria":"- All packages build with OCaml 5.4\n- All interop tests from bluesky-social/atproto-interop-tests pass\n- Effects-based I/O allows pluggable runtime (eio, lwt, etc.)\n- Documentation for each package\n- Example applications demonstrating usage","notes":"## Research Summary (Dec 2025)\n\n### Library Decisions\n\n| Component | Library | Rationale |\n|-----------|---------|-----------|\n| JSON | `jsont` | Declarative codecs, no intermediate repr |\n| CBOR | `cbor` + wrapper | Use existing, add DAG-CBOR sorting |\n| P-256 | `mirage-crypto-ec` | Mature, RFC 6979 support |\n| K-256 | `secp256k1-ml` | Auto low-S, RFC 6979 built-in |\n| Hashing | `digestif` | SHA-256 |\n| Time | `ptime` + `mtime` | High-res timestamps for TID |\n| Big integers | `zarith` | For low-S normalization |\n\n### Key Implementation Notes from Pegasus\n\n1. **DAG-CBOR**: Sort keys by length first, then lexicographically\n2. **CID**: Cache raw bytes, support empty CIDs\n3. **TID**: Use 2-bit chunks for layer calculation\n4. **MST**: Lazy async node hydration, functor over blockstore\n5. **Low-S**: Use Zarith, always left-pad to 32 bytes\n\n### Interop Test Categories\n- syntax/ - 7 identifier types (handle, did, nsid, tid, aturi, datetime, recordkey)\n- crypto/ - signature verification, did:key encoding\n- data-model/ - CBOR encoding, CID computation\n- mst/ - key heights, common prefix\n- lexicon/ - schema and record validation","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:27.257433425+01:00","updated_at":"2025-12-28T13:36:59.7795445+01:00","closed_at":"2025-12-28T13:36:59.7795445+01:00","labels":["atproto","epic","ocaml"]}
2
-
{"id":"atproto-10","title":"Foundation Layer - Core Primitives","description":"Implement the foundation layer libraries that provide core primitives for the AT Protocol. This includes identifier parsing/validation, cryptographic operations, and base encoding utilities.","design":"## Packages\n\n### atproto-syntax\n- Handle validation (domain format) - **parser-based, no regex**\n- DID validation (did:plc, did:web) - **parser-based, no regex**\n- NSID validation (namespaced identifiers) - **parser-based, no regex**\n- TID generation and validation - **codec-based**\n- Record key validation - **parser-based**\n- AT-URI parsing - **recursive descent parser**\n- Datetime parsing (RFC-3339) - **hand-written parser**\n\n### atproto-crypto\n- P-256 (secp256r1) keypair generation/signing\n- K-256 (secp256k1) keypair generation/signing\n- Low-S signature normalization (required by ATP)\n- RFC 6979 deterministic signatures\n- did:key encoding/decoding\n- JWT creation and verification (using jsont)\n\n### atproto-multibase\n- Base32 encoding/decoding (ATP blessed format)\n- Base58btc encoding/decoding (for did:key)\n- Multibase prefix handling\n\n## Design Principles\n\n- **No regex**: All syntax validation uses hand-written parsers\n- **Codec-based**: Use jsont for JSON serialization\n- **Parser combinators optional**: Can use angstrom if needed, but prefer hand-written for simplicity\n\n## Dependencies\n- mirage-crypto-ec (P-256)\n- hacl-star or secp256k1 (K-256)\n- digestif (SHA-256)\n- jsont (JSON handling)\n- ptime (datetime)\n- **NO re or pcre**","acceptance_criteria":"- atproto-syntax package validates all identifier types\n- atproto-crypto package supports P-256 and K-256 with low-S normalization\n- atproto-multibase package supports base32 and base58btc\n- All syntax interop tests pass","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:38.666246387+01:00","updated_at":"2025-12-28T11:57:30.662537723+01:00","closed_at":"2025-12-28T11:57:30.662537723+01:00","labels":["epic","foundation"],"dependencies":[{"issue_id":"atproto-10","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:13.213777505+01:00","created_by":"daemon"}]}
3
-
{"id":"atproto-11","title":"Implement atproto-syntax package","description":"Implement the atproto-syntax package providing parsers and validators for all AT Protocol identifier types.","design":"## Module Structure\n\n```ocaml\n(* atproto-syntax/lib/handle.ml *)\ntype t\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\nval normalize : t -\u003e t (* lowercase *)\n\n(* atproto-syntax/lib/did.ml *)\ntype method_ = Plc | Web | Key | Other of string\ntype t = { method_: method_; identifier: string }\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\n\n(* atproto-syntax/lib/nsid.ml *)\ntype t\nval of_string : string -\u003e (t, error) result\nval authority : t -\u003e string\nval name : t -\u003e string\n\n(* atproto-syntax/lib/tid.ml *)\ntype t\nval generate : unit -\u003e t\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\nval timestamp_us : t -\u003e int64\n\n(* atproto-syntax/lib/at_uri.ml *)\ntype t = { authority: [ `Did of Did.t | `Handle of Handle.t ]; \n collection: Nsid.t option;\n rkey: Record_key.t option }\n\n(* atproto-syntax/lib/datetime.ml *)\nval parse : string -\u003e (Ptime.t, error) result\nval format : Ptime.t -\u003e string\n```\n\n## Parser-based Validation (NO REGEX)\n\n### Handle Parser\n```ocaml\n(* Requirements from interop tests:\n - Max 253 chars total, max 63 chars per segment\n - At least 2 segments\n - Segments: alphanumeric + hyphens (not at start/end)\n - Case-insensitive, normalize to lowercase\n*)\nlet parse_handle s =\n if String.length s \u003e 253 then Error `Too_long\n else\n let labels = String.split_on_char '.' s in\n if List.length labels \u003c 2 then Error `Too_few_segments\n else if not (List.for_all valid_label labels) then Error `Invalid_label\n else if not (valid_tld (List.hd (List.rev labels))) then Error `Invalid_tld\n else Ok (normalize s)\n```\n\n### TID Parser (from Pegasus)\n```ocaml\nlet charset = \"234567abcdefghijklmnopqrstuvwxyz\"\nlet first_char_valid = \"234567abcdefghij\" (* High bit = 0 *)\n\nlet parse_tid s =\n if String.length s \u003c\u003e 13 then Error `Invalid_length\n else if not (String.contains first_char_valid s.[0]) then Error `High_bit_set\n else if not (String.for_all (fun c -\u003e String.contains charset c) s) then\n Error `Invalid_char\n else Ok s\n```\n\n### DateTime Parser (strict ISO 8601)\n```ocaml\n(* From interop tests - strict requirements:\n - Uppercase T and Z required\n - Timezone required (Z or +/-HH:MM)\n - 4-digit year, 2-digit month/day/hour/min/sec\n*)\nlet parse_datetime s =\n (* Hand-written parser, not regex *)\n let year = parse_4_digits s 0 in\n let month = parse_2_digits s 5 in\n let day = parse_2_digits s 8 in\n (* ... validate T separator at pos 10 ... *)\n let hour = parse_2_digits s 11 in\n (* ... continue ... *)\n```\n\n### Record Key Parser\n```ocaml\n(* From interop tests:\n - Max 512 chars\n - Allowed: alphanumeric + . - _ : ~\n - Cannot be \".\" or \"..\"\n*)\nlet valid_rkey_char c =\n (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c = '.' || c = '-' || c = '_' || c = ':' || c = '~'\n\nlet parse_record_key s =\n if String.length s = 0 || String.length s \u003e 512 then Error `Invalid_length\n else if s = \".\" || s = \"..\" then Error `Reserved\n else if not (String.for_all valid_rkey_char s) then Error `Invalid_char\n else Ok s\n```\n\n## Dependencies\n- ptime (datetime handling)\n- mtime (high-res timestamps for TID generation)\n- NO regex libraries","acceptance_criteria":"- Handle regex validation per spec\n- DID validation for did:plc and did:web\n- NSID validation with 317 char limit\n- TID generation with microsecond precision\n- Record key validation for all types\n- AT-URI parsing and construction\n- All syntax interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:36.014427755+01:00","updated_at":"2025-12-28T01:03:43.574485354+01:00","closed_at":"2025-12-28T01:03:43.574485354+01:00","labels":["foundation","syntax"],"dependencies":[{"issue_id":"atproto-11","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:06.385208896+01:00","created_by":"daemon"}]}
4
-
{"id":"atproto-12","title":"Implement atproto-multibase package","description":"Implement the atproto-multibase package providing base encoding utilities required by AT Protocol.","design":"## Module Structure\n\n```ocaml\n(* atproto-multibase/lib/base32.ml *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/base32_sortable.ml *)\n(* ATP uses sortable base32 for TIDs: 234567abcdefghijklmnopqrstuvwxyz *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/base58btc.ml *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/multibase.ml *)\ntype encoding = Base32 | Base58btc | ...\nval encode : encoding -\u003e bytes -\u003e string\nval decode : string -\u003e (bytes * encoding, error) result\n```\n\n## Multibase Prefixes\n- `b` = base32lower\n- `z` = base58btc\n\n## No external dependencies needed","acceptance_criteria":"- Base32 encoding per ATP spec (charset 234567abcdefghijklmnopqrstuvwxyz)\n- Base58btc encoding for did:key\n- Multibase prefix handling\n- Round-trip encoding/decoding works correctly","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:43.386843683+01:00","updated_at":"2025-12-28T00:45:51.37610055+01:00","closed_at":"2025-12-28T00:45:51.37610055+01:00","labels":["encoding","foundation"],"dependencies":[{"issue_id":"atproto-12","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:07.330194621+01:00","created_by":"daemon"}]}
5
-
{"id":"atproto-13","title":"Implement atproto-crypto package","description":"Implement the atproto-crypto package providing cryptographic operations for AT Protocol including P-256 and K-256 elliptic curve support.","design":"## Module Structure\n\n```ocaml\n(* atproto-crypto/lib/keypair.ml *)\nmodule type S = sig\n type public\n type private_\n type signature\n \n val generate : unit -\u003e private_\n val public : private_ -\u003e public\n val sign : private_ -\u003e bytes -\u003e signature\n val verify : public -\u003e bytes -\u003e signature -\u003e bool\n val public_to_bytes : public -\u003e bytes (* compressed *)\n val public_of_bytes : bytes -\u003e (public, error) result\n val signature_to_bytes : signature -\u003e bytes (* 64 bytes, r||s *)\n val signature_of_bytes : bytes -\u003e (signature, error) result\nend\n\n(* atproto-crypto/lib/p256.ml - uses mirage-crypto-ec *)\ninclude Keypair.S\n\n(* atproto-crypto/lib/k256.ml - uses secp256k1-ml *)\ninclude Keypair.S\n(* Note: secp256k1-ml automatically produces low-S signatures *)\n\n(* atproto-crypto/lib/did_key.ml *)\ntype t = P256 of P256.public | K256 of K256.public\nval encode : t -\u003e string (* \"did:key:z...\" *)\nval decode : string -\u003e (t, error) result\n```\n\n## Library Choices\n\n**P-256 (secp256r1)**: Use `mirage-crypto-ec`\n- `P256.Dsa.generate()` for keypairs\n- `P256.Dsa.sign` with RFC 6979\n- `P256.Dsa.pub_to_octets ~compress:true` for serialization\n\n**K-256 (secp256k1)**: Use `secp256k1-ml` (NOT hacl-star)\n- Automatic low-S normalization (libsecp256k1 always produces low-S)\n- RFC 6979 is default behavior\n- `Secp256k1.Key.to_bytes ~compress:true` for compressed keys\n\n## Multicodec Prefixes (for did:key)\n- P-256 public: `0x80 0x24` (multicodec 0x1200)\n- K-256 public: `0xE7 0x01` (multicodec 0xE7)\n\n## Critical: Low-S Normalization\n\nK-256: Handled automatically by secp256k1-ml\n\nP-256: May need manual check using zarith:\n```ocaml\nlet p256_n = Z.of_string\n \"0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551\"\n\nlet is_low_s s =\n let s_z = Z.of_bits (Bytes.to_string s) in\n Z.leq s_z Z.(p256_n / ~$2)\n```\n\n## Dependencies\n- mirage-crypto-ec (P-256)\n- secp256k1 (K-256 via secp256k1-ml)\n- digestif (SHA-256)\n- zarith (big integers for low-S check)\n- multibase (for did:key encoding)","acceptance_criteria":"- P-256 key generation and ECDSA signing\n- K-256 key generation and ECDSA signing\n- Low-S signature normalization (required!)\n- RFC 6979 deterministic signatures\n- did:key encoding and decoding\n- All crypto interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:54.960668746+01:00","updated_at":"2025-12-28T01:42:29.522627602+01:00","closed_at":"2025-12-28T01:42:29.522627602+01:00","labels":["crypto","foundation"],"dependencies":[{"issue_id":"atproto-13","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:08.277647286+01:00","created_by":"daemon"},{"issue_id":"atproto-13","depends_on_id":"atproto-12","type":"blocks","created_at":"2025-12-28T00:08:10.535715566+01:00","created_by":"daemon"}]}
6
-
{"id":"atproto-14","title":"Implement JWT support in atproto-crypto","description":"Implement JWT support for AT Protocol authentication including inter-service and access tokens.","design":"## Module Structure\n\n```ocaml\n(* atproto-crypto/lib/jwt.ml *)\ntype header = { alg: [ `ES256 | `ES256K ]; typ: string }\ntype claims = { \n iss: string; (* DID *)\n aud: string; (* Service DID *)\n exp: int64; (* Expiration timestamp *)\n iat: int64; (* Issued at *)\n lxm: string option; (* Lexicon method *)\n (* ... other claims *)\n}\n\nval create : \n key:[ `P256 of P256.private_ | `K256 of K256.private_ ] -\u003e\n claims:claims -\u003e\n string\n\nval verify :\n key:[ `P256 of P256.public | `K256 of K256.public ] -\u003e\n string -\u003e\n (claims, error) result\n\nval decode_unverified : string -\u003e (header * claims, error) result\n```\n\n## Jsont Codecs for JWT\n\n```ocaml\nlet header_jsont : header Jsont.t =\n Jsont.obj \"jwt_header\" @@ fun o -\u003e\n let alg = Jsont.obj_mem o \"alg\" Jsont.string \n ~dec:(function \"ES256\" -\u003e `ES256 | \"ES256K\" -\u003e `ES256K | _ -\u003e failwith \"invalid alg\")\n ~enc:(function `ES256 -\u003e \"ES256\" | `ES256K -\u003e \"ES256K\") in\n let typ = Jsont.obj_mem o \"typ\" Jsont.string in\n Jsont.obj_finish o { alg; typ }\n\nlet claims_jsont : claims Jsont.t =\n Jsont.obj \"jwt_claims\" @@ fun o -\u003e\n let iss = Jsont.obj_mem o \"iss\" Jsont.string in\n let aud = Jsont.obj_mem o \"aud\" Jsont.string in\n let exp = Jsont.obj_mem o \"exp\" Jsont.int64 in\n let iat = Jsont.obj_mem o \"iat\" Jsont.int64 in\n let lxm = Jsont.obj_mem o \"lxm\" ~opt:true Jsont.string in\n Jsont.obj_finish o { iss; aud; exp; iat; lxm }\n```\n\n## JWT Types for ATP\n- Access token: `typ: \"at+jwt\"`\n- Refresh token: `typ: \"refresh+jwt\"`\n\n## Dependencies\n- atproto-multibase (base64url)\n- jsont","acceptance_criteria":"- JWT creation with ES256 and ES256K algorithms\n- JWT verification with signature validation\n- Token expiration checking\n- Required claims validation (iss, aud, exp, lxm)","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:08:03.209909326+01:00","updated_at":"2025-12-28T11:00:17.646363681+01:00","closed_at":"2025-12-28T11:00:17.646363681+01:00","labels":["auth","crypto"],"dependencies":[{"issue_id":"atproto-14","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:09.279825662+01:00","created_by":"daemon"},{"issue_id":"atproto-14","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:08:11.099737771+01:00","created_by":"daemon"}]}
2
+
{"id":"atproto-10","title":"Foundation Layer - Core Primitives","description":"Implement the foundation layer libraries that provide core primitives for the AT Protocol. This includes identifier parsing/validation, cryptographic operations, and base encoding utilities.","design":"## Packages\n\n### atproto-syntax\n- Handle validation (domain format) - **parser-based, no regex**\n- DID validation (did:plc, did:web) - **parser-based, no regex**\n- NSID validation (namespaced identifiers) - **parser-based, no regex**\n- TID generation and validation - **codec-based**\n- Record key validation - **parser-based**\n- AT-URI parsing - **recursive descent parser**\n- Datetime parsing (RFC-3339) - **hand-written parser**\n\n### atproto-crypto\n- P-256 (secp256r1) keypair generation/signing\n- K-256 (secp256k1) keypair generation/signing\n- Low-S signature normalization (required by ATP)\n- RFC 6979 deterministic signatures\n- did:key encoding/decoding\n- JWT creation and verification (using jsont)\n\n### atproto-multibase\n- Base32 encoding/decoding (ATP blessed format)\n- Base58btc encoding/decoding (for did:key)\n- Multibase prefix handling\n\n## Design Principles\n\n- **No regex**: All syntax validation uses hand-written parsers\n- **Codec-based**: Use jsont for JSON serialization\n- **Parser combinators optional**: Can use angstrom if needed, but prefer hand-written for simplicity\n\n## Dependencies\n- mirage-crypto-ec (P-256)\n- hacl-star or secp256k1 (K-256)\n- digestif (SHA-256)\n- jsont (JSON handling)\n- ptime (datetime)\n- **NO re or pcre**","acceptance_criteria":"- atproto-syntax package validates all identifier types\n- atproto-crypto package supports P-256 and K-256 with low-S normalization\n- atproto-multibase package supports base32 and base58btc\n- All syntax interop tests pass","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:38.666246387+01:00","updated_at":"2025-12-28T11:57:30.662537723+01:00","closed_at":"2025-12-28T11:57:30.662537723+01:00","labels":["epic","foundation"],"dependencies":[{"issue_id":"atproto-10","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:13.213777505+01:00","created_by":"daemon","metadata":"{}"}]}
3
+
{"id":"atproto-11","title":"Implement atproto-syntax package","description":"Implement the atproto-syntax package providing parsers and validators for all AT Protocol identifier types.","design":"## Module Structure\n\n```ocaml\n(* atproto-syntax/lib/handle.ml *)\ntype t\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\nval normalize : t -\u003e t (* lowercase *)\n\n(* atproto-syntax/lib/did.ml *)\ntype method_ = Plc | Web | Key | Other of string\ntype t = { method_: method_; identifier: string }\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\n\n(* atproto-syntax/lib/nsid.ml *)\ntype t\nval of_string : string -\u003e (t, error) result\nval authority : t -\u003e string\nval name : t -\u003e string\n\n(* atproto-syntax/lib/tid.ml *)\ntype t\nval generate : unit -\u003e t\nval of_string : string -\u003e (t, error) result\nval to_string : t -\u003e string\nval timestamp_us : t -\u003e int64\n\n(* atproto-syntax/lib/at_uri.ml *)\ntype t = { authority: [ `Did of Did.t | `Handle of Handle.t ]; \n collection: Nsid.t option;\n rkey: Record_key.t option }\n\n(* atproto-syntax/lib/datetime.ml *)\nval parse : string -\u003e (Ptime.t, error) result\nval format : Ptime.t -\u003e string\n```\n\n## Parser-based Validation (NO REGEX)\n\n### Handle Parser\n```ocaml\n(* Requirements from interop tests:\n - Max 253 chars total, max 63 chars per segment\n - At least 2 segments\n - Segments: alphanumeric + hyphens (not at start/end)\n - Case-insensitive, normalize to lowercase\n*)\nlet parse_handle s =\n if String.length s \u003e 253 then Error `Too_long\n else\n let labels = String.split_on_char '.' s in\n if List.length labels \u003c 2 then Error `Too_few_segments\n else if not (List.for_all valid_label labels) then Error `Invalid_label\n else if not (valid_tld (List.hd (List.rev labels))) then Error `Invalid_tld\n else Ok (normalize s)\n```\n\n### TID Parser (from Pegasus)\n```ocaml\nlet charset = \"234567abcdefghijklmnopqrstuvwxyz\"\nlet first_char_valid = \"234567abcdefghij\" (* High bit = 0 *)\n\nlet parse_tid s =\n if String.length s \u003c\u003e 13 then Error `Invalid_length\n else if not (String.contains first_char_valid s.[0]) then Error `High_bit_set\n else if not (String.for_all (fun c -\u003e String.contains charset c) s) then\n Error `Invalid_char\n else Ok s\n```\n\n### DateTime Parser (strict ISO 8601)\n```ocaml\n(* From interop tests - strict requirements:\n - Uppercase T and Z required\n - Timezone required (Z or +/-HH:MM)\n - 4-digit year, 2-digit month/day/hour/min/sec\n*)\nlet parse_datetime s =\n (* Hand-written parser, not regex *)\n let year = parse_4_digits s 0 in\n let month = parse_2_digits s 5 in\n let day = parse_2_digits s 8 in\n (* ... validate T separator at pos 10 ... *)\n let hour = parse_2_digits s 11 in\n (* ... continue ... *)\n```\n\n### Record Key Parser\n```ocaml\n(* From interop tests:\n - Max 512 chars\n - Allowed: alphanumeric + . - _ : ~\n - Cannot be \".\" or \"..\"\n*)\nlet valid_rkey_char c =\n (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c = '.' || c = '-' || c = '_' || c = ':' || c = '~'\n\nlet parse_record_key s =\n if String.length s = 0 || String.length s \u003e 512 then Error `Invalid_length\n else if s = \".\" || s = \"..\" then Error `Reserved\n else if not (String.for_all valid_rkey_char s) then Error `Invalid_char\n else Ok s\n```\n\n## Dependencies\n- ptime (datetime handling)\n- mtime (high-res timestamps for TID generation)\n- NO regex libraries","acceptance_criteria":"- Handle regex validation per spec\n- DID validation for did:plc and did:web\n- NSID validation with 317 char limit\n- TID generation with microsecond precision\n- Record key validation for all types\n- AT-URI parsing and construction\n- All syntax interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:36.014427755+01:00","updated_at":"2025-12-28T01:03:43.574485354+01:00","closed_at":"2025-12-28T01:03:43.574485354+01:00","labels":["foundation","syntax"],"dependencies":[{"issue_id":"atproto-11","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:06.385208896+01:00","created_by":"daemon","metadata":"{}"}]}
4
+
{"id":"atproto-12","title":"Implement atproto-multibase package","description":"Implement the atproto-multibase package providing base encoding utilities required by AT Protocol.","design":"## Module Structure\n\n```ocaml\n(* atproto-multibase/lib/base32.ml *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/base32_sortable.ml *)\n(* ATP uses sortable base32 for TIDs: 234567abcdefghijklmnopqrstuvwxyz *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/base58btc.ml *)\nval encode : bytes -\u003e string\nval decode : string -\u003e (bytes, error) result\n\n(* atproto-multibase/lib/multibase.ml *)\ntype encoding = Base32 | Base58btc | ...\nval encode : encoding -\u003e bytes -\u003e string\nval decode : string -\u003e (bytes * encoding, error) result\n```\n\n## Multibase Prefixes\n- `b` = base32lower\n- `z` = base58btc\n\n## No external dependencies needed","acceptance_criteria":"- Base32 encoding per ATP spec (charset 234567abcdefghijklmnopqrstuvwxyz)\n- Base58btc encoding for did:key\n- Multibase prefix handling\n- Round-trip encoding/decoding works correctly","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:43.386843683+01:00","updated_at":"2025-12-28T00:45:51.37610055+01:00","closed_at":"2025-12-28T00:45:51.37610055+01:00","labels":["encoding","foundation"],"dependencies":[{"issue_id":"atproto-12","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:07.330194621+01:00","created_by":"daemon","metadata":"{}"}]}
5
+
{"id":"atproto-13","title":"Implement atproto-crypto package","description":"Implement the atproto-crypto package providing cryptographic operations for AT Protocol including P-256 and K-256 elliptic curve support.","design":"## Module Structure\n\n```ocaml\n(* atproto-crypto/lib/keypair.ml *)\nmodule type S = sig\n type public\n type private_\n type signature\n \n val generate : unit -\u003e private_\n val public : private_ -\u003e public\n val sign : private_ -\u003e bytes -\u003e signature\n val verify : public -\u003e bytes -\u003e signature -\u003e bool\n val public_to_bytes : public -\u003e bytes (* compressed *)\n val public_of_bytes : bytes -\u003e (public, error) result\n val signature_to_bytes : signature -\u003e bytes (* 64 bytes, r||s *)\n val signature_of_bytes : bytes -\u003e (signature, error) result\nend\n\n(* atproto-crypto/lib/p256.ml - uses mirage-crypto-ec *)\ninclude Keypair.S\n\n(* atproto-crypto/lib/k256.ml - uses secp256k1-ml *)\ninclude Keypair.S\n(* Note: secp256k1-ml automatically produces low-S signatures *)\n\n(* atproto-crypto/lib/did_key.ml *)\ntype t = P256 of P256.public | K256 of K256.public\nval encode : t -\u003e string (* \"did:key:z...\" *)\nval decode : string -\u003e (t, error) result\n```\n\n## Library Choices\n\n**P-256 (secp256r1)**: Use `mirage-crypto-ec`\n- `P256.Dsa.generate()` for keypairs\n- `P256.Dsa.sign` with RFC 6979\n- `P256.Dsa.pub_to_octets ~compress:true` for serialization\n\n**K-256 (secp256k1)**: Use `secp256k1-ml` (NOT hacl-star)\n- Automatic low-S normalization (libsecp256k1 always produces low-S)\n- RFC 6979 is default behavior\n- `Secp256k1.Key.to_bytes ~compress:true` for compressed keys\n\n## Multicodec Prefixes (for did:key)\n- P-256 public: `0x80 0x24` (multicodec 0x1200)\n- K-256 public: `0xE7 0x01` (multicodec 0xE7)\n\n## Critical: Low-S Normalization\n\nK-256: Handled automatically by secp256k1-ml\n\nP-256: May need manual check using zarith:\n```ocaml\nlet p256_n = Z.of_string\n \"0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551\"\n\nlet is_low_s s =\n let s_z = Z.of_bits (Bytes.to_string s) in\n Z.leq s_z Z.(p256_n / ~$2)\n```\n\n## Dependencies\n- mirage-crypto-ec (P-256)\n- secp256k1 (K-256 via secp256k1-ml)\n- digestif (SHA-256)\n- zarith (big integers for low-S check)\n- multibase (for did:key encoding)","acceptance_criteria":"- P-256 key generation and ECDSA signing\n- K-256 key generation and ECDSA signing\n- Low-S signature normalization (required!)\n- RFC 6979 deterministic signatures\n- did:key encoding and decoding\n- All crypto interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:07:54.960668746+01:00","updated_at":"2025-12-28T01:42:29.522627602+01:00","closed_at":"2025-12-28T01:42:29.522627602+01:00","labels":["crypto","foundation"],"dependencies":[{"issue_id":"atproto-13","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:08.277647286+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-13","depends_on_id":"atproto-12","type":"blocks","created_at":"2025-12-28T00:08:10.535715566+01:00","created_by":"daemon","metadata":"{}"}]}
6
+
{"id":"atproto-14","title":"Implement JWT support in atproto-crypto","description":"Implement JWT support for AT Protocol authentication including inter-service and access tokens.","design":"## Module Structure\n\n```ocaml\n(* atproto-crypto/lib/jwt.ml *)\ntype header = { alg: [ `ES256 | `ES256K ]; typ: string }\ntype claims = { \n iss: string; (* DID *)\n aud: string; (* Service DID *)\n exp: int64; (* Expiration timestamp *)\n iat: int64; (* Issued at *)\n lxm: string option; (* Lexicon method *)\n (* ... other claims *)\n}\n\nval create : \n key:[ `P256 of P256.private_ | `K256 of K256.private_ ] -\u003e\n claims:claims -\u003e\n string\n\nval verify :\n key:[ `P256 of P256.public | `K256 of K256.public ] -\u003e\n string -\u003e\n (claims, error) result\n\nval decode_unverified : string -\u003e (header * claims, error) result\n```\n\n## Jsont Codecs for JWT\n\n```ocaml\nlet header_jsont : header Jsont.t =\n Jsont.obj \"jwt_header\" @@ fun o -\u003e\n let alg = Jsont.obj_mem o \"alg\" Jsont.string \n ~dec:(function \"ES256\" -\u003e `ES256 | \"ES256K\" -\u003e `ES256K | _ -\u003e failwith \"invalid alg\")\n ~enc:(function `ES256 -\u003e \"ES256\" | `ES256K -\u003e \"ES256K\") in\n let typ = Jsont.obj_mem o \"typ\" Jsont.string in\n Jsont.obj_finish o { alg; typ }\n\nlet claims_jsont : claims Jsont.t =\n Jsont.obj \"jwt_claims\" @@ fun o -\u003e\n let iss = Jsont.obj_mem o \"iss\" Jsont.string in\n let aud = Jsont.obj_mem o \"aud\" Jsont.string in\n let exp = Jsont.obj_mem o \"exp\" Jsont.int64 in\n let iat = Jsont.obj_mem o \"iat\" Jsont.int64 in\n let lxm = Jsont.obj_mem o \"lxm\" ~opt:true Jsont.string in\n Jsont.obj_finish o { iss; aud; exp; iat; lxm }\n```\n\n## JWT Types for ATP\n- Access token: `typ: \"at+jwt\"`\n- Refresh token: `typ: \"refresh+jwt\"`\n\n## Dependencies\n- atproto-multibase (base64url)\n- jsont","acceptance_criteria":"- JWT creation with ES256 and ES256K algorithms\n- JWT verification with signature validation\n- Token expiration checking\n- Required claims validation (iss, aud, exp, lxm)","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:08:03.209909326+01:00","updated_at":"2025-12-28T11:00:17.646363681+01:00","closed_at":"2025-12-28T11:00:17.646363681+01:00","labels":["auth","crypto"],"dependencies":[{"issue_id":"atproto-14","depends_on_id":"atproto-10","type":"parent-child","created_at":"2025-12-28T00:08:09.279825662+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-14","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:08:11.099737771+01:00","created_by":"daemon","metadata":"{}"}]}
7
7
{"id":"atproto-1ne","title":"Add missing lexicon record validation tests","description":"15 entries in record-data-invalid.json are currently skipped. These need to be implemented:\n\n**String format validation (12 tests):**\n- invalid string format handle\n- invalid string format did\n- invalid string format atidentifier\n- invalid string format nsid\n- invalid string format aturi\n- invalid string format cid\n- invalid string format datetime\n- invalid string format language\n- invalid string format uri\n- invalid string format tid\n- invalid string format recordkey\n- union inner invalid\n\n**Unknown field type validation (3 tests):**\n- unknown wrong type (bool)\n- unknown wrong type (bytes)\n- unknown wrong type (blob)\n\nThis requires implementing format validation in the Validator module.","acceptance_criteria":"- All 15 currently-skipped tests are enabled and passing\n- Format validation is implemented for all string formats\n- Unknown field type restrictions are enforced\n- 51/51 record-data-invalid.json entries are tested","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T12:12:32.793841929+01:00","updated_at":"2025-12-28T12:47:58.051715126+01:00","closed_at":"2025-12-28T12:47:58.051715126+01:00","labels":["conformance","lexicon","testing"]}
8
-
{"id":"atproto-20","title":"Data Layer - IPLD, MST, Repository","description":"Implement the data layer libraries that handle content-addressed data structures, repositories, and the Merkle Search Tree used by AT Protocol.","design":"## Packages\n\n### atproto-ipld\n- DAG-CBOR encoder/decoder (deterministic)\n- CID creation and parsing (CIDv1, SHA-256)\n- CAR file reading and writing\n- Blob type handling\n\n### atproto-mst\n- Merkle Search Tree implementation\n- Key depth calculation (SHA-256 leading zeros)\n- Incremental add/delete operations\n- Tree diffing for sync\n- Functor-based blockstore abstraction\n\n### atproto-repo\n- Repository structure (v3 format)\n- Commit object creation and signing\n- Record operations (create, update, delete)\n- Repository sync operations\n\n## Dependencies\n- atproto-crypto\n- atproto-ipld\n- digestif","acceptance_criteria":"- IPLD package handles DAG-CBOR and CIDs correctly\n- MST implementation matches spec exactly\n- Repository package supports commits and signing\n- All data-model and MST interop tests pass","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:46.199875469+01:00","updated_at":"2025-12-28T11:57:32.152844222+01:00","closed_at":"2025-12-28T11:57:32.152844222+01:00","labels":["data","epic"],"dependencies":[{"issue_id":"atproto-20","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:14.142103555+01:00","created_by":"daemon"}]}
9
-
{"id":"atproto-21","title":"Implement DAG-CBOR codec","description":"Implement DAG-CBOR encoder and decoder for AT Protocol's data model. DAG-CBOR is a deterministic subset of CBOR used for content-addressed data.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/dag_cbor.ml *)\ntype value =\n | Null\n | Bool of bool\n | Int of int64 (* Use int64 for JavaScript safe integer range *)\n | String of string\n | Bytes of bytes\n | Array of value list\n | Map of (string * value) list (* sorted by key *)\n | Link of Cid.t\n\nval encode : value -\u003e bytes\nval decode : bytes -\u003e (value, error) result\n\n(* JSON representation using jsont *)\nval jsont : value Jsont.t\n```\n\n## Implementation Strategy\n\nUse `cbor` opam library as base, add DAG-CBOR wrapper:\n\n1. **cbor library** handles: CBOR encoding/decoding, tag support\n2. **Our wrapper** adds:\n - Map key sorting (length first, then lexicographic)\n - Float rejection\n - Integer range validation (-2^53 to 2^53)\n - CID tag 42 handling\n\n## CRITICAL: Key Sorting Algorithm (from Pegasus)\n\n```ocaml\nlet compare_keys k1 k2 =\n let len1 = String.length k1 in\n let len2 = String.length k2 in\n if len1 = len2 then String.compare k1 k2\n else Int.compare len1 len2 (* Length first! *)\n\nlet sort_map_keys pairs =\n List.sort (fun (k1, _) (k2, _) -\u003e compare_keys k1 k2) pairs\n```\n\n## CID Tag 42 Encoding\n\n```ocaml\nlet encode_cid cid =\n let cid_bytes = Cid.to_bytes cid in (* Includes \\x00 multibase prefix *)\n `Tag (42, `Bytes cid_bytes)\n```\n\n## Integer Range Check (JavaScript Safety)\n\n```ocaml\nlet js_safe_min = -9007199254740991L (* -(2^53 - 1) *)\nlet js_safe_max = 9007199254740991L (* 2^53 - 1 *)\n\nlet validate_integer i =\n if i \u003c js_safe_min || i \u003e js_safe_max then\n Error `Integer_out_of_range\n else Ok i\n```\n\n## Special JSON Representations\n\n```ocaml\n(* $link for CID *)\nlet cid_link_jsont =\n Jsont.Object.map ~kind:\"cid-link\" (fun link -\u003e Link (Cid.of_string link))\n |\u003e Jsont.Object.mem \"$link\" Jsont.string ~enc:Cid.to_string\n |\u003e Jsont.Object.finish\n\n(* $bytes for raw bytes *)\nlet bytes_jsont =\n Jsont.Object.map ~kind:\"bytes\" (fun b64 -\u003e Bytes (Base64.decode b64))\n |\u003e Jsont.Object.mem \"$bytes\" Jsont.string ~enc:Base64.encode\n |\u003e Jsont.Object.finish\n```\n\n## Dependencies\n- cbor \u003e= 0.5 (base CBOR codec)\n- jsont (JSON handling)\n- digestif (for CID hashing)","acceptance_criteria":"- DAG-CBOR encoding is deterministic (sorted keys, specific types)\n- No floats allowed in data model\n- JSON↔CBOR conversion works correctly\n- All data-model interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:24.992900973+01:00","updated_at":"2025-12-28T02:05:09.703411875+01:00","closed_at":"2025-12-28T02:05:09.703411875+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-21","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:18.587980423+01:00","created_by":"daemon"},{"issue_id":"atproto-21","depends_on_id":"atproto-22","type":"blocks","created_at":"2025-12-28T00:09:25.230617121+01:00","created_by":"daemon"}]}
10
-
{"id":"atproto-22","title":"Implement CID (Content Identifier)","description":"Implement Content Identifier (CID) support for AT Protocol. CIDs are self-describing content-addressed identifiers.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/cid.ml *)\ntype codec = DagCbor | Raw\ntype t\n\n(* Creation *)\nval create : codec:codec -\u003e bytes -\u003e t\nval of_dag_cbor : bytes -\u003e t (* convenience *)\nval of_raw : bytes -\u003e t (* for blobs *)\n\n(* Parsing *)\nval of_string : string -\u003e (t, error) result\nval of_bytes : bytes -\u003e (t, error) result\n\n(* Serialization *)\nval to_string : t -\u003e string (* base32 encoded *)\nval to_bytes : t -\u003e bytes (* binary form for tag 42 *)\n\n(* Accessors *)\nval codec : t -\u003e codec\nval hash : t -\u003e bytes (* raw SHA-256 hash *)\nval equal : t -\u003e t -\u003e bool\nval compare : t -\u003e t -\u003e int\n```\n\n## ATP Blessed CID Format\n\n- Version: CIDv1 only\n- Hash: SHA-256 (multicodec 0x12), 256 bits\n- Codec: dag-cbor (0x71) for data, raw (0x55) for blobs\n- String encoding: base32 (multibase prefix 'b')\n\n## CID Binary Structure\n```\n\u003cversion=1\u003e \u003ccodec-varint\u003e \u003chash-multicodec\u003e \u003chash-length\u003e \u003chash-bytes\u003e\n```\n\n## Dependencies\n- digestif (SHA-256)\n- atproto-multibase","acceptance_criteria":"- CIDv1 creation with SHA-256 and dag-cbor multicodec\n- CID string parsing and validation\n- Binary CID encoding for CBOR tag 42\n- All CID interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:35.195261117+01:00","updated_at":"2025-12-28T01:55:58.641459339+01:00","closed_at":"2025-12-28T01:55:58.641459339+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-22","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:19.549103067+01:00","created_by":"daemon"},{"issue_id":"atproto-22","depends_on_id":"atproto-12","type":"blocks","created_at":"2025-12-28T00:09:24.279353993+01:00","created_by":"daemon"}]}
11
-
{"id":"atproto-23","title":"Implement CAR file format","description":"Implement CAR (Content Addressable aRchive) file format support for AT Protocol. CAR files are used for repository export and sync.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/car.ml *)\ntype header = { version: int; roots: Cid.t list }\ntype block = { cid: Cid.t; data: bytes }\n\n(* Reading *)\nval read_header : bytes -\u003e (header * int, error) result\nval read_blocks : bytes -\u003e offset:int -\u003e block Seq.t\n\n(* Writing *)\nval write : roots:Cid.t list -\u003e blocks:block list -\u003e bytes\n\n(* Streaming API using effects *)\ntype _ Effect.t +=\n | Read_bytes : int -\u003e bytes Effect.t\n \nval stream_blocks : unit -\u003e block option (* uses Read_bytes effect *)\n```\n\n## CAR v1 Format\n\n```\n\u003cheader-length-varint\u003e \u003cdag-cbor-header\u003e\n\u003cblock-1-length-varint\u003e \u003ccid-1\u003e \u003cdata-1\u003e\n\u003cblock-2-length-varint\u003e \u003ccid-2\u003e \u003cdata-2\u003e\n...\n```\n\n## Header Structure\n```cbor\n{ \"version\": 1, \"roots\": [\u003ccid\u003e, ...] }\n```\n\n## Dependencies\n- atproto-ipld (dag-cbor, cid)","acceptance_criteria":"- CAR v1 reading and writing\n- Streaming block iteration\n- Proper varint encoding\n- Root CID validation","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:43.573326253+01:00","updated_at":"2025-12-28T02:08:35.37815686+01:00","closed_at":"2025-12-28T02:08:35.37815686+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-23","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:20.490759113+01:00","created_by":"daemon"},{"issue_id":"atproto-23","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:09:26.481546763+01:00","created_by":"daemon"}]}
12
-
{"id":"atproto-24","title":"Implement Merkle Search Tree (MST)","description":"Implement Merkle Search Tree (MST) for AT Protocol repositories. The MST provides a content-addressed, verifiable key-value store.","design":"## Module Structure\n\n```ocaml\n(* atproto-mst/lib/mst.ml *)\nmodule type Blockstore = sig\n type t\n val get : t -\u003e Cid.t -\u003e bytes option\n val put : t -\u003e Cid.t -\u003e bytes -\u003e unit\nend\n\nmodule Make (Store : Blockstore) : sig\n type t\n \n val empty : Store.t -\u003e t\n val of_root : Store.t -\u003e Cid.t -\u003e t\n \n val get : t -\u003e string -\u003e Cid.t option\n val add : t -\u003e string -\u003e Cid.t -\u003e t\n val delete : t -\u003e string -\u003e t\n \n val root : t -\u003e Cid.t\n val entries : t -\u003e (string * Cid.t) Seq.t\n \n val diff : old:t -\u003e new_:t -\u003e diff list\nend\n```\n\n## CRITICAL: Key Height Calculation (from Pegasus)\n\nATProto uses **2-bit chunks** (fanout = 4), NOT single bits:\n\n```ocaml\nlet leading_zeros_on_hash key =\n let digest = Digestif.SHA256.(digest_string key |\u003e to_raw_string) in\n let rec loop idx zeros =\n if idx \u003e= String.length digest then zeros\n else\n let byte = Char.code digest.[idx] in\n let zeros' = zeros +\n if byte = 0 then 4 (* Full byte = 4 two-bit zeros *)\n else if byte \u003c 4 then 3 (* 0b000000xx *)\n else if byte \u003c 16 then 2 (* 0b0000xxxx *)\n else if byte \u003c 64 then 1 (* 0b00xxxxxx *)\n else 0 (* 0bxxxxxxxx *)\n in\n if byte = 0 then loop (idx + 1) zeros' else zeros'\n in\n loop 0 0\n```\n\n## Raw Node Structure (for CBOR)\n\n```ocaml\ntype node_raw = {\n l: Cid.t option; (* Left subtree *)\n e: entry_raw list (* Entries at this level *)\n}\n\ntype entry_raw = {\n p: int; (* Prefix length shared with previous key *)\n k: bytes; (* Key suffix (after shared prefix) *)\n v: Cid.t; (* Value CID *)\n t: Cid.t option (* Right subtree *)\n}\n```\n\n## Hydrated Node (for traversal)\n\n```ocaml\ntype node = {\n layer: int;\n mutable left: node option Lazy.t;\n mutable entries: entry list\n}\n\ntype entry = {\n layer: int;\n key: string; (* Full key, decompressed *)\n value: Cid.t;\n right: node option Lazy.t\n}\n```\n\n## Key Validation\n\n```ocaml\nlet is_valid_mst_key key =\n match String.split_on_char '/' key with\n | [collection; rkey] -\u003e\n String.length key \u003c= 1024 \u0026\u0026\n collection \u003c\u003e \"\" \u0026\u0026 rkey \u003c\u003e \"\" \u0026\u0026\n String.for_all is_valid_char collection \u0026\u0026\n String.for_all is_valid_char rkey\n | _ -\u003e false\n\nlet is_valid_char c =\n (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c = '.' || c = '-' || c = '_' || c = '~'\n```\n\n## Building from Sorted Leaves\n\n```ocaml\nlet of_assoc store assoc =\n let sorted = List.sort (fun (k1, _) (k2, _) -\u003e String.compare k1 k2) assoc in\n let with_layers = List.map (fun (k, v) -\u003e\n (k, v, leading_zeros_on_hash k)) sorted in\n (* Group by layer, build tree bottom-up *)\n ...\n```\n\n## Dependencies\n- atproto-ipld (dag-cbor, cid)\n- digestif (SHA-256 for key hashing)","acceptance_criteria":"- Correct key depth calculation (SHA-256 leading zeros / 2)\n- Deterministic tree structure from key/value pairs\n- Incremental add/delete operations\n- Tree diffing for sync\n- Functor-based blockstore abstraction\n- All MST interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:56.250995314+01:00","updated_at":"2025-12-28T02:13:22.864318902+01:00","closed_at":"2025-12-28T02:13:22.864318902+01:00","labels":["data","mst"],"dependencies":[{"issue_id":"atproto-24","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:21.767912975+01:00","created_by":"daemon"},{"issue_id":"atproto-24","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:09:27.06774891+01:00","created_by":"daemon"}]}
8
+
{"id":"atproto-20","title":"Data Layer - IPLD, MST, Repository","description":"Implement the data layer libraries that handle content-addressed data structures, repositories, and the Merkle Search Tree used by AT Protocol.","design":"## Packages\n\n### atproto-ipld\n- DAG-CBOR encoder/decoder (deterministic)\n- CID creation and parsing (CIDv1, SHA-256)\n- CAR file reading and writing\n- Blob type handling\n\n### atproto-mst\n- Merkle Search Tree implementation\n- Key depth calculation (SHA-256 leading zeros)\n- Incremental add/delete operations\n- Tree diffing for sync\n- Functor-based blockstore abstraction\n\n### atproto-repo\n- Repository structure (v3 format)\n- Commit object creation and signing\n- Record operations (create, update, delete)\n- Repository sync operations\n\n## Dependencies\n- atproto-crypto\n- atproto-ipld\n- digestif","acceptance_criteria":"- IPLD package handles DAG-CBOR and CIDs correctly\n- MST implementation matches spec exactly\n- Repository package supports commits and signing\n- All data-model and MST interop tests pass","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:46.199875469+01:00","updated_at":"2025-12-28T11:57:32.152844222+01:00","closed_at":"2025-12-28T11:57:32.152844222+01:00","labels":["data","epic"],"dependencies":[{"issue_id":"atproto-20","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:14.142103555+01:00","created_by":"daemon","metadata":"{}"}]}
9
+
{"id":"atproto-21","title":"Implement DAG-CBOR codec","description":"Implement DAG-CBOR encoder and decoder for AT Protocol's data model. DAG-CBOR is a deterministic subset of CBOR used for content-addressed data.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/dag_cbor.ml *)\ntype value =\n | Null\n | Bool of bool\n | Int of int64 (* Use int64 for JavaScript safe integer range *)\n | String of string\n | Bytes of bytes\n | Array of value list\n | Map of (string * value) list (* sorted by key *)\n | Link of Cid.t\n\nval encode : value -\u003e bytes\nval decode : bytes -\u003e (value, error) result\n\n(* JSON representation using jsont *)\nval jsont : value Jsont.t\n```\n\n## Implementation Strategy\n\nUse `cbor` opam library as base, add DAG-CBOR wrapper:\n\n1. **cbor library** handles: CBOR encoding/decoding, tag support\n2. **Our wrapper** adds:\n - Map key sorting (length first, then lexicographic)\n - Float rejection\n - Integer range validation (-2^53 to 2^53)\n - CID tag 42 handling\n\n## CRITICAL: Key Sorting Algorithm (from Pegasus)\n\n```ocaml\nlet compare_keys k1 k2 =\n let len1 = String.length k1 in\n let len2 = String.length k2 in\n if len1 = len2 then String.compare k1 k2\n else Int.compare len1 len2 (* Length first! *)\n\nlet sort_map_keys pairs =\n List.sort (fun (k1, _) (k2, _) -\u003e compare_keys k1 k2) pairs\n```\n\n## CID Tag 42 Encoding\n\n```ocaml\nlet encode_cid cid =\n let cid_bytes = Cid.to_bytes cid in (* Includes \\x00 multibase prefix *)\n `Tag (42, `Bytes cid_bytes)\n```\n\n## Integer Range Check (JavaScript Safety)\n\n```ocaml\nlet js_safe_min = -9007199254740991L (* -(2^53 - 1) *)\nlet js_safe_max = 9007199254740991L (* 2^53 - 1 *)\n\nlet validate_integer i =\n if i \u003c js_safe_min || i \u003e js_safe_max then\n Error `Integer_out_of_range\n else Ok i\n```\n\n## Special JSON Representations\n\n```ocaml\n(* $link for CID *)\nlet cid_link_jsont =\n Jsont.Object.map ~kind:\"cid-link\" (fun link -\u003e Link (Cid.of_string link))\n |\u003e Jsont.Object.mem \"$link\" Jsont.string ~enc:Cid.to_string\n |\u003e Jsont.Object.finish\n\n(* $bytes for raw bytes *)\nlet bytes_jsont =\n Jsont.Object.map ~kind:\"bytes\" (fun b64 -\u003e Bytes (Base64.decode b64))\n |\u003e Jsont.Object.mem \"$bytes\" Jsont.string ~enc:Base64.encode\n |\u003e Jsont.Object.finish\n```\n\n## Dependencies\n- cbor \u003e= 0.5 (base CBOR codec)\n- jsont (JSON handling)\n- digestif (for CID hashing)","acceptance_criteria":"- DAG-CBOR encoding is deterministic (sorted keys, specific types)\n- No floats allowed in data model\n- JSON↔CBOR conversion works correctly\n- All data-model interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:24.992900973+01:00","updated_at":"2025-12-28T02:05:09.703411875+01:00","closed_at":"2025-12-28T02:05:09.703411875+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-21","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:18.587980423+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-21","depends_on_id":"atproto-22","type":"blocks","created_at":"2025-12-28T00:09:25.230617121+01:00","created_by":"daemon","metadata":"{}"}]}
10
+
{"id":"atproto-22","title":"Implement CID (Content Identifier)","description":"Implement Content Identifier (CID) support for AT Protocol. CIDs are self-describing content-addressed identifiers.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/cid.ml *)\ntype codec = DagCbor | Raw\ntype t\n\n(* Creation *)\nval create : codec:codec -\u003e bytes -\u003e t\nval of_dag_cbor : bytes -\u003e t (* convenience *)\nval of_raw : bytes -\u003e t (* for blobs *)\n\n(* Parsing *)\nval of_string : string -\u003e (t, error) result\nval of_bytes : bytes -\u003e (t, error) result\n\n(* Serialization *)\nval to_string : t -\u003e string (* base32 encoded *)\nval to_bytes : t -\u003e bytes (* binary form for tag 42 *)\n\n(* Accessors *)\nval codec : t -\u003e codec\nval hash : t -\u003e bytes (* raw SHA-256 hash *)\nval equal : t -\u003e t -\u003e bool\nval compare : t -\u003e t -\u003e int\n```\n\n## ATP Blessed CID Format\n\n- Version: CIDv1 only\n- Hash: SHA-256 (multicodec 0x12), 256 bits\n- Codec: dag-cbor (0x71) for data, raw (0x55) for blobs\n- String encoding: base32 (multibase prefix 'b')\n\n## CID Binary Structure\n```\n\u003cversion=1\u003e \u003ccodec-varint\u003e \u003chash-multicodec\u003e \u003chash-length\u003e \u003chash-bytes\u003e\n```\n\n## Dependencies\n- digestif (SHA-256)\n- atproto-multibase","acceptance_criteria":"- CIDv1 creation with SHA-256 and dag-cbor multicodec\n- CID string parsing and validation\n- Binary CID encoding for CBOR tag 42\n- All CID interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:35.195261117+01:00","updated_at":"2025-12-28T01:55:58.641459339+01:00","closed_at":"2025-12-28T01:55:58.641459339+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-22","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:19.549103067+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-22","depends_on_id":"atproto-12","type":"blocks","created_at":"2025-12-28T00:09:24.279353993+01:00","created_by":"daemon","metadata":"{}"}]}
11
+
{"id":"atproto-23","title":"Implement CAR file format","description":"Implement CAR (Content Addressable aRchive) file format support for AT Protocol. CAR files are used for repository export and sync.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/car.ml *)\ntype header = { version: int; roots: Cid.t list }\ntype block = { cid: Cid.t; data: bytes }\n\n(* Reading *)\nval read_header : bytes -\u003e (header * int, error) result\nval read_blocks : bytes -\u003e offset:int -\u003e block Seq.t\n\n(* Writing *)\nval write : roots:Cid.t list -\u003e blocks:block list -\u003e bytes\n\n(* Streaming API using effects *)\ntype _ Effect.t +=\n | Read_bytes : int -\u003e bytes Effect.t\n \nval stream_blocks : unit -\u003e block option (* uses Read_bytes effect *)\n```\n\n## CAR v1 Format\n\n```\n\u003cheader-length-varint\u003e \u003cdag-cbor-header\u003e\n\u003cblock-1-length-varint\u003e \u003ccid-1\u003e \u003cdata-1\u003e\n\u003cblock-2-length-varint\u003e \u003ccid-2\u003e \u003cdata-2\u003e\n...\n```\n\n## Header Structure\n```cbor\n{ \"version\": 1, \"roots\": [\u003ccid\u003e, ...] }\n```\n\n## Dependencies\n- atproto-ipld (dag-cbor, cid)","acceptance_criteria":"- CAR v1 reading and writing\n- Streaming block iteration\n- Proper varint encoding\n- Root CID validation","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:43.573326253+01:00","updated_at":"2025-12-28T02:08:35.37815686+01:00","closed_at":"2025-12-28T02:08:35.37815686+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-23","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:20.490759113+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-23","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:09:26.481546763+01:00","created_by":"daemon","metadata":"{}"}]}
12
+
{"id":"atproto-24","title":"Implement Merkle Search Tree (MST)","description":"Implement Merkle Search Tree (MST) for AT Protocol repositories. The MST provides a content-addressed, verifiable key-value store.","design":"## Module Structure\n\n```ocaml\n(* atproto-mst/lib/mst.ml *)\nmodule type Blockstore = sig\n type t\n val get : t -\u003e Cid.t -\u003e bytes option\n val put : t -\u003e Cid.t -\u003e bytes -\u003e unit\nend\n\nmodule Make (Store : Blockstore) : sig\n type t\n \n val empty : Store.t -\u003e t\n val of_root : Store.t -\u003e Cid.t -\u003e t\n \n val get : t -\u003e string -\u003e Cid.t option\n val add : t -\u003e string -\u003e Cid.t -\u003e t\n val delete : t -\u003e string -\u003e t\n \n val root : t -\u003e Cid.t\n val entries : t -\u003e (string * Cid.t) Seq.t\n \n val diff : old:t -\u003e new_:t -\u003e diff list\nend\n```\n\n## CRITICAL: Key Height Calculation (from Pegasus)\n\nATProto uses **2-bit chunks** (fanout = 4), NOT single bits:\n\n```ocaml\nlet leading_zeros_on_hash key =\n let digest = Digestif.SHA256.(digest_string key |\u003e to_raw_string) in\n let rec loop idx zeros =\n if idx \u003e= String.length digest then zeros\n else\n let byte = Char.code digest.[idx] in\n let zeros' = zeros +\n if byte = 0 then 4 (* Full byte = 4 two-bit zeros *)\n else if byte \u003c 4 then 3 (* 0b000000xx *)\n else if byte \u003c 16 then 2 (* 0b0000xxxx *)\n else if byte \u003c 64 then 1 (* 0b00xxxxxx *)\n else 0 (* 0bxxxxxxxx *)\n in\n if byte = 0 then loop (idx + 1) zeros' else zeros'\n in\n loop 0 0\n```\n\n## Raw Node Structure (for CBOR)\n\n```ocaml\ntype node_raw = {\n l: Cid.t option; (* Left subtree *)\n e: entry_raw list (* Entries at this level *)\n}\n\ntype entry_raw = {\n p: int; (* Prefix length shared with previous key *)\n k: bytes; (* Key suffix (after shared prefix) *)\n v: Cid.t; (* Value CID *)\n t: Cid.t option (* Right subtree *)\n}\n```\n\n## Hydrated Node (for traversal)\n\n```ocaml\ntype node = {\n layer: int;\n mutable left: node option Lazy.t;\n mutable entries: entry list\n}\n\ntype entry = {\n layer: int;\n key: string; (* Full key, decompressed *)\n value: Cid.t;\n right: node option Lazy.t\n}\n```\n\n## Key Validation\n\n```ocaml\nlet is_valid_mst_key key =\n match String.split_on_char '/' key with\n | [collection; rkey] -\u003e\n String.length key \u003c= 1024 \u0026\u0026\n collection \u003c\u003e \"\" \u0026\u0026 rkey \u003c\u003e \"\" \u0026\u0026\n String.for_all is_valid_char collection \u0026\u0026\n String.for_all is_valid_char rkey\n | _ -\u003e false\n\nlet is_valid_char c =\n (c \u003e= 'a' \u0026\u0026 c \u003c= 'z') || (c \u003e= 'A' \u0026\u0026 c \u003c= 'Z') ||\n (c \u003e= '0' \u0026\u0026 c \u003c= '9') || c = '.' || c = '-' || c = '_' || c = '~'\n```\n\n## Building from Sorted Leaves\n\n```ocaml\nlet of_assoc store assoc =\n let sorted = List.sort (fun (k1, _) (k2, _) -\u003e String.compare k1 k2) assoc in\n let with_layers = List.map (fun (k, v) -\u003e\n (k, v, leading_zeros_on_hash k)) sorted in\n (* Group by layer, build tree bottom-up *)\n ...\n```\n\n## Dependencies\n- atproto-ipld (dag-cbor, cid)\n- digestif (SHA-256 for key hashing)","acceptance_criteria":"- Correct key depth calculation (SHA-256 leading zeros / 2)\n- Deterministic tree structure from key/value pairs\n- Incremental add/delete operations\n- Tree diffing for sync\n- Functor-based blockstore abstraction\n- All MST interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:08:56.250995314+01:00","updated_at":"2025-12-28T02:13:22.864318902+01:00","closed_at":"2025-12-28T02:13:22.864318902+01:00","labels":["data","mst"],"dependencies":[{"issue_id":"atproto-24","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:21.767912975+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-24","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:09:27.06774891+01:00","created_by":"daemon","metadata":"{}"}]}
13
13
{"id":"atproto-24w","title":"Add missing syntax conformance tests","description":"Add tests for syntax fixtures that are not currently covered:\n\n1. **AT Identifier** - atidentifier_syntax_valid.txt, atidentifier_syntax_invalid.txt\n - Requires implementing an At_identifier module or testing DID/Handle as union\n\n2. **CID syntax** - cid_syntax_valid.txt, cid_syntax_invalid.txt\n - CID module exists in atproto-ipld, need to add syntax tests\n\n3. **URI syntax** - uri_syntax_valid.txt, uri_syntax_invalid.txt\n - Generic URI validation (distinct from AT-URI)\n\n4. **Language tags** - language_syntax_valid.txt, language_syntax_invalid.txt\n - BCP-47 language tag validation","design":"## Implementation Plan\n\n### 1. AT Identifier (DID or Handle union)\n- AT Identifier is either a valid DID or a valid Handle\n- Add `At_identifier` module to atproto-syntax or test inline\n- Test: try DID first, then Handle - if both fail, invalid\n\n### 2. CID Syntax \n- CID module already exists in atproto-ipld\n- Add CID syntax tests to test_syntax.ml using Cid.of_string\n- Need to add atproto_ipld dependency to test\n\n### 3. URI Syntax\n- Generic RFC-3986 URI validation\n- Can use Uri library's parsing or add simple validator\n- Test: Uri.of_string should succeed for valid, parsing should catch invalid\n\n### 4. Language Tags (BCP-47)\n- Need to implement Language module in atproto-syntax\n- BCP-47 format: language[-script][-region][-variant][-extension][-privateuse]\n- Examples: \"en\", \"en-US\", \"zh-Hant\", \"i-navajo\"\n\n## Files to Modify\n- `lib/syntax/atproto_syntax.ml` - expose new modules\n- `lib/syntax/dune` - if new files needed\n- `test/syntax/test_syntax.ml` - add 8 new test functions\n- `test/syntax/dune` - add atproto_ipld dependency for CID tests\n\n## Order of Implementation\n1. AT Identifier tests (uses existing DID/Handle)\n2. CID tests (uses existing Cid module from ipld)\n3. Language module + tests (new implementation)\n4. URI tests (use Uri library)","acceptance_criteria":"- All 4 fixture pairs have corresponding tests\n- Tests load ALL entries from each fixture file\n- Valid entries pass parsing\n- Invalid entries fail parsing with appropriate errors","notes":"Completed all missing syntax conformance tests:\n- AT Identifier tests (valid/invalid from fixtures)\n- CID tests (valid/invalid from fixtures)\n- Language tag tests (BCP-47 validation in lib/syntax/language.ml)\n- URI tests (RFC-3986 validation with strict checks for scheme, whitespace, invalid chars, max length)","status":"closed","priority":1,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T12:12:11.492860987+01:00","updated_at":"2025-12-28T12:40:47.252759691+01:00","closed_at":"2025-12-28T12:40:47.252759691+01:00","labels":["conformance","syntax","testing"]}
14
-
{"id":"atproto-25","title":"Implement Repository and Commit","description":"Implement repository support for AT Protocol. A repository is a signed, content-addressed collection of records organized by the MST.","design":"## Module Structure\n\n```ocaml\n(* atproto-repo/lib/commit.ml *)\ntype t = {\n did: Did.t;\n version: int; (* always 3 *)\n data: Cid.t; (* MST root *)\n rev: Tid.t;\n prev: Cid.t option;\n sig_: bytes;\n}\n\nval create : \n did:Did.t -\u003e \n data:Cid.t -\u003e \n rev:Tid.t -\u003e \n ?prev:Cid.t -\u003e \n key:K256.private_ -\u003e \n t\n\nval verify : t -\u003e public_key:K256.public -\u003e bool\nval to_dag_cbor : t -\u003e bytes\nval of_dag_cbor : bytes -\u003e (t, error) result\n\n(* atproto-repo/lib/repo.ml *)\ntype t\n\nval create : blockstore:Blockstore.t -\u003e did:Did.t -\u003e t\nval load : blockstore:Blockstore.t -\u003e root:Cid.t -\u003e t\n\nval get_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value option\nval create_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value -\u003e t\nval update_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value -\u003e t\nval delete_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e t\n\nval commit : t -\u003e key:K256.private_ -\u003e Commit.t\n```\n\n## Commit Signing Process\n\n1. Create unsigned commit (all fields except sig)\n2. Encode as DAG-CBOR\n3. SHA-256 hash the bytes\n4. Sign hash with account key (low-S!)\n5. Add signature as raw bytes\n\n## Dependencies\n- atproto-mst\n- atproto-crypto\n- atproto-syntax","acceptance_criteria":"- Commit object creation with proper v3 format\n- Commit signing with account key\n- Commit verification\n- Repository operations (create, update, delete records)","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:07.716307822+01:00","updated_at":"2025-12-28T02:25:00.961982054+01:00","closed_at":"2025-12-28T02:25:00.961982054+01:00","labels":["data","repo"],"dependencies":[{"issue_id":"atproto-25","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:22.387797246+01:00","created_by":"daemon"},{"issue_id":"atproto-25","depends_on_id":"atproto-24","type":"blocks","created_at":"2025-12-28T00:09:27.958219661+01:00","created_by":"daemon"},{"issue_id":"atproto-25","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:09:28.920614309+01:00","created_by":"daemon"}]}
15
-
{"id":"atproto-26","title":"Implement Blob handling","description":"Implement blob handling for AT Protocol. Blobs are binary data (images, videos) referenced by CID in records.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/blob.ml *)\ntype ref_ = {\n cid: Cid.t;\n mime_type: string;\n size: int;\n}\n\nval create : data:bytes -\u003e mime_type:string -\u003e ref_\nval to_dag_cbor : ref_ -\u003e Dag_cbor.value\nval of_dag_cbor : Dag_cbor.value -\u003e (ref_, error) result\n\n(* JSON representation *)\n(* { \"$type\": \"blob\", \"ref\": {\"$link\": \"...\"}, \"mimeType\": \"...\", \"size\": ... } *)\n```\n\n## Blob CID Requirements\n\n- Multicodec: `raw` (0x55), NOT dag-cbor\n- Hash: SHA-256 of raw bytes\n\n## Typed vs Untyped Blobs\n\nLegacy (untyped): just a CID link\nModern (typed): full blob object with $type\n\n## Dependencies\n- atproto-ipld","acceptance_criteria":"- Blob type encoding/decoding\n- Blob reference creation and validation\n- MIME type handling\n- Size constraints enforcement","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:09:14.976884267+01:00","updated_at":"2025-12-28T11:03:27.015943079+01:00","closed_at":"2025-12-28T11:03:27.015943079+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-26","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:23.336547933+01:00","created_by":"daemon"}]}
16
-
{"id":"atproto-30","title":"Identity Layer - DID and Handle Resolution","description":"Implement the identity layer libraries that handle DID resolution, handle resolution, and identity verification for the AT Protocol.","design":"## Packages\n\n### atproto-identity\n- DID resolution (did:plc, did:web)\n- Handle resolution (DNS TXT, HTTPS)\n- DID document parsing\n- Identity caching\n- Bidirectional verification (DID↔Handle)\n\n## Resolution Flow\n\n1. Handle → DID: DNS TXT `_atproto.\u003chandle\u003e` or HTTPS `/.well-known/atproto-did`\n2. DID → DID Document: Fetch from PLC directory or .well-known\n3. Extract: Signing key, PDS endpoint, handle\n\n## Effects-based Design\n\n```ocaml\ntype _ Effect.t +=\n | Http_get : Uri.t -\u003e string Effect.t\n | Dns_txt : string -\u003e string list Effect.t\n```\n\n## Dependencies\n- atproto-syntax\n- atproto-crypto\n- jsont or yojson","acceptance_criteria":"- DID resolution works for did:plc and did:web\n- Handle resolution via DNS TXT and HTTPS works\n- DID document parsing is complete\n- Identity verification works end-to-end","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:54.380506112+01:00","updated_at":"2025-12-28T11:57:33.145244873+01:00","closed_at":"2025-12-28T11:57:33.145244873+01:00","labels":["epic","identity"],"dependencies":[{"issue_id":"atproto-30","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:15.083956697+01:00","created_by":"daemon"}]}
17
-
{"id":"atproto-31","title":"Implement DID resolution","description":"Implement DID resolution for AT Protocol supporting did:plc and did:web methods.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/did_resolver.ml *)\ntype did_document = {\n id: Did.t;\n also_known_as: string list; (* handles *)\n verification_method: verification_method list;\n service: service list;\n}\n\nand verification_method = {\n id: string;\n type_: string;\n controller: Did.t;\n public_key_multibase: string;\n}\n\nand service = {\n id: string;\n type_: string;\n service_endpoint: Uri.t;\n}\n\ntype _ Effect.t +=\n | Http_get : Uri.t -\u003e (string, error) result Effect.t\n\nval resolve : Did.t -\u003e (did_document, error) result\nval get_signing_key : did_document -\u003e (Did_key.t, error) result\nval get_pds_endpoint : did_document -\u003e (Uri.t, error) result\nval get_handle : did_document -\u003e Handle.t option\n```\n\n## Jsont Codecs for DID Documents\n\n```ocaml\nlet verification_method_jsont : verification_method Jsont.t =\n Jsont.obj \"verification_method\" @@ fun o -\u003e\n let id = Jsont.obj_mem o \"id\" Jsont.string in\n let type_ = Jsont.obj_mem o \"type\" Jsont.string in\n let controller = Jsont.obj_mem o \"controller\" did_jsont in\n let public_key_multibase = Jsont.obj_mem o \"publicKeyMultibase\" Jsont.string in\n Jsont.obj_finish o { id; type_; controller; public_key_multibase }\n\nlet did_document_jsont : did_document Jsont.t =\n Jsont.obj \"did_document\" @@ fun o -\u003e\n let id = Jsont.obj_mem o \"id\" did_jsont in\n let also_known_as = Jsont.obj_mem o \"alsoKnownAs\" ~opt:true \n (Jsont.list Jsont.string) ~default:[] in\n let verification_method = Jsont.obj_mem o \"verificationMethod\" \n (Jsont.list verification_method_jsont) in\n let service = Jsont.obj_mem o \"service\" ~opt:true \n (Jsont.list service_jsont) ~default:[] in\n Jsont.obj_finish o { id; also_known_as; verification_method; service }\n```\n\n## Resolution Endpoints\n\n- did:plc → `https://plc.directory/\u003cdid\u003e`\n- did:web → `https://\u003cdomain\u003e/.well-known/did.json`\n\n## Effects-based Design\n\nResolution uses effects for HTTP, allowing different runtimes:\n- eio handler for testing\n- cohttp handler for production\n- mock handler for unit tests\n\n## Dependencies\n- atproto-syntax\n- atproto-crypto (for did:key parsing)\n- jsont","acceptance_criteria":"- did:plc resolution from PLC directory\n- did:web resolution from .well-known\n- DID document parsing\n- Caching with configurable TTL","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:42.738403632+01:00","updated_at":"2025-12-28T10:36:57.60764779+01:00","closed_at":"2025-12-28T10:36:57.60764779+01:00","labels":["did","identity"],"dependencies":[{"issue_id":"atproto-31","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:02.183867539+01:00","created_by":"daemon"},{"issue_id":"atproto-31","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:10:04.901673996+01:00","created_by":"daemon"},{"issue_id":"atproto-31","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:10:05.785020408+01:00","created_by":"daemon"}]}
18
-
{"id":"atproto-32","title":"Implement Handle resolution","description":"Implement handle resolution for AT Protocol. Handles are domain-based identifiers that resolve to DIDs.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/handle_resolver.ml *)\ntype _ Effect.t +=\n | Dns_txt : string -\u003e string list Effect.t\n | Http_get : Uri.t -\u003e (string, error) result Effect.t\n\nval resolve : Handle.t -\u003e (Did.t, error) result\n```\n\n## Resolution Algorithm\n\n1. Query DNS TXT record at `_atproto.\u003chandle\u003e`\n2. Look for record with `did=\u003cdid\u003e` value\n3. If no DNS record, try HTTPS: `https://\u003chandle\u003e/.well-known/atproto-did`\n4. Response should be plain text DID\n\n## Example\n\nHandle: `alice.bsky.social`\n1. DNS: `_atproto.alice.bsky.social` TXT → `did=did:plc:abc123`\n2. Or HTTPS: `https://alice.bsky.social/.well-known/atproto-did` → `did:plc:abc123`\n\n## Dependencies\n- atproto-syntax","acceptance_criteria":"- DNS TXT record resolution (_atproto.\u003chandle\u003e)\n- HTTPS fallback (/.well-known/atproto-did)\n- Handle normalization (lowercase)\n- Proper error handling for resolution failures","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:50.77787892+01:00","updated_at":"2025-12-28T10:45:02.168086436+01:00","closed_at":"2025-12-28T10:45:02.168086436+01:00","labels":["handle","identity"],"dependencies":[{"issue_id":"atproto-32","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:02.809033959+01:00","created_by":"daemon"},{"issue_id":"atproto-32","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:10:06.598127952+01:00","created_by":"daemon"}]}
19
-
{"id":"atproto-33","title":"Implement identity verification","description":"Implement bidirectional identity verification ensuring DIDs and handles are properly linked.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/identity.ml *)\ntype verified_identity = {\n did: Did.t;\n handle: Handle.t;\n signing_key: Did_key.t;\n pds_endpoint: Uri.t;\n}\n\ntype verification_error =\n | Did_resolution_failed of error\n | Handle_resolution_failed of error\n | Handle_mismatch of { expected: Handle.t; found: Handle.t option }\n | Did_mismatch of { expected: Did.t; found: Did.t }\n\nval verify_did : Did.t -\u003e (verified_identity, verification_error) result\nval verify_handle : Handle.t -\u003e (verified_identity, verification_error) result\nval verify_bidirectional : Did.t -\u003e Handle.t -\u003e (verified_identity, verification_error) result\n```\n\n## Verification Flow\n\n1. **verify_did**:\n - Resolve DID → DID document\n - Extract handle from alsoKnownAs\n - Resolve handle → DID\n - Verify DIDs match\n\n2. **verify_handle**:\n - Resolve handle → DID\n - Resolve DID → DID document\n - Verify handle in alsoKnownAs\n\n## Dependencies\n- atproto-identity (did_resolver, handle_resolver)","acceptance_criteria":"- DID→Handle verification (handle in alsoKnownAs)\n- Handle→DID verification (DID resolves correctly)\n- Bidirectional verification\n- Proper error messages for mismatches","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:09:58.806441234+01:00","updated_at":"2025-12-28T11:10:15.62066401+01:00","closed_at":"2025-12-28T11:10:15.62066401+01:00","labels":["identity","verification"],"dependencies":[{"issue_id":"atproto-33","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:03.802465302+01:00","created_by":"daemon"},{"issue_id":"atproto-33","depends_on_id":"atproto-31","type":"blocks","created_at":"2025-12-28T00:10:07.905145269+01:00","created_by":"daemon"},{"issue_id":"atproto-33","depends_on_id":"atproto-32","type":"blocks","created_at":"2025-12-28T00:10:08.46247471+01:00","created_by":"daemon"}]}
20
-
{"id":"atproto-40","title":"Network Layer - XRPC and Sync","description":"Implement the network layer libraries that handle HTTP transport (XRPC), WebSocket event streams, and repository synchronization for the AT Protocol.","design":"## Packages\n\n### atproto-xrpc\n- XRPC client (query/procedure calls)\n- XRPC server (Express-like routing)\n- Lexicon-based validation\n- Authentication (OAuth, JWT)\n- Error handling\n\n### atproto-sync\n- Event stream (WebSocket) client\n- Firehose events (#commit, #identity, #account)\n- Repository diff handling\n- Commit proof verification\n\n## XRPC Protocol\n\n- GET /xrpc/\u003cNSID\u003e for queries\n- POST /xrpc/\u003cNSID\u003e for procedures\n- JSON request/response bodies\n- Bearer token authentication\n\n## Event Stream Wire Protocol\n\n- WebSocket with binary frames\n- DAG-CBOR encoded messages\n- Header + payload structure\n\n## Dependencies\n- atproto-syntax\n- atproto-ipld\n- atproto-lexicon","acceptance_criteria":"- XRPC client can make authenticated requests\n- XRPC server can handle requests with Lexicon validation\n- Event stream (firehose) subscription works\n- Repository sync protocol works","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-28T00:07:01.661143114+01:00","updated_at":"2025-12-28T11:57:34.384344188+01:00","closed_at":"2025-12-28T11:57:34.384344188+01:00","labels":["epic","network"],"dependencies":[{"issue_id":"atproto-40","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:16.029904827+01:00","created_by":"daemon"}]}
21
-
{"id":"atproto-41","title":"Implement XRPC client","description":"Implement XRPC client for AT Protocol. XRPC is the HTTP-based API protocol used for client-server communication.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/client.ml *)\ntype t\n\ntype _ Effect.t +=\n | Http_request : request -\u003e response Effect.t\n\nand request = {\n method_: [ `GET | `POST ];\n uri: Uri.t;\n headers: (string * string) list;\n body: string option;\n}\n\nand response = {\n status: int;\n headers: (string * string) list;\n body: string;\n}\n\nval create : base_url:Uri.t -\u003e t\nval with_auth : t -\u003e token:string -\u003e t\n\nval query : \n t -\u003e \n nsid:Nsid.t -\u003e \n params:(string * string) list -\u003e \n (Jsont.json, xrpc_error) result\n\nval procedure :\n t -\u003e\n nsid:Nsid.t -\u003e\n ?params:(string * string) list -\u003e\n input:Jsont.json -\u003e\n (Jsont.json, xrpc_error) result\n\ntype xrpc_error = {\n error: string;\n message: string option;\n}\n```\n\n## Jsont Codec for XRPC Error\n\n```ocaml\nlet xrpc_error_jsont : xrpc_error Jsont.t =\n Jsont.obj \"xrpc_error\" @@ fun o -\u003e\n let error = Jsont.obj_mem o \"error\" Jsont.string in\n let message = Jsont.obj_mem o \"message\" ~opt:true Jsont.string in\n Jsont.obj_finish o { error; message }\n```\n\n## XRPC URL Structure\n\n- Query: `GET /xrpc/\u003cnsid\u003e?param1=val1\u0026param2=val2`\n- Procedure: `POST /xrpc/\u003cnsid\u003e` with JSON body\n\n## Authentication\n\nBearer token in Authorization header:\n`Authorization: Bearer \u003caccess-token\u003e`\n\n## Dependencies\n- atproto-syntax (nsid)\n- jsont","acceptance_criteria":"- Query endpoints (GET) with parameter handling\n- Procedure endpoints (POST) with JSON body\n- Authentication (Bearer token)\n- Proper error response handling\n- Lexicon-based validation","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:23.998190895+01:00","updated_at":"2025-12-28T10:32:40.042969531+01:00","closed_at":"2025-12-28T10:32:40.042969531+01:00","labels":["network","xrpc"],"dependencies":[{"issue_id":"atproto-41","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:03.65623332+01:00","created_by":"daemon"},{"issue_id":"atproto-41","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:11:08.071739524+01:00","created_by":"daemon"}]}
22
-
{"id":"atproto-42","title":"Implement XRPC server","description":"Implement XRPC server for AT Protocol. This enables building PDS and other AT Protocol services.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/server.ml *)\ntype t\ntype handler = context -\u003e (response, xrpc_error) result\n\nand context = {\n params: (string * string) list;\n input: Jsont.json option;\n auth: auth_info option;\n}\n\nand auth_info = {\n did: Did.t;\n scope: string list;\n}\n\nand response =\n | Json of Jsont.json\n | Bytes of { data: bytes; content_type: string }\n\nval create : unit -\u003e t\n\nval query : t -\u003e nsid:Nsid.t -\u003e handler -\u003e t\nval procedure : t -\u003e nsid:Nsid.t -\u003e handler -\u003e t\n\n(* Effects-based request handling *)\ntype _ Effect.t +=\n | Handle_request : request -\u003e response Effect.t\n\nval handle : t -\u003e request -\u003e response\n```\n\n## Middleware Pattern\n\n```ocaml\nval with_auth : t -\u003e (context -\u003e auth_info option) -\u003e t\nval with_validation : t -\u003e lexicons:Lexicon.registry -\u003e t\nval with_rate_limit : t -\u003e limits:rate_limit_config -\u003e t\n```\n\n## Dependencies\n- atproto-syntax\n- atproto-lexicon (for validation)\n- jsont","acceptance_criteria":"- Route registration by NSID\n- Request parameter validation\n- Response serialization\n- Error handling middleware\n- Lexicon schema validation","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:30.734032128+01:00","updated_at":"2025-12-28T11:18:17.597713348+01:00","closed_at":"2025-12-28T11:18:17.597713348+01:00","labels":["network","xrpc"],"dependencies":[{"issue_id":"atproto-42","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:04.381357087+01:00","created_by":"daemon"},{"issue_id":"atproto-42","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:08.952225305+01:00","created_by":"daemon"},{"issue_id":"atproto-42","depends_on_id":"atproto-52","type":"blocks","created_at":"2025-12-28T00:12:10.416004945+01:00","created_by":"daemon"}]}
23
-
{"id":"atproto-43","title":"Implement Firehose (event stream) client","description":"Implement event stream (firehose) client for AT Protocol. The firehose provides real-time updates from the network.","design":"## Module Structure\n\n```ocaml\n(* atproto-sync/lib/firehose.ml *)\ntype event =\n | Commit of commit_event\n | Identity of identity_event\n | Account of account_event\n\nand commit_event = {\n seq: int64;\n repo: Did.t;\n rev: Tid.t;\n since: Tid.t option;\n commit: Cid.t;\n blocks: bytes; (* CAR slice *)\n ops: operation list;\n too_big: bool;\n}\n\nand operation = {\n action: [ `Create | `Update | `Delete ];\n path: string; (* collection/rkey *)\n cid: Cid.t option;\n}\n\nand identity_event = {\n seq: int64;\n did: Did.t;\n time: Ptime.t;\n handle: Handle.t option;\n}\n\nand account_event = {\n seq: int64;\n did: Did.t;\n time: Ptime.t;\n active: bool;\n status: string option;\n}\n\ntype _ Effect.t +=\n | Websocket_connect : Uri.t -\u003e websocket Effect.t\n | Websocket_recv : websocket -\u003e bytes Effect.t\n | Websocket_close : websocket -\u003e unit Effect.t\n\nval subscribe : \n uri:Uri.t -\u003e \n ?cursor:int64 -\u003e \n (event -\u003e unit) -\u003e \n unit\n```\n\n## Wire Protocol\n\n- Binary WebSocket frames\n- Each frame: header (DAG-CBOR) + payload (DAG-CBOR)\n- Header: `{ \"op\": 1, \"t\": \"#commit\" }`\n\n## Dependencies\n- atproto-ipld (dag-cbor)\n- atproto-syntax","acceptance_criteria":"- WebSocket connection management\n- DAG-CBOR frame decoding\n- Event type dispatching (#commit, #identity, #account)\n- Cursor-based resumption\n- All firehose interop tests pass","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:42.406702551+01:00","updated_at":"2025-12-28T10:54:13.835589935+01:00","closed_at":"2025-12-28T10:54:13.835589935+01:00","labels":["network","sync"],"dependencies":[{"issue_id":"atproto-43","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:05.216684474+01:00","created_by":"daemon"},{"issue_id":"atproto-43","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:11:10.008522642+01:00","created_by":"daemon"}]}
24
-
{"id":"atproto-44","title":"Implement Repository sync","description":"Implement repository synchronization for AT Protocol. This enables PDS-to-PDS and relay sync.","design":"## Module Structure\n\n```ocaml\n(* atproto-sync/lib/repo_sync.ml *)\ntype sync_result = {\n commit: Commit.t;\n blocks: (Cid.t * bytes) list;\n}\n\nval get_repo : \n client:Xrpc.Client.t -\u003e \n did:Did.t -\u003e \n (sync_result, error) result\n\nval get_checkout :\n client:Xrpc.Client.t -\u003e\n did:Did.t -\u003e\n commit:Cid.t -\u003e\n (sync_result, error) result\n\n(* Diff handling *)\ntype diff_entry = {\n action: [ `Create | `Update | `Delete ];\n collection: Nsid.t;\n rkey: string;\n cid: Cid.t option;\n value: Dag_cbor.value option;\n}\n\nval compute_diff : \n old_commit:Cid.t -\u003e \n new_commit:Cid.t -\u003e \n blocks:(Cid.t -\u003e bytes option) -\u003e\n diff_entry list\n\nval apply_diff :\n repo:Repo.t -\u003e\n diff:diff_entry list -\u003e\n Repo.t\n```\n\n## Sync Protocol Endpoints\n\n- `com.atproto.sync.getRepo` - Full repo export\n- `com.atproto.sync.getCheckout` - Specific commit\n- `com.atproto.sync.subscribeRepos` - Real-time updates\n\n## Dependencies\n- atproto-repo\n- atproto-xrpc","acceptance_criteria":"- Repository export (getRepo)\n- Incremental sync (subscribeRepos)\n- Diff computation between commits\n- Proof verification","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:51.918242921+01:00","updated_at":"2025-12-28T11:15:00.121154336+01:00","closed_at":"2025-12-28T11:15:00.121154336+01:00","labels":["network","sync"],"dependencies":[{"issue_id":"atproto-44","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:06.164238338+01:00","created_by":"daemon"},{"issue_id":"atproto-44","depends_on_id":"atproto-25","type":"blocks","created_at":"2025-12-28T00:11:10.849151222+01:00","created_by":"daemon"},{"issue_id":"atproto-44","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:11.847570996+01:00","created_by":"daemon"}]}
25
-
{"id":"atproto-45","title":"Implement OAuth client","description":"Implement OAuth client for AT Protocol authentication. OAuth is the preferred authentication method.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/oauth.ml *)\ntype client_config = {\n client_id: string;\n redirect_uri: Uri.t;\n scope: string list;\n}\n\ntype authorization_request = {\n state: string;\n code_verifier: string; (* PKCE *)\n authorization_url: Uri.t;\n}\n\ntype tokens = {\n access_token: string;\n refresh_token: string option;\n expires_at: Ptime.t;\n scope: string list;\n}\n\nval start_authorization : \n config:client_config -\u003e \n pds:Uri.t -\u003e \n authorization_request\n\nval complete_authorization :\n config:client_config -\u003e\n code:string -\u003e\n code_verifier:string -\u003e\n (tokens, error) result\n\nval refresh_tokens :\n config:client_config -\u003e\n refresh_token:string -\u003e\n (tokens, error) result\n```\n\n## OAuth Flow\n\n1. Discover authorization server from PDS\n2. Generate PKCE code_verifier + code_challenge\n3. Redirect to authorization URL\n4. Exchange code for tokens\n5. Use access_token in Bearer header\n6. Refresh when expired\n\n## Dependencies\n- atproto-crypto (for PKCE)\n- atproto-xrpc","acceptance_criteria":"- OAuth 2.0 authorization code flow\n- PKCE support\n- Token refresh\n- DPoP (proof of possession) support","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:59.811580681+01:00","updated_at":"2025-12-28T11:24:41.399056388+01:00","closed_at":"2025-12-28T11:24:41.399056388+01:00","labels":["auth","network"],"dependencies":[{"issue_id":"atproto-45","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:07.109758394+01:00","created_by":"daemon"},{"issue_id":"atproto-45","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:12.874999712+01:00","created_by":"daemon"},{"issue_id":"atproto-45","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:11:13.692776478+01:00","created_by":"daemon"}]}
26
-
{"id":"atproto-50","title":"Application Layer - Lexicon and API","description":"Implement the application layer libraries that handle Lexicon schemas, record validation, and provide a high-level API for building AT Protocol applications.","design":"## Packages\n\n### atproto-lexicon\n- Lexicon schema parser\n- Record validation\n- XRPC param/input/output validation\n- Schema registry\n\n### atproto-lexicon-gen\n- Code generation from Lexicon schemas\n- Type-safe OCaml types\n- Encoder/decoder generation\n\n### atproto-api\n- High-level client API\n- Session management\n- RichText handling\n- Common operations (post, like, follow, etc.)\n\n## Lexicon Types\n\n- record: Repository record schemas\n- query: HTTP GET endpoints\n- procedure: HTTP POST endpoints\n- subscription: WebSocket streams\n\n## Field Types\n\n- Primitives: boolean, integer, string, bytes, cid-link\n- Containers: array, object\n- References: ref, union\n- Special: blob, unknown, token\n\n## Dependencies\n- atproto-xrpc\n- atproto-identity\n- jsont","acceptance_criteria":"- Lexicon parser handles all schema types\n- Record validation works against schemas\n- Code generation produces type-safe OCaml\n- All lexicon interop tests pass","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-28T00:07:09.195003323+01:00","updated_at":"2025-12-28T11:57:35.469581739+01:00","closed_at":"2025-12-28T11:57:35.469581739+01:00","labels":["application","epic"],"dependencies":[{"issue_id":"atproto-50","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:16.879118155+01:00","created_by":"daemon"}]}
27
-
{"id":"atproto-51","title":"Implement Lexicon schema parser","description":"Implement Lexicon schema parser for AT Protocol. Lexicon is the schema language used to define records and APIs.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon/lib/schema.ml *)\ntype lexicon = {\n lexicon: int; (* version, always 1 *)\n id: Nsid.t;\n revision: int option;\n description: string option;\n defs: (string * definition) list;\n}\n\nand definition =\n | Record of record_def\n | Query of query_def\n | Procedure of procedure_def\n | Subscription of subscription_def\n | Object of object_def\n | Array of array_def\n | Token of token_def\n | String of string_def\n (* ... *)\n\nand record_def = {\n description: string option;\n key: record_key;\n record: object_def;\n}\n\nand query_def = {\n description: string option;\n parameters: params_def option;\n output: output_def option;\n errors: error_def list;\n}\n\n(* ... full schema types ... *)\n\nval parse : Jsont.json -\u003e (lexicon, error) result\n\n(* atproto-lexicon/lib/registry.ml *)\ntype t\n\nval create : unit -\u003e t\nval add : t -\u003e lexicon -\u003e t\nval get : t -\u003e Nsid.t -\u003e lexicon option\nval get_def : t -\u003e Nsid.t -\u003e string -\u003e definition option\n```\n\n## Jsont Codecs for Lexicon Schemas\n\n```ocaml\nlet string_def_jsont : string_def Jsont.t =\n Jsont.obj \"string_def\" @@ fun o -\u003e\n let format = Jsont.obj_mem o \"format\" ~opt:true Jsont.string in\n let min_length = Jsont.obj_mem o \"minLength\" ~opt:true Jsont.int in\n let max_length = Jsont.obj_mem o \"maxLength\" ~opt:true Jsont.int in\n let min_graphemes = Jsont.obj_mem o \"minGraphemes\" ~opt:true Jsont.int in\n let max_graphemes = Jsont.obj_mem o \"maxGraphemes\" ~opt:true Jsont.int in\n let enum = Jsont.obj_mem o \"enum\" ~opt:true (Jsont.list Jsont.string) in\n let const = Jsont.obj_mem o \"const\" ~opt:true Jsont.string in\n Jsont.obj_finish o { format; min_length; max_length; min_graphemes; max_graphemes; enum; const }\n\nlet definition_jsont : definition Jsont.t =\n (* Discriminated union based on \"type\" field *)\n Jsont.obj \"definition\" @@ fun o -\u003e\n let type_ = Jsont.obj_mem o \"type\" Jsont.string in\n match type_ with\n | \"record\" -\u003e Record (decode_record_def o)\n | \"query\" -\u003e Query (decode_query_def o)\n | \"procedure\" -\u003e Procedure (decode_procedure_def o)\n | \"object\" -\u003e Object (decode_object_def o)\n | \"string\" -\u003e String (decode_string_def o)\n | _ -\u003e failwith (\"unknown definition type: \" ^ type_)\n\nlet lexicon_jsont : lexicon Jsont.t =\n Jsont.obj \"lexicon\" @@ fun o -\u003e\n let lexicon = Jsont.obj_mem o \"lexicon\" Jsont.int in\n let id = Jsont.obj_mem o \"id\" nsid_jsont in\n let revision = Jsont.obj_mem o \"revision\" ~opt:true Jsont.int in\n let description = Jsont.obj_mem o \"description\" ~opt:true Jsont.string in\n let defs = Jsont.obj_mem o \"defs\" (Jsont.obj_map definition_jsont) in\n Jsont.obj_finish o { lexicon; id; revision; description; defs }\n```\n\n## Lexicon Schema Structure\n\n```json\n{\n \"lexicon\": 1,\n \"id\": \"app.bsky.feed.post\",\n \"defs\": {\n \"main\": { \"type\": \"record\", ... },\n \"entity\": { \"type\": \"object\", ... }\n }\n}\n```\n\n## Dependencies\n- atproto-syntax\n- jsont","acceptance_criteria":"- Parse all Lexicon schema types (record, query, procedure, subscription)\n- Parse all field types (primitives, containers, refs)\n- Parse all format constraints\n- Schema registry with NSID lookup\n- All lexicon interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:28.701630723+01:00","updated_at":"2025-12-28T10:12:30.084906585+01:00","closed_at":"2025-12-28T10:12:30.084906585+01:00","labels":["application","lexicon"],"dependencies":[{"issue_id":"atproto-51","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:04.743859406+01:00","created_by":"daemon"},{"issue_id":"atproto-51","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:12:08.34127929+01:00","created_by":"daemon"}]}
28
-
{"id":"atproto-52","title":"Implement Lexicon validation","description":"Implement Lexicon-based validation for AT Protocol data. This validates records and API payloads against schemas.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon/lib/validator.ml *)\ntype validation_error = {\n path: string list;\n message: string;\n}\n\nval validate_record :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n value:Dag_cbor.value -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_params :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n params:(string * string) list -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_input :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n input:Jsont.json -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_output :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n output:Jsont.json -\u003e\n (unit, validation_error list) result\n```\n\n## Constraint Types\n\n- **String**: minLength, maxLength, minGraphemes, maxGraphemes, format, enum, const\n- **Integer**: minimum, maximum, enum, const\n- **Bytes**: minLength, maxLength\n- **Array**: minLength, maxLength, items type\n- **Blob**: maxSize, accept (MIME types)\n- **Union**: open/closed, refs\n\n## Format Validators (Parser-based, NO REGEX)\n\nEach format has a dedicated parser module:\n\n```ocaml\n(* atproto-lexicon/lib/formats.ml *)\n\nlet validate_did s = Did.of_string s |\u003e Result.is_ok\nlet validate_handle s = Handle.of_string s |\u003e Result.is_ok\nlet validate_nsid s = Nsid.of_string s |\u003e Result.is_ok\nlet validate_tid s = Tid.of_string s |\u003e Result.is_ok\nlet validate_cid s = Cid.of_string s |\u003e Result.is_ok\nlet validate_at_uri s = At_uri.of_string s |\u003e Result.is_ok\nlet validate_at_identifier s = \n Did.of_string s |\u003e Result.is_ok || Handle.of_string s |\u003e Result.is_ok\nlet validate_record_key s = Record_key.of_string s |\u003e Result.is_ok\n\nlet validate_datetime s =\n (* Hand-written RFC-3339 parser *)\n parse_datetime s |\u003e Result.is_ok\n\nlet validate_language s =\n (* BCP-47 language tag parser *)\n parse_language_tag s |\u003e Result.is_ok\n\nlet validate_uri s =\n (* RFC-3986 URI parser *)\n Uri.of_string s |\u003e Option.is_some\n```\n\n## Dependencies\n- atproto-lexicon (schema)\n- atproto-syntax (format validators)\n- jsont","acceptance_criteria":"- Validate records against schemas\n- Validate XRPC params, input, output\n- Proper error messages with paths\n- All constraint types supported\n- All record-data interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:39.125440686+01:00","updated_at":"2025-12-28T10:25:46.671434007+01:00","closed_at":"2025-12-28T10:25:46.671434007+01:00","labels":["application","lexicon"],"dependencies":[{"issue_id":"atproto-52","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:05.375287273+01:00","created_by":"daemon"},{"issue_id":"atproto-52","depends_on_id":"atproto-51","type":"blocks","created_at":"2025-12-28T00:12:09.479940241+01:00","created_by":"daemon"}]}
29
-
{"id":"atproto-53","title":"Implement Lexicon code generation","description":"Implement code generation from Lexicon schemas to OCaml types and API bindings.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon-gen/lib/codegen.ml *)\ntype config = {\n output_dir: string;\n module_prefix: string;\n}\n\nval generate_types : config:config -\u003e lexicon:Lexicon.t -\u003e unit\nval generate_client : config:config -\u003e lexicons:Lexicon.t list -\u003e unit\n```\n\n## Generated Code Example\n\nInput Lexicon:\n```json\n{\n \"id\": \"app.bsky.feed.post\",\n \"defs\": {\n \"main\": {\n \"type\": \"record\",\n \"record\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": { \"type\": \"string\", \"maxGraphemes\": 300 },\n \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n }\n }\n }\n }\n}\n```\n\nGenerated OCaml:\n```ocaml\nmodule App_bsky_feed_post = struct\n type t = {\n text: string;\n created_at: Ptime.t;\n }\n \n let jsont : t Jsont.t =\n Jsont.obj \"app.bsky.feed.post\" @@ fun o -\u003e\n let text = Jsont.obj_mem o \"text\" Jsont.string in\n let created_at = Jsont.obj_mem o \"createdAt\" Datetime.jsont in\n Jsont.obj_finish o { text; created_at }\n \n val to_dag_cbor : t -\u003e Dag_cbor.value\n val of_dag_cbor : Dag_cbor.value -\u003e (t, error) result\nend\n```\n\n## CLI Tool\n\n```bash\natproto-lexicon-gen --input lexicons/ --output lib/generated/\n```\n\n## Dependencies\n- atproto-lexicon\n- jsont","acceptance_criteria":"- Generate OCaml types from Lexicon schemas\n- Generate encoders/decoders\n- Type-safe API bindings\n- CLI tool for code generation","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:47.861552784+01:00","updated_at":"2025-12-28T11:28:03.226633204+01:00","closed_at":"2025-12-28T11:28:03.226633204+01:00","labels":["application","codegen"],"dependencies":[{"issue_id":"atproto-53","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:06.539440409+01:00","created_by":"daemon"},{"issue_id":"atproto-53","depends_on_id":"atproto-51","type":"blocks","created_at":"2025-12-28T00:12:11.189125052+01:00","created_by":"daemon"}]}
30
-
{"id":"atproto-54","title":"Implement high-level API client","description":"Implement high-level API client for AT Protocol / Bluesky. This provides a user-friendly interface for common operations.","design":"## Module Structure\n\n```ocaml\n(* atproto-api/lib/agent.ml *)\ntype t\n\nval create : pds:Uri.t -\u003e t\n\n(* Authentication *)\nval login : t -\u003e identifier:string -\u003e password:string -\u003e (t, error) result\nval login_oauth : t -\u003e tokens:Oauth.tokens -\u003e t\nval refresh_session : t -\u003e (t, error) result\n\n(* Profile *)\nval get_profile : t -\u003e actor:string -\u003e (profile, error) result\nval update_profile : t -\u003e display_name:string option -\u003e ... -\u003e (unit, error) result\n\n(* Posts *)\nval create_post : t -\u003e text:string -\u003e ?reply:reply_ref -\u003e ... -\u003e (post_ref, error) result\nval delete_post : t -\u003e uri:At_uri.t -\u003e (unit, error) result\n\n(* Social *)\nval like : t -\u003e uri:At_uri.t -\u003e cid:Cid.t -\u003e (like_ref, error) result\nval follow : t -\u003e did:Did.t -\u003e (follow_ref, error) result\nval unfollow : t -\u003e uri:At_uri.t -\u003e (unit, error) result\n\n(* Feed *)\nval get_timeline : t -\u003e ?cursor:string -\u003e ?limit:int -\u003e (timeline, error) result\nval get_author_feed : t -\u003e actor:string -\u003e ... -\u003e (feed, error) result\n\n(* atproto-api/lib/richtext.ml *)\ntype t\n\nval create : string -\u003e t\nval detect_facets : t -\u003e t (* auto-detect mentions, links *)\nval add_mention : t -\u003e start:int -\u003e end_:int -\u003e did:Did.t -\u003e t\nval add_link : t -\u003e start:int -\u003e end_:int -\u003e uri:Uri.t -\u003e t\nval to_post_record : t -\u003e Dag_cbor.value\n```\n\n## Jsont Codecs for API Types\n\n```ocaml\nlet profile_jsont : profile Jsont.t =\n Jsont.obj \"profile\" @@ fun o -\u003e\n let did = Jsont.obj_mem o \"did\" did_jsont in\n let handle = Jsont.obj_mem o \"handle\" handle_jsont in\n let display_name = Jsont.obj_mem o \"displayName\" ~opt:true Jsont.string in\n let description = Jsont.obj_mem o \"description\" ~opt:true Jsont.string in\n let avatar = Jsont.obj_mem o \"avatar\" ~opt:true Jsont.string in\n let followers_count = Jsont.obj_mem o \"followersCount\" ~opt:true Jsont.int in\n let follows_count = Jsont.obj_mem o \"followsCount\" ~opt:true Jsont.int in\n let posts_count = Jsont.obj_mem o \"postsCount\" ~opt:true Jsont.int in\n Jsont.obj_finish o { did; handle; display_name; description; avatar; \n followers_count; follows_count; posts_count }\n\nlet facet_jsont : facet Jsont.t =\n Jsont.obj \"facet\" @@ fun o -\u003e\n let index = Jsont.obj_mem o \"index\" byte_slice_jsont in\n let features = Jsont.obj_mem o \"features\" (Jsont.list facet_feature_jsont) in\n Jsont.obj_finish o { index; features }\n```\n\n## RichText Facets\n\n```json\n{\n \"text\": \"Hello @alice.bsky.social!\",\n \"facets\": [\n {\n \"index\": { \"byteStart\": 6, \"byteEnd\": 25 },\n \"features\": [\n { \"$type\": \"app.bsky.richtext.facet#mention\", \"did\": \"did:plc:...\" }\n ]\n }\n ]\n}\n```\n\n## Dependencies\n- atproto-xrpc\n- atproto-identity\n- atproto-repo\n- jsont","acceptance_criteria":"- Session management (login, logout, refresh)\n- Common operations (post, like, follow, etc.)\n- RichText handling (mentions, links, facets)\n- Timeline and feed fetching\n- Profile operations","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:12:00.736309435+01:00","updated_at":"2025-12-28T11:47:47.071271001+01:00","closed_at":"2025-12-28T11:47:47.071271001+01:00","labels":["api","application"],"dependencies":[{"issue_id":"atproto-54","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:07.636789403+01:00","created_by":"daemon"},{"issue_id":"atproto-54","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:12:12.376875324+01:00","created_by":"daemon"},{"issue_id":"atproto-54","depends_on_id":"atproto-33","type":"blocks","created_at":"2025-12-28T00:12:13.060557136+01:00","created_by":"daemon"},{"issue_id":"atproto-54","depends_on_id":"atproto-25","type":"blocks","created_at":"2025-12-28T00:12:13.934360048+01:00","created_by":"daemon"}]}
31
-
{"id":"atproto-5l1","title":"Refactor JSON to simdjsont (replace yojson/jsont)","description":"","notes":"Migrated atproto-lexicon off Yojson onto simdjsont.Json.t + Simdjsont.decode Codec.value. Updated lib/lexicon/{parser,validator,atproto_lexicon,codegen} and lib/lexicon/dune. Updated test/lexicon/test_lexicon.ml fixtures loader + patterns. Verified: dune runtest test/lexicon OK; dune build @install OK.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-01T18:06:10.17746938+01:00","updated_at":"2026-01-01T20:42:14.454112697+01:00","closed_at":"2026-01-01T20:42:14.454112697+01:00","dependencies":[{"issue_id":"atproto-5l1","depends_on_id":"atproto-dqs","type":"blocks","created_at":"2026-01-01T18:06:39.648046004+01:00","created_by":"daemon"}]}
32
-
{"id":"atproto-60","title":"Implement effects-based I/O abstraction","description":"Implement the effects-based I/O abstraction layer that makes all libraries runtime-agnostic.","design":"## Module Structure\n\n```ocaml\n(* atproto-effects/lib/effects.ml *)\n\n(* HTTP effects *)\ntype http_request = {\n method_: [ `GET | `POST | `PUT | `DELETE ];\n uri: Uri.t;\n headers: (string * string) list;\n body: string option;\n}\n\ntype http_response = {\n status: int;\n headers: (string * string) list;\n body: string;\n}\n\ntype _ Effect.t +=\n | Http_request : http_request -\u003e http_response Effect.t\n\n(* DNS effects *)\ntype _ Effect.t +=\n | Dns_txt : string -\u003e string list Effect.t\n | Dns_a : string -\u003e string list Effect.t\n\n(* Time effects *)\ntype _ Effect.t +=\n | Now : Ptime.t Effect.t\n | Sleep : float -\u003e unit Effect.t\n\n(* Random effects *)\ntype _ Effect.t +=\n | Random_bytes : int -\u003e bytes Effect.t\n\n(* atproto-effects-eio/lib/handler.ml *)\nval run : (unit -\u003e 'a) -\u003e 'a\n```\n\n## Handler Example (eio)\n\n```ocaml\nlet run f =\n Effect.Deep.match_ f ()\n {\n retc = Fun.id;\n exnc = raise;\n effc = fun (type a) (e : a Effect.t) -\u003e\n match e with\n | Http_request req -\u003e\n Some (fun (k : (a, _) continuation) -\u003e\n let resp = Eio_client.request req in\n continue k resp)\n | Dns_txt domain -\u003e\n Some (fun k -\u003e\n let records = Eio_dns.txt domain in\n continue k records)\n | _ -\u003e None\n }\n```\n\n## Dependencies\n- eio (for testing handler)","acceptance_criteria":"- Effect types for HTTP, DNS, time, random\n- eio-based handler for testing\n- Handler composition utilities\n- Performance benchmarks","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:12:29.021401617+01:00","updated_at":"2025-12-28T11:57:08.264086142+01:00","closed_at":"2025-12-28T11:57:08.264086142+01:00","labels":["effects","infrastructure"],"dependencies":[{"issue_id":"atproto-60","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:55.467983208+01:00","created_by":"daemon"}]}
33
-
{"id":"atproto-61","title":"Set up interoperability test suite","description":"Set up and run the AT Protocol interoperability tests from bluesky-social/atproto-interop-tests.","design":"## Test Structure\n\n```\ntest/\n├── interop/\n│ ├── syntax_test.ml # Handle, DID, NSID, TID, etc.\n│ ├── crypto_test.ml # Signatures, did:key\n│ ├── data_model_test.ml # DAG-CBOR, CID\n│ ├── mst_test.ml # Key heights, tree structure\n│ ├── lexicon_test.ml # Schema and record validation\n│ └── firehose_test.ml # Commit proofs\n├── fixtures/ # Cloned from atproto-interop-tests\n└── dune\n```\n\n## Test Approach\n\n1. Clone test vectors from GitHub\n2. Parse JSON fixtures using jsont\n3. Parse text fixtures line by line\n4. Run each test case\n5. Compare output to expected values\n\n## Example Test\n\n```ocaml\nlet load_json_fixtures path =\n let json = Jsont.of_file path in\n Jsont.decode (Jsont.list fixture_jsont) json\n\nlet%test \"handle_syntax_valid\" =\n let fixtures = load_lines \"fixtures/syntax/handle_syntax_valid.txt\" in\n List.for_all (fun line -\u003e\n match Handle.of_string line with\n | Ok _ -\u003e true\n | Error _ -\u003e false\n ) fixtures\n\nlet%test \"handle_syntax_invalid\" =\n let fixtures = load_lines \"fixtures/syntax/handle_syntax_invalid.txt\" in\n List.for_all (fun line -\u003e\n match Handle.of_string line with\n | Ok _ -\u003e false\n | Error _ -\u003e true\n ) fixtures\n\nlet%test \"crypto_signature_fixtures\" =\n let fixtures = load_json_fixtures \"fixtures/crypto/signature-fixtures.json\" in\n List.for_all (fun fixture -\u003e\n let message = Base64.decode fixture.message_base64 in\n let signature = Base64.decode fixture.signature_base64 in\n let key = Did_key.of_string fixture.public_key_did in\n let result = Crypto.verify key message signature in\n result = fixture.valid_signature\n ) fixtures\n```\n\n## Dependencies\n- alcotest or ounit2\n- jsont","acceptance_criteria":"- All syntax interop tests pass\n- All crypto interop tests pass\n- All data-model interop tests pass\n- All MST interop tests pass\n- All lexicon interop tests pass\n- All firehose interop tests pass","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T00:12:40.553908313+01:00","updated_at":"2025-12-28T13:25:34.614867702+01:00","closed_at":"2025-12-28T13:25:34.614867702+01:00","labels":["conformance","testing"],"dependencies":[{"issue_id":"atproto-61","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:56.180809368+01:00","created_by":"daemon"}]}
34
-
{"id":"atproto-62","title":"Set up monorepo package structure","description":"Set up the monorepo structure for multiple opam packages within a single repository.","design":"## Repository Structure\n\n```\natproto/\n├── dune-project # Root with all packages\n├── packages/\n│ ├── atproto-syntax/\n│ │ ├── lib/\n│ │ │ ├── dune\n│ │ │ └── *.ml\n│ │ ├── test/\n│ │ │ ├── dune\n│ │ │ └── *_test.ml\n│ │ └── atproto-syntax.opam\n│ ├── atproto-crypto/\n│ ├── atproto-multibase/\n│ ├── atproto-ipld/\n│ ├── atproto-mst/\n│ ├── atproto-repo/\n│ ├── atproto-identity/\n│ ├── atproto-xrpc/\n│ ├── atproto-sync/\n│ ├── atproto-lexicon/\n│ ├── atproto-lexicon-gen/\n│ ├── atproto-api/\n│ └── atproto-effects/\n├── examples/\n│ ├── simple_client/\n│ └── firehose_consumer/\n└── interop-tests/\n```\n\n## dune-project\n\n```lisp\n(lang dune 3.20)\n(name atproto)\n(generate_opam_files true)\n\n(package\n (name atproto-syntax)\n (synopsis \"AT Protocol identifier syntax parsing\")\n (depends\n (ocaml (\u003e= 5.4))\n re\n ptime))\n\n(package\n (name atproto-crypto)\n ...)\n```\n\n## CI (.github/workflows/ci.yml)\n\n- OCaml 5.4 matrix\n- Build all packages\n- Run all tests\n- Run interop tests","acceptance_criteria":"- Multi-package dune-project structure\n- Separate opam files per package\n- CI pipeline for building and testing\n- Documentation generation setup","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T00:12:50.547102438+01:00","updated_at":"2025-12-28T11:57:18.856810633+01:00","closed_at":"2025-12-28T11:57:18.856810633+01:00","labels":["infrastructure","setup"],"dependencies":[{"issue_id":"atproto-62","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:57.015938611+01:00","created_by":"daemon"}]}
35
-
{"id":"atproto-8pf","title":"Migrate lib/api JSON parsing from yojson to simdjson","description":"","acceptance_criteria":"Build passes (dune build @install). lib/api no longer depends on yojson (lib/api/dune depends on simdjsont). All lib/api JSON encode/decode uses Atproto_xrpc.Client.json (Simdjsont.Json.t). API tests updated and passing (dune runtest test/api). No remaining yojson polymorphic variants in lib/api and test/api.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T20:55:46.43041578+01:00","updated_at":"2026-01-01T21:02:47.086579243+01:00","closed_at":"2026-01-01T21:02:47.086579243+01:00"}
36
-
{"id":"atproto-bsh","title":"Switch atproto-xrpc to use atproto-json wrapper instead of Simdjsont","description":"","status":"closed","priority":1,"issue_type":"task","assignee":"gdiazlo","created_at":"2026-01-01T21:08:22.228905638+01:00","updated_at":"2026-01-01T21:10:59.451211971+01:00","closed_at":"2026-01-01T21:10:59.451211971+01:00"}
14
+
{"id":"atproto-25","title":"Implement Repository and Commit","description":"Implement repository support for AT Protocol. A repository is a signed, content-addressed collection of records organized by the MST.","design":"## Module Structure\n\n```ocaml\n(* atproto-repo/lib/commit.ml *)\ntype t = {\n did: Did.t;\n version: int; (* always 3 *)\n data: Cid.t; (* MST root *)\n rev: Tid.t;\n prev: Cid.t option;\n sig_: bytes;\n}\n\nval create : \n did:Did.t -\u003e \n data:Cid.t -\u003e \n rev:Tid.t -\u003e \n ?prev:Cid.t -\u003e \n key:K256.private_ -\u003e \n t\n\nval verify : t -\u003e public_key:K256.public -\u003e bool\nval to_dag_cbor : t -\u003e bytes\nval of_dag_cbor : bytes -\u003e (t, error) result\n\n(* atproto-repo/lib/repo.ml *)\ntype t\n\nval create : blockstore:Blockstore.t -\u003e did:Did.t -\u003e t\nval load : blockstore:Blockstore.t -\u003e root:Cid.t -\u003e t\n\nval get_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value option\nval create_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value -\u003e t\nval update_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e Dag_cbor.value -\u003e t\nval delete_record : t -\u003e collection:Nsid.t -\u003e rkey:string -\u003e t\n\nval commit : t -\u003e key:K256.private_ -\u003e Commit.t\n```\n\n## Commit Signing Process\n\n1. Create unsigned commit (all fields except sig)\n2. Encode as DAG-CBOR\n3. SHA-256 hash the bytes\n4. Sign hash with account key (low-S!)\n5. Add signature as raw bytes\n\n## Dependencies\n- atproto-mst\n- atproto-crypto\n- atproto-syntax","acceptance_criteria":"- Commit object creation with proper v3 format\n- Commit signing with account key\n- Commit verification\n- Repository operations (create, update, delete records)","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:07.716307822+01:00","updated_at":"2025-12-28T02:25:00.961982054+01:00","closed_at":"2025-12-28T02:25:00.961982054+01:00","labels":["data","repo"],"dependencies":[{"issue_id":"atproto-25","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:22.387797246+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-25","depends_on_id":"atproto-24","type":"blocks","created_at":"2025-12-28T00:09:27.958219661+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-25","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:09:28.920614309+01:00","created_by":"daemon","metadata":"{}"}]}
15
+
{"id":"atproto-26","title":"Implement Blob handling","description":"Implement blob handling for AT Protocol. Blobs are binary data (images, videos) referenced by CID in records.","design":"## Module Structure\n\n```ocaml\n(* atproto-ipld/lib/blob.ml *)\ntype ref_ = {\n cid: Cid.t;\n mime_type: string;\n size: int;\n}\n\nval create : data:bytes -\u003e mime_type:string -\u003e ref_\nval to_dag_cbor : ref_ -\u003e Dag_cbor.value\nval of_dag_cbor : Dag_cbor.value -\u003e (ref_, error) result\n\n(* JSON representation *)\n(* { \"$type\": \"blob\", \"ref\": {\"$link\": \"...\"}, \"mimeType\": \"...\", \"size\": ... } *)\n```\n\n## Blob CID Requirements\n\n- Multicodec: `raw` (0x55), NOT dag-cbor\n- Hash: SHA-256 of raw bytes\n\n## Typed vs Untyped Blobs\n\nLegacy (untyped): just a CID link\nModern (typed): full blob object with $type\n\n## Dependencies\n- atproto-ipld","acceptance_criteria":"- Blob type encoding/decoding\n- Blob reference creation and validation\n- MIME type handling\n- Size constraints enforcement","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:09:14.976884267+01:00","updated_at":"2025-12-28T11:03:27.015943079+01:00","closed_at":"2025-12-28T11:03:27.015943079+01:00","labels":["data","ipld"],"dependencies":[{"issue_id":"atproto-26","depends_on_id":"atproto-20","type":"parent-child","created_at":"2025-12-28T00:09:23.336547933+01:00","created_by":"daemon","metadata":"{}"}]}
16
+
{"id":"atproto-30","title":"Identity Layer - DID and Handle Resolution","description":"Implement the identity layer libraries that handle DID resolution, handle resolution, and identity verification for the AT Protocol.","design":"## Packages\n\n### atproto-identity\n- DID resolution (did:plc, did:web)\n- Handle resolution (DNS TXT, HTTPS)\n- DID document parsing\n- Identity caching\n- Bidirectional verification (DID↔Handle)\n\n## Resolution Flow\n\n1. Handle → DID: DNS TXT `_atproto.\u003chandle\u003e` or HTTPS `/.well-known/atproto-did`\n2. DID → DID Document: Fetch from PLC directory or .well-known\n3. Extract: Signing key, PDS endpoint, handle\n\n## Effects-based Design\n\n```ocaml\ntype _ Effect.t +=\n | Http_get : Uri.t -\u003e string Effect.t\n | Dns_txt : string -\u003e string list Effect.t\n```\n\n## Dependencies\n- atproto-syntax\n- atproto-crypto\n- jsont or yojson","acceptance_criteria":"- DID resolution works for did:plc and did:web\n- Handle resolution via DNS TXT and HTTPS works\n- DID document parsing is complete\n- Identity verification works end-to-end","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-12-28T00:06:54.380506112+01:00","updated_at":"2025-12-28T11:57:33.145244873+01:00","closed_at":"2025-12-28T11:57:33.145244873+01:00","labels":["epic","identity"],"dependencies":[{"issue_id":"atproto-30","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:15.083956697+01:00","created_by":"daemon","metadata":"{}"}]}
17
+
{"id":"atproto-31","title":"Implement DID resolution","description":"Implement DID resolution for AT Protocol supporting did:plc and did:web methods.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/did_resolver.ml *)\ntype did_document = {\n id: Did.t;\n also_known_as: string list; (* handles *)\n verification_method: verification_method list;\n service: service list;\n}\n\nand verification_method = {\n id: string;\n type_: string;\n controller: Did.t;\n public_key_multibase: string;\n}\n\nand service = {\n id: string;\n type_: string;\n service_endpoint: Uri.t;\n}\n\ntype _ Effect.t +=\n | Http_get : Uri.t -\u003e (string, error) result Effect.t\n\nval resolve : Did.t -\u003e (did_document, error) result\nval get_signing_key : did_document -\u003e (Did_key.t, error) result\nval get_pds_endpoint : did_document -\u003e (Uri.t, error) result\nval get_handle : did_document -\u003e Handle.t option\n```\n\n## Jsont Codecs for DID Documents\n\n```ocaml\nlet verification_method_jsont : verification_method Jsont.t =\n Jsont.obj \"verification_method\" @@ fun o -\u003e\n let id = Jsont.obj_mem o \"id\" Jsont.string in\n let type_ = Jsont.obj_mem o \"type\" Jsont.string in\n let controller = Jsont.obj_mem o \"controller\" did_jsont in\n let public_key_multibase = Jsont.obj_mem o \"publicKeyMultibase\" Jsont.string in\n Jsont.obj_finish o { id; type_; controller; public_key_multibase }\n\nlet did_document_jsont : did_document Jsont.t =\n Jsont.obj \"did_document\" @@ fun o -\u003e\n let id = Jsont.obj_mem o \"id\" did_jsont in\n let also_known_as = Jsont.obj_mem o \"alsoKnownAs\" ~opt:true \n (Jsont.list Jsont.string) ~default:[] in\n let verification_method = Jsont.obj_mem o \"verificationMethod\" \n (Jsont.list verification_method_jsont) in\n let service = Jsont.obj_mem o \"service\" ~opt:true \n (Jsont.list service_jsont) ~default:[] in\n Jsont.obj_finish o { id; also_known_as; verification_method; service }\n```\n\n## Resolution Endpoints\n\n- did:plc → `https://plc.directory/\u003cdid\u003e`\n- did:web → `https://\u003cdomain\u003e/.well-known/did.json`\n\n## Effects-based Design\n\nResolution uses effects for HTTP, allowing different runtimes:\n- eio handler for testing\n- cohttp handler for production\n- mock handler for unit tests\n\n## Dependencies\n- atproto-syntax\n- atproto-crypto (for did:key parsing)\n- jsont","acceptance_criteria":"- did:plc resolution from PLC directory\n- did:web resolution from .well-known\n- DID document parsing\n- Caching with configurable TTL","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:42.738403632+01:00","updated_at":"2025-12-28T10:36:57.60764779+01:00","closed_at":"2025-12-28T10:36:57.60764779+01:00","labels":["did","identity"],"dependencies":[{"issue_id":"atproto-31","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:02.183867539+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-31","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:10:04.901673996+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-31","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:10:05.785020408+01:00","created_by":"daemon","metadata":"{}"}]}
18
+
{"id":"atproto-32","title":"Implement Handle resolution","description":"Implement handle resolution for AT Protocol. Handles are domain-based identifiers that resolve to DIDs.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/handle_resolver.ml *)\ntype _ Effect.t +=\n | Dns_txt : string -\u003e string list Effect.t\n | Http_get : Uri.t -\u003e (string, error) result Effect.t\n\nval resolve : Handle.t -\u003e (Did.t, error) result\n```\n\n## Resolution Algorithm\n\n1. Query DNS TXT record at `_atproto.\u003chandle\u003e`\n2. Look for record with `did=\u003cdid\u003e` value\n3. If no DNS record, try HTTPS: `https://\u003chandle\u003e/.well-known/atproto-did`\n4. Response should be plain text DID\n\n## Example\n\nHandle: `alice.bsky.social`\n1. DNS: `_atproto.alice.bsky.social` TXT → `did=did:plc:abc123`\n2. Or HTTPS: `https://alice.bsky.social/.well-known/atproto-did` → `did:plc:abc123`\n\n## Dependencies\n- atproto-syntax","acceptance_criteria":"- DNS TXT record resolution (_atproto.\u003chandle\u003e)\n- HTTPS fallback (/.well-known/atproto-did)\n- Handle normalization (lowercase)\n- Proper error handling for resolution failures","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:09:50.77787892+01:00","updated_at":"2025-12-28T10:45:02.168086436+01:00","closed_at":"2025-12-28T10:45:02.168086436+01:00","labels":["handle","identity"],"dependencies":[{"issue_id":"atproto-32","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:02.809033959+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-32","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:10:06.598127952+01:00","created_by":"daemon","metadata":"{}"}]}
19
+
{"id":"atproto-33","title":"Implement identity verification","description":"Implement bidirectional identity verification ensuring DIDs and handles are properly linked.","design":"## Module Structure\n\n```ocaml\n(* atproto-identity/lib/identity.ml *)\ntype verified_identity = {\n did: Did.t;\n handle: Handle.t;\n signing_key: Did_key.t;\n pds_endpoint: Uri.t;\n}\n\ntype verification_error =\n | Did_resolution_failed of error\n | Handle_resolution_failed of error\n | Handle_mismatch of { expected: Handle.t; found: Handle.t option }\n | Did_mismatch of { expected: Did.t; found: Did.t }\n\nval verify_did : Did.t -\u003e (verified_identity, verification_error) result\nval verify_handle : Handle.t -\u003e (verified_identity, verification_error) result\nval verify_bidirectional : Did.t -\u003e Handle.t -\u003e (verified_identity, verification_error) result\n```\n\n## Verification Flow\n\n1. **verify_did**:\n - Resolve DID → DID document\n - Extract handle from alsoKnownAs\n - Resolve handle → DID\n - Verify DIDs match\n\n2. **verify_handle**:\n - Resolve handle → DID\n - Resolve DID → DID document\n - Verify handle in alsoKnownAs\n\n## Dependencies\n- atproto-identity (did_resolver, handle_resolver)","acceptance_criteria":"- DID→Handle verification (handle in alsoKnownAs)\n- Handle→DID verification (DID resolves correctly)\n- Bidirectional verification\n- Proper error messages for mismatches","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T00:09:58.806441234+01:00","updated_at":"2025-12-28T11:10:15.62066401+01:00","closed_at":"2025-12-28T11:10:15.62066401+01:00","labels":["identity","verification"],"dependencies":[{"issue_id":"atproto-33","depends_on_id":"atproto-30","type":"parent-child","created_at":"2025-12-28T00:10:03.802465302+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-33","depends_on_id":"atproto-31","type":"blocks","created_at":"2025-12-28T00:10:07.905145269+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-33","depends_on_id":"atproto-32","type":"blocks","created_at":"2025-12-28T00:10:08.46247471+01:00","created_by":"daemon","metadata":"{}"}]}
20
+
{"id":"atproto-40","title":"Network Layer - XRPC and Sync","description":"Implement the network layer libraries that handle HTTP transport (XRPC), WebSocket event streams, and repository synchronization for the AT Protocol.","design":"## Packages\n\n### atproto-xrpc\n- XRPC client (query/procedure calls)\n- XRPC server (Express-like routing)\n- Lexicon-based validation\n- Authentication (OAuth, JWT)\n- Error handling\n\n### atproto-sync\n- Event stream (WebSocket) client\n- Firehose events (#commit, #identity, #account)\n- Repository diff handling\n- Commit proof verification\n\n## XRPC Protocol\n\n- GET /xrpc/\u003cNSID\u003e for queries\n- POST /xrpc/\u003cNSID\u003e for procedures\n- JSON request/response bodies\n- Bearer token authentication\n\n## Event Stream Wire Protocol\n\n- WebSocket with binary frames\n- DAG-CBOR encoded messages\n- Header + payload structure\n\n## Dependencies\n- atproto-syntax\n- atproto-ipld\n- atproto-lexicon","acceptance_criteria":"- XRPC client can make authenticated requests\n- XRPC server can handle requests with Lexicon validation\n- Event stream (firehose) subscription works\n- Repository sync protocol works","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-28T00:07:01.661143114+01:00","updated_at":"2025-12-28T11:57:34.384344188+01:00","closed_at":"2025-12-28T11:57:34.384344188+01:00","labels":["epic","network"],"dependencies":[{"issue_id":"atproto-40","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:16.029904827+01:00","created_by":"daemon","metadata":"{}"}]}
21
+
{"id":"atproto-41","title":"Implement XRPC client","description":"Implement XRPC client for AT Protocol. XRPC is the HTTP-based API protocol used for client-server communication.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/client.ml *)\ntype t\n\ntype _ Effect.t +=\n | Http_request : request -\u003e response Effect.t\n\nand request = {\n method_: [ `GET | `POST ];\n uri: Uri.t;\n headers: (string * string) list;\n body: string option;\n}\n\nand response = {\n status: int;\n headers: (string * string) list;\n body: string;\n}\n\nval create : base_url:Uri.t -\u003e t\nval with_auth : t -\u003e token:string -\u003e t\n\nval query : \n t -\u003e \n nsid:Nsid.t -\u003e \n params:(string * string) list -\u003e \n (Jsont.json, xrpc_error) result\n\nval procedure :\n t -\u003e\n nsid:Nsid.t -\u003e\n ?params:(string * string) list -\u003e\n input:Jsont.json -\u003e\n (Jsont.json, xrpc_error) result\n\ntype xrpc_error = {\n error: string;\n message: string option;\n}\n```\n\n## Jsont Codec for XRPC Error\n\n```ocaml\nlet xrpc_error_jsont : xrpc_error Jsont.t =\n Jsont.obj \"xrpc_error\" @@ fun o -\u003e\n let error = Jsont.obj_mem o \"error\" Jsont.string in\n let message = Jsont.obj_mem o \"message\" ~opt:true Jsont.string in\n Jsont.obj_finish o { error; message }\n```\n\n## XRPC URL Structure\n\n- Query: `GET /xrpc/\u003cnsid\u003e?param1=val1\u0026param2=val2`\n- Procedure: `POST /xrpc/\u003cnsid\u003e` with JSON body\n\n## Authentication\n\nBearer token in Authorization header:\n`Authorization: Bearer \u003caccess-token\u003e`\n\n## Dependencies\n- atproto-syntax (nsid)\n- jsont","acceptance_criteria":"- Query endpoints (GET) with parameter handling\n- Procedure endpoints (POST) with JSON body\n- Authentication (Bearer token)\n- Proper error response handling\n- Lexicon-based validation","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:23.998190895+01:00","updated_at":"2025-12-28T10:32:40.042969531+01:00","closed_at":"2025-12-28T10:32:40.042969531+01:00","labels":["network","xrpc"],"dependencies":[{"issue_id":"atproto-41","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:03.65623332+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-41","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:11:08.071739524+01:00","created_by":"daemon","metadata":"{}"}]}
22
+
{"id":"atproto-42","title":"Implement XRPC server","description":"Implement XRPC server for AT Protocol. This enables building PDS and other AT Protocol services.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/server.ml *)\ntype t\ntype handler = context -\u003e (response, xrpc_error) result\n\nand context = {\n params: (string * string) list;\n input: Jsont.json option;\n auth: auth_info option;\n}\n\nand auth_info = {\n did: Did.t;\n scope: string list;\n}\n\nand response =\n | Json of Jsont.json\n | Bytes of { data: bytes; content_type: string }\n\nval create : unit -\u003e t\n\nval query : t -\u003e nsid:Nsid.t -\u003e handler -\u003e t\nval procedure : t -\u003e nsid:Nsid.t -\u003e handler -\u003e t\n\n(* Effects-based request handling *)\ntype _ Effect.t +=\n | Handle_request : request -\u003e response Effect.t\n\nval handle : t -\u003e request -\u003e response\n```\n\n## Middleware Pattern\n\n```ocaml\nval with_auth : t -\u003e (context -\u003e auth_info option) -\u003e t\nval with_validation : t -\u003e lexicons:Lexicon.registry -\u003e t\nval with_rate_limit : t -\u003e limits:rate_limit_config -\u003e t\n```\n\n## Dependencies\n- atproto-syntax\n- atproto-lexicon (for validation)\n- jsont","acceptance_criteria":"- Route registration by NSID\n- Request parameter validation\n- Response serialization\n- Error handling middleware\n- Lexicon schema validation","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:30.734032128+01:00","updated_at":"2025-12-28T11:18:17.597713348+01:00","closed_at":"2025-12-28T11:18:17.597713348+01:00","labels":["network","xrpc"],"dependencies":[{"issue_id":"atproto-42","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:04.381357087+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-42","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:08.952225305+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-42","depends_on_id":"atproto-52","type":"blocks","created_at":"2025-12-28T00:12:10.416004945+01:00","created_by":"daemon","metadata":"{}"}]}
23
+
{"id":"atproto-43","title":"Implement Firehose (event stream) client","description":"Implement event stream (firehose) client for AT Protocol. The firehose provides real-time updates from the network.","design":"## Module Structure\n\n```ocaml\n(* atproto-sync/lib/firehose.ml *)\ntype event =\n | Commit of commit_event\n | Identity of identity_event\n | Account of account_event\n\nand commit_event = {\n seq: int64;\n repo: Did.t;\n rev: Tid.t;\n since: Tid.t option;\n commit: Cid.t;\n blocks: bytes; (* CAR slice *)\n ops: operation list;\n too_big: bool;\n}\n\nand operation = {\n action: [ `Create | `Update | `Delete ];\n path: string; (* collection/rkey *)\n cid: Cid.t option;\n}\n\nand identity_event = {\n seq: int64;\n did: Did.t;\n time: Ptime.t;\n handle: Handle.t option;\n}\n\nand account_event = {\n seq: int64;\n did: Did.t;\n time: Ptime.t;\n active: bool;\n status: string option;\n}\n\ntype _ Effect.t +=\n | Websocket_connect : Uri.t -\u003e websocket Effect.t\n | Websocket_recv : websocket -\u003e bytes Effect.t\n | Websocket_close : websocket -\u003e unit Effect.t\n\nval subscribe : \n uri:Uri.t -\u003e \n ?cursor:int64 -\u003e \n (event -\u003e unit) -\u003e \n unit\n```\n\n## Wire Protocol\n\n- Binary WebSocket frames\n- Each frame: header (DAG-CBOR) + payload (DAG-CBOR)\n- Header: `{ \"op\": 1, \"t\": \"#commit\" }`\n\n## Dependencies\n- atproto-ipld (dag-cbor)\n- atproto-syntax","acceptance_criteria":"- WebSocket connection management\n- DAG-CBOR frame decoding\n- Event type dispatching (#commit, #identity, #account)\n- Cursor-based resumption\n- All firehose interop tests pass","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:42.406702551+01:00","updated_at":"2025-12-28T10:54:13.835589935+01:00","closed_at":"2025-12-28T10:54:13.835589935+01:00","labels":["network","sync"],"dependencies":[{"issue_id":"atproto-43","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:05.216684474+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-43","depends_on_id":"atproto-21","type":"blocks","created_at":"2025-12-28T00:11:10.008522642+01:00","created_by":"daemon","metadata":"{}"}]}
24
+
{"id":"atproto-44","title":"Implement Repository sync","description":"Implement repository synchronization for AT Protocol. This enables PDS-to-PDS and relay sync.","design":"## Module Structure\n\n```ocaml\n(* atproto-sync/lib/repo_sync.ml *)\ntype sync_result = {\n commit: Commit.t;\n blocks: (Cid.t * bytes) list;\n}\n\nval get_repo : \n client:Xrpc.Client.t -\u003e \n did:Did.t -\u003e \n (sync_result, error) result\n\nval get_checkout :\n client:Xrpc.Client.t -\u003e\n did:Did.t -\u003e\n commit:Cid.t -\u003e\n (sync_result, error) result\n\n(* Diff handling *)\ntype diff_entry = {\n action: [ `Create | `Update | `Delete ];\n collection: Nsid.t;\n rkey: string;\n cid: Cid.t option;\n value: Dag_cbor.value option;\n}\n\nval compute_diff : \n old_commit:Cid.t -\u003e \n new_commit:Cid.t -\u003e \n blocks:(Cid.t -\u003e bytes option) -\u003e\n diff_entry list\n\nval apply_diff :\n repo:Repo.t -\u003e\n diff:diff_entry list -\u003e\n Repo.t\n```\n\n## Sync Protocol Endpoints\n\n- `com.atproto.sync.getRepo` - Full repo export\n- `com.atproto.sync.getCheckout` - Specific commit\n- `com.atproto.sync.subscribeRepos` - Real-time updates\n\n## Dependencies\n- atproto-repo\n- atproto-xrpc","acceptance_criteria":"- Repository export (getRepo)\n- Incremental sync (subscribeRepos)\n- Diff computation between commits\n- Proof verification","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:51.918242921+01:00","updated_at":"2025-12-28T11:15:00.121154336+01:00","closed_at":"2025-12-28T11:15:00.121154336+01:00","labels":["network","sync"],"dependencies":[{"issue_id":"atproto-44","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:06.164238338+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-44","depends_on_id":"atproto-25","type":"blocks","created_at":"2025-12-28T00:11:10.849151222+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-44","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:11.847570996+01:00","created_by":"daemon","metadata":"{}"}]}
25
+
{"id":"atproto-45","title":"Implement OAuth client","description":"Implement OAuth client for AT Protocol authentication. OAuth is the preferred authentication method.","design":"## Module Structure\n\n```ocaml\n(* atproto-xrpc/lib/oauth.ml *)\ntype client_config = {\n client_id: string;\n redirect_uri: Uri.t;\n scope: string list;\n}\n\ntype authorization_request = {\n state: string;\n code_verifier: string; (* PKCE *)\n authorization_url: Uri.t;\n}\n\ntype tokens = {\n access_token: string;\n refresh_token: string option;\n expires_at: Ptime.t;\n scope: string list;\n}\n\nval start_authorization : \n config:client_config -\u003e \n pds:Uri.t -\u003e \n authorization_request\n\nval complete_authorization :\n config:client_config -\u003e\n code:string -\u003e\n code_verifier:string -\u003e\n (tokens, error) result\n\nval refresh_tokens :\n config:client_config -\u003e\n refresh_token:string -\u003e\n (tokens, error) result\n```\n\n## OAuth Flow\n\n1. Discover authorization server from PDS\n2. Generate PKCE code_verifier + code_challenge\n3. Redirect to authorization URL\n4. Exchange code for tokens\n5. Use access_token in Bearer header\n6. Refresh when expired\n\n## Dependencies\n- atproto-crypto (for PKCE)\n- atproto-xrpc","acceptance_criteria":"- OAuth 2.0 authorization code flow\n- PKCE support\n- Token refresh\n- DPoP (proof of possession) support","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:10:59.811580681+01:00","updated_at":"2025-12-28T11:24:41.399056388+01:00","closed_at":"2025-12-28T11:24:41.399056388+01:00","labels":["auth","network"],"dependencies":[{"issue_id":"atproto-45","depends_on_id":"atproto-40","type":"parent-child","created_at":"2025-12-28T00:11:07.109758394+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-45","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:11:12.874999712+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-45","depends_on_id":"atproto-13","type":"blocks","created_at":"2025-12-28T00:11:13.692776478+01:00","created_by":"daemon","metadata":"{}"}]}
26
+
{"id":"atproto-50","title":"Application Layer - Lexicon and API","description":"Implement the application layer libraries that handle Lexicon schemas, record validation, and provide a high-level API for building AT Protocol applications.","design":"## Packages\n\n### atproto-lexicon\n- Lexicon schema parser\n- Record validation\n- XRPC param/input/output validation\n- Schema registry\n\n### atproto-lexicon-gen\n- Code generation from Lexicon schemas\n- Type-safe OCaml types\n- Encoder/decoder generation\n\n### atproto-api\n- High-level client API\n- Session management\n- RichText handling\n- Common operations (post, like, follow, etc.)\n\n## Lexicon Types\n\n- record: Repository record schemas\n- query: HTTP GET endpoints\n- procedure: HTTP POST endpoints\n- subscription: WebSocket streams\n\n## Field Types\n\n- Primitives: boolean, integer, string, bytes, cid-link\n- Containers: array, object\n- References: ref, union\n- Special: blob, unknown, token\n\n## Dependencies\n- atproto-xrpc\n- atproto-identity\n- jsont","acceptance_criteria":"- Lexicon parser handles all schema types\n- Record validation works against schemas\n- Code generation produces type-safe OCaml\n- All lexicon interop tests pass","status":"closed","priority":2,"issue_type":"epic","created_at":"2025-12-28T00:07:09.195003323+01:00","updated_at":"2025-12-28T11:57:35.469581739+01:00","closed_at":"2025-12-28T11:57:35.469581739+01:00","labels":["application","epic"],"dependencies":[{"issue_id":"atproto-50","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:07:16.879118155+01:00","created_by":"daemon","metadata":"{}"}]}
27
+
{"id":"atproto-51","title":"Implement Lexicon schema parser","description":"Implement Lexicon schema parser for AT Protocol. Lexicon is the schema language used to define records and APIs.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon/lib/schema.ml *)\ntype lexicon = {\n lexicon: int; (* version, always 1 *)\n id: Nsid.t;\n revision: int option;\n description: string option;\n defs: (string * definition) list;\n}\n\nand definition =\n | Record of record_def\n | Query of query_def\n | Procedure of procedure_def\n | Subscription of subscription_def\n | Object of object_def\n | Array of array_def\n | Token of token_def\n | String of string_def\n (* ... *)\n\nand record_def = {\n description: string option;\n key: record_key;\n record: object_def;\n}\n\nand query_def = {\n description: string option;\n parameters: params_def option;\n output: output_def option;\n errors: error_def list;\n}\n\n(* ... full schema types ... *)\n\nval parse : Jsont.json -\u003e (lexicon, error) result\n\n(* atproto-lexicon/lib/registry.ml *)\ntype t\n\nval create : unit -\u003e t\nval add : t -\u003e lexicon -\u003e t\nval get : t -\u003e Nsid.t -\u003e lexicon option\nval get_def : t -\u003e Nsid.t -\u003e string -\u003e definition option\n```\n\n## Jsont Codecs for Lexicon Schemas\n\n```ocaml\nlet string_def_jsont : string_def Jsont.t =\n Jsont.obj \"string_def\" @@ fun o -\u003e\n let format = Jsont.obj_mem o \"format\" ~opt:true Jsont.string in\n let min_length = Jsont.obj_mem o \"minLength\" ~opt:true Jsont.int in\n let max_length = Jsont.obj_mem o \"maxLength\" ~opt:true Jsont.int in\n let min_graphemes = Jsont.obj_mem o \"minGraphemes\" ~opt:true Jsont.int in\n let max_graphemes = Jsont.obj_mem o \"maxGraphemes\" ~opt:true Jsont.int in\n let enum = Jsont.obj_mem o \"enum\" ~opt:true (Jsont.list Jsont.string) in\n let const = Jsont.obj_mem o \"const\" ~opt:true Jsont.string in\n Jsont.obj_finish o { format; min_length; max_length; min_graphemes; max_graphemes; enum; const }\n\nlet definition_jsont : definition Jsont.t =\n (* Discriminated union based on \"type\" field *)\n Jsont.obj \"definition\" @@ fun o -\u003e\n let type_ = Jsont.obj_mem o \"type\" Jsont.string in\n match type_ with\n | \"record\" -\u003e Record (decode_record_def o)\n | \"query\" -\u003e Query (decode_query_def o)\n | \"procedure\" -\u003e Procedure (decode_procedure_def o)\n | \"object\" -\u003e Object (decode_object_def o)\n | \"string\" -\u003e String (decode_string_def o)\n | _ -\u003e failwith (\"unknown definition type: \" ^ type_)\n\nlet lexicon_jsont : lexicon Jsont.t =\n Jsont.obj \"lexicon\" @@ fun o -\u003e\n let lexicon = Jsont.obj_mem o \"lexicon\" Jsont.int in\n let id = Jsont.obj_mem o \"id\" nsid_jsont in\n let revision = Jsont.obj_mem o \"revision\" ~opt:true Jsont.int in\n let description = Jsont.obj_mem o \"description\" ~opt:true Jsont.string in\n let defs = Jsont.obj_mem o \"defs\" (Jsont.obj_map definition_jsont) in\n Jsont.obj_finish o { lexicon; id; revision; description; defs }\n```\n\n## Lexicon Schema Structure\n\n```json\n{\n \"lexicon\": 1,\n \"id\": \"app.bsky.feed.post\",\n \"defs\": {\n \"main\": { \"type\": \"record\", ... },\n \"entity\": { \"type\": \"object\", ... }\n }\n}\n```\n\n## Dependencies\n- atproto-syntax\n- jsont","acceptance_criteria":"- Parse all Lexicon schema types (record, query, procedure, subscription)\n- Parse all field types (primitives, containers, refs)\n- Parse all format constraints\n- Schema registry with NSID lookup\n- All lexicon interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:28.701630723+01:00","updated_at":"2025-12-28T10:12:30.084906585+01:00","closed_at":"2025-12-28T10:12:30.084906585+01:00","labels":["application","lexicon"],"dependencies":[{"issue_id":"atproto-51","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:04.743859406+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-51","depends_on_id":"atproto-11","type":"blocks","created_at":"2025-12-28T00:12:08.34127929+01:00","created_by":"daemon","metadata":"{}"}]}
28
+
{"id":"atproto-52","title":"Implement Lexicon validation","description":"Implement Lexicon-based validation for AT Protocol data. This validates records and API payloads against schemas.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon/lib/validator.ml *)\ntype validation_error = {\n path: string list;\n message: string;\n}\n\nval validate_record :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n value:Dag_cbor.value -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_params :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n params:(string * string) list -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_input :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n input:Jsont.json -\u003e\n (unit, validation_error list) result\n\nval validate_xrpc_output :\n registry:Registry.t -\u003e\n nsid:Nsid.t -\u003e\n output:Jsont.json -\u003e\n (unit, validation_error list) result\n```\n\n## Constraint Types\n\n- **String**: minLength, maxLength, minGraphemes, maxGraphemes, format, enum, const\n- **Integer**: minimum, maximum, enum, const\n- **Bytes**: minLength, maxLength\n- **Array**: minLength, maxLength, items type\n- **Blob**: maxSize, accept (MIME types)\n- **Union**: open/closed, refs\n\n## Format Validators (Parser-based, NO REGEX)\n\nEach format has a dedicated parser module:\n\n```ocaml\n(* atproto-lexicon/lib/formats.ml *)\n\nlet validate_did s = Did.of_string s |\u003e Result.is_ok\nlet validate_handle s = Handle.of_string s |\u003e Result.is_ok\nlet validate_nsid s = Nsid.of_string s |\u003e Result.is_ok\nlet validate_tid s = Tid.of_string s |\u003e Result.is_ok\nlet validate_cid s = Cid.of_string s |\u003e Result.is_ok\nlet validate_at_uri s = At_uri.of_string s |\u003e Result.is_ok\nlet validate_at_identifier s = \n Did.of_string s |\u003e Result.is_ok || Handle.of_string s |\u003e Result.is_ok\nlet validate_record_key s = Record_key.of_string s |\u003e Result.is_ok\n\nlet validate_datetime s =\n (* Hand-written RFC-3339 parser *)\n parse_datetime s |\u003e Result.is_ok\n\nlet validate_language s =\n (* BCP-47 language tag parser *)\n parse_language_tag s |\u003e Result.is_ok\n\nlet validate_uri s =\n (* RFC-3986 URI parser *)\n Uri.of_string s |\u003e Option.is_some\n```\n\n## Dependencies\n- atproto-lexicon (schema)\n- atproto-syntax (format validators)\n- jsont","acceptance_criteria":"- Validate records against schemas\n- Validate XRPC params, input, output\n- Proper error messages with paths\n- All constraint types supported\n- All record-data interop tests pass","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:39.125440686+01:00","updated_at":"2025-12-28T10:25:46.671434007+01:00","closed_at":"2025-12-28T10:25:46.671434007+01:00","labels":["application","lexicon"],"dependencies":[{"issue_id":"atproto-52","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:05.375287273+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-52","depends_on_id":"atproto-51","type":"blocks","created_at":"2025-12-28T00:12:09.479940241+01:00","created_by":"daemon","metadata":"{}"}]}
29
+
{"id":"atproto-53","title":"Implement Lexicon code generation","description":"Implement code generation from Lexicon schemas to OCaml types and API bindings.","design":"## Module Structure\n\n```ocaml\n(* atproto-lexicon-gen/lib/codegen.ml *)\ntype config = {\n output_dir: string;\n module_prefix: string;\n}\n\nval generate_types : config:config -\u003e lexicon:Lexicon.t -\u003e unit\nval generate_client : config:config -\u003e lexicons:Lexicon.t list -\u003e unit\n```\n\n## Generated Code Example\n\nInput Lexicon:\n```json\n{\n \"id\": \"app.bsky.feed.post\",\n \"defs\": {\n \"main\": {\n \"type\": \"record\",\n \"record\": {\n \"type\": \"object\",\n \"properties\": {\n \"text\": { \"type\": \"string\", \"maxGraphemes\": 300 },\n \"createdAt\": { \"type\": \"string\", \"format\": \"datetime\" }\n }\n }\n }\n }\n}\n```\n\nGenerated OCaml:\n```ocaml\nmodule App_bsky_feed_post = struct\n type t = {\n text: string;\n created_at: Ptime.t;\n }\n \n let jsont : t Jsont.t =\n Jsont.obj \"app.bsky.feed.post\" @@ fun o -\u003e\n let text = Jsont.obj_mem o \"text\" Jsont.string in\n let created_at = Jsont.obj_mem o \"createdAt\" Datetime.jsont in\n Jsont.obj_finish o { text; created_at }\n \n val to_dag_cbor : t -\u003e Dag_cbor.value\n val of_dag_cbor : Dag_cbor.value -\u003e (t, error) result\nend\n```\n\n## CLI Tool\n\n```bash\natproto-lexicon-gen --input lexicons/ --output lib/generated/\n```\n\n## Dependencies\n- atproto-lexicon\n- jsont","acceptance_criteria":"- Generate OCaml types from Lexicon schemas\n- Generate encoders/decoders\n- Type-safe API bindings\n- CLI tool for code generation","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:11:47.861552784+01:00","updated_at":"2025-12-28T11:28:03.226633204+01:00","closed_at":"2025-12-28T11:28:03.226633204+01:00","labels":["application","codegen"],"dependencies":[{"issue_id":"atproto-53","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:06.539440409+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-53","depends_on_id":"atproto-51","type":"blocks","created_at":"2025-12-28T00:12:11.189125052+01:00","created_by":"daemon","metadata":"{}"}]}
30
+
{"id":"atproto-54","title":"Implement high-level API client","description":"Implement high-level API client for AT Protocol / Bluesky. This provides a user-friendly interface for common operations.","design":"## Module Structure\n\n```ocaml\n(* atproto-api/lib/agent.ml *)\ntype t\n\nval create : pds:Uri.t -\u003e t\n\n(* Authentication *)\nval login : t -\u003e identifier:string -\u003e password:string -\u003e (t, error) result\nval login_oauth : t -\u003e tokens:Oauth.tokens -\u003e t\nval refresh_session : t -\u003e (t, error) result\n\n(* Profile *)\nval get_profile : t -\u003e actor:string -\u003e (profile, error) result\nval update_profile : t -\u003e display_name:string option -\u003e ... -\u003e (unit, error) result\n\n(* Posts *)\nval create_post : t -\u003e text:string -\u003e ?reply:reply_ref -\u003e ... -\u003e (post_ref, error) result\nval delete_post : t -\u003e uri:At_uri.t -\u003e (unit, error) result\n\n(* Social *)\nval like : t -\u003e uri:At_uri.t -\u003e cid:Cid.t -\u003e (like_ref, error) result\nval follow : t -\u003e did:Did.t -\u003e (follow_ref, error) result\nval unfollow : t -\u003e uri:At_uri.t -\u003e (unit, error) result\n\n(* Feed *)\nval get_timeline : t -\u003e ?cursor:string -\u003e ?limit:int -\u003e (timeline, error) result\nval get_author_feed : t -\u003e actor:string -\u003e ... -\u003e (feed, error) result\n\n(* atproto-api/lib/richtext.ml *)\ntype t\n\nval create : string -\u003e t\nval detect_facets : t -\u003e t (* auto-detect mentions, links *)\nval add_mention : t -\u003e start:int -\u003e end_:int -\u003e did:Did.t -\u003e t\nval add_link : t -\u003e start:int -\u003e end_:int -\u003e uri:Uri.t -\u003e t\nval to_post_record : t -\u003e Dag_cbor.value\n```\n\n## Jsont Codecs for API Types\n\n```ocaml\nlet profile_jsont : profile Jsont.t =\n Jsont.obj \"profile\" @@ fun o -\u003e\n let did = Jsont.obj_mem o \"did\" did_jsont in\n let handle = Jsont.obj_mem o \"handle\" handle_jsont in\n let display_name = Jsont.obj_mem o \"displayName\" ~opt:true Jsont.string in\n let description = Jsont.obj_mem o \"description\" ~opt:true Jsont.string in\n let avatar = Jsont.obj_mem o \"avatar\" ~opt:true Jsont.string in\n let followers_count = Jsont.obj_mem o \"followersCount\" ~opt:true Jsont.int in\n let follows_count = Jsont.obj_mem o \"followsCount\" ~opt:true Jsont.int in\n let posts_count = Jsont.obj_mem o \"postsCount\" ~opt:true Jsont.int in\n Jsont.obj_finish o { did; handle; display_name; description; avatar; \n followers_count; follows_count; posts_count }\n\nlet facet_jsont : facet Jsont.t =\n Jsont.obj \"facet\" @@ fun o -\u003e\n let index = Jsont.obj_mem o \"index\" byte_slice_jsont in\n let features = Jsont.obj_mem o \"features\" (Jsont.list facet_feature_jsont) in\n Jsont.obj_finish o { index; features }\n```\n\n## RichText Facets\n\n```json\n{\n \"text\": \"Hello @alice.bsky.social!\",\n \"facets\": [\n {\n \"index\": { \"byteStart\": 6, \"byteEnd\": 25 },\n \"features\": [\n { \"$type\": \"app.bsky.richtext.facet#mention\", \"did\": \"did:plc:...\" }\n ]\n }\n ]\n}\n```\n\n## Dependencies\n- atproto-xrpc\n- atproto-identity\n- atproto-repo\n- jsont","acceptance_criteria":"- Session management (login, logout, refresh)\n- Common operations (post, like, follow, etc.)\n- RichText handling (mentions, links, facets)\n- Timeline and feed fetching\n- Profile operations","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:12:00.736309435+01:00","updated_at":"2025-12-28T11:47:47.071271001+01:00","closed_at":"2025-12-28T11:47:47.071271001+01:00","labels":["api","application"],"dependencies":[{"issue_id":"atproto-54","depends_on_id":"atproto-50","type":"parent-child","created_at":"2025-12-28T00:12:07.636789403+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-54","depends_on_id":"atproto-41","type":"blocks","created_at":"2025-12-28T00:12:12.376875324+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-54","depends_on_id":"atproto-33","type":"blocks","created_at":"2025-12-28T00:12:13.060557136+01:00","created_by":"daemon","metadata":"{}"},{"issue_id":"atproto-54","depends_on_id":"atproto-25","type":"blocks","created_at":"2025-12-28T00:12:13.934360048+01:00","created_by":"daemon","metadata":"{}"}]}
31
+
{"id":"atproto-5l1","title":"Refactor JSON to simdjsont (replace yojson/jsont)","notes":"Migrated atproto-lexicon off Yojson onto simdjsont.Json.t + Simdjsont.decode Codec.value. Updated lib/lexicon/{parser,validator,atproto_lexicon,codegen} and lib/lexicon/dune. Updated test/lexicon/test_lexicon.ml fixtures loader + patterns. Verified: dune runtest test/lexicon OK; dune build @install OK.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-01T18:06:10.17746938+01:00","updated_at":"2026-01-01T20:42:14.454112697+01:00","closed_at":"2026-01-01T20:42:14.454112697+01:00","dependencies":[{"issue_id":"atproto-5l1","depends_on_id":"atproto-dqs","type":"blocks","created_at":"2026-01-01T18:06:39.648046004+01:00","created_by":"daemon","metadata":"{}"}]}
32
+
{"id":"atproto-60","title":"Implement effects-based I/O abstraction","description":"Implement the effects-based I/O abstraction layer that makes all libraries runtime-agnostic.","design":"## Module Structure\n\n```ocaml\n(* atproto-effects/lib/effects.ml *)\n\n(* HTTP effects *)\ntype http_request = {\n method_: [ `GET | `POST | `PUT | `DELETE ];\n uri: Uri.t;\n headers: (string * string) list;\n body: string option;\n}\n\ntype http_response = {\n status: int;\n headers: (string * string) list;\n body: string;\n}\n\ntype _ Effect.t +=\n | Http_request : http_request -\u003e http_response Effect.t\n\n(* DNS effects *)\ntype _ Effect.t +=\n | Dns_txt : string -\u003e string list Effect.t\n | Dns_a : string -\u003e string list Effect.t\n\n(* Time effects *)\ntype _ Effect.t +=\n | Now : Ptime.t Effect.t\n | Sleep : float -\u003e unit Effect.t\n\n(* Random effects *)\ntype _ Effect.t +=\n | Random_bytes : int -\u003e bytes Effect.t\n\n(* atproto-effects-eio/lib/handler.ml *)\nval run : (unit -\u003e 'a) -\u003e 'a\n```\n\n## Handler Example (eio)\n\n```ocaml\nlet run f =\n Effect.Deep.match_ f ()\n {\n retc = Fun.id;\n exnc = raise;\n effc = fun (type a) (e : a Effect.t) -\u003e\n match e with\n | Http_request req -\u003e\n Some (fun (k : (a, _) continuation) -\u003e\n let resp = Eio_client.request req in\n continue k resp)\n | Dns_txt domain -\u003e\n Some (fun k -\u003e\n let records = Eio_dns.txt domain in\n continue k records)\n | _ -\u003e None\n }\n```\n\n## Dependencies\n- eio (for testing handler)","acceptance_criteria":"- Effect types for HTTP, DNS, time, random\n- eio-based handler for testing\n- Handler composition utilities\n- Performance benchmarks","status":"closed","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2025-12-28T00:12:29.021401617+01:00","updated_at":"2025-12-28T11:57:08.264086142+01:00","closed_at":"2025-12-28T11:57:08.264086142+01:00","labels":["effects","infrastructure"],"dependencies":[{"issue_id":"atproto-60","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:55.467983208+01:00","created_by":"daemon","metadata":"{}"}]}
33
+
{"id":"atproto-61","title":"Set up interoperability test suite","description":"Set up and run the AT Protocol interoperability tests from bluesky-social/atproto-interop-tests.","design":"## Test Structure\n\n```\ntest/\n├── interop/\n│ ├── syntax_test.ml # Handle, DID, NSID, TID, etc.\n│ ├── crypto_test.ml # Signatures, did:key\n│ ├── data_model_test.ml # DAG-CBOR, CID\n│ ├── mst_test.ml # Key heights, tree structure\n│ ├── lexicon_test.ml # Schema and record validation\n│ └── firehose_test.ml # Commit proofs\n├── fixtures/ # Cloned from atproto-interop-tests\n└── dune\n```\n\n## Test Approach\n\n1. Clone test vectors from GitHub\n2. Parse JSON fixtures using jsont\n3. Parse text fixtures line by line\n4. Run each test case\n5. Compare output to expected values\n\n## Example Test\n\n```ocaml\nlet load_json_fixtures path =\n let json = Jsont.of_file path in\n Jsont.decode (Jsont.list fixture_jsont) json\n\nlet%test \"handle_syntax_valid\" =\n let fixtures = load_lines \"fixtures/syntax/handle_syntax_valid.txt\" in\n List.for_all (fun line -\u003e\n match Handle.of_string line with\n | Ok _ -\u003e true\n | Error _ -\u003e false\n ) fixtures\n\nlet%test \"handle_syntax_invalid\" =\n let fixtures = load_lines \"fixtures/syntax/handle_syntax_invalid.txt\" in\n List.for_all (fun line -\u003e\n match Handle.of_string line with\n | Ok _ -\u003e false\n | Error _ -\u003e true\n ) fixtures\n\nlet%test \"crypto_signature_fixtures\" =\n let fixtures = load_json_fixtures \"fixtures/crypto/signature-fixtures.json\" in\n List.for_all (fun fixture -\u003e\n let message = Base64.decode fixture.message_base64 in\n let signature = Base64.decode fixture.signature_base64 in\n let key = Did_key.of_string fixture.public_key_did in\n let result = Crypto.verify key message signature in\n result = fixture.valid_signature\n ) fixtures\n```\n\n## Dependencies\n- alcotest or ounit2\n- jsont","acceptance_criteria":"- All syntax interop tests pass\n- All crypto interop tests pass\n- All data-model interop tests pass\n- All MST interop tests pass\n- All lexicon interop tests pass\n- All firehose interop tests pass","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T00:12:40.553908313+01:00","updated_at":"2025-12-28T13:25:34.614867702+01:00","closed_at":"2025-12-28T13:25:34.614867702+01:00","labels":["conformance","testing"],"dependencies":[{"issue_id":"atproto-61","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:56.180809368+01:00","created_by":"daemon","metadata":"{}"}]}
34
+
{"id":"atproto-62","title":"Set up monorepo package structure","description":"Set up the monorepo structure for multiple opam packages within a single repository.","design":"## Repository Structure\n\n```\natproto/\n├── dune-project # Root with all packages\n├── packages/\n│ ├── atproto-syntax/\n│ │ ├── lib/\n│ │ │ ├── dune\n│ │ │ └── *.ml\n│ │ ├── test/\n│ │ │ ├── dune\n│ │ │ └── *_test.ml\n│ │ └── atproto-syntax.opam\n│ ├── atproto-crypto/\n│ ├── atproto-multibase/\n│ ├── atproto-ipld/\n│ ├── atproto-mst/\n│ ├── atproto-repo/\n│ ├── atproto-identity/\n│ ├── atproto-xrpc/\n│ ├── atproto-sync/\n│ ├── atproto-lexicon/\n│ ├── atproto-lexicon-gen/\n│ ├── atproto-api/\n│ └── atproto-effects/\n├── examples/\n│ ├── simple_client/\n│ └── firehose_consumer/\n└── interop-tests/\n```\n\n## dune-project\n\n```lisp\n(lang dune 3.20)\n(name atproto)\n(generate_opam_files true)\n\n(package\n (name atproto-syntax)\n (synopsis \"AT Protocol identifier syntax parsing\")\n (depends\n (ocaml (\u003e= 5.4))\n re\n ptime))\n\n(package\n (name atproto-crypto)\n ...)\n```\n\n## CI (.github/workflows/ci.yml)\n\n- OCaml 5.4 matrix\n- Build all packages\n- Run all tests\n- Run interop tests","acceptance_criteria":"- Multi-package dune-project structure\n- Separate opam files per package\n- CI pipeline for building and testing\n- Documentation generation setup","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T00:12:50.547102438+01:00","updated_at":"2025-12-28T11:57:18.856810633+01:00","closed_at":"2025-12-28T11:57:18.856810633+01:00","labels":["infrastructure","setup"],"dependencies":[{"issue_id":"atproto-62","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T00:12:57.015938611+01:00","created_by":"daemon","metadata":"{}"}]}
35
+
{"id":"atproto-8pf","title":"Migrate lib/api JSON parsing from yojson to simdjson","acceptance_criteria":"Build passes (dune build @install). lib/api no longer depends on yojson (lib/api/dune depends on simdjsont). All lib/api JSON encode/decode uses Atproto_xrpc.Client.json (Simdjsont.Json.t). API tests updated and passing (dune runtest test/api). No remaining yojson polymorphic variants in lib/api and test/api.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T20:55:46.43041578+01:00","updated_at":"2026-01-01T21:02:47.086579243+01:00","closed_at":"2026-01-01T21:02:47.086579243+01:00"}
36
+
{"id":"atproto-bsh","title":"Switch atproto-xrpc to use atproto-json wrapper instead of Simdjsont","status":"closed","priority":1,"issue_type":"task","assignee":"gdiazlo","created_at":"2026-01-01T21:08:22.228905638+01:00","updated_at":"2026-01-01T21:10:59.451211971+01:00","closed_at":"2026-01-01T21:10:59.451211971+01:00"}
37
37
{"id":"atproto-cir","title":"Implement DAG-JSON codec","description":"Create lib/ipld/dag_json.ml, encode Links as {\"/\": \"\u003ccid-string\u003e\"}, encode Bytes as {\"/\": {\"bytes\": \"\u003cbase64\u003e\"}}","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T15:47:30.578398468+01:00","updated_at":"2025-12-28T16:06:13.475583417+01:00","closed_at":"2025-12-28T16:06:13.475583417+01:00","labels":["compliance","ipld"]}
38
-
{"id":"atproto-dqs","title":"Fix baseline failing crypto tests (signature fixtures)","description":"","notes":"Follow-up: enforced mirage-crypto lower bounds to \u003e= 2.0.2 in dune-project (regenerated opam); pushed commit a6b9a13 to avoid P-256 fixture failures with older versions.","status":"closed","priority":0,"issue_type":"bug","created_at":"2026-01-01T18:06:34.897822579+01:00","updated_at":"2026-01-01T18:52:37.333901891+01:00","closed_at":"2026-01-01T18:52:37.333901891+01:00"}
38
+
{"id":"atproto-dqs","title":"Fix baseline failing crypto tests (signature fixtures)","notes":"Follow-up: enforced mirage-crypto lower bounds to \u003e= 2.0.2 in dune-project (regenerated opam); pushed commit a6b9a13 to avoid P-256 fixture failures with older versions.","status":"closed","priority":0,"issue_type":"bug","created_at":"2026-01-01T18:06:34.897822579+01:00","updated_at":"2026-01-01T18:52:37.333901891+01:00","closed_at":"2026-01-01T18:52:37.333901891+01:00"}
39
39
{"id":"atproto-ex-bot","title":"Implement Bluesky Bot Example (bsky_bot)","description":"Bot that can login, post with rich text, reply to mentions, follow back. Uses atproto-api Agent, Richtext, atproto-xrpc Client. Eio for I/O, Climate for CLI.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T09:07:59.407148786+01:00","updated_at":"2025-12-29T09:16:27.789416628+01:00","closed_at":"2025-12-29T09:16:27.789416628+01:00","labels":["api","bot","cli","example"]}
40
40
{"id":"atproto-ex-feed","title":"Implement Feed Generator Example (feed_generator)","description":"Custom feed algorithm with XRPC server serving app.bsky.feed.getFeedSkeleton. Subscribes to firehose, filters posts by keyword/language. Uses atproto-xrpc Server, atproto-sync Firehose.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T09:08:01.619973249+01:00","updated_at":"2025-12-29T09:18:53.434611454+01:00","closed_at":"2025-12-29T09:18:53.434611454+01:00","labels":["example","feed","firehose","server"]}
41
41
{"id":"atproto-ex-identity","title":"Implement Handle/DID Lookup Tool (identity_tool)","description":"CLI tool using Climate to resolve handles to DIDs, DIDs to documents, verify bidirectional identity links. Uses atproto-identity, atproto-syntax. Eio for I/O.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-29T09:07:57.316042741+01:00","updated_at":"2025-12-29T09:12:20.634271257+01:00","closed_at":"2025-12-29T09:12:20.634271257+01:00","labels":["cli","example","identity"]}
42
42
{"id":"atproto-ex-repo","title":"Implement Repository Inspector Example (repo_inspector)","description":"Tool to explore AT Protocol repositories: download CAR, parse MST structure, show records with CIDs, verify commit signatures. Uses atproto-repo, atproto-mst, atproto-ipld, atproto-crypto.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-29T09:08:03.622391028+01:00","updated_at":"2025-12-29T09:21:32.223029796+01:00","closed_at":"2025-12-29T09:21:32.223029796+01:00","labels":["cli","educational","example","repo"]}
43
43
{"id":"atproto-fw8","title":"Add filter option to firehose demo","description":"Add --filter option to firehose demo to filter events by type (posts, likes, follows, reposts, profiles, blocks, lists, etc.). This makes it easier to monitor specific activity types.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T21:08:20.29215305+01:00","updated_at":"2025-12-28T21:13:31.957432542+01:00","closed_at":"2025-12-28T21:13:31.957432542+01:00","labels":["demo","enhancement","firehose"]}
44
-
{"id":"atproto-h09","title":"Add package documentation","description":"Add documentation for each of the 11 AT Protocol packages including:\n- Module-level documentation with examples\n- README.md for the root project\n- CONTRIBUTING.md guide\n- API documentation generation with odoc","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T13:34:10.559554696+01:00","updated_at":"2025-12-28T13:50:20.509417248+01:00","closed_at":"2025-12-28T13:50:20.509417248+01:00","labels":["documentation"],"dependencies":[{"issue_id":"atproto-h09","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T13:34:16.081103184+01:00","created_by":"daemon"}]}
44
+
{"id":"atproto-h09","title":"Add package documentation","description":"Add documentation for each of the 11 AT Protocol packages including:\n- Module-level documentation with examples\n- README.md for the root project\n- CONTRIBUTING.md guide\n- API documentation generation with odoc","status":"closed","priority":2,"issue_type":"task","assignee":"claude","created_at":"2025-12-28T13:34:10.559554696+01:00","updated_at":"2025-12-28T13:50:20.509417248+01:00","closed_at":"2025-12-28T13:50:20.509417248+01:00","labels":["documentation"],"dependencies":[{"issue_id":"atproto-h09","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T13:34:16.081103184+01:00","created_by":"daemon","metadata":"{}"}]}
45
45
{"id":"atproto-i1c","title":"Migrate atproto-xrpc JSON from yojson to simdjsont","description":"Migrate all atproto modules to use Atproto_json exclusively. Remove all direct Simdjsont and Yojson usage.\n\n## Completed\n- Added Atproto_json shape helpers (to_object_opt, to_array_opt, to_string_opt, etc.)\n- Migrated lib/xrpc/{client,oauth,server}.ml to use Atproto_json only\n- Fixed lib/ipld/dag_json.ml to use Atproto_json helpers\n\n## Remaining lib/ files\n- lib/lexicon/parser.ml (heavy Simdjsont usage)\n- lib/lexicon/atproto_lexicon.ml (doc examples)\n- lib/crypto/jwt.ml (heavy Simdjsont usage)\n- lib/ipld/blob.ml (1 Simdjsont match)\n\n## Remaining test files\n- test/lexicon/test_lexicon.ml (Simdjsont)\n- test/xrpc/test_xrpc.ml (Simdjsont)\n- test/crypto/test_crypto.ml (Yojson)\n- test/ipld/test_ipld.ml (Yojson)\n- test/mst/test_mst.ml (Yojson)","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T20:42:57.768582979+01:00","updated_at":"2026-01-01T22:40:10.309111402+01:00","closed_at":"2026-01-01T22:40:10.309111402+01:00"}
46
46
{"id":"atproto-kc7","title":"Verify/enforce CBOR strictness","description":"Check that CBOR library produces shortest-form encoding, add explicit rejection of indefinite-length items during decode","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T15:47:28.665226526+01:00","updated_at":"2025-12-28T16:02:55.549763384+01:00","closed_at":"2025-12-28T16:02:55.549763384+01:00","labels":["compliance","ipld"]}
47
47
{"id":"atproto-kn2","title":"Decode post content from CAR blocks in firehose","description":"Extract and display actual post text content by decoding the CAR blocks in commit events. Currently we only show the path/action, but the actual record data (post text, like subject, etc.) is in the blocks field.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T21:08:24.577853756+01:00","updated_at":"2025-12-28T21:17:19.863093228+01:00","closed_at":"2025-12-28T21:17:19.863093228+01:00","labels":["demo","enhancement","firehose"]}
48
-
{"id":"atproto-ona","title":"Bump mirage-crypto constraints to \u003e= 2.0.2","description":"","notes":"Acceptance criteria:\\n- Pin mirage-crypto* dependencies to \u003e= 2.0.2 in all relevant *.opam files (mirage-crypto, mirage-crypto-pk, etc as applicable)\\n- Ensure CI/local: all tests pass with: dune runtest\\n\\nContext:\\nCrypto tests only pass with mirage-crypto* 2.0.2; enforce lower-bound constraints.","status":"closed","priority":0,"issue_type":"task","created_at":"2026-01-01T18:43:11.890905354+01:00","updated_at":"2026-01-01T18:43:57.093494274+01:00","closed_at":"2026-01-01T18:43:57.093494274+01:00"}
48
+
{"id":"atproto-ona","title":"Bump mirage-crypto constraints to \u003e= 2.0.2","notes":"Acceptance criteria:\\n- Pin mirage-crypto* dependencies to \u003e= 2.0.2 in all relevant *.opam files (mirage-crypto, mirage-crypto-pk, etc as applicable)\\n- Ensure CI/local: all tests pass with: dune runtest\\n\\nContext:\\nCrypto tests only pass with mirage-crypto* 2.0.2; enforce lower-bound constraints.","status":"closed","priority":0,"issue_type":"task","created_at":"2026-01-01T18:43:11.890905354+01:00","updated_at":"2026-01-01T18:43:57.093494274+01:00","closed_at":"2026-01-01T18:43:57.093494274+01:00"}
49
49
{"id":"atproto-pg8","title":"Add MST example_keys.txt fixture tests","description":"Add tests using the example_keys.txt fixture file which contains 156 structured MST keys.\n\nTests should:\n1. Load all 156 keys from the fixture\n2. Build an MST containing all keys\n3. Verify all keys are retrievable\n4. Verify iteration order matches sorted key order\n5. Optionally verify tree structure properties","acceptance_criteria":"- example_keys.txt is loaded and all 156 keys are used\n- MST is built with all keys\n- All keys are retrievable after insertion\n- Iteration produces keys in sorted order","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T12:12:19.180139823+01:00","updated_at":"2025-12-28T12:43:14.192342391+01:00","closed_at":"2025-12-28T12:43:14.192342391+01:00","labels":["conformance","mst","testing"]}
50
50
{"id":"atproto-q0h","title":"Add firehose commit-proof-fixtures.json tests","description":"Add tests for the commit-proof-fixtures.json file which contains 6 test cases for MST proof verification:\n\n1. two deep split\n2. two deep leafless split\n3. add on edge with neighbor two layers down\n4. merge and split in multi-op commit\n5. complex multi-op commit\n6. split with earlier leaves on same layer\n\nEach fixture includes:\n- keys (existing keys in MST)\n- adds (keys to add)\n- dels (keys to delete)\n- rootBeforeCommit / rootAfterCommit (expected CIDs)\n- blocksInProof (CIDs of blocks needed for proof)\n\nThis tests the commit proof verification needed for firehose sync.","acceptance_criteria":"- All 6 commit-proof fixtures are tested\n- MST operations (add/delete) produce correct root CIDs\n- Proof blocks are correctly identified\n- Tests verify rootBeforeCommit and rootAfterCommit match","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T12:12:34.999268893+01:00","updated_at":"2025-12-28T12:58:39.408679225+01:00","closed_at":"2025-12-28T12:58:39.408679225+01:00","labels":["conformance","firehose","testing"]}
51
51
{"id":"atproto-s19","title":"Add CIDv0 parsing support","description":"Detect base58btc strings starting with \"Qm\", decode as multihash (sha2-256), implicit dag-pb codec","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-28T15:47:26.962896421+01:00","updated_at":"2025-12-28T15:58:38.151319563+01:00","closed_at":"2025-12-28T15:58:38.151319563+01:00","labels":["compliance","ipld"]}
52
52
{"id":"atproto-udz","title":"Add missing data-model conformance tests","description":"Add tests for data-model fixtures that are not currently covered:\n\n1. **data-model-valid.json** (5 entries) - Valid AT Protocol data model examples:\n - trivial record\n - float but integer-like (123.0)\n - empty list and object\n - list of nullable\n - list of lists\n\n2. **data-model-invalid.json** (12 entries) - Invalid examples that must be rejected:\n - top-level not an object\n - non-integer float\n - record with $type null/wrong type/empty\n - blob with string size/missing key\n - bytes with wrong field type/extra fields\n - link with wrong field type/bogus CID/extra fields","acceptance_criteria":"- test_data_model_valid() tests all 5 valid entries\n- test_data_model_invalid() tests all 12 invalid entries\n- Valid entries encode/decode correctly\n- Invalid entries are rejected with appropriate errors","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-28T12:12:14.579573063+01:00","updated_at":"2025-12-28T12:42:16.291981859+01:00","closed_at":"2025-12-28T12:42:16.291981859+01:00","labels":["conformance","ipld","testing"]}
53
-
{"id":"atproto-w30","title":"Migrate test fixture loading from Yojson to Atproto_json","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T22:43:58.821369697+01:00","updated_at":"2026-01-01T22:53:56.562250588+01:00","closed_at":"2026-01-01T22:53:56.562250588+01:00"}
54
-
{"id":"atproto-w5i","title":"Create example applications","description":"Create example applications demonstrating the AT Protocol libraries:\n1. Simple client - authenticate and make posts\n2. Firehose consumer - subscribe to real-time events\n3. Bot example - automated posting/interactions","notes":"Added firehose_demo example showing how to use the firehose module with OCaml 5 effects. Additional examples (client, bot) can be added in future iterations.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T13:34:11.928213055+01:00","updated_at":"2025-12-28T13:36:39.963890666+01:00","closed_at":"2025-12-28T13:36:39.963890666+01:00","labels":["documentation","examples"],"dependencies":[{"issue_id":"atproto-w5i","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T13:34:17.10878+01:00","created_by":"daemon"}]}
53
+
{"id":"atproto-w30","title":"Migrate test fixture loading from Yojson to Atproto_json","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T22:43:58.821369697+01:00","updated_at":"2026-01-01T22:53:56.562250588+01:00","closed_at":"2026-01-01T22:53:56.562250588+01:00"}
54
+
{"id":"atproto-w5i","title":"Create example applications","description":"Create example applications demonstrating the AT Protocol libraries:\n1. Simple client - authenticate and make posts\n2. Firehose consumer - subscribe to real-time events\n3. Bot example - automated posting/interactions","notes":"Added firehose_demo example showing how to use the firehose module with OCaml 5 effects. Additional examples (client, bot) can be added in future iterations.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-28T13:34:11.928213055+01:00","updated_at":"2025-12-28T13:36:39.963890666+01:00","closed_at":"2025-12-28T13:36:39.963890666+01:00","labels":["documentation","examples"],"dependencies":[{"issue_id":"atproto-w5i","depends_on_id":"atproto-1","type":"parent-child","created_at":"2025-12-28T13:34:17.10878+01:00","created_by":"daemon","metadata":"{}"}]}
55
55
{"id":"atproto-xfg","title":"Add Float support to DAG-CBOR","description":"Add Float of float variant to value type, encode as 64-bit double (CBOR major type 7, additional info 27), reject NaN and Infinity values, keep AT Protocol mode that rejects all floats","status":"closed","priority":1,"issue_type":"feature","created_at":"2025-12-28T15:47:24.836568239+01:00","updated_at":"2025-12-28T15:53:03.229484165+01:00","closed_at":"2025-12-28T15:53:03.229484165+01:00","labels":["compliance","ipld"]}
56
56
{"id":"atproto-y4h","title":"Add JSON output mode to firehose demo","description":"Add --json flag to output events as JSON lines (JSONL format) instead of human-readable format. This enables piping to jq or other tools for further processing.","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-12-28T21:08:22.085375231+01:00","updated_at":"2025-12-28T21:13:33.122865944+01:00","closed_at":"2025-12-28T21:13:33.122865944+01:00","labels":["demo","enhancement","firehose"]}
+15
LICENSE
+15
LICENSE
···
1
+
ISC License
2
+
3
+
Copyright (c) 2026 Gabriel Díaz López de la Llave
4
+
5
+
Permission to use, copy, modify, and/or distribute this software for any
6
+
purpose with or without fee is hereby granted, provided that the above
7
+
copyright notice and this permission notice appear in all copies.
8
+
9
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+
PERFORMANCE OF THIS SOFTWARE.