Skip to content

Commit a1019c2

Browse files
dmealingclaude
andcommitted
docs: changelog for the five remaining estate findings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTcEKXTQMYt84fAjuw5A2M
1 parent 865c1ef commit a1019c2

1 file changed

Lines changed: 140 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,146 @@ Also removed from `@metaobjectsdev/sdk`: the forge memory record schemas (`Recor
4141
`listRecords` / `promoteRecord` / `supersede` / …). Nothing in the library imported either,
4242
and a sweep of all seven adopter estates found no use of any of the fifteen names.
4343

44+
### BREAKING — `meta docs` defaults `outDir` to `./docs/generated`
45+
46+
It was `./docs`, which in most repositories is the *human* documentation folder. One estate's
47+
held a business-strategy document, four product screenshots, email drafts and an analytics
48+
roadmap, and a bare `meta docs` scattered **29 generated pages** through it — then wanted to
49+
own `docs/README.md`. `meta verify --docs` reported all 29 as missing until the adopter found
50+
the config key.
51+
52+
The docs gate's own design language is right — *"`docs.outDir` is a directory, not a namespace
53+
MetaObjects owns"* — and that is exactly the argument against defaulting into the most-owned
54+
directory name in the ecosystem. A project relying on the old default pins `docs: { outDir:
55+
"./docs" }` and nothing moves; `meta init` now scaffolds the key so a new project's choice is
56+
visible rather than implied. `--metamodel` keeps `./docs/metamodel` — a different artifact,
57+
not this project's pages. Migration: [`0.x-to-1.0.md` §13](docs/features/migrations/0.x-to-1.0.md).
58+
59+
Two smaller things travel with it. The flag default and the config default are now **one
60+
constant** — they were two literals that happened to be the same string. And `meta docs
61+
--help` documented only `--out`, the flag that does **not** survive the run, which is the one
62+
thing a gate needs: it now names the config key.
63+
64+
**The change exposed a suite that was never comparing the two doors.** The docs-drift
65+
integration cases generated with an explicit `--out <dir>/docs` while `verify --docs` resolved
66+
the config default, and passed only because the two coincided. Sixteen cases failed the moment
67+
they stopped. They now take the config path, which is what the gate reads.
68+
69+
### Fixed — `agent/ui.md` described a UI tier the run does not emit
70+
71+
`meta docs --agent`'s own `--help` promises *"each page is skipped when its tier has nothing to
72+
describe."* It emitted `agent/ui.md` for an estate with `generators: []`, no `view.*` node and
73+
no route anywhere — and the page was not empty, it was **specific and wrong**: a
74+
control/HTML-type/rules table per entity for forms that do not exist, and an `Endpoint` line
75+
per object (`/wake_events`, `/alarms`, …) for addresses nothing serves. The audience is an
76+
agent the surface explicitly instructs to read the page BEFORE touching a tier, i.e. the
77+
audience least able to check. Same family as the headless-project agent-docs finding, with the
78+
failure **inverted**: that one emitted nothing where something was expected; this emits
79+
something where nothing is true.
80+
81+
The gate was `hasUiSurface(obj) = servesReadApi(obj)` — a **metadata** predicate, which answers
82+
"could a UI be generated for this object?" and can never answer "does this run generate one?".
83+
Whether an artifact exists is a GENERATOR fact. The page now gates on `emitsUiTier`, a marker
84+
on each form/hook/grid generator aggregated by the runner into
85+
`ResolvedGenConfig.includeUiTier` — the same mechanism `emitsHonoRoutes` and `emitsNames`
86+
already use — through `runEmitsUiTier`, the ONE derivation both doors call (`meta gen`'s runner
87+
and `meta docs`, the other door onto this page). The sibling pages are unaffected: the
88+
showcase, which wires no UI generator, keeps the `agent/schema.md` that is genuinely true for
89+
it. An owned copy ejected before the marker existed still emits the page (matched by name) and
90+
**warns**, naming the one-line fix; `includeUiTier` in the config covers a copy that was also
91+
renamed. Nothing degrades quietly.
92+
93+
**Found while wiring the second door:** `meta docs` read the RAW config, where a generator
94+
wired by stable name (`generators: ["routes-hono"]`, legal under ADR-0021 #1) is a *string*
95+
carrying no marker at all — so `api-docs` documented the Fastify-only surface for a project
96+
that had wired the Hono one. Both aggregations now run after `resolveGenerators`, and an
97+
unresolvable suite says so rather than silently answering "no".
98+
99+
### Fixed — `verify --replay`'s remedy prescribed something that cannot work
100+
101+
`meta verify --replay` earned its keep immediately on two estates — it turned a paragraph of
102+
tribal knowledge into an exit code. Then it printed *"Applied migrations are immutable, so fix
103+
this with a NEW migration that creates the missing object."* **That fix cannot work, in any
104+
case:** migrations apply in timestamp order, so anything authored now sorts AFTER the file that
105+
failed and the object is still missing when it runs.
106+
107+
Two estates hit it from opposite directions. One's chain has a FIRST migration needing a schema
108+
an out-of-band script creates (the *third* migration emits `CREATE SCHEMA IF NOT EXISTS`; the
109+
first predates it), so nothing in the chain could have created it. The other's schema is built
110+
by **drizzle-kit**, with `.metaobjects/migrations/` holding later incremental patches —
111+
following the printed remedy literally means hand-authoring every `CREATE TABLE` into the
112+
MetaObjects chain, i.e. duplicating a schema this project does not own, which is the opposite
113+
of what the agent context teaches two paragraphs into its own Principles. One message,
114+
assuming MetaObjects is the only schema authority and offering no third possibility.
115+
116+
The remedy now names the fixes that exist: don't wire `--replay` when another tool owns
117+
creation; correct the migration in place if no database has applied it yet; or re-baseline from
118+
a live database. Immutability is stated as the **condition** it is — a checksum guard on
119+
databases that already ran the file — rather than a blanket ban, because a migration nothing
120+
has applied is edited in place every day. The head of the chain gets the extra sentence that is
121+
only true there. Choosing correctly needs the POSITION, so `applyPending` now throws
122+
`MigrationApplyError` carrying the migration name, its index among the pending set and the
123+
pending count — which also means `meta migrate apply-pending` finally names the file that
124+
failed instead of printing a bare driver error over a directory of migrations.
125+
126+
### Added — `meta verify` advises when a provider silently drops `apiPrefix`
127+
128+
1.0 moved `$apiPrefix` out of the entity descriptor, and the migration note rests its safety
129+
argument on `<EntityFetcherProvider value={…}>` no longer typechecking. A real adopter tree
130+
falsified two unstated preconditions.
131+
132+
**It only protects a project that runs `tsc`.** On an estate whose web app had no typecheck
133+
script, no turbo task and no CI job, the unmigrated tree passed `turbo run build` (vite
134+
transpiles, it does not typecheck), 150 tests and every `meta verify` gate. `tsc --noEmit`, run
135+
by hand, was the only signal in the building.
136+
137+
**And `tsc` cannot see the half that compiles.** `baseUrl` is optional with default `""`, so
138+
reading the note, renaming `value` → `fetcher` and stopping there is type-correct and silently
139+
drops the prefix from every generated hook — the likelier outcome, precisely because it
140+
compiles. On that estate it would have shipped eleven objects × five hooks plus six grids
141+
404ing across a public storefront and an admin app. A project whose `apiPrefix` is `""` is
142+
unaffected, which is why the trap is easy to ship: `meta init` scaffolds `apiPrefix: ""`, so
143+
the default-configured project is the one the design was reasoned about.
144+
145+
So `meta verify` now advises when `apiPrefix` is non-empty and a source file mounts a provider
146+
with no `baseUrl`, covering both tiers (the JSX provider and Angular's
147+
`provideEntityFetcher`). It rides in the existing verify-as-teacher pass — same suppression
148+
flag, same structured row shape, **never** an exit code, since a project may legitimately serve
149+
its API at the origin root. The JSX match spans the whole opening tag rather than one line,
150+
because real providers are written multi-line and a `>` inside an attribute would otherwise cut
151+
the tag in half. The migration note now states the precondition out loud.
152+
153+
### Fixed — Python `.gen-state` keys were relative to `--out`, not to the project
154+
155+
The manifest lives at the project root (`<project>/.metaobjects/.gen-state`, anchored on the
156+
metadata directory's parent — deliberate, and matching C#). Its **keys** were relative to
157+
`--out`, so two runs with different `--out` shared ONE manifest keyed by names that mean
158+
different files: `gen --out a` and `gen --out b` both record `Program.py`, and whichever ran
159+
last decided whether the other run's file still counted as ours. That manifest is the only
160+
thing that can tell "this is exactly what I wrote" from "somebody edited this" on a machine
161+
that did not generate the code, so one that can be populated from an unrelated tree and keyed
162+
ambiguously is one that can **say yes to the wrong file**. The multi-target path (#267) has the
163+
same shape by construction: every target writes its own `outDir` and all of them share one
164+
gen-state dir.
165+
166+
Keys are now project-root-relative, as TypeScript's have always been — which is also what makes
167+
multiple output targets work there. **Nothing to do:** the old spelling is read as a fallback
168+
and dropped once each file is recorded under the new key, so an existing manifest keeps working
169+
and converges on its own. Re-keying with no fallback is fail-closed (`gen` REFUSES a
170+
now-unrecorded file) and the wrong cost to impose for a change an adopter did not ask for.
171+
172+
`verify --codegen`'s jurisdiction guard reads the same manifest and moved with it. **That half
173+
fails in the direction nothing notices:** a lookup that finds nothing makes every file "not
174+
ours", so the `extra` verdict silently empties while the gate still prints `in sync` — the
175+
break-probe run printed *"in sync (0 file(s))"* over stale committed output. The existing
176+
jurisdiction test covered only the negative direction (a stranger's file is not convicted) and
177+
would have stayed green throughout; the positive direction is now gated too. Migration:
178+
[`0.x-to-1.0.md` §13](docs/features/migrations/0.x-to-1.0.md).
179+
180+
*(The report that found this also claimed `_record` replaces the manifest wholesale — 21
181+
entries becoming 6. Not reproducible from the code: `_record` loads and adds. Only the keying
182+
was wrong.)*
183+
44184
### Fixed — a hand-edited `<Entity>Names` read as "in sync with the metadata"
45185

46186
Hand-edit a generated `Subscriber.names.ts` so a column reads `first_name_BROKEN` and

0 commit comments

Comments
 (0)