From 749ac2ad6bce3597de5f872eaf9cede9bf83b406 Mon Sep 17 00:00:00 2001 From: phernandez Date: Thu, 6 Aug 2026 09:02:14 -0500 Subject: [PATCH] docs(core): define constructive domain modeling Signed-off-by: phernandez --- .agents/skills/pythonic-code/SKILL.md | 59 ++++++++++++++++++++++----- docs/ENGINEERING_STYLE.md | 42 +++++++++++++++++++ 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/.agents/skills/pythonic-code/SKILL.md b/.agents/skills/pythonic-code/SKILL.md index eef36eb4e..9fe547c3f 100644 --- a/.agents/skills/pythonic-code/SKILL.md +++ b/.agents/skills/pythonic-code/SKILL.md @@ -2,9 +2,10 @@ 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 @@ -12,6 +13,10 @@ description: >- 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. @@ -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. @@ -75,13 +104,15 @@ 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. @@ -89,25 +120,33 @@ local. ### 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 diff --git a/docs/ENGINEERING_STYLE.md b/docs/ENGINEERING_STYLE.md index f0b28b188..a8f5c8b84 100644 --- a/docs/ENGINEERING_STYLE.md +++ b/docs/ENGINEERING_STYLE.md @@ -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 @@ -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 @@ -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 @@ -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`,