Skip to content

Commit 30375af

Browse files
committed
docs(spec): add program C — the startup schema validator ADR-0015 half-removed
The Java port used to ship MetaClassDBValidatorService: it checked each MetaObject's mapping against the live DB on boot AND created what was missing, under one autoCreate flag. ADR-0015 removed it, and the ADR names what it removed precisely — the "auto-create path" plus createTable/createIndex/createForeignKey/ createSequence. Every deleted capability is a WRITE. The read half went with it only because the two were one service behind one flag. So a read-only validator does not reopen ADR-0015. That ADR defends schema AUTHORITY — who may emit DDL, and the answer stays "the TypeScript toolchain alone". Introspecting and comparing claims no authority. The spec makes read-only a hard rule rather than a default, and puts "no DDL under any flag" in non-goals, because it was the option's existence that made the last validator removable. The gap it closes is not hypothetical and is a direct consequence of ADR-0015: `verify --db` is Node-only, so the four ports that cannot run it are exactly the four whose services run against a schema they do not own with no way to detect that it disagrees with the metadata they just loaded. Centralising schema in Node was right; leaving those ports blind at runtime was its unpriced cost. The implementation constraint that keeps it honest: compare live introspection against the COMMITTED SNAPSHOT the TS toolchain already produces and verify has gated since #292 — not against metadata directly. Each port then needs dialect introspection and a comparison, never a diff engine, so this cannot re-fork the engine ADR-0015 consolidated. That is also what ties the three programs together. Each link in the chain is checked somewhere by something already built: metadata->code+constants (verify --codegen, every port), metadata->snapshot (verify --db, #292), snapshot->migration chain (--replay-snapshot), snapshot->the live DB a process actually uses (program C, every port). The one place the composition genuinely breaks is recorded too: column naming is config, not metadata, so two ports with different columnNaming settings each pass their own gate while disagreeing with each other — which is a concrete argument for declaring @column explicitly.
1 parent 6f6edda commit 30375af

1 file changed

Lines changed: 105 additions & 2 deletions

File tree

‎docs/superpowers/specs/2026-08-30-name-constants-and-magic-string-elimination-design.md‎

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
**Date:** 2026-08-30
44
**Status:** proposed — not yet approved for implementation
55
**Scope:** all five ports (TypeScript, C#, Java, Kotlin, Python), the agent-context skills,
6-
and the metaobjects.dev codegen snippets.
6+
and the metaobjects.dev codegen snippets. Three programs; they can be planned and shipped
7+
independently, and Program A has a hard prerequisite (§A4).
78

89
## Summary
910

@@ -17,6 +18,9 @@ different consumers, and different risk profiles:
1718
- **Program B — metamodel vocabulary names.** Generate, per port, constants for every
1819
registered type name, subtype name and attribute name, so that no code in this repo or
1920
in an adopter's provider/tooling spells `"object.entity"` or `"@column"` as a literal.
21+
- **Program C — a read-only startup schema validator**, per port, closing the one link in
22+
the chain that no build-time gate can reach: whether the database *this running process is
23+
actually connected to* matches the metadata it just loaded.
2024

2125
They share one goal — **eliminate magic strings** — and one doctrinal home: the
2226
`metaobjects-*` skills must teach it in every language, which today they do not.
@@ -285,6 +289,95 @@ the exception.
285289

286290
---
287291

292+
## Program C — read-only startup schema validation
293+
294+
### C1. Why this is not a revival of what ADR-0015 removed
295+
296+
The Java port once shipped `MetaClassDBValidatorService`: it verified each MetaObject's
297+
table/view mapping against the live database **and created what was missing** — tables,
298+
sequences, indexes, foreign keys, views — under an `autoCreate` flag
299+
(`docs/superpowers/specs/2026-05-22-fr-003-…-design.md:17`).
300+
301+
[ADR-0015](../../../spec/decisions/ADR-0015-single-shared-migrate-engine.md) removed it, and
302+
the ADR names exactly what it removed: "Java's OMDB runtime schema **auto-create** path
303+
(`MetaClassDBValidatorService` + the drivers' `createTable`/`createIndex`/`createForeignKey`/
304+
`createSequence` DDL) was **also removed** — OMDB is now pure data-access." Every deleted
305+
capability is a **write**. The read half went with it only because the two were one service
306+
behind one flag.
307+
308+
ADR-0015 is about schema **authority** — who is allowed to emit DDL, and the answer is the
309+
TypeScript toolchain alone. A validator that only introspects and compares claims no
310+
authority. **Program C is read-only by hard rule: it emits no DDL, ever, under any flag.**
311+
That is the line that keeps ADR-0015 intact, and it is not a soft guideline — an
312+
`autoCreate` option must not exist, because its existence is what made the last one
313+
removable.
314+
315+
### C2. The gap it closes
316+
317+
The existing gates are build-time and, for the schema link, Node-only:
318+
319+
| Gate | When | Which ports | What it proves |
320+
|---|---|---|---|
321+
| `verify --codegen` | build | all five | generated code and constants match the metadata |
322+
| `verify --db` | build / CI | **Node only** | metadata matches the DB *the developer or CI* pointed at |
323+
| `verify --replay` / `--replay-snapshot` | build | Node only | the committed migration chain applies and rebuilds the recorded snapshot |
324+
| **Program C** | **process start** | **the port the service runs in** | the DB **this process is connected to** matches the model it just loaded |
325+
326+
The failure classes only Program C can see: the deployed artifact pointed at the wrong
327+
database; a migration that never ran in *this* environment; a hand-patched production table;
328+
a read replica behind on schema; two services sharing one database where one shipped a newer
329+
model.
330+
331+
The strongest form of the argument is a direct consequence of ADR-0015: **the four ports that
332+
cannot run `verify --db` are the four that most need this.** A Java, Kotlin, C# or Python
333+
service runs against a schema it does not own and today has no mechanism at all for detecting
334+
that the schema disagrees with the metadata it loaded. Centralising schema authority in Node
335+
was right; leaving the other four ports blind at runtime was its unpriced cost.
336+
337+
### C3. Compare against the committed snapshot, not against metadata
338+
339+
The obvious implementation — each port diffs live introspection against loaded metadata — is
340+
wrong, because it re-forks the diff engine ADR-0015 consolidated into one place, in four
341+
languages, and that is the exact failure the ADR was written to end.
342+
343+
Instead: **each port compares live introspection against the committed schema snapshot** that
344+
the TypeScript toolchain already produces and that `verify` has gated since
345+
[#292](https://github.com/metaobjectsdev/metaobjects/issues/292). Each port then needs only
346+
(a) dialect-specific introspection queries and (b) a comparison of two schema descriptions —
347+
never a diff engine, never a notion of what the schema *should* be derived independently.
348+
349+
This also completes the chain, and is why the three programs belong in one spec. Each link is
350+
checked somewhere, by something already built:
351+
352+
```
353+
metadata --(verify --codegen)--> generated code + name constants [every port, build]
354+
metadata --(verify --db, #292)--> committed schema snapshot [Node, build]
355+
snapshot --(verify --replay-snapshot)--> migration chain [Node, build]
356+
snapshot --(Program C)--> the live DB this process uses [every port, boot]
357+
```
358+
359+
A wrong physical name in a generated constant would have to survive all four to reach
360+
production, and it cannot, because every link is anchored to the same metadata.
361+
362+
**Where the chain genuinely breaks, stated honestly:** column naming is *config*, not
363+
metadata (`KotlinExposedTableGenerator.kt:44-56`). Two ports generating against one database
364+
with different `columnNaming` settings each pass their own `verify --codegen` while
365+
disagreeing with each other. The composition therefore holds only while the naming config
366+
agrees across ports — which is a strong argument for **declaring `@column` explicitly**, since
367+
a declared physical name is config-independent. The showcase model already does this, and its
368+
comment already says why.
369+
370+
### C4. Open shape questions
371+
372+
- **Fail-fast or warn?** A startup gate that hard-fails takes down a fleet on a false
373+
positive, and the schema-drift engine has a false-positive history (the D1 phantom-drift
374+
batch in 0.20.2). Boot is a worse place to be wrong than CI. Likely: warn by default, fail
375+
on an explicit opt-in, never silent.
376+
- **How deep?** Table/view presence, column presence, nullability and a coarse type class is
377+
probably the whole valuable set. Full type fidelity re-creates the diff engine by degrees.
378+
- **Cost.** Dialect-specific introspection (`information_schema`/`pg_catalog`,
379+
`PRAGMA table_info`, …) × four ports. Scope discipline decides whether this is small.
380+
288381
## The doctrine, and where it is taught
289382

290383
Getting rid of magic strings is the point; the constants are only the mechanism. The
@@ -312,6 +405,9 @@ highest-leverage work is the part that reaches adopters:
312405
- **No removal of `$table` / `dbCol` / the existing descriptor keys.** Additive only.
313406
- **Not a runtime metadata API.** These are compile-time constants; the reflective routes
314407
each ORM already offers are unaffected and, where they are typed handles, preferred.
408+
- **No DDL from Program C, under any flag.** No `autoCreate`, no create-if-missing, no
409+
"just for dev". Schema authority stays with the TypeScript toolchain (ADR-0015). The
410+
option's *existence* is what made the previous validator removable.
315411

316412
---
317413

@@ -325,7 +421,14 @@ highest-leverage work is the part that reaches adopters:
325421
coverage. Recommended as coverage-only above, but if the curated names turn out to be
326422
mechanically derivable after all, one generator for five ports is simpler than a
327423
generator plus an exception.
328-
3. **Whether the motivating adopter code is TypeScript-only.** If it is, Program A's
424+
3. **Program C's failure mode and depth** — see §C4. Warn-by-default with opt-in
425+
fail-fast is the likely answer, but boot-time behaviour deserves its own decision rather
426+
than inheriting CI's.
427+
4. **Whether Program C is one FR or three.** It has an independent justification (four ports
428+
are blind at runtime today) and could be specced separately. It is here because it closes
429+
the last link in the same source-of-truth chain, which is only visible when all three are
430+
read together.
431+
5. **Whether the motivating adopter code is TypeScript-only.** If it is, Program A's
329432
"separate artifact in five ports" arithmetic collapses and merging into the existing TS
330433
descriptor becomes the cheaper right answer — it was verified safe (see below).
331434

0 commit comments

Comments
 (0)