@@ -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,
4242and 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
46186Hand-edit a generated `Subscriber.names.ts` so a column reads `first_name_BROKEN` and
0 commit comments