Skip to content

Commit c01ac5f

Browse files
jbedaclaude
andcommitted
docs(adr): record that modelith is not modelled in modelith (ADR-0009)
The self-model is the obvious dogfooding move and the question keeps coming up. Decide against it: the format's vocabulary already lives in the schema, the structs, and the schema reference, two of which TestSchemaStructSync holds in sync. A self-model would be a fourth copy that nothing checks, and drift between the tool's own model and its own schema is the worst possible advertisement for it. It would also compete with examples/ for the worked-example job. Pinned by TestADR_0009_NoSelfModel: the set of committed *.modelith.yaml files must be exactly the two known examples. Verified to fail on an added root-level model, not just to pass on the status quo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Joe Beda <joe@stacklok.com>
1 parent fcc3b4b commit c01ac5f

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

internal/model/selfmodel_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package model
2+
3+
import (
4+
"io/fs"
5+
"os"
6+
"path/filepath"
7+
"slices"
8+
"strings"
9+
"testing"
10+
)
11+
12+
// TestADR_0009_NoSelfModel guards ADR-0009: modelith's own domain is not
13+
// modelled in a *.modelith.yaml. The vocabulary lives in the schema, these
14+
// structs, and docs/06-schema-reference.md, two of which TestSchemaStructSync
15+
// holds in sync; a self-model would be a fourth, unchecked copy.
16+
//
17+
// The allowlist is the mechanism: a new model file in the repo fails this test
18+
// until someone adds it here, which puts the ADR in front of them.
19+
func TestADR_0009_NoSelfModel(t *testing.T) {
20+
t.Parallel()
21+
22+
allowed := map[string]string{
23+
"examples/example.modelith.yaml": "the worked example (golden fixture)",
24+
"docs/05-parking-garage/garage.modelith.yaml": "the docs example (golden fixture)",
25+
}
26+
27+
root := repoRoot(t)
28+
var found []string
29+
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
30+
if err != nil {
31+
return err
32+
}
33+
// Skip dot-directories: .git, and the gitignored .scratch/ and
34+
// .claude/worktrees/, which are full of throwaway fixtures.
35+
if d.IsDir() && strings.HasPrefix(d.Name(), ".") && path != root {
36+
return fs.SkipDir
37+
}
38+
if d.IsDir() || !strings.HasSuffix(d.Name(), ".modelith.yaml") {
39+
return nil
40+
}
41+
rel, err := filepath.Rel(root, path)
42+
if err != nil {
43+
return err
44+
}
45+
found = append(found, filepath.ToSlash(rel))
46+
return nil
47+
})
48+
if err != nil {
49+
t.Fatalf("walking repo: %v", err)
50+
}
51+
slices.Sort(found)
52+
53+
for _, rel := range found {
54+
if _, ok := allowed[rel]; !ok {
55+
t.Errorf("unexpected model file %s\n"+
56+
"modelith does not model itself: see project-docs/adr/0009-modelith-is-not-modelled-in-modelith.md.\n"+
57+
"If this is a legitimate new example, add it to the allowlist in this test.", rel)
58+
}
59+
}
60+
for rel, why := range allowed {
61+
if !slices.Contains(found, rel) {
62+
t.Errorf("allowlisted model file %s is missing (%s); update the allowlist if it moved", rel, why)
63+
}
64+
}
65+
}
66+
67+
// repoRoot walks up from the test's working directory to the directory holding
68+
// go.mod.
69+
func repoRoot(t *testing.T) string {
70+
t.Helper()
71+
dir, err := os.Getwd()
72+
if err != nil {
73+
t.Fatalf("getwd: %v", err)
74+
}
75+
for {
76+
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
77+
return dir
78+
}
79+
parent := filepath.Dir(dir)
80+
if parent == dir {
81+
t.Fatal("could not find repo root (no go.mod in any parent)")
82+
}
83+
dir = parent
84+
}
85+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# modelith is not modelled in modelith
2+
3+
Do not write a `*.modelith.yaml` describing modelith's own domain — the domain
4+
model format itself. The vocabulary it would capture is already defined in
5+
three places that tests hold in sync; a self-model would be a fourth copy that
6+
nothing checks, and it would drift.
7+
8+
## Context
9+
10+
modelith's domain is the domain-model format: entities, attributes,
11+
relationships, invariants, actions, scenarios, enums, glossary terms. Modelling
12+
that in modelith is the obvious dogfooding move, and the question comes up
13+
naturally — most recently while adapting a design-interview skill whose
14+
upstream version maintains a `CONTEXT.md` glossary.
15+
16+
## Decision
17+
18+
There is no self-model, and no `CONTEXT.md` either. The format's vocabulary
19+
lives where it already lives:
20+
21+
- `internal/schema/v1/modelith.schema.json` — the normative definition.
22+
- `internal/model/model.go` — the Go structs, held property-for-property
23+
against the schema by `TestSchemaStructSync`.
24+
- `docs/06-schema-reference.md` — the prose definition users read.
25+
- `examples/example.modelith.yaml` and `docs/05-parking-garage/` — the worked
26+
examples, rendered under `task render-check` and pinned by a golden test.
27+
28+
## Why
29+
30+
- **A fourth copy that nothing checks.** Two of the three definitions are
31+
machine-checked against each other. A self-model would join them as prose
32+
nobody validates, and drift between a tool's own model and its own schema is
33+
the worst possible advertisement for the tool.
34+
- **It would compete with the worked examples.** `examples/` exists to show what
35+
a good model looks like, and it is a golden fixture. A second in-repo model
36+
with a stronger claim to being "the real one" muddies which file a newcomer
37+
should read, and invites edits made for demo reasons.
38+
- **The self-referential framing costs more than it teaches.** A model whose
39+
`Entity` entity has an `attributes` relationship to an `Attribute` entity
40+
reads as a puzzle. Readers learning the format are better served by the
41+
parking garage.
42+
- **Dogfooding still happens, elsewhere.** The format is exercised by real
43+
models in other repos, which is where the pressure that produced issues #8#13
44+
and #24#25 came from. That is the useful kind of dogfooding: a model of a
45+
different domain, hitting real gaps.
46+
47+
## Consequences
48+
49+
- `.claude/skills/grill-with-docs/` states this so a design-interview session
50+
doesn't helpfully start one; it also drops the upstream skill's `CONTEXT.md`
51+
behavior for the same reason.
52+
- Format vocabulary changes land as one change across the schema, the structs,
53+
and the schema reference — never as an edit to a self-model.
54+
- Pinned by `TestADR_0009_NoSelfModel`, which asserts the set of committed
55+
`*.modelith.yaml` files is exactly the known examples. Adding a legitimate
56+
new example means extending that list, which puts this ADR in front of
57+
whoever does it.
58+
- Reopen if the format grows a construct that only a self-model can exercise —
59+
a concrete gap, not the appeal of dogfooding.

0 commit comments

Comments
 (0)