Skip to content

Commit 265def6

Browse files
dmealingclaude
andcommitted
feat(gen)!: --baseline=adopt, and a refusal that fails the run on every port
The no-manifest refusal LED with "commit .metaobjects/.gen-state/.hashes.json", which the population it names cannot do: nothing writes a manifest until a gen succeeds, and a run where every file refuses writes none. So there was nothing to commit and the next run refused identically. The only escape was --baseline=fresh, correctly framed as the lossy one. Found by an adopter estate running the RC with 50 files refused and the leading instruction unfollowable. --baseline=adopt separates the two things fresh does at once: record the files you HAVE as the merge base, write nothing. The manifest then exists to commit, and the regeneration arrives on the next run as its own reviewable diff. Placed at the refusal rather than at the top of the decision, so adopting can ONLY convert a refusal into a recorded baseline — unchanged and the pristine-file overwrite a fresh clone depends on are untouched, and passing it cannot freeze a project's regeneration. What adopting DECLARES is stated everywhere it is offered, because the name invites the other reading: that the files you have are generated output. It protects edits made from then on; an edit already inside one becomes part of the base and the next gen replaces it. A test pins that, rather than trusting the prose. Shipped in TypeScript, Python and C# — the three ports with a hash manifest. A new `adopted` write status carries it through the reporting layer instead of being folded into `unchanged`: the run's whole product is the manifest, and a summary that hid it would defeat the point. `fresh` stays TypeScript-only, and the other two REFUSE that value naming what they do implement rather than accepting it and refusing files anyway. BREAKING: a refused file now FAILS gen on Python and C# (exit 1), as it always has on TypeScript. Both printed the warning and exited 0, so an adopter who wired gen into CI as a drift gate was green while codegen refused to write — the gate did not gate, and nothing documented it. Found by probing the adopt work: no conformance corpus covers CLI exit codes, and the divergence had no test on either side. A build that starts failing was already unprotected, and the failure names its remedy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent db3c226 commit 265def6

37 files changed

Lines changed: 1166 additions & 98 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,51 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
### `meta gen --baseline=adopt` — the first run a pre-manifest project can actually perform
11+
12+
The no-manifest refusal LED with *"ONE-TIME FIX: commit
13+
`.metaobjects/.gen-state/.hashes.json`"*, which the population it names cannot do: nothing
14+
writes a manifest until a `gen` succeeds, and a run where every file refuses writes none.
15+
So there was nothing to commit, and the next run refused identically. The only escape was
16+
`--baseline=fresh`, correctly framed as the lossy one — an adopter whose committed output
17+
was already a byte-identical regen still had to reason past a warning about losing work.
18+
Found by an adopter estate running the release candidate: 50 files refused, and the leading
19+
instruction was unfollowable.
20+
21+
`--baseline=adopt` separates the two things `fresh` does at once. It records the files you
22+
HAVE as the merge base and **writes nothing**, so the manifest exists to commit and the
23+
regeneration arrives on the next run as its own reviewable diff. Placed at the refusal
24+
rather than at the top of the decision, so it can only ever convert a refusal into a
25+
recorded baseline: `unchanged` and the pristine-file overwrite a fresh clone depends on
26+
behave exactly as they do without the flag.
27+
28+
**What adopting declares, stated because the name invites the other reading:** that the
29+
files you have *are* generated output. It protects edits made from that point on — they
30+
finally have a base to merge against — but an edit already inside one of those files
31+
becomes part of the base, so the next `gen` replaces it. The message says so, and a test
32+
pins it. Where the edit is the point, the sequence that keeps it is unchanged and still
33+
needs `--baseline=fresh` (see [own-your-codegen](docs/features/own-your-codegen.md)).
34+
35+
Shipped in **TypeScript, Python and C#** — the three ports with a hash manifest. A new
36+
`adopted` write status carries it through the reporting layer rather than being folded
37+
into `unchanged`, because the run's whole product is the manifest and a summary that hid
38+
it would defeat the point. `fresh` remains TypeScript-only; Python and C# REFUSE that
39+
value with a message naming what they do implement, rather than accepting it and refusing
40+
files anyway.
41+
42+
### BREAKING — a refused file now FAILS `gen` on Python and C# (exit 1), as it always has on TypeScript
43+
44+
`metaobjects gen` and `dotnet meta gen` printed the refusal warning and exited **0**. An
45+
adopter who wired either into CI as a drift gate was therefore green while codegen was
46+
refusing to write — the gate did not gate, and nothing in the docs said so. TypeScript has
47+
exited 1 on a refusal since the manifest shipped.
48+
49+
The three ports now agree. A build that starts failing on this was already unprotected,
50+
and the failure names its own remedy (`--baseline=adopt` above). Found while probing the
51+
adopt work, not by a gate: no conformance corpus covers CLI exit codes, and the
52+
divergence had no test on either side.
53+
54+
1055
### BREAKING — the default provider composition registered vocabulary no other port has
1156

1257
`forgeTypesProvider` leaves `defaultLoadMemoryProviders` and `meta types`. A `decision` /

‎agent-context/skills/metaobjects-codegen/SKILL.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ You run a `gen` step. The runner:
3636
and the token, so an `@`-prefixed tag does not match it. Remove that token and
3737
regeneration never touches the file again.
3838

39+
**A refusal FAILS the run — exit 1 on every port** (TypeScript always did; Python and
40+
C# warned and exited 0 until 1.0, so a `gen` wired into CI was green while codegen
41+
refused to write). The one-time fix for a project that predates the committed manifest
42+
is **`gen --baseline=adopt`**: it records the files you have as the baseline and writes
43+
nothing, so there is finally a `.hashes.json` to commit — then `gen` again, and the
44+
regeneration lands as its own diff. Adopting DECLARES those files to be generated
45+
output, so an edit already inside one is part of the baseline and that regeneration
46+
replaces it; commit before you run it. (TypeScript additionally has
47+
`--baseline=fresh`: write fresh output now and discard the edits.)
48+
3949
**The two rules protect a hand edit in opposite ways, so do not carry a habit across
4050
ports.** On TypeScript, C# and Python, *editing the content* is what takes ownership —
4151
that is what breaks the hash — and deleting the header changes nothing except your

‎agent-context/skills/metaobjects-codegen/references/typescript.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,15 @@ refused, path named, exit 1). So the merge is machine-local: a file you edited a
272272
pushed is REFUSED on a fresh clone or in CI, not merged. Recovery is in
273273
`docs/features/own-your-codegen.md`.
274274

275+
A project with NO manifest at all (it predates the manifest being committed) is a
276+
different case with a different fix: every stale file refuses, and a run where everything
277+
refuses writes no manifest — so "commit `.hashes.json`" has nothing to commit. Run
278+
`meta gen --baseline=adopt` once: it records the files you have as the merge base and
279+
writes nothing, leaving exactly one file to commit; the next `meta gen` is then the
280+
regeneration, as its own reviewable diff. Adopting declares those files to BE generated
281+
output, so an edit already inside one is part of the base and that regeneration replaces
282+
it — `--baseline=fresh` + `git checkout` is the sequence that KEEPS an edit.
283+
275284
Hand-customizations that metadata can't express go in a sibling module you create and
276285
import yourself — `<Entity>.extra.ts` by convention. The name carries no tool behaviour:
277286
the file is safe because codegen writes only the paths it records, and the generated

‎docs/features/cli.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,42 @@ at `-Dmeta.verify.templateRoot`). The one goal covers BOTH Java (`codegen-spring
120120
migrate engine, ADR-0015"); an unknown mode fails listing the valid ones. (Schema `--db` remains
121121
Node-only by the ADR-0015 design — see below.)
122122

123+
## `gen` refuses rather than guesses — and `--baseline` is how you answer it
124+
125+
Every port's `gen` decides per file whether it may overwrite, from a committed hash
126+
manifest (`.metaobjects/.gen-state/.hashes.json`). A file that still hashes to what the
127+
generator recorded writing is safe to overwrite; one that was edited, or that no record
128+
covers, is **refused by name and left exactly as it is**.
129+
130+
**A refusal fails the run — exit 1 — on every port.** TypeScript has always done this;
131+
Python and C# printed the warning and exited 0 until 1.0, so an adopter who wired `gen`
132+
into CI as a drift gate was green while codegen was refusing to write. If a build starts
133+
failing on this at 1.0, it was already not protecting anything; the remedy below is the
134+
one-time fix.
135+
136+
| flag | ports | what it does |
137+
|---|---|---|
138+
| `--baseline=default` | all | the above: refuse what cannot be proved to be generated output |
139+
| `--baseline=adopt` | TS · Python · C# | record the files you HAVE as the merge base and **write nothing** |
140+
| `--baseline=fresh` | TS only | overwrite the refused files from fresh output and re-baseline (**discards** hand edits) |
141+
142+
**`adopt` exists because the refusal's own remedy was unreachable.** It told you to commit
143+
`.hashes.json` — but nothing writes a manifest until a `gen` succeeds, and a run where
144+
every file refuses writes none, so there was nothing to commit and the next run refused
145+
identically. `adopt` breaks that loop: it records what is on disk, writes not one byte of
146+
output, and leaves exactly one file to commit. Then `gen` again, and the regeneration
147+
arrives as its own reviewable diff.
148+
149+
**What adopting declares** — say this out loud before running it: that the files you have
150+
*are* generated output. It protects edits made from that point on, because they finally
151+
have a base to merge against. An edit already inside one of those files becomes part of
152+
the base, so the next `gen` will replace it. Commit first, then read that second diff.
153+
Where the edit is the point, the sequence that keeps it is in
154+
[own-your-codegen](own-your-codegen.md#recovering-a-refused-file-on-a-machine-that-never-generated-it),
155+
and it needs `--baseline=fresh` (TypeScript). Python and C# implement `default` and
156+
`adopt` only; there, discard a refused file by deleting it and re-running, which writes it
157+
as new.
158+
123159
## `meta docs` surfaces — and the `agent/` one
124160

125161
`meta docs` writes MARKDOWN, from metadata, into `docs.outDir` (default `./docs`). It

‎docs/features/migrations/commit-the-gen-state-hash-manifest.md‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,29 @@ into an excluded directory, so the negation below it can never take effect — t
3434
would stay ignored and you would get none of this. `.gen-state/*` excludes the directory's
3535
*contents*, which leaves the negation able to re-include one of them.
3636

37-
Then commit the manifest:
37+
Then produce the manifest and commit it:
3838

3939
```bash
40-
meta gen # writes .gen-state/.hashes.json
40+
meta gen --baseline=adopt # records what you have; writes NOTHING else
4141
git add .metaobjects/.gitignore .metaobjects/.gen-state/.hashes.json
4242
git commit -m "chore: commit the codegen hash manifest"
43+
meta gen # the regeneration, now on its own
4344
```
4445

46+
**Why `--baseline=adopt` and not a plain `meta gen`.** A plain run works only if your
47+
committed output still matches fresh output. If it does not — an engine or formatter moved
48+
it, which is the usual state of a project that has not regenerated in a few releases — every
49+
such file is refused, and a run where *everything* refuses writes **no manifest at all**:
50+
there is then nothing to commit, and the next run refuses identically. `--baseline=adopt`
51+
is the run that breaks that loop. It records the files you have as the merge base, writes
52+
not one byte of output, and leaves you with exactly one file to commit.
53+
54+
**What adopting declares.** That the files you have *are* generated output. It protects
55+
edits you make from that point on — they now have a base to merge against — but an edit
56+
already inside one of those files becomes part of the base, so the `meta gen` that follows
57+
will replace it. Commit before you adopt and read that second diff; if it contains
58+
something you meant to keep, the "Keep the differences" recipe below is the way to keep it.
59+
4560
## Why the manifest and not the whole snapshot
4661

4762
`.gen-state/` holds two different things. The snapshot **bodies** are a full second copy of
@@ -82,6 +97,12 @@ That means those files differ from fresh output and there is no hash recording t
8297
snapshot, and the second merges your version back in.
8398
- **Discard them.** `meta gen --baseline=fresh` overwrites and adopts fresh output as the
8499
baseline. This throws away hand edits in generated files, so read the refusal list first.
100+
- **Adopt what you have, then look.** `meta gen --baseline=adopt` writes nothing and only
101+
records the current files as the base, so the refusals stop and the regeneration becomes
102+
a separate, reviewable diff on the next run. Use it when you do not yet know whether
103+
those differences are edits or just older output — it is the one option that changes no
104+
file while you find out. It is not a way to keep an edit: see what adopting declares,
105+
above.
85106

86107
Run it once, commit the manifest, and the refusals stop: from then on every machine can
87108
tell your edits from its own output.

‎docs/features/own-your-codegen.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ meta gen # real three-way merge; reports `merged`, exit
6969
To **discard** your version instead, `meta gen --baseline=fresh` on its own is the whole
7070
answer.
7171

72+
`--baseline=adopt` is **not** a third option here, and it is worth saying why, because its
73+
name invites the mistake. Adopting records what is on disk as the merge base and writes
74+
nothing — right for a project that has no manifest at all (see
75+
[the manifest migration guide](migrations/commit-the-gen-state-hash-manifest.md)), where
76+
there is no other way to produce the file you are asked to commit. But the base it records
77+
is your *edited* text, so base and working copy are identical and the next `meta gen`
78+
merges fresh output straight over it. When the edit is the point, use the four-step
79+
sequence above.
80+
7281
The remedy you will not find here is *"move the edit into a non-generated file"*. It
7382
works where the edit can live outside the generated file — and does not where the
7483
artifact is the point of the edit. A `requirementTests()` stub is the standing

‎fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ You run a `gen` step. The runner:
3636
and the token, so an `@`-prefixed tag does not match it. Remove that token and
3737
regeneration never touches the file again.
3838

39+
**A refusal FAILS the run — exit 1 on every port** (TypeScript always did; Python and
40+
C# warned and exited 0 until 1.0, so a `gen` wired into CI was green while codegen
41+
refused to write). The one-time fix for a project that predates the committed manifest
42+
is **`gen --baseline=adopt`**: it records the files you have as the baseline and writes
43+
nothing, so there is finally a `.hashes.json` to commit — then `gen` again, and the
44+
regeneration lands as its own diff. Adopting DECLARES those files to be generated
45+
output, so an edit already inside one is part of the baseline and that regeneration
46+
replaces it; commit before you run it. (TypeScript additionally has
47+
`--baseline=fresh`: write fresh output now and discard the edits.)
48+
3949
**The two rules protect a hand edit in opposite ways, so do not carry a habit across
4050
ports.** On TypeScript, C# and Python, *editing the content* is what takes ownership —
4151
that is what breaks the hash — and deleting the header changes nothing except your

‎fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ You run a `gen` step. The runner:
3636
and the token, so an `@`-prefixed tag does not match it. Remove that token and
3737
regeneration never touches the file again.
3838

39+
**A refusal FAILS the run — exit 1 on every port** (TypeScript always did; Python and
40+
C# warned and exited 0 until 1.0, so a `gen` wired into CI was green while codegen
41+
refused to write). The one-time fix for a project that predates the committed manifest
42+
is **`gen --baseline=adopt`**: it records the files you have as the baseline and writes
43+
nothing, so there is finally a `.hashes.json` to commit — then `gen` again, and the
44+
regeneration lands as its own diff. Adopting DECLARES those files to be generated
45+
output, so an edit already inside one is part of the baseline and that regeneration
46+
replaces it; commit before you run it. (TypeScript additionally has
47+
`--baseline=fresh`: write fresh output now and discard the edits.)
48+
3949
**The two rules protect a hand edit in opposite ways, so do not carry a habit across
4050
ports.** On TypeScript, C# and Python, *editing the content* is what takes ownership —
4151
that is what breaks the hash — and deleting the header changes nothing except your

‎fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ You run a `gen` step. The runner:
3636
and the token, so an `@`-prefixed tag does not match it. Remove that token and
3737
regeneration never touches the file again.
3838

39+
**A refusal FAILS the run — exit 1 on every port** (TypeScript always did; Python and
40+
C# warned and exited 0 until 1.0, so a `gen` wired into CI was green while codegen
41+
refused to write). The one-time fix for a project that predates the committed manifest
42+
is **`gen --baseline=adopt`**: it records the files you have as the baseline and writes
43+
nothing, so there is finally a `.hashes.json` to commit — then `gen` again, and the
44+
regeneration lands as its own diff. Adopting DECLARES those files to be generated
45+
output, so an edit already inside one is part of the baseline and that regeneration
46+
replaces it; commit before you run it. (TypeScript additionally has
47+
`--baseline=fresh`: write fresh output now and discard the edits.)
48+
3949
**The two rules protect a hand edit in opposite ways, so do not carry a habit across
4050
ports.** On TypeScript, C# and Python, *editing the content* is what takes ownership —
4151
that is what breaks the hash — and deleting the header changes nothing except your

‎fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ You run a `gen` step. The runner:
3636
and the token, so an `@`-prefixed tag does not match it. Remove that token and
3737
regeneration never touches the file again.
3838

39+
**A refusal FAILS the run — exit 1 on every port** (TypeScript always did; Python and
40+
C# warned and exited 0 until 1.0, so a `gen` wired into CI was green while codegen
41+
refused to write). The one-time fix for a project that predates the committed manifest
42+
is **`gen --baseline=adopt`**: it records the files you have as the baseline and writes
43+
nothing, so there is finally a `.hashes.json` to commit — then `gen` again, and the
44+
regeneration lands as its own diff. Adopting DECLARES those files to be generated
45+
output, so an edit already inside one is part of the baseline and that regeneration
46+
replaces it; commit before you run it. (TypeScript additionally has
47+
`--baseline=fresh`: write fresh output now and discard the edits.)
48+
3949
**The two rules protect a hand edit in opposite ways, so do not carry a habit across
4050
ports.** On TypeScript, C# and Python, *editing the content* is what takes ownership —
4151
that is what breaks the hash — and deleting the header changes nothing except your

0 commit comments

Comments
 (0)