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
38 changes: 38 additions & 0 deletions .claude/skills/SOURCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Where the skills in this directory came from

Provenance and licence notes for skills imported from outside this repo. This
file is not a skill and is not loaded into an agent's context; it exists so the
origin of borrowed text is auditable.

**Convention:** when importing a skill from elsewhere, add an entry here
recording the upstream source, the commit it was taken at, its licence, and
what was changed. Skills written from scratch for modelith don't need an entry.

## grilling

- **Upstream:** [`mattpocock/skills`](https://github.com/mattpocock/skills),
`skills/productivity/grilling/SKILL.md`
- **Taken at:** commit `ed37663cc5fbef691ddfecd080dff42f7e7e350d` (2026-07-21)
- **Licence:** MIT — Copyright (c) 2026 Matt Pocock. Full text:
<https://github.com/mattpocock/skills/blob/main/LICENSE>
- **Changes:** none; the body is verbatim upstream.

## grill-with-docs

- **Upstream:** same repo, `skills/engineering/grill-with-docs/SKILL.md` and
the `skills/engineering/domain-modeling/` skill it delegates to, at the same
commit.
- **Licence:** MIT, as above.
- **Changes:** substantially rewritten for this repo. Upstream maintains a
`CONTEXT.md` glossary and its own ADR format doc; both were dropped, because
modelith's schema, structs, and schema reference already hold that vocabulary
under test, and `.claude/rules/adr.md` already sets the ADR bar. The retained
upstream material is the session-conduct advice (sharpen fuzzy language,
stress-test with scenarios, cross-reference the code) plus the
emission-frequency probe.

Upstream also ships an installer (`npx skills@latest add mattpocock/skills`,
from [`vercel-labs/skills`](https://github.com/vercel-labs/skills)) and a
Claude Code plugin marketplace entry. Neither is used here: the installer
writes no lockfile, so it records no less than this file does, and both of the
skills above are edited rather than tracked upstream.
60 changes: 60 additions & 0 deletions .claude/skills/grill-with-docs/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
name: grill-with-docs
description: A relentless interview to sharpen a plan or design, capturing decisions in this repo's canonical homes as they land. Use when the user wants to stress-test a design and record what comes out of it.
disable-model-invocation: true
---

Run a `/grilling` session over the plan or decision at hand, then capture what
crystallises where it belongs. Capture inline, as each decision lands — not
batched at the end.

## Where things land in this repo

**Hard-to-reverse decisions with a real trade-off → write the ADR now.**
`project-docs/adr/`, shape and bar in [`.claude/rules/adr.md`](../../rules/adr.md).
Read that rule before writing one; it is stricter than the generic advice
(sequential numbering, supersede rather than edit, and a `TestADR_NNNN_*` test
when the decision is one a test can pin).

**Format or vocabulary changes → flag them, do not write them.** A new schema
key, a renamed concept, a changed definition: note it for the implementing
change and move on. Format vocabulary lives in three places that are checked
against each other — `internal/schema/v1/modelith.schema.json`,
`internal/model/model.go` (guarded by `TestSchemaStructSync`), and
`docs/06-schema-reference.md` — plus the golden `examples/`. Editing one of
them mid-grilling leaves the repo failing `task check` with the design still
half-argued.

**Running state → `HANDOFF.md`** at the end of the session (gitignored,
local-only; see `CLAUDE.md`).

## This repo has no CONTEXT.md, deliberately

The upstream version of this skill maintains a `CONTEXT.md` glossary. Don't
create one here. modelith's domain *is* the domain-model format, and that
vocabulary is already defined in the schema, the Go structs, and the schema
reference — two of which a test holds in sync. A `CONTEXT.md` would be a fourth
copy that nothing checks.

For the same reason, don't propose modelling modelith in `*.modelith.yaml`.
It would drift from the enforced definitions and compete with `examples/` for
the worked-example job.

## During the session

Beyond the interview itself, the angles worth pressing:

- **Sharpen fuzzy language.** When a term is vague or overloaded, propose a
precise canonical one. Two names for one concept, or one name for two, is the
bug that this whole tool exists to catch — don't let it pass in our own
design conversation.
- **Stress-test with concrete scenarios.** Invent cases that probe the edges
and force precision about where one concept stops and the next starts.
- **Cross-reference against the code.** When a claim is made about how modelith
behaves, check it. The linter, the renderers, and the schema are right here;
a contradiction between what we believe and what the binary does is a finding.
- **Probe emission frequency.** When the design emits something observable — a
lint diagnostic, a log line, a rendered element — pin down how often it fires:
once per model, per entity, or per attribute? A diagnostic that turns out to
fire per attribute is a wall of output, and usually belongs on a different
surface.
12 changes: 12 additions & 0 deletions .claude/skills/grilling/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: grilling
description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
---

Interview me relentlessly about every aspect of this until we reach a shared understanding. Walk down each branch of the decision tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.

Ask the questions one at a time, waiting for feedback on each question before continuing. Asking multiple questions at once is bewildering.

If a *fact* can be found by exploring the environment (filesystem, tools, etc.), look it up rather than asking me. The *decisions*, though, are mine — put each one to me and wait for my answer.

Do not act on it until I confirm we have reached a shared understanding.
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ tracked as [issue #1](https://github.com/stacklok/modelith/issues/1).
[`.claude/rules/agent-workflow.md`](./.claude/rules/agent-workflow.md). It is
available discipline for changes that put a correctness-critical surface at
risk, not a mandate on every commit.
- **Imported skills record where they came from.**
[`.claude/skills/`](./.claude/skills/) holds skills for working *on* modelith
(distinct from [`plugin/skills/`](./plugin/skills/), which ships to users).
When a skill is copied in from elsewhere, add its upstream source, commit,
licence, and local changes to
[`.claude/skills/SOURCES.md`](./.claude/skills/SOURCES.md).
- **Scratch work** (spikes, smoke tests, throwaway fixtures, review
round-records) goes in the repo-local, gitignored
[`.scratch/`](./.scratch/) — not `/tmp` or a session temp dir. It stays
Expand Down
85 changes: 85 additions & 0 deletions internal/model/selfmodel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package model

import (
"io/fs"
"os"
"path/filepath"
"slices"
"strings"
"testing"
)

// TestADR_0009_NoSelfModel guards ADR-0009: modelith's own domain is not
// modelled in a *.modelith.yaml. The vocabulary lives in the schema, these
// structs, and docs/06-schema-reference.md, two of which TestSchemaStructSync
// holds in sync; a self-model would be a fourth, unchecked copy.
//
// The allowlist is the mechanism: a new model file in the repo fails this test
// until someone adds it here, which puts the ADR in front of them.
func TestADR_0009_NoSelfModel(t *testing.T) {
t.Parallel()

allowed := map[string]string{
"examples/example.modelith.yaml": "the worked example (golden fixture)",
"docs/05-parking-garage/garage.modelith.yaml": "the docs example (golden fixture)",
}

root := repoRoot(t)
var found []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Skip dot-directories: .git, and the gitignored .scratch/ and
// .claude/worktrees/, which are full of throwaway fixtures.
if d.IsDir() && strings.HasPrefix(d.Name(), ".") && path != root {
return fs.SkipDir
}
if d.IsDir() || !strings.HasSuffix(d.Name(), ".modelith.yaml") {
return nil
}
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
found = append(found, filepath.ToSlash(rel))
return nil
})
if err != nil {
t.Fatalf("walking repo: %v", err)
}
slices.Sort(found)

for _, rel := range found {
if _, ok := allowed[rel]; !ok {
t.Errorf("unexpected model file %s\n"+
"modelith does not model itself: see project-docs/adr/0009-modelith-is-not-modelled-in-modelith.md.\n"+
"If this is a legitimate new example, add it to the allowlist in this test.", rel)
}
}
for rel, why := range allowed {
if !slices.Contains(found, rel) {
t.Errorf("allowlisted model file %s is missing (%s); update the allowlist if it moved", rel, why)
}
}
}

// repoRoot walks up from the test's working directory to the directory holding
// go.mod.
func repoRoot(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatalf("getwd: %v", err)
}
for {
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
t.Fatal("could not find repo root (no go.mod in any parent)")
}
dir = parent
}
}
59 changes: 59 additions & 0 deletions project-docs/adr/0009-modelith-is-not-modelled-in-modelith.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# modelith is not modelled in modelith

Do not write a `*.modelith.yaml` describing modelith's own domain — the domain
model format itself. The vocabulary it would capture is already defined in
three places that tests hold in sync; a self-model would be a fourth copy that
nothing checks, and it would drift.

## Context

modelith's domain is the domain-model format: entities, attributes,
relationships, invariants, actions, scenarios, enums, glossary terms. Modelling
that in modelith is the obvious dogfooding move, and the question comes up
naturally — most recently while adapting a design-interview skill whose
upstream version maintains a `CONTEXT.md` glossary.

## Decision

There is no self-model, and no `CONTEXT.md` either. The format's vocabulary
lives where it already lives:

- `internal/schema/v1/modelith.schema.json` — the normative definition.
- `internal/model/model.go` — the Go structs, held property-for-property
against the schema by `TestSchemaStructSync`.
- `docs/06-schema-reference.md` — the prose definition users read.
- `examples/example.modelith.yaml` and `docs/05-parking-garage/` — the worked
examples, rendered under `task render-check` and pinned by a golden test.

## Why

- **A fourth copy that nothing checks.** Two of the three definitions are
machine-checked against each other. A self-model would join them as prose
nobody validates, and drift between a tool's own model and its own schema is
the worst possible advertisement for the tool.
- **It would compete with the worked examples.** `examples/` exists to show what
a good model looks like, and it is a golden fixture. A second in-repo model
with a stronger claim to being "the real one" muddies which file a newcomer
should read, and invites edits made for demo reasons.
- **The self-referential framing costs more than it teaches.** A model whose
`Entity` entity has an `attributes` relationship to an `Attribute` entity
reads as a puzzle. Readers learning the format are better served by the
parking garage.
- **Dogfooding still happens, elsewhere.** The format is exercised by real
models in other repos, which is where the pressure that produced issues #8–#13
and #24–#25 came from. That is the useful kind of dogfooding: a model of a
different domain, hitting real gaps.

## Consequences

- `.claude/skills/grill-with-docs/` states this so a design-interview session
doesn't helpfully start one; it also drops the upstream skill's `CONTEXT.md`
behavior for the same reason.
- Format vocabulary changes land as one change across the schema, the structs,
and the schema reference — never as an edit to a self-model.
- Pinned by `TestADR_0009_NoSelfModel`, which asserts the set of committed
`*.modelith.yaml` files is exactly the known examples. Adding a legitimate
new example means extending that list, which puts this ADR in front of
whoever does it.
- Reopen if the format grows a construct that only a self-model can exercise —
a concrete gap, not the appeal of dogfooding.
Loading