Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions .agents/skills/pythonic-code/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
name: pythonic-code
description: >-
Write, refactor, and review Python for clarity, explicit behavior, local reasoning, strong
types, and minimal abstraction. Use when creating or changing nontrivial Python, simplifying
object-heavy or helper-heavy code, evaluating whether code is Pythonic, or reviewing Python
maintainability in Basic Memory repositories.
types, constructive domain modeling, and minimal abstraction. Use when creating or changing
nontrivial Python, simplifying object-heavy, helper-heavy, or overly procedural code,
evaluating whether code is Pythonic, or reviewing Python maintainability in Basic Memory
repositories.
---

# Pythonic Code

Write Python that makes domain behavior obvious to human and AI readers. Apply a WWGD lens:
choose the simplest correct design that feels native to Python and is easy to verify.

The preferred design method is **Constructive Domain Modeling**: define the valid values and
outcomes a program can construct, then let their types carry obligations to the code that
consumes them.

## Orient Before Coding

1. Read the repository's `AGENTS.md` or `CLAUDE.md` instructions.
Expand All @@ -33,6 +38,30 @@ Let local project rules override generic style advice.
5. Use Python idioms when they clarify intent rather than merely shorten code.
6. Prove the result with types, tests, and repository tooling.

## Model The Positive Space

Constructive Domain Modeling describes what the program supports instead of starting with a
broad representation and a growing list of invalid combinations.

- Represent one valid state with a product of required fields, usually a frozen dataclass.
- Represent meaningful alternatives with a closed union using a Python 3.12 `type` alias.
- Use Pydantic models and discriminated unions at API, CLI, MCP, configuration, and persistence
boundaries where untrusted values require runtime validation or serialization.
- Parse or classify a broad boundary shape once, then pass the narrower domain value internally.
Do not make every consumer rediscover the invariant through checks and casts.
- Consume a closed union with explicit `match` cases. Use `typing.assert_never` when it proves
exhaustive handling, and avoid catch-all cases that hide a newly added variant.
- Prefer a total function over a partial one. When a case is expected, either narrow the input so
the case is impossible or widen the return union so the caller must handle it.
- Return explicit variants for recoverable domain outcomes when callers can respond differently.
Keep exceptions for broken invariants, cancellation, and unpredictable filesystem, network,
queue, or database failures.
- Choose the simplest model that rules out a real error. Do not add wrapper-only IDs, Result
types around every operation, or maximum-precision unions that cost more than they clarify.

Before narrowing an ORM model or compatibility schema, trace its writers and serialized forms.
Storage may remain broad while a parser constructs a safer domain value for the core workflow.

## Prefer Functions Before Hierarchies

- Start with an ordinary, fully typed function.
Expand Down Expand Up @@ -75,39 +104,49 @@ local.
- Name values after the domain concept they carry.
- Use full annotations and narrow types. Do not hide uncertainty with `Any`, broad casts,
speculative `getattr`, or unstructured dictionaries.
- Use dataclasses for internal values and Pydantic at validation and serialization boundaries.
- Use frozen dataclasses for internal domain values and Pydantic at validation and serialization
boundaries. A Pydantic model is not automatically the best internal state representation.
- Prefer direct iteration, context managers, standard-library building blocks, and simple
comprehensions where their meaning is immediate.
- Distinguish absence from falsiness; use truth-value testing only when empty values share the
intended meaning.
- Keep async work, resource ownership, cancellation, and cleanup visible.
- Fail fast with specific errors. Do not add silent fallbacks or broad exception handling.
- Fail fast with specific errors when an invariant or external operation fails. Do not use
exceptions for ordinary domain branching, or add silent fallbacks and broad exception handling.
- Comment decisions and constraints, not mechanics.
- Optimize measured hot paths; do not trade readability for hypothetical performance.

## Match The Requested Mode

### Write

Establish the contract and domain values first. Implement the direct path, then add only the
abstractions required by real variation, state, or boundaries.
Establish the valid states, outcomes, and boundary parser first. Implement the direct path, make
closed variants exhaustive, then add only the abstractions required by real variation, state, or
boundaries.

### Refactor

Preserve observable behavior, keep the diff focused, and add or update a regression test when
the behavior is risky. Do not mechanically rewrite already-clear code to apply an idiom.
the behavior is risky. Look for status strings coupled to optional fields, repeated validation,
"should never happen" branches, and expected outcomes carried by exceptions. Replace them only
when a smaller constructive model removes a real unsupported state. Do not mechanically rewrite
already-clear code, convert I/O failures to Result types, or reshape persisted data before tracing
its writers.

### Review

Report concrete readability, abstraction, typing, lifecycle, and domain-model risks. Explain the
smallest practical improvement. Do not edit unless the user asks for fixes.
smallest practical improvement. Ask which invalid state or unhandled obligation a proposed type
actually removes; stronger-looking types without a concrete payoff are not an improvement. Do not
edit unless the user asks for fixes.

## Verify The Result

Run the narrowest command that proves the change, then widen according to risk:

1. Focused tests for the changed behavior.
2. Formatter, linter, and type checker configured by the project.
2. Formatter, linter, and type checker configured by the project. Use the type checker to prove
exhaustive consumers where the domain is a closed union.
3. Repository health, package, integration, or full gates when boundaries are affected.

Lead the final response with the outcome and verification. Explain design choices only when they
Expand Down
42 changes: 42 additions & 0 deletions docs/ENGINEERING_STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Style is how we make code easier to verify. Prefer explicit, typed, local-first
Markdown as the canonical product representation while the file materialization, database, API,
and MCP surfaces stay in sync.

Our default design method is **Constructive Domain Modeling**: describe the states and outcomes
the product supports, construct those values at trusted boundaries, and let their types carry
obligations through the program.

## Design Center

- Basic Memory is local-first. In local flows, Markdown files are the durable source and
Expand All @@ -17,6 +21,39 @@ and MCP surfaces stay in sync.
- Prefer small, explicit abstractions that match a real domain boundary. Avoid object
hierarchies when a function, dataclass, type alias, or protocol describes the concept better.

## Constructive Domain Modeling

Constructive Domain Modeling defines a domain by its **positive space**: the values that can be
constructed and handled correctly. It is a practical Python application of product types, sum
types, parsing, and exhaustive handling—not a mandate to eliminate classes or exceptions.

- Model one valid state with a small value whose required fields are always present. Model
meaningful alternatives as a closed union of values, rather than one record with a status
string and mutually conditional optional fields.
- Prefer frozen dataclasses for internal domain values and Python 3.12 `type` aliases for closed
unions. At JSON-facing boundaries, use Pydantic discriminated unions when runtime validation,
serialization, or generated schemas need to preserve the alternatives.
- Parse and classify external data once at the boundary. After construction, internal functions
should accept the domain value instead of repeatedly validating the same invariant.
- Consume closed unions with an explicit `match`. Use `typing.assert_never` when it lets the type
checker prove that every variant is handled; avoid a catch-all branch that silently absorbs a
future domain case.
- Make functions total over their declared input when practical. If a recoverable case is part of
normal operation, strengthen the input type or include that case in a structured return type
instead of raising a "should never happen" exception deep in the workflow.
- Return explicit variants for expected domain outcomes when callers can make a meaningful
decision about them. Keep exceptions for broken invariants, cancellation, and unpredictable
resource failures such as filesystem, network, queue, or database errors.
- Choose the least precise model that removes a real unsupported state. Do not introduce wrapper
types, Result types, or elaborate unions merely to make the type graph look stronger.
- Before reshaping persisted state, trace every writer and compatibility constraint. ORM rows and
old payloads may need a parser that converts their broad storage shape into a narrower domain
value.

For example, prefer separate `Completed(result)` and `Failed(reason)` values joined by a
`type OperationOutcome = Completed | Failed` alias over a single `Operation` record where
`result` and `reason` are both optional and their validity depends on a status string.

## Functions Before Hierarchies

- Start with an ordinary, fully typed function. Pair functions with a dataclass when related
Expand Down Expand Up @@ -52,6 +89,8 @@ and MCP surfaces stay in sync.

- Fail fast when an invariant is broken. Do not swallow exceptions, add warning-only error
handling, or introduce fallback behavior unless the user explicitly agrees to that behavior.
- Do not use exceptions as ordinary branching for expected domain outcomes. Translate a typed
outcome to HTTP, CLI, or MCP errors at the outer adapter that owns that presentation contract.
- Keep control flow simple and close to the domain decision. Push `if` statements up into the
function that owns orchestration; keep leaf helpers focused on computation or one side effect.
- Make async/resource boundaries visible with context managers and explicit lifecycles. Do not
Expand Down Expand Up @@ -87,6 +126,9 @@ and MCP surfaces stay in sync.
relevant doc/repo hygiene checks.
- Prefer tests that exercise real code paths. Use mocks, doubles, or `monkeypatch` only when
the external boundary would be slow, nondeterministic, or impossible to trigger directly.
- Test every meaningful domain variant and the boundary that constructs it. Let the type checker
enforce exhaustive consumers; use runtime tests for behavior and compatibility, not to
compensate for an unnecessarily broad internal state model.
- Keep coverage at 100% for new code. Use `# pragma: no cover` only for code that would require
disproportionate mocking and is covered through an integration or runtime path.
- Start with targeted commands, then widen as risk grows: focused pytest, `just fast-check`,
Expand Down
Loading