My aggregated monorepo of OCaml code, automaintained

Merge branch 'main' of git.recoil.org:anil.recoil.org/monopam-myspace

+1082 -320
+27
.changes/monopam-2026-01-17.json
··· 1 + { 2 + "repository": "monopam", 3 + "entries": [ 4 + { 5 + "date": "2026-01-17", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "New 'monopam changes' command generates AI-powered changelogs from git history", 9 + "changes": [ 10 + "Added 'monopam changes' command for AI-powered changelog generation", 11 + "Added Changes module with jsont codecs for changelog serialization", 12 + "Added Git.log function with date filtering for commit history", 13 + "Added Claude AI integration for intelligent commit analysis", 14 + "Added aggregated CHANGES.md generation at monorepo root" 15 + ], 16 + "commit_range": { 17 + "from": "0231c1d", 18 + "to": "77840a3", 19 + "count": 2 20 + }, 21 + "contributors": [ 22 + "Anil Madhavapeddy" 23 + ], 24 + "repo_url": "https://tangled.org/@anil.recoil.org/monopam.git" 25 + } 26 + ] 27 + }
+24
.changes/monopam-2026-01-18.json
··· 1 + { 2 + "repository": "monopam", 3 + "entries": [ 4 + { 5 + "date": "2026-01-18", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Improved monopam push workflow with auto-checkout and fixed tangled.org URL handling", 9 + "changes": [ 10 + "Added auto-clone of upstream repos on `monopam push` when checkout missing", 11 + "Fixed tangled.org URL parsing to strip @ prefix from usernames" 12 + ], 13 + "commit_range": { 14 + "from": "793bea4", 15 + "to": "d0fb2e4", 16 + "count": 3 17 + }, 18 + "contributors": [ 19 + "Anil Madhavapeddy" 20 + ], 21 + "repo_url": "https://tangled.org/@anil.recoil.org/monopam.git" 22 + } 23 + ] 24 + }
+27
.changes/monopam-2026-01-19.json
··· 1 + { 2 + "repository": "monopam", 3 + "entries": [ 4 + { 5 + "date": "2026-01-19", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:12:08Z", 8 + "summary": "Added changes broadcast system with new monopam_changes library and poe bot automation", 9 + "changes": [ 10 + "Added monopam_changes library with Aggregated and Query modules for changes format", 11 + "Added --aggregate flag to `monopam changes --daily` for structured JSON output", 12 + "Added Daily module with Map-based indexes and query functions (since, for_repo, for_date)", 13 + "Changed daily files from <repo>-daily.json to <repo>-<date>.json with hour tracking", 14 + "Added poe bot commands: loop, last-broadcast, reset-broadcast, storage management" 15 + ], 16 + "commit_range": { 17 + "from": "5331f9b", 18 + "to": "440e98b", 19 + "count": 2 20 + }, 21 + "contributors": [ 22 + "Anil Madhavapeddy" 23 + ], 24 + "repo_url": "https://tangled.org/@anil.recoil.org/monopam.git" 25 + } 26 + ] 27 + }
-41
.changes/monopam-daily.json
··· 1 - { 2 - "repository": "monopam", 3 - "entries": [ 4 - { 5 - "date": "2026-01-18", 6 - "summary": "monopam push now auto-creates missing checkout directories", 7 - "changes": [ 8 - "Added auto-clone on push: new packages no longer need manual pull first" 9 - ], 10 - "commit_range": { 11 - "from": "937635c", 12 - "to": "d0fb2e4", 13 - "count": 2 14 - }, 15 - "contributors": [ 16 - "Anil Madhavapeddy" 17 - ], 18 - "repo_url": "https://tangled.org/@anil.recoil.org/monopam.git" 19 - }, 20 - { 21 - "date": "2026-01-17", 22 - "summary": "New 'monopam changes' command uses AI to generate changelogs from git history", 23 - "changes": [ 24 - "Added 'monopam changes' command for AI-powered changelog generation", 25 - "Added Changes module with jsont codecs for changelog serialization", 26 - "Added Git.log function with date filtering for commit history", 27 - "Added aggregated CHANGES.md generation at monorepo root", 28 - "Added README files for jsonwt, owntracks, monopam, and srcsetter" 29 - ], 30 - "commit_range": { 31 - "from": "0231c1d", 32 - "to": "77840a3", 33 - "count": 2 34 - }, 35 - "contributors": [ 36 - "Anil Madhavapeddy" 37 - ], 38 - "repo_url": "https://tangled.org/@anil.recoil.org/monopam.git" 39 - } 40 - ] 41 - }
+27
.changes/ocaml-apubt-2026-01-19.json
··· 1 + { 2 + "repository": "ocaml-apubt", 3 + "entries": [ 4 + { 5 + "date": "2026-01-19", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:12:08Z", 8 + "summary": "Added authentication system with XDG-based credential storage and completed CLI with webfinger integration.", 9 + "changes": [ 10 + "Added apub_auth library with XDG-compliant session persistence in ~/.config/apub/", 11 + "Added CLI commands: auth setup/status/logout, profile list/switch/current", 12 + "Added write commands: post, follow, like, boost (auto-load saved credentials)", 13 + "Integrated ocaml-webfinger for RFC 7033/7565 compliant actor discovery", 14 + "Added Question activity support with one_of, any_of, closed fields" 15 + ], 16 + "commit_range": { 17 + "from": "46d4063", 18 + "to": "fbd519f", 19 + "count": 4 20 + }, 21 + "contributors": [ 22 + "Anil Madhavapeddy" 23 + ], 24 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-apubt.git" 25 + } 26 + ] 27 + }
+23
.changes/ocaml-atp-2026-01-18.json
··· 1 + { 2 + "repository": "ocaml-atp", 3 + "entries": [ 4 + { 5 + "date": "2026-01-18", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Regenerated OCaml bindings for AT Protocol lexicons", 9 + "changes": [ 10 + "Updated generated bindings for atproto, bsky, standard-site, and tangled lexicons" 11 + ], 12 + "commit_range": { 13 + "from": "1933188", 14 + "to": "c8ccec6", 15 + "count": 2 16 + }, 17 + "contributors": [ 18 + "Anil Madhavapeddy" 19 + ], 20 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-atp.git" 21 + } 22 + ] 23 + }
+24
.changes/ocaml-atp-2026-01-19.json
··· 1 + { 2 + "repository": "ocaml-atp", 3 + "entries": [ 4 + { 5 + "date": "2026-01-19", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:12:08Z", 8 + "summary": "Fixed non-deterministic code generation in hermest lexicon generator", 9 + "changes": [ 10 + "Fixed hermest producing different output on each run by sorting alphabetically", 11 + "Regenerated all lexicon files with deterministic ordering" 12 + ], 13 + "commit_range": { 14 + "from": "ae93f40", 15 + "to": "ae93f40", 16 + "count": 1 17 + }, 18 + "contributors": [ 19 + "Anil Madhavapeddy" 20 + ], 21 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-atp.git" 22 + } 23 + ] 24 + }
-21
.changes/ocaml-atp-daily.json
··· 1 - { 2 - "repository": "ocaml-atp", 3 - "entries": [ 4 - { 5 - "date": "2026-01-18", 6 - "summary": "Regenerated AT Protocol lexicon bindings from upstream schemas", 7 - "changes": [ 8 - "Updated atproto, bsky, standard-site, and tangled lexicon bindings" 9 - ], 10 - "commit_range": { 11 - "from": "c8ccec6", 12 - "to": "c8ccec6", 13 - "count": 1 14 - }, 15 - "contributors": [ 16 - "Anil Madhavapeddy" 17 - ], 18 - "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-atp.git" 19 - } 20 - ] 21 - }
+3 -1
.changes/ocaml-bushel-daily.json .changes/ocaml-bushel-2026-01-18.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-18", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 6 8 "summary": "Initial addition of ocaml-bushel library to the monorepo", 7 9 "changes": [ 8 - "Added ocaml-bushel library as new subtree" 10 + "Added ocaml-bushel library as a new subtree in the monorepo" 9 11 ], 10 12 "commit_range": { 11 13 "from": "96663ea",
+3 -1
.changes/ocaml-conpool-daily.json .changes/ocaml-conpool-2026-01-17.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-17", 6 - "summary": "Improved is_healthy function readability with reduced nesting", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Improved is_healthy function readability with clearer control flow", 7 9 "changes": [ 8 10 "Refactored is_healthy to reduce nesting and improve clarity" 9 11 ],
+4 -2
.changes/ocaml-frontmatter-daily.json .changes/ocaml-frontmatter-2026-01-18.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-18", 6 - "summary": "Initial import of ocaml-frontmatter library into the monorepo", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Initial addition of ocaml-frontmatter library to the monorepo", 7 9 "changes": [ 8 - "Added ocaml-frontmatter library for parsing frontmatter in documents" 10 + "Added ocaml-frontmatter library for parsing YAML/TOML frontmatter in documents" 9 11 ], 10 12 "commit_range": { 11 13 "from": "088ec34",
+27
.changes/ocaml-imap-2026-01-17.json
··· 1 + { 2 + "repository": "ocaml-imap", 3 + "entries": [ 4 + { 5 + "date": "2026-01-17", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Fixed critical IMAP parsing bugs and reorganized library structure into imap/imapd modules.", 9 + "changes": [ 10 + "Fixed RECENT response parsing that was overwriting EXISTS count", 11 + "Changed UIDs and UIDVALIDITY to int64 to handle values up to 4294967295", 12 + "Fixed writer lifecycle bug causing \"cannot write to closed writer\" errors", 13 + "Added Logs library integration for debugging with imap.client source", 14 + "Reorganized lib/ into imap/ (client) and imapd/ (server) with clearer module names" 15 + ], 16 + "commit_range": { 17 + "from": "15fe09d", 18 + "to": "5eb9e22", 19 + "count": 6 20 + }, 21 + "contributors": [ 22 + "Anil Madhavapeddy" 23 + ], 24 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-imap.git" 25 + } 26 + ] 27 + }
+27
.changes/ocaml-imap-2026-01-18.json
··· 1 + { 2 + "repository": "ocaml-imap", 3 + "entries": [ 4 + { 5 + "date": "2026-01-18", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Major expansion of IMAP RFC compliance with new extensions and unified mail-flag library for IMAP/JMAP interoperability.", 9 + "changes": [ 10 + "Added mail-flag library with shared keyword/mailbox types for IMAP/JMAP protocols", 11 + "Added ESEARCH, THREAD, QUOTA, LIST-EXTENDED, UTF-8, and CONDSTORE extensions", 12 + "Added SORT command with sort keys (Arrival, Date, From, Size, Subject, To)", 13 + "Fixed SEARCH response parsing and APPEND literal synchronization (LITERAL+)", 14 + "Added BODY/BODYSTRUCTURE recursive MIME parsing with section specifiers" 15 + ], 16 + "commit_range": { 17 + "from": "2c70927", 18 + "to": "4b0e3e4", 19 + "count": 5 20 + }, 21 + "contributors": [ 22 + "Anil Madhavapeddy" 23 + ], 24 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-imap.git" 25 + } 26 + ] 27 + }
-45
.changes/ocaml-imap-daily.json
··· 1 - { 2 - "repository": "ocaml-imap", 3 - "entries": [ 4 - { 5 - "date": "2026-01-18", 6 - "summary": "Major IMAP RFC compliance update with comprehensive extension support and new mail-flag interoperability library", 7 - "changes": [ 8 - "Added mail-flag library for IMAP/JMAP interop (keywords, mailbox attrs, colors)", 9 - "Added SORT/THREAD extensions (RFC 5256) with sort(), uid_sort() functions", 10 - "Added CONDSTORE (RFC 7162) with changedsince/unchangedsince modifiers", 11 - "Fixed SEARCH response parsing and APPEND literal synchronization bugs", 12 - "Added QUOTA (RFC 9208), ESEARCH (RFC 4731), LIST-EXTENDED (RFC 5258)" 13 - ], 14 - "commit_range": { 15 - "from": "db82956", 16 - "to": "4b0e3e4", 17 - "count": 4 18 - }, 19 - "contributors": [ 20 - "Anil Madhavapeddy" 21 - ], 22 - "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-imap.git" 23 - }, 24 - { 25 - "date": "2026-01-17", 26 - "summary": "Major restructuring of IMAP library with critical bug fixes for UID parsing and response handling.", 27 - "changes": [ 28 - "Reorganized library into lib/imap/ (client) and lib/imapd/ (server) modules", 29 - "Fixed RECENT response parsing that was incorrectly overwriting EXISTS count", 30 - "Changed UIDs and UIDVALIDITY from int32 to int64 to handle large values (RFC 9051)", 31 - "Added Logs library integration with imap.client source for debugging", 32 - "Added AUTHENTICATE PLAIN support via --plain flag in client" 33 - ], 34 - "commit_range": { 35 - "from": "15fe09d", 36 - "to": "5eb9e22", 37 - "count": 6 38 - }, 39 - "contributors": [ 40 - "Anil Madhavapeddy" 41 - ], 42 - "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-imap.git" 43 - } 44 - ] 45 - }
+27
.changes/ocaml-jmap-2026-01-18.json
··· 1 + { 2 + "repository": "ocaml-jmap", 3 + "entries": [ 4 + { 5 + "date": "2026-01-18", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Added unified mail-flag library for IMAP/JMAP email flag interoperability.", 9 + "changes": [ 10 + "Added mail-flag library with Keyword, Mailbox_attr, and Flag_color modules", 11 + "Added RFC 8621 keywords: Seen, Answered, Flagged, Draft, Forwarded, Phishing, etc.", 12 + "Added RFC 6154 mailbox roles: Inbox, Drafts, Sent, Trash, Archive, Snoozed, etc.", 13 + "Added role_of_special_use/special_use_of_role to Mail_mailbox module", 14 + "Added keywords_to_assoc/keywords_of_assoc to Mail_email module" 15 + ], 16 + "commit_range": { 17 + "from": "2c70927", 18 + "to": "db82956", 19 + "count": 2 20 + }, 21 + "contributors": [ 22 + "Anil Madhavapeddy" 23 + ], 24 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-jmap.git" 25 + } 26 + ] 27 + }
-25
.changes/ocaml-jmap-daily.json
··· 1 - { 2 - "repository": "ocaml-jmap", 3 - "entries": [ 4 - { 5 - "date": "2026-01-18", 6 - "summary": "Added mail-flag library for unified email flag handling across IMAP and JMAP protocols", 7 - "changes": [ 8 - "Added Keyword module with RFC 8621 message keywords (Seen, Flagged, Draft, etc.)", 9 - "Added Mailbox_attr module for RFC 6154/5258 mailbox roles (Inbox, Sent, Trash)", 10 - "Added Flag_color module for Apple Mail 7-color flag encoding", 11 - "Added Imap_wire and Jmap_wire adapters for protocol-specific serialization", 12 - "Integrated mail-flag types into ocaml-jmap Keyword and Role modules" 13 - ], 14 - "commit_range": { 15 - "from": "db82956", 16 - "to": "db82956", 17 - "count": 1 18 - }, 19 - "contributors": [ 20 - "Anil Madhavapeddy" 21 - ], 22 - "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-jmap.git" 23 - } 24 - ] 25 - }
+4 -2
.changes/ocaml-jsonwt-daily.json .changes/ocaml-jsonwt-2026-01-17.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-17", 6 - "summary": "Added README documentation for the jsonwt library", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Added README documentation for the library", 7 9 "changes": [ 8 - "Added README file documenting the library" 10 + "Added README file documenting the jsonwt library" 9 11 ], 10 12 "commit_range": { 11 13 "from": "77840a3",
+4 -2
.changes/ocaml-langdetect-daily.json .changes/ocaml-langdetect-2026-01-17.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-17", 6 - "summary": "Fixed documentation to correctly state 49 supported languages instead of 47", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Fixed language count from 47 to 49 in dune-project metadata", 7 9 "changes": [ 8 - "Fixed language count in dune-project: now correctly reports 49 languages" 10 + "Fixed language count accuracy (47→49) in dune-project synopsis" 9 11 ], 10 12 "commit_range": { 11 13 "from": "77840a3",
+23
.changes/ocaml-mail-flag-2026-01-18.json
··· 1 + { 2 + "repository": "ocaml-mail-flag", 3 + "entries": [ 4 + { 5 + "date": "2026-01-18", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Initial import of ocaml-mail-flag library for IMAP flag manipulation", 9 + "changes": [ 10 + "Added ocaml-mail-flag library for parsing and manipulating IMAP flags" 11 + ], 12 + "commit_range": { 13 + "from": "d665de0", 14 + "to": "d665de0", 15 + "count": 1 16 + }, 17 + "contributors": [ 18 + "Anil Madhavapeddy" 19 + ], 20 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-mail-flag.git" 21 + } 22 + ] 23 + }
+5 -4
.changes/ocaml-matrix-daily.json .changes/ocaml-matrix-2026-01-18.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-18", 6 - "summary": "Added pretty-printers, accessors, and interface files to improve API usability and documentation.", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Added pretty-printers, accessors, and missing .mli interface files to ocaml-matrix.", 7 9 "changes": [ 8 - "Added pp functions to Matrix_id modules (User_id, Room_id, etc.)", 10 + "Added pp functions to matrix_id modules (User_id, Room_id, etc.)", 9 11 "Added make constructors and accessors to event content types", 10 - "Added pp functions to Msgtype, Event_type, and message content types", 11 12 "Added 13 missing .mli files for matrix_client modules", 12 - "Fixed all odoc documentation warnings for clean doc builds" 13 + "Fixed odoc documentation warnings for clean @doc-full builds" 13 14 ], 14 15 "commit_range": { 15 16 "from": "39c4ad0",
+3 -1
.changes/ocaml-owntracks-daily.json .changes/ocaml-owntracks-2026-01-17.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-17", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 6 8 "summary": "Added README documentation for the library", 7 9 "changes": [ 8 - "Added README documentation file" 10 + "Added README file documenting library purpose and usage" 9 11 ], 10 12 "commit_range": { 11 13 "from": "7f68d5b",
+4 -2
.changes/ocaml-punycode-daily.json .changes/ocaml-punycode-2026-01-17.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-17", 6 - "summary": "Added documentation about unimplemented IDNA features", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Added documentation for unimplemented IDNA features", 7 9 "changes": [ 8 - "Added README documenting IDNA features not yet implemented" 10 + "Added README documenting unimplemented IDNA 2008 features" 9 11 ], 10 12 "commit_range": { 11 13 "from": "77840a3",
+3 -1
.changes/ocaml-requests-daily.json .changes/ocaml-requests-2026-01-17.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-17", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 6 8 "summary": "Fixed build error caused by missing Uri module re-export in implementation", 7 9 "changes": [ 8 - "Fixed missing `Uri` module re-export that caused build errors" 10 + "Fixed missing Uri module re-export that caused build errors" 9 11 ], 10 12 "commit_range": { 11 13 "from": "8977e4b",
+27
.changes/ocaml-webfinger-2026-01-19.json
··· 1 + { 2 + "repository": "ocaml-webfinger", 3 + "entries": [ 4 + { 5 + "date": "2026-01-19", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:12:08Z", 8 + "summary": "New library providing RFC 7033 WebFinger protocol implementation for OCaml", 9 + "changes": [ 10 + "Added ocaml-webfinger library implementing RFC 7033 WebFinger protocol", 11 + "Added abstract Link and Jrd types with jsont JSON encoding/decoding", 12 + "Added HTTP request support via requests library", 13 + "Added command-line interface with cmdliner for WebFinger lookups", 14 + "Added nullable property support for JRD responses" 15 + ], 16 + "commit_range": { 17 + "from": "04ac158", 18 + "to": "cab4ed5", 19 + "count": 3 20 + }, 21 + "contributors": [ 22 + "Anil Madhavapeddy" 23 + ], 24 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-webfinger.git" 25 + } 26 + ] 27 + }
+5 -3
.changes/ocaml-yamlt-daily.json .changes/ocaml-yamlt-2026-01-18.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-18", 6 - "summary": "Added convenience functions for decoding YAML strings and pre-parsed values", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Added convenience functions for decoding YAML directly from strings and pre-parsed values.", 7 9 "changes": [ 8 - "Added decode_string for direct decoding from YAML strings", 9 - "Added decode_value and decode_value' for decoding from Yamlrw.value" 10 + "Added decode_string for decoding YAML directly from string input", 11 + "Added decode_value and decode_value' for decoding from pre-parsed Yamlrw.value" 10 12 ], 11 13 "commit_range": { 12 14 "from": "2887a07",
+23
.changes/ocaml-zulip-2026-01-17.json
··· 1 + { 2 + "repository": "ocaml-zulip", 3 + "entries": [ 4 + { 5 + "date": "2026-01-17", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Improved documentation for message retention types in channels.mli", 9 + "changes": [ 10 + "Improved retention type documentation in channels.mli" 11 + ], 12 + "commit_range": { 13 + "from": "77840a3", 14 + "to": "77840a3", 15 + "count": 1 16 + }, 17 + "contributors": [ 18 + "Anil Madhavapeddy" 19 + ], 20 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-zulip.git" 21 + } 22 + ] 23 + }
+25
.changes/ocaml-zulip-2026-01-18.json
··· 1 + { 2 + "repository": "ocaml-zulip", 3 + "entries": [ 4 + { 5 + "date": "2026-01-18", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Config module now supports Zulip's native [api] section format alongside existing formats.", 9 + "changes": [ 10 + "Added support for Zulip's native [api] config section with \"key\" field", 11 + "Added xdg_app parameter to Config for custom XDG config paths", 12 + "Config loading now tries [bot], then [api], then bare format automatically" 13 + ], 14 + "commit_range": { 15 + "from": "7a2df8c", 16 + "to": "8b9f2d2", 17 + "count": 2 18 + }, 19 + "contributors": [ 20 + "Anil Madhavapeddy" 21 + ], 22 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-zulip.git" 23 + } 24 + ] 25 + }
+24
.changes/ocaml-zulip-2026-01-19.json
··· 1 + { 2 + "repository": "ocaml-zulip", 3 + "entries": [ 4 + { 5 + "date": "2026-01-19", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:12:08Z", 8 + "summary": "Improved bot functionality and cleaned up build configuration", 9 + "changes": [ 10 + "Improved bot functionality", 11 + "Removed public_name/package from test and example executables" 12 + ], 13 + "commit_range": { 14 + "from": "de14ffa", 15 + "to": "fb94ffb", 16 + "count": 2 17 + }, 18 + "contributors": [ 19 + "Anil Madhavapeddy" 20 + ], 21 + "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-zulip.git" 22 + } 23 + ] 24 + }
-38
.changes/ocaml-zulip-daily.json
··· 1 - { 2 - "repository": "ocaml-zulip", 3 - "entries": [ 4 - { 5 - "date": "2026-01-18", 6 - "summary": "Added xdg_app parameter for custom XDG config paths in zulip-bot", 7 - "changes": [ 8 - "Added xdg_app parameter to Config module for custom XDG directory paths", 9 - "Renamed config file from \"config\" to \"zulip.config\" for clarity" 10 - ], 11 - "commit_range": { 12 - "from": "39b79ed", 13 - "to": "39b79ed", 14 - "count": 1 15 - }, 16 - "contributors": [ 17 - "Anil Madhavapeddy" 18 - ], 19 - "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-zulip.git" 20 - }, 21 - { 22 - "date": "2026-01-17", 23 - "summary": "Improved documentation for retention types in channels.mli", 24 - "changes": [ 25 - "Improved retention type documentation in channels.mli" 26 - ], 27 - "commit_range": { 28 - "from": "77840a3", 29 - "to": "77840a3", 30 - "count": 1 31 - }, 32 - "contributors": [ 33 - "Anil Madhavapeddy" 34 - ], 35 - "repo_url": "https://tangled.org/@anil.recoil.org/ocaml-zulip.git" 36 - } 37 - ] 38 - }
+27
.changes/poe-2026-01-19.json
··· 1 + { 2 + "repository": "poe", 3 + "entries": [ 4 + { 5 + "date": "2026-01-19", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:12:08Z", 8 + "summary": "Added automated changes broadcast system with new polling loop and admin commands", 9 + "changes": [ 10 + "Added `poe loop --interval` command for automated hourly change broadcasting", 11 + "Added admin commands: last-broadcast, reset-broadcast, storage keys/get/delete", 12 + "Added Commands module with deterministic parsing (help, status, broadcast, admin)", 13 + "Added Broadcast module for smart change detection (only sends new changes)", 14 + "Added config options: admin_emails and changes_dir fields" 15 + ], 16 + "commit_range": { 17 + "from": "de14ffa", 18 + "to": "440e98b", 19 + "count": 2 20 + }, 21 + "contributors": [ 22 + "Anil Madhavapeddy" 23 + ], 24 + "repo_url": "https://tangled.org/@anil.recoil.org/poe.git" 25 + } 26 + ] 27 + }
+5 -3
.changes/poe-daily.json .changes/poe-2026-01-18.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-18", 6 - "summary": "Configuration now stored under unified XDG path ~/.config/poe/", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Configuration now stored under unified XDG path (~/.config/poe/)", 7 9 "changes": [ 8 10 "Added xdg_app parameter to zulip-bot Config for custom XDG paths", 9 - "Moved all config files to ~/.config/poe/ directory", 11 + "Changed config location to ~/.config/poe/ for all poe settings", 10 12 "Renamed zulip config file from \"config\" to \"zulip.config\"" 11 13 ], 12 14 "commit_range": { 13 - "from": "39b79ed", 15 + "from": "8b9f2d2", 14 16 "to": "ed3535e", 15 17 "count": 2 16 18 },
+4 -2
.changes/srcsetter-daily.json .changes/srcsetter-2026-01-17.json
··· 3 3 "entries": [ 4 4 { 5 5 "date": "2026-01-17", 6 - "summary": "Added README documentation for the library", 6 + "hour": 16, 7 + "timestamp": "2026-01-19T16:15:30Z", 8 + "summary": "Added README documentation for srcsetter library", 7 9 "changes": [ 8 - "Added README file documenting library purpose and usage" 10 + "Added README file documenting the library" 9 11 ], 10 12 "commit_range": { 11 13 "from": "77840a3",
+50 -30
DAILY-CHANGES.md
··· 1 1 # Daily Changelog 2 2 3 + ## 2026-01-19 4 + 5 + ### New Libraries 6 + 7 + - **[ocaml-webfinger](https://tangled.org/@anil.recoil.org/ocaml-webfinger.git)**: OCaml implementation of RFC 7033 WebFinger protocol for discovering information about resources using standard HTTP. Includes abstract Link and JRD types with jsont JSON encoding/decoding, nullable property support, and a command-line interface built with cmdliner. Uses [ocaml-requests](https://tangled.org/@anil.recoil.org/ocaml-requests.git) for HTTP operations. — *Anil Madhavapeddy* 8 + 9 + ### Major Features 10 + 11 + - **[ocaml-apubt](https://tangled.org/@anil.recoil.org/ocaml-apubt.git)**: Added complete authentication system with XDG-compliant credential storage in ~/.config/apub/. New CLI commands for auth (setup/status/logout), profile management (list/switch/current), and write operations (post, follow, like, boost) with auto-loaded credentials. Integrated [ocaml-webfinger](https://tangled.org/@anil.recoil.org/ocaml-webfinger.git) for RFC 7033/7565 compliant actor discovery. Added Question activity support with one_of, any_of, closed fields. — *Anil Madhavapeddy* 12 + 13 + - **[poe](https://tangled.org/@anil.recoil.org/poe.git)**: Added automated changes broadcast system with new `poe loop --interval` command for hourly change broadcasting. New admin commands (last-broadcast, reset-broadcast, storage keys/get/delete), Commands module with deterministic parsing, and Broadcast module for smart change detection that only sends new changes. Added config options for admin_emails and changes_dir. — *Anil Madhavapeddy* 14 + 15 + - **[monopam](https://tangled.org/@anil.recoil.org/monopam.git)**: Added monopam_changes library with Aggregated and Query modules for structured changelog format. New --aggregate flag for `monopam changes --daily` producing structured JSON output. Daily module with Map-based indexes and query functions (since, for_repo, for_date). Changed daily files from <repo>-daily.json to <repo>-<date>.json with hour tracking. — *Anil Madhavapeddy* 16 + 17 + ### Bug Fixes 18 + 19 + - **[ocaml-atp](https://tangled.org/@anil.recoil.org/ocaml-atp.git)**: Fixed non-deterministic code generation in hermest lexicon generator by sorting alphabetically. Regenerated all lexicon files with deterministic ordering. — *Anil Madhavapeddy* 20 + 21 + ### Code Quality Improvements 22 + 23 + - **[ocaml-zulip](https://tangled.org/@anil.recoil.org/ocaml-zulip.git)**: Improved bot functionality and cleaned up build configuration by removing public_name/package from test and example executables. — *Anil Madhavapeddy* 24 + 25 + --- 26 + 3 27 ## 2026-01-18 4 28 5 29 ### New Libraries 6 30 7 - - **[ocaml-frontmatter](https://tangled.org/@anil.recoil.org/ocaml-frontmatter.git)**: OCaml library for parsing YAML/TOML frontmatter from Markdown and other document formats — commonly used for static site generators and content management systems. — *Anil Madhavapeddy* 31 + - **[ocaml-mail-flag](https://tangled.org/@anil.recoil.org/ocaml-mail-flag.git)**: Unified library for parsing and manipulating email flags across protocols. Provides shared Keyword, Mailbox_attr, and Flag_color modules used by both [ocaml-imap](https://tangled.org/@anil.recoil.org/ocaml-imap.git) and [ocaml-jmap](https://tangled.org/@anil.recoil.org/ocaml-jmap.git) for IMAP/JMAP interoperability. — *Anil Madhavapeddy* 8 32 9 - - **[ocaml-bushel](https://tangled.org/@anil.recoil.org/ocaml-bushel.git)**: OCaml implementation of the Bushel document format — a structured approach to organizing and managing document collections. — *Anil Madhavapeddy* 33 + - **[ocaml-frontmatter](https://tangled.org/@anil.recoil.org/ocaml-frontmatter.git)**: OCaml library for parsing YAML and TOML frontmatter in documents, useful for static site generators and document processors. Uses [ocaml-yamlt](https://tangled.org/@anil.recoil.org/ocaml-yamlt.git) for YAML parsing. — *Anil Madhavapeddy* 10 34 11 - ### Email Protocol Libraries 35 + - **[ocaml-bushel](https://tangled.org/@anil.recoil.org/ocaml-bushel.git)**: Added ocaml-bushel library to the monorepo. — *Anil Madhavapeddy* 12 36 13 - - **[ocaml-imap](https://tangled.org/@anil.recoil.org/ocaml-imap.git)**: Major RFC compliance update with comprehensive extension support — added SORT/THREAD (RFC 5256), CONDSTORE (RFC 7162), QUOTA (RFC 9208), ESEARCH (RFC 4731), and LIST-EXTENDED (RFC 5258). Fixed SEARCH response parsing and APPEND literal synchronization bugs. — *Mark Elvers* and *Anil Madhavapeddy* 37 + ### Email Protocol Improvements 14 38 15 - - **[ocaml-jmap](https://tangled.org/@anil.recoil.org/ocaml-jmap.git)**: Added mail-flag library for unified email flag handling across IMAP and JMAP protocols — includes RFC 8621 keywords (Seen, Flagged, Draft), RFC 6154/5258 mailbox roles (Inbox, Sent, Trash), Apple Mail color flags, and protocol-specific serialization adapters. This library bridges [ocaml-imap](https://tangled.org/@anil.recoil.org/ocaml-imap.git) and [ocaml-jmap](https://tangled.org/@anil.recoil.org/ocaml-jmap.git) for cross-protocol email applications. — *Anil Madhavapeddy* 39 + - **[ocaml-imap](https://tangled.org/@anil.recoil.org/ocaml-imap.git)**: Major expansion of IMAP RFC compliance with ESEARCH, THREAD, QUOTA, LIST-EXTENDED, UTF-8, and CONDSTORE extensions. Added SORT command with sort keys (Arrival, Date, From, Size, Subject, To). Fixed SEARCH response parsing and APPEND literal synchronization (LITERAL+). Added BODY/BODYSTRUCTURE recursive MIME parsing with section specifiers. Integrated [ocaml-mail-flag](https://tangled.org/@anil.recoil.org/ocaml-mail-flag.git) for shared keyword/mailbox types. — *Anil Madhavapeddy* 16 40 17 - ### Configuration & XDG Improvements 41 + - **[ocaml-jmap](https://tangled.org/@anil.recoil.org/ocaml-jmap.git)**: Integrated [ocaml-mail-flag](https://tangled.org/@anil.recoil.org/ocaml-mail-flag.git) library for unified email flag handling. Added RFC 8621 keywords (Seen, Answered, Flagged, Draft, Forwarded, Phishing) and RFC 6154 mailbox roles (Inbox, Drafts, Sent, Trash, Archive, Snoozed). Added role/special_use conversion functions to Mail_mailbox and keywords conversion to Mail_email modules. — *Anil Madhavapeddy* 18 42 19 - - **[poe](https://tangled.org/@anil.recoil.org/poe.git)**: Unified all configuration under `~/.config/poe/` directory with renamed config files for clarity. — *Anil Madhavapeddy* 43 + ### Configuration & Tooling 20 44 21 - - **[ocaml-zulip](https://tangled.org/@anil.recoil.org/ocaml-zulip.git)**: Added `xdg_app` parameter to Config module for custom XDG directory paths; renamed config file from "config" to "zulip.config". — *Anil Madhavapeddy* 45 + - **[poe](https://tangled.org/@anil.recoil.org/poe.git)**: Configuration now stored under unified XDG path (~/.config/poe/). Added xdg_app parameter to zulip-bot Config and renamed zulip config file from "config" to "zulip.config". — *Anil Madhavapeddy* 22 46 23 - ### API Improvements 47 + - **[ocaml-zulip](https://tangled.org/@anil.recoil.org/ocaml-zulip.git)**: Config module now supports Zulip's native [api] section format alongside existing formats. Added xdg_app parameter for custom XDG config paths. Config loading tries [bot], then [api], then bare format automatically. — *Anil Madhavapeddy* 24 48 25 - - **[ocaml-matrix](https://tangled.org/@anil.recoil.org/ocaml-matrix.git)**: Added pretty-printers, accessors, and 13 missing `.mli` interface files — improves API usability with `pp` functions for Matrix_id modules, `make` constructors for event types, and clean odoc builds. — *Anil Madhavapeddy* 49 + - **[monopam](https://tangled.org/@anil.recoil.org/monopam.git)**: Improved push workflow with auto-clone of upstream repos when checkout missing. Fixed tangled.org URL parsing to strip @ prefix from usernames. — *Anil Madhavapeddy* 26 50 27 - - **[ocaml-yamlt](https://tangled.org/@anil.recoil.org/ocaml-yamlt.git)**: Added convenience functions `decode_string`, `decode_value`, and `decode_value'` for more flexible YAML decoding. — *Anil Madhavapeddy* 51 + ### API Improvements 28 52 29 - ### Tooling 53 + - **[ocaml-yamlt](https://tangled.org/@anil.recoil.org/ocaml-yamlt.git)**: Added convenience functions decode_string, decode_value, and decode_value' for decoding YAML directly from strings and pre-parsed Yamlrw.value types. — *Anil Madhavapeddy* 30 54 31 - - **[monopam](https://tangled.org/@anil.recoil.org/monopam.git)**: `monopam push` now auto-creates missing checkout directories — new packages no longer require manual pull first. — *Anil Madhavapeddy* 55 + - **[ocaml-matrix](https://tangled.org/@anil.recoil.org/ocaml-matrix.git)**: Added pp functions to matrix_id modules (User_id, Room_id, etc.), make constructors and accessors to event content types. Added 13 missing .mli files for matrix_client modules. Fixed odoc documentation warnings for clean @doc-full builds. — *Anil Madhavapeddy* 32 56 33 - - **[ocaml-atp](https://tangled.org/@anil.recoil.org/ocaml-atp.git)**: Regenerated AT Protocol lexicon bindings (atproto, bsky, standard-site, tangled) from upstream schemas. — *Anil Madhavapeddy* 57 + - **[ocaml-atp](https://tangled.org/@anil.recoil.org/ocaml-atp.git)**: Regenerated OCaml bindings for atproto, bsky, standard-site, and tangled lexicons. — *Anil Madhavapeddy* 34 58 35 59 --- 36 60 ··· 38 62 39 63 ### Major Features 40 64 41 - - **[ocaml-imap](https://tangled.org/@anil.recoil.org/ocaml-imap.git)**: Major restructuring with critical bug fixes — reorganized into `lib/imap/` (client) and `lib/imapd/` (server), fixed RECENT response parsing that was overwriting EXISTS count, changed UIDs/UIDVALIDITY from int32 to int64 for RFC 9051 compliance, added Logs integration and AUTHENTICATE PLAIN support. — *Anil Madhavapeddy* 42 - 43 - - **[monopam](https://tangled.org/@anil.recoil.org/monopam.git)**: New `monopam changes` command uses AI to generate changelogs from git history — added Changes module with jsont codecs, Git.log with date filtering, and aggregated CHANGES.md generation at monorepo root. — *Anil Madhavapeddy* 65 + - **[monopam](https://tangled.org/@anil.recoil.org/monopam.git)**: New 'monopam changes' command generates AI-powered changelogs from git history. Added Changes module with jsont codecs for changelog serialization, Git.log function with date filtering, Claude AI integration for intelligent commit analysis, and aggregated CHANGES.md generation at monorepo root. — *Anil Madhavapeddy* 44 66 45 - ### Bug Fixes 67 + ### Critical Bug Fixes 46 68 47 - - **[ocaml-requests](https://tangled.org/@anil.recoil.org/ocaml-requests.git)**: Fixed missing `Uri` module re-export that caused build errors. — *Anil Madhavapeddy* 69 + - **[ocaml-imap](https://tangled.org/@anil.recoil.org/ocaml-imap.git)**: Fixed RECENT response parsing that was overwriting EXISTS count. Changed UIDs and UIDVALIDITY to int64 to handle values up to 4294967295. Fixed writer lifecycle bug causing "cannot write to closed writer" errors. Added Logs library integration for debugging. Reorganized lib/ into imap/ (client) and imapd/ (server) with clearer module names. — *Anil Madhavapeddy* 48 70 49 - ### Code Quality 71 + - **[ocaml-requests](https://tangled.org/@anil.recoil.org/ocaml-requests.git)**: Fixed missing Uri module re-export that caused build errors. — *Anil Madhavapeddy* 50 72 51 - - **[ocaml-conpool](https://tangled.org/@anil.recoil.org/ocaml-conpool.git)**: Refactored `is_healthy` function to reduce nesting and improve readability. — *Anil Madhavapeddy* 73 + ### Code Quality Improvements 52 74 53 - ### Documentation 75 + - **[ocaml-conpool](https://tangled.org/@anil.recoil.org/ocaml-conpool.git)**: Refactored is_healthy function to reduce nesting and improve clarity. — *Anil Madhavapeddy* 54 76 55 77 - **[ocaml-zulip](https://tangled.org/@anil.recoil.org/ocaml-zulip.git)**: Improved retention type documentation in channels.mli. — *Anil Madhavapeddy* 56 78 57 - - **[ocaml-langdetect](https://tangled.org/@anil.recoil.org/ocaml-langdetect.git)**: Fixed language count in dune-project to correctly report 49 supported languages. — *Anil Madhavapeddy* 58 - 59 - - **[ocaml-punycode](https://tangled.org/@anil.recoil.org/ocaml-punycode.git)**: Added README documenting IDNA features not yet implemented. — *Anil Madhavapeddy* 60 - 61 - - **[ocaml-jsonwt](https://tangled.org/@anil.recoil.org/ocaml-jsonwt.git)**: Added README documentation. — *Anil Madhavapeddy* 79 + ### Documentation Updates 62 80 63 - - **[ocaml-owntracks](https://tangled.org/@anil.recoil.org/ocaml-owntracks.git)**: Added README documentation. — *Anil Madhavapeddy* 64 - 65 - - **[srcsetter](https://tangled.org/@anil.recoil.org/srcsetter.git)**: Added README documentation. — *Anil Madhavapeddy* 81 + - **[srcsetter](https://tangled.org/@anil.recoil.org/srcsetter.git)**: Added README documentation for the library. — *Anil Madhavapeddy* 82 + - **[ocaml-punycode](https://tangled.org/@anil.recoil.org/ocaml-punycode.git)**: Added README documenting unimplemented IDNA 2008 features. — *Anil Madhavapeddy* 83 + - **[ocaml-owntracks](https://tangled.org/@anil.recoil.org/ocaml-owntracks.git)**: Added README file documenting library purpose and usage. — *Anil Madhavapeddy* 84 + - **[ocaml-jsonwt](https://tangled.org/@anil.recoil.org/ocaml-jsonwt.git)**: Added README file documenting the jsonwt library. — *Anil Madhavapeddy* 85 + - **[ocaml-langdetect](https://tangled.org/@anil.recoil.org/ocaml-langdetect.git)**: Fixed language count accuracy (47→49) in dune-project synopsis. — *Anil Madhavapeddy*
+50 -18
monopam/lib/changes.ml
··· 5 5 6 6 Changes are stored in a .changes directory at the monorepo root: 7 7 - .changes/<repo_name>.json - weekly changelog entries 8 - - .changes/<repo_name>-daily.json - daily changelog entries *) 8 + - .changes/<repo_name>-<YYYY-MM-DD>.json - daily changelog entries (one file per day per repo) *) 9 9 10 10 type commit_range = { 11 11 from_hash : string; ··· 23 23 24 24 type daily_entry = { 25 25 date : string; (* ISO date YYYY-MM-DD *) 26 + hour : int; (* Hour of day 0-23 *) 27 + timestamp : Ptime.t; (* RFC3339 timestamp for precise ordering *) 26 28 summary : string; (* One-line summary *) 27 29 changes : string list; (* Bullet points *) 28 30 commit_range : commit_range; ··· 72 74 |> Jsont.Object.mem "entries" (Jsont.list weekly_entry_jsont) ~enc:(fun (f : changes_file) -> f.entries) 73 75 |> Jsont.Object.finish 74 76 77 + let ptime_jsont = 78 + let enc t = 79 + Ptime.to_rfc3339 t ~tz_offset_s:0 80 + in 81 + let dec s = 82 + match Ptime.of_rfc3339 s with 83 + | Ok (t, _, _) -> t 84 + | Error _ -> failwith ("Invalid timestamp: " ^ s) 85 + in 86 + Jsont.map ~dec ~enc Jsont.string 87 + 75 88 let daily_entry_jsont : daily_entry Jsont.t = 76 - let make date summary changes commit_range contributors repo_url : daily_entry = 77 - { date; summary; changes; commit_range; contributors; repo_url } 89 + let make date hour timestamp summary changes commit_range contributors repo_url : daily_entry = 90 + { date; hour; timestamp; summary; changes; commit_range; contributors; repo_url } 78 91 in 92 + (* Default hour and timestamp for backwards compat when reading old files *) 93 + let default_hour = 0 in 94 + let default_timestamp = Ptime.epoch in 79 95 Jsont.Object.map ~kind:"daily_entry" make 80 96 |> Jsont.Object.mem "date" Jsont.string ~enc:(fun (e : daily_entry) -> e.date) 97 + |> Jsont.Object.mem "hour" Jsont.int ~dec_absent:default_hour ~enc:(fun (e : daily_entry) -> e.hour) 98 + |> Jsont.Object.mem "timestamp" ptime_jsont ~dec_absent:default_timestamp ~enc:(fun (e : daily_entry) -> e.timestamp) 81 99 |> Jsont.Object.mem "summary" Jsont.string ~enc:(fun (e : daily_entry) -> e.summary) 82 100 |> Jsont.Object.mem "changes" (Jsont.list Jsont.string) ~enc:(fun (e : daily_entry) -> e.changes) 83 101 |> Jsont.Object.mem "commit_range" commit_range_jsont ~enc:(fun (e : daily_entry) -> e.commit_range) ··· 124 142 Ok () 125 143 | Error e -> Error (Format.sprintf "Failed to encode %s.json: %s" cf.repository e) 126 144 127 - (* Load daily changes from .changes/<repo>-daily.json in monorepo *) 128 - let load_daily ~fs ~monorepo repo_name = 129 - let file_path = Eio.Path.(fs / Fpath.to_string monorepo / ".changes" / (repo_name ^ "-daily.json")) in 145 + (* Filename for daily changes: <repo>-<YYYY-MM-DD>.json *) 146 + let daily_filename repo_name date = 147 + repo_name ^ "-" ^ date ^ ".json" 148 + 149 + (* Load daily changes from .changes/<repo>-<date>.json in monorepo *) 150 + let load_daily ~fs ~monorepo ~date repo_name = 151 + let filename = daily_filename repo_name date in 152 + let file_path = Eio.Path.(fs / Fpath.to_string monorepo / ".changes" / filename) in 130 153 match Eio.Path.kind ~follow:true file_path with 131 154 | `Regular_file -> ( 132 155 let content = Eio.Path.load file_path in 133 156 match Jsont_bytesrw.decode_string daily_changes_file_jsont content with 134 157 | Ok cf -> Ok cf 135 - | Error e -> Error (Format.sprintf "Failed to parse %s-daily.json: %s" repo_name e)) 158 + | Error e -> Error (Format.sprintf "Failed to parse %s: %s" filename e)) 136 159 | _ -> Ok { repository = repo_name; entries = [] } 137 160 | exception Eio.Io _ -> Ok { repository = repo_name; entries = [] } 138 161 139 - (* Save daily changes to .changes/<repo>-daily.json in monorepo *) 140 - let save_daily ~fs ~monorepo (cf : daily_changes_file) = 162 + (* Save daily changes to .changes/<repo>-<date>.json in monorepo *) 163 + let save_daily ~fs ~monorepo ~date (cf : daily_changes_file) = 141 164 ensure_changes_dir ~fs monorepo; 142 - let file_path = Eio.Path.(fs / Fpath.to_string monorepo / ".changes" / (cf.repository ^ "-daily.json")) in 165 + let filename = daily_filename cf.repository date in 166 + let file_path = Eio.Path.(fs / Fpath.to_string monorepo / ".changes" / filename) in 143 167 match Jsont_bytesrw.encode_string ~format:Jsont.Indent daily_changes_file_jsont cf with 144 168 | Ok content -> 145 169 Eio.Path.save ~create:(`Or_truncate 0o644) file_path content; 146 170 Ok () 147 - | Error e -> Error (Format.sprintf "Failed to encode %s-daily.json: %s" cf.repository e) 171 + | Error e -> Error (Format.sprintf "Failed to encode %s: %s" filename e) 148 172 149 173 (* Markdown generation *) 150 174 ··· 276 300 let (y, m, d), _ = Ptime.to_date_time t in 277 301 format_date (y, m, d) 278 302 279 - let has_day (cf : daily_changes_file) ~date = 280 - List.exists (fun (e : daily_entry) -> e.date = date) cf.entries 303 + let has_day (cf : daily_changes_file) ~date:_ = 304 + (* With per-day files, the file is already for a specific date. 305 + This function now checks if the file has any entries. *) 306 + cf.entries <> [] 281 307 282 308 (* Aggregate daily changes into DAILY-CHANGES.md *) 283 309 let aggregate_daily ~history (cfs : daily_changes_file list) = ··· 726 752 let generate_aggregated ~fs ~monorepo ~date ~git_head = 727 753 let changes_dir = Eio.Path.(fs / Fpath.to_string monorepo / ".changes") in 728 754 729 - (* List all *-daily.json files *) 755 + (* List all *-<date>.json files (new per-day format) *) 730 756 let files = 731 757 try Eio.Path.read_dir changes_dir 732 758 with Eio.Io _ -> [] 733 759 in 760 + (* Match files like "<repo>-2026-01-19.json" for the given date *) 761 + let date_suffix = "-" ^ date ^ ".json" in 762 + let date_suffix_len = String.length date_suffix in 734 763 let daily_files = List.filter (fun f -> 735 - String.ends_with ~suffix:"-daily.json" f) files 764 + String.ends_with ~suffix:date_suffix f && String.length f > date_suffix_len) files 736 765 in 737 766 738 - (* Load all daily files and collect entries for the target date *) 767 + (* Load all daily files for this date and collect entries *) 739 768 let entries = List.concat_map (fun filename -> 740 - let repo_name = String.sub filename 0 (String.length filename - 11) in 769 + (* Extract repo name: filename is "<repo>-<date>.json" *) 770 + let repo_name = String.sub filename 0 (String.length filename - date_suffix_len) in 741 771 let path = Eio.Path.(changes_dir / filename) in 742 772 try 743 773 let content = Eio.Path.load path in 744 774 match Jsont_bytesrw.decode_string daily_changes_file_jsont content with 745 775 | Ok dcf -> 746 776 List.filter_map (fun (e : daily_entry) -> 747 - if e.date = date && e.changes <> [] then 777 + if e.changes <> [] then 748 778 Some (repo_name, e) 749 779 else 750 780 None) dcf.entries ··· 758 788 let change_type = infer_change_type e.summary in 759 789 Monopam_changes.Aggregated.{ 760 790 repository = repo_name; 791 + hour = e.hour; 792 + timestamp = e.timestamp; 761 793 summary = e.summary; 762 794 changes = e.changes; 763 795 commit_range = {
+11 -7
monopam/lib/changes.mli
··· 5 5 6 6 Changes are stored in a .changes directory at the monorepo root: 7 7 - .changes/<repo_name>.json - weekly changelog entries 8 - - .changes/<repo_name>-daily.json - daily changelog entries *) 8 + - .changes/<repo_name>-<YYYY-MM-DD>.json - daily changelog entries (one file per day per repo) *) 9 9 10 10 (** {1 Types} *) 11 11 ··· 27 27 28 28 type daily_entry = { 29 29 date : string; (** ISO date YYYY-MM-DD *) 30 + hour : int; (** Hour of day 0-23 for filtering *) 31 + timestamp : Ptime.t; (** RFC3339 timestamp for precise ordering *) 30 32 summary : string; (** One-line summary *) 31 33 changes : string list; (** Bullet points *) 32 34 commit_range : commit_range; 33 35 contributors : string list; (** List of contributors for this entry *) 34 36 repo_url : string option; (** Upstream repository URL *) 35 37 } 36 - (** A single day's changelog entry. *) 38 + (** A single day's changelog entry with hour tracking for real-time updates. *) 37 39 38 40 type changes_file = { 39 41 repository : string; ··· 76 78 val save : fs:_ Eio.Path.t -> monorepo:Fpath.t -> changes_file -> (unit, string) result 77 79 (** [save ~fs ~monorepo cf] saves the changes file to .changes/<repo_name>.json. *) 78 80 79 - val load_daily : fs:_ Eio.Path.t -> monorepo:Fpath.t -> string -> (daily_changes_file, string) result 80 - (** [load_daily ~fs ~monorepo repo_name] loads daily changes from .changes/<repo_name>-daily.json. 81 - Returns an empty changes file if the file does not exist. *) 81 + val load_daily : fs:_ Eio.Path.t -> monorepo:Fpath.t -> date:string -> string -> (daily_changes_file, string) result 82 + (** [load_daily ~fs ~monorepo ~date repo_name] loads daily changes from .changes/<repo_name>-<date>.json. 83 + Returns an empty changes file if the file does not exist. 84 + @param date Date in YYYY-MM-DD format *) 82 85 83 - val save_daily : fs:_ Eio.Path.t -> monorepo:Fpath.t -> daily_changes_file -> (unit, string) result 84 - (** [save_daily ~fs ~monorepo cf] saves the changes file to .changes/<repo_name>-daily.json. *) 86 + val save_daily : fs:_ Eio.Path.t -> monorepo:Fpath.t -> date:string -> daily_changes_file -> (unit, string) result 87 + (** [save_daily ~fs ~monorepo ~date cf] saves the changes file to .changes/<repo_name>-<date>.json. 88 + @param date Date in YYYY-MM-DD format *) 85 89 86 90 (** {1 Markdown Generation} *) 87 91
+41 -46
monopam/lib/monopam.ml
··· 1041 1041 1042 1042 Log.info (fun m -> m "Processing %s" repo_name); 1043 1043 1044 - (* Load existing daily changes from .changes/<repo>-daily.json *) 1045 - match Changes.load_daily ~fs:fs_t ~monorepo repo_name with 1046 - | Error e -> Error (Claude_error e) 1047 - | Ok changes_file -> 1048 - (* Process each day *) 1049 - let rec process_days day_offset updated_cf = 1050 - if day_offset >= days then Ok updated_cf 1051 - else begin 1052 - (* Calculate day boundaries *) 1053 - let offset_seconds = float_of_int (day_offset * 24 * 60 * 60) in 1054 - let day_time = match Ptime.of_float_s (now -. offset_seconds) with 1055 - | Some t -> t 1056 - | None -> now_ptime 1057 - in 1058 - let date = Changes.date_of_ptime day_time in 1044 + (* Process each day - with per-day files, we load/save per day *) 1045 + let rec process_days day_offset = 1046 + if day_offset >= days then Ok () 1047 + else begin 1048 + (* Calculate day boundaries *) 1049 + let offset_seconds = float_of_int (day_offset * 24 * 60 * 60) in 1050 + let day_time = match Ptime.of_float_s (now -. offset_seconds) with 1051 + | Some t -> t 1052 + | None -> now_ptime 1053 + in 1054 + let date = Changes.date_of_ptime day_time in 1059 1055 1056 + (* Load existing daily changes from .changes/<repo>-<date>.json *) 1057 + match Changes.load_daily ~fs:fs_t ~monorepo ~date repo_name with 1058 + | Error e -> Error (Claude_error e) 1059 + | Ok changes_file -> 1060 1060 (* Skip if day already has an entry *) 1061 - if Changes.has_day updated_cf ~date then begin 1061 + if Changes.has_day changes_file ~date then begin 1062 1062 Log.info (fun m -> m " Day %s already has entry, skipping" date); 1063 - process_days (day_offset + 1) updated_cf 1063 + all_changes_files := changes_file :: !all_changes_files; 1064 + process_days (day_offset + 1) 1064 1065 end 1065 1066 else begin 1066 1067 (* Get commits for this day *) ··· 1071 1072 | Ok commits -> 1072 1073 if commits = [] then begin 1073 1074 Log.info (fun m -> m " No commits for day %s" date); 1074 - process_days (day_offset + 1) updated_cf 1075 + process_days (day_offset + 1) 1075 1076 end 1076 1077 else begin 1077 1078 Log.info (fun m -> m " Found %d commits for day %s" (List.length commits) date); ··· 1079 1080 if dry_run then begin 1080 1081 Log.app (fun m -> m " [DRY RUN] Would analyze %d commits for %s on %s" 1081 1082 (List.length commits) repo_name date); 1082 - process_days (day_offset + 1) updated_cf 1083 + process_days (day_offset + 1) 1083 1084 end 1084 1085 else begin 1085 1086 (* Analyze commits with Claude *) ··· 1089 1090 | Error e -> Error (Claude_error e) 1090 1091 | Ok None -> 1091 1092 Log.info (fun m -> m " No user-facing changes for day %s" date); 1092 - process_days (day_offset + 1) updated_cf 1093 + process_days (day_offset + 1) 1093 1094 | Ok (Some response) -> 1094 1095 Log.app (fun m -> m " Generated changelog for %s on %s" repo_name date); 1095 1096 (* Extract unique contributors from commits *) ··· 1108 1109 else 1109 1110 Some url 1110 1111 in 1111 - (* Create new entry *) 1112 + (* Create new entry with hour and timestamp *) 1112 1113 let first_hash = (List.hd commits).Git.hash in 1113 1114 let last_hash = (List.hd (List.rev commits)).Git.hash in 1115 + let (_, ((hour, _, _), _)) = Ptime.to_date_time now_ptime in 1114 1116 let entry : Changes.daily_entry = { 1115 1117 date; 1118 + hour; 1119 + timestamp = now_ptime; 1116 1120 summary = response.Changes.summary; 1117 1121 changes = response.Changes.changes; 1118 1122 commit_range = { ··· 1123 1127 contributors; 1124 1128 repo_url; 1125 1129 } in 1126 - (* Add entry (sorted by date descending) *) 1130 + (* Add entry (sorted by timestamp descending) *) 1127 1131 let new_entries = 1128 - entry :: updated_cf.Changes.entries 1132 + entry :: changes_file.Changes.entries 1129 1133 |> List.sort (fun e1 e2 -> 1130 - String.compare e2.Changes.date e1.Changes.date) 1134 + Ptime.compare e2.Changes.timestamp e1.Changes.timestamp) 1131 1135 in 1132 - process_days (day_offset + 1) 1133 - { updated_cf with entries = new_entries } 1136 + let updated_cf = { changes_file with Changes.entries = new_entries } in 1137 + (* Save the per-day file *) 1138 + match Changes.save_daily ~fs:fs_t ~monorepo ~date updated_cf with 1139 + | Error e -> Error (Claude_error e) 1140 + | Ok () -> 1141 + Log.app (fun m -> m "Saved .changes/%s-%s.json" repo_name date); 1142 + all_changes_files := updated_cf :: !all_changes_files; 1143 + process_days (day_offset + 1) 1134 1144 end 1135 1145 end 1136 1146 end 1137 - end 1138 - in 1139 - match process_days 0 changes_file with 1140 - | Error e -> Error e 1141 - | Ok updated_cf -> 1142 - (* Save if changed and not dry run *) 1143 - let save_result = 1144 - if not dry_run && updated_cf.entries <> changes_file.entries then 1145 - match Changes.save_daily ~fs:fs_t ~monorepo updated_cf with 1146 - | Error e -> Error (Claude_error e) 1147 - | Ok () -> 1148 - Log.app (fun m -> m "Saved .changes/%s-daily.json" repo_name); 1149 - Ok () 1150 - else Ok () 1151 - in 1152 - match save_result with 1153 - | Error e -> Error e 1154 - | Ok () -> 1155 - all_changes_files := updated_cf :: !all_changes_files; 1156 - process_repos rest 1147 + end 1148 + in 1149 + match process_days 0 with 1150 + | Error e -> Error e 1151 + | Ok () -> process_repos rest 1157 1152 in 1158 1153 match process_repos repos with 1159 1154 | Error e -> Error e
+20 -14
monopam/lib_changes/aggregated.ml
··· 35 35 36 36 type entry = { 37 37 repository : string; 38 + hour : int; 39 + timestamp : Ptime.t; 38 40 summary : string; 39 41 changes : string list; 40 42 commit_range : commit_range; ··· 71 73 |> Jsont.Object.mem "count" Jsont.int ~enc:(fun r -> r.count) 72 74 |> Jsont.Object.finish 73 75 76 + let ptime_jsont = 77 + let enc t = 78 + Ptime.to_rfc3339 t ~tz_offset_s:0 79 + in 80 + let dec s = 81 + match Ptime.of_rfc3339 s with 82 + | Ok (t, _, _) -> t 83 + | Error _ -> failwith ("Invalid timestamp: " ^ s) 84 + in 85 + Jsont.map ~dec ~enc Jsont.string 86 + 74 87 let entry_jsont = 75 - let make repository summary changes commit_range contributors repo_url change_type = 76 - { repository; summary; changes; commit_range; contributors; repo_url; change_type } 88 + let make repository hour timestamp summary changes commit_range contributors repo_url change_type = 89 + { repository; hour; timestamp; summary; changes; commit_range; contributors; repo_url; change_type } 77 90 in 91 + (* Default hour and timestamp for backwards compat when reading old files *) 92 + let default_hour = 0 in 93 + let default_timestamp = Ptime.epoch in 78 94 Jsont.Object.map ~kind:"aggregated_entry" make 79 95 |> Jsont.Object.mem "repository" Jsont.string ~enc:(fun e -> e.repository) 96 + |> Jsont.Object.mem "hour" Jsont.int ~dec_absent:default_hour ~enc:(fun e -> e.hour) 97 + |> Jsont.Object.mem "timestamp" ptime_jsont ~dec_absent:default_timestamp ~enc:(fun e -> e.timestamp) 80 98 |> Jsont.Object.mem "summary" Jsont.string ~enc:(fun e -> e.summary) 81 99 |> Jsont.Object.mem "changes" (Jsont.list Jsont.string) ~enc:(fun e -> e.changes) 82 100 |> Jsont.Object.mem "commit_range" commit_range_jsont ~enc:(fun e -> e.commit_range) ··· 84 102 |> Jsont.Object.mem "repo_url" (Jsont.option Jsont.string) ~dec_absent:None ~enc:(fun e -> e.repo_url) 85 103 |> Jsont.Object.mem "change_type" change_type_jsont ~dec_absent:Unknown ~enc:(fun e -> e.change_type) 86 104 |> Jsont.Object.finish 87 - 88 - let ptime_jsont = 89 - let enc t = 90 - match Ptime.to_rfc3339 t ~tz_offset_s:0 with 91 - | s -> s 92 - in 93 - let dec s = 94 - match Ptime.of_rfc3339 s with 95 - | Ok (t, _, _) -> t 96 - | Error _ -> failwith ("Invalid timestamp: " ^ s) 97 - in 98 - Jsont.map ~dec ~enc Jsont.string 99 105 100 106 let jsont = 101 107 let make date generated_at git_head entries authors =
+2
monopam/lib_changes/aggregated.mli
··· 36 36 (** A single repository's changes for the day. *) 37 37 type entry = { 38 38 repository : string; (** Repository name *) 39 + hour : int; (** Hour of day 0-23 for filtering *) 40 + timestamp : Ptime.t; (** RFC3339 timestamp for precise ordering *) 39 41 summary : string; (** One-line summary of changes *) 40 42 changes : string list; (** List of change bullet points *) 41 43 commit_range : commit_range; (** Commits included *)
+246
monopam/lib_changes/daily.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2026 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + type commit_range = { 7 + from_hash : string; 8 + to_hash : string; 9 + count : int; 10 + } 11 + 12 + type entry = { 13 + repository : string; 14 + hour : int; 15 + timestamp : Ptime.t; 16 + summary : string; 17 + changes : string list; 18 + commit_range : commit_range; 19 + contributors : string list; 20 + repo_url : string option; 21 + } 22 + 23 + type day = { 24 + repository : string; 25 + date : string; 26 + entries : entry list; 27 + } 28 + 29 + module String_map = Map.Make(String) 30 + 31 + type t = { 32 + by_repo : day list String_map.t; 33 + by_date : day list String_map.t; 34 + all_entries : entry list; 35 + } 36 + 37 + (* JSON codecs for the per-day file format *) 38 + 39 + let commit_range_jsont = 40 + let make from_hash to_hash count = { from_hash; to_hash; count } in 41 + Jsont.Object.map ~kind:"commit_range" make 42 + |> Jsont.Object.mem "from" Jsont.string ~enc:(fun r -> r.from_hash) 43 + |> Jsont.Object.mem "to" Jsont.string ~enc:(fun r -> r.to_hash) 44 + |> Jsont.Object.mem "count" Jsont.int ~enc:(fun r -> r.count) 45 + |> Jsont.Object.finish 46 + 47 + let ptime_jsont = 48 + let enc t = Ptime.to_rfc3339 t ~tz_offset_s:0 in 49 + let dec s = 50 + match Ptime.of_rfc3339 s with 51 + | Ok (t, _, _) -> t 52 + | Error _ -> failwith ("Invalid timestamp: " ^ s) 53 + in 54 + Jsont.map ~dec ~enc Jsont.string 55 + 56 + (* Entry codec for the file format (without repository, added during load) *) 57 + type file_entry = { 58 + hour : int; 59 + timestamp : Ptime.t; 60 + summary : string; 61 + changes : string list; 62 + commit_range : commit_range; 63 + contributors : string list; 64 + repo_url : string option; 65 + } 66 + 67 + let file_entry_jsont = 68 + let make hour timestamp summary changes commit_range contributors repo_url = 69 + { hour; timestamp; summary; changes; commit_range; contributors; repo_url } 70 + in 71 + let default_hour = 0 in 72 + let default_timestamp = Ptime.epoch in 73 + Jsont.Object.map ~kind:"daily_entry" make 74 + |> Jsont.Object.mem "hour" Jsont.int ~dec_absent:default_hour ~enc:(fun e -> e.hour) 75 + |> Jsont.Object.mem "timestamp" ptime_jsont ~dec_absent:default_timestamp ~enc:(fun e -> e.timestamp) 76 + |> Jsont.Object.mem "summary" Jsont.string ~enc:(fun e -> e.summary) 77 + |> Jsont.Object.mem "changes" (Jsont.list Jsont.string) ~enc:(fun e -> e.changes) 78 + |> Jsont.Object.mem "commit_range" commit_range_jsont ~enc:(fun e -> e.commit_range) 79 + |> Jsont.Object.mem "contributors" (Jsont.list Jsont.string) ~dec_absent:[] ~enc:(fun e -> e.contributors) 80 + |> Jsont.Object.mem "repo_url" (Jsont.option Jsont.string) ~dec_absent:None ~enc:(fun e -> e.repo_url) 81 + |> Jsont.Object.finish 82 + 83 + type json_file = { 84 + json_repository : string; 85 + json_entries : file_entry list; 86 + } 87 + 88 + let json_file_jsont = 89 + let make json_repository json_entries = { json_repository; json_entries } in 90 + Jsont.Object.map ~kind:"daily_changes_file" make 91 + |> Jsont.Object.mem "repository" Jsont.string ~enc:(fun f -> f.json_repository) 92 + |> Jsont.Object.mem "entries" (Jsont.list file_entry_jsont) ~enc:(fun f -> f.json_entries) 93 + |> Jsont.Object.finish 94 + 95 + (* Parse date from filename: <repo>-<YYYY-MM-DD>.json *) 96 + let parse_daily_filename filename = 97 + (* Check for pattern: ends with -YYYY-MM-DD.json *) 98 + let len = String.length filename in 99 + if len < 16 || not (String.ends_with ~suffix:".json" filename) then 100 + None 101 + else 102 + (* Try to extract date: last 15 chars are -YYYY-MM-DD.json *) 103 + let date_start = len - 15 in 104 + let potential_date = String.sub filename (date_start + 1) 10 in 105 + (* Validate date format YYYY-MM-DD *) 106 + if String.length potential_date = 10 && 107 + potential_date.[4] = '-' && potential_date.[7] = '-' then 108 + let repo = String.sub filename 0 date_start in 109 + Some (repo, potential_date) 110 + else 111 + None 112 + 113 + (* Load a single daily file *) 114 + let load_file ~fs ~changes_dir ~repo ~date : entry list = 115 + let filename = repo ^ "-" ^ date ^ ".json" in 116 + let file_path = Eio.Path.(fs / Fpath.to_string changes_dir / filename) in 117 + match Eio.Path.kind ~follow:true file_path with 118 + | `Regular_file -> ( 119 + let content = Eio.Path.load file_path in 120 + match Jsont_bytesrw.decode_string json_file_jsont content with 121 + | Ok jf -> 122 + List.map (fun (fe : file_entry) : entry -> 123 + { repository = repo; 124 + hour = fe.hour; 125 + timestamp = fe.timestamp; 126 + summary = fe.summary; 127 + changes = fe.changes; 128 + commit_range = fe.commit_range; 129 + contributors = fe.contributors; 130 + repo_url = fe.repo_url; 131 + }) jf.json_entries 132 + | Error _ -> []) 133 + | _ -> [] 134 + | exception Eio.Io _ -> [] 135 + 136 + let empty = { 137 + by_repo = String_map.empty; 138 + by_date = String_map.empty; 139 + all_entries = []; 140 + } 141 + 142 + let list_repos ~fs ~changes_dir = 143 + let dir_path = Eio.Path.(fs / Fpath.to_string changes_dir) in 144 + match Eio.Path.kind ~follow:true dir_path with 145 + | `Directory -> 146 + let files = Eio.Path.read_dir dir_path in 147 + files 148 + |> List.filter_map parse_daily_filename 149 + |> List.map fst 150 + |> List.sort_uniq String.compare 151 + | _ -> [] 152 + | exception Eio.Io _ -> [] 153 + 154 + let list_dates ~fs ~changes_dir ~repo = 155 + let dir_path = Eio.Path.(fs / Fpath.to_string changes_dir) in 156 + match Eio.Path.kind ~follow:true dir_path with 157 + | `Directory -> 158 + let files = Eio.Path.read_dir dir_path in 159 + files 160 + |> List.filter_map (fun filename -> 161 + match parse_daily_filename filename with 162 + | Some (r, date) when r = repo -> Some date 163 + | _ -> None) 164 + |> List.sort (fun a b -> String.compare b a) (* descending *) 165 + | _ -> [] 166 + | exception Eio.Io _ -> [] 167 + 168 + let load_repo_day ~fs ~changes_dir ~repo ~date = 169 + load_file ~fs ~changes_dir ~repo ~date 170 + 171 + let load_repo_all ~fs ~changes_dir ~repo = 172 + let dates = list_dates ~fs ~changes_dir ~repo in 173 + List.concat_map (fun date -> load_file ~fs ~changes_dir ~repo ~date) dates 174 + 175 + let load_all ~fs ~changes_dir = 176 + let dir_path = Eio.Path.(fs / Fpath.to_string changes_dir) in 177 + match Eio.Path.kind ~follow:true dir_path with 178 + | `Directory -> 179 + let files = Eio.Path.read_dir dir_path in 180 + let parsed_files = List.filter_map parse_daily_filename files in 181 + 182 + (* Load all files and build days *) 183 + let days : day list = List.filter_map (fun (repo, date) -> 184 + let loaded_entries : entry list = load_file ~fs ~changes_dir ~repo ~date in 185 + if loaded_entries = [] then None 186 + else 187 + let sorted_entries : entry list = List.sort (fun (e1 : entry) (e2 : entry) -> 188 + Ptime.compare e1.timestamp e2.timestamp) loaded_entries 189 + in 190 + Some ({ repository = repo; date; entries = sorted_entries } : day) 191 + ) parsed_files in 192 + 193 + (* Build by_repo map *) 194 + let by_repo : day list String_map.t = List.fold_left (fun acc (d : day) -> 195 + let existing = String_map.find_opt d.repository acc |> Option.value ~default:[] in 196 + String_map.add d.repository (d :: existing) acc 197 + ) String_map.empty days in 198 + 199 + (* Sort each repo's days by date descending *) 200 + let by_repo : day list String_map.t = String_map.map (fun (ds : day list) -> 201 + List.sort (fun (d1 : day) (d2 : day) -> String.compare d2.date d1.date) ds 202 + ) by_repo in 203 + 204 + (* Build by_date map *) 205 + let by_date : day list String_map.t = List.fold_left (fun acc (d : day) -> 206 + let existing = String_map.find_opt d.date acc |> Option.value ~default:[] in 207 + String_map.add d.date (d :: existing) acc 208 + ) String_map.empty days in 209 + 210 + (* Sort each date's days by repo name *) 211 + let by_date : day list String_map.t = String_map.map (fun (ds : day list) -> 212 + List.sort (fun (d1 : day) (d2 : day) -> String.compare d1.repository d2.repository) ds 213 + ) by_date in 214 + 215 + (* Collect all entries sorted by timestamp *) 216 + let all_entries : entry list = 217 + days 218 + |> List.concat_map (fun (d : day) -> d.entries) 219 + |> List.sort (fun (e1 : entry) (e2 : entry) -> Ptime.compare e1.timestamp e2.timestamp) 220 + in 221 + 222 + { by_repo; by_date; all_entries } 223 + 224 + | _ -> empty 225 + | exception Eio.Io _ -> empty 226 + 227 + let since (t : t) (timestamp : Ptime.t) : entry list = 228 + List.filter (fun (e : entry) -> Ptime.compare e.timestamp timestamp > 0) t.all_entries 229 + 230 + let for_repo t repo = 231 + String_map.find_opt repo t.by_repo |> Option.value ~default:[] 232 + 233 + let for_date t date = 234 + String_map.find_opt date t.by_date |> Option.value ~default:[] 235 + 236 + let repos t = 237 + String_map.bindings t.by_repo |> List.map fst 238 + 239 + let dates t = 240 + String_map.bindings t.by_date 241 + |> List.map fst 242 + |> List.sort (fun a b -> String.compare b a) (* descending *) 243 + 244 + let entries_since ~fs ~changes_dir ~since:timestamp = 245 + let t = load_all ~fs ~changes_dir in 246 + since t timestamp
+117
monopam/lib_changes/daily.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2026 Anil Madhavapeddy <anil@recoil.org>. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Daily changes with per-day-per-repo structure. 7 + 8 + This module provides an immutable data structure for loading and querying 9 + daily changes from per-day-per-repo JSON files. Files are named 10 + [<repo>-<YYYY-MM-DD>.json] and contain timestamped entries for real-time 11 + tracking. *) 12 + 13 + (** {1 Types} *) 14 + 15 + type commit_range = { 16 + from_hash : string; 17 + to_hash : string; 18 + count : int; 19 + } 20 + (** Commit range information. *) 21 + 22 + type entry = { 23 + repository : string; 24 + hour : int; 25 + timestamp : Ptime.t; 26 + summary : string; 27 + changes : string list; 28 + commit_range : commit_range; 29 + contributors : string list; 30 + repo_url : string option; 31 + } 32 + (** A single timestamped changelog entry. *) 33 + 34 + type day = { 35 + repository : string; 36 + date : string; 37 + entries : entry list; (** Sorted by timestamp ascending. *) 38 + } 39 + (** All entries for a single repository on a single day. *) 40 + 41 + module String_map : Map.S with type key = string 42 + (** String-keyed map type. *) 43 + 44 + type t = { 45 + by_repo : day list String_map.t; 46 + (** Map from repository name to list of days. *) 47 + by_date : day list String_map.t; 48 + (** Map from date (YYYY-MM-DD) to list of days across repos. *) 49 + all_entries : entry list; 50 + (** All entries sorted by timestamp ascending. *) 51 + } 52 + (** Immutable collection of all loaded daily changes. *) 53 + 54 + (** {1 Construction} *) 55 + 56 + val empty : t 57 + (** Empty daily changes structure. *) 58 + 59 + val load_all : fs:_ Eio.Path.t -> changes_dir:Fpath.t -> t 60 + (** [load_all ~fs ~changes_dir] loads all [<repo>-<YYYY-MM-DD>.json] files 61 + from the changes directory and returns an immutable structure for querying. *) 62 + 63 + (** {1 Querying} *) 64 + 65 + val since : t -> Ptime.t -> entry list 66 + (** [since t timestamp] returns all entries with timestamp after [timestamp], 67 + sorted by timestamp ascending. *) 68 + 69 + val for_repo : t -> string -> day list 70 + (** [for_repo t repo] returns all days for the given repository, 71 + sorted by date descending. *) 72 + 73 + val for_date : t -> string -> day list 74 + (** [for_date t date] returns all days (across repos) for the given date. *) 75 + 76 + val repos : t -> string list 77 + (** [repos t] returns list of all repository names with changes. *) 78 + 79 + val dates : t -> string list 80 + (** [dates t] returns list of all dates with changes, sorted descending. *) 81 + 82 + (** {1 File Discovery} *) 83 + 84 + val list_repos : fs:_ Eio.Path.t -> changes_dir:Fpath.t -> string list 85 + (** [list_repos ~fs ~changes_dir] returns all repository names that have 86 + daily change files. *) 87 + 88 + val list_dates : fs:_ Eio.Path.t -> changes_dir:Fpath.t -> repo:string -> string list 89 + (** [list_dates ~fs ~changes_dir ~repo] returns all dates for which the given 90 + repository has change files. *) 91 + 92 + (** {1 Loading Individual Files} *) 93 + 94 + val load_repo_day : 95 + fs:_ Eio.Path.t -> 96 + changes_dir:Fpath.t -> 97 + repo:string -> 98 + date:string -> 99 + entry list 100 + (** [load_repo_day ~fs ~changes_dir ~repo ~date] loads entries for a specific 101 + repo and date. Returns empty list if file doesn't exist. *) 102 + 103 + val load_repo_all : 104 + fs:_ Eio.Path.t -> 105 + changes_dir:Fpath.t -> 106 + repo:string -> 107 + entry list 108 + (** [load_repo_all ~fs ~changes_dir ~repo] loads all entries for a repository 109 + across all dates. *) 110 + 111 + val entries_since : 112 + fs:_ Eio.Path.t -> 113 + changes_dir:Fpath.t -> 114 + since:Ptime.t -> 115 + entry list 116 + (** [entries_since ~fs ~changes_dir ~since] returns all entries created after 117 + the given timestamp, useful for real-time updates. *)
+5 -1
monopam/lib_changes/monopam_changes.ml
··· 6 6 (** Library for parsing and querying aggregated daily changes. 7 7 8 8 This library provides types and functions for working with the aggregated 9 - daily changes format used by the monopam tool and the poe Zulip bot. *) 9 + daily changes format used by the monopam tool and the poe Zulip bot. 10 + 11 + The {!Daily} module provides an immutable data structure for loading 12 + per-day-per-repo JSON files ([<repo>-<YYYY-MM-DD>.json]). *) 10 13 11 14 module Aggregated = Aggregated 15 + module Daily = Daily 12 16 module Query = Query
+51
monopam/lib_changes/query.ml
··· 87 87 count (if count = 1 then "" else "s") 88 88 (List.length repos) (if List.length repos = 1 then "y" else "ies") 89 89 (String.concat ", " repos) 90 + 91 + (** {1 Daily Changes (Real-time)} *) 92 + 93 + let daily_changes_since ~fs ~changes_dir ~since = 94 + Daily.entries_since ~fs ~changes_dir ~since 95 + 96 + let has_new_daily_changes ~fs ~changes_dir ~since = 97 + daily_changes_since ~fs ~changes_dir ~since <> [] 98 + 99 + let format_daily_for_zulip ~entries ~include_date ~date = 100 + if entries = [] then 101 + "No changes to report." 102 + else begin 103 + let buf = Buffer.create 1024 in 104 + if include_date then begin 105 + match date with 106 + | Some d -> Buffer.add_string buf (Printf.sprintf "## Changes for %s\n\n" d) 107 + | None -> Buffer.add_string buf "## Recent Changes\n\n" 108 + end; 109 + (* Group by repository *) 110 + let repos = List.sort_uniq String.compare 111 + (List.map (fun (e : Daily.entry) -> e.repository) entries) in 112 + List.iter (fun repo -> 113 + let repo_entries = List.filter (fun (e : Daily.entry) -> e.repository = repo) entries in 114 + if repo_entries <> [] then begin 115 + let first_entry = List.hd repo_entries in 116 + let repo_link = format_repo_link repo first_entry.repo_url in 117 + Buffer.add_string buf (Printf.sprintf "### %s\n\n" repo_link); 118 + List.iter (fun (entry : Daily.entry) -> 119 + Buffer.add_string buf (Printf.sprintf "**%s**\n" entry.summary); 120 + List.iter (fun change -> 121 + Buffer.add_string buf (Printf.sprintf "- %s\n" change)) entry.changes; 122 + if entry.contributors <> [] then 123 + Buffer.add_string buf (Printf.sprintf "*Contributors: %s*\n" 124 + (String.concat ", " entry.contributors)); 125 + Buffer.add_string buf "\n") repo_entries 126 + end) repos; 127 + Buffer.contents buf 128 + end 129 + 130 + let format_daily_summary ~entries = 131 + if entries = [] then 132 + "No new changes." 133 + else 134 + let count = List.length entries in 135 + let repos = List.sort_uniq String.compare 136 + (List.map (fun (e : Daily.entry) -> e.repository) entries) in 137 + Printf.sprintf "%d change%s across %d repositor%s: %s" 138 + count (if count = 1 then "" else "s") 139 + (List.length repos) (if List.length repos = 1 then "y" else "ies") 140 + (String.concat ", " repos)
+30
monopam/lib_changes/query.mli
··· 40 40 entries:Aggregated.entry list -> 41 41 string 42 42 (** Format a brief summary of the changes. *) 43 + 44 + (** {1 Daily Changes (Real-time)} *) 45 + 46 + val daily_changes_since : 47 + fs:_ Eio.Path.t -> 48 + changes_dir:Fpath.t -> 49 + since:Ptime.t -> 50 + Daily.entry list 51 + (** Get all daily change entries created after [since] timestamp. 52 + Uses the per-day-per-repo files for real-time access. *) 53 + 54 + val has_new_daily_changes : 55 + fs:_ Eio.Path.t -> 56 + changes_dir:Fpath.t -> 57 + since:Ptime.t -> 58 + bool 59 + (** Check if there are any new daily changes since the given timestamp. *) 60 + 61 + val format_daily_for_zulip : 62 + entries:Daily.entry list -> 63 + include_date:bool -> 64 + date:string option -> 65 + string 66 + (** Format daily entries as markdown suitable for Zulip. 67 + Groups entries by repository. *) 68 + 69 + val format_daily_summary : 70 + entries:Daily.entry list -> 71 + string 72 + (** Format a brief summary of daily changes. *)
+14 -2
ocaml-zulip/lib/zulip_bot/storage.ml
··· 90 90 Log.warn (fun m -> m "Failed to parse storage response: %s" msg); 91 91 None 92 92 with Eio.Exn.Io (e, _) -> 93 - Log.warn (fun m -> m "Error fetching key %s: %a" key Eio.Exn.pp_err e); 94 - None) 93 + let is_key_not_found = match e with 94 + | Zulip.Error.E err -> 95 + Zulip.Error.code err = Zulip.Error.Bad_request && 96 + String.equal (Zulip.Error.message err) "Key does not exist." 97 + | _ -> false 98 + in 99 + if is_key_not_found then begin 100 + (* Key not found is a normal case, not an error *) 101 + Log.debug (fun m -> m "Key not found in storage: %s" key); 102 + None 103 + end else begin 104 + Log.warn (fun m -> m "Error fetching key %s: %a" key Eio.Exn.pp_err e); 105 + None 106 + end) 95 107 96 108 let set t key value = 97 109 Log.debug (fun m -> m "Storing key: %s" key);
+16 -8
poe/bin/main.ml
··· 16 16 let clock = Eio.Stdenv.clock env in 17 17 18 18 (* Load poe config: explicit path > XDG > current dir > defaults *) 19 - let poe_config = 19 + let poe_config, config_source = 20 20 match config_file with 21 - | Some path -> Poe.Config.load ~fs path 21 + | Some path -> (Poe.Config.load ~fs path, Printf.sprintf "explicit path: %s" path) 22 22 | None -> ( 23 23 match Poe.Config.load_xdg_opt ~fs with 24 - | Some c -> c 24 + | Some c -> (c, "XDG config (~/.config/poe/config.toml)") 25 25 | None -> ( 26 26 match Poe.Config.load_opt ~fs "poe.toml" with 27 - | Some c -> c 28 - | None -> Poe.Config.default)) 27 + | Some c -> (c, "current directory (poe.toml)") 28 + | None -> (Poe.Config.default, "built-in defaults"))) 29 29 in 30 30 31 + Logs.info (fun m -> m "Poe config loaded from %s" config_source); 32 + Logs.info (fun m -> m " Channel: %s, Topic: %s" poe_config.channel poe_config.topic); 33 + Logs.info (fun m -> m " Monorepo: %s, Changes dir: %s" poe_config.monorepo_path poe_config.changes_dir); 34 + let admin_count = List.length poe_config.admin_emails in 35 + if admin_count > 0 then 36 + Logs.info (fun m -> m " Admin users: %d configured (%s)" admin_count 37 + (String.concat ", " poe_config.admin_emails)) 38 + else 39 + Logs.info (fun m -> m " Admin users: none configured"); 40 + 31 41 (* Load zulip bot config from poe's XDG directory *) 32 42 let zulip_config = Zulip_bot.Config.load_or_env ~xdg_app:"poe" ~fs bot_name in 33 43 ··· 38 48 39 49 (* Create and run the bot *) 40 50 let handler = Poe.Handler.make_handler handler_env poe_config in 41 - Logs.info (fun m -> 42 - m "Starting Poe bot, broadcasting to %s/%s" poe_config.channel 43 - poe_config.topic); 51 + Logs.info (fun m -> m "Starting Poe bot..."); 44 52 Zulip_bot.Bot.run ~sw ~env ~config:zulip_config ~handler 45 53 46 54 let run_cmd =