okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petfix tools failing to mention capabilities they had48a5b3f

main
12.9 KiB234 linesraw

name: gleam-design description: Design, implement, refactor, and review idiomatic Gleam code using source-backed patterns for modules, custom and opaque types, explicit dependencies, state ownership, and concurrency. Use for .gleam projects, Gleam architecture reviews, OTP actors and supervision, ETS or Cell sharing, database handles, Lustre state, event-driven services, or deciding between message passing and ordinary state/value passing. Distinguish Erlang and JavaScript targets and avoid unnecessary actors, unsafe sharing, and generic abstraction sprawl.

Gleam Design

Prefer ordinary values and functions. Introduce processes for a real concurrency, ordering, resource-lifetime, or failure boundary. Introduce shared mutable storage for a demonstrated access pattern with an explicit consistency contract. Do not treat "Gleam runs on the BEAM" as a reason to actorify every module.

1. Inspect before designing

Load only the supporting references relevant to the task, not the entire corpus.

Read the project's gleam.toml, manifest.toml, entrypoint, relevant modules, FFI, and tests. Establish the compiler/library versions, supported targets, and existing public contracts. Follow coherent local conventions rather than imposing a new architecture because a reference repository uses it.

Trace one representative path end to end: input decoding, domain operation, state access, effects, response, and recovery. Read the actual dependency API before using version-sensitive OTP, selector, database, or framework functions. Consult source/documentation when local dependency code does not answer a material question. Do not guess APIs from old examples.

For a small pure helper, implement and test the helper; do not produce an application architecture document. For stateful changes, identify:

  • The authoritative state and any derived snapshots or caches.
  • The owner, readers, writers, and lifetime of each state/resource.
  • The invariant, atomic operation, readiness condition, and failure behavior.

Use a compact ownership ledger when several resources interact:

resource | authority/owner | readers/writers | consistency | restart/close

2. Choose the smallest mechanism

NeedDefault choiceCheck before escalating
Local transformation or immutable configFunction arguments and returned valuesIs any independent lifecycle needed?
Evolving data used by one execution flowA record/Dict threaded through functionsIs there actually a second writer?
A safely shareable dependencyExplicit handle or captured contextDoes this particular handle permit concurrent use?
Ordered decisions or asynchronous lifecycleOne actor owning the relevant stateWhat invariant or resource does it own?
Many readers of an occasionally replaced valuePublished snapshot, possibly ETS-backedPublication atomicity, writer discipline, readiness, lifetime
Concurrent independent updatesDocumented atomic store operationDoes the guarantee cover the whole operation?
Durable or multi-record invariantsSuitable database transaction/authorityWhich connection and isolation boundary apply?
Very rarely changed VM-wide dataConsider persistent_term only deliberatelyAre its replacement and GC costs acceptable?

Distinguish sharing a value, sharing a resource handle, and sharing mutable storage. A record containing a cell or socket does not make the referenced resource immutable. A typed handle does not prove liveness, thread-safety, uniqueness, or correct lifetime.

For a tree or graph owned by one process, start with ordinary recursive values or Dict(Id, Node). Do not create an actor or a cell for every node or field. Local value passing is not the same cost model as sending the whole structure between processes.

Read state and concurrency before adding shared access, changing ownership, or relying on restart behavior.

3. Put types at the right boundaries

Use variants for meaningful alternatives, with the data needed by each variant. Prefer Idle | Running(run_id) | Stopping(run_id) to correlated booleans and optional fields. Keep independent dimensions independent; do not build a giant sum type merely to avoid every optional field.

Use public records when callers should assemble data. Use opaque types when a constructor must enforce an invariant, callers must not depend on representation, or a handle should expose only selected operations. State the invariant rather than adding wrappers by habit. A type alias is not a distinct nominal type.

Preserve relationships through type parameters: request/body, query/decoded row, runtime/message, or snapshot/status. Use phantom states only when they eliminate real invalid combinations; they do not enforce resource ownership by themselves.

Decode JSON, database rows, and foreign-runtime values at explicit boundaries. Keep Dynamic, unsafe casts, and wire-specific shapes out of ordinary domain logic. Preserve one canonical native model; do not invent JSON mirrors just to move typed data between modules or local processes.

Keep expected failures in Result with useful error variants. Reserve assertions for programmer invariants or intentionally fatal startup conditions. Distinguish bad input, dependency failure, transport failure, and corrupted internal state.

Read types and modules for examples from HTTP, Pog, Squirrel, Birdie, and the standard library.

4. Separate module boundaries from process boundaries

Keep related types and operations in the module that owns their meaning. Extract an internal module, adapter, or protocol module when there is a real boundary. Avoid a catch-all types.gleam, duplicate model definitions, and one-module-per- function fragmentation. Do not impose arbitrary file-size limits.

Compose dependencies at startup or the request boundary. Capture an immutable context in a handler closure when appropriate; give lower-level functions the narrow values or handles they actually need. Do not add a dependency-container actor just to avoid passing a record.

For a complex event-driven service, consider separating a pure transition from its executor. Keep business transitions, protocol decoding, worker supervision, and persistence distinguishable. Do not require an effect framework for a simple handler that can directly call a few functions.

Use pipelines for transformations, case for meaningful alternatives, and use when callback composition improves readability. Inspect the called function: use is callback syntax, not automatic async execution or finally. A function-valued field can still do IO.

5. Design the process protocol, not just its messages

Keep private runtime state separate from the handle or startup data returned to callers. Hide message constructors behind ordinary functions unless callers legitimately need the protocol for integration.

Send operations that protect whole invariants. Prefer Reserve(amount, reply) to a client-side Get followed by Set when interleaving would be incorrect. Use plain snapshot reads where snapshot semantics are sufficient.

Separate application events from transport/lifecycle events. Use explicit variants for user input, scheduled wakeups, worker results, stream events, monitor notifications, and shutdown. Do not forge user messages for internal wakes merely because an external API has a narrower message format.

Keep the owner responsive to the events it must service. Offload long jobs when necessary, with bounded admission and appropriate supervision/monitoring. Return compact work results and correlate them with run/request identity. An Effect value does not itself mean work runs in another process.

Treat a reply containing Result separately from a fallible call transport. In the inspected gleam_erlang API, process.call can panic rather than return a timeout error. Verify the installed version and choose its supported failure boundary; do not invent a try_call function.

Define what an acknowledgment means: accepted, applied, published, or durable. A timeout does not prove work stopped. Cancellation needs a real protocol, and late results need identity/generation checks. Include an owner-incarnation token when identity could be reused after restart. Add idempotency or reconciliation where retrying external effects could duplicate them.

Define overload behavior at the ingress, not only inside the actor handler. Rejecting Busy after dequeue does not bound an already growing mailbox. Specify admission limits, batching, coalescing, or demand as appropriate.

6. Share state without inventing guarantees

For a writer-actor/direct-reader hybrid, identify the authoritative state, publish a complete snapshot in one appropriate operation, and let each logical reader capture it once. Decide whether a successful write acknowledgment follows publication. Define initialization, stale-read, and unavailable-owner behavior.

Do not assume a writer actor makes several ETS writes atomic to direct readers. Do not implement concurrent read-modify-write as separate cell.read and cell.write operations. Use a suitable atomic operation, an explicit retry protocol, or owner serialization covering the full invariant.

Distinguish protected ETS from Cell's inspected public ETS implementation. Single-writer discipline in an application is not a guarantee enforced by Cell. Do not claim a snapshot is consistent when it contains independently mutable handles, or when a logical read spans unrelated database/store operations.

Distinguish pool handles from checked-out transaction connections. Keep a transaction's work on the supplied connection. Do not leak it beyond its scope or assume queries through the original pool join the transaction. A failed row decoder does not imply the SQL write did not happen.

Create resources in their intended owner. Passing an ETS ID to an actor does not transfer table ownership. A parent-owned table may be correct when the parent's lifetime is intentional; an actor-owned table belongs in its initializer or a deliberate ownership-transfer protocol.

Design recovery for handles as well as processes. A stale PID subject or ETS ID does not become valid when a child restarts. Choose refresh/lookup, grouped restarts, ownership transfer, or another explicit recovery boundary. Choose supervision strategy from dependencies, not habit.

Allocate a finite set of process names at application composition time, not per request or inside repeatedly restarted initialization. Avoid unbounded atom creation. Use direct subjects or a suitable dynamic registry where required.

Keep these BEAM-specific rules out of JavaScript-only designs. Encapsulated local mutation in a foreign implementation can preserve an immutable API; it does not justify exposing shared mutable objects to arbitrary callers.

7. Verify the behavior that matters

Run the project's supported equivalents of:

gleam format --check src test
gleam check
gleam test

Adjust paths to the project; do not create nonexistent test directories merely to fit this command. Run each supported target when relevant. Check the local CLI for version-specific options. Report missing tools or unrun checks honestly.

Test pure transitions and boundary decoders without infrastructure first. Then exercise the applicable concurrency cases: initialization failure, owner death, which children restart, stale handles, duplicate/late results, cancellation that has not completed, timeout after an effect may have occurred, concurrent writers, snapshot readiness, and overload. Use acknowledgments or monitors to control ordering rather than treating a sleep as proof of synchronization.

Review FFI contracts and cleanup explicitly. Supervision, a typed signature, a callback, and passing unit tests are not interchangeable guarantees.

Use review and test cases. For long-lived session/event loops, read the worked example and its original core and test cases. The bundled Gleam files were source-reviewed but not compiler- or runtime-verified in this study.

8. Deliver a small, justified change

State the chosen ownership/consistency boundary, the important alternative not chosen, and the tests actually run. For a review, prioritize correctness, lifetime, and observable failure semantics before cosmetic style. Do not rewrite an entire architecture or add a cache, actor, registry, or persistence layer without a requirement that justifies it.

Consult repository studies for the 16-repo comparison and counterexamples. Use sources or the machine-readable ledger for exact revisions and reading scope. Treat upstream code as evidence, not infallible authority; check local versions before borrowing an API or a behavior.

1---
2name: gleam-design
3description: Design, implement, refactor, and review idiomatic Gleam code using source-backed patterns for modules, custom and opaque types, explicit dependencies, state ownership, and concurrency. Use for .gleam projects, Gleam architecture reviews, OTP actors and supervision, ETS or Cell sharing, database handles, Lustre state, event-driven services, or deciding between message passing and ordinary state/value passing. Distinguish Erlang and JavaScript targets and avoid unnecessary actors, unsafe sharing, and generic abstraction sprawl.
4---
5
6# Gleam Design
7
8Prefer ordinary values and functions. Introduce processes for a real concurrency,
9ordering, resource-lifetime, or failure boundary. Introduce shared mutable storage
10for a demonstrated access pattern with an explicit consistency contract.
11Do not treat "Gleam runs on the BEAM" as a reason to actorify every module.
12
13## 1. Inspect before designing
14
15Load only the supporting references relevant to the task, not the entire corpus.
16
17Read the project's `gleam.toml`, `manifest.toml`, entrypoint, relevant modules,
18FFI, and tests. Establish the compiler/library versions, supported targets, and
19existing public contracts. Follow coherent local conventions rather than imposing
20a new architecture because a reference repository uses it.
21
22Trace one representative path end to end: input decoding, domain operation,
23state access, effects, response, and recovery. Read the actual dependency API
24before using version-sensitive OTP, selector, database, or framework functions.
25Consult source/documentation when local dependency code does not answer a
26material question. Do not guess APIs from old examples.
27
28For a small pure helper, implement and test the helper; do not produce an
29application architecture document. For stateful changes, identify:
30
31- The authoritative state and any derived snapshots or caches.
32- The owner, readers, writers, and lifetime of each state/resource.
33- The invariant, atomic operation, readiness condition, and failure behavior.
34
35Use a compact ownership ledger when several resources interact:
36
37```text
38resource | authority/owner | readers/writers | consistency | restart/close
39```
40
41## 2. Choose the smallest mechanism
42
43| Need | Default choice | Check before escalating |
44| --- | --- | --- |
45| Local transformation or immutable config | Function arguments and returned values | Is any independent lifecycle needed? |
46| Evolving data used by one execution flow | A record/Dict threaded through functions | Is there actually a second writer? |
47| A safely shareable dependency | Explicit handle or captured context | Does this particular handle permit concurrent use? |
48| Ordered decisions or asynchronous lifecycle | One actor owning the relevant state | What invariant or resource does it own? |
49| Many readers of an occasionally replaced value | Published snapshot, possibly ETS-backed | Publication atomicity, writer discipline, readiness, lifetime |
50| Concurrent independent updates | Documented atomic store operation | Does the guarantee cover the whole operation? |
51| Durable or multi-record invariants | Suitable database transaction/authority | Which connection and isolation boundary apply? |
52| Very rarely changed VM-wide data | Consider `persistent_term` only deliberately | Are its replacement and GC costs acceptable? |
53
54Distinguish **sharing a value**, **sharing a resource handle**, and **sharing
55mutable storage**. A record containing a cell or socket does not make the
56referenced resource immutable. A typed handle does not prove liveness,
57thread-safety, uniqueness, or correct lifetime.
58
59For a tree or graph owned by one process, start with ordinary recursive values
60or `Dict(Id, Node)`. Do not create an actor or a cell for every node or field.
61Local value passing is not the same cost model as sending the whole structure
62between processes.
63
64Read [state and concurrency](references/state-and-concurrency.md) before adding
65shared access, changing ownership, or relying on restart behavior.
66
67## 3. Put types at the right boundaries
68
69Use variants for meaningful alternatives, with the data needed by each variant.
70Prefer `Idle | Running(run_id) | Stopping(run_id)` to correlated booleans and
71optional fields. Keep independent dimensions independent; do not build a giant
72sum type merely to avoid every optional field.
73
74Use public records when callers should assemble data. Use opaque types when a
75constructor must enforce an invariant, callers must not depend on representation,
76or a handle should expose only selected operations. State the invariant rather
77than adding wrappers by habit. A type alias is not a distinct nominal type.
78
79Preserve relationships through type parameters: request/body, query/decoded row,
80runtime/message, or snapshot/status. Use phantom states only when they eliminate
81real invalid combinations; they do not enforce resource ownership by themselves.
82
83Decode JSON, database rows, and foreign-runtime values at explicit boundaries.
84Keep `Dynamic`, unsafe casts, and wire-specific shapes out of ordinary domain
85logic. Preserve one canonical native model; do not invent JSON mirrors just to
86move typed data between modules or local processes.
87
88Keep expected failures in `Result` with useful error variants. Reserve assertions
89for programmer invariants or intentionally fatal startup conditions. Distinguish
90bad input, dependency failure, transport failure, and corrupted internal state.
91
92Read [types and modules](references/types-and-modules.md) for examples from HTTP,
93Pog, Squirrel, Birdie, and the standard library.
94
95## 4. Separate module boundaries from process boundaries
96
97Keep related types and operations in the module that owns their meaning. Extract
98an `internal` module, adapter, or protocol module when there is a real boundary.
99Avoid a catch-all `types.gleam`, duplicate model definitions, and one-module-per-
100function fragmentation. Do not impose arbitrary file-size limits.
101
102Compose dependencies at startup or the request boundary. Capture an immutable
103context in a handler closure when appropriate; give lower-level functions the
104narrow values or handles they actually need. Do not add a dependency-container
105actor just to avoid passing a record.
106
107For a complex event-driven service, consider separating a pure transition from
108its executor. Keep business transitions, protocol decoding, worker supervision,
109and persistence distinguishable. Do not require an effect framework for a simple
110handler that can directly call a few functions.
111
112Use pipelines for transformations, `case` for meaningful alternatives, and
113`use` when callback composition improves readability. Inspect the called
114function: `use` is callback syntax, not automatic async execution or `finally`.
115A function-valued field can still do IO.
116
117## 5. Design the process protocol, not just its messages
118
119Keep private runtime state separate from the handle or startup data returned to
120callers. Hide message constructors behind ordinary functions unless callers
121legitimately need the protocol for integration.
122
123Send operations that protect whole invariants. Prefer `Reserve(amount, reply)`
124to a client-side `Get` followed by `Set` when interleaving would be incorrect.
125Use plain snapshot reads where snapshot semantics are sufficient.
126
127Separate application events from transport/lifecycle events. Use explicit
128variants for user input, scheduled wakeups, worker results, stream events,
129monitor notifications, and shutdown. Do not forge user messages for internal
130wakes merely because an external API has a narrower message format.
131
132Keep the owner responsive to the events it must service. Offload long jobs when
133necessary, with bounded admission and appropriate supervision/monitoring.
134Return compact work results and correlate them with run/request identity.
135An `Effect` value does not itself mean work runs in another process.
136
137Treat a reply containing `Result` separately from a fallible call transport. In
138the inspected `gleam_erlang` API, `process.call` can panic rather than return a
139timeout error. Verify the installed version and choose its supported failure
140boundary; do not invent a `try_call` function.
141
142Define what an acknowledgment means: accepted, applied, published, or durable.
143A timeout does not prove work stopped. Cancellation needs a real protocol, and
144late results need identity/generation checks. Include an owner-incarnation token
145when identity could be reused after restart. Add idempotency or reconciliation
146where retrying external effects could duplicate them.
147
148Define overload behavior at the ingress, not only inside the actor handler.
149Rejecting `Busy` after dequeue does not bound an already growing mailbox.
150Specify admission limits, batching, coalescing, or demand as appropriate.
151
152## 6. Share state without inventing guarantees
153
154For a writer-actor/direct-reader hybrid, identify the authoritative state,
155publish a complete snapshot in one appropriate operation, and let each logical
156reader capture it once. Decide whether a successful write acknowledgment follows
157publication. Define initialization, stale-read, and unavailable-owner behavior.
158
159Do not assume a writer actor makes several ETS writes atomic to direct readers.
160Do not implement concurrent read-modify-write as separate `cell.read` and
161`cell.write` operations. Use a suitable atomic operation, an explicit retry
162protocol, or owner serialization covering the full invariant.
163
164Distinguish protected ETS from Cell's inspected **public** ETS implementation.
165Single-writer discipline in an application is not a guarantee enforced by Cell.
166Do not claim a snapshot is consistent when it contains independently mutable
167handles, or when a logical read spans unrelated database/store operations.
168
169Distinguish pool handles from checked-out transaction connections. Keep a
170transaction's work on the supplied connection. Do not leak it beyond its scope
171or assume queries through the original pool join the transaction. A failed row
172decoder does not imply the SQL write did not happen.
173
174Create resources in their intended owner. Passing an ETS ID to an actor does
175not transfer table ownership. A parent-owned table may be correct when the
176parent's lifetime is intentional; an actor-owned table belongs in its initializer
177or a deliberate ownership-transfer protocol.
178
179Design recovery for **handles as well as processes**. A stale PID subject or ETS
180ID does not become valid when a child restarts. Choose refresh/lookup, grouped
181restarts, ownership transfer, or another explicit recovery boundary. Choose
182supervision strategy from dependencies, not habit.
183
184Allocate a finite set of process names at application composition time, not per
185request or inside repeatedly restarted initialization. Avoid unbounded atom
186creation. Use direct subjects or a suitable dynamic registry where required.
187
188Keep these BEAM-specific rules out of JavaScript-only designs. Encapsulated local
189mutation in a foreign implementation can preserve an immutable API; it does not
190justify exposing shared mutable objects to arbitrary callers.
191
192## 7. Verify the behavior that matters
193
194Run the project's supported equivalents of:
195
196```sh
197gleam format --check src test
198gleam check
199gleam test
200```
201
202Adjust paths to the project; do not create nonexistent test directories merely
203to fit this command. Run each supported target when relevant. Check the local
204CLI for version-specific options. Report missing tools or unrun checks honestly.
205
206Test pure transitions and boundary decoders without infrastructure first. Then
207exercise the applicable concurrency cases: initialization failure, owner death,
208which children restart, stale handles, duplicate/late results, cancellation that
209has not completed, timeout after an effect may have occurred, concurrent writers,
210snapshot readiness, and overload. Use acknowledgments or monitors to control
211ordering rather than treating a sleep as proof of synchronization.
212
213Review FFI contracts and cleanup explicitly. Supervision, a typed signature, a
214callback, and passing unit tests are not interchangeable guarantees.
215
216Use [review and test cases](references/review-and-tests.md). For long-lived
217session/event loops, read [the worked example](references/session-example.md)
218and its original [core](assets/examples/session_core.gleam) and
219[test cases](assets/examples/session_core_test.gleam). The bundled Gleam files
220were source-reviewed but **not compiler- or runtime-verified** in this study.
221
222## 8. Deliver a small, justified change
223
224State the chosen ownership/consistency boundary, the important alternative not
225chosen, and the tests actually run. For a review, prioritize correctness,
226lifetime, and observable failure semantics before cosmetic style. Do not rewrite
227an entire architecture or add a cache, actor, registry, or persistence layer
228without a requirement that justifies it.
229
230Consult [repository studies](references/repository-studies.md) for the 16-repo
231comparison and counterexamples. Use [sources](references/sources.md) or the
232[machine-readable ledger](references/source-ledger.json) for exact revisions and
233reading scope. Treat upstream code as evidence, not infallible authority; check
234local versions before borrowing an API or a behavior.