Distribution
distribution is a leaf: it depends on kernel only, and knows no tool and no manifest. It
answers exactly one question — where does content come from, and how is it fetched — for
whoever asks. framework is the only context that reaches it (framework → distribution); a
plugin's own content and how it gets translated are someone else's job once it has arrived here.
What goes in
| Concept | Location |
|---|---|
| A marketplace registration (name, source, scope, staleness) | domain/marketplace.ts, domain/marketplace-source-mode.ts |
| A cached catalog fetch | domain/ports/marketplace-cache.ts |
| The plugin catalog shape | domain/catalog.ts (the Claude-shaped parser lives here too) |
| A reader for a non-Claude catalog shape | domain/catalog-parsers/ |
| A port this context's callers hold | domain/ports/ — registry, cache, trust-store, catalog-repository, fetcher, raw-catalog-fetcher |
| Add / list / refresh / register / resolve / fetch a marketplace source | application/ |
| The concrete adapter behind one of the six ports | infrastructure/ |
How
- This context is a leaf by construction: it must never gain an edge to
tools,translate, orframework—tests/architecture/context-graph.arch.test.tsenforces the chain (framework → distribution, plus everything tokernel) and fails the build the moment a new edge appears. If a change seems to need one, the orchestration belongs to the caller (framework), not here — seeBASELINEintests/architecture/helpers.tsfor the one documented exception,distribution->framework, and the comment there that explains it (marketplace add --overwriteremoving before adding), which is framework work that has not yet been moved out. - A port here follows the port/adapter rule in
.claude/rules/00-architecture/: interface only, ≤5 methods, nonullin the return type unless "not found" is genuinely a normal domain state (documented per-port, not assumed). - An adapter owns every technical constant for its integration (API base URLs, cache TTLs,
error-pattern regexes for classifying a third-party failure) — none of that belongs in a port,
a use-case, or a domain model.
try/catchinside an adapter exists only to translate a raw error into a typed one fromkernel/errors.ts. - A new foreign catalog shape gets its own parser in
domain/catalog-parsers/, producing the samePluginCatalog/PluginCatalogEntryshape the Claude parser produces — callers above this context never branch on which format a catalog came from. - Follow the use-case rule in
.claude/rules/00-architecture/for the application layer's shape.
Public surface
Nothing outside contexts/distribution/ may import a module this context has not declared
public — tests/architecture/context-boundary.arch.test.ts holds the list
(PUBLIC_MODULES.distribution). Measured at extraction, ten modules were reached from outside
and not one was an adapter: the adapters are wired by the composition root
(runtime/wiring/distribution.ts) alone, and stay internal for that reason. A module that
exposes its own plumbing to a caller outside the composition root is not a leaf context anymore
— keep new adapters unreachable from outside.
How it's tested
tests/contexts/distribution/mirrorssrc/contexts/distribution/— domain models and application use-cases are unit-tier; adapters against a real temp filesystem or a mocked network boundary are integration-tier. See thetestskill for tier conventions.- A new catalog parser needs a fixture of the real foreign format and a test asserting the parsed
PluginCatalogmatches what the Claude-shaped parser would produce for an equivalent catalog.


