Skip to content

fix: resolve non-object references, make dbImport/dialect optional, stamp gen-state engine version - #245

Open
dmealing wants to merge 3 commits into
mainfrom
fix/gen-engine-version-stamp-and-adopter-docs
Open

fix: resolve non-object references, make dbImport/dialect optional, stamp gen-state engine version#245
dmealing wants to merge 3 commits into
mainfrom
fix/gen-engine-version-stamp-and-adopter-docs

Conversation

@dmealing

@dmealing dmealing commented Jul 29, 2026

Copy link
Copy Markdown
Member

Intent

Fable-adjudicated fixes for the 'batch of small fixes'. The maintainer challenged that I'd been lazy — documenting adopter papercuts instead of fixing them — so Fable audited all three #194 constraints against the code and ruled two of them real bugs to FIX. This branch already carries the earlier-validated #232 (gen-state engine-version stamp + the pipeline's F1 isolation fix), unchanged. The new work fixes:

ITEM 2 (metadata pkg): the loader symbol table indexed ONLY object.* nodes, so a ReferenceDescriptor targeting a custom top-level type (targetType:'adapter') registered cleanly then UNCONDITIONALLY failed every reference with a false 'does not resolve to an object' — a silently-unsatisfiable config, breaking the mechanism's 'present and future' promise. Fix: symbol table keyed PER node type (byType map); resolve(type,ref,referrerPkg) added to SymbolTable with resolveObject delegating; and a TOP-LEVEL node of any type now establishes package context for its subtree (previously only objects did). Object-target resolution byte-identical: 2252 metadata tests + whole conformance corpus unchanged; strictly enabling. Test added (probe->adapter resolves; dangling errors naming the kind).

ITEM 3 (codegen-ts + cli): dbImport/dialect were REQUIRED config even for a value-object-only project that emits zero DB code. Made them optional on the USER config; ResolvedGenConfig (generators' view) keeps them required so no generator changes. The runner guards BEFORE defaulting: a model with any concrete non-object.value object (entity/projection) that omits them throws a clear error naming the entities — never a silent sqlite default for a forgotten Postgres dialect (fail-closed). VO-only gets inert placeholders. This rippled to ONE cli consumer (gen.ts display header via output.ts GenResultShape.dialect) which is now widened to show the dialect only when present — the pre-push build+typecheck gate correctly caught this (a stale-dist typecheck had missed it); now build+typecheck clean across all 18 packages. 1017 codegen-ts + 141 golden + 418 cli tests green; DB-project output unchanged. VO-only test added (omit both=>generates; DB entity omitting both=>clear throw; with both=>normal).

ITEM 1 (kept as DOCUMENT): requiring registry.extend on metadata.root to license a custom top-level type is chartered fail-closed design (FR-033 rejected auto-licensing). Docs rewritten into a 'defining a custom top-level node type' how-to that also documents item 2's now-working non-object references.

Deliberate: kept ResolvedTarget.dbImport as string (defaulting) rather than widening to string|undefined, to avoid rippling undefined through render-context; per-type symbol-table bucketing also prevents a template and object sharing pkg::Name from colliding.

What Changed

  • Loader symbol table is now keyed per node type (byType map) with a new resolve(type, ref, referrerPkg) (which resolveObject delegates to): a reference descriptor targeting a custom non-object top-level type (its targetType is a free string) now resolves instead of unconditionally failing with a false "does not resolve to an object", and a top-level node of any type now establishes package context for its subtree. Error messages name the expected kind; object-target resolution is byte-identical.
  • dbImport / dialect are now optional on the user-facing metaobjects.config.ts (still required on ResolvedGenConfig): the runner guards before defaulting — a model with any concrete non-object.value object that omits them throws a clear error naming the entities (fail-closed, never a silent sqlite default for a forgotten Postgres dialect), while a value-object-only project gets inert placeholders. The CLI gen header now shows the dialect only when present.
  • The codegen runner stamps its engine version into a persistent .metaobjects/.gen-state/.engine.json (separate from .hashes.json, never affects the merge) and warns when it changed since the last gen; adopter docs are rewritten into a "defining a custom top-level node type" how-to covering the now-working non-object references.

Risk Assessment

✅ Low: Clean, well-bounded change: the symbol-table refactor is byte-identical for every core reference path (all core descriptors target TYPE_OBJECT and no core reference-bearing node is top-level-non-object), the optional dbImport/dialect guard is fail-closed and reads raw config before defaulting, and the engine-version stamp is informational and isolated to a separate file under already-gitignored .gen-state — no material bugs found.

Testing

Built the TS packages and drove the actual meta CLI and metadata loader: ITEM 3 is shown three ways via real meta gen (VO-only with omitted dbImport/dialect generates with a dialect-less header; DB-entity omitting them fail-closes with a clear error naming the entity; with both set it generates and shows the dialect in the header), ITEM 2 is shown via the real loader (a custom non-object adapter reference resolves cleanly, a dangling one errors naming the kind), and #232 is shown via two real meta gen runs (the .engine.json stamp is written and an engine change emits the explanatory warning). The four focused test files pass (13 tests) and a 55-test regression set over the changed config/output/reference surfaces passes, confirming object-target resolution stays byte-identical. No failures; worktree left clean (only gitignored build outputs).

Evidence: ITEM 3 — value-object-only gen (dbImport/dialect omitted)

$ meta gen --format text meta gen — generated NEW generated/Address.ts 1 written exit=0

########## ITEM 3 — scenario 1: value-object-only project, dbImport/dialect OMITTED ##########
$ meta gen --format text
meta gen — generated

  NEW        generated/Address.ts

  1 written

exit=0
Evidence: ITEM 3 — DB entity omitting dbImport/dialect fail-closes

$ meta gen meta: gen failed: codegen config is missing dialect and dbImport — required because this model generates database code for: User. Set dialect and dbImport in metaobjects.config.ts. (Only a value-object-only model may omit them.) exit=1

$ meta gen
meta: gen failed: codegen config is missing dialect and dbImport — required because this model generates database code for: User. Set dialect and dbImport in metaobjects.config.ts. (Only a value-object-only model may omit them.)
exit=1
Evidence: ITEM 3 — DB entity WITH dbImport+dialect generates, header shows dialect

$ meta gen --format text meta gen — postgres, generated NEW generated/User.ts 1 written exit=0

$ meta gen --format text
meta gen — postgres, generated

  NEW        generated/User.ts

  1 written

exit=0
Evidence: ITEM 2 — non-object reference resolves; dangling errors naming the kind

===== VALID non-object reference (probe -> adapter resolves) ===== OK — no errors (reference resolved) ===== DANGLING non-object reference ===== ERR_PROBE_ADAPTER_UNRESOLVED: probe.ping "HealthCheck" @adapterRef "NoSuchAdapter" does not resolve to a adapter.

===== VALID non-object reference (probe -> adapter resolves) =====
OK — no errors (reference resolved)

===== DANGLING non-object reference =====
ERR_PROBE_ADAPTER_UNRESOLVED: probe.ping "HealthCheck" @adapterRef "NoSuchAdapter" does not resolve to a adapter.
Evidence: #232 — engine-version change emits an explanatory warning

$ meta gen meta: codegen engine 0.20.8 → 0.20.9 since last gen — generated output may differ; see CHANGELOG. gen[1]{file,status}: generated/User.ts,unchanged summary: 1 unchanged exit=0

$ meta gen
meta: codegen engine 0.20.8 → 0.20.9 since last gen — generated output may differ; see CHANGELOG.
gen[1]{file,status}:
  generated/User.ts,unchanged
summary: 1 unchanged
help[2]: typecheck the generated code with `npx tsc`,create your database tables with `meta migrate --from-db --db <url> --dialect <sqlite|postgres> --slug init --apply`
exit=0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • bun test packages/metadata/test/validation-registry.test.ts — 4 pass: non-object reference (probe→adapter) resolves; dangling reference errors naming the kind ('does not resolve to a adapter'); widget object-reference valid/dangling cases.
  • bun test packages/codegen-ts/test/vo-only-config-optional.test.ts — 3 pass: VO-only omit dbImport/dialect generates; DB-entity omit throws clear error naming the entity; DB-entity with both generates.
  • bun test packages/codegen-ts/test/engine-version-stamp.test.ts — 3 pass: unstamped→undefined; save/load round-trip writes separate .engine.json (not .hashes.json); malformed→undefined never throws.
  • bun test packages/codegen-ts/test/metaobjects-config.test.ts packages/cli/test/unit/output-gen.test.ts packages/metadata/test/relationship-definition-completeness.test.ts packages/metadata/test/relationship-m2m.test.ts packages/metadata/test/template-validation.test.ts — 55 pass: confirms byte-identical object-target resolution + the config-shape and CLI dialect-header changes.
  • Manual end-user CLI run meta gen on a real value-object-only project with dbImport/dialect omitted → header meta gen — generated (no dialect), generates Address.ts.
  • Manual end-user CLI run meta gen on a DB-entity project omitting dbImport/dialect → fail-closed error naming User, exit 1.
  • Manual end-user CLI run meta gen --format text on a DB-entity project WITH dbImport+dialect:postgres → header meta gen — postgres, generated, generates User.ts.
  • Manual loader run with a custom adapter/probe provider: valid non-object reference resolves (no errors); dangling reference → ERR_PROBE_ADAPTER_UNRESOLVED ... does not resolve to a adapter.
  • Manual end-user CLI run meta gen twice: first stamps .metaobjects/.gen-state/.engine.json (codegenVersion 0.20.9); after simulating an older stamp, re-run warns codegen engine 0.20.8 → 0.20.9 since last gen and refreshes the stamp.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dmealing and others added 3 commits July 28, 2026 22:09
…e; docs(#194): adopter constraints

#232 — meta gen records the @metaobjectsdev/codegen-ts engine version alongside
the gen-state hashes (a separate .engine.json, never part of the three-way merge)
and prints one informational line when it differs from the last run, so a
post-`npm update` regen diff is explained rather than surprising. No change to
generated output.

#194 — document three adopter constraints in extending-with-providers.md: a new
top-level node type needs registry.extend on metadata.root's child rules; the
built-in ref resolver only indexes object.* targets; and dbImport/dialect are
required config even for value-object-only projects (inert when no DB code is
generated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJRi8FtEW24z9HKGUL1xh8
…onal (replaces docs)

A Fable adjudication of the three #194 adopter constraints ruled two of them real
bugs the prior commit had merely DOCUMENTED rather than fixed. This fixes both in
code and keeps only the genuinely correct-by-design one as docs:

- Item 2 (FIX): a ReferenceDescriptor's targetType is a free string, but the loader
  symbol table indexed only object.* nodes, so a descriptor targeting a custom
  top-level type resolved cleanly then unconditionally failed with a false
  'does not resolve to an object'. The symbol table is now keyed per node type; a
  reference to any registered top-level kind resolves under the ADR-0042 contract,
  and a top-level non-object node establishes package context for its subtree.
  Object-target refs are byte-identical (2252 metadata tests green).

- Item 3 (FIX): dbImport/dialect were required config even for a value-object-only
  project that emits zero DB code. They are now optional on the user config; the
  runner fills inert defaults when absent AND no DB object is present, and throws a
  clear error naming the entities when a DB-emitting model omits them (never a
  silent sqlite default for a forgotten Postgres dialect). Resolved config the
  generators consume stays required — DB-project output unchanged.

- Item 1 (DOCUMENT): requiring registry.extend on metadata.root to license a custom
  top-level type is chartered fail-closed design (FR-033) — kept as docs, rewritten
  to a 'defining a custom top-level node type' how-to that also notes item 2's fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TJRi8FtEW24z9HKGUL1xh8
@dmealing dmealing changed the title feat(codegen-ts): stamp codegen engine version in gen-state and warn on change fix: resolve non-object references, make dbImport/dialect optional, stamp gen-state engine version Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant