Skip to content

Architecture

Ferran Buireu edited this page Oct 18, 2026 · 11 revisions

Architecture

The action follows a Domain-Driven Design(ish) layering with a Functional Core, Imperative Shell pattern: the domain is pure, one ubiquitous language spans the whole tree, how code is written is one CODING_STANDARDS.md, and each layer documents what it does in a colocated AGENTS.md. The dependency direction is strict and enumerated rather than conventional: the core knows nothing about GitHub, git, the filesystem or SMTP, and everything points inward toward it.

---
config:
  look: handDrawn
  layout: dagre
---
flowchart RL
    entry["index.ts"] --> application
    application --> config
    application --> domain
    application --> i18n
    application --> infrastructure
    application --> presentation
    config --> domain
    config --> i18n
    infrastructure --> config
    infrastructure --> domain
    infrastructure --> i18n
    presentation --> config
    presentation --> domain
    presentation --> i18n
    domain --> i18n
    application --> shared
    config --> shared
    infrastructure --> shared
    shared -.->|test fixtures only| config
    shared -.->|test fixtures only| domain
Loading

Every arrow above is an import a layer is allowed to make, whether or not its source makes it; anything not drawn is forbidden, bar the test exceptions ARCHITECTURE.md names. The two dashed arrows are the fixture factories in shared/tests, which read DEFAULTS and the Config and domain types and are imported by *.test.ts files only.

Each layer documents what it does, and what bites while changing it, in a colocated AGENTS.md; the rules for how code is written anywhere in the tree are CODING_STANDARDS.md.


Layers

Layer Role Side effect
index.ts Calls trackStars() at module load Starts the run
domain Pure business core: comparison, snapshots, the Tracked Set, forecast, velocity, stargazer diffing, star-history reconstruction, formatting None
presentation Pure rendering: data in, markdown / HTML / SVG / CSV / badge string out None
i18n Locale bundles and interpolation. A true leaf: it imports no other layer, only its own JSON None
config Action inputs plus star-tracker.yml resolved into one typed Config Reads the inputs and one YAML file
infrastructure Everything outbound: the GitHub REST API, the git CLI, the Data Branch worktree, SMTP Network, child_process, filesystem
application Sequences the single use case, trackStars() Writes the Action log and the action outputs
shared Cross-cutting code no layer owns: errorMessage and the schema-issue wording, plus the test fixtures used from *.test.ts only None

assets/ sits beside them and is not a layer: it holds the brand files the README embeds, imports nothing and is imported by nothing.


What the (ish) means

DDD applied where it pays, not by the book. The method splits in two, and only one half is negotiable.

The strategic half does the real work and is taken whole. The tactical half, the catalogue of value objects, aggregates, repositories and events, asks for an abstraction before the code has earned it; taken on faith it adds boilerplate that ends up hiding the very rules it was meant to protect. It is adopted here only where a concrete force calls for it.

The strategic ideas the design leans on:

One ubiquitous language. GLOSSARY.md defines every domain word (Snapshot, Baseline Snapshot, Delta, Tracked Set, Covered Stars, Delivery) and, for each, lists the synonyms it displaces, so a near-miss word cannot drift in. A term means the same thing in the star mathematics, in a chart title, in an email and on this page.

A domain that performs no I/O: domain, presentation and i18n reach no network, no filesystem and no clock beyond an injectable now, which is what lets the arithmetic and every rendered artefact be exercised on plain values with no GitHub API, git or SMTP anywhere near the test.

The rest of the tactical catalogue is taken only where it fits. There are no bounded contexts, because there is one language and one use case. There are no aggregates, because no value here has a lifecycle to guard: each is built once per run and never updated in place. The Repository pattern is present in shape, as the single data-branch facade, without the vocabulary or the interface-with-one-implementer that usually travels with it. Value objects are decided per concept rather than by default.

Neither half of that is a matter of taste. The boundaries are asserted by a test that reads the layer table as data, and the vocabulary is what the glossary and the per-layer guides are for. ADR 0004 records the reasoning pattern by pattern, and ADR 0022 the one question it cannot answer once for the whole tree, when a bare string or number earns a type of its own.


Where the detail lives

This page is the shape, not the rules. The normative statement of which layer may import which is the layer table in ARCHITECTURE.md, and a test reads that table as data and asserts it against every import in src, so the arrows above cannot quietly stop being true. Restating the import rules here would be another copy that nothing checks.

Question Where
What does this domain word mean, and what does it displace? GLOSSARY.md
Which layer may import which, and what runs in what order? ARCHITECTURE.md
Why is the tree layered at all, and what did the (ish) drop? ADR 0004
When does a bare string or number earn a type of its own? ADR 0022
What does one layer actually guarantee? The AGENTS.md inside that layer's folder
How is code written here, and what does a review hold a change to? CODING_STANDARDS.md
What happens, step by step, on a run? How It Works
Why these tools and these dependencies? Technical Stack

Clone this wiki locally