Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ Releases up to 2.63.0 are listed in
template ([#2102])
- The frontend-static-site template is retired; its theme, trailing-slash and
404 rules move to frontend-quality and frontend-content ([#2103])
- Object-oriented design rules leave the core tier; declare base-oop in Extras to
keep them ([#2104])

### Added

Expand Down Expand Up @@ -212,6 +214,7 @@ Releases up to 2.63.0 are listed in
[#2101]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2101
[#2102]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2102
[#2103]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2103
[#2104]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2104

## [2.90.0] - 2026-09-12

Expand Down
7 changes: 3 additions & 4 deletions docs/architecture/05-building-block-view.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,14 @@ The core tier, as the manifest states it today:
- `templates/core/docs.md`
- `templates/core/testing.md`
- `templates/core/review.md`
- `templates/core/oop.md`

Measured: 8 `[DEPENDS ON]` tokens across 5 templates name one of these
files, and 5 manifest entries name a core id in `depends_on`. Declaring one
is redundant rather than forbidden — the seeding reaches the file either
way, and the directive documents a dependency a reader would otherwise
infer. Reached by the seeding alone: `base-review`, `base-oop`. A chain
assembled by walking the directives does not reach that set and reports
nothing about it, because nothing declares what is absent.
infer. Reached by the seeding alone: `base-review`. A chain assembled by
walking the directives does not reach that set and reports nothing about it,
because nothing declares what is absent.
<!-- /generated:spec-core-tier -->

Every top-level list of entries except `core` and `fork_core` is a section
Expand Down
6 changes: 3 additions & 3 deletions docs/decisions/046-object-oriented-design-guidance-is-core.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
id: "046"
status: Accepted
date: 2026-09-11
status: Superseded
date: 2026-10-21
category: composition
supersedes: []
superseded_by: []
superseded_by: ["057"]
---

# ADR-046: Object-oriented design guidance is core
Expand Down
84 changes: 84 additions & 0 deletions docs/decisions/057-object-oriented-design-guidance-is-opt-in.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
id: "057"
status: Accepted
date: 2026-10-21
category: composition
supersedes: ["046"]
superseded_by: []
---

# ADR-057: Object-oriented design guidance is opt-in

## Context

`base-oop` sits in the core tier, so every chain loads it, including chains
whose code has no classes to design.

| Force | What it shows |
| ----- | ------------- |
| Reach | Measured on `main` on 2026-10-21: `templates/core/oop.md` resolved into all 41 roots, documentation-only and static-site roots among them |
| Fit | SOLID, class hierarchies and design patterns govern object-oriented code; a documentation repository or a static site applies none of it |
| Content | The file held a GoF catalogue and a categorical ban on AOP frameworks, preferences rather than rules every project needs |
| The freeze | From 3.0.0 core-tier membership is frozen until 4.0.0, so leaving the core is possible only before the tag |
| The earlier gap | The file once reached only service chains, by accident of the graph; library and service projects still need a way to load it |

## Decision

`base-oop` leaves the core tier and becomes an opt-in template that a
project declares through `Extras` or a stack names in `depends_on`.

## Rationale

- A chain whose code has no classes stops carrying rules it cannot apply,
which answers the reach and fit forces.
- A library or service project still loads the file by declaring one
template, so the earlier gap does not return by accident: reach becomes a
stated choice instead of a graph side effect.
- Doing it before 3.0.0 is the only window the freeze leaves.

## Design

1. **Membership**: the manifest's `core:` entry drops `base-oop`; its entry
stays under `base:` at `templates/core/oop.md`
2. **Reach**: a project declares `base-oop` in `Extras`; a stack for an
object-oriented language MAY name it in `depends_on`
3. **IDs**: `base-oop`, `base-oop-restraint` and `base-oop-aop` keep their
names; the AOP section is retitled "Cross-cutting concerns"
4. **Content**: the GoF catalogue gives way to one rule, use a named
pattern where it fits and name it in code; the AOP-framework ban becomes
a rule to keep cross-cutting behaviour visible at its call site

## Alternatives considered

| Alternative | Why not |
| ----------- | ------- |
| Keep it in core, trimmed | Every non-object-oriented chain still carries it, and the freeze then fixes that until 4.0.0 |
| Name it in each object-oriented stack's `depends_on` only | Upstream ships no stacks; a project without a fork stack would have no way in but `Extras`, which this design already gives |
| Drop the file | Library and service projects lose the only interface and abstraction rules the templates hold |

## Consequences

| Area | Change |
| ---- | ------ |
| Chains | Every chain that does not declare `base-oop` loses the file |
| Manifest | `core:` drops `base-oop` |
| Generated docs | The core-tier listing in the building-block view regenerates |
| Projects | A project that applies object-oriented design adds `base-oop` to `Extras` |
| Forks | A fork's object-oriented stacks add `base-oop` to `depends_on` |
| Migration guide | Lists the move |

## Compatibility

No section ID is renamed or removed. A chain that relied on the core tier
for these rules adds `base-oop` to `Extras`; a template extending or
overriding one of its sections adds it to `depends_on`. The change ships in
3.0.0, before the freeze, and the CHANGELOG lists it under `Breaking
changes`.

## Security

None.

## Related

- `templates/core/oop.md`, the file this record moves out of the core tier.
1 change: 1 addition & 0 deletions docs/migrating-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ receives nothing (ADR-051).
| `backend-quality`: N+1, async I/O, API deprecation, circuit breaker, outbox | `backend-quality` | already in `backend-database`, `backend-concurrency`, `backend-api`, `backend-errors`, `backend-messaging` |
| `frontend-static-site`: one stylesheet, BEM naming, system fonts, the `localStorage` theme snippet, the page table, lint and format files | `frontend-static-site` | removed with the template; a fork's static-site stack carries its choices |
| `static-site-theme`, `static-site-trailing-slash`, the custom 404 page, build-time rendering by default | `frontend-static-site` | `frontend-quality` and `frontend-content`, IDs kept |
| `base-oop`: SOLID, object design, patterns, cross-cutting concerns | the core tier | opt-in by `Extras`; the GoF catalogue and the AOP-framework ban are cut |
| The numbered-document check after concurrent merges, the first-CI-run rule for a check on its own change | `base-git` | this repository's playbook; the `check-heading-numbers` hook still ships |
| The submodule update after a pin bump, the base64 bundle mask, the `.vscode` allowlist | `base-git` | removed; a neutral editor-settings rule remains |
| Factory classmethods; `*-overlay.png` naming | `base-oop`, `base-measurement` | neutral rules remain |
Expand Down
110 changes: 44 additions & 66 deletions templates/core/oop.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,89 +4,67 @@

## SOLID principles

Apply SOLID at the class, module, and service level:
Apply SOLID at the class, module and service level:

- **S — Single Responsibility**: every class or module has exactly one reason
to change; split anything that serves more than one concern
- **O — Open/Closed**: extend behaviour by adding new code, not by modifying
existing code; use interfaces, abstract base classes, or composition
- **L — Liskov Substitution**: subtypes must be fully substitutable for their
base type without altering correctness; never override a method in a way
that weakens its contract
- **I — Interface Segregation**: prefer many small, focused interfaces over
one large general-purpose one; callers should not depend on methods they
do not use
- **D — Dependency Inversion**: depend on abstractions, not concretions;
inject dependencies rather than instantiating them inside a class
- **Single responsibility**: give each class or module one reason to
change; split one that serves two concerns
- **Open/closed**: extend behaviour by adding code, not by editing it,
through interfaces, abstract base classes or composition
- **Liskov substitution**: keep every subtype substitutable for its base;
never override a method so that it weakens the contract
- **Interface segregation**: prefer small, focused interfaces; never make a
caller depend on methods it does not use
- **Dependency inversion**: depend on abstractions, and inject dependencies
rather than constructing them inside a class

## OOP
## Object design

- Prefer **composition over inheritance** — inherit only to model a true
is-a relationship; compose for code reuse
- **Encapsulate** implementation details — expose behaviour through a public
interface, hide state and implementation
- Design to interfaces (or protocols / abstract base classes), not concrete
- Prefer composition over inheritance; inherit only for a true is-a
relationship
- Encapsulate state and implementation; expose behaviour through a public
interface
- Design to interfaces, protocols or abstract base classes, not concrete
types
- Keep class hierarchies shallow — more than two levels of inheritance is a
signal to refactor towards composition
- **Closed type families are tagged unions** — where every variant is
known when the type is written, discriminate a union by a literal field
(`kind`, `type`) so the compiler checks every branch handles every
variant; keep class hierarchies for families callers extend
- **Internal identifiers must align with their layer** — a module or class
whose name implies a role it does not hold (an `endpoints.py` with no
routes, a `*RestApi` class that is not the REST API) misleads readers and
forces clarifying asides; when the layering and the name disagree, rename
to match the layer
- Treat a hierarchy deeper than two levels as a signal to refactor toward
composition
- Model a closed type family, every variant known when the type is
written, as a tagged union on a literal field (`kind`, `type`), so the
compiler checks every branch
- Keep class hierarchies for families callers extend
- Name a module or class for the role its layer gives it; rename an
`endpoints.py` with no routes or a `*RestApi` that is not the REST API

## When not to reach for a class
[ID: base-oop-restraint]

SOLID and the patterns above bias toward classes; this is the
counterweight. Reach for a class to encapsulate state, not to house
stateless logic.
SOLID and the patterns below bias toward classes; these rules are the
counterweight.

- **Prefer a free function for stateless logic** — introduce a class
only to encapsulate state or attach behaviour to state it owns. A pure
transform (inputs → output, no retained state) stays a function; a
class only couples it to state it does not use
- **A class whose only public method is `run()` / `execute()` is usually
a function in disguise** — keep the free function unless the retained
state genuinely warrants an object
- **Put state-mutating behaviour on the object that owns the state** —
avoid the anemic model where an external function mutates a data bag;
this is the active-voice complement of "encapsulate state, expose
behaviour" above
- **Construct through named constructors or factory methods on the type**
rather than free functions that build and return it
- Write stateless logic as a free function; introduce a class only to
encapsulate state it owns
- Treat a class whose only public method is `run()` or `execute()` as a
function in disguise, unless its retained state warrants an object
- Put state-mutating behaviour on the object that owns the state, never in
an external function that mutates a data bag
- Construct through named constructors or factory methods on the type,
not free functions that build and return it

## Design patterns

- Apply established **GoF design patterns** where they fit the problem —
do not invent ad-hoc solutions for problems that have named solutions
- Favour **behavioural patterns** for algorithm variation:
Strategy, Command, Observer, Template Method
- Favour **structural patterns** for object composition:
Adapter, Decorator, Facade, Proxy
- Use **creational patterns** to decouple object creation:
Factory Method, Abstract Factory, Builder
- Use an established pattern where it fits the problem; never invent an
ad-hoc solution to a problem that has a named one
- Use **Singleton** only for stateless services or infrastructure objects
(logger, config) — never for mutable shared state
- Name the pattern in code when you use one: a class named
`OrderExportStrategy` communicates intent; a class named `OrderHelper`
does not

## Aspect-Oriented Programming (AOP)
## Cross-cutting concerns
[ID: base-oop-aop]

- **Do not use AOP frameworks** — hidden cross-cutting behaviour (method
interception, bytecode weaving, runtime proxies) makes code hard to read,
debug, and test
- Implement cross-cutting concerns explicitly:
- Logging: call the logger directly in the function
- Auth: explicit middleware or guard in the call chain
- Transactions: explicit context manager or decorator with visible
call site
- Validation: explicit call at the boundary
- Transparent decorators (a decorator that wraps and clearly delegates) are
acceptable; opaque interceptors that inject hidden behaviour are not
- Keep cross-cutting behaviour visible at its call site: a logger call, an
auth middleware or guard, a transaction context manager, a validation
call at the boundary
- Never inject behaviour through method interception, bytecode weaving or
an opaque runtime proxy — it is hard to read, debug and test
- Use a decorator only when it wraps and visibly delegates
4 changes: 2 additions & 2 deletions templates/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

version: "1.0"

core: [base-quality, base-git, base-docs, base-testing, base-review, base-oop]
core: [base-quality, base-git, base-docs, base-testing, base-review]

base:
- id: base-git
Expand Down Expand Up @@ -74,7 +74,7 @@ base:
depends_on: [base-data-modeling]
- id: base-oop
file: templates/core/oop.md
description: SOLID, OOP, GoF design patterns, AOP guidance
description: SOLID, object design, design patterns, cross-cutting concerns; opt-in
- id: base-quality-gates
file: templates/core/quality-gates.md
description: How a check is built - retrofitting a gate, pairing a constraint with its check, reading a verdict
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/template-quality.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ templates/core/docs.md 99.1
templates/core/embedded-checks.md 83.6
templates/core/git.md 89.4
templates/core/measurement.md 95.2
templates/core/oop.md 82.8
templates/core/oop.md 96.0
templates/core/quality-gates.md 92.5
templates/core/quality.md 98.7
templates/core/review.md 100.0
Expand Down
Loading