[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7193EJJ4AX370MBF708143E":3,"skill-same:01M7193EJJ4AX370MBF708143E":70},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":48,"commitSha":49,"skillDirectory":50,"forksCount":14,"repositoryHtmlUrl":51,"repositoryHomepageUrl":6,"ownerHtmlUrl":52,"ownerBlogUrl":53,"ownerDescription":54,"updatedAt":55,"install":56,"resources":69},"01M7193EJJ4AX370MBF708143E","glimpse",null,"GLIMPSE architecture rules for Python projects. Load before creating, moving, or reviewing anything under pacts, specs, mills, links, gates, or inits: services, repositories, DTOs, protocols, entry points, DI wiring. Covers layer import rules, file layout and slicing (noun, verb, page, port, adapter, kind), the __init__.py re-export policy, growth thresholds, required patterns, and drift red flags. Also use when deciding where new code goes or which test type a layer needs.","fancysnake\u002Fglimpse-architecture","fancysnake","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F219587?v=4","01M7193D8JHQB1H3B0CW2VPFN3","Radosław Ganczarek","01M7193D9A9WP0KRFDCN9F8ZKZ",0,"MIT","https:\u002F\u002Fgithub.com\u002Ffancysnake\u002Fglimpse-architecture\u002Fblob\u002Fmain\u002Fskills\u002Fglimpse\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[5,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"policy","repositories","Rules","load","use","adapter","new","di","init","python","re","page","for","protocols","thresholds","import","which","links","projects","2026-11-10 10:12:30",false,"---\nname: glimpse\ndescription: >-\n  GLIMPSE architecture rules for Python projects. Load before creating,\n  moving, or reviewing anything under pacts, specs, mills, links, gates, or\n  inits: services, repositories, DTOs, protocols, entry points, DI wiring.\n  Covers layer import rules, file layout and slicing (noun, verb, page, port,\n  adapter, kind), the __init__.py re-export policy, growth thresholds,\n  required patterns, and drift red flags. Also use when deciding where new\n  code goes or which test type a layer needs.\n---\n\n\u003C!-- Generated from SKILL.src.md — edit that, then run: mise run skill -->\n\n# GLIMPSE Architecture Reference\n\nThis file is the compressed form of the full reference at\n\u003Chttps:\u002F\u002Fglimpse.fancysnake.dev\u002F>, which is authoritative where the two\ndisagree and carries the worked examples and the reasoning behind each rule.\n\n## Layers\n\n```text\npacts   Protocols, DTOs, errors, enums, TypedDicts. Depends on nothing.\nspecs   Business invariants (pure constants, no IO). Only for mills; see below.\nmills   Business logic, services. Depends on pacts + specs. No side-effect imports.\nlinks   Repositories, external clients. pacts + ORM \u002F driver \u002F SDK.\ngates   Entry points: request handlers, forms, routing, CLI commands. pacts only.\ninits   DI container, middleware. The only layer where gates, mills, links meet.\nedges   Settings, wsgi, manage.py. Outside GLIMPSE; empty on a CLI, never absent.\n```\n\nAll seven exist from the first commit — `edges\u002F` as an empty package where the\nproject has nothing for it. The import contracts are taken as a set on day one,\nand a contract can only name a module that exists.\n\nImport rules enforced by `importlinter` (`pyproject.toml` →\n`[tool.importlinter]`). No exceptions without explicit approval.\n\n**What is fixed, and what is guidance.** Absolute: which layer code belongs to,\nand which layer may import which. Everything below — slicing axes, thresholds,\nfile names, patterns — is recorded practice. Follow it by default; treat an\narrangement it does not describe as legitimate when it follows a real need and\nbreaks no import rule. **Absence of a rule is not a violation**, so do not\nflag code for failing to match a shape this reference never required.\n\n**Composition is per-port.** `inits` composes the object graph everywhere; how\nit reaches gates differs. Web: the framework dispatches to gates, so `inits`\nnever imports them — middleware builds `Services()` per request and attaches\nit to the request (the framework's thread-safe seam). CLI: nothing dispatches,\nso `inits` imports the gate classes, injects mills into their constructors,\nand is named by dotted string in `pyproject.toml`\n(`[project.scripts]` → `myproject.inits.cli:run`).\n\n**edges is two-way isolated.** Nothing imports `edges`; `edges` imports\nnothing first-party — it names project code only by dotted string\n(`DJANGO_SETTINGS_MODULE`, `MIDDLEWARE`, `ROOT_URLCONF`). The root middleware\nimports services, so it lives in `inits`, not `edges`.\n\n**specs or pacts — what it is, not where it is used.** A constant more than one\nlayer must enforce is a fact about the shape of the data (a max length lives in\nthe column, the form, and the rule) — that is a contract, so it goes to `pacts`\nbeside the DTO it constrains. A constant only the rule can observe (seat limit,\ngrace period) is a business invariant and goes to `specs`, which mills alone may\nimport: a threshold reaching `links` ends up encoded in the schema. Tell: change\na `specs` value and only `mills` changes; change a `pacts` value and every\nenforcer changes together.\n\n**Configuration enters at `inits`.** `inits` reads the environment and passes\neach value to the leaf that needs it; `mills` never reads settings at all. On a\nframework with a settings singleton, `links` and `gates` read it through the\nframework's accessor (`django.conf.settings`) — an import of the framework, not\nof `edges`. Without such a framework there is no settings layer at all, and\n`edges\u002F` stays empty. A leaf may still probe its own environment when the\nprobe is injectable for tests; what the environment *decides*, `inits`\ndecides. User\ninput read at runtime — a config file, a command flag — is not configuration:\nit comes in through a port, shaped in `pacts`, validated in a mill.\n\n**\"Framework-free\" means no side effects, not package names.** Forbidden in\n`mills`: imports that do IO, touch global state, or own control flow (ORM,\nHTTP machinery, settings access). Pure computation is fine wherever it comes\nfrom — `django.utils.text.slugify` qualifies. Enforcement level (ban package \u002F\nreview-guarded \u002F ban effectful subtrees) is a per-project choice.\n\nThe rule guards against destructive operations and output the program depends\non, not everything ambient: the clock, a random draw, a UUID, a log line are\nall fine in a mill, and all mockable. Logging reads better at the edge anyway\n(mills raise, gates catch and log) — a preference, not a rule. If a generated\nvalue is *input* to what the service does rather than something it produces on\nthe way, take it as an argument; when the making is somebody else's job, that\nis a link, and a link earns its place even as a one-line wrapper because it\nnames an external capability.\n\n**No DDD tactical patterns.** GLIMPSE has no aggregates or value objects —\ndata moves as DTOs and write TypedDicts; invariants live in service code. The\nslicing axes are GLIMPSE's own (see **Slicing vocabulary**).\n\n## File layout\n\n**`pacts`, `specs`, `mills`, `inits` start as single modules; `links` and\n`gates` are packages from day one.** The first three are sliced by noun, and on\nday one you have one `mills.py` — don't plan further ahead. `inits` stays thin\nwhatever the project does, so split it however is convenient. `links` and\n`gates` take the port axis immediately: the port is knowable before a line is\nwritten — you know you are building a CLI, you know you are talking to a\ndatabase — and skipping it means renaming every import the day a second adapter\nappears.\n\n```text\n# Day one — axis-free layers stay flat\npacts.py\nspecs.py\nmills.py\ninits.py\nlinks\u002F{port}\u002F{adapter}.py                   # e.g. links\u002Fdb\u002Fsqlite.py\ngates\u002F{port}\u002F{adapter}.py                   # e.g. gates\u002Fcli\u002Fargparse.py\nedges\u002F__init__.py                           # empty until a framework fills it\n\n# Grown\npacts\u002F{noun}.py                             # or pacts\u002F{noun}\u002F{verb}.py\npacts\u002F{port}.py                             # port machinery (e.g. pacts\u002Fdb.py)\npacts\u002Fservices.py                           # ServicesProtocol + service protocols\nspecs\u002F{noun}.py\nmills\u002F{noun}.py                             # or mills\u002F{noun}\u002F{verb}.py\ninits\u002Frepositories.py                       # a module per registry, plus one that binds\ninits\u002Fservices.py                           # (+ middleware.py on web, cli.py on a CLI)\nlinks\u002F{port}\u002F{adapter}\u002F{kind}.py            # while small (models.py, repositories.py)\nlinks\u002F{port}\u002F{adapter}\u002F{kind}\u002F{module}.py   # when {kind} crosses threshold\nlinks\u002F{port}\u002F{adapter}\u002F__init__.py          # facade — re-exports the public surface\ngates\u002F{port}\u002F{adapter}\u002F{page}.py            # or ...\u002F{page_group}\u002F{page}.py,\ngates\u002F{port}\u002F{adapter}\u002F{page}\u002F{subpage}.py  #    or ...\u002F{page}\u002F{subpage}.py\n```\n\nThresholds for every promotion above: **Growing rules**.\n\n**`__init__.py` re-export policy.** Default: keep `__init__.py` empty and import\neach symbol from the module that defines it (`from pkg.foo.bar import Bar`, not\n`from pkg.foo import Bar`). A facade `__init__.py` that re-exports a public\nsurface is allowed only for: a framework or public-API package whose inner\nlayout is implementation detail (the `links` adapter facade), relief from\nline-length pressure, or a pre-existing legacy facade. It is not the default.\n\n## Slicing vocabulary\n\n**Port**\n: A delivery mechanism, named after the domain concept rather than the\n  technology.\n: Examples: `cli`, `web`, `db`, `payment_api`, `email`\n: The first axis for `links` and `gates`, known before any code exists.\n\n**Adapter**\n: The specific technology implementing a port.\n: Examples: `postgres`, `sqlite`, `argparse`, `stripe`, `sendgrid`\n: One port can have multiple adapters — usually **coexisting**, not\n  interchangeable. `payment_api\u002Fstripe` and `payment_api\u002Fpaypal` are both wired\n  and both live; which one handles a given payment is a business decision made\n  in a mill, through the protocols it holds. Genuine substitution\n  (`db\u002Fpostgres` vs `db\u002Fsqlite`) is the rarer case: one adapter wired per\n  deployment, chosen in `inits`.\n: One technology can serve multiple ports. A full-stack framework shipping both\n  an ORM and a request layer appears as `db\u002F{framework}` and `web\u002F{framework}` —\n  two separate adapters that share nothing but a name.\n\n**Kind**\n: The type of thing a `links` module holds, below the adapter.\n: Examples: `models` and `repositories` for a `db` adapter; `transport`,\n  `types`, `signer` for an API client.\n: Kinds are per-adapter. The `db` shape is not a universal template.\n\n**Noun**\n: A fat data cow — the model cluster everything else hangs off.\n: Examples: `invoices`, `customers`, `proposals`, `users`\n: The primary slicing axis for `pacts`, `mills`, and `specs`.\n: Which nouns exist depends on what the system is *for*, not on the words in\n  the domain. An invoice tracker slices by `invoices`; a subscription service\n  slices by `billing`, and an invoice is a detail inside it.\n: Plurality is not prescribed; it follows the noun. `events` are many, a\n  `panel` is one.\n: Nouns are GLIMPSE's own axis, not a DDD import. If another axis fits a\n  project better, slice by that instead; the layer rules don't change.\n\n**Verb**\n: An activity cut inside a noun.\n: Examples: `issue`, `refund`, `enroll`, `schedule`\n: A verb module holds the records and logic of actions, not first-class data.\n  Verbs nest inside nouns: `invoices` might cut into `issue` and `refund`.\n: No catch-all verbs. `manage`, `organize`, and `misc` name no activity — if\n  you cannot name a real one, the file is not too big yet.\n\n**Page**\n: What the user touches — the slicing axis for `gates`, below port and adapter.\n: Gates take whatever grouping the interface already has: a command or command\n  group for a CLI, a page group and page — or a page and its subpages — for the\n  web, a view for a TUI, a tool for MCP.\n: Gates mirror the shape of the interface; mills mirror the domain. The two\n  need not line up, and forcing them to is how a sitemap ends up in `mills`.\n\n**Entity**\n: A persistence-level concept: the unit that a DTO and a repository wrap.\n: Narrower than a noun — one noun spans many entities.\n: Conceptual, **not a file-layout axis**. `links` slices by kind, so one\n  `models.py` holds many entities' models. There is no\n  `links\u002Fdb\u002F{adapter}\u002F{entity}.py`.\n\nA noun contains verb cuts. A verb cut depends on entities.\n\n## Slicing rules\n\n**pacts, mills, specs — by noun, then verb. links — port \u002F adapter \u002F kind.\ngates — port \u002F adapter \u002F page. inits — however is convenient.**\n\nEach pacts module holds all boundary contracts for that noun or verb cut:\nDTOs, write TypedDicts, repository protocols, errors. Split by domain concern,\nnot by technical kind — no `pacts\u002Fdtos.py`, `pacts\u002Fprotocols.py`, or\n`pacts\u002Frepos\u002F` directories, and never a `pacts\u002Fcore.py` or `common` bucket. A\ncontract two nouns share gets no bucket either: it stays in the noun that\nneeded it first, and earns its own module named after the thing it is\n(`pacts\u002Fmoney.py`) once the sharing makes the case. The ban is on the name, not\non the extraction.\n\n**pacts placement algorithm** — pacts mirrors the whole system; place each\ncontract by three questions, in order: (1) tied to a noun? →\n`pacts\u002F{noun}.py` (DTOs, write dicts, domain errors, repo protocols);\n(2) tied to a port? → `pacts\u002F{port}.py` (e.g. `pacts\u002Fdb.py` for\n`TransactionProtocol` — test: would it survive a total change of business\ndomain?); (3) about the wiring? → `pacts\u002Fservices.py` for `ServicesProtocol`\n**and the service protocols it names** — that module mirrors `inits\u002Fservices.py`,\nand a service protocol describes a registry leaf, not the noun its methods\nmention.\n\n**Protocols exist where a boundary needs them, not by policy.** Repository\nprotocols: essential — mills depend on them. Service protocols: optional —\nneeded for every service on a typed services namespace (web always; a CLI when\n`inits` hands the gate the whole registry rather than individual mills) and\nrecommended for service-to-service dependencies. Gate classes: no protocols —\nnothing outside inits refers to them.\n\n**Errors are coarse and shared** (`NotFoundError`, not `ProposalNotFound`);\nthe gate catching one decides what it means for that screen, at the call-site\n— no central error-to-status mapping. Adapter code translates store exceptions\ninto pacts errors (`IntegrityError` → `DatabaseConstraintError`), so no ORM\nexception reaches a mill.\n\n**Boundary vs core — where does it go?** Decide by what the code does. If it\n*crosses a boundary* (data shapes moving between layers) it is a contract →\n`pacts` (DTOs, protocols). If it *enforces business rules* (service logic,\ninvariants) it is core → `mills`. DTOs stay in `pacts` even though\nthey feel like domain objects: repo protocols in `pacts` return them, so moving\nthem to `mills` would make `pacts → mills → pacts` circular. A DTO is a data\ncontract for a port, not a domain object.\n\n**Repo methods follow the needs.** Parameters express variation within a use\ncase; a different scope is a different method. Filtering an event's meetings\nby facilitator or topic: parameters. Switching events or including\nnever-accepted meetings: a second use case → a second method. No generic\nquery objects through the protocol.\n\n**A `links` adapter's kinds are its own** — a `db` adapter is not the universal\ntemplate. For `db`, the kinds are typically `models` (internal) and\n`repositories` (public, exposed through the facade and consumed via the\nprotocols in `pacts`); a `payment_api\u002Fstripe` adapter may stay a single file, or\nsplit into transport \u002F types \u002F signer with no internal-vs-public distinction.\nThe public face of any `links` adapter is whatever its facade re-exports —\ninternal modules stay internal, so external code does `from\nmyproject.links.db.postgres import SessionRepository` and never reaches\n`models`.\n\n`pacts` and `mills` share the noun\u002Fverb axis, but nothing requires them to\npromote in lockstep — `mills\u002F` may be a package while `pacts.py` is still one\nfile. Nothing enforces it and nothing depends on it.\n\n## Growing rules\n\n**Default: start as small as possible. Split only when size or friction makes\nthe case for itself.** Premature splitting creates churn, bloats the import\ngraph, and makes the layout look complete before the requirements actually\ndemand it.\n\nConcrete thresholds — none is a hard line, all are \"watch for this\":\n\n- **A layer becomes a package when it earns it.** Promote `mills.py` → `mills\u002F`\n  on any one of: it crosses ~1000 lines, two unrelated concerns in it cause\n  merge friction, or a second noun genuinely exists. Not before. `inits` is a\n  convenience call either way — it stays thin whatever it holds.\n- **~1000 lines per file** — split a file when it crosses this and the two\n  halves are unrelated enough that they cause merge friction. A 1500-line file\n  holding one tightly coupled service is fine; a 600-line file holding three\n  independent services is not.\n- **~12 public symbols per namespace level** — applies to repository\n  registries, the services tree, pacts noun modules, and the inits namespaces.\n  At 13+ leaves, introduce a sub-bucket grouped by noun or verb. With ≤12,\n  stay flat.\n- **Folder must contain at least 2 files before it exists.** Never create\n  `inits\u002Fservices\u002Finvoices\u002Fissuing\u002F` for a single leaf. Never create\n  `pacts\u002F{noun}\u002F{verb}.py` while the noun has only one cut.\n  Reverse the speculative scaffold; flatten back when the leaf count drops.\n- **Split links by kind first.** Default: one file per kind. When a kind\n  crosses ~1000 lines, **promote it to a package** and split into submodules\n  (`kind1\u002Fpart1.py`, `kind1\u002Fpart2.py`) — same pattern as growing `views.py`\n  into `views\u002F`. Do **not** create suffixed siblings (`kind1_a.py`). The\n  baseline is **halve, don't shard, and arrange parts to avoid circular\n  imports**; the right grouping is adapter-specific (e.g. `db` models often\n  split by foreign-key dependency hierarchy — the entities that change\n  together — repositories along the same lines; an external-API adapter may\n  not need to split at all). The\n  `links\u002F{port}\u002F{adapter}\u002F__init__.py` facade keeps the public import path\n  stable across the promotion. Framework technicality: if the ORM discovers\n  model classes by importing the package (Django does), `models\u002F__init__.py`\n  must re-export them; `repositories\u002F__init__.py` can stay empty since the\n  facade lives at the parent.\n\nWhen in doubt, keep it flat. The ~12 rule and the ~1000-line rule are escape\nhatches, not invitations.\n\n## Patterns\n\n1. **Entry points return DTOs, never models.** Templates, serializers, and CLI\n   output receive DTOs from pacts. ORM instances never leave `links`.\n2. **Entry points call services, not repos.** `request.services.\u003Cname>.method(...)`\n   is the data path out of a view — `pacts` is the only project import a gate\n   has, so never a repo, a model, or a service class, and never a repository\n   reached directly. Services are exposed as a flat namespace\n   wired in `inits\u002Fservices.py`; CLI gates receive theirs at construction.\n3. **Services take specific repo protocols + a `TransactionProtocol` via\n   constructor** — not imports of concrete repos, not dependencies passed as\n   method arguments. With an ambient ORM (Django), never a whole Unit of Work;\n   with a session-based ORM (SQLAlchemy) the session already is one, and\n   injecting it is idiomatic. ISP at the service boundary: declare the\n   two-or-three protocols actually used.\n4. **Mills have no side-effect imports.** Only protocols and DTOs from pacts,\n   constants from specs, pure helpers from anywhere (see **Layers**).\n5. **Writes use TypedDicts.** DTOs for reads, TypedDicts for writes — gates →\n   mills as input, mills → links as what repo write methods accept\n   (`create(data: CreateProposalDict) -> ProposalDTO`; a `CreateXDict` has no\n   `id` — the store assigns it). The split is the `id`: a nullable one on the\n   DTO would push a null check into everything that touches it, and a write\n   shape is short-lived enough that it never makes the trip a DTO makes.\n6. **Web requests typed via a gate-local typing-only subclass** of the\n   framework request (`class RootRequest(HttpRequest): services:\n   ServicesProtocol` in the web adapter) — never instantiated; middleware\n   mutates the real request. Only `ServicesProtocol` comes from pacts. CLI\n   gates have no context.\n7. **Multi-repo writes use `transaction.atomic()` from `TransactionProtocol`.**\n   Entry points never start transactions; that is a service concern. The\n   protocol: `atomic()` and `savepoint()`, both returning a context manager;\n   `savepoint()` rolls back only its block on constraint violation,\n   re-raising as a pacts error with the outer transaction usable. The\n   implementation is inits binding glue when it wraps an ambient ORM with no\n   store behind it, and a links adapter when it holds a connection.\n8. New repo methods need matching Protocol in pacts.\n9. **DTOs must be constructible from what the adapter loaded.** Pydantic is not\n   required — a dataclass, `NamedTuple`, or attrs class is a DTO too (write\n   shapes are already `TypedDict`, so `pacts` can be pure stdlib); pick one and\n   use it throughout. With Pydantic, attribute rows (an ORM instance) need\n   `model_config = ConfigDict(from_attributes=True)`; mapping rows\n   (`sqlite3.Row`, a dict cursor) validate from `dict(row)` with no config;\n   without it, construction is a plain call. A row that does not match the DTO\n   is mapped in the repository, not by a method on the DTO.\n10. New repositories exposed as `@cached_property` on `inits\u002Frepositories.py`\n    (flat). New services exposed as `@cached_property` on `inits\u002Fservices.py`\n    (flat, zero-arg `Services()` builds its own dependencies — no DI inside\n    the composition root). Lifetimes: `@cached_property` = per container\n    (per request); `@functools.lru_cache` on a module-level inits factory =\n    per process (pooled clients, connections). Closing follows the owner: a\n    process-lifetime connection is wound down in inits (often nothing to\n    write — exit does it); one opened mid-flight is closed by a context manager\n    the link exposes, like `TransactionProtocol`. See **Growing rules** for\n    when to bucket.\n11. **Protocol implementations declare the protocol as a base class** — where\n    a protocol exists — so the intent is explicit and a type checker verifies\n    conformance. This assumes one runs: an explicit `Protocol` subclass\n    inherits the stub bodies, so an unimplemented method returns `None` rather\n    than failing. Exception: very generic structural protocols\n    (`TransactionProtocol`, callbacks) with multiple unrelated duck-typed\n    implementations.\n12. **Gates validate format, mills validate meaning.** A gate checks input\n    parses (an email, an int); a mill checks it makes sense (\"email or\n    username required\", seat limits from specs). Parse vs semantics, not\n    single-field vs cross-field. Permissions: trivial checks\n    (`is_authenticated`) in gates; rule-bearing permission systems in mills.\n13. **Django apps are markers, not structure.** An `AppConfig` sits at the\n    lowest directory Django must discover (models, commands, templatetags),\n    with a custom `label` (names all end in `.django` — default labels\n    collide). Migrations live in `links\u002Fdb\u002F{framework}\u002Fmigrations\u002F`; admin\n    (model-coupled by design) at `links\u002Fdb\u002Fdjango\u002Fadmin.py`; plain `Form`s\n    only, never `ModelForm`. Framework-owned surfaces (`request.user`,\n    `django_login`) are named exemptions — contain them in gates; mills see\n    ids and DTOs.\n\n## Dependency direction\n\n**Cross-noun access is fine.** Repos cross nouns freely — data access is not\nbehavior. An entry point on one noun's turf reading another noun's data is\nnormal, not a boundary violation. The smell to watch is duplicated *behavior*\nacross nouns; the fix is a shared lower-level mill function that both call,\nnot a rule against cross-noun repo reads.\n\n**Service-to-service calls are fine** when reusing real orchestration. The\ngenuine smells are narrower: layering inversion (a low-level unit depending on a\nhigh-level orchestrator), cycles, and anemic delegation (one service calling\nanother for a single trivial read it could do via a repo).\n\n## Testing\n\nThe layer under test dictates the test type — not convenience, not what is\neasiest for coverage:\n\n- Pure-logic core (`mills`) → **unit** tests. No IO; the service gets\n  `MagicMock`s for the repo protocols (and for data when only one field\n  matters); assert how the mocks were called. `MagicMock` covers\n  `TransactionProtocol` — it speaks the context-manager protocol.\n- IO-bearing boundary layers (`links`, `gates`, adapters, templates) →\n  **integration** tests against real infrastructure. Mock at the lowest level or\n  not at all; assert side effects. A gate test is a full-request test via the\n  framework's test client, asserting **all** fields of the response (context\n  data, redirect URL, status), with strict templates that fail on unknown\n  variables.\n\nTests live in a repo-root `tests\u002F` split by type — `tests\u002Funit\u002F` (mills, plus\npure helpers from any layer; organised by convenience, not mirroring the\ncode) and `tests\u002Fintegration\u002F` (gates by port + page, links by port +\nadapter).\n\nAn uncovered line is covered by the test type that owns its layer — never raise\n`links`\u002F`gates` coverage with a mock-everything unit test of IO-bearing code\n(views, commands, repositories, importers). Exception: a pure, IO-free helper\n(no DB, HTTP, request\u002Fresponse, template render, or framework objects) may be\nunit-tested wherever it lives.\n\n## Drift red flags\n\n### Layout and slicing\n\n**`links.py` or `gates.py` as a single file**\n: Both need the `{port}\u002F{adapter}` axis from day one. The port is knowable\n  before any code is written; deferring it costs an import rewrite the day a\n  second adapter appears.\n\n**A layer promoted to a package before it earned it**\n: `pacts\u002F`, `specs\u002F`, or `mills\u002F` as a directory while there is one noun, well\n  under ~1000 lines, and no merge friction. The tree is anticipating nouns you\n  have not discovered.\n\n**Nested folders holding one or two small files**\n: `pacts\u002Finvoices\u002Fissue\u002Fcreate.py` when `pacts\u002Finvoices\u002Fissue.py` would do, or\n  `inits\u002Fservices\u002Finvoices\u002Fissuing.py` with no sibling. A folder needs at least\n  two leaves to exist.\n\n**Port axis inside mills or specs**\n: `mills\u002Fweb\u002Fproposals.py` or `specs\u002Fapi\u002F...`. Mills and specs have no\n  delivery-mechanism axis. If you see a port word inside these layers, the code\n  belongs elsewhere.\n\n**A catch-all verb module**\n: `manage.py`, `organize.py`, `misc.py` inside a noun. A verb cut must name a\n  real activity.\n\n**A noun axis inside gates**\n: `gates\u002Fweb\u002Fdjango\u002Finvoices.py` when the interface has no such page. Gates\n  mirror the interface; mills mirror the domain. The two trees are not expected\n  to match.\n\n**`common`, `shared`, `utils`, or `entities` as a module or folder name**\n: Magnets for unrelated code. Each says where a file sits, not what it holds,\n  so anything can be filed there and nothing can ever be found. Shared types go\n  to `pacts`, under the noun that needed them first; everything else takes a\n  name from the axis it belongs to. The exception is a real concept that\n  happens to carry the word — a `DOMEntity` in a browser-port adapter earns\n  `entities.py`; a bag of dataclasses does not.\n\n### pacts\n\n**pacts split by technical kind instead of noun**\n: `pacts\u002Fdtos.py`, `pacts\u002Fprotocols.py`, `pacts\u002Frepos\u002F`. These group by what\n  the type *is*, not by what domain concern it belongs to. This forces\n  unrelated nouns to share files and makes the package harder to navigate.\n\n**`pacts\u002Fcore.py`, `pacts\u002Fcommon.py`, or similar**\n: A `common\u002F` bucket wearing a nicer name. Every contract has a principled home\n  under the noun \u002F port \u002F wiring axes.\n\n**A DTO that cannot be built from a store row or ORM instance**\n: Repositories cannot return it. Whatever the project uses for DTOs — Pydantic\n  is not required — construction has to work from the row the adapter loaded.\n  A row that does not match the DTO is mapped in the repository, not by a\n  method on the DTO.\n\n**A protocol implementation that does not name the protocol as a base class**\n: The conformance check is left to a structural match that can silently drift.\n  The exception is very generic structural protocols — `TransactionProtocol`,\n  callbacks — with multiple unrelated duck-typed implementations.\n\n### specs\n\n**specs imported from links, gates, or inits**\n: `specs` are business invariants, and business rules are enforced in `mills`\n  alone. A constant needed elsewhere is either a contract (`pacts` — a max\n  length or an allowed range is a fact about the shape of the data, and belongs\n  beside the contract it constrains) or configuration, which enters at `inits`\n  or comes from the framework's settings accessor where there is one.\n\n**specs reading from `os.environ` or `settings`, or performing IO**\n: It is a constants layer. Environment-dependent values enter at `inits`.\n\n### mills\n\n**mills importing anything with side effects**\n: An ORM, HTTP machinery, a CLI parser, settings access — absolute violation.\n  Pure computation is fine wherever it comes from. So is the ambient stuff the\n  rule was never about: the clock, a random draw, a UUID, a log line.\n\n**A service taking a whole Unit of Work instead of the protocols it uses**\n: Applies to ambient-ORM projects. With a session-based ORM the session already\n  is a unit of work, and injecting it is idiomatic.\n\n**A page axis inside mills**\n: Gates mirror the interface, mills mirror the domain. A sitemap in `mills` is\n  the interface leaking inward.\n\n### links\n\n**Model and repository in the same links file**\n: This collapses the internal-vs-public boundary. Models are internal to the\n  adapter; repositories are its public surface.\n\n**links files named per entity**\n: `links\u002Fdb\u002Fpostgres\u002Fuser.py`. `links` slices by kind, not by entity. One\n  `models.py` holds many entities' models.\n\n**Suffix-sibling links files**\n: `repositories_invoices.py`, `models_users.py`. Promote the kind to a\n  `{kind}\u002F` package with submodules instead. Halve, don't shard.\n\n**A links facade that re-exports models, or omits a public repository**\n: `links\u002F{port}\u002F{adapter}\u002F__init__.py` *is* the public surface. Whatever it\n  exports is public; everything else is internal.\n\n**An ORM model imported from outside links\u002F**\n: Use the repository protocol from `pacts` instead.\n\n**A repository imported directly in a gate or a mill**\n: Inject it through `inits`.\n\n### gates\n\n**A gate importing project code other than `pacts`**\n: An ORM model, a repository class, a service class from `mills` — all the same\n  violation. A gate calls services through their protocols. If none exists,\n  create one — a mill in `mills`, a protocol in `pacts`, a leaf in\n  `inits\u002Fservices.py` — before writing the gate.\n\n**A gate returning ORM instances to templates or serializers**\n: Return DTOs from `pacts`. ORM instances never leave `links`.\n\n**A gate that opens a transaction**\n: Atomicity is a service concern.\n\n**Business rules in form validation**\n: Gates validate format — an email, an int, a date. Meaning (\"email or username\n  required\", seat limits) belongs in mills, which alone may read `specs`.\n\n**A non-port axis at the top level of gates**\n: `gates\u002Fmills\u002F...`. The first axis below `gates` is always the port.\n\n### inits\n\n**A gate constructing repository or service instances**\n: That is the wiring `inits` owns, and doing it in a gate breaks it.\n\n**mills importing from inits**\n: The dependency runs the other way. `inits` knows the concrete classes;\n  `mills` sees only protocols.\n\n**inits containing business logic**\n: It should only wire, never decide.\n","\u003C!-- Generated from SKILL.src.md — edit that, then run: mise run skill -->\n\n# GLIMPSE Architecture Reference\n\nThis file is the compressed form of the full reference at\n\u003Chttps:\u002F\u002Fglimpse.fancysnake.dev\u002F>, which is authoritative where the two\ndisagree and carries the worked examples and the reasoning behind each rule.\n\n## Layers\n\n```text\npacts   Protocols, DTOs, errors, enums, TypedDicts. Depends on nothing.\nspecs   Business invariants (pure constants, no IO). Only for mills; see below.\nmills   Business logic, services. Depends on pacts + specs. No side-effect imports.\nlinks   Repositories, external clients. pacts + ORM \u002F driver \u002F SDK.\ngates   Entry points: request handlers, forms, routing, CLI commands. pacts only.\ninits   DI container, middleware. The only layer where gates, mills, links meet.\nedges   Settings, wsgi, manage.py. Outside GLIMPSE; empty on a CLI, never absent.\n```\n\nAll seven exist from the first commit — `edges\u002F` as an empty package where the\nproject has nothing for it. The import contracts are taken as a set on day one,\nand a contract can only name a module that exists.\n\nImport rules enforced by `importlinter` (`pyproject.toml` →\n`[tool.importlinter]`). No exceptions without explicit approval.\n\n**What is fixed, and what is guidance.** Absolute: which layer code belongs to,\nand which layer may import which. Everything below — slicing axes, thresholds,\nfile names, patterns — is recorded practice. Follow it by default; treat an\narrangement it does not describe as legitimate when it follows a real need and\nbreaks no import rule. **Absence of a rule is not a violation**, so do not\nflag code for failing to match a shape this reference never required.\n\n**Composition is per-port.** `inits` composes the object graph everywhere; how\nit reaches gates differs. Web: the framework dispatches to gates, so `inits`\nnever imports them — middleware builds `Services()` per request and attaches\nit to the request (the framework's thread-safe seam). CLI: nothing dispatches,\nso `inits` imports the gate classes, injects mills into their constructors,\nand is named by dotted string in `pyproject.toml`\n(`[project.scripts]` → `myproject.inits.cli:run`).\n\n**edges is two-way isolated.** Nothing imports `edges`; `edges` imports\nnothing first-party — it names project code only by dotted string\n(`DJANGO_SETTINGS_MODULE`, `MIDDLEWARE`, `ROOT_URLCONF`). The root middleware\nimports services, so it lives in `inits`, not `edges`.\n\n**specs or pacts — what it is, not where it is used.** A constant more than one\nlayer must enforce is a fact about the shape of the data (a max length lives in\nthe column, the form, and the rule) — that is a contract, so it goes to `pacts`\nbeside the DTO it constrains. A constant only the rule can observe (seat limit,\ngrace period) is a business invariant and goes to `specs`, which mills alone may\nimport: a threshold reaching `links` ends up encoded in the schema. Tell: change\na `specs` value and only `mills` changes; change a `pacts` value and every\nenforcer changes together.\n\n**Configuration enters at `inits`.** `inits` reads the environment and passes\neach value to the leaf that needs it; `mills` never reads settings at all. On a\nframework with a settings singleton, `links` and `gates` read it through the\nframework's accessor (`django.conf.settings`) — an import of the framework, not\nof `edges`. Without such a framework there is no settings layer at all, and\n`edges\u002F` stays empty. A leaf may still probe its own environment when the\nprobe is injectable for tests; what the environment *decides*, `inits`\ndecides. User\ninput read at runtime — a config file, a command flag — is not configuration:\nit comes in through a port, shaped in `pacts`, validated in a mill.\n\n**\"Framework-free\" means no side effects, not package names.** Forbidden in\n`mills`: imports that do IO, touch global state, or own control flow (ORM,\nHTTP machinery, settings access). Pure computation is fine wherever it comes\nfrom — `django.utils.text.slugify` qualifies. Enforcement level (ban package \u002F\nreview-guarded \u002F ban effectful subtrees) is a per-project choice.\n\nThe rule guards against destructive operations and output the program depends\non, not everything ambient: the clock, a random draw, a UUID, a log line are\nall fine in a mill, and all mockable. Logging reads better at the edge anyway\n(mills raise, gates catch and log) — a preference, not a rule. If a generated\nvalue is *input* to what the service does rather than something it produces on\nthe way, take it as an argument; when the making is somebody else's job, that\nis a link, and a link earns its place even as a one-line wrapper because it\nnames an external capability.\n\n**No DDD tactical patterns.** GLIMPSE has no aggregates or value objects —\ndata moves as DTOs and write TypedDicts; invariants live in service code. The\nslicing axes are GLIMPSE's own (see **Slicing vocabulary**).\n\n## File layout\n\n**`pacts`, `specs`, `mills`, `inits` start as single modules; `links` and\n`gates` are packages from day one.** The first three are sliced by noun, and on\nday one you have one `mills.py` — don't plan further ahead. `inits` stays thin\nwhatever the project does, so split it however is convenient. `links` and\n`gates` take the port axis immediately: the port is knowable before a line is\nwritten — you know you are building a CLI, you know you are talking to a\ndatabase — and skipping it means renaming every import the day a second adapter\nappears.\n\n```text\n# Day one — axis-free layers stay flat\npacts.py\nspecs.py\nmills.py\ninits.py\nlinks\u002F{port}\u002F{adapter}.py                   # e.g. links\u002Fdb\u002Fsqlite.py\ngates\u002F{port}\u002F{adapter}.py                   # e.g. gates\u002Fcli\u002Fargparse.py\nedges\u002F__init__.py                           # empty until a framework fills it\n\n# Grown\npacts\u002F{noun}.py                             # or pacts\u002F{noun}\u002F{verb}.py\npacts\u002F{port}.py                             # port machinery (e.g. pacts\u002Fdb.py)\npacts\u002Fservices.py                           # ServicesProtocol + service protocols\nspecs\u002F{noun}.py\nmills\u002F{noun}.py                             # or mills\u002F{noun}\u002F{verb}.py\ninits\u002Frepositories.py                       # a module per registry, plus one that binds\ninits\u002Fservices.py                           # (+ middleware.py on web, cli.py on a CLI)\nlinks\u002F{port}\u002F{adapter}\u002F{kind}.py            # while small (models.py, repositories.py)\nlinks\u002F{port}\u002F{adapter}\u002F{kind}\u002F{module}.py   # when {kind} crosses threshold\nlinks\u002F{port}\u002F{adapter}\u002F__init__.py          # facade — re-exports the public surface\ngates\u002F{port}\u002F{adapter}\u002F{page}.py            # or ...\u002F{page_group}\u002F{page}.py,\ngates\u002F{port}\u002F{adapter}\u002F{page}\u002F{subpage}.py  #    or ...\u002F{page}\u002F{subpage}.py\n```\n\nThresholds for every promotion above: **Growing rules**.\n\n**`__init__.py` re-export policy.** Default: keep `__init__.py` empty and import\neach symbol from the module that defines it (`from pkg.foo.bar import Bar`, not\n`from pkg.foo import Bar`). A facade `__init__.py` that re-exports a public\nsurface is allowed only for: a framework or public-API package whose inner\nlayout is implementation detail (the `links` adapter facade), relief from\nline-length pressure, or a pre-existing legacy facade. It is not the default.\n\n## Slicing vocabulary\n\n**Port**\n: A delivery mechanism, named after the domain concept rather than the\n  technology.\n: Examples: `cli`, `web`, `db`, `payment_api`, `email`\n: The first axis for `links` and `gates`, known before any code exists.\n\n**Adapter**\n: The specific technology implementing a port.\n: Examples: `postgres`, `sqlite`, `argparse`, `stripe`, `sendgrid`\n: One port can have multiple adapters — usually **coexisting**, not\n  interchangeable. `payment_api\u002Fstripe` and `payment_api\u002Fpaypal` are both wired\n  and both live; which one handles a given payment is a business decision made\n  in a mill, through the protocols it holds. Genuine substitution\n  (`db\u002Fpostgres` vs `db\u002Fsqlite`) is the rarer case: one adapter wired per\n  deployment, chosen in `inits`.\n: One technology can serve multiple ports. A full-stack framework shipping both\n  an ORM and a request layer appears as `db\u002F{framework}` and `web\u002F{framework}` —\n  two separate adapters that share nothing but a name.\n\n**Kind**\n: The type of thing a `links` module holds, below the adapter.\n: Examples: `models` and `repositories` for a `db` adapter; `transport`,\n  `types`, `signer` for an API client.\n: Kinds are per-adapter. The `db` shape is not a universal template.\n\n**Noun**\n: A fat data cow — the model cluster everything else hangs off.\n: Examples: `invoices`, `customers`, `proposals`, `users`\n: The primary slicing axis for `pacts`, `mills`, and `specs`.\n: Which nouns exist depends on what the system is *for*, not on the words in\n  the domain. An invoice tracker slices by `invoices`; a subscription service\n  slices by `billing`, and an invoice is a detail inside it.\n: Plurality is not prescribed; it follows the noun. `events` are many, a\n  `panel` is one.\n: Nouns are GLIMPSE's own axis, not a DDD import. If another axis fits a\n  project better, slice by that instead; the layer rules don't change.\n\n**Verb**\n: An activity cut inside a noun.\n: Examples: `issue`, `refund`, `enroll`, `schedule`\n: A verb module holds the records and logic of actions, not first-class data.\n  Verbs nest inside nouns: `invoices` might cut into `issue` and `refund`.\n: No catch-all verbs. `manage`, `organize`, and `misc` name no activity — if\n  you cannot name a real one, the file is not too big yet.\n\n**Page**\n: What the user touches — the slicing axis for `gates`, below port and adapter.\n: Gates take whatever grouping the interface already has: a command or command\n  group for a CLI, a page group and page — or a page and its subpages — for the\n  web, a view for a TUI, a tool for MCP.\n: Gates mirror the shape of the interface; mills mirror the domain. The two\n  need not line up, and forcing them to is how a sitemap ends up in `mills`.\n\n**Entity**\n: A persistence-level concept: the unit that a DTO and a repository wrap.\n: Narrower than a noun — one noun spans many entities.\n: Conceptual, **not a file-layout axis**. `links` slices by kind, so one\n  `models.py` holds many entities' models. There is no\n  `links\u002Fdb\u002F{adapter}\u002F{entity}.py`.\n\nA noun contains verb cuts. A verb cut depends on entities.\n\n## Slicing rules\n\n**pacts, mills, specs — by noun, then verb. links — port \u002F adapter \u002F kind.\ngates — port \u002F adapter \u002F page. inits — however is convenient.**\n\nEach pacts module holds all boundary contracts for that noun or verb cut:\nDTOs, write TypedDicts, repository protocols, errors. Split by domain concern,\nnot by technical kind — no `pacts\u002Fdtos.py`, `pacts\u002Fprotocols.py`, or\n`pacts\u002Frepos\u002F` directories, and never a `pacts\u002Fcore.py` or `common` bucket. A\ncontract two nouns share gets no bucket either: it stays in the noun that\nneeded it first, and earns its own module named after the thing it is\n(`pacts\u002Fmoney.py`) once the sharing makes the case. The ban is on the name, not\non the extraction.\n\n**pacts placement algorithm** — pacts mirrors the whole system; place each\ncontract by three questions, in order: (1) tied to a noun? →\n`pacts\u002F{noun}.py` (DTOs, write dicts, domain errors, repo protocols);\n(2) tied to a port? → `pacts\u002F{port}.py` (e.g. `pacts\u002Fdb.py` for\n`TransactionProtocol` — test: would it survive a total change of business\ndomain?); (3) about the wiring? → `pacts\u002Fservices.py` for `ServicesProtocol`\n**and the service protocols it names** — that module mirrors `inits\u002Fservices.py`,\nand a service protocol describes a registry leaf, not the noun its methods\nmention.\n\n**Protocols exist where a boundary needs them, not by policy.** Repository\nprotocols: essential — mills depend on them. Service protocols: optional —\nneeded for every service on a typed services namespace (web always; a CLI when\n`inits` hands the gate the whole registry rather than individual mills) and\nrecommended for service-to-service dependencies. Gate classes: no protocols —\nnothing outside inits refers to them.\n\n**Errors are coarse and shared** (`NotFoundError`, not `ProposalNotFound`);\nthe gate catching one decides what it means for that screen, at the call-site\n— no central error-to-status mapping. Adapter code translates store exceptions\ninto pacts errors (`IntegrityError` → `DatabaseConstraintError`), so no ORM\nexception reaches a mill.\n\n**Boundary vs core — where does it go?** Decide by what the code does. If it\n*crosses a boundary* (data shapes moving between layers) it is a contract →\n`pacts` (DTOs, protocols). If it *enforces business rules* (service logic,\ninvariants) it is core → `mills`. DTOs stay in `pacts` even though\nthey feel like domain objects: repo protocols in `pacts` return them, so moving\nthem to `mills` would make `pacts → mills → pacts` circular. A DTO is a data\ncontract for a port, not a domain object.\n\n**Repo methods follow the needs.** Parameters express variation within a use\ncase; a different scope is a different method. Filtering an event's meetings\nby facilitator or topic: parameters. Switching events or including\nnever-accepted meetings: a second use case → a second method. No generic\nquery objects through the protocol.\n\n**A `links` adapter's kinds are its own** — a `db` adapter is not the universal\ntemplate. For `db`, the kinds are typically `models` (internal) and\n`repositories` (public, exposed through the facade and consumed via the\nprotocols in `pacts`); a `payment_api\u002Fstripe` adapter may stay a single file, or\nsplit into transport \u002F types \u002F signer with no internal-vs-public distinction.\nThe public face of any `links` adapter is whatever its facade re-exports —\ninternal modules stay internal, so external code does `from\nmyproject.links.db.postgres import SessionRepository` and never reaches\n`models`.\n\n`pacts` and `mills` share the noun\u002Fverb axis, but nothing requires them to\npromote in lockstep — `mills\u002F` may be a package while `pacts.py` is still one\nfile. Nothing enforces it and nothing depends on it.\n\n## Growing rules\n\n**Default: start as small as possible. Split only when size or friction makes\nthe case for itself.** Premature splitting creates churn, bloats the import\ngraph, and makes the layout look complete before the requirements actually\ndemand it.\n\nConcrete thresholds — none is a hard line, all are \"watch for this\":\n\n- **A layer becomes a package when it earns it.** Promote `mills.py` → `mills\u002F`\n  on any one of: it crosses ~1000 lines, two unrelated concerns in it cause\n  merge friction, or a second noun genuinely exists. Not before. `inits` is a\n  convenience call either way — it stays thin whatever it holds.\n- **~1000 lines per file** — split a file when it crosses this and the two\n  halves are unrelated enough that they cause merge friction. A 1500-line file\n  holding one tightly coupled service is fine; a 600-line file holding three\n  independent services is not.\n- **~12 public symbols per namespace level** — applies to repository\n  registries, the services tree, pacts noun modules, and the inits namespaces.\n  At 13+ leaves, introduce a sub-bucket grouped by noun or verb. With ≤12,\n  stay flat.\n- **Folder must contain at least 2 files before it exists.** Never create\n  `inits\u002Fservices\u002Finvoices\u002Fissuing\u002F` for a single leaf. Never create\n  `pacts\u002F{noun}\u002F{verb}.py` while the noun has only one cut.\n  Reverse the speculative scaffold; flatten back when the leaf count drops.\n- **Split links by kind first.** Default: one file per kind. When a kind\n  crosses ~1000 lines, **promote it to a package** and split into submodules\n  (`kind1\u002Fpart1.py`, `kind1\u002Fpart2.py`) — same pattern as growing `views.py`\n  into `views\u002F`. Do **not** create suffixed siblings (`kind1_a.py`). The\n  baseline is **halve, don't shard, and arrange parts to avoid circular\n  imports**; the right grouping is adapter-specific (e.g. `db` models often\n  split by foreign-key dependency hierarchy — the entities that change\n  together — repositories along the same lines; an external-API adapter may\n  not need to split at all). The\n  `links\u002F{port}\u002F{adapter}\u002F__init__.py` facade keeps the public import path\n  stable across the promotion. Framework technicality: if the ORM discovers\n  model classes by importing the package (Django does), `models\u002F__init__.py`\n  must re-export them; `repositories\u002F__init__.py` can stay empty since the\n  facade lives at the parent.\n\nWhen in doubt, keep it flat. The ~12 rule and the ~1000-line rule are escape\nhatches, not invitations.\n\n## Patterns\n\n1. **Entry points return DTOs, never models.** Templates, serializers, and CLI\n   output receive DTOs from pacts. ORM instances never leave `links`.\n2. **Entry points call services, not repos.** `request.services.\u003Cname>.method(...)`\n   is the data path out of a view — `pacts` is the only project import a gate\n   has, so never a repo, a model, or a service class, and never a repository\n   reached directly. Services are exposed as a flat namespace\n   wired in `inits\u002Fservices.py`; CLI gates receive theirs at construction.\n3. **Services take specific repo protocols + a `TransactionProtocol` via\n   constructor** — not imports of concrete repos, not dependencies passed as\n   method arguments. With an ambient ORM (Django), never a whole Unit of Work;\n   with a session-based ORM (SQLAlchemy) the session already is one, and\n   injecting it is idiomatic. ISP at the service boundary: declare the\n   two-or-three protocols actually used.\n4. **Mills have no side-effect imports.** Only protocols and DTOs from pacts,\n   constants from specs, pure helpers from anywhere (see **Layers**).\n5. **Writes use TypedDicts.** DTOs for reads, TypedDicts for writes — gates →\n   mills as input, mills → links as what repo write methods accept\n   (`create(data: CreateProposalDict) -> ProposalDTO`; a `CreateXDict` has no\n   `id` — the store assigns it). The split is the `id`: a nullable one on the\n   DTO would push a null check into everything that touches it, and a write\n   shape is short-lived enough that it never makes the trip a DTO makes.\n6. **Web requests typed via a gate-local typing-only subclass** of the\n   framework request (`class RootRequest(HttpRequest): services:\n   ServicesProtocol` in the web adapter) — never instantiated; middleware\n   mutates the real request. Only `ServicesProtocol` comes from pacts. CLI\n   gates have no context.\n7. **Multi-repo writes use `transaction.atomic()` from `TransactionProtocol`.**\n   Entry points never start transactions; that is a service concern. The\n   protocol: `atomic()` and `savepoint()`, both returning a context manager;\n   `savepoint()` rolls back only its block on constraint violation,\n   re-raising as a pacts error with the outer transaction usable. The\n   implementation is inits binding glue when it wraps an ambient ORM with no\n   store behind it, and a links adapter when it holds a connection.\n8. New repo methods need matching Protocol in pacts.\n9. **DTOs must be constructible from what the adapter loaded.** Pydantic is not\n   required — a dataclass, `NamedTuple`, or attrs class is a DTO too (write\n   shapes are already `TypedDict`, so `pacts` can be pure stdlib); pick one and\n   use it throughout. With Pydantic, attribute rows (an ORM instance) need\n   `model_config = ConfigDict(from_attributes=True)`; mapping rows\n   (`sqlite3.Row`, a dict cursor) validate from `dict(row)` with no config;\n   without it, construction is a plain call. A row that does not match the DTO\n   is mapped in the repository, not by a method on the DTO.\n10. New repositories exposed as `@cached_property` on `inits\u002Frepositories.py`\n    (flat). New services exposed as `@cached_property` on `inits\u002Fservices.py`\n    (flat, zero-arg `Services()` builds its own dependencies — no DI inside\n    the composition root). Lifetimes: `@cached_property` = per container\n    (per request); `@functools.lru_cache` on a module-level inits factory =\n    per process (pooled clients, connections). Closing follows the owner: a\n    process-lifetime connection is wound down in inits (often nothing to\n    write — exit does it); one opened mid-flight is closed by a context manager\n    the link exposes, like `TransactionProtocol`. See **Growing rules** for\n    when to bucket.\n11. **Protocol implementations declare the protocol as a base class** — where\n    a protocol exists — so the intent is explicit and a type checker verifies\n    conformance. This assumes one runs: an explicit `Protocol` subclass\n    inherits the stub bodies, so an unimplemented method returns `None` rather\n    than failing. Exception: very generic structural protocols\n    (`TransactionProtocol`, callbacks) with multiple unrelated duck-typed\n    implementations.\n12. **Gates validate format, mills validate meaning.** A gate checks input\n    parses (an email, an int); a mill checks it makes sense (\"email or\n    username required\", seat limits from specs). Parse vs semantics, not\n    single-field vs cross-field. Permissions: trivial checks\n    (`is_authenticated`) in gates; rule-bearing permission systems in mills.\n13. **Django apps are markers, not structure.** An `AppConfig` sits at the\n    lowest directory Django must discover (models, commands, templatetags),\n    with a custom `label` (names all end in `.django` — default labels\n    collide). Migrations live in `links\u002Fdb\u002F{framework}\u002Fmigrations\u002F`; admin\n    (model-coupled by design) at `links\u002Fdb\u002Fdjango\u002Fadmin.py`; plain `Form`s\n    only, never `ModelForm`. Framework-owned surfaces (`request.user`,\n    `django_login`) are named exemptions — contain them in gates; mills see\n    ids and DTOs.\n\n## Dependency direction\n\n**Cross-noun access is fine.** Repos cross nouns freely — data access is not\nbehavior. An entry point on one noun's turf reading another noun's data is\nnormal, not a boundary violation. The smell to watch is duplicated *behavior*\nacross nouns; the fix is a shared lower-level mill function that both call,\nnot a rule against cross-noun repo reads.\n\n**Service-to-service calls are fine** when reusing real orchestration. The\ngenuine smells are narrower: layering inversion (a low-level unit depending on a\nhigh-level orchestrator), cycles, and anemic delegation (one service calling\nanother for a single trivial read it could do via a repo).\n\n## Testing\n\nThe layer under test dictates the test type — not convenience, not what is\neasiest for coverage:\n\n- Pure-logic core (`mills`) → **unit** tests. No IO; the service gets\n  `MagicMock`s for the repo protocols (and for data when only one field\n  matters); assert how the mocks were called. `MagicMock` covers\n  `TransactionProtocol` — it speaks the context-manager protocol.\n- IO-bearing boundary layers (`links`, `gates`, adapters, templates) →\n  **integration** tests against real infrastructure. Mock at the lowest level or\n  not at all; assert side effects. A gate test is a full-request test via the\n  framework's test client, asserting **all** fields of the response (context\n  data, redirect URL, status), with strict templates that fail on unknown\n  variables.\n\nTests live in a repo-root `tests\u002F` split by type — `tests\u002Funit\u002F` (mills, plus\npure helpers from any layer; organised by convenience, not mirroring the\ncode) and `tests\u002Fintegration\u002F` (gates by port + page, links by port +\nadapter).\n\nAn uncovered line is covered by the test type that owns its layer — never raise\n`links`\u002F`gates` coverage with a mock-everything unit test of IO-bearing code\n(views, commands, repositories, importers). Exception: a pure, IO-free helper\n(no DB, HTTP, request\u002Fresponse, template render, or framework objects) may be\nunit-tested wherever it lives.\n\n## Drift red flags\n\n### Layout and slicing\n\n**`links.py` or `gates.py` as a single file**\n: Both need the `{port}\u002F{adapter}` axis from day one. The port is knowable\n  before any code is written; deferring it costs an import rewrite the day a\n  second adapter appears.\n\n**A layer promoted to a package before it earned it**\n: `pacts\u002F`, `specs\u002F`, or `mills\u002F` as a directory while there is one noun, well\n  under ~1000 lines, and no merge friction. The tree is anticipating nouns you\n  have not discovered.\n\n**Nested folders holding one or two small files**\n: `pacts\u002Finvoices\u002Fissue\u002Fcreate.py` when `pacts\u002Finvoices\u002Fissue.py` would do, or\n  `inits\u002Fservices\u002Finvoices\u002Fissuing.py` with no sibling. A folder needs at least\n  two leaves to exist.\n\n**Port axis inside mills or specs**\n: `mills\u002Fweb\u002Fproposals.py` or `specs\u002Fapi\u002F...`. Mills and specs have no\n  delivery-mechanism axis. If you see a port word inside these layers, the code\n  belongs elsewhere.\n\n**A catch-all verb module**\n: `manage.py`, `organize.py`, `misc.py` inside a noun. A verb cut must name a\n  real activity.\n\n**A noun axis inside gates**\n: `gates\u002Fweb\u002Fdjango\u002Finvoices.py` when the interface has no such page. Gates\n  mirror the interface; mills mirror the domain. The two trees are not expected\n  to match.\n\n**`common`, `shared`, `utils`, or `entities` as a module or folder name**\n: Magnets for unrelated code. Each says where a file sits, not what it holds,\n  so anything can be filed there and nothing can ever be found. Shared types go\n  to `pacts`, under the noun that needed them first; everything else takes a\n  name from the axis it belongs to. The exception is a real concept that\n  happens to carry the word — a `DOMEntity` in a browser-port adapter earns\n  `entities.py`; a bag of dataclasses does not.\n\n### pacts\n\n**pacts split by technical kind instead of noun**\n: `pacts\u002Fdtos.py`, `pacts\u002Fprotocols.py`, `pacts\u002Frepos\u002F`. These group by what\n  the type *is*, not by what domain concern it belongs to. This forces\n  unrelated nouns to share files and makes the package harder to navigate.\n\n**`pacts\u002Fcore.py`, `pacts\u002Fcommon.py`, or similar**\n: A `common\u002F` bucket wearing a nicer name. Every contract has a principled home\n  under the noun \u002F port \u002F wiring axes.\n\n**A DTO that cannot be built from a store row or ORM instance**\n: Repositories cannot return it. Whatever the project uses for DTOs — Pydantic\n  is not required — construction has to work from the row the adapter loaded.\n  A row that does not match the DTO is mapped in the repository, not by a\n  method on the DTO.\n\n**A protocol implementation that does not name the protocol as a base class**\n: The conformance check is left to a structural match that can silently drift.\n  The exception is very generic structural protocols — `TransactionProtocol`,\n  callbacks — with multiple unrelated duck-typed implementations.\n\n### specs\n\n**specs imported from links, gates, or inits**\n: `specs` are business invariants, and business rules are enforced in `mills`\n  alone. A constant needed elsewhere is either a contract (`pacts` — a max\n  length or an allowed range is a fact about the shape of the data, and belongs\n  beside the contract it constrains) or configuration, which enters at `inits`\n  or comes from the framework's settings accessor where there is one.\n\n**specs reading from `os.environ` or `settings`, or performing IO**\n: It is a constants layer. Environment-dependent values enter at `inits`.\n\n### mills\n\n**mills importing anything with side effects**\n: An ORM, HTTP machinery, a CLI parser, settings access — absolute violation.\n  Pure computation is fine wherever it comes from. So is the ambient stuff the\n  rule was never about: the clock, a random draw, a UUID, a log line.\n\n**A service taking a whole Unit of Work instead of the protocols it uses**\n: Applies to ambient-ORM projects. With a session-based ORM the session already\n  is a unit of work, and injecting it is idiomatic.\n\n**A page axis inside mills**\n: Gates mirror the interface, mills mirror the domain. A sitemap in `mills` is\n  the interface leaking inward.\n\n### links\n\n**Model and repository in the same links file**\n: This collapses the internal-vs-public boundary. Models are internal to the\n  adapter; repositories are its public surface.\n\n**links files named per entity**\n: `links\u002Fdb\u002Fpostgres\u002Fuser.py`. `links` slices by kind, not by entity. One\n  `models.py` holds many entities' models.\n\n**Suffix-sibling links files**\n: `repositories_invoices.py`, `models_users.py`. Promote the kind to a\n  `{kind}\u002F` package with submodules instead. Halve, don't shard.\n\n**A links facade that re-exports models, or omits a public repository**\n: `links\u002F{port}\u002F{adapter}\u002F__init__.py` *is* the public surface. Whatever it\n  exports is public; everything else is internal.\n\n**An ORM model imported from outside links\u002F**\n: Use the repository protocol from `pacts` instead.\n\n**A repository imported directly in a gate or a mill**\n: Inject it through `inits`.\n\n### gates\n\n**A gate importing project code other than `pacts`**\n: An ORM model, a repository class, a service class from `mills` — all the same\n  violation. A gate calls services through their protocols. If none exists,\n  create one — a mill in `mills`, a protocol in `pacts`, a leaf in\n  `inits\u002Fservices.py` — before writing the gate.\n\n**A gate returning ORM instances to templates or serializers**\n: Return DTOs from `pacts`. ORM instances never leave `links`.\n\n**A gate that opens a transaction**\n: Atomicity is a service concern.\n\n**Business rules in form validation**\n: Gates validate format — an email, an int, a date. Meaning (\"email or username\n  required\", seat limits) belongs in mills, which alone may read `specs`.\n\n**A non-port axis at the top level of gates**\n: `gates\u002Fmills\u002F...`. The first axis below `gates` is always the port.\n\n### inits\n\n**A gate constructing repository or service instances**\n: That is the wiring `inits` owns, and doing it in a gate breaks it.\n\n**mills importing from inits**\n: The dependency runs the other way. `inits` knows the concrete classes;\n  `mills` sees only protocols.\n\n**inits containing business logic**\n: It should only wire, never decide.\n",{"name":5,"description":7},"main","42c3c2e0402a6275d6339c696566d716fb60bee9","skills\u002Fglimpse","https:\u002F\u002Fgithub.com\u002Ffancysnake\u002Fglimpse-architecture","https:\u002F\u002Fgithub.com\u002Ffancysnake","https:\u002F\u002Ffancysnake.dev","Human Sorcerer\u002FBard","2026-11-12 03:05:44",{"source":8,"skillName":5,"commands":57,"prompt":67,"zipAvailable":68},[58,61,64],{"tool":59,"command":60},"npx","npx skills add fancysnake\u002Fglimpse-architecture --skill glimpse",{"tool":62,"command":63},"bunx","bunx skills add fancysnake\u002Fglimpse-architecture --skill glimpse",{"tool":65,"command":66},"pnpm","pnpm dlx skills add fancysnake\u002Fglimpse-architecture --skill glimpse","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Ffancysnake\u002Fglimpse-architecture\u002Fblob\u002Fmain\u002Fskills\u002Fglimpse\u002FSKILL.md",true,[],[]]