Skip to content

docs(agent-context): make schema-apply doctrine per-stack (#192) - #263

Merged
dmealing merged 2 commits into
mainfrom
fix/192-flyway-scaffold-doctrine
Aug 2, 2026
Merged

docs(agent-context): make schema-apply doctrine per-stack (#192)#263
dmealing merged 2 commits into
mainfrom
fix/192-flyway-scaffold-doctrine

Conversation

@dmealing

@dmealing dmealing commented Aug 2, 2026

Copy link
Copy Markdown
Member

Intent

Branch fix/192-flyway-scaffold-doctrine fixes the actionable-now half of issue #192 (npm-side; the agent-context emitter is TS-owned).

THE PROBLEM: the shared agent-context always-on scaffold doctrine (agent-context/templates/always-on.md.mustache, which generates .metaobjects/AGENTS.md + CLAUDE.md for every port) asserted 'apply schema only through meta migrate' UNIVERSALLY. ADR-0015 removed the Java meta:migrate mojo and the Flyway output adapter was never built, so a JVM/Flyway consumer has no such command — the generated scaffold misdirected adopters (human + agent) to tooling not shipped for their stack.

THE FIX (scope = the scaffold-text correction only; building the Flyway output adapter is the separate, explicitly-deferred other half of #192): reword the doctrine to attribute the schema-apply mechanism PER STACK — meta migrate on the Node/TS-owned migrate stack (ADR-0015); on Flyway/Liquibase stacks the migration is authored to MATCH the generated schema and enforced by the boot-time meta verify --db drift gate. The core principle is unchanged (schema is codegen-derived; never ad-hoc hand-apply to the live DB; always drift-gate). Regenerated the four agent-context conformance fixtures (java-react, java-kotlin-react-tanstack, python, ts-react-tanstack — AGENTS.md + CLAUDE.md) via the repo's regen script; the repo-root agent-context/ is the tracked source, the sdk copy is a gitignored build artifact.

Verified: agent-context-conformance 4/4 pass, full sdk agent-context suite 37/37 pass. This is a documentation/scaffold-content change only — no code logic. INTENT: do not treat the reworded doctrine or the regenerated fixtures as a defect; the fixtures are the expected regen output of the template change.

What Changed

  • Reworded the shared agent-context/templates/always-on.md.mustache scaffold doctrine to attribute the schema-apply mechanism per stack instead of asserting "apply schema only through meta migrate" universally — meta migrate on the Node/TS-owned migrate stack (ADR-0015), and on Flyway/Liquibase stacks the migration is authored to match the generated schema and drift-gated via meta verify --db.
  • Regenerated the four agent-context conformance fixtures (AGENTS.md + CLAUDE.md for java-react, java-kotlin-react-tanstack, python, ts-react-tanstack) from the updated template via the repo's regen script.
  • Dropped an overclaimed "boot-time drift gate" phrase so live-DB enforcement is correctly attributed to the Node meta verify --db gate, matching the sibling verify skill and ADR-0015.

Risk Assessment

✅ Low: Docs-only change is now well-bounded and consistent — the over-claimed boot-time-gate wording is removed, the accurate Node meta verify --db drift gate remains, all 8 fixtures are byte-identical and match the template, and nothing outside the template + fixtures was touched.

Testing

Installed deps once at the bun workspace root, then ran the targeted validation: the agent-context-conformance suite (4/4 pass — live assemble() output equals the committed goldens byte-for-byte), the full sdk agent-context suite (42/42 pass, 0 fail), and the repo's own regen script (reproduced all four fixtures with a clean working tree, proving the goldens are exactly the template's regen output). For product-level evidence I rendered the scaffold through the live pipeline for a Java/Flyway and a TS stack and confirmed the corrected per-stack doctrine is present and the old universal "Apply schema only through meta migrate" misdirection is gone — and that the Java output explicitly names Flyway. This is a docs/scaffold-content change with no code logic; the reworded doctrine and regenerated fixtures are the intended output. Working tree left clean; all evidence written to the evidence directory.

Evidence: Doctrine presence check (live assemble output, Java/Flyway + TS)

[OK] java-react AGENTS.md: corrected-per-stack=true old-universal-present=false Flyway mentioned (JVM migration tooling) = true [OK] java-react CLAUDE.md: corrected-per-stack=true old-universal-present=false [OK] ts-react-tanstack AGENTS.md: corrected-per-stack=true old-universal-present=false [OK] ts-react-tanstack CLAUDE.md: corrected-per-stack=true old-universal-present=false OLD universal phrasing (must be ABSENT everywhere): "Apply schema only through meta migrate" failures=0

[OK] java-react AGENTS.md: corrected-per-stack=true old-universal-present=false
      Flyway mentioned (JVM migration tooling) = true
[OK] java-react CLAUDE.md: corrected-per-stack=true old-universal-present=false
[OK] ts-react-tanstack AGENTS.md: corrected-per-stack=true old-universal-present=false
[OK] ts-react-tanstack CLAUDE.md: corrected-per-stack=true old-universal-present=false

OLD universal phrasing (must be ABSENT everywhere): "Apply schema only through `meta migrate`"
failures=0
Evidence: Rendered .metaobjects/AGENTS.md a Java/Flyway consumer actually receives (live assemble output)

Corrected principle bullets now ship to the JVM consumer: - Pattern-derivable from metadata = codegen, never hand-write — ... The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files (Flyway/Liquibase), they must match the generated schema, not invent it. - The live database is a derived artifact too — ... Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is meta migrate (metadata → DDL, ADR-0015); on a JVM stack whose migration tool meta migrate does not emit for (e.g. Flyway/Liquibase) author the migration to match the generated schema (the source of truth). ... Run meta verify --db after any DB-touching work to catch that drift early.

# Working with MetaObjects in this project

> Stack: java server, react client.
>
> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
> **root `CLAUDE.md`**, not here — this file is regenerated; see the note at the end before editing it.

MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the durable
spine; generated code is the disposable artifact. Regenerate with `mvn metaobjects:generate`.

## Principles
- **Adopting onto existing code? Metadata FOLLOWS the code.** On a migration (existing working code / live DB), author metadata + tune codegen to *reproduce* what the code already is — native types (`field.uuid` when the code uses `UUID`, not `field.string`), names, nullability — so regen changes as little existing code as possible. The only existing code that should change is the hand-written layer codegen replaces; ask when a modeling choice is ambiguous. (Greenfield: model-first, below.)
- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files (Flyway/Liquibase), they must **match** the generated schema, not invent it.
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a JVM stack whose migration tool `meta migrate` does not emit for (e.g. **Flyway**/Liquibase) author the migration to match the generated schema (the source of truth). A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db` after any DB-touching work to catch that drift early.
- Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions).
- Use the generated constants for any string that names metadata.
- The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases.

## Authoring rules you must not violate
- Nodes are fused-key maps: `{"<type>.<subType>": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
- Package paths use `::` (`acme::common::id`).

## Keep all MetaObjects ports in sync
MetaObjects ships as separate packages per language on DIFFERENT version lines (npm/PyPI/NuGet `0.x`/`1.x`, Maven Central `7.x`/`8.x`). Because the numbers differ by ecosystem, a stale port is INVISIBLE — an old TS client next to a new Java backend *looks* fine. Ports are only truly in sync when every one implements the same **Metamodel spec version** (`metamodelVersion`, on the registry manifest). Upgrade ALL ports together and confirm they land on the same Metamodel version; a lagging port silently disagrees on vocabulary + wire behavior.

## Going deeper (Claude Code)
For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the
matching `metaobjects-*` skill — its body links the `references/<lang>.md` fragment
installed for this project's stack.

## Refreshing / customizing this generated context
This file and the `metaobjects-*` skills are **generated** by MetaObjects for this project's
stack (plain Markdown, no tools/hooks) — refresh with `meta init --refresh-docs`. Don't hand-edit
them expecting the edit to survive: a refresh leaves a file you've edited intact and writes the
new copy to `<path>.new` (only `--force` overwrites in place — never `--force` a file with local
edits). For project-specific guidance, put it in your **root `CLAUDE.md`** (always-on concerns) or
copy a skill to a new name (skill-level overrides) — don't edit the generated originals in place.

After a refresh, for each `<path>.new`: diff it against `<path>`, **keep the hand-edits and take
the upstream changes**, write the merged result back to `<path>`, then delete `<path>.new`.
Evidence: Live render evidence producer (calls assemble() for java-react + ts-react-tanstack)
// Evidence producer: renders the always-on scaffold through the LIVE assemble()
// pipeline for a Java/Flyway stack and a TS stack, then checks the end-user-facing
// doctrine text. Proves the template change ships the corrected, stack-attributed
// wording and no longer misdirects a JVM consumer to a `meta migrate` they lack.
import { join } from "node:path";
import { writeFileSync, mkdirSync } from "node:fs";
import { assemble } from "/home/doug/.no-mistakes/worktrees/4a36a911fd68/01KZ1QWMA07Z4RDJASY9WRDC89/server/typescript/packages/sdk/src/agent-context/assemble.ts";
import { makeStack } from "/home/doug/.no-mistakes/worktrees/4a36a911fd68/01KZ1QWMA07Z4RDJASY9WRDC89/server/typescript/packages/sdk/src/agent-context/resolve.ts";

const CONTENT_ROOT = "/home/doug/.no-mistakes/worktrees/4a36a911fd68/01KZ1QWMA07Z4RDJASY9WRDC89/agent-context";
const OUT = "/tmp/no-mistakes-evidence/01KZ1QWMA07Z4RDJASY9WRDC89";
mkdirSync(OUT, { recursive: true });

// Old universal phrasing that misdirected a JVM/Flyway consumer.
const FORBIDDEN = "Apply schema only through `meta migrate`";
// Corrected, stack-attributed markers.
const NEW_PERSTACK = "Drive every schema change from the metadata, never ad-hoc";
const FLYWAY_MARKER = "Flyway";

const stacks = [
  { label: "java-react", servers: ["java"] as never, clients: ["react"] as never },
  { label: "ts-react-tanstack", servers: ["typescript"] as never, clients: ["react", "tanstack"] as never },
];

let report = "";
let failures = 0;

for (const s of stacks) {
  const files = assemble({ contentRoot: CONTENT_ROOT, stack: makeStack(s.servers, s.clients) });
  const agents = files.find((f) => f.path.endsWith("AGENTS.md"))!;
  const claude = files.find((f) => f.path.endsWith("CLAUDE.md"))!;
  writeFileSync(join(OUT, `AGENTS.${s.label}.md`), agents.contents);
  writeFileSync(join(OUT, `CLAUDE.${s.label}.md`), claude.contents);

  for (const [which, contents] of [["AGENTS.md", agents.contents], ["CLAUDE.md", claude.contents]] as const) {
    const hasNew = contents.includes(NEW_PERSTACK);
    const hasForbidden = contents.includes(FORBIDDEN);
    const status = hasNew && !hasForbidden ? "OK" : "FAIL";
    if (status === "FAIL") failures++;
    report += `[${status}] ${s.label} ${which}: corrected-per-stack=${hasNew} old-universal-present=${hasForbidden}\n`;
    if (s.label === "java-react" && which === "AGENTS.md") {
      report += `      Flyway mentioned (JVM migration tooling) = ${contents.includes(FLYWAY_MARKER)}\n`;
    }
  }
}

report += `\nOLD universal phrasing (must be ABSENT everywhere): "${FORBIDDEN}"\n`;
report += `failures=${failures}\n`;
writeFileSync(join(OUT, "doctrine-check.txt"), report);
console.log(report);
process.exit(failures === 0 ? 0 : 1);
Evidence: Rendered TS consumer AGENTS.md (live assemble output, for comparison)
# Working with MetaObjects in this project

> Stack: typescript server, react, tanstack client.
>
> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your
> **root `CLAUDE.md`**, not here — this file is regenerated; see the note at the end before editing it.

MetaObjects is a metadata standard: typed metadata in `metaobjects/` is the durable
spine; generated code is the disposable artifact. Regenerate with `npx meta gen`.

## Principles
- **Adopting onto existing code? Metadata FOLLOWS the code.** On a migration (existing working code / live DB), author metadata + tune codegen to *reproduce* what the code already is — native types (`field.uuid` when the code uses `UUID`, not `field.string`), names, nullability — so regen changes as little existing code as possible. The only existing code that should change is the hand-written layer codegen replaces; ask when a modeling choice is ambiguous. (Greenfield: model-first, below.)
- Pattern-derivable from metadata = codegen, never hand-write — FKs, CRUD, validators, finders, and the database schema and migrations. The schema is a disposable, generated artifact: change the metadata and regenerate. Don't hand-write divergent SQL; where a stack owns its own migration files (Flyway/Liquibase), they must **match** the generated schema, not invent it.
- The **live database** is a derived artifact too — never hand-apply a schema change to a running DB (ad-hoc `psql`/console `ALTER`/`CREATE`/`DROP`), not even to preview a column or unblock a boot. Drive every schema change from the metadata, never ad-hoc: on the Node/TS-owned migrate stack that is `meta migrate` (metadata → DDL, ADR-0015); on a JVM stack whose migration tool `meta migrate` does not emit for (e.g. **Flyway**/Liquibase) author the migration to match the generated schema (the source of truth). A hand-applied ad-hoc change drifts the live DB from the metadata + migration history and collides at the next migrate/boot ("column already exists") — a state no migration can reproduce. Run `meta verify --db` after any DB-touching work to catch that drift early.
- Never hand-edit generated files — change the metadata and regenerate (three-way merge preserves hand-written regions).
- Use the generated constants for any string that names metadata.
- The loaded metadata model is READ-ONLY — never inject nodes or mutate the tree at load time (no "enrich the model on load" hooks). Need an extra field/column? Author it in the metadata, or derive it during codegen (read the metadata, emit output). Mutating the loaded model makes it diverge from what's declared — a bad practice reserved for very rare cases.

## Authoring rules you must not violate
- Nodes are fused-key maps: `{"<type>.<subType>": { ... }}` (e.g. `{"field.string": {"name": "email"}}`) — never split the type and subtype into separate keys.
- Attribute names are unique within a node; for multi-value use one array attr (`@values: [...]`).
- An inline `@maxLength: 50` equals an `attr` child of the same name — never write both.
- Package paths use `::` (`acme::common::id`).

## Keep all MetaObjects ports in sync
MetaObjects ships as separate packages per language on DIFFERENT version lines (npm/PyPI/NuGet `0.x`/`1.x`, Maven Central `7.x`/`8.x`). Because the numbers differ by ecosystem, a stale port is INVISIBLE — an old TS client next to a new Java backend *looks* fine. Ports are only truly in sync when every one implements the same **Metamodel spec version** (`metamodelVersion`, on the registry manifest). Upgrade ALL ports together and confirm they land on the same Metamodel version; a lagging port silently disagrees on vocabulary + wire behavior.

## Going deeper (Claude Code)
For authoring, codegen, runtime/UI, prompts, verify, or adoption-audit work, use the
matching `metaobjects-*` skill — its body links the `references/<lang>.md` fragment
installed for this project's stack.

## Refreshing / customizing this generated context
This file and the `metaobjects-*` skills are **generated** by MetaObjects for this project's
stack (plain Markdown, no tools/hooks) — refresh with `meta init --refresh-docs`. Don't hand-edit
them expecting the edit to survive: a refresh leaves a file you've edited intact and writes the
new copy to `<path>.new` (only `--force` overwrites in place — never `--force` a file with local
edits). For project-specific guidance, put it in your **root `CLAUDE.md`** (always-on concerns) or
copy a skill to a new name (skill-level overrides) — don't edit the generated originals in place.

After a refresh, for each `<path>.new`: diff it against `<path>`, **keep the hand-edits and take
the upstream changes**, write the merged result back to `<path>`, then delete `<path>.new`.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ agent-context/templates/always-on.md.mustache:14 - The reworded doctrine (and its 8 regenerated fixtures, same text) tells a JVM/Flyway consumer to author the migration to match the generated schema "and let the boot-time drift gate hold it honest." That framing is not stack-accurate — the change's own stated goal (commit: "stack-accurate, not meta-migrate-only"). The only shipped "boot-time drift gate" is the Kotlin MetadataStartupValidator (server/java/codegen-kotlin/.../KotlinValidatorGenerator.kt): ExposedTableValidator.check compares obj.metaFields vs the in-process Exposed Table.columns — it never introspects the live DB, so it cannot catch the hand-applied ad-hoc ALTER / divergent Flyway migration that THIS bullet's own failure mode ("column already exists") describes. The Java (non-Kotlin) port (codegen-spring) emits no startup validator at all, so a pure-Java consumer has no boot-time gate. The authoritative sibling guidance already says it correctly: metaobjects-verify/SKILL.md:98-103 and references/migration.md:13-14,102 + ADR-0015:229 establish that the live-DB drift gate on EVERY stack is the Node meta verify --db CLI (TS-owned shared engine), not a boot-time gate. The bullet's own closing line ("Run meta verify --db...") agrees and is the real gate, so an adopter following the whole bullet is still pointed at the right tool — but the inserted "boot-time drift gate" phrase overstates/non-exists for live-DB drift on the JVM path and contradicts the sibling verify skill. Concrete reachable path: a JVM/Flyway adopter relies on the stated boot-time gate, hand-applies or mis-authors a Flyway migration, and nothing catches the live-DB drift. Flagged ask-user because the intent explicitly says "do not treat the reworded doctrine as a defect" — the author should decide whether to reconcile the wording (e.g., attribute enforcement to Node meta verify --db, matching the verify skill, rather than "boot-time drift gate").

🔧 Fix: fix(#192): drop overclaimed boot-time drift-gate wording
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • git diff 88d8d73..25149a6d -- agent-context/templates/always-on.md.mustache — confirmed template reword to per-stack attribution (Flyway/Liquibase marker + ADR-0015 reference), core drift-gate principle unchanged
  • bun test packages/sdk/test/agent-context-conformance.test.ts (from server/typescript) — 4/4 pass, 74 expect() calls: assemble() output == committed expected/ goldens byte-for-byte for java-react, java-kotlin-react-tanstack, python, ts-react-tanstack
  • bun test packages/sdk/test/agent-context-conformance.test.ts packages/sdk/test/agent-context-capability-grounding.test.ts packages/sdk/test/agent-context/ — 42/42 pass, 156 expect() calls, 0 fail (the full sdk agent-context suite; 37 in the intent grew to 42 with added tests, all green)
  • bun packages/sdk/scripts/regen-agent-context-conformance.ts then git status --porcelain — regen reproduced all 4 fixtures (14/15/20/16 files) with ZERO diff; working tree clean. Directly confirms the fixtures are the expected regen output of the template change
  • bun /tmp/no-mistakes-evidence/01KZ1QWMA07Z4RDJASY9WRDC89/render-scaffold.ts — live assemble() render of AGENTS.md+CLAUDE.md for java-react and ts-react-tanstack stacks: corrected per-stack doctrine present in all 4 outputs, old universal 'Apply schema only through meta migrate' phrasing ABSENT in all 4, Flyway named in the JVM output (failures=0, exit 0)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dmealing and others added 2 commits August 2, 2026 13:19
…ta-migrate-only

The always-on scaffold doctrine asserted 'apply schema only through meta migrate'
universally. On a JVM/Flyway stack there is no such command (ADR-0015 removed the
meta:migrate mojo and the Flyway output adapter was never built), so the generated
.metaobjects/AGENTS.md misdirected adopters to tooling that isn't shipped for
their stack. Reword the doctrine to attribute the apply mechanism per stack:
meta migrate on the Node/TS-owned migrate stack; on Flyway/Liquibase stacks the
migration is authored to MATCH the generated schema and enforced by the boot-time
meta verify --db drift gate. The core principle (schema is codegen-derived; never
ad-hoc hand-apply to the live DB; always drift-gate) is unchanged.

Building the ADR-0015 Flyway-prefix output adapter on the shared migrate engine
remains a separate follow-up (the other half of #192). Regenerated the four
agent-context conformance fixtures (emitter is TS-owned).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
@dmealing
dmealing merged commit 6ea8deb into main Aug 2, 2026
1 check passed
@dmealing
dmealing deleted the fix/192-flyway-scaffold-doctrine branch August 2, 2026 17:44
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