Gleam PDS
subdermal LEARNING_LOG.md
123 kB · 380 lines
Markdown
at main

Learning log #

Current card: M5.2 — builder done on structural types (next: validating tree reader back to a mapping) Open prior compatibility check: M1.2 / NSID-001; strict upstream agreement remains incomplete Current release gate: L passed — local learning server only; R/S/C/O not passed Last working checkpoint: M4.3 CID content hashing and immutable blocks verified; 207 unit tests, compiler and format pass; M5.1 MST key layers are next; NSID-001 remains open Last updated: 2026-10-18

This is the project's progress file. Resume the current card and the next exact action below; do not skip a card whose required checks or learner exercise remain incomplete. The implementation plan's original checkpoint describes planning time.

Version control: git repo initialized 2026-10-08 at the learner's request; origin is git@tangled.org:did:plc:bitiurrsu4thrcdgtsekuj3z (branch main). A stray superseded draft at repo-root block_store.gleam was excluded from the initial commit and has since been deleted; the real module is src/pds/block_store.gleam.

Completed cards #

Card Outcome Learner concept Evidence Remaining limitation
M0.1 Minimal Erlang-targeted Gleam package runs; learner changed greeting, observed and corrected a type error Modules, entry function, final-expression return value, Nil, type annotations, build/check/test cycle Learner shared changed output and expected Int/Nil diagnostic; source restored to Nil; agent corrected gleam check passed. Baseline run/test-runner/format passed in prior checkpoint; manifest pins dependencies No server or protocol behavior yet; test runner is empty
M0.2 Architecture exercise complete; official reading notes and source register prepared; both reference checkouts pinned Record vs repository; PDS vs relay vs AppView; locating the entry function Learner correctly answered PDS, AppView, main; agent verified expected checkout remotes, clean working trees, and actual revisions recorded in SOURCES.md References are not implementation or compatibility evidence
M1.1 Learner implemented structural path parsing with distinct typed errors Custom types, list patterns, ordered exhaustive case, Result Agent reviewed learner code; ten contract tests pass; gleam check and gleam format --check pass Strings are not yet protocol-valid opaque identifiers; a/b intentionally passes
M2.1 Local HTTP listener with JSON health and explicit routing errors; learner added service field HTTP request/response, JSON object field tuples, server lifetime Real health response includes status/service; 404 and 405/Allow GET verified; repeated requests and prior port-in-use failure verified; compiler/71 tests/format pass Local listener only; no XRPC/identity/repository behavior; NSID-001 remains a separate open compatibility issue
M2.2 describeServer uses explicit development configuration; shared XRPC errors and GET dispatch work Canonical Lexicon contract, configuration injection, JSON types, prefix patterns and nested cases 82 unit tests pass; all 9 real HTTP checks pass (200/400/501/404/health); compiler and format pass Local development DID fixture only; no public identity or interop claim; NSID-001 remains open
M2.3 Development-only WebSocket echoes binary bytes and closes correctly; HTTP remains available Binary vs UTF-8 text, handshake vs payload, send Result and callback lifecycle 84 unit tests pass; independent Node client verified upgrade/exact 00 ff 41/close/health; on_close log and socket cleanup verified; compiler/format pass Local transport experiment only; no subscribeRepos or repository interoperability claim
M3.1 Configured SQLite file, foreign keys, transactional migrations and parameterized insert work Composite primary key; SQL placeholders bind values separately from SQL text Fresh/reopen/rollback/checksum/foreign-key checks pass; learner's quoted JSON round trip and composite-key cases pass; insert checks now in unit suite; compiler/format pass Development table only; no signed repository or public write route; gate L awaits M3.2/M3.3
M3.2 Development CRUD and scoped live keyset pagination work Learner implemented retrieval and typed invariant errors; keyset vs offset walkthrough, with offset prediction corrected Full 120-test suite, compiler and format pass; reopen/scope/cursor/live-mutation checks verified No public storage route or signed repository; write ownership/concurrency/transaction lesson remains in M3.3
M3.3 Typed connection-owning actor serializes atomic write batches; read replies, restart and bounded lock/caller waits verified Learner implemented Get error wrapping, reply send and continuation; familiar rollback exercise completed by agent at learner's request 140 unit tests pass; compiler/format and explicit actor lesson pass; concurrent writes/batches, rollback, restart, busy/dead-owner/timeout recovery verified; fresh HTTP/WebSocket checks pass Gate L only; no public storage route, automatic supervision or protocol interoperability claim; NSID-001 remains open

M1.2 checkpoint history #

  • Goal: review the learner's validated repository-path composition and verify all M1.2 path behaviors.
  • Files involved: src/pds/syntax/nsid.gleam, src/pds/syntax/repo_path.gleam, test/pds/validated_repo_path_test.gleam, existing syntax/fixture tests, protocol docs, README, this log.
  • Learner exercise / attempt: record-key, name, segment, and complete-authority functions are correct. Learner implemented validate_authority with total-length handling, [] | [_] alternatives for insufficient segments, a leading-digit check, and recursive segment validation. When asked to change its check order, the learner questioned whether that order came from the spec or plan. Rechecked both: neither requires it. The agent's exercise had imposed that order unnecessarily. Preserved all learner implementation logic and corrected the tests instead.
  • Explanation or correction: splitting proves a path's shape only; each field needs its own syntax rules. Result keeps the reason for a failure explicit. Added an opaque RecordKey type: its constructor is accessible only inside its module. to_string exposes its original text. Supplied has_allowed_characters as a character-set helper; it alone does not validate length or forbidden whole values and never constructs an opaque key.
    • Added opaque Nsid, final-name error variants, format function, and character/leading-digit helpers. validate_name returns the original String rather than constructing Nsid, because authority and whole-identifier rules are still pending. string.first returns a Result; the supplied helper handles empty input explicitly without asserting. Authority segments permit digits at the beginning and internal hyphens; the first (top-level-domain) authority segment's leading-digit prohibition will be enforced when validating the complete authority.
    • Added AuthorityTooLong, TooFewAuthoritySegments, and NumericTopLevelDomain, plus a recursive traversal helper. [segment, ..rest] matches the first item and remaining list; each successful check recurses on the shorter remainder, an error is returned immediately, and [] returns Ok(Nil) because no unchecked segments remain. The helper does not establish whole-authority validity; the learner's next function supplies count, total-length, and first-segment rules.
    • Learner's [] | [_] branch correctly rejects lists with zero/one items. Because that branch runs first, the following [first, ..rest] branch receives at least two items. let after stores a Result; matching it propagates a length error without proceeding. Error order for multiple violations is an implementation choice here. The spec requires syntax constraints; the plan requires useful typed errors; neither requires segment errors to precede top-level digit errors. Keep normative requirements, project choices, and exercise suggestions distinct.
    • New exercise uses a tuple #(authority, name) to carry two values from a structural helper; Ok(#(authority, name)) unpacks it when matching the result. Added NsidTooLong, TooFewSegments, and split_parts, retaining empty segments and original text until validation. Recorded D13: full parse normalizes only the authority to lowercase while preserving final-name case; validation helpers remain unchanged. This representation choice follows the checked casing guidance, and does not impose error precedence.
  • Commands and actual outcomes:
    • Read current official record-key and NSID specs earlier in this card; pinned validator and fixture routes and normative discrepancies are in protocol docs.
    • Learner's parser: gleam check passed before tests were added.
    • Imported exact fixture files and CC0 license, recording SHA-256 checksums and source revision 056e5741bb330757205d6b16db5266fffcae937b; embedded all cases for portable tests. Import excludes only empty separator lines and # comments, preserving invalid #extra.
    • Added nine focused record-key tests and two fixture-adapter tests. Initial adapter compilation failed because the agent used call labels that its helper had not declared; corrected those calls to positional arguments. No learner code change was required.
    • Final gleam check: passed. Final gleam test: 21 passed, no failures; fixture reports 16 valid and 12 invalid passed, 0 failed, 0 skipped. Tests cover exact dots vs other dots, 1/512/513 lengths, every allowed character, case preservation, whitespace/slash/Unicode rejection, and error precedence.
    • gleam format --check: passed. Updated record-key contract checklist, source register, and README to reflect actual verification.
    • Inspected pinned NSID fixtures: one upstream valid case has total length 287 but authority length 283, exceeding the checked specification's separate 253 limit. Recorded the exact discrepancy in CONTRACTS.md; no fixture was changed or skipped.
    • Inspected stdlib APIs; initial NSID scaffold compiled and formatted successfully. After reviewing the learner's final-name function, added eight focused tests. gleam check: passed. gleam test: 29 passed, no failures; all 28 record-key fixtures still pass, zero skipped. gleam format --check: passed.
    • Added four authority-segment error variants and two helper functions; formatted the module, then gleam check and gleam format --check passed. This was scaffold verification only; behavioral tests were added after the learner's attempt below.
    • Reviewed learner's authority-segment implementation and added eight boundary/character/Unicode/case/error-precedence tests. Supplied the recursive segment traversal with two tests verifying success and first-error propagation. Final gleam check: passed. gleam test: 39 passed, no failures; 16 valid and 12 invalid record-key fixtures pass, 0 skipped. gleam format --check: passed.
    • Reviewed complete-authority implementation; gleam check passed. Added nine meaningful contract tests including 253/254-character inputs with individually valid segment lengths, segment count, first-domain digit rules, case/Unicode/empty segments, propagation, and declared error ordering. Formatting and compilation passed. gleam test: 47 passed, 1 failed. Failure: validates_segments_before_top_level_domain_test in test/pds/nsid_authority_test.gleam expects Error(InvalidAuthorityCharacter) for 8.bad_, but received Error(NumericTopLevelDomain). All 28 record-key fixtures still pass, none skipped.
    • After the learner's source-attribution question, re-read the official NSID specification and applicable M1.2 plan; found no required syntax-error ordering. Replaced the unwarranted authority-order test with rejection of multiple violations. Applied the same correction to mixed-invalid key/name/segment tests, preserving single-violation error assertions. This corrects our own tests, not official fixture expectations. Removed obsolete authority exercise comments; no learner implementation logic changed.
    • Final gleam check: passed. gleam test: 48 passed, no failures; 16 valid and 12 invalid record-key fixtures pass, zero skipped. gleam format --check: passed. Authority validation is complete; no learner rewrite is needed.
    • Rechecked official NSID syntax/casing guidance and actual installed list.reverse/string.join/string.lowercase APIs. Added four structural split-helper tests. gleam check: passed; gleam test: 52 passed, no failures, all 28 record-key fixtures pass with zero skipped; gleam format --check: passed. Full parse is still a learner exercise, not implemented.
    • Reviewed the learner's complete nsid.parse: length check, structural decomposition, both validators, and opaque construction only on success. Its multi-subject case returns either validation error and constructs the identifier only for two Ok results. Preserved all learner logic; removed obsolete exercise comments.
    • Added nine full-parser tests covering valid identifiers, authority-only normalization/name-case distinction, malformed shape, propagated single violations, Unicode/whitespace, mixed-invalid rejection without error-order assumptions, total 317/318 bounds, and separate authority overflow below the total bound. All nine pass.
    • Copied NSID fixture files byte-for-byte at the same pinned interop revision, with CC0 license/checksums/counts in nsid-provenance.json. Embedded all 25 valid and 27 invalid cases without trimming case text. Adapter retains upstream expectations and evaluates/reports all cases before asserting; no skips or expectation changes.
    • Rechecked https://atproto.com/specs/nsid: its comprehensive rules still separately require authority <=253. Pinned TypeScript validator lacks the separate authority bound. The imported valid fixture with total 287/authority 283 reproduces the disagreement and fails with AuthorityTooLong. Source search found no authoritative resolution; compatibility remains incomplete. This is not a learner bug, and relaxing the normative bound is not justified.
    • gleam format src test and gleam check: passed. gleam test: 62 passed, 1 failed; the sole failure is official_valid_nsids_test for the documented long-authority fixture. NSID report: valid 24 passed/1 failed, invalid 27 passed/0 failed, 0 skipped. Record-key report remains 16 valid/12 invalid passed, 0 failed/skipped. Full-parser normative boundary regression passes. gleam format --check: passed. Copied fixture checksums and exact embedded lists verified.
    • Prepared the next exercise's types only: ValidatedRepoPath contains nsid.Nsid and record_key.RecordKey; PathError gains InvalidCollection(NsidError) and InvalidKey(RecordKeyError). Existing structural RepoPath/split_path behavior is unchanged. The new validated parse function is intentionally left to the learner.
    • Reviewed the learner's repo_path.parse: propagates structural errors, unpacks RepoPath, validates both fields, wraps NsidError/RecordKeyError in the corresponding PathError, and constructs ValidatedRepoPath only for two successful values. Preserved implementation logic; removed obsolete scaffold/scratch comments and clarified documentation.
    • Added nine integration tests in test/pds/validated_repo_path_test.gleam: valid opaque fields; camel-case collection/name and key preservation; structural versus validated parsing of a/b; wrong/missing/extra slash and empty-field errors including ../; nested collection and forbidden-key causes; Unicode/whitespace; one/512/513-key boundaries and underscore/tilde/colon/pre:fix/self/...; multiple invalid fields rejected without imposing error priority.
    • gleam check: passed. gleam test: 71 passed, 1 failed; all nine new path tests pass. The sole failure is the unchanged upstream NSID long-authority fixture. NSID fixtures: valid 24 passed/1 failed, invalid 27 passed/0 failed, 0 skipped. Record-key fixtures: 16 valid/12 invalid passed, 0 failed/skipped. gleam format --check: passed. No learner logic change was needed.
    • Separate error branches correctly reflect different payload types: InvalidCollection wraps NsidError; InvalidKey wraps RecordKeyError. An example result is Error(InvalidKey(ForbiddenDotKey)); the outer constructor identifies the field while the inner value explains the violation.
    • Learner challenged leaving an ignored failing normal test and supplied M1.2 explanation answers. Answer 1 is correct: InvalidKey(ForbiddenDotKey). Answer 2 should be InvalidCollection(NameStartsWithDigit), because the digit is in final name 3post; top-level domain is com. Explained that pre:fix uses explicitly allowed letters/colon, fits 1–512, and is neither forbidden dot value. Explained literal:self as a fixed singleton key within a collection, exemplified by app.bsky.actor.profile/self; generic validation does not enforce individual Lexicon key policies.
    • Rechecked official NSID and record-key pages and current online reference validator. The explicit 253-character authority bound still conflicts with the valid length fixture and omitted reference check. Measured exact copied fixture line 4: authority 283, name foo length 3, total 287, maximum segment length 6. Added local discrepancy tracker docs/protocol/DISCREPANCIES.md (NSID-001), with source revisions, reproduction, policy and resolution conditions; no external issue was filed.
    • Corrected test organization in response to the learner's concern: moved adapter to test/pds/nsid_interop.gleam and added a strict main runnable with gleam run -m pds/nsid_interop. Main checks/reports both fixture groups before asserting complete agreement. Removed only the valid-fixture comparison from automatic unit discovery; all invalid fixtures remain in gleam test. No fixture text/expectations, parser logic or normative boundaries changed. Separate strict command is required compatibility evidence, not optional/skipped coverage.
    • gleam check: passed; gleam test: 71 passed, zero failures; gleam format --check: passed. gleam run -m pds/nsid_interop: exit 1, valid 24 passed/1 failed, invalid 27 passed/0 failed, zero skips. Unit success and compatibility failure are separate results; the upstream disagreement remains unresolved. D14 records this verification policy.
  • Open questions: NSID-001 remains open. Default normative tests must pass; strict upstream compatibility must also be run and its failure reported until authoritative clarification reconciles the authority bound and fixture. A green unit suite alone does not complete M1.2 or establish interoperability. See docs/protocol/DISCREPANCIES.md for reproduction and closure requirements.
  • Next exact action: learner understood the M1.2 explanations and requested the next lesson. The local HTTP scaffold is independent of NSID validation; ongoing work is M2.1. Keep the prior NSID compatibility check open and retain its required strict command; no interoperability gate/card completion is claimed.

M2.1 checkpoint evidence #

  • Outcome: GET http://127.0.0.1:2583/health returns HTTP 200 and JSON {"status":"ok"}; unknown route and unsupported method return explicit responses; listener stays alive.
  • Files: src/pds/config.gleam, src/pds/http/server.gleam, src/pds/http/router.gleam, src/pds/http/json_response.gleam, src/pds/http/health.gleam, src/pds.gleam, gleam.toml/manifest.toml, docs/ENDPOINTS.md, README, this log.
  • Attempted dependency installation from the agent environment previously failed accessing repo.hex.pm. Learner installed dependencies successfully and reported compiler deprecation warnings. Inspected the actual installed README/source/APIs: Mist 6.0.3, gleam_http 4.4.0, gleam_erlang 1.3.0, logging 1.5.0, gleam_json 3.1.0. Existing stdlib 1.0.5 and gleeunit 1.11.0 remain pinned. No dependency upgrade was performed by the agent.
  • Scaffold: fixed local host/port in config; method/path router returns Response(String); separate JSON encoder sets application/json; Mist adapter converts strings to Bytes at the socket boundary. main preserves the learner's greeting and calls server.run. Server starts Mist and waits with process.sleep_forever so main does not exit after startup. Returned startup errors are matched explicitly and surfaced with their reason; linked OTP startup failures can instead terminate the process with the underlying listener log.
  • Two ideas for the learner: request handlers are functions Mist calls for each request; pipelines pass the previous result as the next function's first argument. The JSON object is a list of #(field_name, json_value) tuples.
  • Compiler correction: the agent initially wrote panic as "prefix" <> string.inspect(reason), which parsed concatenation after panic and produced unreachable-code/type errors. Corrected by building the message in a let binding before panic. No learner implementation was affected.
  • gleam check: passed; gleam test: 71 passed, no failures; gleam format --check: passed after scaffold creation. Agent check emitted no deprecation warnings; learner's earlier warnings were not reproduced, so their exact origin remains unverified.
  • First gleam run failed under restricted permissions with listener Eperm, exit 1; this was not passing HTTP evidence. User loosened permissions and requested a retry.
  • Retried gleam run: successful, printed greeting and Listening on http://127.0.0.1:2583. Real HTTP checks using Python http.client on one connection: GET /health ->200 with application/json and parsed body {"status":"ok"}; GET /missing ->404 and {"error":"NotFound"}; POST /health ->405 and {"error":"MethodNotAllowed"}, Allow GET; another GET /health ->200. All four checks passed and the server remained available.
  • While the original listener ran, a second gleam run exited 1 and logged Failed to start socket listener: Eaddrinuse (followed by OTP supervisor shutdown). This accurately reports a busy port; it does not reach the application's custom returned-error panic branch. The first listener continued serving.
  • Final gleam check and gleam format --check passed. Stopped only the agent-started listener PID 81076 with SIGTERM; the exec session completed. Port is released for the learner. No server left running by the agent.
  • Original learner exercise (now verified below): in src/pds/http/health.gleam add #("service", json.string("pds")) alongside the status tuple. Run gleam run in one terminal and curl --fail-with-body -i http://127.0.0.1:2583/health in another. Expect status 200, application/json and {"status":"ok","service":"pds"}; JSON field order is not the contract. Agent reviews the change and verifies responses again after the learner attempt.
  • Learner added the service tuple correctly. Preserved all learner logic and removed stale exercise comments. gleam check, gleam test (71 passed), and gleam format --check passed. Tested the learner-owned running listener over real HTTP on one connection: GET /health twice returned 200, application/json, parsed {"status":"ok","service":"pds"}; GET /missing returned 404 NotFound; POST /health returned 405 MethodNotAllowed with Allow GET. All checks passed. Did not start/stop or replace the learner's server.
  • M2.1 complete. Its port-in-use check remains valid from the previous session; the learner's field addition does not change listener startup. NSID-001 remains open and no interop gate completion is claimed.

M2.2 checkpoint evidence #

  • Outcome to work toward: /xrpc/com.atproto.server.describeServer reports explicitly configured development server information using its canonical Lexicon contract and shared XRPC errors.
  • Read M2.2, official XRPC endpoint/error conventions, the current published describeServer Lexicon, and the pinned local schema/reference dispatch. No XRPC code is implemented yet.
  • Copied the exact pinned describeServer schema to priv/lexicons/com/atproto/server/describeServer.json. Source revision, date and SHA-256 hashes are in priv/lexicons/provenance.json. Copied the source licensing files alongside it. #links/#contact references are definitions within this file; no external Lexicon dependencies.
  • Schema reading exercise complete: learner correctly identified did and availableUserDomains as required and GET for query. The exact JSON field spelling is availableUserDomains. Required fields establish the minimal output contract; the copied schema also declares optional capabilities/contact/links.
  • Rechecked the official XRPC spec: JSON errors have a required error String and optional message String, with application/json. The reference maps InvalidRequest to 400 and MethodNotImplemented to 501 (packages/xrpc/src/types.ts) and reports wrong methods for known XRPC routes as InvalidRequest (packages/xrpc-server/src/server.ts). These status/name conventions are checked reference behavior, rather than a claim that the generic spec prescribes every error mapping. Health's local 405 contract remains unchanged.
  • Added type-only scaffold src/pds/http/xrpc_error.gleam with XrpcError constructors InvalidRequest(message: String) and MethodNotImplemented(message: String). No endpoint/error formatter or router changes are implemented. gleam check and gleam format --check pass. Behavior tests will follow the learner's function attempt.
  • Pending implementation: server DID/domain configuration, truthful closed-registration capabilities, describeServer handler, central error encoding, router dispatch and structured invalid-method exercise. A successful response needs a configured development server DID; do not invent a resolvable public identity or return an unrelated account's DID. Resolve this local configuration before wiring the handler.
  • Learner implemented the formatter with a private build_response(code, error, message) helper that encodes both fields once, and a public match over the two constructors. Statuses 400/501, content type, InvalidRequest name, and preservation of messages are correct. At that attempt, the MethodNotImplemented branch emitted "Methodnotimplemented", which does not match the documented exact error name "MethodNotImplemented". Preserved learner code; provided a targeted correction rather than replacing it.
  • Added three meaningful tests in test/pds/xrpc_error_test.gleam: both status/content-type/envelope contracts; message preservation for empty input, quotes/newline/backslash, and Unicode. JSON is decoded to a dictionary so field order is not imposed. gleam check passed; gleam test: 73 passed, 1 failed, solely unimplemented_method_response_contract_test due to wire-name casing. The InvalidRequest and escaping tests pass. gleam format --check passed. This is a correctable implementation failure, separate from the upstream NSID compatibility discrepancy.
  • Learner corrected the wire name to MethodNotImplemented. Reviewed the exact edit; gleam check passed; gleam test: 74 passed, no failures; gleam format --check passed. Removed obsolete exercise comments only, preserving all learner implementation logic. Both envelope/status contracts and empty/escaped/Unicode message cases pass.
  • Prepared Config fields service_did: String and available_user_domains: List(String), preserving local host/port. Explicit local fixture is did:web:localhost%3A2583 with empty domain list. Current official DID spec (rechecked 2026-10-04) explicitly permits localhost and its encoded port in testing/development. This configuration does not publish or resolve a DID document and is not production/public identity evidence. No signup/domain capability is invented.
  • Added src/pds/api/describe_server.gleam with its exact NSID constant and a learner exercise comment; response implementation and router integration remain pending. Checked installed json.array signature: takes List(a) and an element encoder fn(a)->Json. Scaffold gleam check and gleam format --check pass.
  • Learner implemented describe_server.response correctly: status 200, did from settings.service_did, availableUserDomains encoded as array of strings from settings.available_user_domains. Preserved implementation logic and removed obsolete exercise comments. Added two meaningful tests in test/pds/describe_server_test.gleam: local fixture contract; alternate DID/nonempty multiple-domain config to prevent hardcoded local values and verify string/array types. JSON decoding does not impose property order. Both pass.
  • Prepared configuration injection for router integration: router.handle now accepts (req, settings: config.Config); the Mist callback captures the startup settings and passes them through server.handle into router.handle. Existing route behavior is unchanged; no XRPC branch has been added by the agent. gleam check and gleam format --check pass; gleam test: 76 passed, no failures. There is one temporary Unused function argument warning for settings in router.handle until the learner uses it in the describeServer branch; it is scaffold state, not a deprecation or error.
  • Learner implemented dispatch using the valid string-prefix pattern method, "/xrpc" <> rest, then matches the remaining endpoint and method in nested cases. GET describeServer uses the supplied settings; incorrect methods return InvalidRequest; unknown XRPC endpoints return MethodNotImplemented; health and ordinary 404 behavior remain intact. No string.starts_with import is needed with this equivalent pattern approach. Reviewed and preserved learner logic.
  • Found one prefix-boundary defect: matching /xrpc without its following slash also classifies /xrpcish, /xrpc-other, and /xrpc as XRPC endpoint paths. The intended endpoint prefix is /xrpc/, while ordinary unknown paths retain 404. This is route classification, not an arbitrary precedence requirement.
  • Added six meaningful tests in test/pds/router_test.gleam: supplied config reaches describeServer; known endpoint rejects Post/Put/Head/Options with exact 400/error envelope; unknown GET/POST XRPC endpoints return 501; slash boundary; preserved health 200/405/Allow GET; ordinary 404. gleam check passed; gleam test: 81 passed, 1 failed (requires_slash_boundary_for_xrpc_routes_test: expected404, found501 for /xrpcish). gleam format --check passed; no unused-settings warning remains.
  • Started the agent's local listener (PID84421) and tested real HTTP on one persistent connection: GET describeServer ->200 with configured local DID and []domains; POST describeServer ->400 InvalidRequest/Expected GET; unknown XRPC endpoint ->501 MethodNotImplemented; health GET200, POST405/Allow GET; /missing404. Those six checks passed. Boundary paths /xrpcish, /xrpc-other, /xrpc each returned501 instead of intended404. Script reported all 6 passes/3 mismatches explicitly. Stopped only the agent-started PID84421 with SIGTERM and the session completed, freeing the port.
  • Learner changed both the prefix to /xrpc/ and the endpoint remainder to com.atproto.server.describeServer. Reviewed exact source; preserved logic and removed obsolete router exercise comments. gleam check passed; gleam test: all 82 passed; gleam format --check passed.
  • Repeated real HTTP against the corrected agent-started listener PID84686 on one persistent connection: describeServer GET200 with the configured fixture/types, POST400 InvalidRequest, unknown XRPC GET501 MethodNotImplemented, health GET200/POST405 with Allow GET, ordinary /missing404, and all three boundary paths (/xrpcish, /xrpc-other, /xrpc)404. All9 checks passed. Stopped only the agent's listener with SIGTERM; session completed and port released.
  • Trace now established by the learner's dispatch: Mist callback captures startup settings -> server.handle converts the response body -> router.handle matches /xrpc/ and the endpoint remainder -> describe_server.response encodes the configured DID/domains -> json_response.new sets application/json and body -> Mist sends bytes. Wrong methods follow xrpc_error.response with InvalidRequest. M2.2 is complete locally; no public identity or interoperability gate is claimed.
  • Next exact action: M2.3, recorded below.

M2.3 checkpoint history #

  • Observable outcome: a development-only /dev/ws route performs a WebSocket upgrade through Mist and echoes an exact binary frame; disconnect cleanup and ordinary HTTP remain correct.
  • Learner correctly selected binary for bytes 00 ff 41. Explained that FF is not valid UTF-8; text frames require valid UTF-8 while binary frames carry arbitrary bytes, per RFC6455 section5.6. Upgrade headers belong to the HTTP handshake, separate from application payload.
  • Read actual installed Mist6.0.3 websocket, send_binary_frame, continue/stop, on_init and on_close APIs and internal disconnect handling. on_init returns #(state, optional selector); this exercise uses #(Nil, None). Mist dispatches messages to a callback whose result tells it to keep the connection or stop.
  • Added config.Environment (Development/Production); config.local explicitly selects Development. dev_ws.accepts selects only GET /dev/ws in Development, tested for production rejection, wrong method and path boundaries. No production startup/configuration is implemented; local DID fixtures remain development-only.
  • Added src/pds/http/dev_ws.gleam: upgrade wrapper, initialization/close callbacks, and a deliberately incomplete binary branch that stops abnormally with Binary echo exercise pending. It is a local development scaffold, not a successful echo. Other message kinds stop. server.handle selects this transport path before ordinary response mapping/body handling; ordinary HTTP routing remains unchanged.
  • Added two route-gate tests (including Production disabled) in test/pds/dev_ws_test.gleam. gleam check passes with temporary unused state/connection warnings, which disappear when learner uses them. gleam test: 84 passed; gleam format --check passes. No actual binary echo/cleanup/network success has been claimed yet; those behavioral checks wait for the callback implementation.
  • Prepared integration/dev_ws.mjs: existing Node v24.16.0 with its built-in WebSocket client (runtime/global availability checked), no npm packages or new runtime dependencies. Consulted current official Node WebSocket docs; version-specific online URL was unavailable. Client asserts binary ArrayBuffer with exact bytes 00 ff 41, requests close and waits for close event, then checks health JSON. Five-second timeout/error paths exit nonzero. node --check passes; full client execution is pending the learner's echo implementation.
  • Learner completed the Binary branch with mist.send_binary_frame(connection, payload); Ok() continues with state, Error() stops. Reviewed actual code; no text conversion occurs. Preserved learner logic and removed obsolete exercise comments.
  • Actual verification: gleam check passed without warnings; gleam test: all 84 passed; gleam format --check passed. Started our own listener PID85875 and ran node integration/dev_ws.mjs: upgrade succeeded, exact binary bytes 00 ff 41 returned, disconnect completed, health response still passed. Server logged Development WebSocket closed; lsof -nP -iTCP:2583 showed only the listening socket after the client exited, with no remaining accepted sockets. Stopped only our own listener with SIGTERM; its session exited successfully.
  • M2.3 is complete locally. This is not subscribeRepos; no production deployment route is enabled. NSID-001 remains a separate unresolved fixture compatibility issue. Next card: M3.1 below.

M3.1 checkpoint history #

  • Observable outcome for this card: an isolated SQLite database gains the development records table through a transactional, checksum-tracked migration; a parameterized insert stores a quoted value literally.
  • First preparation: gleam add sqlight succeeded, installing sqlight1.2.0 and esqlite0.9.0, pinned in manifest.toml. gleam check compiled the native SQLite dependency and passed. Read actual installed APIs: open/close return typed Results, exec runs statements, query binds a List(Value) and decodes rows, text wraps String parameters. with_connection crashes on open/close failure, so production storage wrappers must use typed open/close handling rather than copy that convenience helper blindly.
  • Checked official sqlight documentation (https://hexdocs.pm/sqlight/) and SQLite CREATE TABLE rules (https://www.sqlite.org/lang_createtable.html). Prepared priv/migrations/001_dev_records.sql using the exact development schema from the plan: owner, collection, rkey, value_json, and PRIMARY KEY(owner, collection, rkey). At that initial checkpoint no database had been created or migration applied; isolated database verification followed below.
  • Learner correctly answered that only the same owner/collection/key conflicts, and rkey alone would wrongly reject both another owner and another collection using that key. Composite-key explanation complete.
  • Prepared typed database support in src/pds/storage/database.gleam: opens Config.db_path explicitly (local default ./data/pds.sqlite), creates its parent directory, enables foreign keys before migrations, and returns typed file/SQL/migration errors. An initialization failure closes its connection; close failure preserves both errors. HTTP startup does not open this database yet. No real runtime database was touched.
  • Prepared src/pds/storage/migrations.gleam: reads the packaged priv/migrations/001_dev_records.sql, uses exact SQL UTF-8 bytes' SHA-256 checksum, checks ordered migration numbers and previously applied checksums, rejects removed/unknown applied migrations, and applies all pending SQL plus bookkeeping in one BEGIN IMMEDIATE/COMMIT transaction. Failures roll back, with rollback failure represented explicitly. Migration SQL is developer-owned and must not include transaction-control statements. Recorded D15.
  • Added gleam_crypto1.6.0 as a direct dependency for migration checksums (already present transitively; pinned version unchanged). No new filesystem package: thin Erlang/OTP file:read_file and filelib:ensure_dir bridge normalizes errors; Gleam checks UTF-8. Existing gleam_erlang1.3.0 application.priv_directory locates packaged migrations.
  • Added eight database contract tests. Fresh configured file creation and reopen/data preservation pass; migration is not reapplied; changed checksum is rejected; first migration failure, later batch failure, and bookkeeping failure all roll back; foreign-key enforcement, invalid configured path, and invalid/missing migration order checks pass. Isolated temporary directories and test-only in-memory connections are closed and removed even on assertion failure through a small try/after test bridge. Initial test run had an agent-authored unused-Result warning; corrected its callback's return to Nil. Final gleam check, gleam test (92 passed, no failures), and gleam format --check pass without warnings.
  • Learner completed insert in test/pds/storage_insert_exercise.gleam with fixed SQL, four placeholders and four sqlight.text values in column order. Its Error(e) branch propagates the SQL error; the following wildcard returns Ok(Nil). This is correct because every Error has already matched and Result has only Ok/Error variants. Preserved all learner logic; removed obsolete exercise comments.
  • Learner assumed sqlight.text handles escaping. Clarified binding vs escaping: installed sqlight.text uses coerce_value, which is identity on Erlang; query passes SQL and values separately to esqlite3:q, whose SQLite parameter binding supplies the text as data. An apostrophe cannot become SQL syntax at that boundary. Read official SQLite binding documentation: https://www.sqlite.org/c3ref/bind_blob.html.
  • Actual gleam check passed; gleam run -m pds/storage_insert_exercise passed and printed Quoted JSON round trip and all three composite-key cases passed. Existing 92 unit tests and format passed. Added the same insert contract checks to ordinary test discovery; later suite with M3.2 create checks passes 96 tests, no failures. M3.1 is complete; gate L still awaits the remaining storage cards.

M3.2 checkpoint history #

  • Observable outcome for this first exercise: retrieve exactly the requested owner/collection/key's stored JSON or return a typed NotFound; preserve real SQL failures as SqlError.
  • Prepared src/pds/storage/dev_records.gleam for the development-only table. create reuses the learner's fixed-SQL/bound-value approach, accepts a ValidatedRepoPath so collection/key already passed M1 syntax rules, rejects malformed JSON before writing, preserves the valid input JSON text exactly, and translates duplicate composite keys into AlreadyExists. Owner is still an explicit local fixture string; identity/auth and signed repository storage are later cards. No public write route or HTTP startup database integration exists.
  • Added three create contract tests: valid JSON including whitespace/Unicode/apostrophe/SQL-looking text round-trips exactly, another owner/collection can reuse the key, duplicate creation preserves the original value and returns AlreadyExists, and invalid JSON writes nothing. Final gleam check, gleam test (96 passed, no failures), gleam format --check pass without warnings.
  • Learner implemented get with Error(e) -> Error(SqlError(e)), Ok([]) -> Error(NotFound), Ok([first]) -> Ok(first), and a final panic for more than one row. The supplied row decoder extracts column zero as String. The complete composite-key WHERE predicate and NOT NULL primary key guarantee at most one row with the correct schema; the extra branch is an invariant assertion, not handling an ordinary SQL failure.
  • Actual gleam run -m pds/storage_get_exercise passed and printed Create/get, missing records, isolation, reopen and SQL errors passed. It verifies missing/success/wrong owner/wrong collection/wrong key, close/reopen persistence and deliberately failed SQL. Added the same checks to ordinary test discovery. Final gleam check, gleam test (97 passed, no failures), gleam format --check all pass. Removed obsolete pending comments/error variant; preserved learner logic including the panic.
  • Learner asked whether panic is reasonable/idiomatic. Read official Gleam tour panic/Result guidance and SQLite primary-key rules. Explained that panic is defensible as a prototype invariant assertion but skips the Result error channel and crashes the executing process if reached. Our storage API should prefer typed errors for database state, retaining exact single-row checking rather than silently accepting multiple matches. This is an implementation recommendation, not an AT Protocol requirement; the valid-schema retrieval passes as written.
  • Learner added gleam/list and UnexpectedRowCount(Int), replacing the panic with Ok(values) -> Error(UnexpectedRowCount(list.length(values))). Reviewed actual source: earlier Error/empty/singleton cases exhaust those outcomes, so the last branch handles only successful queries with two or more matches. Preserved learner code unchanged.
  • Added multiple_matching_rows_return_typed_count_without_panicking_test in test/pds/dev_records_test.gleam. Its isolated in-memory table deliberately omits the primary key to reach the invariant branch. Two exact matches return UnexpectedRowCount(2); adding a third returns UnexpectedRowCount(3). Rows for another owner, collection and key do not affect that count. This test schema is closed afterward and never touches the configured runtime database. Final gleam check, gleam test (98 passed, no failures), gleam format --check all pass without warnings. Recorded D16; retrieval refinement is complete.
  • Pace preference explicitly approved by learner: agent handles routine CRUD implementation/tests, while learner exercises focus on pagination and transaction rollback. Do not require a separate learner exercise for every SQL statement or ask for that authorization again. This does not authorize unattended implementation of the whole PDS; continue small learning checkpoints.
  • Implemented replace/delete in dev_records with bound full-key predicates. Replace validates JSON, never creates missing records, and preserves exact valid input text; delete reports repeated/missing deletion as NotFound. Both use a single SQLite UPDATE/DELETE RETURNING statement to determine existence while changing the row, including successful replacement with unchanged JSON. Checked installed sqlight APIs and esqlite0.9.0 vendored SQLite header3.50.4 against current official RETURNING docs. Seven mutation contract tests passed: missing/duplicate scope isolation, invalid JSON without changing the original, exact replacement/delete, SQL errors and persistence through reopen. Intermediate suite: 105 passed.
  • Implemented list_page: validated collection and record keys, explicit Forward/Reverse, COLLATE BINARY key ordering, strict > or < continuation, at most limit returned records, one extra fetched row to detect continuation, and cursor from the last returned key. Local limits1–100 and cursor8192 bytes are resource policies, not protocol constants. Reuse may change page size. New page_cursor module encodes unpadded base64url/canonical JSON v1 with owner/collection/direction/last key; parsing checks size/encoding/version/scope/key and canonical form. No signing or authorization claim. Recorded D17 and docs/STORAGE.md.
  • Added five cursor tests and ten listing tests. Both-direction walks cover 0/1/many records, exact-page endings, no duplicates/gaps, bytewise case/numeric/punctuation order, key-preserving colon, limits, scope and malformed cursors, invalid stored keys, SQL failure, last-returned rather than lookahead cursor, page-size change, cursor reuse after reopen, deleted cursor key and live inserts/deletes/replacements. Live pages do not revisit newly inserted keys behind the cursor and may end early after deletions. Full gleam check, gleam test (120 passed, no failures), gleam format --check passed without warnings.
  • Re-ran required strict upstream check gleam run -m pds/nsid_interop: 51 passed, 1 failed, 0 skipped; exits1 on the unchanged NSID-001 long-authority fixture. No syntax behavior or fixture expectation changed. Storage checks pass independently; protocol compatibility remains open.
  • Reviewed pagination walkthrough: learner correctly predicted bb,c using cursor b, but also predicted bb,c for LIMIT2 OFFSET2. Explained that the new sorted dataset is b,bb,c,d,e. Keyset selects keys >b, yielding bb,c; OFFSET2 skips the first two current rows b,bb, yielding c,d. The cursor retains a key boundary; an offset retains a row count, which can shift after mutations. Recorded the actual attempt and correction rather than claiming both initial answers were correct. M3.2 implementation, required checks and walkthrough are complete.
  • Gate L remains incomplete pending M3.3 owner/concurrency/transaction checks; NSID-001 remains a separate open compatibility issue. No public storage route or HTTP startup database integration exists.

M3.3 checkpoint #

  • Observable outcome: two writes either commit together or both roll back, and a process owns the write connection so transactions cannot interleave.
  • Learner acknowledged the pagination explanation with “got it.” Agent continued with routine transaction scaffolding per the approved pace preference.
  • Implemented src/pds/storage/transaction.gleam: generic callback/return value, BEGIN IMMEDIATE, explicit commit/rollback, typed begin/operation/commit/rollback failures retaining both errors, and nested-BEGIN rejection without rolling back an outer transaction. Added a narrow Erlang exception bridge to roll back and re-raise programmer exceptions; close if that cleanup rollback fails. Caller must exclusively own the connection and close it on typed RollbackFailed; no callback transaction control or network requests.
  • Added eight meaningful transaction contract tests: two writes persist after reopen; duplicate second operation undoes first without losing preexisting data and connection recovers; plain duplicate leaves first; validation failure undoes replace/delete; nested begin preserves outer transaction; deferred foreign-key failure at commit rolls back; injected rollback failure retains original error; programmer exception cleans up before re-raise.
  • Prepared test/pds/storage_transaction_exercise.gleam. The familiar case propagates a first-write error, otherwise attempts the second. Runner uses separate temporary files to print the two-write result and first-record lookup with/without a transaction. Initial scaffold successfully writes different keys in both cases. Learner exercise is pending, not counted as complete or a default-suite test.
  • Verification: agent initially used a nonexistent io.debug helper in the runner; compiler caught it. Replaced it with io.println plus installed stdlib string.inspect. gleam check and gleam test passed (128 tests, no failures); initial explicit runner printed Ok(Nil) and first record Ok("{}") in both cases. D18 and STORAGE.md record the transaction contract and limitations.
  • Final scaffold check: replaced the exercise's result.try shorthand with a familiar case expression; compiler, initial runner and format check pass. Required strict NSID comparison rerun: 51 passed, 1 failed, 0 skipped, exits1 on unchanged NSID-001. This remains a separate open compatibility issue; no fixture or parser changed.
  • Learner requested that the agent handle rollback because transactions/rollbacks are already familiar. This explicitly replaces the hands-on rollback exercise; do not require the learner to perform it or explain basic transaction semantics before proceeding. Agent changed the second insert to duplicate the first key and removed the unused second path. The isolated runner now asserts both outcomes and is included in the default test suite. Plain writes return AlreadyExists while the first record remains Ok("{}"); grouped writes return OperationFailed(AlreadyExists) and the first record is NotFound. Rollback work is complete by delegation, not a claimed learner attempt.
  • Verification after that change: explicit rollback runner passed; gleam check, gleam test (129 passed, no failures), and gleam format --check passed without warnings. Runtime database untouched. Existing transaction tests still cover success/reopen, typed failures, rollback recovery and programmer-exception cleanup.
  • Actor scaffold implemented in src/pds/storage/owner.gleam. Added gleam_otp as a direct dependency, retaining installed 1.3.0. Actor initialiser opens the configured file inside the new process; opaque Owner exposes only messaging/monitoring, never the connection. Private messages carry typed reply Subjects. Write executes the entire ordered Create/Replace/Delete batch in one transaction inside one handler; expected write failures reply and continue, uncertain rollback state closes and stops. Shutdown closes before replying/stopping. Start is linked to its starter; automatic supervision is deferred. Restart evidence currently uses explicit shutdown/start, not automatic crash recovery.
  • Configured/verified foreign keys, WAL, synchronous FULL (pragma2), and busy_timeout250ms on fresh/reopened files. No lock retries. Request waits use a 1000ms monitored wait in a disposable relay, plus a 1100ms outer wait protected by an Erlang alias; expected timeout/dead owner returns typed Error instead of process.call panic. Relay/monitor/alias cleanup prevents late replies accumulating in the caller. Timeout does not cancel a queued/running write or prove it failed; do not blindly retry. These are local policies, not protocol constants or a hard execution-time guarantee for arbitrary SQLite work.
  • Added eight actor tests: gated concurrent duplicate creates yield exactly one commit; concurrent conflicting batches leave no losing partial write; injected JSON failure restores replaced/deleted/created rows and actor remains usable; explicit owner restart reopens durable state; stopped owner replies Unavailable without caller panic; a competing connection yields bounded Busy and recovery; suspended owner causes bounded timeout then processes the queued write after resume without caller mailbox pollution; bad startup returns typed InitFailed. Expanded database pragma assertions on fresh/reopened files.
  • Prepared test/pds/storage_owner_exercise.gleam. Get is deliberately unfinished: it receives the message but currently only continues, so the isolated runner prints an explicit Pending message after a typed timeout. It does not claim the lesson passed or join the default suite yet. After implementation, the same runner asserts successful reply, wrong-owner NotFound wrapping, continued processing, and read persistence after restart. Unit actor tests verify stored data through separate isolated database reads meanwhile; no Get success test is skipped or ignored.
  • Session was interrupted first by the learner's status question, then by a daemon restart. Agent checked actual files before continuing. Compiler, full suite (137 passed, no failures), format check, and initial exercise runner completed; pending Get message observed as expected. No server/listener or runtime data was used. M3.3/gate L remain incomplete until learner branch/read checks and card review are done; NSID-001 remains separately open.
  • Required strict NSID adapter rerun at the final actor-scaffold checkpoint: 51 passed, 1 failed, 0 skipped, exit1 on the unchanged NSID-001 fixture. No identifier code or fixture expectation changed. D19, README, STORAGE.md and SOURCES.md now reflect actor scaffold verification and the pending learner branch.
  • Learner completed Get correctly: bound account/path/reply, called dev_records.get, piped through result.map_error(RecordError), sent the full Result with process.send(reply, rsp), and returned actor.continue(connection). Agent preserved that implementation and removed only the obsolete pending comment. Reply goes to the caller; continuation supplies actor state for the next message. Actor memory disappears on restart; committed rows survive in SQLite.
  • Explicit actor exercise passed. Removed its pending fallback and added it to the default suite, so a missing/late reply now fails assertions rather than appearing as an unfinished exercise. Added two owner read contracts: full owner/collection/key/case scope, and genuine SQL failure stays typed while actor continues. Final gleam check, gleam test (140 passed, no failures), gleam format --check passed. Required strict upstream NSID command again reported 51 passed, 1 failed, 0 skipped, exit1 on unchanged NSID-001; compatibility remains unresolved separately.
  • Gate L evaluated against the plan's actual scope: working local requests, persisted CRUD, restart/rollback checks. Fresh listener on127.0.0.1:2583: eight HTTP health/method/describeServer/routing checks passed; independent Node binary WebSocket upgrade/exact00ff41/disconnect/HTTP-after-close checks passed, with on_close log observed. Stopped only that owned listener and verified no listener remains on2583. Persisted CRUD/keyset checks plus atomic owner/transaction/restart/busy/timeout tests pass. M3.3 and gate L pass for the local learning server only. This does not resolve M1.2/NSID-001 or pass R/S/C/O; no public storage route, runtime DB access or automatic supervisor was added.

M4.1 checkpoint #

  • Observable outcome: represent application records as a recursive AT Protocol value type, then convert checked JSON without losing null/empty/type distinctions.
  • Read current official data-model/DRISL/CID sources and Gleam tour; inspected actual pinned lex-data recursive type under packages/lex/lex-data/src/lex.ts and discovered lex-json source/test paths. Initial guessed paths were missing the lex directory and corrected via rg. Reference revisions verified unchanged. Dated original notes and contract checklist record reading status and outstanding requirements.
  • Imported all20 cases byte-for-byte from pinned interop data-model/ files: 5 valid, 12 invalid, 3 JSON/CBOR/CID vectors. SHA256/source URLs/counts/license are recorded in test/fixtures/interop/data-model/provenance.json; shared CC0 license already included. Fixture adapter is not implemented; import is not a fixture pass. Unchanged valid case123.0 must be accounted for in numeric decoding; do not equate all decimal-form JSON numbers with invalid protocol Float values without checking the original expectations.
  • Prepared src/pds/data_model.gleam with initial recursive Value: Null, Boolean, Integer, Text, Bytes, Array(List(Value)), Object(List(#(String, Value))). This is an unchecked teaching candidate, not the completed protocol type. CID Link will be added with a checked CID representation; Int bounds, byte alignment, map rules and resource budgets remain pending. Lists of field pairs preserve duplicate evidence; field helper detects duplicate matches only for its requested key and distinguishes MissingField/ExpectedObject from explicit Null. kind is a worked exhaustive case example. D20 records this implementation choice, not a protocol requirement.
  • Prepared test/pds/data_model_exercise.gleam. Learner implements example_record matching the JSON-shaped comment (Hello text, two tags, explicit reply null, nested active/count metadata) using constructors, and as_text using a Text match plus a non-text Error branch. Initial bodies are intentionally incomplete; explicit runner prints Pending, not a false passing claim. After implementation it checks nested fields and non-text rejection, and will then join the normal suite. Compiler and initial explicit runner passed; worked null/missing distinction passed before the Pending message. No runtime DB/listener touched.
  • Final scaffold verification: full gleam test remains140 passed with no failures; gleam format --check passed. Required strict NSID comparison remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. Data-model fixture adapter and learner exercise remain pending, not counted as passing tests.
  • Reviewed learner implementation: example_record correctly constructs Hello text, both Text tags inside Array, and explicit Null reply. It omits the required metadata Object. as_text is correct and idiomatic: Text(s) returns Ok(s), every other variant Error(Nil). Preserved learner code. gleam check and gleam format --check pass; explicit runner fails at the metadata assertion with Error(MissingField("metadata")), so the full construction exercise is not complete.
  • The runner stops before checking as_text because metadata is absent. Independently invoked the compiled as_text function using Erlang (after verifying its generated representation) on all8 prescribed cases: Hello/empty text succeed; null, false, zero, empty bytes/list/object return Error(Nil). All8 passed. No extra fixture adapter or default-suite test was added, and no runtime DB/listener used.
  • Learner added the correct nested metadata object with active/count fields, initially spelling the constructor model.Bool(True). Compiler reported Unknown module value and suggested Boolean. Agent corrected only that naming slip to model.Boolean(True); preserved the learner's remaining construction and as_text logic. Bool is the built-in Gleam type; Boolean is our Value constructor carrying a Bool.
  • The complete exercise now passes: nested text/list/null/object/boolean/integer construction, present-null vs missing lookup, Hello/empty text extraction and rejection of all non-text variants. Removed pending fallbacks, retained assertions, and added the completed runner to default discovery. Final gleam check, explicit exercise, gleam test (141 passed, no failures) and gleam format --check passed without warnings. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001; no fixture or identifier code changed.
  • Inspected installed gleam_json3.1.0 and OTP29 JSON parsing: default parsing hides duplicate fields and converts decimal tokens to machine floats. Local probes show duplicate x fields collapse to one, and 9223372036854775807.0 rounds. OTP json:decode/3 exposes object and number callbacks for retaining pairs and original numeric tokens; this is the planned decoder approach, not implemented decoding. Pinned lex-json accepts integer-like123.0; its JavaScript safe-integer cap does not replace our signed64-bit contract.
  • Added src/pds/encoding/json.gleam as an incremental in-memory encoder lesson. Supplied scalar/object conversion checks exact signed64-bit bounds, byte alignment, standard padded base64 wrappers and duplicate keys before encoding objects. Object traversal is a worked list.try_map/result.map example. CID links, reserved-field semantics, budgets and decoding remain pending; this encoder is not a complete protocol boundary.
  • Added four default regression tests for exact integer limits, bytes/alignment, nested duplicate rejection and escaping/false/zero/empty/null/absence. gleam check, gleam test (145 passed, no failures), and gleam format --check pass. Explicit gleam run -m pds/json_encoding_exercise reports Pending as designed; array assertions have not run and are not counted as passed or silently skipped fixture checks. Required strict NSID adapter still reports51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database or listener touched.
  • Learner completed encode_array with an explicit case on list.try_map(values, to_json): Ok children become json.preprocessed_array; Error propagates unchanged. This is correct and idiomatic; result.map would be equivalent shorthand, not a required rewrite. Preserved learner logic, removed the temporary ArrayExercisePending variant and obsolete exercise comments, and replaced the runner's pending fallback with assertions. Added the completed lesson to default discovery.
  • Verification: gleam check, explicit JSON encoding exercise, gleam test (146 passed, no failures), and gleam format --check pass. Empty/mixed/nested arrays, the previous example_record, and propagation of integer/duplicate/unaligned-byte errors all pass. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database or listener touched.
  • Implemented json_syntax.gleam with a typed raw syntax tree and opaque NumberToken; pds_json_syntax.erl uses OTP callbacks to retain field pairs and numeric information without machine-float conversion. Duplicate decoded keys are rejected before dictionary conversion, including escaped aliases. Numeric spelling is not verbatim: OTP normalizes1e2 to1.0e2 and -0 to0, while preserving mathematical value. Correct the earlier original-token wording accordingly; exact numeric information, not source spelling, is the contract.
  • Parser local budgets: input<=1,048,576 UTF8 bytes, container depth<=64, all values including root<=10,000 nodes (field names excluded), callback number text<=128 bytes. These are project resource policies, not protocol bounds. Node accounting is carried across sibling containers; checks include empty containers. Reject malformed JSON and non-whitespace trailing data with typed errors. Exact integer conversion strips insignificant decimal zeroes and computes scale without machine floats or exponent-sized allocations; fractional and signed64-bit-overflow errors stay distinct. Zero with a huge exponent remains zero.
  • Added nine default parser/numeric regression tests covering shapes/order, decoded duplicates and separate scopes, integral decimals/exponents, exact integer bounds, fractions/overflow/huge exponents, malformed/trailing input, byte/number/depth/global-node boundaries. Initial scaffold compile failed because an agent-authored test referenced nonexistent string.from_utf8; corrected the test before verification. Final gleam check, gleam test (155 passed, no failures), and gleam format --check pass. Scalar runner verifies supplied null/false/empty-text cases then reports Pending for the numeric branch; its remaining exercise assertions have not run and do not count as passes. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener or fixture edits.
  • Learner completed the JsonNumber branch with syntax.number_to_integer(token) |> result.map(model.Integer) |> result.map_error(InvalidNumber). Correct and idiomatic: map transforms only success, map_error transforms only failure. Preserved learner implementation; removed temporary NumberExercisePending and replaced the runner's pending fallback with assertions. Added completed scalar lesson to default tests, including scalar encode/decode round trips for null, true/false, zero, integer extrema, empty and escaped Unicode text.
  • Final verification: gleam check, explicit scalar decoding exercise, gleam test (156 passed, no failures), and gleam format --check pass. Integral decimals/exponents, exact signed64-bit values, fractional/overflow error wrapping, malformed syntax and scalar round trips pass. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener or fixture edits.
  • Separated syntax parsing from private from_json conversion; preserved the learner's scalar numeric pipeline. Added public decode(text) for recursive conversion of the bounded, duplicate-checked tree. Supplied Array conversion as a worked list.try_map/from_json/model.Array example. Kept decode_scalar as a scalar-only convenience API with meaningful ExpectedScalar for compound inputs, replacing its obsolete CompoundValuesPending variant and updating existing scope assertions.
  • Objects containing $bytes, $link or $type return explicit ProtocolObjectPending until checked protocol interpretation exists; recognized wrappers cannot silently become generic Objects. This is a teaching-stage limitation, not a claim that such objects are invalid. Unknown application/reserved names are retained without assigning semantics by the upcoming ordinary-object traversal; current spec was rechecked and says unknown reserved fields should be ignored. The fixture adapter remains unimplemented and all20 imported expectations remain unchanged/unexecuted.
  • Prepared private decode_object_fields as the learner exercise and test/pds/json_object_decoding_exercise.gleam as explicit runner. It currently reports Pending, not a pass. Once completed, assertions check empty/nested objects and arrays, the previous example_record, field spelling/order, application/unknown-reserved field preservation, Null vs absent, numeric/duplicate errors and nested ProtocolObjectPending. No object exercise assertions are counted as passed yet.
  • Scaffold verification: gleam check, gleam test (159 passed, no failures), and gleam format --check pass. Three new default tests verify supplied array traversal, nested numeric/syntax error propagation and explicit protocol-object pending guards; these are scaffold checks, not protocol fixture conformance. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener or fixture edits.
  • Learner completed decode_object_fields correctly: destructures each #(name, raw), recursively calls from_json, maps successful children back to #(name, value), and wraps the successful list in model.Object. Both result.map stages preserve errors and list.try_map preserves order. The final fn(arr) { model.Object(arr) } is valid; passing model.Object directly would be optional shorthand, not a required rewrite. Preserved learner logic, removed ObjectExercisePending and obsolete exercise comments, and put the asserted completed runner into default discovery.
  • Final verification: gleam check, explicit object decoding exercise, gleam test (160 passed, no failures), and gleam format --check pass. Empty/nested object/array round trips, previous example_record, field spelling/order and unknown application/reserved field retention, Null vs missing, numeric/duplicate errors and nested protocol-object guards pass. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener or fixture edits.
  • Rechecked current data-model spec and RFC4648 plus actual pinned lex-json bytes helpers/tests and lex-data base64 adapters. $bytes must be a one-field object containing a standard-base64 string; padding is optional. Installed stdlib/OTP decoder accepts whitespace and normalizes some malformed partial padding, so add alphabet/length/padding checks before decoding. RFC4648 allows decoders to tolerate nonzero unused pad bits; our helper tolerates them, while our encoder emits canonical zero pad bits. This decoder tolerance is a documented local choice, not a mandatory protocol rule.
  • Added pds/encoding/base64.gleam: typed TooLarge/InvalidAlphabet/InvalidPadding/InvalidLength/InvalidEncoding, standard alphabet only, full correct padding or none, no whitespace/URL-safe alphabet, local1MiB encoded-input cap. Six default regressions verify RFC vectors with/without padding, binary +/ characters, malformed alphabet/length/padding, all256 single-byte round trips, pad-bit tolerance and exact helper input cap. Initial agent test referenced nonexistent list.range; corrected before verification.
  • Prepared private decode_bytes_wrapper as the learner exercise; dispatch finds $bytes regardless of field order before generic-object conversion. Introduced BytesError ExpectedString/ExtraFields/InvalidBase64(cause), DecodeError InvalidBytes(cause), and explicit temporary BytesExercisePending. Link/type objects remain ProtocolObjectPending. Updated previous temporary guard assertions to continue covering link/type pending states; bytes shape/content checks now live in the explicit bytes lesson, whose assertions have not run yet.
  • Scaffold verification: gleam check, gleam test (166 passed, no failures), and gleam format --check pass. gleam run -m pds/json_bytes_decoding_exercise reports Pending as designed, not wrapper success. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener, fixture edits or data-model conformance claim.
  • Learner requested help with a stuck attempt. Reviewed source and ran gleam check: two errors, at result.map(model.Bytes) because result.map(base64.decode) adds a nested Result, and at InvalidBase64(e) because earlier shape errors already have the outer DecodeError type. list.map also processes individual fields before validating wrapper cardinality; its final panic is reachable for extra-field input. Supplied a corrective walkthrough: case-match the whole fields list, call base64.decode directly in the one-string branch, map its success/error only there, and return already-wrapped shape errors in the other branches. No learner source change or successful wrapper/test claim made; the current attempt remains to be corrected.
  • Learner applied the corrective whole-list case: one $bytes JsonText directly decodes base64 and maps its success/error; one wrong-kind value returns ExpectedString; remaining shapes return ExtraFields. This removes nested Results and the reachable panic. Preserved learner implementation, removed BytesExercisePending/obsolete exercise comments, and added the completed asserted bytes runner to default discovery.
  • Final verification: gleam check, explicit bytes exercise, gleam test (167 passed, no failures), and gleam format --check pass. Empty/padded/unpadded/escaped-text bytes wrappers, exact binary and nested round trips, wrong value kinds, extra fields in either order, malformed base64 with retained causes and duplicate-key errors pass. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener or fixture edits.
  • Rechecked current AT Protocol data-model, DASL CID and RFC4648 sources plus pinned lex-data CID flavor checks/tests. The required profile uses exactly four one-byte metadata fields (version1, raw0x55/DRISL0x71, SHA2560x12, size0x20) and32 digest bytes; text is lowercase b-base32. Thus binary36 bytes/text59 characters. No generic varint machinery is needed for this fixed profile; M4.2 still teaches varints for other formats. Generic CID syntax fixtures accept broader IPFS multibases/codecs and are not the expected-valid inputs of this narrower protocol-value type. No generic CID fixture adapter or compatibility claim added.
  • Added bounded lowercase/unpadded base32 helper with canonical zero unused bits and local max64 byte/max103 text-byte limits; four default tests cover RFC vectors, every single-byte round trip, alphabet/length/padding bits and alignment/resource bounds. CID module exposes Codec(DagCbor/Raw), opaque Cid, parse/from_bytes/getters and typed metadata/text errors. Only private construction after validation can return Cid; validate_codec remains intentionally CodecExercisePending, so valid CID construction is still pending. No model.Link or JSON $link conversion added yet.
  • Three default CID tests verify supplied version/hash/digest-size checks, truncated/extra/unaligned bytes, nonminimal-varint form rejection, text prefix/length/alphabet/unused-bit rejection. Explicit test/pds/cid_exercise.gleam reports Pending, then after implementation will check both codecs, unsupported codes, exact structured digest/header, and text/binary round trips for structured/raw links from unchanged data-model-fixtures.json. Independently decoded both texts with Python standard base32 to verify the runner's metadata and exact expected digest; this is reference preparation, not passing Gleam CID vectors or full data-model fixture conformance.
  • Scaffold verification: gleam check, gleam test (174 passed, no failures), and gleam format --check pass. Explicit CID runner reports Pending, not success. Agent fixed bit-array construction syntax/options before verification; no learner code changed. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener, fixture edits or hashing/CBOR/block-storage claims.
  • Reviewed learner's validate_codec: hexadecimal0x71 returns Ok(DagCbor),0x55 returns Ok(Raw), all other codes return Error(UnsupportedCodec(code)). Preserved the match. Explicit CID runner passes both codecs, unsupported codes and selected exact structured/raw text-binary vectors. Removed CodecExercisePending and added the completed runner to default discovery. This verifies CID identifier format, not CBOR encoding or content hashes; no complete data-model fixture-vector result claimed.
  • Added Value.Link(cid.Cid), an exhaustive kind branch and supplied JSON encoding as a single $link string field. Two default regressions verify both codecs' exact wrappers, nested wire shape and sibling error propagation. Linked content is not fetched or decoded here.
  • Prepared decode_link_wrapper and test/pds/json_link_decoding_exercise.gleam. Learner matches the complete field list as in the completed bytes lesson, calls cid.parse, maps its success to model.Link and wraps typed errors. Explicit runner currently reports Pending; after implementation it checks structured/raw and escaped-key links, nested binary/link round trips, wrong value kinds, extra fields in either order, typed invalid CID causes and duplicate-key rejection. No default test freezes LinkExercisePending behavior; existing protocol-object guards now target still-pending $type objects.
  • Verification: gleam check passes; gleam test passes177 tests with no failures; explicit link runner reports Pending. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener, fixture edits or content hashing.
  • Learner explicitly delegated $link handling because it repeats the completed bytes helper with only a minor change. Agent implemented exact whole-list matching, cid.parse -> model.Link and InvalidLink(InvalidCid(error)) wrapping, plus wrong-kind/extra-field errors. Removed LinkExercisePending and added the asserted link runner to default discovery. Record this as agent-completed at the learner's request, not a learner implementation.
  • Initial runner exposed an agent test-input mistake: bogus starts with the valid b prefix, so it correctly returns InvalidTextLength rather than the expected InvalidTextPrefix. Corrected the prefix-isolation input to zogus; no CID/parser policy or learner code change. Explicit link runner now passes both codecs, escaped-key recognition, nested round trips, shape failures, exact CID causes and decoded-key duplicate rejection.
  • Final verification: gleam check, explicit link runner, gleam test (178 passed, no failures) and gleam format --check pass. Strict NSID command was rerun earlier this checkpoint:51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener or fixture edits; full JSON fixture conformance and content hashing remain unclaimed.
  • Rechecked current data-model, Lexicon and repository sources plus pinned lex-data/lex-json blob and typed-object helpers. Revisions remain a7c8604d876a200a1e4fa4aec83d4189ac4f1c12 /056e5741bb330757205d6b16db5266fffcae937b. Read actual copied valid/invalid JSON cases. A generic Value decoder accepts scalars/arrays; future record/fixture object boundaries must separately require the applicable object shape. Nonempty $type is a generic discriminator check, not full Lexicon schema or NSID validation (definition fragments and blob are distinct cases).
  • Supplied decode_typed_object: reject empty/wrong-kind tags, preserve the complete Object and recursively converted children, and dispatch the blob tag to blob.from_value. Removed ProtocolObjectPending; previous pending-type guards now assert actual typed-object preservation. Two new default typed-object tests cover nested round trips, definition fragments/unknown field retention, exact123.0 normalization, bad tags, child errors and escaped-key duplicates. These selected local checks are not the full fixture adapter.
  • Added src/pds/blob.gleam: opaque checked Blob metadata with reference/MIME/size getters; supplied from_value validates exact blob marker, required field presence and target duplicates. Two default tests verify those supplied checks and missing-ref JSON error propagation. JSON keeps the complete Object, including unknown fields; metadata is a separate projection. The learner implements validate_fields for Link with Raw codec, nonempty Text MIME and positive signed64-bit Integer size, returning private Blob only on success. Distinct typed errors and an explicit pending runner are supplied. Error precedence for multiply-invalid inputs is not imposed.
  • Source distinction recorded in D25/reading notes: live blob spec requires positive nonzero size and nonempty MIME string; pinned helper's size condition accepts0 and uses JS safe-integer bounds, requires a MIME slash and rejects additional fields. We follow the checked normative size/integer rules and retain unknown object fields as the current project representation policy. MIME grammar/unknown-field behavior are not a claim of reference equivalence. No unchanged data-model fixture currently covers these reference differences; no full compatibility claim, adapter or source edit added.
  • Scaffold verification: gleam check, gleam test (182 passed, no failures), and gleam format --check pass. Explicit blob runner prints Pending; positive metadata/complete blob assertions await the learner. Agent corrected the runner's final-expression return type during preparation and used constructor destructuring in the supplied getter; final compilation has no warnings. Strict NSID adapter remains51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener or fixture edits.
  • Learner supplied validate_fields using Link/Text matching, result.try to propagate codec/size/MIME failures, guarded signed64-bit size validation and private Blob construction. gleam check passes. Explicit runner reaches the first wrong-kind ref case and fails: ref Null expects RefExpectedLink but receives RefMustBeRaw from the final , fallback. The RefMustBeRaw case inside cid.codec is appropriate for an actual Link with the structured codec. Preserved the attempt; no implementation edit made.
  • Added a contextual assert ... as message to the failing loop, including ref input and expected/actual errors. Re-ran the explicit exercise: same intended failure, now prints For ref=Null, expected Error(RefExpectedLink), got Error(RefMustBeRaw). Compiler passes; later exercise assertions have not run, and complete blob validation is not yet claimed.
  • Learner corrected the final fallback to RefExpectedLink. Reviewed and preserved the remaining implementation: Link/Text matching establishes field kinds, result.try propagates codec/size/MIME errors, the integer guard checks the exact positive signed64-bit range, and private Blob construction occurs only after success. All explicit blob assertions now pass, including wrong-kind vs wrong-codec errors, metadata getters, size boundaries, missing/duplicate fields and nested JSON round trips. No imposed error precedence or implementation rewrite.
  • Removed ValidationExercisePending and obsolete exercise comments; replaced the runner's pending branch with unconditional assertions and added blob_metadata_field_validation_and_nested_json_test to default discovery. Kept the contextual assertion message so regressions show input/actual/expected.
  • Final verification: gleam check, explicit blob exercise, gleam test (183 passed, no failures) and gleam format --check pass. Strict NSID comparison again reports51 passed,1 failed,0 skipped, exit1 on unchanged NSID-001. No runtime database/listener or original fixture changes. Metadata shape is verified; actual file bytes/hashes, full schema validity and full data-model fixture conformance remain unclaimed.
  • Rechecked the data-model, Lexicon and repository sources plus pinned reference record handling. Generic values legitimately include scalars, arrays and untyped objects; repository values are CBOR objects, and Lexicon record types always include a $type discriminator. A generic typed object may carry a definition fragment, but a repository record type is an NSID. This is a narrow shape boundary only: it does not know the Lexicon schema or validate application fields.
  • Added src/pds/record.gleam: opaque RepositoryRecord, typed errors and getters/round-trip conversion. Added json.decode_record, which decodes generic JSON first, then invokes that narrow boundary. A default regression confirms generic decoding still accepts text, arrays and untyped objects. test/pds/record_exercise.gleam covers valid NSID records, non-object roots, missing/wrong/duplicate $type, blob/fragment non-record types and JSON boundary behavior.
  • Learner implemented record.from_value correctly: it obtains Object fields, uses model.field to retain missing/duplicate causes, requires Text, maps nsid.parse failures into InvalidRecordType, and constructs the opaque record only after those checks. The use of result.try keeps each failure typed and short-circuits safely. Preserved the implementation.
  • Removed RecordExercisePending and temporary runner branch; added the completed record runner to default discovery. Final verification: gleam check, explicit record exercise, gleam test (185 passed, no failures), and gleam format --check pass. No fixture outcome, schema validation or repository persistence claim is made.
  • Next exact action: specify and implement map/reserved-field semantics. Recheck the data-model rule that unknown $ fields are ignored semantically, distinguish wrapper-reserved shapes from ordinary unknown $ fields, preserve duplicate-key rejection at syntax parsing, then add constructed-value encoder budgets and the unchanged data-model fixture adapter before closing M4.1.
  • Learner completed validate_object_fields with a list.fold over fields. It preserves unknown reserved fields, rejects $bytes/$link with the field name, and is invoked after duplicate-key checking in to_json; nested Objects therefore receive the same validation during recursive encoding. Removed the pending constructor and added the runner to default discovery. Final verification: gleam run -m pds/json_reserved_fields_exercise, gleam check, gleam test (186 passed, no failures), and gleam format --check pass. $type remains a valid generic typed Object rather than a wrapper collision; its decode-side tag/blob checks are already separate.
  • Next exact action: add finite constructed-value encoding budgets (total JSON output bytes, nesting depth, and value-node count) with typed errors, then create the unchanged official fixture adapter before closing M4.1.
  • Started the constructed-value tree-budget exercise. ValueBudgetError, parser-matching depth/node constants, a pending validate_value_limits, and an explicit json_value_budgets_exercise runner now exist. The learner will count containers for depth and every Value including the root for nodes, then invoke the validator once in encode before to_json. Output-byte accounting is deliberately the following substep. The existing suite remains green while this explicit exercise is pending.
  • Learner completed the tree-budget traversal. It increments the node total before processing each Value, carries that total through each list/object field fold, and propagates depth through nested containers. encode maps its typed failure before building JSON. Removed the pending constructor and added the runner to default discovery. Final verification: explicit runner, gleam check, gleam test (187 passed, no failures), and format check pass.
  • Started the serialized-output budget exercise. OutputBudgetError, max_output_bytes, a pending validate_encoded_bytes, and an explicit runner cover exact ASCII boundaries, multibyte UTF-8, and JSON escape expansion. The learner will validate the completed JSON string in encode; current default tests remain green until then.
  • Learner completed validate_encoded_bytes with string.byte_size, then restructured encode to validate the constructed tree, convert to JSON, validate the final serialized string, and return it. This correctly counts UTF-8 bytes and JSON expansions after serialization. Removed the pending constructor and added the runner to default discovery. Final verification: explicit runner, gleam check, gleam test (188 passed, no failures), and format check pass.
  • Next exact action: implement the unchanged official data-model JSON fixture adapter, report valid/invalid outcomes separately, and resolve any real contract gap without weakening fixture expectations.
  • Added test/pds/data_model_interop_test.gleam, a semantic adapter for every unchanged JSON fixture: all 5 valid and all 12 invalid cases are evaluated and reported before assertion. The adapter requires an Object root only for this fixture family, preserving the broader generic codec.decode API for valid scalar/array roots. 123.0 normalizes exactly to Integer; typed-object, blob, bytes, and link failures match their invalid fixture expectations. Final verification: gleam check, gleam test (190 passed, no failures), and format check pass. The three copied CBOR/CID vectors remain explicitly pending M4.2/M4.3; no complete binary-vector result is claimed.
  • Next exact action: begin M4.2 with unsigned varint encoding/decoding and consumed-byte accounting, then use the imported binary vectors for deterministic CBOR/CID work.
  • Started M4.2 unsigned-varint encoding. Added pds/encoding/varint with a pending encoder and explicit runner covering zero, one-byte boundary127, multi-byte 128/255/300/16,384 vectors, and typed negative-input rejection. The format is the multiformats unsigned LEB128 variant: seven low-order bits per byte, continuation high bit, and minimal output. Decode-side 9-byte/63-bit, minimality, truncation and consumed-byte checks follow after this first encoding exercise.
  • Learner completed the recursive encoder: negative values return the typed error; final payload groups emit unchanged; non-final groups set the high continuation bit before appending the recursively encoded remainder. Removed the pending constructor and added the runner to default discovery. Final verification: explicit runner, gleam check, gleam test (191 passed, no failures), and format check pass.
  • Started bounded unsigned-varint decoding. Added typed outcomes for unaligned input, truncation, nine-byte cap and non-minimal forms, plus an explicit runner. The learner will decode one prefix into #(value, consumed_bytes), leaving trailing bytes for CBOR. Exact checks cover 0/127/128/300, signed63 maximum, a trailing-byte consumption count, empty/continued truncation, leading-zero non-minimal form, too-long input and a partial byte.
  • At learner request, the agent completed the decoder mechanics. It prechecks byte alignment, accumulates seven-bit payload groups, stops at the first clear continuation bit, returns consumed bytes, rejects an empty/unterminated sequence as truncated, rejects a ninth continuation byte, and rejects multi-byte zero final groups. Removed the pending constructor and added the runner to default discovery. Final verification: explicit runner, gleam check, gleam test (192 passed, no failures), and format check pass.
  • Resumption audit on 2026-10-06 found unlogged, newer M4.2 work in src/pds/encoding/cbor.gleam and test/pds/cbor_scalar_test.gleam. It implements canonical scalar headers and UTF-8 byte lengths, strict scalar/array decoding, definite array encoding, canonical text-map encoding with duplicate-key rejection, and DAG-CBOR tag-42 link encoding. The five focused CBOR tests pass. gleam check, gleam test (197 passed, no failures), and gleam format --check passed at that recovery point.
  • At the learner's request, the agent completed the remaining M4.2 work. decode_prefix reports exact consumed bytes; strict decode rejects trailing data. It decodes definite text-key maps in wire order and rejects non-text/duplicate keys, decodes tag-42 identity-prefixed checked CID links, and rejects nonminimal/indefinite forms. Both directions enforce the local depth/node/input budgets. Nine focused CBOR tests include map/link boundaries, budget failures, prefix consumption, and all three imported exact CBOR byte vectors; each vector decodes and re-encodes byte-for-byte. gleam check, gleam test (204 passed, no failures), and gleam format --check pass. The fixture CIDs have not been recomputed from content yet: that is M4.3's hashing work.
  • Next exact action: begin M4.3 by adding a narrow SHA-256 adapter, construct raw/DAG-CBOR CIDs from exact content bytes, and create immutable in-memory block insertion that rejects equal CIDs with different bytes.
  • M4.3 complete locally: pds/encoding/hash is a narrow SHA-256 boundary; cid.for_raw/for_dag_cbor derive the digest from exact bytes, and matches_content independently verifies it. The SHA-256 abc vector verifies the exact digest, Raw and DAG-CBOR CIDs differ for identical content, and all three imported CBOR byte vectors hash to their unchanged expected DAG-CBOR CID strings. pds/block_store is a pure immutable store: verified insertion is idempotent for identical bytes, lookup works, and supplying changed bytes under an existing CID returns ContentDoesNotMatchCid without replacing the original. Final gleam check, gleam test (207 passed, no failures), and gleam format --check pass.
  • Next exact action: begin M5.1 by inspect the pinned MST fixture format/reference helpers, then implement and test the deterministic key-layer calculation and empty-tree wire representation.
  • M5.1 first substep: inspected pinned packages/repo/src/mst/util.ts. Its layer rule counts leading base-4 zero digits in SHA-256 output: byte thresholds 64/16/4 add one digit each, and an all-zero byte adds the fourth then continues. Learner implemented total layer_from_hash; empty, high-byte and partial-bit tails stop at zero. Eight byte-boundary/multi-byte and two partial-byte tests pass. gleam check, gleam test (209 passed, no failures), and gleam format --check pass.
  • Next exact action: scaffold layer_for_key around the existing SHA-256 adapter so it hashes UTF-8 key bytes then delegates to layer_from_hash; test fixed known keys against independent reference values before beginning node encoding.
  • Learner completed layer_for_key by converting the String to UTF-8 bytes, hashing with the narrow SHA-256 adapter and delegating to layer_from_hash. The independently generated com.example.record/a/b and app.bsky.feed.post/self vectors produce layers0/1/2. The completed explicit runner now joins default discovery. gleam check, runner, gleam test (210 passed, no failures), and gleam format --check pass.
  • Next exact action: model the empty MST node as left subtree Null plus an empty entry array, encode it deterministically as DAG-CBOR, and derive its CID before adding nonempty entries/prefix compression.
  • Learner completed empty_node_value as an Object with l: Null and e: Array([]). Exact deterministic CBOR is a2 61 65 80 61 6c f6 (canonical key order e/l); its independently computed DAG-CBOR CID is bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm. The completed runner now joins default discovery. gleam check, explicit runner, gleam test (211 passed, no failures), and gleam format --check pass.
  • Next exact action: implement ASCII-only shared-prefix counting for adjacent MST keys, then use it to form the first compressed leaf entry; key syntax and full node validation follow separately.
  • Learner completed total byte-oriented common_prefix_length; it recurses only across equal byte-aligned leading bytes and safely stops at a mismatch, empty input or a partial tail. The adjacent-key example returns16 and focused 0/2/3-byte cases pass. An initially incorrect agent partial-tail test was corrected to the documented stopping result0; learner code required no change. gleam check, explicit runner, gleam test (212 passed, no failures), and gleam format --check pass.
  • Next exact action: introduce a single compressed leaf Value from a validated ASCII key and a checked CID, with prefix0 for the first key; defer subtree pointers and multi-entry node assembly to the next substep.
  • Learner implemented first_leaf_value: UTF-8 bytes are checked bytewise for ASCII, non-ASCII returns NonAsciiKey, and a valid first leaf has p:0, bytes k, CID v, and null right-subtree t. Empty ASCII keys intentionally remain outside this substep's structural validation. Exact field/value and Unicode-rejection tests pass. gleam check, explicit runner, gleam test (213 passed, no failures), and gleam format --check pass.
  • Next exact action: define structural MST-key validation (two nonempty ASCII collection/rkey segments, allowed MST key characters, 1024-byte bound) before accepting leaf keys for complete node serialization.
  • Learner completed validate_key and wired it into leaf construction. It first rejects non-ASCII UTF-8 bytes, enforces the1024-byte maximum, requires exactly two nonempty slash-separated segments, then validates allowed characters in each segment (not the separator). Valid 1024-byte, structural, Unicode, whitespace and punctuation cases pass. An initial agent test exposed the deliberate separator/segment distinction; learner fixed it by checking the split parts. gleam check, explicit runner, gleam test (214 passed, no failures), and gleam format --check pass.
  • Next exact action: serialize a list of validated leaf Values into a complete single-level MST node, preserving lexical key order and prefix-compressing each key relative to its predecessor; left/right subtree support follows separately.
  • Learner completed compressed_leaf_value: validates the current key, calculates the byte-prefix length from its predecessor and retains only the remaining key bytes as k; each leaf has null t. First-leaf and shared-prefix outputs pass, as does current-key Unicode rejection. An initial agent test used a three-segment illustrative key and was corrected to the valid equivalent bsky.posts/...; learner code required no change. gleam check, explicit runner, gleam test (215 passed, no failures), and gleam format --check pass.
  • Next exact action: define a single-level node constructor for a list of key/CID pairs: validate keys, sort them lexically, reject duplicates, and map each into compressed leaf values under l: null and e: [...].
  • Started the single-level node-constructor exercise. leaf_node_value in src/pds/repo/mst.gleam is a pending stub returning Error(NodeExercisePending); permanent DuplicateKey and temporary NodeExercisePending variants are added to LeafError. test/pds/mst_node_exercise.gleam prints Pending until implemented, then asserts empty/single/unsorted-two/duplicate/non-ASCII cases (second-entry prefix 19 for com.example.record/). Scaffold verification: gleam check passes with one expected unused-argument warning on the stub; gleam test (215 passed, no failures); gleam format --check and explicit runner pass. The runner joins default discovery after the learner's implementation.
  • Learner's first attempt missed the sort: the recursion processed caller-supplied order, so the out-of-order runner assertion (b then a) failed with the wrong leaf order and compression basis. Reviewed the trace with the learner, who then added list.sort(entries, fn(a, b) { string.compare(a.0, b.0) }) before recursing and switched the accumulator to prepend plus one list.reverse at the end. Reviewed the corrected code; preserved all learner logic. Removed the temporary NodeExercisePending variant and obsolete exercise comments; the runner's assertions are now unconditional and single_level_mst_node_test joins default discovery. Final verification: explicit runner, gleam check, gleam test (216 passed, no failures), and gleam format --check pass.
  • Next exact action: encode a single-level node as deterministic DAG-CBOR and derive its CID (checking against an independently computed value), then introduce left/right subtree links (l/t) so a node can point at child subtrees.
  • Started the node-block encoding exercise. node_block in src/pds/repo/mst.gleam is a pending stub returning Error(NodeBlockExercisePending); the new NodeError type wraps each stage's error (InvalidEntries(LeafError), EncodingFailed(cbor.EncodeError), CidFailed(cid.CidError)) plus the temporary pending variant. test/pds/mst_node_block_exercise.gleam prints Pending until implemented, then asserts the empty node's exact bytes/CID, a two-entry node's 134-byte block and independently computed CID, and wrapped InvalidEntries failures for non-ASCII and duplicate keys. The expected two-entry CID bafyreifvm6svoq3hzvrrnhchtz7d6gsnj4j65lc7lbz5l6hstimmpdff74 was computed with a from-scratch Python DAG-CBOR encoder whose empty-node output first reproduced the known empty CID bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm. Scaffold verification: gleam check passes with one expected unused-argument warning on the stub; gleam test (216 passed, no failures); gleam format --check and explicit runner pass. The runner joins default discovery after the learner's implementation.
  • Next exact action: learner implements node_block — leaf_node_value, cbor.encode, cid.for_dag_cbor composed with result.try, converting each stage's error with result.map_error into the matching NodeError constructor, returning Ok(#(cid, bytes)).
  • Learner completed node_block on the first attempt: three use <- result.try stages with result.map_error lifting each stage's error into NodeError, returning Ok(#(cid, enc)). The independently computed two-entry CID matches, so validation, sorting, compression, CBOR and hashing agree with a from-scratch external encoder. One readability observation (left unchanged): the bound local name cid shadows the imported cid module alias inside that continuation; harmless here because the module is not referenced afterward. Removed the temporary NodeBlockExercisePending variant and obsolete exercise comments; runner assertions are unconditional and mst_node_block_test joins default discovery. Final verification: explicit runner, gleam check, gleam test (217 passed, no failures), and gleam format --check pass.
  • Next exact action: introduce subtree links — represent a child subtree as its block CID, generalize node construction so l and each entry's t can hold an optional link instead of always null, then partition a sorted entry list by key layer into the gaps that become child subtrees.
  • Started the subtree-link exercise. Confirmed the wire contract in pinned packages/repo/src/mst/util.ts (serializeNodeData): l and entry t are always written, explicit null when absent; a subtree after a leaf is that leaf's t; a leading subtree is l; two adjacent subtrees are invalid. (Read-only reference checkout; its AGENTS.md contributes rules for that repo only.) Scaffold: LeafError gains temporary SubtreeLinkPending; supplied worked-example helper link_or_null(Option(Cid)); leaf_node_value now takes left: Option(Cid) and entries as #(key, value, right) triples behind a gate — link-free input behaves exactly as before, any requested link returns Error(SubtreeLinkPending); node_block takes the same input. Agent updated the two completed node runners to the new signatures with None links (plumbing only, assertions unchanged). test/pds/mst_subtree_link_exercise.gleam prints Pending until links are threaded, then asserts the intermediate empty node (l link only), a right-link entry, a left-link two-entry node, and a full linked block whose 214-byte size and CID bafyreihyyxi4wpzoxovjl256mkniqpmeik5ipvodlkxrxvf2dfspugtjze were computed independently with the from-scratch Python encoder (empty-node sanity check passed again). Scaffold verification: gleam check passes with one expected unused warning on the supplied link_or_null; gleam test (217 passed, no failures); gleam format --check and explicit runner pass.
  • Next exact action: learner threads the links — remove the gate, give both leaf builders a right: Option(Cid) parameter emitting t via link_or_null, carry each triple's right link through the recursion, build the terminal node's l from the threaded left, honor left in the empty-entries branch, and update the two completed leaf runners to pass None (compiler points at the call sites).
  • Learner asked what "thread it in or restructure" meant; agent explained the scope problem (left bound in leaf_node_value but needed in the helper's terminal branch) with both options: add a carried parameter vs return the leaf list and assemble the node in the public function. Learner chose the carried parameter.
  • Learner's first completed attempt updated signatures and the terminal branch but the two recursive branches still used old arities and discarded each triple's right link with _ (four compiler arity errors). After review, learner fixed them: right link bound from the triple and passed to the builders, left forwarded at each recursive call. Learner also generalized empty_node_value(left) on their own initiative, covering the intermediate-empty-node case in one place. Agent updated the four affected completed runners to the new arities with None links (plumbing only, assertions unchanged) at the learner's request, since they are the check-of-record. All exercise assertions pass, including the independently computed linked-block CID. Removed the gate machinery (SubtreeLinkPending, has_right_link) and obsolete exercise comments; runner is now unconditional and mst_subtree_links_test joins default discovery. Final verification: explicit runner, gleam check (no warnings), gleam test (218 passed, no failures), and gleam format --check pass.
  • Next exact action: partition a sorted, layer-annotated entry list at a given layer — keys at this layer stay in the node, each sorted gap between them becomes input for a child subtree — then assemble a complete multi-level tree recursively and compare its root CID against an independently computed value.
  • Started the layer-partitioning exercise. Generated additional known-layer keys with an independent Python layer calculation (after correcting the script's double halving — the leading base-4 digit count is the layer): com.example.record/ab→2, ba→1, c→0, zz→1, reconfirming the three existing vectors. Scaffold in src/pds/repo/mst.gleam: Partition record type (here plus gaps, always exactly one more gap than kept entries), supplied annotate_layers worked example, and a partition_at_layer stub returning Partition([], []) (a value no correct call produces, so the runner detects it). test/pds/mst_partition_exercise.gleam prints Pending until implemented, then asserts annotation, empty input (Partition([], [[]])), gaps on both sides of one kept entry, unsorted-input equivalence, two kept entries with a trailing empty gap, all-below-target, and an empty left gap (app... sorts before com... bytewise). Scaffold verification: gleam check passes with one expected unused-argument warning on the stub; gleam test (218 passed, no failures); gleam format --check and explicit runner pass.
  • Next exact action: learner implements partition_at_layer — sort by key, single pass carrying current-gap/completed-gaps/kept accumulators, close gaps around kept entries, reverse into ascending order with exactly one more gap than kept entries.
  • Learner completed partition_at_layer after two reviewed attempts. First attempt's terminal branch special-cased an empty current gap (dropping the final empty gap, caught by the empty-input assertion expecting Partition([], [[]])) and never reversed kept. Second attempt fixed the terminal branch but still matched True, [] vs True, _ when a kept entry arrived, swallowing the empty left gap — caught by the app...-sorts-first assertion expecting Partition([self_], [[], [a, b]]). Final version collapses both branches into an unconditional close; the lesson recorded: an empty gap is a first-class fact (no subtree on that side), not the absence of a gap, and when an accumulator's empty and non-empty cases want the same operation, there should be no case split. Preserved learner logic; removed obsolete exercise comments; runner is unconditional and mst_partition_test joins default discovery. Final verification: explicit runner, gleam check (no warnings), gleam test (219 passed, no failures), and gleam format --check pass.
  • Next exact action: assemble a complete multi-level tree — build_root over annotated entries, recursing each nonempty gap into a child block at layer − 1 (intermediate empty nodes when a gap has no key at that layer), placing child CIDs into l/t, collecting all blocks, and comparing the root CID against an independently computed value.
  • Started the tree-assembly exercise (first M5.2 build-side step). Verified list.try_map/list.flatten/list.zip/int.max in installed stdlib 1.0.5. Built an independent from-scratch Python tree builder implementing the full spec algorithm (root at max layer, gaps recurse at layer − 1, intermediate empty nodes, prefix compression), sanity-checked against the known empty-tree root; it computes the four-key tree (a/0, ab/2, ba/1, c/0 with alternating CIDs) as root bafyreibyf2t6vzo4xyc7hai5msev25yko5qamzvsyll72dplybbx6beu6y with five blocks in gap order (leaf a, intermediate empty, leaf c, node ba+t, root). Scaffold in src/pds/repo/mst.gleam: temporary TreeBuildPending in NodeError, supplied build_gap worked example (empty gap → no link/blocks; nonempty → mutual recursion at layer − 1), pending stubs for build_at_layer and build_root with exercise steps. test/pds/mst_build_tree_exercise.gleam prints Pending until implemented, then asserts the empty root block, single-entry agreement with node_block, the independent root CID plus the exact five-block CID order, insertion-order independence, and typed duplicate/non-ASCII errors through the recursion. Scaffold verification: gleam check passes with two expected unused-argument warnings on the stubs; gleam test (219 passed, no failures); gleam format --check and explicit runner pass. (Agent caught and fixed its own runner slip: mst.none() → None before verification.)
  • Next exact action: learner implements build_at_layer (partition, list.try_map gaps through build_gap, first gap result → left link, zip the rest with kept entries into node triples, node_block, collect gap blocks plus own block) and build_root (empty → node_block(None, []); otherwise annotate, max layer via list.fold/int.max, build there).
  • Learner asked for clarification on list.zip; agent explained the positional correspondence (gap 0 → l, gap i → kept entry i−1's t) and the zipper pairing, noting a hand-rolled lockstep recursion is equivalent. Learner then wired partition + try_map + zip + triple reshaping correctly. Two iterations on the same type error shape: returning node_block's single-block #(Cid, BitArray) where the whole-subtree #(Cid, List(blocks)) contract was required — first in build_at_layer (fixed by flattening gap blocks and appending the own block), then in build_root's empty branch (wrapping the single empty block in a list). Lesson recorded: node_block speaks one node, the build functions speak the whole subtree; the signatures caught both. Learner's [left, ..rights] match carries an explicit invariant panic for the impossible empty-gaps case. Final code: mutual recursion build_at_layer/build_gap, all steps correct. Removed the temporary TreeBuildPending variant and obsolete exercise comments; runner is unconditional and mst_build_tree_test joins default discovery. Final verification: explicit runner (independent root CID and five-block CID sequence match), gleam check (no warnings), gleam test (220 passed, no failures), and gleam format --check pass.
  • Next exact action: M5.2 reader side — a validating traversal that walks a complete tree from blocks back to a key→record-CID mapping, checking prefix reconstruction, link consistency and block presence; then pure lookup/create/replace/delete through the mapping-to-tree interface, before M5.3 fixture and differential coverage.

Tuple-to-record refactor (detour between M5.2 build and reader) #

  • Learner proposed the detour after noticing tuple proliferation (#(String, Cid, Int) vs #(String, Cid, Option(Cid)) distinguished only by position, and the e.0.0-style coordinate access) and asked about the panic smell. Agreed scope: the real win is making the "one more gap than entries" invariant structural, not cosmetics. New types in src/pds/repo/mst.gleam: RecordEntry(key, value), LayeredEntry(key, value, layer), LinkedEntry(key, value, right), Block(cid, bytes), BuiltTree(root, blocks), and Partition(left_gap, kept: List(KeptEntry)) with KeptEntry(entry, right_gap) — each kept entry carries its right gap, eliminating the zip and the unreachable panic in build_at_layer.
  • Agent did the type declarations and mechanical conversions (leaf builders take LinkedEntry, node_block returns Block, build_root fully converted, build_gap destructures BuiltTree), and updated all seven MST runners to the new constructors with unchanged assertion semantics. The five completed runners pass unchanged-in-substance, including both independently computed node CIDs and the empty-node bytes — the mechanical conversion is proven behavior-preserving.
  • Learner's exercise: rewrite the partition_at_layer walk for the new shape (current gap + completed kept + one pending kept entry awaiting its right gap; a kept entry closes the current gap as the pending entry's right gap or as left_gap) and simplify build_at_layer (no zip, no panic). Pending stubs: partition_at_layer returns the sentinel Partition(left_gap: [], kept: []) (detected by the runner on input [a]@0, whose correct answer differs); build_at_layer returns Error(RefactorPending) (temporary NodeError variant). Scaffold verification: gleam check passes with expected unused-argument warnings on both stubs; gleam test (218 passed, no failures — the two pending runners are out of default discovery until completed); gleam format --check passes.
  • Next exact action: learner implements the new partition_at_layer walk and build_at_layer; then agent removes RefactorPending, makes both runners unconditional, and returns them to default discovery.
  • After a few days away, the learner asked the agent to complete the exercise (delegation at learner request, per the established pattern). Reviewed their partial work en route: the recursive-walk signature needed the remaining input, target_layer, List(KeptEntry) for completed, and a left accumulator; their first terminal branch dropped the final gap and mixed reversal conventions; their two-way pending split was right after fixing a dropped Some(entry). Final code: terminal [] branch closes current as the pending entry's right gap (or as left_gap when nothing was kept, using option.unwrap(left, []) for the structurally-impossible Some/None combination); kept-entry branches unchanged from the learner's corrected version; below-target branch prepends to current. build_at_layer: partition, build_gap the left gap, list.try_map kept entries into LinkedEntry + blocks, node_block, BuiltTree with left blocks, right-gap blocks in entry order, own block last — no zip, no panic. Removed RefactorPending and obsolete exercise comments; both runners unconditional and back in default discovery. Final verification: both explicit runners, gleam check (no warnings), gleam test (220 passed, no failures), and gleam format --check pass. The independent root CID and five-block CID sequence pass unchanged under the structural types, proving the refactor behavior-preserving.
  • Next exact action: M5.2 reader side — a validating traversal that walks a complete tree from blocks back to a RecordEntry mapping, checking prefix reconstruction, link consistency and block presence.
  • Remaining boundary requirements: record/map-key/reserved-field rules, constructed-value encoder budgets, and actual official fixture coverage. Existing numeric, bytes, syntax duplicate-key and parser-budget checks are verified separately. No M4.1 completion or codec interoperability claim. Continue tracking strict NSID-001 independently; do not modify or skip its fixture.

M1.1 checkpoint evidence #

  • Goal: write a pure structural parser accepting exactly two nonempty slash-separated segments.
  • Files involved: src/pds/syntax/repo_path.gleam (types scaffolded), test/pds/repo_path_test.gleam (create after the learner's parser attempt), this log.
  • Learner exercise / attempt: complete. Learner implemented split_path with empty-collection and empty-key patterns before the general two-item pattern, followed by a wildcard returning WrongSegmentCount. It preserves both strings and prioritizes EmptyCollection for /. Agent left the learner's source unchanged.
  • Explanation or correction: a custom type defines data we can construct and match. Result distinguishes a success value (Ok) from a typed failure (Error). case selects a branch by matching a value's shape; a two-item list pattern binds both items and _ handles any unmatched shape.
  • Commands and actual outcomes: verified installed gleam_stdlib 1.0.5's string.split signature and documentation; it returns List(String) and preserves trailing empty segments. Type-only scaffold initially passed compilation; formatting was corrected with the formatter. After reviewing the learner implementation, added ten public _test functions covering both valid examples, missing/extra segments, empty input/segments, /, repeated slashes, and an extra trailing slash. gleam check: passed. gleam test: 10 passed, no failures. gleam format --check: passed. Updated README to describe the actual parser and test suite.
  • Open questions: none for M1.1. a/b is structurally valid; identifier validation is deferred to M1.2.
  • Next exact action: M1.2, recorded in the current session above.

M0.2 checkpoint evidence #

  • Goal: explain where a post is stored and how it reaches a feed; prepare checked protocol and language references.
  • Files involved: this log, docs/protocol/SOURCES.md, docs/protocol/reference/2026-10-04-m0-reading-notes.md; project source remains unchanged.
  • Learner exercise / attempt: complete. The learner answered PDS, AppView, and main? for record storage, feed serving, and the current entry function. All three are correct; confirmed that the entry point is main in the pds module, defined in src/pds.gleam.
  • Explanation or correction: a client submits the post to its PDS; the PDS stores it in the account's signed repository; a relay collects repository updates; an AppView indexes the data for application queries such as feeds. A record is one item, a repository is the account's signed collection, and identity locates the account's host and verification key. Our program currently only prints a greeting.
  • Commands and actual outcomes:
    • Read src/pds.gleam: learner restored main() -> Nil and preserved Hello from pds again!.
    • gleam check: passed after restoration, completing M0.1's remaining check.
    • Checked both reference destinations before cloning; neither existed.
    • git clone --depth 1 https://github.com/bluesky-social/atproto.git .reference/atproto: failed, exit 128, Could not resolve host: github.com.
    • git clone --depth 1 https://github.com/bluesky-social/atproto-interop-tests.git .reference/interop: failed with the same DNS error.
    • Read the official AT Protocol overview and Gleam tour sections using the web tool; checked URLs and source status are in docs/protocol/SOURCES.md. Local reading notes summarize the checked pages.
    • After reviewing the learner's answers, inspected .reference/: still empty; neither checkout exists.
    • Learner reported clone completion; agent verified remote get-url origin, rev-parse HEAD, and status --short for each checkout. Origins match the plan, both working trees are clean, and HEADs are a7c8604d876a200a1e4fa4aec83d4189ac4f1c12 (atproto) and 056e5741bb330757205d6b16db5266fffcae937b (interop).
  • Open questions: none for M0.2.
  • Next exact action: M1.1, recorded in the current session above.

M0.1 checkpoint evidence #

  • Goal: run a minimal Gleam package, then let the learner change its greeting and inspect a type error.
  • Files involved: gleam.toml, src/pds.gleam, test/pds_test.gleam, .gitignore, README.md, this log, docs/DECISIONS.md, docs/ENDPOINTS.md.
  • Learner exercise / attempt: complete. The learner changed the greeting, shared the expected Int/Nil diagnostic, and then reported restoring the valid annotation. The agent confirmed main() -> Nil in the source and verified successful compilation.
  • Explanation or correction: import gleam/io makes the I/O module available; pub fn main() -> Nil declares the public entry function. The last expression is the return value. io.println prints text and returns Nil, so declaring this function's return type as Int causes a type mismatch.
  • Commands and actual outcomes:
    • gleam --version: passed, 1.18.1.
    • erl -noshell -eval 'io:format("OTP ~s / ERTS ~s~n", [erlang:system_info(otp_release), erlang:system_info(version)]), halt().': passed, OTP 29 / ERTS 17.1.
    • rebar3 version: passed, 3.27.1.
    • Initial agent gleam run: failed during dependency resolution, requesting https://repo.hex.pm/packages/gleam_stdlib; no manifest was generated at that time.
    • Initial curl -I --connect-timeout 10 --max-time 15 https://repo.hex.pm/packages/gleam_stdlib: failed with exit 6, Could not resolve host: repo.hex.pm.
    • Initially inspected standard Gleam/Hex cache locations had no cached dependencies.
    • After the learner's terminal run, dependencies and manifest.toml are present. Locked versions: gleam_stdlib 1.0.5 and gleeunit 1.11.0. Direct agent network access has not been rechecked; cached dependencies now suffice for this card.
    • Agent gleam run: passed, printed Hello from pds again!.
    • Agent gleam check: passed.
    • Agent gleam test: passed, reported No tests found!; this verifies the initial runner only, not protocol behavior.
    • gleam format --check: passed.
    • Learner gleam check with main() -> Int: failed as intended, expected Int, found Nil at src/pds.gleam:4:3. This is exercise evidence, not a passing baseline check.
    • After the learner restored Nil, agent gleam check: passed.
  • Open questions: none for M0.1.
  • Next exact action: M0.2, as recorded in the current session above.

Plan progress #

Milestone Status Required outcome
M0 — Setup and architecture Complete: M0.1 and M0.2 Runnable program, learner exercise, pinned references, post's path explained
M1 — Records and identifiers M1.1 complete; M1.2 normative checks pass, strict NSID-001 compatibility remains open Typed parser and protocol-valid identifiers
M2 — HTTP and XRPC Complete locally: M2.1/M2.2/M2.3 Health/XRPC responses and binary WebSocket experiment
M3 — Local storage Complete locally: M3.1/M3.2/M3.3; gate L passed Durable CRUD, pagination, rollback and typed write-owner actor
M4 — Data encoding and CIDs Complete locally: M4.1/M4.2/M4.3 Official encoding fixtures and consistent identifiers
M5 — Merkle Search Tree Not started Deterministic roots and MST fixtures
M6 — Signed repositories Not started Keys, commits, CAR, independent verification
M7 — Identity and access Not started Identity resolution, provisioning, secure sessions
M8 — Writes and media Not started Authorized atomic writes, reads, blobs; gate R with M6
M9 — Synchronization Not started Durable stream, replay, independent reconstruction; gate S
M10 — OAuth and clients Not started OAuth, AppView access, observed client requirements
M11 — Public integration Not started Actual test-client login, posting, media and indexing; gate C
M12 — Recovery and operations Not started Restore, rotation, migration, operational evidence; gate O

Deferred work #

Item Reason Required before which gate?
Public host, hostname, and publishing authority Needed at public integration, not local setup C