Skip to content

Commit 4090951

Browse files
brabojclaude
andauthored
feat(templates): state the language idioms as rules in core, checks stay with language/ (#1926) (#1963)
ADR-050 rule 4 is rewritten in place: core carries no language-specific code, because a Python check fails the waist test and grows every chain. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 07168f3 commit 4090951

24 files changed

Lines changed: 413 additions & 30 deletions

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ alters no template carries no entry.
2323

2424
### Added
2525

26+
- `base-quality` states three language idioms as rules: a library writes
27+
nothing to the host's streams (`quality-library-output`), an optional
28+
dependency sits behind one module, and a test walks every raise.
29+
`base-oop` adds tagged unions for closed type families.
2630
- `templates/fixture/` adds `stack-fixture` and `platform-fixture`, test
2731
input marked `fixture: true` in the manifest. Listings and `generated/`
2832
omit them; per-stack checks select by the manifest's `stacks:` key, and

‎README.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,12 @@ effective; declining a new rule needs no ADR or ticket. See the
123123
<!-- generated:readme-model-limits -->
124124
| Stack category | Stacks | Largest chain | Prompt | Min context |
125125
|----------------|--------|---------------|--------|-------------|
126-
| abstract | 2 | `stack-python-service` — 556K chars | ~159K tokens | 200K |
127-
| backend | 8 | `stack-django` — 570K chars | ~163K tokens | 200K |
128-
| embedded | 1 | `stack-c-embedded` — 363K chars | ~104K tokens | 128K |
129-
| hypermedia | 1 | `stack-htmx` — 382K chars | ~109K tokens | 128K |
130-
| library | 3 | `stack-python-lib` — 486K chars | ~139K tokens | 200K |
131-
| static | 2 | `stack-tutorial` — 520K chars | ~149K tokens | 200K |
126+
| abstract | 2 | `stack-python-service` — 557K chars | ~159K tokens | 200K |
127+
| backend | 8 | `stack-django` — 572K chars | ~163K tokens | 200K |
128+
| embedded | 1 | `stack-c-embedded` — 364K chars | ~104K tokens | 128K |
129+
| hypermedia | 1 | `stack-htmx` — 383K chars | ~109K tokens | 128K |
130+
| library | 3 | `stack-python-lib` — 487K chars | ~139K tokens | 200K |
131+
| static | 2 | `stack-tutorial` — 521K chars | ~149K tokens | 200K |
132132
<!-- /generated:readme-model-limits -->
133133

134134
Measured rather than estimated. Each row takes the largest resolved

‎docs/decisions/050-what-stays-upstream.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,19 @@ every answer to those questions moves to forks.
5353

5454
## Design
5555

56-
![Upstream keeps the engine, shared policy, idioms as checks and the questions; each fork carries stacks, platforms, workflow, process and the answers; git.md, docs.md and quality-gates.md split between the two](../diagrams/template-boundary.png)
56+
![Upstream keeps the engine, shared policy, language-neutral rules and the questions; each fork carries stacks, platforms, workflow, process and the answers; git.md, docs.md and quality-gates.md split between the two](../diagrams/template-boundary.png)
5757

5858
| # | Rule | In practice |
5959
| - | ---- | ----------- |
6060
| 1 | The waist test | A rule stays upstream only if changing the stack, the platform or the team's way of working would not change it |
6161
| 2 | Engine upstream | The manifest schema, IDs, EXTEND and OVERRIDE, the resolver, `tools/`, `tests/`, and a fixture stack and platform as test input |
6262
| 3 | Shared policy upstream | `core/`: config, oop, quality, testing, readme, the review checklists, and `agents.md` as the output contract; `security/`, `data/`, `infra/`, `backend/` and `frontend/` |
63-
| 4 | Language idioms as checks | Each idiom becomes the language-specific check beside its generic rule in `core/`, and `language/` leaves upstream (#1926) |
63+
| 4 | Rules, not language checks | `core/` states each idiom's rule language-neutrally and carries no language-specific code; the check moves with `language/`, which leaves upstream whole (#1926) |
6464
| 5 | Governance asks, forks answer | Upstream asks who approves a change, how work is tracked and what a release is, and answers none of them. Labels, milestones, merge and review policy, the journal, wrap-up and the release cut move to forks |
6565
| 6 | Git splits | Upstream keeps never force-push, never rebase a pushed branch, one concern per pull request, committer identity, submodules, off-limits paths and `.gitignore`. Branch prefixes, commit and title form, squash policy, versioning and the release process move |
6666
| 7 | Decision records stay upstream | The ADR threshold, the record layout, the immutability rules and the schema and layout checks; a fork sets only its categories and the record its layout binds from |
6767
| 8 | Workflow moves | All of `workflow/` moves, `release.md` included. From `quality-gates.md`, how a check is built stays; when gates run moves: the three-layer gate model, thresholds and periodic audits |
68-
| 9 | Ends move | `stack/`, `platform/`, `INTERVIEW.md`, `examples/`, `generated/`, `backend/templating.md`, the `language/` Tooling sections, `go.md`, `c.md`, and `core/` cli, skills and examples |
68+
| 9 | Ends move | `stack/`, `platform/`, `INTERVIEW.md`, `examples/`, `generated/`, `backend/templating.md`, `language/`, and `core/` cli, skills and examples |
6969
| 10 | Concern directories | Upstream drops `base/`: `core/`, `security/`, `data/` and `infra/` sit beside `backend/` and `frontend/` under `templates/` (#1504) |
7070
| 11 | IDs never change | A file or section keeps its ID when it moves or splits, so every EXTEND and OVERRIDE still resolves; a split file's moved sections go to a fork-owned file |
7171
| 12 | The audit is the starting list | Each section not named above is placed by rule 1; the audit's per-section verdicts are the starting list, checked against the test as the path move lands (#1480) |
@@ -74,7 +74,7 @@ every answer to those questions moves to forks.
7474
| ---- | -------------------- | ----------- |
7575
| 1, 12 | Mechanism | Review of the path-move pull request (#1504) |
7676
| 2, 3, 6, 7 | Promise to projects and forks | Review of the split commit; the ownership check then keeps forks from editing these keys (#1923) |
77-
| 4 | Promise to projects | The idioms fold (#1926) |
77+
| 4 | Promise to projects | The generic rules land in `core/` (#1926) |
7878
| 5, 8, 9 | Promise to forks | The split commit and the migration guide (#1292) |
7979
| 10, 11 | Promise to projects and forks | The path move: every root resolves the same ordered ID list before and after (#1504) |
8080

@@ -98,6 +98,7 @@ leaves it for the project or its fork to fill in:
9898
| Move the whole git policy to forks | The history-safety rules hold for every team, and a project with no fork would lose them. |
9999
| Move decision records to forks | Every fork would rebuild the same threshold, layout and check. |
100100
| Split by whole file | Three files mix policy and process, so either half would land on the wrong side. |
101+
| Fold each language check into `core/` beside its rule | A Python check changes with the stack, so it fails rule 1, and language code in `core/` grows every chain, the non-Python ones included. |
101102

102103
## Consequences
103104

‎docs/diagrams/template-boundary.drawio‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
<mxCell id="g2" parent="1" style="rounded=1;arcSize=2;whiteSpace=wrap;html=1;fillColor=#F8FAFC;strokeColor=#CBD5E1;strokeWidth=1.5;align=left;verticalAlign=top;spacingLeft=16;spacingTop=10;fontFamily=Segoe UI;fontSize=14;fontColor=#334155;fontStyle=1;" value="Each fork" vertex="1"><mxGeometry x="750" y="100" width="660" height="592" as="geometry" /></mxCell>
1111
<mxCell id="u0" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#FEF3C7;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#B45309;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Engine&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#92400E&#x27;&gt;manifest schema, IDs, EXTEND and OVERRIDE, resolver,&lt;br&gt;tools/, tests/, a fixture stack and platform&lt;/span&gt;" vertex="1"><mxGeometry x="60" y="150" width="630" height="62" as="geometry" /></mxCell>
1212
<mxCell id="u1" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#FEF3C7;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#B45309;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Shared engineering policy&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#92400E&#x27;&gt;core/: config, oop, quality, testing, readme, review checklists, agents.md&lt;br&gt;security/, data/, infra/, backend/, frontend/&lt;/span&gt;" vertex="1"><mxGeometry x="60" y="226" width="630" height="62" as="geometry" /></mxCell>
13-
<mxCell id="u2" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#FEF3C7;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#B45309;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Language idioms, as checks&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#92400E&#x27;&gt;each beside its generic rule in core/&lt;/span&gt;" vertex="1"><mxGeometry x="60" y="302" width="630" height="62" as="geometry" /></mxCell>
13+
<mxCell id="u2" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#FEF3C7;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#B45309;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Language idioms, as rules&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#92400E&#x27;&gt;stated in core/ with no language code&lt;/span&gt;" vertex="1"><mxGeometry x="60" y="302" width="630" height="62" as="geometry" /></mxCell>
1414
<mxCell id="u3" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#FEF3C7;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#B45309;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Questions every project answers&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#92400E&#x27;&gt;who approves a change, how work is tracked,&lt;br&gt;what a release is&lt;/span&gt;" vertex="1"><mxGeometry x="60" y="378" width="630" height="62" as="geometry" /></mxCell>
1515
<mxCell id="u4" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#FEF3C7;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#B45309;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;dashed=1;dashPattern=6 4;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;git.md: safety&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#92400E&#x27;&gt;never force-push, never rebase a pushed branch,&lt;br&gt;one concern per pull request&lt;/span&gt;" vertex="1"><mxGeometry x="60" y="454" width="630" height="62" as="geometry" /></mxCell>
1616
<mxCell id="u5" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#FEF3C7;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#B45309;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;dashed=1;dashPattern=6 4;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;docs.md: records and writing&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#92400E&#x27;&gt;decision records, writing style, README, generated files&lt;/span&gt;" vertex="1"><mxGeometry x="60" y="530" width="630" height="62" as="geometry" /></mxCell>
1717
<mxCell id="u6" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#FEF3C7;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#B45309;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;dashed=1;dashPattern=6 4;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;quality-gates.md: building a check&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#92400E&#x27;&gt;freeze instances, pair a constraint with its check,&lt;br&gt;run the check in the form it ships&lt;/span&gt;" vertex="1"><mxGeometry x="60" y="606" width="630" height="62" as="geometry" /></mxCell>
1818
<mxCell id="f0" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#F1F5F9;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#475569;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Stacks and platforms&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#475569&#x27;&gt;stack/, platform/, INTERVIEW.md,&lt;br&gt;examples/, generated/, backend/templating.md&lt;/span&gt;" vertex="1"><mxGeometry x="780" y="150" width="600" height="62" as="geometry" /></mxCell>
1919
<mxCell id="f1" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#F1F5F9;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#475569;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Workflow&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#475569&#x27;&gt;workflow/: ai-workflow, 360, scope, issues, communication,&lt;br&gt;compression, two-pass-review, release&lt;/span&gt;" vertex="1"><mxGeometry x="780" y="226" width="600" height="62" as="geometry" /></mxCell>
20-
<mxCell id="f2" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#F1F5F9;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#475569;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Process and tooling&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#475569&#x27;&gt;core/: cli, skills, examples&lt;br&gt;language/: Tooling sections, go.md, c.md&lt;/span&gt;" vertex="1"><mxGeometry x="780" y="302" width="600" height="62" as="geometry" /></mxCell>
20+
<mxCell id="f2" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#F1F5F9;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#475569;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Process and tooling&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#475569&#x27;&gt;core/: cli, skills, examples&lt;br&gt;language/: every file, checks included&lt;/span&gt;" vertex="1"><mxGeometry x="780" y="302" width="600" height="62" as="geometry" /></mxCell>
2121
<mxCell id="f3" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#F1F5F9;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#475569;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;Answers to the questions&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#475569&#x27;&gt;labels, milestones, merge and review policy,&lt;br&gt;journal, wrap-up, release cut&lt;/span&gt;" vertex="1"><mxGeometry x="780" y="378" width="600" height="62" as="geometry" /></mxCell>
2222
<mxCell id="f4" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#F1F5F9;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#475569;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;dashed=1;dashPattern=6 4;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;git.md: workflow&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#475569&#x27;&gt;branch prefixes, commit and title form, squash,&lt;br&gt;versioning, release process&lt;/span&gt;" vertex="1"><mxGeometry x="780" y="454" width="600" height="62" as="geometry" /></mxCell>
2323
<mxCell id="f5" parent="1" style="rounded=1;arcSize=8;whiteSpace=wrap;html=1;fillColor=#F1F5F9;gradientColor=#FFFFFF;gradientDirection=north;strokeColor=#475569;strokeWidth=1.6;shadow=1;align=center;fontFamily=Segoe UI;fontColor=#0F172A;spacing=6;dashed=1;dashPattern=6 4;" value="&lt;b style=&#x27;font-size:13px;color:#0F172A&#x27;&gt;docs.md: process&lt;/b&gt;&lt;br&gt;&lt;span style=&#x27;font-size:11px;color:#475569&#x27;&gt;development journal, ONBOARDING and PLAYBOOK structure,&lt;br&gt;release changelog&lt;/span&gt;" vertex="1"><mxGeometry x="780" y="530" width="600" height="62" as="geometry" /></mxCell>
-1.02 KB
Loading

‎generated/stack-astro.md‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ that has since lifted reads exactly like a live one.
121121
- Corollary: if swapping the implementation would force a caller to
122122
change code, the name has leaked. Which library sits behind a tier
123123
changes without notice and is not the caller's problem
124+
- A tier whose dependency is heavy, native, or unavailable on some target
125+
platform MUST be reached through one module that owns its import, which
126+
re-raises with a message naming the extra and the command that installs
127+
it. The package MUST import without the extra, checked in a CI leg that
128+
does not install it
124129
- Avoid negative conditions in `if` statements where possible —
125130
`if isEnabled` reads better than `if !isDisabled`
126131

@@ -360,7 +365,8 @@ raise SystemExit(1 if findings else 0)
360365
the messages as class names and serves the tests, not the callers. The
361366
contract erodes one commit at a time and no linter reads it — one
362367
checks that exceptions are handled, never that they are declared — so
363-
the language file names the test that walks every raise
368+
a test walks every raise in the package and fails on one outside the
369+
hierarchy
364370
- **A caller-side input filter is not a precondition**: when a runner
365371
filters its inputs by a caller-side criterion (data availability,
366372
ground-truth presence, a feature flag) that is tighter than the inner
@@ -448,6 +454,17 @@ raise SystemExit(1 if findings else 0)
448454
full suite runs once — and a green run of the untouched suite plus a
449455
byte-identical output diff is what proves the split behaviour-neutral
450456
457+
## A library writes nothing to the host's streams
458+
[ID: quality-library-output]
459+
460+
- A library MUST NOT write to the host's standard output or error, and
461+
MUST NOT configure logging beyond its own logger; the host decides
462+
where output goes
463+
- The library's logger MUST be injectable, with its own module logger as
464+
the fallback
465+
- The silence MUST be checked in a fresh process: a test runner that
466+
installs its own handler hides the library's default
467+
451468
## A workaround comment is a half-filed defect report
452469
453470
[ID: quality-rejected-mechanism-sweep]
@@ -6992,6 +7009,10 @@ Apply SOLID at the class, module, and service level:
69927009
types
69937010
- Keep class hierarchies shallow — more than two levels of inheritance is a
69947011
signal to refactor towards composition
7012+
- **Closed type families are tagged unions** — where every variant is
7013+
known when the type is written, discriminate a union by a literal field
7014+
(`kind`, `type`) so the compiler checks every branch handles every
7015+
variant; keep class hierarchies for families callers extend
69957016
- **Internal identifiers must align with their layer** — a module or class
69967017
whose name implies a role it does not hold (an `endpoints.py` with no
69977018
routes, a `*RestApi` class that is not the REST API) misleads readers and

0 commit comments

Comments
 (0)