From 0e5141984f0125a064f56965bf9864d3ceb36616 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev <66906831+braboj@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:15:55 +0300 Subject: [PATCH 1/2] refactor(oop): make object-oriented guidance opt-in (#2087) base-oop leaves the core tier, so chains without classes stop carrying it; a project declares it in Extras. The GoF catalogue becomes one rule, and the AOP-framework ban becomes a rule to keep cross-cutting behaviour visible. ADR-057 records the decision and supersedes ADR-046. Co-Authored-By: Claude Opus 5.5 --- docs/architecture/05-building-block-view.md | 3 +- ...object-oriented-design-guidance-is-core.md | 6 +- ...ject-oriented-design-guidance-is-opt-in.md | 84 +++++++++++++ docs/migrating-to-v3.md | 1 + templates/core/oop.md | 110 +++++++----------- templates/manifest.yaml | 4 +- tests/baselines/template-quality.txt | 2 +- 7 files changed, 136 insertions(+), 74 deletions(-) create mode 100644 docs/decisions/057-object-oriented-design-guidance-is-opt-in.md diff --git a/docs/architecture/05-building-block-view.md b/docs/architecture/05-building-block-view.md index 8a1d0553..5f1e41c6 100644 --- a/docs/architecture/05-building-block-view.md +++ b/docs/architecture/05-building-block-view.md @@ -60,9 +60,8 @@ 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: 11 `[DEPENDS ON]` tokens across 7 templates name one of these +Measured: 10 `[DEPENDS ON]` tokens across 7 templates name one of these files, and 7 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 diff --git a/docs/decisions/046-object-oriented-design-guidance-is-core.md b/docs/decisions/046-object-oriented-design-guidance-is-core.md index 2f14f26b..755d2f27 100644 --- a/docs/decisions/046-object-oriented-design-guidance-is-core.md +++ b/docs/decisions/046-object-oriented-design-guidance-is-core.md @@ -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 diff --git a/docs/decisions/057-object-oriented-design-guidance-is-opt-in.md b/docs/decisions/057-object-oriented-design-guidance-is-opt-in.md new file mode 100644 index 00000000..da9791d8 --- /dev/null +++ b/docs/decisions/057-object-oriented-design-guidance-is-opt-in.md @@ -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. diff --git a/docs/migrating-to-v3.md b/docs/migrating-to-v3.md index d12d1498..97fce30a 100644 --- a/docs/migrating-to-v3.md +++ b/docs/migrating-to-v3.md @@ -59,6 +59,7 @@ receives nothing (ADR-051). | `quality-exemption-doc-duty`, the exit-3 and readings rules, the immutable-corpus sweep | `base-quality` | this repository's playbook | | Language porting rules; `inspect.getattr_static`; Python `__init__` and extras wording | `base-quality` | removed or neutral | | The eight-stage pipeline order, its push and tag triggers, the three-environment minimum, the post-publish deploy hook | `base-cicd` | removed; the gates, packaging once, DAST before production and artifact promotion remain | +| `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 | diff --git a/templates/core/oop.md b/templates/core/oop.md index d9237c22..96ddb967 100644 --- a/templates/core/oop.md +++ b/templates/core/oop.md @@ -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 diff --git a/templates/manifest.yaml b/templates/manifest.yaml index d63d95e3..fdc9cacf 100644 --- a/templates/manifest.yaml +++ b/templates/manifest.yaml @@ -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 @@ -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 diff --git a/tests/baselines/template-quality.txt b/tests/baselines/template-quality.txt index f8ab5f40..98945061 100644 --- a/tests/baselines/template-quality.txt +++ b/tests/baselines/template-quality.txt @@ -25,7 +25,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 From c8d0bfeaa24de733a663bc86d302b459c62505d1 Mon Sep 17 00:00:00 2001 From: Branimir Georgiev <66906831+braboj@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:18:03 +0300 Subject: [PATCH 2/2] docs(changelog): note #2104 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 939946d9..77326963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ Releases up to 2.63.0 are listed in check-harness rules ([#2082]) - The git template drops its submodule, numbered-document and base64 rules and the .vscode allowlist ([#2093]) +- Object-oriented design rules leave the core tier; declare base-oop in Extras to + keep them ([#2104]) ### Added @@ -197,6 +199,7 @@ Releases up to 2.63.0 are listed in [#2096]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2096 [#2097]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2097 [#2098]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2098 +[#2104]: https://github.com/solid-ai-dev/solid-ai-templates/pull/2104 ## [2.90.0] - 2026-09-12