From 92f067a0535febab70d173f4eb5bc2ab3ff1d382 Mon Sep 17 00:00:00 2001 From: Doug Mealing Date: Wed, 11 Nov 2026 19:30:33 -0500 Subject: [PATCH 1/4] feat(agent-context): working loop first, universal requirements pages, init --dialect The always-on file meta init scaffolds now opens with the whole loop for the declared stack: generate (one step per declared server, plus the web client), schema (first migration, apply-pending, rename and allow flags), requirements, and the verify commands. It drops from about 19 KB to about 5 KB; the principles move verbatim to metaobjects-codegen/references/principles.md and the port-sync and upgrade text to metaobjects-verify/references/upgrading.md. It no longer tells agents to read SKILL.md by path. - Requirements reference pages install in every stack (the concern gate observed nodes at init time, before a new project had any). meta gen and meta verify say when a scaffolded context lacks them. - meta gen advises wiring requirementTests() when requirements exist and nothing tests them. - meta init --dialect sqlite|postgres|mysql|d1, dialect-aware next steps, and a shorter scaffolded config comment carrying the generator import line. - migration.md documents --rename-column, --rename-table and --allow. - Skill trigger evals gain mid-task cases. - meta gen --list off a terminal is unchanged: its TOON default is parsed by tools and skills. --- CHANGELOG.md | 31 ++ agent-context/README.md | 7 +- agent-context/evals/README.md | 9 +- .../graders/trigger.md | 6 + .../prompt.md | 8 + .../graders/trigger.md | 6 + .../14-pos-midtask-need-new-output/prompt.md | 8 + .../graders/trigger.md | 8 + .../15-neg-midtask-business-logic/prompt.md | 8 + .../graders/trigger.md | 8 + .../16-neg-midtask-rename-migration/prompt.md | 8 + .../graders/trigger.md | 6 + .../14-pos-midtask-inline-prompt/prompt.md | 8 + .../graders/trigger.md | 8 + .../15-neg-midtask-retry-wrapper/prompt.md | 8 + agent-context/servers/csharp.meta.json | 5 +- agent-context/servers/java.meta.json | 5 +- agent-context/servers/kotlin.meta.json | 5 +- agent-context/servers/python.meta.json | 5 +- agent-context/servers/typescript.meta.json | 5 +- .../skills/metaobjects-authoring/SKILL.md | 4 +- .../skills/metaobjects-codegen/SKILL.md | 1 + .../references/principles.md | 66 +--- .../skills/metaobjects-verify/SKILL.md | 3 +- .../references/migration.md | 39 +- .../references/upgrading.md | 10 + agent-context/templates/always-on.md.mustache | 166 +++----- agent-context/templates/loops/client.md | 8 + agent-context/templates/loops/csharp.md | 17 + agent-context/templates/loops/java.md | 12 + agent-context/templates/loops/kotlin.md | 13 + agent-context/templates/loops/python.md | 18 + agent-context/templates/loops/typescript.md | 26 ++ docs/features/cli.md | 2 +- .../.claude/skills/metaobjects-audit/SKILL.md | 0 .../references/calibration.md | 0 .../references/capability-checklist.md | 0 .../metaobjects-audit/references/csharp.md | 167 ++++++++ .../references/drift-signatures.md | 0 .../references/prompt-anti-patterns.md | 0 .../references/requirements.md | 0 .../references/surface-review-axes.md | 0 .../references/vocabulary-hygiene.md | 0 .../skills/metaobjects-authoring/SKILL.md | 4 +- .../references/abstracts-and-overlay.md | 0 .../references/adopting-existing-code.md | 0 .../references/adopting-existing-models.md | 0 .../references/choosing-a-shape.md | 0 .../references/csharp-providers.md | 85 ++++ .../references/declaring-reports.md | 0 .../references/identities-and-indexes.md | 0 .../references/inheritance-tph.md | 0 .../references/json-columns.md | 0 .../references/metadata-dependencies.md | 0 .../references/model-first-rationale.md | 0 .../references/read-views-and-projections.md | 0 .../references/relationships.md | 0 .../references/reporting.md | 0 .../references/requirements.md | 0 .../references/sources.md | 0 .../skills/metaobjects-codegen/SKILL.md | 1 + .../references/any-stack.md | 0 .../references/choosing-layers.md | 0 .../metaobjects-codegen/references/csharp.md | 331 ++++++++++++++++ .../dependencies-and-shared-models.md | 0 .../references/owning-generators.md | 0 .../references/physical-names.md | 0 .../references/principles.md} | 66 +--- .../references/template-or-code.md | 0 .../references/wire-format.md | 0 .../skills/metaobjects-prompts/SKILL.md | 0 .../metaobjects-prompts/references/csharp.md | 150 +++++++ .../skills/metaobjects-runtime-ui/SKILL.md | 0 .../references/csharp.md | 115 ++++++ .../skills/metaobjects-verify/SKILL.md | 3 +- .../references/migration.md | 39 +- .../references/requirements.md | 0 .../references/upgrading.md | 10 + .../csharp/expected/.metaobjects/AGENTS.md | 88 +++++ .../csharp/expected/.metaobjects/CLAUDE.md | 88 +++++ .../csharp/stack.json | 1 + .../references/requirements.md | 154 ++++++++ .../skills/metaobjects-authoring/SKILL.md | 4 +- .../references/requirements.md | 225 +++++++++++ .../skills/metaobjects-codegen/SKILL.md | 1 + .../references/principles.md | 36 ++ .../skills/metaobjects-verify/SKILL.md | 3 +- .../references/migration.md | 39 +- .../references/requirements.md | 218 ++++++++++ .../references/upgrading.md | 10 + .../expected/.metaobjects/AGENTS.md | 181 +++++---- .../expected/.metaobjects/CLAUDE.md | 181 +++++---- .../references/requirements.md | 154 ++++++++ .../skills/metaobjects-authoring/SKILL.md | 4 +- .../references/requirements.md | 225 +++++++++++ .../skills/metaobjects-codegen/SKILL.md | 1 + .../references/principles.md | 36 ++ .../skills/metaobjects-verify/SKILL.md | 3 +- .../references/migration.md | 39 +- .../references/requirements.md | 218 ++++++++++ .../references/upgrading.md | 10 + .../expected/.metaobjects/AGENTS.md | 167 ++++---- .../expected/.metaobjects/CLAUDE.md | 167 ++++---- .../references/requirements.md | 154 ++++++++ .../skills/metaobjects-authoring/SKILL.md | 4 +- .../references/requirements.md | 225 +++++++++++ .../skills/metaobjects-codegen/SKILL.md | 1 + .../references/principles.md | 36 ++ .../skills/metaobjects-verify/SKILL.md | 3 +- .../references/migration.md | 39 +- .../references/requirements.md | 218 ++++++++++ .../references/upgrading.md | 10 + .../python/expected/.metaobjects/AGENTS.md | 160 ++++---- .../python/expected/.metaobjects/CLAUDE.md | 160 ++++---- .../references/requirements.md | 154 ++++++++ .../skills/metaobjects-authoring/SKILL.md | 4 +- .../references/requirements.md | 225 +++++++++++ .../skills/metaobjects-codegen/SKILL.md | 1 + .../references/principles.md | 36 ++ .../skills/metaobjects-verify/SKILL.md | 3 +- .../references/migration.md | 39 +- .../references/requirements.md | 218 ++++++++++ .../references/upgrading.md | 10 + .../expected/.metaobjects/AGENTS.md | 180 +++++---- .../expected/.metaobjects/CLAUDE.md | 180 +++++---- .../references/typescript.md | 218 ---------- .../references/typescript-providers.md | 74 ---- .../references/typescript-docs.md | 112 ------ .../references/typescript-document-store.md | 161 -------- .../references/typescript-mysql.md | 139 ------- .../references/typescript-prisma.md | 160 -------- .../references/typescript-retargeting.md | 116 ------ .../references/typescript-templates.md | 53 --- .../references/typescript.md | 373 ------------------ .../references/typescript.md | 244 ------------ .../references/typescript.md | 264 ------------- .../ts-requirements/stack.json | 1 - .../packages/cli/src/commands/gen.ts | 16 +- .../packages/cli/src/commands/init.ts | 127 +++--- .../packages/cli/src/commands/verify.ts | 5 +- server/typescript/packages/cli/src/index.ts | 6 +- .../cli/src/lib/agent-context-staleness.ts | 37 +- .../typescript/packages/cli/src/lib/args.ts | 4 + .../packages/cli/src/lib/detect-stack.ts | 45 +-- .../cli/src/lib/requirement-tests-advice.ts | 18 + .../typescript/packages/cli/test/cli.test.ts | 5 +- .../test/gen-requirement-advisories.test.ts | 133 +++++++ .../typescript/packages/cli/test/init.test.ts | 27 ++ .../cli/test/unit/detect-stack.test.ts | 114 +----- .../test/unit/init-scaffold-config.test.ts | 2 +- .../unit/requirement-tests-advice.test.ts | 21 + .../regen-agent-context-conformance.ts | 3 +- .../sdk/src/agent-context/assemble.ts | 131 +++--- .../packages/sdk/src/agent-context/resolve.ts | 24 +- .../sdk/src/agent-context/scaffold.ts | 30 ++ .../packages/sdk/src/agent-context/types.ts | 12 +- .../test/agent-context-conformance.test.ts | 6 +- .../sdk/test/agent-context/assemble.test.ts | 134 ++++--- .../sdk/test/agent-context/resolve.test.ts | 22 +- .../sdk/test/agent-context/scaffold.test.ts | 28 +- .../test/no-hardcoded-metadata-dir.test.ts | 8 +- 161 files changed, 5476 insertions(+), 3228 deletions(-) create mode 100644 agent-context/evals/skill-triggers/metaobjects-codegen/13-pos-midtask-generated-shape-wrong/graders/trigger.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-codegen/13-pos-midtask-generated-shape-wrong/prompt.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-codegen/14-pos-midtask-need-new-output/graders/trigger.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-codegen/14-pos-midtask-need-new-output/prompt.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-codegen/15-neg-midtask-business-logic/graders/trigger.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-codegen/15-neg-midtask-business-logic/prompt.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-codegen/16-neg-midtask-rename-migration/graders/trigger.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-codegen/16-neg-midtask-rename-migration/prompt.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-prompts/14-pos-midtask-inline-prompt/graders/trigger.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-prompts/14-pos-midtask-inline-prompt/prompt.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-prompts/15-neg-midtask-retry-wrapper/graders/trigger.md create mode 100644 agent-context/evals/skill-triggers/metaobjects-prompts/15-neg-midtask-retry-wrapper/prompt.md rename fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md => agent-context/skills/metaobjects-codegen/references/principles.md (67%) create mode 100644 agent-context/skills/metaobjects-verify/references/upgrading.md create mode 100644 agent-context/templates/loops/client.md create mode 100644 agent-context/templates/loops/csharp.md create mode 100644 agent-context/templates/loops/java.md create mode 100644 agent-context/templates/loops/kotlin.md create mode 100644 agent-context/templates/loops/python.md create mode 100644 agent-context/templates/loops/typescript.md rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-audit/SKILL.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-audit/references/calibration.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md (100%) create mode 100644 fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/csharp.md rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-audit/references/drift-signatures.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-audit/references/prompt-anti-patterns.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-audit/references/requirements.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-audit/references/surface-review-axes.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-audit/references/vocabulary-hygiene.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/SKILL.md (99%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/abstracts-and-overlay.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-code.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-models.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/choosing-a-shape.md (100%) create mode 100644 fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/csharp-providers.md rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/identities-and-indexes.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/inheritance-tph.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/json-columns.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/metadata-dependencies.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/model-first-rationale.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/read-views-and-projections.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/relationships.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/reporting.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/requirements.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-authoring/references/sources.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-codegen/SKILL.md (99%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-codegen/references/any-stack.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-codegen/references/choosing-layers.md (100%) create mode 100644 fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/csharp.md rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-codegen/references/dependencies-and-shared-models.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-codegen/references/physical-names.md (100%) rename fixtures/agent-context-conformance/{ts-requirements/expected/.metaobjects/CLAUDE.md => csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md} (67%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-codegen/references/wire-format.md (100%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-prompts/SKILL.md (100%) create mode 100644 fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-prompts/references/csharp.md rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-runtime-ui/SKILL.md (100%) create mode 100644 fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-runtime-ui/references/csharp.md rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-verify/SKILL.md (99%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-verify/references/migration.md (86%) rename fixtures/agent-context-conformance/{ts-requirements => csharp}/expected/.claude/skills/metaobjects-verify/references/requirements.md (100%) create mode 100644 fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/references/upgrading.md create mode 100644 fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md create mode 100644 fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md create mode 100644 fixtures/agent-context-conformance/csharp/stack.json create mode 100644 fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references/requirements.md create mode 100644 fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/requirements.md create mode 100644 fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md create mode 100644 fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/requirements.md create mode 100644 fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/upgrading.md create mode 100644 fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/references/requirements.md create mode 100644 fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/references/requirements.md create mode 100644 fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md create mode 100644 fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/requirements.md create mode 100644 fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/upgrading.md create mode 100644 fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/requirements.md create mode 100644 fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/references/requirements.md create mode 100644 fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/principles.md create mode 100644 fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/requirements.md create mode 100644 fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/upgrading.md create mode 100644 fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/requirements.md create mode 100644 fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/requirements.md create mode 100644 fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md create mode 100644 fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/requirements.md create mode 100644 fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/upgrading.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/typescript-providers.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-document-store.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-mysql.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-prisma.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-retargeting.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-templates.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/references/typescript.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md delete mode 100644 fixtures/agent-context-conformance/ts-requirements/stack.json create mode 100644 server/typescript/packages/cli/src/lib/requirement-tests-advice.ts create mode 100644 server/typescript/packages/cli/test/gen-requirement-advisories.test.ts create mode 100644 server/typescript/packages/cli/test/unit/requirement-tests-advice.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3492c0eb..918e4c2873 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,25 @@ Each item below is described in full under Added, Changed or Fixed; this list is ### Added +- **A working loop first in the agent context.** The always-on file `meta init` scaffolds + (`.metaobjects/AGENTS.md` and `CLAUDE.md`) now opens with the whole loop for the declared + stack, in order: configure and run the generators (one step per declared server, with the + import or class names to wire, plus the web client when one is declared), the schema + (`meta migrate --from-db` for the first migration, `apply-pending` at boot, and a later + change with `--rename-column`, `--rename-table` and `--allow`), requirements, and the verify + commands to run before calling work done. It shrinks from about 19 KB to about 5 KB. The + principles it carried move, word for word, to `metaobjects-codegen/references/principles.md`, + and the port-sync and upgrade paragraphs to `metaobjects-verify/references/upgrading.md`. It + no longer tells an agent to read a skill's `SKILL.md` by path, and says the loop needs no + skill. The skill trigger evals gain mid-task cases. +- **`meta init --dialect sqlite|postgres|mysql|d1`.** Writes the codegen `dialect` and, for + sqlite, postgres and d1, `migrate.dialect`, and the next steps print that dialect's first + migration. `--dialect d1` is `--d1`; the two together with different dialects are refused. + MySQL is codegen only, as before: the next steps say `meta migrate` does not own its schema. +- **`meta gen` names `requirementTests()` when nothing tests the requirements.** A model that + declares `requirement.*` nodes, with no `requirement-tests` generator wired, gets one advisory + line naming the import. The exit code is unchanged. + - **Cross-port vocabulary that only some ports implement so far (`implementedIn`).** A registry type or attr may name the ports that implement it so far (`"implementedIn": ["typescript"]` on its `spec/metamodel` entry, carried into the registry manifest). It is still cross-port @@ -499,6 +518,18 @@ Each item below is described in full under Added, Changed or Fixed; this list is ### Changed +- **The requirements reference pages install in every stack.** They used to install only when + `meta init` saw a `requirement.*` node, which a new project has not written yet, so a project + that declared requirements later had skills pointing at pages that were not there. `meta gen` + and `meta verify` now say so for a context scaffolded that way, and name + `meta init --docs-only --refresh-docs`. +- **A shorter scaffolded `metaobjects.config.ts` comment.** It now shows the import line for the + usual data layer (`entityFile`, `namesFile`, `barrel`) and the catalog command, and drops the + rest. +- **`meta gen --list` off a terminal is unchanged.** Its default there stays the TOON document, + which tools and the skills parse for `wire.import` and `layer`; the agent context gives a + one-line filter for a compact view instead. + - **A new top-level requirement declared into a dependency's package is refused** with `ERR_DEPENDENCY_PACKAGE_NOT_OWNED`, as a new object there already was. An overlay of the dependency's own requirement is still accepted. diff --git a/agent-context/README.md b/agent-context/README.md index c58135e9f9..81175b33bd 100644 --- a/agent-context/README.md +++ b/agent-context/README.md @@ -6,10 +6,11 @@ the slim always-on Markdown (`.metaobjects/AGENTS.md` + `CLAUDE.md`) and the six `metaobjects-*` Claude skills (each a universal `SKILL.md` plus the `references/.md` fragments matching the project's resolved stack). -- `servers/.meta.json` — per-server install + codegen command (drives the always-on). -- `templates/always-on.md.mustache` — the slim always-on body (`{{stackLine}}`, `{{codegenCommand}}`). +- `servers/.meta.json` — per-server install, codegen command and the always-on's verify step (`verifyStep`). +- `templates/always-on.md.mustache` — the slim always-on body: a working loop first (`{{stackLine}}`, `{{stackLoops}}`, `{{verifySteps}}`), then the rules. The full principles it shortens live in `metaobjects-codegen/references/principles.md`. +- `templates/loops/.md`, `templates/loops/client.md` — the generate steps of that loop, one per declared server (and one for a web client), concatenated by the assembler. - `skills//SKILL.md` — universal skill body. -- `skills//references/.md` — language/framework or concern fragment; installed iff `` is in the stack (`Stack.tokens`; concern tokens are OBSERVED from project state, e.g. `requirements` for a project declaring `requirement.*` nodes — see `SDK`'s `CONCERN_TOKENS`). +- `skills//references/.md` — language/framework fragment; installed iff `` is in the stack (`Stack.tokens`). The requirements pages are universal topic files: they install in every stack, because whether a project declares `requirement.*` nodes changes after `meta init` runs. - `skills//references/-.md` — a topic split out of a `.md` fragment; scoped by the same token. - `skills//references/.md` — any other name is a universal topic file and installs in every stack; the skill's `SKILL.md` says when to open it. diff --git a/agent-context/evals/README.md b/agent-context/evals/README.md index f4f3cc5895..355d6de9ec 100644 --- a/agent-context/evals/README.md +++ b/agent-context/evals/README.md @@ -7,6 +7,13 @@ one-message prompt plus a `tool_used: Skill` grader: positive cases require the fire, negative cases (near-misses: LLM code in a project that does not use MetaObjects prompts, model edits that need no generated output) forbid it. +Most cases are a fresh request. The `*-midtask-*` cases are the shape real work takes: the +prompt states what has already been done in the session (entities declared, `meta gen` run, a +migration applied) and then the next step. A skill that fires only on an opening request and +never once the agent is past the loop in the always-on file is the failure they catch, and the +negative ones catch the opposite: a skill loaded for a step the loop already covers (writing +business logic over generated types, running a rename migration). + - `skill-triggers//-pos-*|neg-*/` — `prompt.md` and `graders/trigger.md`, in the `claude plugin eval` case format. - `run-skill-triggers.sh` — wraps `agent-context/skills` in a throwaway plugin (all seven @@ -18,7 +25,7 @@ prompts, model edits that need no generated output) forbid it. agent-context/evals/run-skill-triggers.sh ``` -It starts real Claude sessions on your own login (25 cases x 3 runs = 75 sessions, a few +It starts real Claude sessions on your own login (31 cases x 3 runs = 93 sessions, a few minutes with `-j 4`) and writes `/result.json`. Use `--case ''` to run a subset. It is manual-only and not part of CI, because model behaviour is not deterministic. diff --git a/agent-context/evals/skill-triggers/metaobjects-codegen/13-pos-midtask-generated-shape-wrong/graders/trigger.md b/agent-context/evals/skill-triggers/metaobjects-codegen/13-pos-midtask-generated-shape-wrong/graders/trigger.md new file mode 100644 index 0000000000..b8e0fb8475 --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-codegen/13-pos-midtask-generated-shape-wrong/graders/trigger.md @@ -0,0 +1,6 @@ +--- +type: tool_used +tool: Skill +input_match: metaobjects-codegen +min: 1 +--- diff --git a/agent-context/evals/skill-triggers/metaobjects-codegen/13-pos-midtask-generated-shape-wrong/prompt.md b/agent-context/evals/skill-triggers/metaobjects-codegen/13-pos-midtask-generated-shape-wrong/prompt.md new file mode 100644 index 0000000000..effedb7844 --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-codegen/13-pos-midtask-generated-shape-wrong/prompt.md @@ -0,0 +1,8 @@ +--- +max_turns: 3 +timeout_seconds: 180 +allowed_tools: [Skill] +runs: 3 +--- + +This project uses MetaObjects (it has .metaobjects/config.json and a metaobjects/ directory of meta.*.json files). Progress so far: you added the Order and OrderLine entities, ran `npx meta gen`, and wired the generated Drizzle tables into the service. The typecheck now fails because the generated OrderLine.ts exports `orderLineInsertSchema` and the service expects `OrderLineInsert` as a named type next to it. Make the generated file export that type on every regen. diff --git a/agent-context/evals/skill-triggers/metaobjects-codegen/14-pos-midtask-need-new-output/graders/trigger.md b/agent-context/evals/skill-triggers/metaobjects-codegen/14-pos-midtask-need-new-output/graders/trigger.md new file mode 100644 index 0000000000..b8e0fb8475 --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-codegen/14-pos-midtask-need-new-output/graders/trigger.md @@ -0,0 +1,6 @@ +--- +type: tool_used +tool: Skill +input_match: metaobjects-codegen +min: 1 +--- diff --git a/agent-context/evals/skill-triggers/metaobjects-codegen/14-pos-midtask-need-new-output/prompt.md b/agent-context/evals/skill-triggers/metaobjects-codegen/14-pos-midtask-need-new-output/prompt.md new file mode 100644 index 0000000000..01cf245256 --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-codegen/14-pos-midtask-need-new-output/prompt.md @@ -0,0 +1,8 @@ +--- +max_turns: 3 +timeout_seconds: 180 +allowed_tools: [Skill] +runs: 3 +--- + +This project uses MetaObjects (it has .metaobjects/config.json and a metaobjects/ directory of meta.*.json files). Progress so far: entities are declared, `npx meta gen` writes the Drizzle tables, and the first migration is applied. Next on the task list: the admin service needs a TypeScript map from each entity name to its list of searchable column names, kept in step with the metadata. Nothing in `meta gen --list` emits it. Produce it. diff --git a/agent-context/evals/skill-triggers/metaobjects-codegen/15-neg-midtask-business-logic/graders/trigger.md b/agent-context/evals/skill-triggers/metaobjects-codegen/15-neg-midtask-business-logic/graders/trigger.md new file mode 100644 index 0000000000..5a6f0d6123 --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-codegen/15-neg-midtask-business-logic/graders/trigger.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: metaobjects-codegen +min: 0 +max: 0 +arm: both +--- diff --git a/agent-context/evals/skill-triggers/metaobjects-codegen/15-neg-midtask-business-logic/prompt.md b/agent-context/evals/skill-triggers/metaobjects-codegen/15-neg-midtask-business-logic/prompt.md new file mode 100644 index 0000000000..8b4e2ead6d --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-codegen/15-neg-midtask-business-logic/prompt.md @@ -0,0 +1,8 @@ +--- +max_turns: 3 +timeout_seconds: 180 +allowed_tools: [Skill] +runs: 3 +--- + +This project uses MetaObjects (it has .metaobjects/config.json and a metaobjects/ directory of meta.*.json files). Progress so far: the Invoice entity is declared, `npx meta gen` wrote Invoice.ts with its Drizzle table and Zod schemas, and the migration is applied. Next: in src/billing/totals.ts, write the function that sums an invoice's lines, applies the customer's discount percentage and rounds to cents. diff --git a/agent-context/evals/skill-triggers/metaobjects-codegen/16-neg-midtask-rename-migration/graders/trigger.md b/agent-context/evals/skill-triggers/metaobjects-codegen/16-neg-midtask-rename-migration/graders/trigger.md new file mode 100644 index 0000000000..5a6f0d6123 --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-codegen/16-neg-midtask-rename-migration/graders/trigger.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: metaobjects-codegen +min: 0 +max: 0 +arm: both +--- diff --git a/agent-context/evals/skill-triggers/metaobjects-codegen/16-neg-midtask-rename-migration/prompt.md b/agent-context/evals/skill-triggers/metaobjects-codegen/16-neg-midtask-rename-migration/prompt.md new file mode 100644 index 0000000000..e270bf7f55 --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-codegen/16-neg-midtask-rename-migration/prompt.md @@ -0,0 +1,8 @@ +--- +max_turns: 3 +timeout_seconds: 180 +allowed_tools: [Skill] +runs: 3 +--- + +This project uses MetaObjects (it has .metaobjects/config.json and a metaobjects/ directory of meta.*.json files). Progress so far: you renamed the Customer field `fullName` to `displayName` in metaobjects/meta.crm.json and regenerated. Now write the migration for it with `npx meta migrate` so the existing data in that column is kept. diff --git a/agent-context/evals/skill-triggers/metaobjects-prompts/14-pos-midtask-inline-prompt/graders/trigger.md b/agent-context/evals/skill-triggers/metaobjects-prompts/14-pos-midtask-inline-prompt/graders/trigger.md new file mode 100644 index 0000000000..ba923f268c --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-prompts/14-pos-midtask-inline-prompt/graders/trigger.md @@ -0,0 +1,6 @@ +--- +type: tool_used +tool: Skill +input_match: metaobjects-prompts +min: 1 +--- diff --git a/agent-context/evals/skill-triggers/metaobjects-prompts/14-pos-midtask-inline-prompt/prompt.md b/agent-context/evals/skill-triggers/metaobjects-prompts/14-pos-midtask-inline-prompt/prompt.md new file mode 100644 index 0000000000..ff5fa22466 --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-prompts/14-pos-midtask-inline-prompt/prompt.md @@ -0,0 +1,8 @@ +--- +max_turns: 3 +timeout_seconds: 180 +allowed_tools: [Skill] +runs: 3 +--- + +This project uses MetaObjects, including declared prompt templates (template.prompt nodes under metaobjects/ and their text under prompts/). Progress so far: you built the TicketSummary endpoint and it works. While wiring it you wrote the LLM call in src/tickets/summarize.ts with the prompt as a template literal that interpolates ticket.title, ticket.body and the last five comments, and you parse the reply with JSON.parse. Before you commit, bring that call in line with how this project does prompts. diff --git a/agent-context/evals/skill-triggers/metaobjects-prompts/15-neg-midtask-retry-wrapper/graders/trigger.md b/agent-context/evals/skill-triggers/metaobjects-prompts/15-neg-midtask-retry-wrapper/graders/trigger.md new file mode 100644 index 0000000000..7b6368ff0b --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-prompts/15-neg-midtask-retry-wrapper/graders/trigger.md @@ -0,0 +1,8 @@ +--- +type: tool_used +tool: Skill +input_match: metaobjects-prompts +min: 0 +max: 0 +arm: both +--- diff --git a/agent-context/evals/skill-triggers/metaobjects-prompts/15-neg-midtask-retry-wrapper/prompt.md b/agent-context/evals/skill-triggers/metaobjects-prompts/15-neg-midtask-retry-wrapper/prompt.md new file mode 100644 index 0000000000..1867bf0e68 --- /dev/null +++ b/agent-context/evals/skill-triggers/metaobjects-prompts/15-neg-midtask-retry-wrapper/prompt.md @@ -0,0 +1,8 @@ +--- +max_turns: 3 +timeout_seconds: 180 +allowed_tools: [Skill] +runs: 3 +--- + +This project uses MetaObjects, including declared prompt templates (template.prompt nodes under metaobjects/ and their text under prompts/). Progress so far: the TicketSummary prompt is declared, rendered and parsed through the generated code, and its tests pass. Next: the HTTP client that sends the rendered prompt to the model sometimes gets a 529. Add a retry with exponential backoff, three attempts, around that client call in src/llm/client.ts. diff --git a/agent-context/servers/csharp.meta.json b/agent-context/servers/csharp.meta.json index aeb474cb45..6d033e575a 100644 --- a/agent-context/servers/csharp.meta.json +++ b/agent-context/servers/csharp.meta.json @@ -2,8 +2,5 @@ "displayName": "C#", "install": "install the MetaObjects .NET tool (dotnet tool install) + add the MetaObjects.Codegen package", "codegenCommand": "dotnet meta gen", - "docsCommand": "dotnet meta gen", - "docsCondition": " (when the project's generator list declares the ApiDocsGenerator)", - "docsMissing": "Declare `ApiDocsGenerator` in the generator list in codegen/Generators.cs if it is missing", - "apiSubDir": "api/csharp" + "verifyStep": "`dotnet meta verify --codegen` (the generated code and the requirements)" } diff --git a/agent-context/servers/java.meta.json b/agent-context/servers/java.meta.json index b0fffe2464..b9ee208f5b 100644 --- a/agent-context/servers/java.meta.json +++ b/agent-context/servers/java.meta.json @@ -2,8 +2,5 @@ "displayName": "Java", "install": "add com.metaobjects:metaobjects-metadata + metaobjects-codegen-spring + metaobjects-maven-plugin to your pom.xml", "codegenCommand": "mvn metaobjects:generate", - "docsCommand": "mvn metaobjects:generate", - "docsCondition": " (when the project's generator list declares the api-docs generator)", - "docsMissing": "Declare `com.metaobjects.generator.apidocs.JavaApiDocsGenerator` as a `` entry in the pom's `` if it is missing", - "apiSubDir": "api/java" + "verifyStep": "`mvn metaobjects:verify` (the generated code and the requirements)" } diff --git a/agent-context/servers/kotlin.meta.json b/agent-context/servers/kotlin.meta.json index 72beb7ef8f..2a91b8f567 100644 --- a/agent-context/servers/kotlin.meta.json +++ b/agent-context/servers/kotlin.meta.json @@ -2,8 +2,5 @@ "displayName": "Kotlin", "install": "add com.metaobjects:metaobjects-codegen-kotlin + metaobjects-metadata-ktx + metaobjects-maven-plugin to your pom.xml", "codegenCommand": "mvn metaobjects:generate", - "docsCommand": "mvn metaobjects:generate", - "docsCondition": " (when the project's generator list declares the api-docs generator)", - "docsMissing": "Declare `com.metaobjects.generator.kotlin.apidocs.KotlinApiDocsGenerator` as a `` entry in the pom's `` if it is missing", - "apiSubDir": "api/kotlin" + "verifyStep": "`mvn metaobjects:verify` (the generated code and the requirements)" } diff --git a/agent-context/servers/python.meta.json b/agent-context/servers/python.meta.json index f6ab39c486..f852f76310 100644 --- a/agent-context/servers/python.meta.json +++ b/agent-context/servers/python.meta.json @@ -2,8 +2,5 @@ "displayName": "Python", "install": "pip install metaobjects", "codegenCommand": "metaobjects gen", - "docsCommand": "metaobjects gen", - "docsCondition": " (when a target's generator list declares `api-docs`)", - "docsMissing": "Declare `api-docs` in that target's `generators` in metaobjects.config.yaml if it is missing", - "apiSubDir": "api/python" + "verifyStep": "`metaobjects verify` (the generated code and the requirements; `--codegen` is its default)" } diff --git a/agent-context/servers/typescript.meta.json b/agent-context/servers/typescript.meta.json index 65f9e1dbd2..8af887ac43 100644 --- a/agent-context/servers/typescript.meta.json +++ b/agent-context/servers/typescript.meta.json @@ -2,8 +2,5 @@ "displayName": "TypeScript", "install": "npm install -D @metaobjectsdev/cli @metaobjectsdev/codegen-ts", "codegenCommand": "npx meta gen", - "docsCommand": "npx meta gen", - "docsCondition": "", - "docsMissing": "", - "apiSubDir": "api" + "verifyStep": "`npx meta verify --codegen` (the generated code and docs pages; the requirement gate runs on every `meta verify`)" } diff --git a/agent-context/skills/metaobjects-authoring/SKILL.md b/agent-context/skills/metaobjects-authoring/SKILL.md index 1979d161f2..de11bce2cc 100644 --- a/agent-context/skills/metaobjects-authoring/SKILL.md +++ b/agent-context/skills/metaobjects-authoring/SKILL.md @@ -38,7 +38,7 @@ This file covers what almost every model needs. The topics below live in | `references/inheritance-tph.md` | several entities are variants of one thing sharing a single table (`@discriminator`) | | `references/metadata-dependencies.md` | the project builds on another package's metadata (`dependencies`, cross-package `overlay`) | | `references/reporting.md` | what a declared report gives you: what columns a report gets, time grains, null rules, rows with no facts (`@spine`), defaults (`@default`), engine differences | -| `references/requirements.md` | installed only when the project declares `requirement.*` nodes | +| `references/requirements.md` | you add, change or review a `requirement.*` node | ## The operating principle: model-first, generate-first @@ -483,7 +483,7 @@ Every requirement must be **violable**: if you cannot say what breaking it looks **A commit that adds, changes or removes behaviour updates the ledger in the same commit** (the new or changed claim, its `@status` and `@implementedBy`), or says in the commit message why it does not. `verify` checks that links resolve, not that the ledger is current. -If the project declares any `requirement.*` node, `references/requirements.md` is installed with the full authoring rules. If it does not and you are adding the first one, the shape is: +`references/requirements.md` has the full authoring rules. The shape of one node: ```yaml - requirement.architectural: diff --git a/agent-context/skills/metaobjects-codegen/SKILL.md b/agent-context/skills/metaobjects-codegen/SKILL.md index 36123e0d8f..a337e663e3 100644 --- a/agent-context/skills/metaobjects-codegen/SKILL.md +++ b/agent-context/skills/metaobjects-codegen/SKILL.md @@ -35,6 +35,7 @@ exact config, generator names and commands for this project; read it. The other | File | Open it when | |---|---| +| `references/principles.md` | a rule in the always-on file does not settle the case: the full standing principles (adopting onto existing code, the schema and live database, prompts, requirements, owning and writing generators, physical names) with their reasons | | `references/any-stack.md` | **the database, data store, HTTP framework or UI framework is not one a reference generator targets** — MySQL, MongoDB, DynamoDB, Express, NestJS, Next.js, Vue, … | | `references/owning-generators.md` | generated output is wrong; your framework is not the one a reference emits for; you write or review a generator (the `own*()` accessor table) | | `references/choosing-layers.md` | the generated API does not fit, you need joined or derived data, or you are adopting onto existing code | diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md b/agent-context/skills/metaobjects-codegen/references/principles.md similarity index 67% rename from fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md rename to agent-context/skills/metaobjects-codegen/references/principles.md index 2fbe964b28..2c2f61d72d 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/AGENTS.md +++ b/agent-context/skills/metaobjects-codegen/references/principles.md @@ -1,38 +1,16 @@ -# Working with MetaObjects in this project +# Standing principles -> Stack: typescript server, no 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. +The rules that hold in every MetaObjects project, whatever the stack and whatever the task. The +always-on file in `.metaobjects/` carries the short form; this page is the full text, with the +reasons. Read it when a rule there does not settle the case in front of you. -MetaObjects is a metadata standard: typed metadata (in `metaobjects/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `npx meta gen`. +## Contents -## Read the generated reference before you touch a tier -`meta gen` writes four reference files under this project's docs target -(`targets.docs.outDir` in `metaobjects.config.ts`) when `generators` lists the generator -for each — `agentDocsFile()` for the three `agent/` pages, `apiDocsFile()` for the api -reference. There is no separate docs command: documentation is one more entry in the same -list every other generator is in. They are generated from THIS project's metadata, so they -describe what it actually is rather than what the framework does in general — and reading -the relevant one first is how you avoid inferring the answer from generated code, which is -the disposable half. - -- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each - column came from, keys, foreign keys, constraints. -- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter - operators the generators actually emitted. -- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and - filterability per field, and each declared grid. -- **`agent/requirements.md`** — before adding a capability. What is already claimed, and - what was deliberately **retired** — an entry saying a capability must not be rebuilt is - the one thing you cannot learn from the code, because the code is gone. - -Not there? List the missing generator — and a `docs` target, if `targets` has none — in -`generators`, then run `meta gen`. +- Principles +- Authoring rules you must not violate ## 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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. - **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. @@ -50,35 +28,9 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. ## Authoring rules you must not violate + - **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. - Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. diff --git a/agent-context/skills/metaobjects-verify/SKILL.md b/agent-context/skills/metaobjects-verify/SKILL.md index 57b556e1f6..fcdb550249 100644 --- a/agent-context/skills/metaobjects-verify/SKILL.md +++ b/agent-context/skills/metaobjects-verify/SKILL.md @@ -334,4 +334,5 @@ what's wrong. --- -For the migration tooling read `references/migration.md`. +For the migration tooling read `references/migration.md`. Before upgrading MetaObjects, or +when a project runs more than one port, read `references/upgrading.md`. diff --git a/agent-context/skills/metaobjects-verify/references/migration.md b/agent-context/skills/metaobjects-verify/references/migration.md index f589708490..ff97eb6b8e 100644 --- a/agent-context/skills/metaobjects-verify/references/migration.md +++ b/agent-context/skills/metaobjects-verify/references/migration.md @@ -18,6 +18,7 @@ stays native to the language. - Install (Node, dev-only) - Output format - The workflow +- Renames and destructive changes - Dialects - Live-DB drift: `meta verify --db` - Index modeling (Postgres) @@ -109,8 +110,42 @@ meta migrate baseline --from-db --db postgresql://... --dialect postgres 2. **Review the SQL.** Read the emitted `up.sql` (forward) and `down.sql` (rollback) before applying. Destructive changes (drop column / drop table) are - opt-in — the engine blocks them unless explicitly allowed, and routes ambiguous - rename-vs-drop+add decisions through a prompt rather than guessing. + opt-in — the engine refuses them until you allow each kind by name (`--allow`, + below), and an ambiguous rename-vs-drop+add aborts unless you declare the rename + or choose with `--on-ambiguous`. It never guesses. + +## Renames and destructive changes + +A renamed field or object is a renamed column or table, and the diff cannot tell that +from a drop plus an add unless the names are close. Declare the rename and the data is +kept: + +```bash +meta migrate --dialect postgres --slug rename-origin --rename-column shipment.origin_city=origin_port +meta migrate --dialect postgres --slug rename-stop --rename-table stop=port_call +``` + +- `--rename-column [schema.]table.old=new` and `--rename-table [schema.]old=new` are + repeatable. For a column, `table` is the table's name in the metadata, so it is the + **new** name when the table is renamed in the same run. A declared rename needs no + `--allow drop-column`, and one that finds nothing to rename is refused rather than + falling back to drop and add. Change a column's type, nullability or default in a + later run, not in the rename's. +- `--allow ` permits a destructive change by kind. A refusal names the token it + needs. The tokens: `drop-column`, `drop-table`, `type-change`, `drop-index`, `drop-fk`, + `drop-check`, `drop-view`, `drop-view-cascade`, `drop-column-read-by-view`, + `adopt-view`, `nullable-to-not-null`, `drop-identity-default`, `drop-comment`, + `drop-unmanaged`. Changing an enum's values replaces its CHECK, so it needs + `--allow drop-check`. `drop-column` deletes the column's data: for a rename use + `--rename-column`, never `--allow drop-column`. +- `--on-ambiguous abort|rename|drop-add` (default `abort`) decides a pair the + heuristic offers as a possible rename. Declaring the rename is clearer than `rename`. +- `migrate.allow` and `migrate.onAmbiguous` in `.metaobjects/config.json` set the same + things for every run. Prefer the flag: a standing allowance hides the next destructive + change from review. + +`docs/features/migrations-and-drift.md`, "Renames and populated tables", covers what a +rename carries with it (constraint names, views) and changes that need a backfill. 3. **Apply** the pending migrations against the DB; migration history is tracked in a ledger table: diff --git a/agent-context/skills/metaobjects-verify/references/upgrading.md b/agent-context/skills/metaobjects-verify/references/upgrading.md new file mode 100644 index 0000000000..23ccb45965 --- /dev/null +++ b/agent-context/skills/metaobjects-verify/references/upgrading.md @@ -0,0 +1,10 @@ +# Upgrading MetaObjects and keeping the ports in sync + +Read this before you move a project to a new MetaObjects release, and when a project runs more +than one port (a TypeScript client beside a Java backend, say). + +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. + +`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. + +**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. diff --git a/agent-context/templates/always-on.md.mustache b/agent-context/templates/always-on.md.mustache index 9eef331f35..cc0003da65 100644 --- a/agent-context/templates/always-on.md.mustache +++ b/agent-context/templates/always-on.md.mustache @@ -3,104 +3,68 @@ > {{stackLine}} > > 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/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `{{codegenCommand}}`. - -## Read the generated reference before you touch a tier -{{#nodeDocsSurface}} -`meta gen` writes four reference files under this project's docs target -(`targets.docs.outDir` in `metaobjects.config.ts`) when `generators` lists the generator -for each — `agentDocsFile()` for the three `agent/` pages, `apiDocsFile()` for the api -reference. There is no separate docs command: documentation is one more entry in the same -list every other generator is in. They are generated from THIS project's metadata, so they -describe what it actually is rather than what the framework does in general — and reading -the relevant one first is how you avoid inferring the answer from generated code, which is -the disposable half. - -- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each - column came from, keys, foreign keys, constraints. -- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter - operators the generators actually emitted. -- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and - filterability per field, and each declared grid. -- **`agent/requirements.md`** — before adding a capability. What is already claimed, and - what was deliberately **retired** — an entry saying a capability must not be rebuilt is - the one thing you cannot learn from the code, because the code is gone. - -Not there? List the missing generator — and a `docs` target, if `targets` has none — in -`generators`, then run `meta gen`. -{{/nodeDocsSurface}} -{{^nodeDocsSurface}} -`{{docsCommand}}` writes ONE reference file under this project's docs output directory{{docsCondition}}. It -is generated from THIS project's metadata, so it describes what the project actually is -rather than what the framework does in general — and reading it first is how you avoid -inferring the answer from generated code, which is the disposable half. There is no -separate docs command on this port either: the api reference is one more entry in the same -generator list every other generator is in. - -- **`{{apiSubDir}}/AGENT-API.md`** — before calling generated code. The types, endpoints and - filter operators the generators actually emitted. - -Not there? {{docsMissingAction}} `{{docsCommand}}`. - -The other three pages of that reference — `agent/schema.md` (persistence), `agent/ui.md` -(forms and grids) and `agent/requirements.md` (what is already claimed, and what was -deliberately **retired**) — are written by the Node `meta gen` command, from -`agentDocsFile()` in a `metaobjects.config.ts`, which this stack does not have. Until it -does, read the metadata itself for those three: it is the source those pages are generated -from. -{{/nodeDocsSurface}} - -## 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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. -- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. -- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. -- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. -- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. -- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. -- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. -- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. -- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. -- 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. -- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. - -## Authoring rules you must not violate -- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. -- Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +{{stackLoops}} +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column .=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +{{verifySteps}}- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/agent-context/templates/loops/client.md b/agent-context/templates/loops/client.md new file mode 100644 index 0000000000..e543e4d437 --- /dev/null +++ b/agent-context/templates/loops/client.md @@ -0,0 +1,8 @@ +### Web client: generated by the Node `npx meta gen` + +The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` +from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from +`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and +`@metaobjectsdev/tanstack`. Wrap the app in ``: +`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every +hook without an error. The `metaobjects-runtime-ui` skill has the detail. diff --git a/agent-context/templates/loops/csharp.md b/agent-context/templates/loops/csharp.md new file mode 100644 index 0000000000..f004e5e206 --- /dev/null +++ b/agent-context/templates/loops/csharp.md @@ -0,0 +1,17 @@ +### C#: generate with `dotnet meta gen` + +1. **Config.** `codegen/Generators.cs` (written by `dotnet meta init`) is the generator list. + Nothing is generated until it is listed. The EF Core data layer and its routes: + + ```csharp + new MetaObjects.Codegen.Generators.EntityGenerator(), + new MetaObjects.Codegen.Generators.DbContextGenerator(), + new MetaObjects.Codegen.Generators.FilterAllowlistGenerator(), + new MetaObjects.Codegen.Generators.RoutesGenerator(), + ``` + + `RequirementTestsGenerator { OutDir = "/Generated" }` writes one test per + requirement; `ApiDocsGenerator` writes `api/csharp/AGENT-API.md`. `dotnet meta gen --list` + is the catalog. +2. **Generate.** `dotnet meta gen`. `dotnet meta eject ` copies a generator into the repo + to change what it emits. diff --git a/agent-context/templates/loops/java.md b/agent-context/templates/loops/java.md new file mode 100644 index 0000000000..638e593fec --- /dev/null +++ b/agent-context/templates/loops/java.md @@ -0,0 +1,12 @@ +### Java: generate with `mvn metaobjects:generate` + +1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator + list, one `` each, under `` pointing at the metadata. + Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`, + `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and + `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a + list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator` + writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The + full table is in the `metaobjects-codegen` skill's `references/java.md`. +2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java` + copies a generator to change what it emits. diff --git a/agent-context/templates/loops/kotlin.md b/agent-context/templates/loops/kotlin.md new file mode 100644 index 0000000000..ec62287786 --- /dev/null +++ b/agent-context/templates/loops/kotlin.md @@ -0,0 +1,13 @@ +### Kotlin: generate with `mvn metaobjects:generate` + +1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator + list, one `` each. Nothing is generated until it is listed. The Exposed + and Spring tier is `KotlinEntityGenerator`, `KotlinExposedTableGenerator`, + `KotlinRelationsGenerator`, `KotlinFilterAllowlistGenerator` and + `KotlinSpringControllerGenerator` (package `com.metaobjects.generator.kotlin`). + `KotlinRequirementTestsGenerator` writes one test per requirement; `KotlinApiDocsGenerator` + writes `api/kotlin/AGENT-API.md`. The full table is in the `metaobjects-codegen` skill's + `references/kotlin.md`. +2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=kotlin` + copies a generator to change what it emits. Several Kotlin generators truncate their output + on every run, so never put your own code in a generated file. diff --git a/agent-context/templates/loops/python.md b/agent-context/templates/loops/python.md new file mode 100644 index 0000000000..2af51299e2 --- /dev/null +++ b/agent-context/templates/loops/python.md @@ -0,0 +1,18 @@ +### Python: generate with `metaobjects gen` + +1. **Config.** `metaobjects.config.yaml` names each target's `outDir` and its generators. Nothing + is generated until it is listed: + + ```yaml + targets: + app: + outDir: generated + generators: [entity, names] # Pydantic models + physical-name constants + ``` + + `routes` and `filter-allowlist` add FastAPI routers over a repository `Protocol` you + implement; `requirement-tests` writes one pytest file per package of requirements; + `api-docs` writes `api/python/AGENT-API.md`. `metaobjects gen --list` is the catalog. + Install what the output imports: `pip install "pydantic[email]>=2" fastapi`. +2. **Generate.** `metaobjects gen`. `metaobjects eject ` copies a generator into the repo + to change what it emits. diff --git a/agent-context/templates/loops/typescript.md b/agent-context/templates/loops/typescript.md new file mode 100644 index 0000000000..6007a7ca92 --- /dev/null +++ b/agent-context/templates/loops/typescript.md @@ -0,0 +1,26 @@ +### TypeScript: generate with `npx meta gen` + +1. **Config.** In `metaobjects.config.ts` set `dialect` (`"postgres" | "sqlite" | "mysql"`; D1 is + `"sqlite"`) and list the generators. Nothing is generated until it is listed. The usual data + layer: + + ```ts + import { entityFile, namesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; + // generators: [entityFile(), namesFile(), barrel()] + ``` + + Per entity: `.ts` (Drizzle table, row types, Zod schemas, enum types) and + `.names.ts` (physical names), plus an `index.ts` barrel. Install what they import: + `npm i drizzle-orm zod` and your database driver. Every other generator (CRUD `queriesFile()` + / `routesFile()`, docs, `requirementTests()` for a test stub per requirement) is one line too: + + ``` + npx meta gen --list --probe 2>/dev/null | grep -E "^ - name|wouldEmit| import:" | paste - - - + ``` + + `npx meta eject ` copies one into `codegen/generators/` to change it; + `npx meta generator new ` writes a new one. +2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `. +3. **Reference pages.** `agentDocsFile()` and `apiDocsFile()` with `targets: { docs: { outDir: + "docs" } }` write `agent/schema.md`, `agent/ui.md`, `agent/requirements.md` and + `api/AGENT-API.md`. When they exist, read the one for the tier you touch. diff --git a/docs/features/cli.md b/docs/features/cli.md index 2841806f50..1788fc29d6 100644 --- a/docs/features/cli.md +++ b/docs/features/cli.md @@ -25,7 +25,7 @@ command surface splits in two: | Capability | Tool | Invocation | Notes | |---|---|---|---| -| Project scaffold (`init`) | Node `meta` | `meta init` | TS projects | +| Project scaffold (`init`) | Node `meta` | `meta init` | TS projects; `--dialect sqlite\|postgres\|mysql\|d1` writes the codegen `dialect` in `metaobjects.config.ts` and, for sqlite, postgres and d1, `migrate.dialect` in `.metaobjects/config.json` (d1 is `--d1`; MySQL schema is not `meta migrate`'s) | | **Agent-context scaffold** (`agent-docs`) | **Node `meta`** | `npx meta agent-docs --server ` | **any backend** — single assembler (ADR-0033); non-Node CLIs redirect here | | **Schema migrate** | **Node `meta`** | `meta migrate` | **any backend** — schema is Node-only (ADR-0015) | | **Schema drift** (`verify --db`) | **Node `meta`** | `meta verify --db` | **any backend** — live-DB drift, Node-only | diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/SKILL.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/SKILL.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/SKILL.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/calibration.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/calibration.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/calibration.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/calibration.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/capability-checklist.md diff --git a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/csharp.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/csharp.md new file mode 100644 index 0000000000..7bb28c7c76 --- /dev/null +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/csharp.md @@ -0,0 +1,167 @@ +# C# port — audit reference + +The C# port targets .NET consumers (EF Core + ASP.NET Minimal API). Codegen runs +through the `dotnet meta` .NET tool (`MetaObjects.Cli`). Schema migration and +live-DB drift are **Node-`meta`-only** (ADR-0015) — `dotnet meta` has no `migrate` +subcommand and `dotnet meta verify --db` is rejected with exit 2. + +--- + +## Contents +- Finding generated files +- Run codegen + verify +- Drift signatures (what to grep for) +- Owned generators +- Version-skew check +- Calibration gaps (do NOT flag these) + +## Finding generated files + +Generated files are named `.g.cs` and carry an `@generated` guard comment: + +```csharp +// @generated by MetaObjects.Codegen +``` + +The metadata source directory is the first positional arg (``); `--out` +is required (errors if absent, no default). The runner overwrites only files bearing +this header; it refuses to touch files without it. Locate the output directory from +how the team invokes `dotnet meta gen` (or a CI script) — the arg is explicit. What is +generated is the list in `codegen/Generators.cs`; an entry with its own `OutDir` +(`ApiDocsGenerator`, `RequirementTestsGenerator`) writes there instead of under `--out`. + +--- + +## Run codegen + verify + +```bash +dotnet meta gen metaobjects --out Generated --namespace Acme.Generated + +dotnet meta gen --list # list all registered generators +dotnet meta gen metaobjects --out Generated --generators entity # narrow to declared entries + +dotnet meta verify --codegen # regenerate to temp, diff vs committed output +dotnet meta verify --templates # check {{field}} refs vs payload VO (bare default) +dotnet meta verify --deps # shared-model dependencies vs deps.lock.json (opt-in) +``` + +`verify --deps` applies when the project declares `dependencies` in `.metaobjects/config.json`: it +hashes each `path` dependency against the lock and fails with `ERR_DEPENDENCY_UPSTREAM_DRIFT` on +`drifted` or `unresolved` (a `maven`, `nuget`, `npm` or `python` dependency reports `unresolved`; +run the Node `meta deps check` for those). A project that publishes a model wires the +`SharedModelGenerator` in `codegen/Generators.cs`; `verify --codegen` drift-gates its output. + +Schema migration + live-DB drift run through the **Node `meta` CLI** regardless of +server language — see the migration reference. `dotnet meta verify --db` is +rejected (exit 2). + +--- + +## Drift signatures (what to grep for) + +| Signature | What it means | +|---|---| +| `[Column(...)]` / `[Table(...)]` in hand-written `.cs` (not `.g.cs`) | hand-authored EF Core entity — candidate to generate | +| `DbSet` in a hand-written `DbContext` subclass | generated `AppDbContext` (`db-context` generator) should own this | +| `HasConversion()` by hand | the `entity` generator emits this for `field.enum`; duplicate if entity is modeled | +| `FromSqlInterpolated(` outside `.g.cs` | stored-proc call — candidate for the `callable` generator | +| `// keep in sync with` / `// mirrors the` | second-source-of-truth comment — always a finding | +| `HasPrecision(` hand-coded | `field.decimal` with `@precision`/`@scale` drives this from the `entity` generator | +| a member of a generated enum typed as a hand string (`"Error"` in raw SQL, a JSON body, a topic or event name, a `switch` over a `string`) beside the generated enum constant | **signature 13.** Value-first from each enum's `@values`. A value typed as the generated C# enum is resolved and exempt; a `string` position, SQL text or a payload is compared and scores | +| a status-like column written with a property assignment then `SaveChanges`, or `ExecuteSqlRaw("UPDATE ... SET Status")`, with no condition on the current value | **axis G3.** Guard it through EF Core: `Where(x => x.Id == id && x.Status == )` then `ExecuteUpdateAsync`, with the generated enum constant, and check the affected-row count. Never raw SQL | +| a table/column string in raw-SQL EF calls, or `nameof(Entity.Prop)` standing in for a column | second spelling of a declared physical name — reference `Names.g.cs` (`AuthorNames.SourcePrimaryTable` / `Column`, emitted when `codegen/Generators.cs` lists `names` — `Names.Name` is the OBJECT's name, not the table); an EF property inside LINQ is the typed handle — correct | + +--- + +## Owned generators + +`dotnet meta eject ...` copies a reference generator into `codegen/generators/`; +the copy is listed in `codegen/Generators.cs`, the generator list `dotnet meta gen` and +`dotnet meta verify --codegen` run through the `codegen/` console project. Ejecting `routes` also +copies the helper runtime the generated routes call (`FilterParser`, `EfCoreFilterDispatch`, +`ValueObjectValidator`, `ConstraintErrors`, `Iso8601TimestampConverter`) into +`codegen/runtime/` under the namespace `Codegen.Runtime`. That folder is owned code, not a +hand-rolled filter parser: do not flag it. `dotnet meta gen --list` marks each owned copy +`identical` or `DIFFERS`. + +**Audit the owned copies first.** A copy exists to carry a change; one that carries none is +duplication that falls behind the reference. In `dotnet meta gen --list` an owned row reads +`[owned — identical]` or `[owned — DIFFERS: N behind, M of your own]` (`0 behind, 0 of your +own, reordered` for a copy with the same lines in another order), and `routes` adds a clause +for each `codegen/runtime/` file that differs. Only `identical`, with every runtime clause +identical too, proves a copy unmodified; that row is a **finding**: point the +`codegen/Generators.cs` entry back at the stock generator and remove the copy +(`dotnet meta eject --undo` removes every identical copy and prints the edit). `DIFFERS` +with `0 of your own` rules out added lines, not removed ones: confirm from `git log -p` on +the copy that nothing changed after the eject, else report it as **possibly customized: +check its history**. + +**`DIFFERS` is printed for BOTH a deliberate customization and a copy gone stale by version**, +so the listing cannot tell them apart, and a copy with lines of its own is NOT thereby a +customization: `M of your own` counts every line absent from today's reference, so after the +reference moved on, a line it removed or rewrote reads as "yours" and is an older upstream +line. Run the history check on any `DIFFERS` row, `M` above 0 included: lines the copy's later +commits added are the project's own; lines already in the eject commit are upstream's and +count as behind. Report two figures, ` lines behind the reference` and ` lines of the +project's own (confirmed from history)`. A copy whose own lines history does not confirm is +**possibly customized or stale: check its history**; one that is stale by version is a finding +(`owned-generator-stale`): re-eject at the current version or return to the stock entry. +Count lines for the Phase 0 total: lines in +copies proven unmodified, over all lines under `codegen/generators/` and `codegen/runtime/`. + +A generator the project WRITES is an `IGenerator` listed in +`codegen/Generators.cs` the same way. With no `codegen/Generators.cs`, `dotnet meta gen` +exits 2 and generates nothing. + +**A finding of the form "the built-ins do not emit the shape this project needs", or a +hand-written layer the model fully describes, resolves to a generator the project writes** +(ADR-0034 Amendment 4), or to a declarative template (a `TemplateSpecGenerator { Spec = +"", TemplateRoot = "" }` entry in `codegen/Generators.cs`) where the shape is what +matters. Recommend it; it is the +primary path. Worked template example: `docs/ports/csharp.md`. + +To re-scaffold the agent-context into a C# project, use the Node `meta` CLI (the +single agent-docs assembler per ADR-0033): + +```bash +npx meta agent-docs --server csharp [--out ] +``` + +(`dotnet meta agent-docs` is a non-executing stub — it prints this redirect and exits 1.) + +--- + +## Version-skew check + +The resolved NuGet package version is what actually ran codegen — check the lock file +or the restore log, not the `.csproj` range: + +```bash +dotnet list package --include-transitive | grep -i MetaObjects +``` + +`MetaObjects` (loader), `MetaObjects.Codegen`, `MetaObjects.Render`, and +`MetaObjects.Cli` all publish in lockstep on the C# `0.x` line. A mixed resolved +version across these packages is an intra-port skew finding. + +--- + +## Calibration gaps (do NOT flag these) + +- **No ObjectManager runtime tier.** C# has no `ObjectManager` equivalent — EF Core + (the generated `AppDbContext` + generated entity classes) **is** the runtime persistence + layer. Hand-written service classes over the generated `AppDbContext` are the expected + pattern, not a defect. +- **No C# migrate command.** Schema migration is Node-`meta`-owned for every port + (ADR-0015). `dotnet meta` has no migrate subcommand; `meta migrate` is correct. +- **Core filter-operator codegen ships in C# — do NOT treat it as deferred.** The `routes` + generator (`Routes.cs`, Minimal API) generates the `?filter[field][op]=value` grammar + (all 9 operators `eq/ne/gt/gte/lt/lte/in/like/isNull`) via the runtime `FilterParser` + + `EfCoreFilterDispatch`, validated against the generated filter allowlist (api-contract corpus, + both lanes). **Flag a hand-rolled filter parser as a finding.** Only the richer surface + (`?search=`, `filter[or][N]` / `filter[and][N]` combinators, leading-wildcard gating) is + TS-only — do NOT flag its absence in C#. +- **Output-parser codegen ships in C#.** `output-parser` / `extractor` / `render-helper` + generators are available (list `OutputParserGenerator` in `codegen/Generators.cs`). Absence of + wired output parsers where a **responding `template.prompt`** (`@responseRef`) exists IS a + finding. ADR-0052: a `template.output` is outbound only and generates no parser. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/drift-signatures.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/drift-signatures.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/drift-signatures.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/drift-signatures.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/prompt-anti-patterns.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/prompt-anti-patterns.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/prompt-anti-patterns.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/prompt-anti-patterns.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/requirements.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/requirements.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/requirements.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/requirements.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/surface-review-axes.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/surface-review-axes.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/surface-review-axes.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/surface-review-axes.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/vocabulary-hygiene.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/vocabulary-hygiene.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/vocabulary-hygiene.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-audit/references/vocabulary-hygiene.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/SKILL.md similarity index 99% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/SKILL.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/SKILL.md index 1979d161f2..de11bce2cc 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -38,7 +38,7 @@ This file covers what almost every model needs. The topics below live in | `references/inheritance-tph.md` | several entities are variants of one thing sharing a single table (`@discriminator`) | | `references/metadata-dependencies.md` | the project builds on another package's metadata (`dependencies`, cross-package `overlay`) | | `references/reporting.md` | what a declared report gives you: what columns a report gets, time grains, null rules, rows with no facts (`@spine`), defaults (`@default`), engine differences | -| `references/requirements.md` | installed only when the project declares `requirement.*` nodes | +| `references/requirements.md` | you add, change or review a `requirement.*` node | ## The operating principle: model-first, generate-first @@ -483,7 +483,7 @@ Every requirement must be **violable**: if you cannot say what breaking it looks **A commit that adds, changes or removes behaviour updates the ledger in the same commit** (the new or changed claim, its `@status` and `@implementedBy`), or says in the commit message why it does not. `verify` checks that links resolve, not that the ledger is current. -If the project declares any `requirement.*` node, `references/requirements.md` is installed with the full authoring rules. If it does not and you are adding the first one, the shape is: +`references/requirements.md` has the full authoring rules. The shape of one node: ```yaml - requirement.architectural: diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/abstracts-and-overlay.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/abstracts-and-overlay.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/abstracts-and-overlay.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/abstracts-and-overlay.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-code.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-code.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-code.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-code.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-models.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-models.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-models.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/adopting-existing-models.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/choosing-a-shape.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/choosing-a-shape.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/choosing-a-shape.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/choosing-a-shape.md diff --git a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/csharp-providers.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/csharp-providers.md new file mode 100644 index 0000000000..45896849cf --- /dev/null +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/csharp-providers.md @@ -0,0 +1,85 @@ +# Your own vocabulary in C# — a provider + +> Part of the `metaobjects-authoring` skill, "Your own vocabulary". Read it when the project +> needs a subtype or an attribute the core does not ship, and its C# tools load the metadata. + +A provider is a public class implementing `IMetaDataTypeProvider`, with a public parameterless +constructor: an `Id`, the provider ids it depends on, and a `RegisterTypes(registry)` body that +calls `Register` (a new subtype) and `Extend` (attributes on an existing one). Put it in its +own class library that references the `MetaObjects` package at the version your `dotnet meta` +tool runs. This one registers `layout.picker` and adds `@searchable` and `@valueLabels` to +`field.string`: + +```csharp +// vocabulary/AcmeVocabularyProvider.cs (vocabulary/Acme.Vocabulary.csproj: net8.0 class +// library with ) +using MetaObjects; +using MetaObjects.Meta; +using MetaObjects.Shared; + +namespace Acme.Vocabulary; + +public sealed class AcmeVocabularyProvider : IMetaDataTypeProvider +{ + public string Id => "acme-vocabulary"; + public IReadOnlyList Dependencies => ["metaobjects-core-types"]; + + public void RegisterTypes(TypeRegistry registry) + { + // A new subtype, with its own attribute (an own attribute may be required). + registry.Register(new TypeDefinition( + typeId: new TypeId("layout", "picker"), + description: "A picker over the owning object.", + childRules: [new ChildRule(BaseTypes.TYPE_ATTR, Structural.CHILD_RULE_WILDCARD, Structural.CHILD_RULE_WILDCARD)], + factory: (typeId, name) => new MetaLayout(typeId, name), + attributes: [new AttrSchema("displayField", "string", Required: false)])); + + // Attributes on a type another provider owns: always optional. + registry.Extend("field", "string", attributes: + [ + new AttrSchema("searchable", "boolean", Required: false), + new AttrSchema("valueLabels", "string", Required: false, IsArray: true), + ]); + } +} +``` + +Wire it once, in `.metaobjects/config.json`. `assembly` is the BUILT dll, relative to the +directory holding `.metaobjects/`; `type` is the provider's full type name: + +```json +{ + "schema_version": 1, + "csharp": { + "providers": [ + { "assembly": "vocabulary/bin/Debug/net8.0/Acme.Vocabulary.dll", "type": "Acme.Vocabulary.AcmeVocabularyProvider" } + ] + } +} +``` + +Build the vocabulary first (`dotnet build vocabulary`); a missing dll is refused with the path +and that instruction. Then `dotnet meta gen` and `verify --codegen` (through the project's +`codegen/` runner), `dotnet meta verify` (templates, requirements, field lint) and +`dotnet meta fmt` all compose the core providers plus this one. The `codegen/` project may +also reference the vocabulary project directly; the already-loaded assembly is reused. + +An application host that loads the model itself composes the registry in code: + +```csharp +var registry = Provider.ComposeRegistry([.. CoreTypes.LibraryProviders, new AcmeVocabularyProvider()]); +var load = MetaDataLoader.FromDirectory("metaobjects", registry, strict: true); +``` + +The metadata uses it like core vocabulary: + + +```json +{ "field.string": { "name": "name", "@searchable": true } } +{ "layout.picker": { "name": "contactPicker", "@displayField": "name" } } +``` + +Without the provider the same file fails `dotnet meta verify` with `ERR_UNKNOWN_SUBTYPE` on +`layout.picker` and `ERR_UNKNOWN_ATTR` on `@searchable`. If another port's tools load this +metadata too, that port needs its own copy of the provider (same id, same vocabulary). Each port's `verify` loads strict, so a clean run there proves that port's copy is complete. +Full contract: `docs/features/extending-with-providers.md`. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/declaring-reports.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/identities-and-indexes.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/identities-and-indexes.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/identities-and-indexes.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/identities-and-indexes.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/inheritance-tph.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/inheritance-tph.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/inheritance-tph.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/inheritance-tph.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/json-columns.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/json-columns.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/json-columns.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/json-columns.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/metadata-dependencies.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/metadata-dependencies.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/metadata-dependencies.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/metadata-dependencies.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/model-first-rationale.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/model-first-rationale.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/model-first-rationale.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/model-first-rationale.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/read-views-and-projections.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/read-views-and-projections.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/read-views-and-projections.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/read-views-and-projections.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/relationships.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/relationships.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/relationships.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/relationships.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/reporting.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/reporting.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/reporting.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/reporting.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/requirements.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/requirements.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/requirements.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/requirements.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/sources.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/sources.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/sources.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-authoring/references/sources.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/SKILL.md similarity index 99% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/SKILL.md index 36123e0d8f..a337e663e3 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -35,6 +35,7 @@ exact config, generator names and commands for this project; read it. The other | File | Open it when | |---|---| +| `references/principles.md` | a rule in the always-on file does not settle the case: the full standing principles (adopting onto existing code, the schema and live database, prompts, requirements, owning and writing generators, physical names) with their reasons | | `references/any-stack.md` | **the database, data store, HTTP framework or UI framework is not one a reference generator targets** — MySQL, MongoDB, DynamoDB, Express, NestJS, Next.js, Vue, … | | `references/owning-generators.md` | generated output is wrong; your framework is not the one a reference emits for; you write or review a generator (the `own*()` accessor table) | | `references/choosing-layers.md` | the generated API does not fit, you need joined or derived data, or you are adopting onto existing code | diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/any-stack.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/any-stack.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/any-stack.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/any-stack.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/choosing-layers.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/choosing-layers.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/choosing-layers.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/choosing-layers.md diff --git a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/csharp.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/csharp.md new file mode 100644 index 0000000000..d86cf4cea2 --- /dev/null +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/csharp.md @@ -0,0 +1,331 @@ +# C# codegen specifics + +The C# port targets .NET consumers (EF Core + ASP.NET). Codegen runs through the +**`dotnet meta` .NET tool** — there is no Maven plugin and the Node `meta` binary +is **schema-migrations only** on the C# side (ADR-0015): `meta migrate` / +`meta verify --db` are Node-`meta`-owned; everything below is `dotnet meta`. + +## Contents +- Install +- Run +- `MetaObjects.Codegen` generators +- Docs — one more entry in `codegen/Generators.cs` +- Persistence + routes are the deployed artifact +- The JSON the API writes +- Write your own generator +- Scoping a generator to some objects +- Extending the generators (open-for-extension, ADR-0002) +- Re-scaffold this context + +## Install + +Per the always-on descriptor: + +```bash +dotnet tool install --global MetaObjects.Cli # provides `dotnet meta` +dotnet add package MetaObjects.Codegen # the codegen generators +dotnet meta init # writes codegen/: Generators.cs, Program.cs, Codegen.csproj +``` + +`dotnet meta` is a .NET tool invoked as `dotnet meta ` (the underlying +command is `dotnet-meta`). + +## Run + +The generator list is `codegen/Generators.cs` (ADR-0034 Amendment 9) — every generator the +project runs, documentation included, each with its options as init properties. +`codegen/Program.cs` is two lines that run it; `dotnet meta init` writes both and +`codegen/Codegen.csproj`. Nothing is generated that the list does not declare. + +```csharp +// codegen/Generators.cs +using MetaObjects.Codegen; + +public static class Generators +{ + public static IReadOnlyList All { get; } = + [ + new MetaObjects.Codegen.Generators.EntityGenerator(), + new MetaObjects.Codegen.Generators.DbContextGenerator(), + new MetaObjects.Codegen.Generators.FilterAllowlistGenerator(), + new MetaObjects.Codegen.Generators.RoutesGenerator(), + ]; +} +``` + +**Column names come from `naming` in `.metaobjects/config.json`, the one setting every port +reads** (ADR-0058): `meta migrate`, every port's codegen and every runtime `ObjectManager`. +A hosted runner that builds its own `GenConfig` sets `ColumnNamingStrategy` from the loaded +root (see "Scoping a generator to some objects", last paragraph). +The default is `snake_case`, what `meta migrate` creates. A database whose columns are named +exactly like the fields declares it there, once, for every port: + +```json +{ "schema_version": 1, "naming": { "columns": "literal" } } +``` + +There is no per-port or command-line naming: `--column-naming` is refused, and +`verify --codegen` names a naming mismatch as one rather than telling you to regenerate. Through 1.0 this port defaulted to the field names; a +database built that way needs the `literal` line above. + +```bash +dotnet meta gen metaobjects --out Generated --namespace Acme.Generated # runs the list +dotnet meta gen --list # list registered generators +dotnet meta gen metaobjects --out Generated --generators entity # narrow to declared entries +dotnet meta verify metaobjects --codegen --out Generated # codegen-drift gate (regenerate + diff vs committed) +``` + +`gen` and `verify --codegen` always run the list through `dotnet run --project codegen`; with +no `codegen/Generators.cs` they exit 2 and write nothing. `--generators` narrows a run to the +declared entries named and never adds one: naming an entry the list does not declare is +refused. `verify --codegen` regenerates the same list, so it never disagrees with `gen`. + +**Where `codegen/` goes, and sharing it with a TypeScript port.** `gen` looks for +`codegen/` in the project root: the parent of the `` it is given, or the +directory it runs in when given none. Run `dotnet meta init` there (or pass that directory as +`--root `); a `codegen/` anywhere else is never run. In a repository that also has a +TypeScript port, that is the same `codegen/` the TypeScript side keeps its owned generators +in, and C# ejects land in its `codegen/generators/` beside the `.ts` copies. They coexist: +`Codegen.csproj` compiles only `*.cs` and `tsconfig.codegen.json` only `*.ts`. Ignore the +build output (`codegen/bin/`, `codegen/obj/`) in `.gitignore`. + +A bare `dotnet meta verify` runs `--templates` (the FR-004 prompt/template drift +gate, see the prompts reference) and `--codegen` (the codegen-output drift gate; +skipped with a note unless an output directory, `csharp.outDir` in `.metaobjects/config.json` or `--out `, and `codegen/Generators.cs` are present). +**Schema migration + live-DB drift are NOT `dotnet meta`** — they run through the +Node `meta` tool (see the migration reference). + +## `MetaObjects.Codegen` generators + +Wire generators as a list in **`codegen/Generators.cs`** — a static list of constructed +generators, with their options as init properties (ADR-0034 Amendment 9); `codegen/Program.cs` +is a two-line entry point that passes the list to `CodegenCli.Run`. `dotnet meta init` +(new in 1.1.0) scaffolds both files; `dotnet meta eject` writes them when either is missing. +`dotnet meta gen` runs the declared list, documentation included — there is no default +set and no separate docs command. `--generators ` narrows a run to entries the +list already declares, by stable name; it never adds one, and naming an entry the list +does not declare is refused. `dotnet meta gen --list` is the catalog of what the port +registers, not what this project runs; under each name it prints the expression that lists +that generator in `codegen/Generators.cs` and its options. Output lands in `--out`, in the +namespace its metadata package maps to (see `docs/ports/csharp.md`), unless a generator's own +init properties say otherwise — documentation does (below). + +| Stable name | Entry in `codegen/Generators.cs` (`new MetaObjects.Codegen.Generators.…`) | Output | +|---|---|---| +| `entity` | `EntityGenerator()` | `.g.cs` — an EF Core entity class per `object.entity` / projection: PascalCase props mapped via `[Column]`, `[Table]`, `[Key]` (or class-level `[PrimaryKey]` for composites), `[MaxLength]`/validators, nullability from `@required`. Enum fields → a nested (or shared) C# `enum`; object fields → owned-type navigations; every concrete value object and sourceless projection → a POCO, which is also the template tier's payload/response type (ADR-0056; package-qualified, e.g. `AcmeAlphaNote`, when another object shares its short name). A TPH `@discriminator` base is emitted `abstract` with `: Base` subtypes (single-table). | +| `db-context` | `DbContextGenerator()` | one `AppDbContext` — a `DbSet` per entity + `OnModelCreating`: `.HasConversion()` (enums), `.OwnsOne(...)`/`.ToJson(...)` (owned/jsonb object fields), `.HasPrecision(p,s)` (decimals), `.UsingEntity<>(...)` (M:N), `.HasDiscriminator(e => e.Type).HasValue(...)` (TPH). | +| `routes` | `RoutesGenerator()` | `Routes.cs` — ASP.NET **Minimal API** CRUD per writable entity (`source.rdb @kind="table"`) on the cross-port REST contract (`?filter[field][op]=`, `?sort=field:asc`, `?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). A TPH base emits polymorphic `GET /(+/{id})` + a per-subtype CRUD set at `//` (create injects the discriminator, cross-subtype get/update/delete → 404). | +| `filter-allowlist` | `FilterAllowlistGenerator()` | per-entity `FilterAllowlist` (FR-009 — the server-side field+operator allowlist the routes validate against). | +| `callable` | `CallableGenerator()` | `.callable.g.cs` — an FR-015 calling method for a `source.rdb @kind="storedProc"|"tableFunction"`, via EF `FromSqlInterpolated` (args from the `@parameterRef` value object in declaration order). | +| `output-parser` / `extractor` / `output-prompt` / `render-helper` | `OutputParserGenerator()`, `ExtractorGenerator()`, `OutputPromptGenerator()`, `RenderHelperGenerator("prompts")` (its constructor argument is the template root its refs resolve from) | the prompt-pillar artifacts for a **responding `template.prompt`** — one carrying `@responseRef` (ADR-0052: these tiers are INBOUND; `template.output` is outbound only and emits no parser). The strict parser, the tolerant `extract`, the **output-format prompt fragment** (`output-prompt`; presentation via `@promptStyle: guide`/`inline`/`exampleOnly`), and the typed render helper. They reference the value objects' POCOs from `entity` and declare no payload type — wire `entity` too (`--list` says so). See the **prompts** reference. | +| `names` | `NamesGenerator()` | `Names.g.cs` — `public abstract class Names` of `const` physical database names, MIRRORING THE METADATA TREE: the object's own `Type`/`SubType`/`Name` (`Name` is the OBJECT's name, never a physical one), then per `source.rdb` keyed by `@role` — `SourcePrimaryType`/`SubType`/`Kind`, `SourcePrimarySchema` when declared, and the physical name under the alias for its `@kind` (`SourcePrimaryTable` / `SourceReplicaView` / `SourcePrimaryProc` / `…Function` / `…MaterializedView`) — then a `Field`/`Column` pair per field, then each identity and index (`IdentityPkName`, and `IdentityUqCustEmailIndex` / `IndexIxCustStatusIndex` — the DATABASE name, carried for `identity.secondary` and `index.lookup` only), then a complete `ColumnsByField`. There is no `ReadOnly`: it is a derivation over `@kind`, so ask `SourcePrimaryKind`. Emitted for every object with a declared or inherited primary source, PLUS a fragment for any abstract base such an object extends (its own identity + columns, and NO source members — it has no table). A class whose object extends another one **inherits** it (`class CopayAuthNames : AuthNames`) rather than restating its constants — a C# `const` is inherited, so `CopayAuthNames.IdColumn` and `CopayAuthNames.SourcePrimaryTable` resolve through the base (`Type`/`SubType`/`Name` are redeclared with `new`, since every artifact carries its own). `abstract`, not `static`, precisely so it can be inherited; it still cannot be instantiated. In the run, `entity` and `db-context` reference the constants (`[Table(OrderNames.SourcePrimaryTable)]`, `[Column(OrderNames.StatusColumn)]`, `.ToView(OrderNames.SourceReplicaView)`); a run without `names` falls back to literals. | +| `template` | `TemplateGenerator.Create(name, template, walk, provider)` (a factory, not `new`) | the generic Mustache `templateGenerator()` primitive. | + +Metadata lives in the directory the gen run's positional `` argument +names (`dotnet meta gen ./model --out …`); with none, `sources` in +`.metaobjects/config.json` decides, and `metaobjects/` is only that file's default. There +is no `--metadata-dir` flag. It is the same canonical JSON every port reads — fused-key form, `source.rdb` + `@table`, +`@column` for a renamed physical column. + +**Projections (read-only views).** An `object.projection` (read-only `source.rdb` +`@kind: view` child) gets an EF entity mapped with `.ToView(...)` and no `[Table]`. + +Its REST surface is generated and READ-ONLY (F22): GET list + GET by id, the same +`?filter[...]`/`?sort=` grammar as a table entity against allowlists built from the +projection's OWN declared field set, and `POST` / `PATCH` / `PUT` / `DELETE` each +answering `405 {"error": "method_not_allowed"}` — 405 and not 404 because the same +path answers GET. A KEYLESS projection mounts no `/{id}` route at all, so it refuses only +the collection verb. Keyless here means anything but a declared single-column +`identity.primary`: no identity, or a composite one. + +Those refusals are mounted EXPLICITLY, not left to ASP.NET: unmounted, the framework +answers its own 405 with an EMPTY body, which is a wire shape no other port sends. + +**Reports.** A concrete `object.report` whose read source is `source.rdb @kind: view` is +served like a keyless projection. For a report ``: `.g.cs` (a keyless row class, +one property per derived field) mapped in the `DbContext` with a `DbSet` and +`HasNoKey().ToView(...)`, `Routes.g.cs` (`MapGet` list, `MapPost` answering 405, no +`{id}` route) and `FilterAllowlist.g.cs`. Every derived field with filter operators +is filterable and sortable; on a report an enum dimension sorts too (an entity's enum +field is still not sortable in C#). The view and its columns are bound by literal, with +no names artifact. `gen` refuses a served report with a derived field over a +`field.object`, or one whose Pascal name equals the report's class name. A report with +no view source, or an abstract one, generates nothing. The route and contract: +`references/reporting.md` in the `metaobjects-authoring` skill. + +**Entity read-view (write-through).** An `object.entity` that keeps its writable `table` +primary source and adds a `@role: replica` `@kind: view` source is a write-through +read-view (#214): the generated EF entity carries the derived `origin.*` fields read-only +and `db-context` registers that read model against the replica view (`.ToView(...)`); +reads route to the view, writes to the table (derived fields excluded), and a +create/update re-reads the row via the view by primary key (read-your-writes). The replica +view's DDL is emitted by the Node `meta migrate` from the same origin assembly as a +projection view. + +**Requirement tests (`requirement-tests`).** A recommended approach, not a contract. For a +model that declares `requirement.*` nodes it writes, per metamodel package, +`Requirements__Witnesses.g.cs` (an interface with one default member per +non-skipped test, each failing with `unimplemented requirement: …`) and +`Requirements__Tests.g.cs` (one xUnit `[Fact]` per requirement; `[Fact(Skip = …)]` +for a `planned` or `retired` one), in `.Requirements`. Both are rewritten whole +on every run, so never edit them: the project's code goes in one witness class +(`.RequirementWitnesses` by default) that implements every generated interface. +Implement each member EXPLICITLY (`void Requirements__Witnesses.req_…() { … }`): a +witness whose requirement is retired or deleted then stops compiling (CS0539), where an +implicit `public void req_…()` goes stale silently. List it in `codegen/Generators.cs` with +its own `OutDir`, the test project's directory, since the tests need xunit: +`new MetaObjects.Codegen.Generators.RequirementTestsGenerator { OutDir = "tests/Acme.Shop.Tests/Generated" }`. +The plain `dotnet meta gen ./metadata --out ./Generated` then writes them there and +`dotnet meta verify --codegen` checks them there, in the same run as everything else. +Options (`OutDir`, `TestNamespace`, `WitnessClass`, `Grain`, `Filter`, `Renderer`, +`WarnUncovered`) are init properties set on the entry in an owned +`codegen/Generators.cs`; `Grain` needs `using MetaObjects.Core.Requirement;`. A filter +REPLACES the default of functional L4/L5. `dotnet meta eject requirement-tests` copies the +generator with its default rendering. The requirement checks in `dotnet meta verify` are +core and are not ejectable. + +## Docs — one more entry in `codegen/Generators.cs` + +There is no `dotnet meta docs` command (removed in 1.1.0, ADR-0034 Amendment 9): the api +reference is `ApiDocsGenerator`, an ordinary entry in the same list as every other +generator: + +```csharp +new ApiDocsGenerator { OutDir = "docs/csharp" } // → docs/csharp/api/csharp (AGENT-API.md + per-entity pages) +``` + +`dotnet meta gen` writes it, alongside everything else `codegen/Generators.cs` declares. +`AGENT-API.md` has the exact imports, signatures, and payload field shapes for the +generated code. **Before calling any generated code, read `api/csharp/AGENT-API.md`.** +Options (init properties): `OutDir` (give it a directory of generated files only; unset, the pages land under `--out`), `ApiSubDir` (default `api/csharp`), +`ProjectName`, `ModelBaseUrl`. To write only the documentation, +`dotnet meta gen --generators api-docs`. In a repository with more than one port, give each port's documentation its own directory: another port's generated pages count as foreign files, and in a shared directory each port's `verify --codegen` reports the other ports' pages as drift. Migrating a project that ran `dotnet meta docs`: +, section "C#". + +## Persistence + routes are the deployed artifact + +C# generates a *complete* server stack: the entity classes + `AppDbContext` ARE the +persistence layer (EF Core), and the minimal-API routes mount on your `WebApplication`. +There is no runtime "ObjectManager" layer to wire — the generated EF Core code is +what runs. (The other ports leave a repository seam; C# does not.) + +## The JSON the API writes + +With no `wire` setting, the generated API writes what System.Text.Json writes: a decimal is a +JSON number with its scale (`12.50`, through `[DecimalWire.Number]`), and a value object's null +member is `null` with its key present. That is the 1.0.x output. `"wire": { "decimals": +"string", "nullMembers": "omit" }` in `.metaobjects/config.json` gives the cross-port form +(`"12.5"`, null members left out). The input checks (a string or a number; `400` past `@scale` +or `@precision`) hold in every mode. Never strip the attributes or delete `DecimalWire.g.cs` to +change the output: set `wire` and regenerate. `dotnet meta verify --codegen` names the `wire` +block that reproduces committed output generated under another setting. See +`references/wire-format.md`. + +## Write your own generator + +For any output the model describes and no reference emits, write an `IGenerator` in the +console project `codegen/` (`dotnet meta init` writes it) and list it in +`codegen/Generators.cs`; `dotnet meta gen` and `dotnet meta verify --codegen` run that list. + +```csharp +// codegen/generators/FieldListGenerator.cs +using MetaObjects.Codegen; + +namespace Codegen.Generators; + +public sealed class FieldListGenerator : IGenerator +{ + public string Name => "field-list"; + + public IEnumerable Generate(GenContext ctx) => + ctx.Entities.Where(o => !o.IsAbstract) // EVERY object arrives, abstract bases included + .Select(o => new EmittedFile( + $"field-list/{o.Name}.txt", + string.Join("\n", o.Fields().Select(f => $"{f.Name}: {f.SubType}{(f.ResolvedIsArray() ? "[]" : "")}")) + "\n")); +} +``` + +List it in `codegen/Generators.cs` as `new Codegen.Generators.FieldListGenerator()`; every +entry runs, and `--generators` only narrows. Model reads: `Fields()`, `Attr(n)`, +`ResolvedIsArray()`, `EffectiveEnumValues`, `MaxLength` resolve; `IsArray`, `OwnAttr`, +`EnumValues` do not. `ValueObjectNames.ResolveFieldRef(field, ctx.Root)` resolves an +`@objectRef`; `CSharpNaming.RoutePath(obj)` is the REST segment; constants come from +`using static MetaObjects.Core.Field.FieldConstants;`. A generator that throws is reported +as the generator, with its message. + +## Scoping a generator to some objects + +C# has no TypeScript-style `filter`: `IGenerator` is `Name` + `Generate(GenContext)` + `OutDir`. +Since ADR-0056 `EntityGenerator` emits a POCO for every concrete `object.value` in +`ctx.Entities`, so a project whose value objects belong to another port (catalog or config +VOs) has to say which objects this generator is for. In order of preference: + +1. **`Include` on the packaged generator.** `EntityGenerator`, `DbContextGenerator`, + `RoutesGenerator`, `FilterAllowlistGenerator`, `NamesGenerator` and `CallableGenerator` take + an optional `Func? Include` init property (it is `Include`, not `Filter`: + `Filter(MetaObject)` is the per-entity generators' own applicability method, which stays + the subclass seam). `null`, the default, accepts every object. + ```csharp + new MetaObjects.Codegen.Generators.EntityGenerator { Include = o => NamingRefs.EffectivePackageOf(o) == "acme::orders" }, + new MetaObjects.Codegen.Generators.DbContextGenerator { Include = o => NamingRefs.EffectivePackageOf(o) == "acme::orders" }, + ``` + Give the same predicate to every entry that should agree: an `Include` on `entity` alone + leaves `db-context` mapping classes that were never emitted. +2. **Hand a generator a narrowed context.** For any `IGenerator` without `Include` (your own, + or a subclass), wrap it: `inner.Generate(ctx.Narrow(o => ...))`. `Narrow` returns the same + context with `Entities` reduced and everything else (`Root`, `Config`, `Warn`, + `Applicability`) kept. Build the `GenContext` by hand only if you must, and copy all of + those members. +3. **Eject and edit** (`dotnet meta eject `): the owned copy's object selection is yours. + +What narrowing reaches. Narrowing reduces `ctx.Entities`, and that is the set `entity`, +`db-context`, `routes`, `filter-allowlist`, `names` and `callable` iterate. A generator that +walks `ctx.Root` instead (the prompt tier: `output-parser`, `extractor`, `output-prompt`, +`render-helper`) is not narrowed, and neither is the resolution of a reference: +`EntityGenerator` finds the value objects an emitted entity points at through `ctx.Root`, so an +anchored value object is still emitted when `Include` rejects it by name. To drop a value +object nothing anchors, reject it in `Include`; to drop one an entity references, change the +model. + +**The anti-pattern: letting a generator emit everything and dropping files by name +afterwards** (a `Where(f => !f.Path.StartsWith(...))` over `Generate`'s result). The emitted +set and the committed set then disagree, `verify --codegen` regenerates the unfiltered set and +reports drift, and a renamed file silently stops matching. Narrow the input, or own the +generator. + +**A hosted runner** (your own `CodegenRunner.Run` call, not `dotnet meta gen`) must apply the +project's `naming` itself. `MetaDataLoader.FromDirectory(dir, registry, ...)` reads +`.metaobjects/config.json` above the directory and sets `root.PhysicalNaming` from it (since +1.1.2; through 1.1.1 a host got snake_case whatever the project said). `GenConfig` is built by +hand, so set its strategy from the root: `ColumnNamingStrategy = load.Root.PhysicalNaming.Columns`. +`MetadataLocation.Resolve(...).Load()` and `dotnet meta gen` do both for you. + +## Extending the generators (open-for-extension, ADR-0002) + +The generators are subclassable: the per-class emit methods are `protected virtual`, +plus finer hooks — `EmitClassHeader` / `EmitClassDeclarationLine` (class declaration: +`partial`, marker interfaces), `EmitPropertyAttributes` (per-property C# attributes), +`EmitFileUsings` (extra usings), `EmitClassBodyTrailer` (extra members). Subclass a +generator and override only the seam you need rather than forking. `@default` on a +scalar emits a literal initializer; an override of `EmitClassDeclarationLine` must not name a +concrete parent as the C# base (the child already declares the parent's properties, so a base would +redeclare them; only an emitted abstract shape is a base); an `object.value`'s default storage is jsonb. + +**Shared + externally-provided enums (FR-019).** A package-level abstract +`field.enum` (`abstract: true`, `@values`) extended by concrete entity fields is +materialized **once** (`Enums.g.cs`) and referenced — no per-entity nested enum. +Adding `@provided: true` to that declaration suppresses materialization entirely: +consuming fields reference a hand-written/third-party enum, and the C# namespace +**binds to the enum's declaring metadata package** via +`GenConfig.PackageNamespaces[""] = "Your.Namespace"` (one entry per namespace; +`ProvidedEnumNamespace` is the single fallback). The `@values` still drive the DB +`CHECK` + validation. This replaces the retired C#-only `@csEnumType` FQN attr +(ADR-0026) — no language FQN ever lives in metadata (ADR-0001). + +## Re-scaffold this context + +Agent-context scaffolding is owned by the Node `meta` CLI (ADR-0015). `dotnet meta +agent-docs` only prints a redirect and exits non-zero — it does **not** scaffold. +(Re)scaffold the slim always-on Markdown + these `metaobjects-*` skills with +`npx meta agent-docs --server csharp [--client ] [--out ]`. A C# consumer +needs Node `meta` (npx) for this one scaffold step; codegen + verify stay on `dotnet meta`. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/dependencies-and-shared-models.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/dependencies-and-shared-models.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/dependencies-and-shared-models.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/dependencies-and-shared-models.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/owning-generators.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/physical-names.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/physical-names.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/physical-names.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/physical-names.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md similarity index 67% rename from fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md index 2fbe964b28..2c2f61d72d 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -1,38 +1,16 @@ -# Working with MetaObjects in this project +# Standing principles -> Stack: typescript server, no 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. +The rules that hold in every MetaObjects project, whatever the stack and whatever the task. The +always-on file in `.metaobjects/` carries the short form; this page is the full text, with the +reasons. Read it when a rule there does not settle the case in front of you. -MetaObjects is a metadata standard: typed metadata (in `metaobjects/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `npx meta gen`. +## Contents -## Read the generated reference before you touch a tier -`meta gen` writes four reference files under this project's docs target -(`targets.docs.outDir` in `metaobjects.config.ts`) when `generators` lists the generator -for each — `agentDocsFile()` for the three `agent/` pages, `apiDocsFile()` for the api -reference. There is no separate docs command: documentation is one more entry in the same -list every other generator is in. They are generated from THIS project's metadata, so they -describe what it actually is rather than what the framework does in general — and reading -the relevant one first is how you avoid inferring the answer from generated code, which is -the disposable half. - -- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each - column came from, keys, foreign keys, constraints. -- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter - operators the generators actually emitted. -- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and - filterability per field, and each declared grid. -- **`agent/requirements.md`** — before adding a capability. What is already claimed, and - what was deliberately **retired** — an entry saying a capability must not be rebuilt is - the one thing you cannot learn from the code, because the code is gone. - -Not there? List the missing generator — and a `docs` target, if `targets` has none — in -`generators`, then run `meta gen`. +- Principles +- Authoring rules you must not violate ## 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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. - **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. @@ -50,35 +28,9 @@ Not there? List the missing generator — and a `docs` target, if `targets` has - **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. ## Authoring rules you must not violate + - **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. - Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/template-or-code.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/wire-format.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/wire-format.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/wire-format.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-codegen/references/wire-format.md diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/SKILL.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-prompts/SKILL.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-prompts/SKILL.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-prompts/SKILL.md diff --git a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-prompts/references/csharp.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-prompts/references/csharp.md new file mode 100644 index 0000000000..e6e210e2c8 --- /dev/null +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-prompts/references/csharp.md @@ -0,0 +1,150 @@ +# C# parser-on-receipt + +For every RESPONDING `template.prompt` — one declaring `@responseRef` — +`MetaObjects.Codegen`'s `OutputParserGenerator` emits a **typed parser** that validates +a model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never +`@payloadRef` (which types the request the prompt renders outbound), and a +`template.output` gets no parser at all. This is the receive side only — codegen emits +**no** provider/LLM-call layer; you compose the call yourself. C# names its types after the +resolved VALUE OBJECT, so the response type simply IS that VO's class — no second +naming convention, and the parser and the record can't silently drift. + +## Contents +- Wire the generator +- What it emits +- The response-format prompt fragment (FR-010) +- The three-step consumer pattern +- Recommended LLM caller (bring-your-own) +- Consumer dependency +- Drift gate + +## Wire the generator + +`OutputParserGenerator` (stable name `output-parser`) runs as part of +`dotnet meta gen`, alongside `entity`, which emits the `@responseRef` value object's POCO +it parses into (ADR-0056 — the template tier declares no copy). List both in +`codegen/Generators.cs`, the generator list `dotnet meta gen` runs: + +```csharp +new MetaObjects.Codegen.Generators.EntityGenerator(), +new MetaObjects.Codegen.Generators.OutputParserGenerator(), +``` + +```bash +dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog +``` + +## What it emits + +Per responding `template.prompt`, `dotnet meta gen` writes one +`.response.cs` with a static `Parser` following the .NET BCL +`Parse`/`TryParse` dual API. The strict tier is JSON-only — an `@responseFormat: xml` +reply gets the tolerant extract and neither `Parse` nor `TryParse` — +`Parse` throws, `TryParse` returns a bool plus an out-error: + +```csharp +// generated .response.cs (shape) +public static class NpcResponseParser +{ + /// malformed JSON or schema mismatch. + public static NpcResponse Parse(string text) => + JsonSerializer.Deserialize(text, Options) + ?? throw new JsonException("deserialized to null"); + + public static bool TryParse(string text, + [NotNullWhen(true)] out NpcResponse? value, + [NotNullWhen(false)] out string? error) { /* ... */ } +} +``` + +The `[NotNullWhen]` attrs let nullable-flow analysis use `value` without a null-check +after a `true` return (and `error` after `false`). `NpcResponse` here is the +`@responseRef` value object's own POCO from `entity`, and a `@required` member is +enforced by a generated `JsonTypeInfo` modifier. The generator also emits a tolerant +`Extract(string[, ExtractOptions])` (self-contained) + `Extract(MetaObject, string, ...)` +(runtime-delegating, fully populating nested components) returning an `ExtractionResult` +with the nullable `Extracted` mirror — its own file, once per value object per run — +for a classified per-field report rather than a throw. + +## The response-format prompt fragment (FR-010) + +For every responding `template.prompt`, `MetaObjects.Codegen`'s `OutputPromptGenerator` +(stable name `output-prompt-generator`) emits a `.responseFormat.cs` +declaring a static `ResponseFormat` class with a `RenderFormat()` / +`RenderFormat(PromptOverrides)` pair, backed by the render engine's +`OutputFormatRenderer` — the "produce your answer like this" fragment for the model. It runs as part of the same `dotnet meta gen` invocation as the entity +and parser generators, once `codegen/Generators.cs` lists it too: + +```csharp +new MetaObjects.Codegen.Generators.OutputPromptGenerator(), +``` + +`@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`) +controls the fragment's presentation; guidance is never emitted as comments. Skipped for +`template.output` nodes and an unresolved `@responseRef` — the same skip contract as the +parser generator. There is NO format gate: the old `@format ∈ {json,xml}` test read the +syntax of the outbound body to decide whether to describe the reply. The baked spec's +root name is the response record's, agreeing with the parser's root. + +## The three-step consumer pattern + +Render the prompt → call your LLM client (provider-agnostic; nothing is generated +here) → parse the response with the generated parser: + +```csharp +string llmResponse = await myLlmClient.CompleteAsync(promptText); // YOUR code — no generated provider + +// Throwing path +var npc = NpcResponseParser.Parse(llmResponse); + +// TryParse for explicit error handling +if (NpcResponseParser.TryParse(llmResponse, out var npc, out var error)) + return Ok(npc); +return BadRequest(new { error }); +``` + +## Recommended LLM caller (bring-your-own) + +`dotnet meta gen` emits **no** provider/LLM-call layer and never will — calling is a +commodity the ecosystem already solves (ADR-0024). You bring the caller; MetaObjects +owns the typed render → parse (above) → record. For the call step use the idiomatic +.NET library: + +```csharp +using Microsoft.Extensions.AI; // recommended — IChatClient is provider-agnostic + +IChatClient client = /* an Anthropic / OpenAI / Azure IChatClient */; +ChatResponse resp = await client.GetResponseAsync(promptText); +string text = resp.Text; + +var npc = NpcResponseParser.Parse(text); // the generated parser, above +``` + +**Recommended: Microsoft.Extensions.AI** (`IChatClient`) — the emerging +provider-agnostic .NET standard; its one-method shape matches MetaObjects' call seam. +For higher-level agent orchestration, **Semantic Kernel** layers on top of the same +`IChatClient`. + +> The typed-trace recorder + render→call→record convenience loop ship in TypeScript +> today; the C# port is planned (ADR-0024). Until then the call is your code and the +> generated parser is the typed receive side. + +## Consumer dependency + +`System.Text.Json` ships in the .NET 8 BCL — no NuGet to add. The generated parser +uses strict (case-sensitive) options. + +## Drift gate + +`MetaObjects.Render`'s `Verify.Check(string templateText, IReadOnlyList +fields, VerifyOptions? options = null) -> IReadOnlyList` walks a Mustache +template's tokens against the payload field tree — each `{{...}}` that doesn't resolve +yields a `VerifyError(Code, Path)` (empty list = no drift). Assert it is empty in an +xUnit test to fail the build on prompt/payload drift; `dotnet meta verify` additionally +catches a stale committed parser. + +**Mustache only (ADR-0061).** Jinja bodies are a Python and TypeScript engine. A ref whose +only body is a `.jinja` / `.j2` / `.jinja2` file is refused here with +`ERR_TEMPLATE_ENGINE_UNSUPPORTED` (by the filesystem provider, and as a drift line by +`verify`), rather than reported as missing. Keep templates the C# port renders in +Mustache. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-runtime-ui/SKILL.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-runtime-ui/SKILL.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-runtime-ui/SKILL.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-runtime-ui/SKILL.md diff --git a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-runtime-ui/references/csharp.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-runtime-ui/references/csharp.md new file mode 100644 index 0000000000..671794f9b3 --- /dev/null +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-runtime-ui/references/csharp.md @@ -0,0 +1,115 @@ +# C# server runtime + +The C# runtime tier **is the generated EF Core stack itself** — there is no separate +`ObjectManager` engine to wire. `EntityGenerator` + `DbContextGenerator` emit +`.g.cs` classes plus one `AppDbContext.g.cs` that ARE the persistence layer (EF +Core), and `RoutesGenerator` emits ASP.NET minimal-API routes that mount on your +`WebApplication`. Unlike the other ports, C# leaves **no repository seam** — the generated +code is what runs. Schema is TS-owned (ADR-0015); EF Core is pure data-access at runtime. + +## Contents +- The generated persistence layer +- Query + persist with EF Core +- Return-type contract +- Serving the REST contract +- Physical names below LINQ + +## The generated persistence layer + +`dotnet meta gen` emits an entity POCO per `object.entity` / projection and a single +`AppDbContext` with a `DbSet` per entity (naively pluralized) plus an `OnModelCreating` +that carries the mapping: + +```csharp +// generated AppDbContext.g.cs +public class AppDbContext : DbContext +{ + public AppDbContext(DbContextOptions options) : base(options) { } + + public DbSet Authors { get; set; } = default!; + + protected override void OnModelCreating(ModelBuilder b) + { + // .HasConversion() (enums), .OwnsOne(...).ToJson(...) / .OwnsMany(...).ToJson(...) + // (jsonb object fields), .HasPrecision(p,s) (decimals), .ToView("v_...") (projections), + // .HasDiscriminator(...).HasValue(...) (TPH single-table). + } +} +``` + +## Query + persist with EF Core + +Register the generated context against your provider, then query/write with plain LINQ + +`SaveChangesAsync` — the generated entities and `AppDbContext` are ordinary EF Core: + +```csharp +builder.Services.AddDbContext(o => o.UseNpgsql(connString)); +// ... +var authors = await db.Authors.AsNoTracking() + .Where(a => a.Name == "Ada").ToListAsync(); + +db.Authors.Add(new Author { Name = "Ada" }); +await db.SaveChangesAsync(); // server-generated PK round-trips back onto the entity +``` + +## Return-type contract + +The EF Core query path materializes **native in-process CLR types**, never wire strings +(ADR-0019), verified by the port's runtime-return-type test: + +- `field.decimal` → `decimal` — exact, **lossless end-to-end**, no float round-tripping. +- `field.long` → `long`; other scalars to their native CLR types. +- a default `field.timestamp` (instant) → `DateTimeOffset` over a `timestamptz` column; a + naive `@localTime:true` timestamp → `DateTime` — native temporals, not strings. +- an untyped `@dbColumnType:jsonb` open-JSON column → a `System.Text.Json.JsonDocument` (the + parsed value, not raw text); a **typed** `field.object @storage:jsonb` (with `@objectRef`) + materializes the generated value-object class via `OwnsOne`/`OwnsMany(...).ToJson(...)`. + +Wire canonicalization (currency → integer minor units, temporals → ISO-8601, UUID → +canonical hex) happens only when a row leaves over HTTP — at the serialization boundary in +the generated routes — never inside the EF Core query path. Currency is integer minor units +on the wire and in storage (a native `long` in-process); the server never formats — +formatting is client-side in the universal (TS/Angular) web client. + +## Serving the REST contract + +`RoutesGenerator` emits `Routes.g.cs` — full CRUD per writable entity +(`source.rdb @kind="table"`), read-only list/get for a projection/view, and a collection-GET +for a composite-/no-PK entity — with a `MapRoutes(this IEndpointRouteBuilder, +string prefix = "/api")` extension, on the +cross-port REST contract (five CRUD endpoints, `?filter[field][op]=`, `?sort=field:asc`, +`?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). A TPH `@discriminator` +base emits polymorphic `GET /(+/{id})` plus a per-subtype CRUD set at +`//` (create injects the discriminator, cross-subtype +get/update/delete → 404). Wire it after registering the context: + +```csharp +var app = builder.Build(); +app.MapAuthorRoutes("/api"); // the generated extension; you pass the prefix at mount time (default "/api") +app.Run(); +``` + +Filter operators (`eq` `ne` `gt` `gte` `lt` `lte` `in` `like` `isNull`) ship via the +per-entity `FilterAllowlist` (from `FilterAllowlistGenerator`); the generated list +handler calls the `FilterParser` / `EfCoreFilterDispatch` runtime helpers in +`MetaObjects.Codegen`, so your ASP.NET host references that assembly at runtime. After +`dotnet meta eject routes` it calls your own copies in `codegen/runtime/` (namespace +`Codegen.Runtime`) instead: compile that folder into the app and the reference goes away. The same +universal TS/Angular web client consumes those routes unchanged — the wire format matches +the Java, Kotlin, and Python backends byte-for-byte. + +## Physical names below LINQ + +Inside LINQ keep the property (`db.Authors.Where(a => a.Name == …)`): it is type-checked +against the model, and a string constant there trades a compile error for a runtime one. +Where LINQ does not reach — raw SQL, a migration script, a log line — take the physical +name from the generated `Names.g.cs`. `names` is opt-in — list it in +`codegen/Generators.cs` — and once listed the generated entity and `AppDbContext` already read +it, so it cannot disagree with the +mapping: `AuthorNames.SourcePrimaryTable` is the table (the member is named for the +source's `@kind`, so a view reads `SourceReplicaView` / `SourcePrimaryView` and a stored +procedure `SourcePrimaryProc`), `AuthorNames.Column` the column, +`AuthorNames.ColumnsByField` the whole map. `AuthorNames.Name` is the OBJECT's name — +`Author`, not `authors` — so it is the one member that compiles while meaning something +else than it did before 0.25.0. Never a literal — and never +`nameof(Author.Name)`, which is the CLR property, not the column. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-verify/SKILL.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/SKILL.md similarity index 99% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-verify/SKILL.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/SKILL.md index 57b556e1f6..fcdb550249 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-verify/SKILL.md +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/SKILL.md @@ -334,4 +334,5 @@ what's wrong. --- -For the migration tooling read `references/migration.md`. +For the migration tooling read `references/migration.md`. Before upgrading MetaObjects, or +when a project runs more than one port, read `references/upgrading.md`. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-verify/references/migration.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/references/migration.md similarity index 86% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-verify/references/migration.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/references/migration.md index f589708490..ff97eb6b8e 100644 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-verify/references/migration.md +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/references/migration.md @@ -18,6 +18,7 @@ stays native to the language. - Install (Node, dev-only) - Output format - The workflow +- Renames and destructive changes - Dialects - Live-DB drift: `meta verify --db` - Index modeling (Postgres) @@ -109,8 +110,42 @@ meta migrate baseline --from-db --db postgresql://... --dialect postgres 2. **Review the SQL.** Read the emitted `up.sql` (forward) and `down.sql` (rollback) before applying. Destructive changes (drop column / drop table) are - opt-in — the engine blocks them unless explicitly allowed, and routes ambiguous - rename-vs-drop+add decisions through a prompt rather than guessing. + opt-in — the engine refuses them until you allow each kind by name (`--allow`, + below), and an ambiguous rename-vs-drop+add aborts unless you declare the rename + or choose with `--on-ambiguous`. It never guesses. + +## Renames and destructive changes + +A renamed field or object is a renamed column or table, and the diff cannot tell that +from a drop plus an add unless the names are close. Declare the rename and the data is +kept: + +```bash +meta migrate --dialect postgres --slug rename-origin --rename-column shipment.origin_city=origin_port +meta migrate --dialect postgres --slug rename-stop --rename-table stop=port_call +``` + +- `--rename-column [schema.]table.old=new` and `--rename-table [schema.]old=new` are + repeatable. For a column, `table` is the table's name in the metadata, so it is the + **new** name when the table is renamed in the same run. A declared rename needs no + `--allow drop-column`, and one that finds nothing to rename is refused rather than + falling back to drop and add. Change a column's type, nullability or default in a + later run, not in the rename's. +- `--allow ` permits a destructive change by kind. A refusal names the token it + needs. The tokens: `drop-column`, `drop-table`, `type-change`, `drop-index`, `drop-fk`, + `drop-check`, `drop-view`, `drop-view-cascade`, `drop-column-read-by-view`, + `adopt-view`, `nullable-to-not-null`, `drop-identity-default`, `drop-comment`, + `drop-unmanaged`. Changing an enum's values replaces its CHECK, so it needs + `--allow drop-check`. `drop-column` deletes the column's data: for a rename use + `--rename-column`, never `--allow drop-column`. +- `--on-ambiguous abort|rename|drop-add` (default `abort`) decides a pair the + heuristic offers as a possible rename. Declaring the rename is clearer than `rename`. +- `migrate.allow` and `migrate.onAmbiguous` in `.metaobjects/config.json` set the same + things for every run. Prefer the flag: a standing allowance hides the next destructive + change from review. + +`docs/features/migrations-and-drift.md`, "Renames and populated tables", covers what a +rename carries with it (constraint names, views) and changes that need a backfill. 3. **Apply** the pending migrations against the DB; migration history is tracked in a ledger table: diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-verify/references/requirements.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/references/requirements.md similarity index 100% rename from fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-verify/references/requirements.md rename to fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/references/requirements.md diff --git a/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/references/upgrading.md b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/references/upgrading.md new file mode 100644 index 0000000000..23ccb45965 --- /dev/null +++ b/fixtures/agent-context-conformance/csharp/expected/.claude/skills/metaobjects-verify/references/upgrading.md @@ -0,0 +1,10 @@ +# Upgrading MetaObjects and keeping the ports in sync + +Read this before you move a project to a new MetaObjects release, and when a project runs more +than one port (a TypeScript client beside a Java backend, say). + +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. + +`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. + +**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. diff --git a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md new file mode 100644 index 0000000000..cc76b4b873 --- /dev/null +++ b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/AGENTS.md @@ -0,0 +1,88 @@ +# Working with MetaObjects in this project + +> Stack: csharp server, no client. +> +> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### C#: generate with `dotnet meta gen` + +1. **Config.** `codegen/Generators.cs` (written by `dotnet meta init`) is the generator list. + Nothing is generated until it is listed. The EF Core data layer and its routes: + + ```csharp + new MetaObjects.Codegen.Generators.EntityGenerator(), + new MetaObjects.Codegen.Generators.DbContextGenerator(), + new MetaObjects.Codegen.Generators.FilterAllowlistGenerator(), + new MetaObjects.Codegen.Generators.RoutesGenerator(), + ``` + + `RequirementTestsGenerator { OutDir = "/Generated" }` writes one test per + requirement; `ApiDocsGenerator` writes `api/csharp/AGENT-API.md`. `dotnet meta gen --list` + is the catalog. +2. **Generate.** `dotnet meta gen`. `dotnet meta eject ` copies a generator into the repo + to change what it emits. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `dotnet meta verify --codegen` (the generated code and the requirements). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md new file mode 100644 index 0000000000..cc76b4b873 --- /dev/null +++ b/fixtures/agent-context-conformance/csharp/expected/.metaobjects/CLAUDE.md @@ -0,0 +1,88 @@ +# Working with MetaObjects in this project + +> Stack: csharp server, no client. +> +> Generated by MetaObjects (refresh: `meta init --refresh-docs`). Put project notes in your +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### C#: generate with `dotnet meta gen` + +1. **Config.** `codegen/Generators.cs` (written by `dotnet meta init`) is the generator list. + Nothing is generated until it is listed. The EF Core data layer and its routes: + + ```csharp + new MetaObjects.Codegen.Generators.EntityGenerator(), + new MetaObjects.Codegen.Generators.DbContextGenerator(), + new MetaObjects.Codegen.Generators.FilterAllowlistGenerator(), + new MetaObjects.Codegen.Generators.RoutesGenerator(), + ``` + + `RequirementTestsGenerator { OutDir = "/Generated" }` writes one test per + requirement; `ApiDocsGenerator` writes `api/csharp/AGENT-API.md`. `dotnet meta gen --list` + is the catalog. +2. **Generate.** `dotnet meta gen`. `dotnet meta eject ` copies a generator into the repo + to change what it emits. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `dotnet meta verify --codegen` (the generated code and the requirements). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/csharp/stack.json b/fixtures/agent-context-conformance/csharp/stack.json new file mode 100644 index 0000000000..82576207c7 --- /dev/null +++ b/fixtures/agent-context-conformance/csharp/stack.json @@ -0,0 +1 @@ +{ "servers": ["csharp"], "clients": [] } diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references/requirements.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references/requirements.md new file mode 100644 index 0000000000..2bcebe9ec1 --- /dev/null +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-audit/references/requirements.md @@ -0,0 +1,154 @@ +# Requirements — the audit dimension + +This project declares `requirement.*` nodes, so the ledger is an auditable surface. Audit it +for **truthfulness**, not volume. A large ledger that lies is worse than a small one that +does not, because every later reader trusts it. + +Run `verify` first — the project's own: `meta verify`, `metaobjects verify`, +`mvn metaobjects:verify` or `dotnet meta verify`. Every port runs the same requirement +gate. It settles referential integrity mechanically — do not spend audit effort re-deriving +what a green run already proves. + +## Contents +- What verify has already proven (do not re-check by hand) +- Currency checks — run these before sampling +- What only a human or an agent reading the code can catch +- Scope — do NOT flag these as defects +- Reporting + +## What verify has already proven (do not re-check by hand) + +A run answers two different questions in two separate sections. Both are already-proven +ground; neither is worth an auditor's time. + +**The gate** settles referential integrity: links sit at or below the L4 floor, nesting +agrees with levels, `@status` values are legal, and references resolve (with dangling allowed +on `planned`, whose nodes do not exist yet). + +**The authoring lint** — printed under its own heading, advisory, and unable to change the +exit code — settles the naming and prose defects you would otherwise find by reading every +entry. **Only the Node `meta verify` prints it.** On a Python, Java, Kotlin or C# project +whose pipeline runs that port's `verify` alone, nothing below has been proven: run +the Node `meta verify` over the same metadata, or audit these by hand. + +| code | what a clean run has already proven | +|---|---| +| `WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE` | no `name` is blank, space-padded, or carries a `.` / `/` / `:` that breaks the dotted path or the generated stub's filename | +| `WARN_REQUIREMENT_NAME_READS_AS_PROSE` | no `name` is a sentence sitting in the address slot | +| `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT` | no `name` and `@statement` are the same sentence written twice | +| `WARN_REQUIREMENT_PROSE_EMPTY` | no `@statement` / `@counterexample` is declared-but-blank (the loader enforces presence, never content) | +| `WARN_REQUIREMENT_PROSE_DUPLICATED` | no `description` repeats `@statement` whole or as its opening sentence, and no `@counterexample` repeats it | +| `WARN_REQUIREMENT_INERT_DOC_SLOT` | no `summary` is set — nothing reads it, and `@statement` is already the required one-liner | +| `WARN_REQUIREMENT_TITLE_IS_AN_ID` | no `title` leads with a catalogue or ticket id | + +Three limits, and each puts something back on your list: + +- **It is one port's.** See above: establish which `verify` the project's CI runs before + crediting a green pipeline with the table. +- **The lint is mutable.** `--no-requirement-lint` / `META_NO_REQUIREMENT_LINT=1` silences the + advisory half while the gate still runs. **Establish whether the project mutes it** (§F of + the checklist asks this) — against a muted lint the whole table above proves nothing. +- **It reports only EXACT repeats.** A paraphrase — `description` restating `@statement` in + different words, a `@counterexample` that merely negates it — is invisible to the tool and + is a legitimate hand finding. + +`title` is deliberately NOT flagged as an inert slot; it is chartered as the entry's label and +is rendered (see item 6). Do not report a populated `title` as a defect. + +## Currency checks — run these before sampling + +A green gate proves the ledger's links resolve, not that the ledger keeps up with the code. +These three checks measure that, and the first one chooses the sample for item 1 below. + +**(a) Ledger stale.** From git history, take the commits that add a feature (a `feat:` / +`feat(...)` subject under Conventional Commits; otherwise merged feature branches or PRs +labelled as features) over the last six months or the last 50 such commits, whichever is +fewer. Report two numbers: the share of those commits that also change requirement metadata +(any file holding `requirement.*` nodes), and the count of feature commits since the most +recent change to that metadata. A share **under half**, or **more than ten** feature commits +since the last ledger change, is the finding `ledger-stale`, stating both numbers. The +feature commits that did not touch the ledger are the sample for item 1: read what each one +built and check that a requirement says so with a true status. + +**(b) Nothing implements a live claim.** Run the project's verify with the strict switch: +`meta verify --require-implementers`, `metaobjects verify --require-implementers`, +`mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` or +`dotnet meta verify --require-implementers` (all ports also read +`META_REQUIRE_IMPLEMENTERS=1`). Each `WARN_REQUIREMENT_NOTHING_IMPLEMENTS` on a `live` entry +is a finding (`live-claim-unimplemented`) naming the entry. State whether the project's own +gate passes the switch; if it does not and the ledger has no such entries, recommend adding +it so the ledger cannot fall behind again. + +**(c) Live claim, unwired implementer.** For each `@implementedBy` target of a `live` entry, +find the generated code that target produces and confirm hand-written code imports or calls +it. A live claim whose implementer is generated code nothing uses is the finding +`live-claim-unwired-implementer`: the claim resolves, so verify is satisfied, but the +capability is not reachable. Before raising it, rule out a runtime use: code that reaches the +entity through the runtime `ObjectManager` (or a metadata-driven admin or tool registry) +uses it without importing anything generated, so search for the entity's name in those calls +too. + +## What only a human or an agent reading the code can catch + +**1. Statuses that are false.** The highest-value finding in the whole dimension. A +requirement marked `live` whose implementation was gutted; one marked `partial` that is now +complete; one marked `planned` that was quietly built months ago. Sample the claims +and read the nodes, starting with the feature commits currency check (a) found outside the +ledger. `status` is the only payload with controlled evidence behind it — the +resurrection protection rides entirely on it being true. + +**2. Non-violable statements.** Every requirement must state what breaking it looks like. +*"Every entity has a uuid primary key"* is violable — point at one with a composite key. +*"Things are persisted"* is not; it is a description wearing a requirement's shape. Flag +these: they inflate the ledger while proving nothing. + +**3. Claim-padding.** An entity appended to an existing unrelated requirement's +`implementedBy` purely to silence the coverage warning. The claim resolves, so verify is +happy, but it carries no information about what the entity is *for*. Look for entities whose +only claim is a high-fan-out architectural rule when they plainly have product meaning. + +**4. Misfiled kind.** `functional` is checked by EXISTENCE, `architectural` by UNIVERSALITY — +opposite polarity, and nothing mechanical catches a misfile. The discriminator: did this +exist because someone asked for something (functional), or because every entity here looks +like this (architectural)? An architectural entry with one claimant is usually a misfiled +functional one. + +**5. Levels used as decoration.** L1–L3 are organisational and must never reference the +model. If the tree is flat, or every entry is L3, the levels are carrying no information and +should be simplified rather than defended. + +**6. Titles that are not labels.** `title` is now **rendered** — `meta gen`'s +`requirementsFile()` heads each entry `## checkout.payment — Payment capture`, the path +first and the label after it — so a slot +that used to be inert is read by every reader of the generated page. The lint catches only +the id-shaped case (`FR-448 …`); it cannot tell a *useless* label from a good one. Read the +titles: one that restates `@statement`, or repeats the path in prose, is now visible noise in +a heading rather than a private authoring habit. An absent `title` is NOT a finding — the +entry heads by its path alone, which is what every sibling surface addresses it by. + +**7. Witnesses that do not test the claim.** Where the project generates requirement tests +(the `requirement-tests` generator, in any port), a passing generated test proves only that +its witness — or, in TypeScript, the hand-filled stub — ran. Sample the witnesses and read +them against the counterexample each generated test carries: one that asserts nothing, or asserts +something else, is the same defect `@verifiedBy` had. Check also for witnesses that have +gone stale silently: a Java witness that lacks the override annotation, a C# witness implemented +implicitly, and any Python witness keep existing after their requirement is retired or +deleted, and nothing reports them. + +## Scope — do NOT flag these as defects + +- **Unclaimed `object.value` / `object.projection`.** Exempt by design: a value is a shape, a + projection derives from a claimable entity. +- **Fields, views, validators and identities without their own requirement.** Member-grain + coverage is explicitly rejected — plumbing members are covered by architectural + requirements with high fan-out. L5 exists so a member claim *can* be made where it means + something, never so every member must carry one. +- **Dangling links on `planned`.** Correct — the plan precedes the nodes. +- **A project with no requirements at all.** The feature is opt-in by declaration; absence is + not a finding. + +## Reporting + +Report findings as claims to verify, not as a score. "These 4 requirements are marked `live` +and I could not find their implementation" is actionable; "ledger maturity: 62%" is not, and +invites optimising the number. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md index 1979d161f2..de11bce2cc 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -38,7 +38,7 @@ This file covers what almost every model needs. The topics below live in | `references/inheritance-tph.md` | several entities are variants of one thing sharing a single table (`@discriminator`) | | `references/metadata-dependencies.md` | the project builds on another package's metadata (`dependencies`, cross-package `overlay`) | | `references/reporting.md` | what a declared report gives you: what columns a report gets, time grains, null rules, rows with no facts (`@spine`), defaults (`@default`), engine differences | -| `references/requirements.md` | installed only when the project declares `requirement.*` nodes | +| `references/requirements.md` | you add, change or review a `requirement.*` node | ## The operating principle: model-first, generate-first @@ -483,7 +483,7 @@ Every requirement must be **violable**: if you cannot say what breaking it looks **A commit that adds, changes or removes behaviour updates the ledger in the same commit** (the new or changed claim, its `@status` and `@implementedBy`), or says in the commit message why it does not. `verify` checks that links resolve, not that the ledger is current. -If the project declares any `requirement.*` node, `references/requirements.md` is installed with the full authoring rules. If it does not and you are adding the first one, the shape is: +`references/requirements.md` has the full authoring rules. The shape of one node: ```yaml - requirement.architectural: diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/requirements.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/requirements.md new file mode 100644 index 0000000000..abd10504a0 --- /dev/null +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/requirements.md @@ -0,0 +1,225 @@ +# Requirements — `requirement.functional` / `requirement.architectural` + +Capabilities are **metadata**, declared in the metadata directory beside the entities they +describe. Read the existing requirement nodes before designing anything. Two rules matter +more than the rest. + +## Contents +- A requirement is PRESCRIPTIVE — it states what should be true, never what happened +- Do not delete a retired capability's entry +- When you add an entity, claim it +- A feature commit updates the ledger in the same commit +- Every requirement states its counterexample +- Four prose slots, and `statement` is the one that means "description" +- `title` is a LABEL and is chartered here; `summary` is not +- Never put a catalogue or ticket id in `title` +- The `name` is an address, so write it as an identifier +- Hierarchy is nesting, and links live at the bottom +- Write an L5 ref as a dotted `pkg::Owner.member` path +- L1–L3 are levels of abstraction and ownership in the problem domain +- Architectural requirements are the other kind +- Record gaps rather than rounding them off +- `status: planned` locks in work you have not started +- Which `verify` says what +- A requirement's name and package end up in generated test names + +**1. A requirement is PRESCRIPTIVE — it states what should be true, never what happened.** +`status` is `planned | live | partial | retired`, and every one of them states something +meant to be true. `retired` is not the exception: it means **"this was built, then +deliberately removed, and must not be rebuilt"** — a prohibition in force, falsifiable by +exactly one observable, the capability reappearing. + +So write a retired entry as a rule, not a diary. `statement: "An unpaid order is never +expired by a wall-clock timer"` is a prohibition; `"We used to expire orders on a timer"` is +history wearing a requirement's shape, and no gate will catch it. On a retired entry +`counterexample` describes **the revival** — the thing a future reader is about to propose. + +**Do not delete a retired capability's entry.** Keeping it is the single most load-bearing +thing this vocabulary does: given a brief for a capability that had been retired, agents +working from the model alone proposed rebuilding it 24 times out of 24. The entry is what +stops that, so a retirement is a status change plus a rewritten statement, never a deletion. + +Three rules the loader enforces on `retired`: + +- **`implementedBy` is REFUSED** — a retired capability has no implementation by definition. + What used to implement it goes in `notes`. If deleting the references feels wrong, the + capability is probably not retired: an entry whose nodes are still there is `live` or + `partial`. +- **`supersededBy` names the requirement that replaced it**, legal here only, and RESOLVED — + so the chain still works when the replacement is itself retired later. +- It never counts toward object coverage, and is exempt from the architectural universality + check. + +Leaving a dangling `implementedBy` is **correct only on `planned`**: the entry precedes the +nodes. On `live` or `partial` the same dangling reference is an error — the model moved and +the requirement went stale. + +**2. When you add an entity, claim it.** Every `object.entity` should appear in some +requirement's `implementedBy`, or `verify` says so. + +**A commit that adds, changes or removes behaviour updates the ledger in the same commit.** +`verify` checks that the ledger's links resolve, never that it is current, so nothing fails +when a feature ships and its requirement does not move. In the same commit, write the new or +changed claim, set its `@status` (`planned` to `live` when it lands, `retired` when it is +removed) and keep its `@implementedBy` pointing at the real nodes (a retired entry carries +none). If the commit should not +touch the ledger, say why in the commit message. + +**Every requirement states its counterexample.** One sentence: what breaking it looks like. +*"Every entity has a uuid primary key"* is violable — point at one with a composite string +key. *"Things are persisted"* is not, and is a description rather than a requirement. Same +rule kills *"the system is reliable"*. If you cannot say what breaking it looks like, +delete it. + +**Four prose slots, and `statement` is the one that means "description".** A requirement can +also carry the common `description` and `notes`, and they overlap badly unless you decide the +split before writing any of them: + +- `statement` — **the claim**. This IS the description of what the requirement is +- `counterexample` — **what would falsify the claim**, which is what makes it checkable +- `description` — **the scope**: what the claim covers, what it deliberately does not, and + which sibling entry owns the rest +- `notes` — **the evidence**: how you know the `status` is true — citations, vocabularies, the + control you ran to prove an absence was real + +**`title` is a LABEL and is chartered here; `summary` is not.** A requirement's `name` is an +identifier and its address renders as a dotted camelCase path, so a short noun-phrase `title` is +what an index shows — the requirement attribute table in `spec/capability-ledger.md` says so by +name, and the generated requirements page renders it in the heading after the path +(`## checkout.payment — Payment capture`). `summary` is different: `statement` is already the +required one-line sentence, so a `summary` can only repeat it and nothing reads it (`verify` warns, +`WARN_REQUIREMENT_INERT_DOC_SLOT`). `notes` is unrendered on purpose — chartered internal-only. + +**Never put a catalogue or ticket id in `title`.** A title is a noun phrase; an id is not a name. +`title: "FR-467 — Order recording"` is two things in one slot — put the id in `trackedBy` and +keep the phrase as the title. `verify` warns (`WARN_REQUIREMENT_TITLE_IS_AN_ID`). + +**The `name` is an address, so write it as an identifier.** It is the segment of the dotted +path (`Ordering.Placement.Recorded`) and the filename of the generated test stub. A `.` in a +name is indistinguishable from nesting — `Orders.Recorded` and `Orders` containing `Recorded` +produce the same path — and a sentence for a name puts the claim somewhere nothing reads it. +Both load; `verify` warns (`WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE`, +`WARN_REQUIREMENT_NAME_READS_AS_PROSE`, `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT`). + +Two failure modes, both of which look like diligence. A `description` that **paraphrases the +statement** is padding, and it makes every later reader trust the ledger less — leave it off +instead, it is optional. A `description` that **narrates the evidence** belongs in `notes`; +the tell is a fact you had to read the implementation to learn. Mechanical test for the last +line: *would this sentence have to change if the code changed but the model did not?* Then it +is `notes`. `verify` warns on the exact repeats (`WARN_REQUIREMENT_PROSE_DUPLICATED`), and on +a `statement` or `counterexample` that is present but blank (`WARN_REQUIREMENT_PROSE_EMPTY`) — +the loader requires those attrs to exist, never to say anything. + +**Hierarchy is nesting, and links live at the bottom.** L1 solution, L2 segment, L3 +service — these never reference the model. **L4** binds a declared top-level node — an +`object.*` **or a `template.*`** — and **L5** binds a member of one: a field, view, +validator, identity, or a template's child. `implementedBy` above L4 is an error. +Regrouping *moves* a node; it does not edit a parent string. + +**Write an L5 ref as a dotted `pkg::Owner.member` path** — the level and the shape of the +ref must agree, and this is the single easiest thing to get wrong: + +```yaml +# L4 binds the OBJECT +level: 4 +implementedBy: ["acme::orders::Order"] + +# L5 binds ONE MEMBER of it — dotted owner.member, not the object +level: 5 +implementedBy: ["acme::orders::Order.placedAt"] +``` + +Both mismatches are caught and they are symmetric: an L5 whose ref names an object is +`ERR_REQUIREMENT_L5_NOT_MEMBER`, an L4 whose ref names a member is +`ERR_REQUIREMENT_L4_NOT_OBJECT`. **In both cases the fix is to move the entry to the other +level, not to rewrite the ref** — the ref is usually right and the level is usually the +mistake. The instinct that produces the error is reaching for the object a rule is *about*; +what L5 wants is the member the rule is *carried by*. Copying an L4 block and changing only +`level: 5` produces exactly this failure. + +Reach for L5 when the statement is about ONE column and its entity's own requirement could +not express it — "this lifecycle is `resolvedAt IS NULL` and never `status`", "this id is +nullable, which is why the parent is `partial`". Do NOT use L5 for a blanket rule that +happens to touch many fields; that is an architectural requirement. + +Claim your prompts. A `template.prompt` is a model node realising a capability exactly as +an entity is, and it is the node whose retirement is hardest to see later — a removed +prompt leaves no table behind. A prompt estate with no requirement entries is the same +blind spot this whole mechanism exists to close. + +**L1–L3 are levels of abstraction and ownership in the problem domain** — whose need is +this, and at what altitude — and are NEVER a directory, package, deployable or module. +Technical constructs appear only at L4/L5, which is the allocation step. Test every node: +*if a refactor that changes no behaviour would force it to move, its level is wrong.* + +**Architectural requirements are the other kind.** `requirement.architectural` is flat by +default — a uuid-PK rule, change attribution, tenant scoping. Its check is *universality* +rather than existence, so one that is `live` and claimed by nothing fails: a policy +declared and applied to nothing. A `@level` is OPTIONAL here and opts the node into a tree +(for organising non-functional requirements under a quality taxonomy); once levelled, the +same nesting and link-floor rules apply as to a functional node. + +`@status` is a closed enum enforced by the loader, so a typo fails the load rather than +silently disabling the entry. + +**Record gaps rather than rounding them off.** `partial` says *this works and here is what +is wrong with it*, and it is the most useful status in the enum — a ledger with none is +usually one nobody read carefully. Then say what was DECIDED, which is a separate question: + +- `@disposition: accepted` — understood, deliberately not being closed +- `@disposition: deferred` — will be closed, not now (name a ticket in `@trackedBy`, or + `verify` warns; deferring without one is how a known problem becomes an unknown one) +- **absent** — undecided, and that is a real state. `verify` counts these, because + *"which gaps has nobody ruled on?"* is the question a review exists to answer. + +A `partial` nobody intends to finish is `partial` + `@disposition: accepted` — the gap is +understood and deliberately not being closed. If the capability itself is gone, delete the +requirement instead. + +**`status: planned` locks in work you have not started.** Its references may dangle (write +the requirement before the entity), and it never counts toward object coverage — otherwise +declaring an intention would clear the unclaimed-entity warning and the gate would measure +ambition rather than work. + +```yaml +- requirement.functional: + name: Pacing + level: 3 + status: live + statement: "Scene pacing follows story beats" + counterexample: "A scene that advances on a clock rather than on the story" + children: + - requirement.functional: + name: BeatProgression + level: 4 + status: planned # not built yet -- refs may dangle + statement: "A scene advances when its beat completes" + counterexample: "A scene that advances with its beat unresolved" + trackedBy: ["acme/game#412"] + implementedBy: ["game::turn::BeatProgression"] # does not exist YET + +- requirement.architectural: + name: UuidPrimaryKeys + status: live + statement: "Every entity has a uuid primary key" + counterexample: "An entity keyed by a composite string" + implementedBy: ["game::turn::Turn", "game::world::Location"] +``` + +**Which `verify` says what.** The gate — dangling references, links above the floor, +level and nesting errors, an unclaimed entity, a live requirement nothing implements — runs +in every port's `verify` (`meta verify`, `metaobjects verify`, `mvn metaobjects:verify`, +`dotnet meta verify`). The authoring warnings named on this page (`WARN_REQUIREMENT_NAME_*`, +`WARN_REQUIREMENT_PROSE_*`, `WARN_REQUIREMENT_INERT_DOC_SLOT`, +`WARN_REQUIREMENT_TITLE_IS_AN_ID`) are printed by the Node `meta verify` only, so on another +port follow these rules without waiting for a warning. + +**A requirement's name and package end up in generated test names.** Every port's +`requirement-tests` generator names a test from the requirement's package and dotted path +(`acme::shop::Orders.Recorded` becomes `req_acme_shop_Orders_Recorded…`), and two +requirements whose names differ only in punctuation (`Orders.Recorded` beside +`Orders_Recorded`) are refused as a collision outside TypeScript. Another reason to keep +names plain identifiers. + +There is **no `satisfies:` on a field or entity** — links live on the requirement node, not +on the nodes it claims. Full reference: the repo's `spec/capability-ledger.md`. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 36123e0d8f..a337e663e3 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -35,6 +35,7 @@ exact config, generator names and commands for this project; read it. The other | File | Open it when | |---|---| +| `references/principles.md` | a rule in the always-on file does not settle the case: the full standing principles (adopting onto existing code, the schema and live database, prompts, requirements, owning and writing generators, physical names) with their reasons | | `references/any-stack.md` | **the database, data store, HTTP framework or UI framework is not one a reference generator targets** — MySQL, MongoDB, DynamoDB, Express, NestJS, Next.js, Vue, … | | `references/owning-generators.md` | generated output is wrong; your framework is not the one a reference emits for; you write or review a generator (the `own*()` accessor table) | | `references/choosing-layers.md` | the generated API does not fit, you need joined or derived data, or you are adopting onto existing code | diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md new file mode 100644 index 0000000000..2c2f61d72d --- /dev/null +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -0,0 +1,36 @@ +# Standing principles + +The rules that hold in every MetaObjects project, whatever the stack and whatever the task. The +always-on file in `.metaobjects/` carries the short form; this page is the full text, with the +reasons. Read it when a rule there does not settle the case in front of you. + +## Contents + +- Principles +- Authoring rules you must not violate + +## 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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. +- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. +- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. +- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. +- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. +- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. +- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. +- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. +- 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. +- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. + +## Authoring rules you must not violate + +- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. +- Nodes are fused-key maps: `{".": { ... }}` (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`). diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md index 57b556e1f6..fcdb550249 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md @@ -334,4 +334,5 @@ what's wrong. --- -For the migration tooling read `references/migration.md`. +For the migration tooling read `references/migration.md`. Before upgrading MetaObjects, or +when a project runs more than one port, read `references/upgrading.md`. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md index f589708490..ff97eb6b8e 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md @@ -18,6 +18,7 @@ stays native to the language. - Install (Node, dev-only) - Output format - The workflow +- Renames and destructive changes - Dialects - Live-DB drift: `meta verify --db` - Index modeling (Postgres) @@ -109,8 +110,42 @@ meta migrate baseline --from-db --db postgresql://... --dialect postgres 2. **Review the SQL.** Read the emitted `up.sql` (forward) and `down.sql` (rollback) before applying. Destructive changes (drop column / drop table) are - opt-in — the engine blocks them unless explicitly allowed, and routes ambiguous - rename-vs-drop+add decisions through a prompt rather than guessing. + opt-in — the engine refuses them until you allow each kind by name (`--allow`, + below), and an ambiguous rename-vs-drop+add aborts unless you declare the rename + or choose with `--on-ambiguous`. It never guesses. + +## Renames and destructive changes + +A renamed field or object is a renamed column or table, and the diff cannot tell that +from a drop plus an add unless the names are close. Declare the rename and the data is +kept: + +```bash +meta migrate --dialect postgres --slug rename-origin --rename-column shipment.origin_city=origin_port +meta migrate --dialect postgres --slug rename-stop --rename-table stop=port_call +``` + +- `--rename-column [schema.]table.old=new` and `--rename-table [schema.]old=new` are + repeatable. For a column, `table` is the table's name in the metadata, so it is the + **new** name when the table is renamed in the same run. A declared rename needs no + `--allow drop-column`, and one that finds nothing to rename is refused rather than + falling back to drop and add. Change a column's type, nullability or default in a + later run, not in the rename's. +- `--allow ` permits a destructive change by kind. A refusal names the token it + needs. The tokens: `drop-column`, `drop-table`, `type-change`, `drop-index`, `drop-fk`, + `drop-check`, `drop-view`, `drop-view-cascade`, `drop-column-read-by-view`, + `adopt-view`, `nullable-to-not-null`, `drop-identity-default`, `drop-comment`, + `drop-unmanaged`. Changing an enum's values replaces its CHECK, so it needs + `--allow drop-check`. `drop-column` deletes the column's data: for a rename use + `--rename-column`, never `--allow drop-column`. +- `--on-ambiguous abort|rename|drop-add` (default `abort`) decides a pair the + heuristic offers as a possible rename. Declaring the rename is clearer than `rename`. +- `migrate.allow` and `migrate.onAmbiguous` in `.metaobjects/config.json` set the same + things for every run. Prefer the flag: a standing allowance hides the next destructive + change from review. + +`docs/features/migrations-and-drift.md`, "Renames and populated tables", covers what a +rename carries with it (constraint names, views) and changes that need a backfill. 3. **Apply** the pending migrations against the DB; migration history is tracked in a ledger table: diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/requirements.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/requirements.md new file mode 100644 index 0000000000..e859076180 --- /dev/null +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/requirements.md @@ -0,0 +1,218 @@ +# Requirements — what `meta verify` checks + +This project declares `requirement.*` nodes, so `verify` checks them. **There is no +subverb**: requirements are metadata, so they are checked on *every* `meta verify` run. + +## Contents +- Which command runs the gate +- The split, and why it matters when you read a failure +- A load failure on RETIRED vocabulary — run `meta upgrade`, do not hand-sweep +- The status asymmetry — the one that surprises people +- Exit codes +- The strict switch — `--require-implementers` +- Keeping the ledger current +- The error codes, and the fix for each +- The authoring lint — a second section, never an error +- `verify` does not look at your tests +- What a green run does NOT prove + +## Which command runs the gate + +Every port's `verify` runs the same gate, with the same codes, severities and message +text (a shared corpus pins them). Run the one for the project's server language: + +| port | command | +|---|---| +| TypeScript | `meta verify` | +| Python | `metaobjects verify` | +| Java, Kotlin | `mvn metaobjects:verify` (one goal for both) | +| C# | `dotnet meta verify` | + +The gate is core: it is not a generator and cannot be ejected or replaced. What differs +by port is the **authoring lint** further down this page, which only the Node `meta verify` +prints. This page writes `meta verify` throughout; read it as the port's own command. + +## The split, and why it matters when you read a failure + +| | owns | +|---|---| +| **loader** | `@status` enum, required attrs, child rules, levels — unconditional | +| **`verify`** | `@implementedBy` resolution — **severity depends on `@status`** | + +A typo'd `@status` fails the **load** ("failed to load metadata"), before verify runs. If you +see that, no other diagnostic in the run is trustworthy — fix it first and re-run. + +## A load failure on RETIRED vocabulary — run `meta upgrade`, do not hand-sweep + +`@status` is not the only way the load fails before any of this runs. **0.24.0** retired +`@violation` (→ `@counterexample`) and `@verifiedBy`; **0.24.1** made an index key `@fields` +**XOR** `@expr`, so a node declaring both is now `ERR_INVALID_INDEX`. **0.24.2 reversed half +of the 0.24.0 cut**: `abandoned` and `superseded` become **`@status: retired`**, and +`@supersededBy` is registered again — this time as a reference the loader resolves. There is no deprecation shim for any of them — +the registry is sealed (ADR-0023), so a legacy document does not load at all and every check on +this page is unreachable until it does. + +``` +meta upgrade # previews every rewrite; writes nothing +meta upgrade --apply # makes them +``` + +It rewrites from the same table the loader's error text is generated from, so the fix you are +told about and the edit the tool makes cannot drift apart. It fixes only what has one correct +answer and **refuses the rest, exiting non-zero** — a partial migration can never be recorded +as finished by CI. Two refusals are expected on a real ledger and both are yours to decide: + +- **`@status: abandoned` / `superseded` are no longer refused** — they rewrite to `retired`, + and the same run drops the `@implementedBy` that a retired entry may not carry. This was the + canonical judgement case in 0.24.0; FR-039 made it determinate. +- **`origin.collection`.** Retired to `origin.aggregate @agg: collect`; the attribute sets + differ, so the tool will not guess. + +`@fields` beside `@expr` **is** rewritten, and the survivor is not a coin toss: that pair loaded +before 0.24.1 with `@fields` silently discarded, so the index already in your database is the +expression one. `upgrade` drops `@fields` — which reproduces the object that exists and emits no +DDL change. Do not hand-pick the other survivor; that invents a new index and migrates live data. + +## The status asymmetry — the one that surprises people + +The **same** unresolved `@implementedBy` reference is: + +- an **error** on `live` / `partial` — the model moved and the requirement is stale; +- **allowed** on `planned` — those nodes do not exist YET. That is + the entry doing its job, not drift. + +So on a `planned` entry a dangling reference is the entry doing its job, not drift — do not +"fix" it by deleting the reference, or you delete the plan. + +## Exit codes + +| situation | exit | +|---|---| +| clean tree, or no `requirement.*` nodes at all | 0 | +| dangling `@implementedBy` on `live`/`partial` | 1 | +| the same reference on `planned` | 0 | +| `@implementedBy` above the L4 link floor | 1 | +| live `requirement.architectural` claimed by nothing | 1 | +| an entity no requirement claims | 0 (warning) | +| a `live`/`partial` functional requirement nothing implements | 0 (warning); **1 under the strict switch** | + +## The strict switch — `--require-implementers` + +`WARN_REQUIREMENT_NOTHING_IMPLEMENTS` (a `live` or `partial` functional requirement where +neither it nor anything nested under it names a node) is a warning by default. A project +whose ledger has caught up with its links can make it fail the build: + +| port | flag | environment | +|---|---|---| +| TypeScript | `meta verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Python | `metaobjects verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Java, Kotlin | `mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` | `META_REQUIRE_IMPLEMENTERS=1` | +| C# | `dotnet meta verify … --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | + +It raises that one finding to an error and **keeps its code**. Every other warning stays a +warning. If CI went red on this code, check whether the switch is set before treating it +as a new defect: the fix is to add the `@implementedBy` link (or drop the entry to +`planned`), not to unset the switch. + +Once the ledger is clean, set the switch where the project already runs `verify`: its verify +script, CI job or pre-push hook. Until then a stale `live` claim is only a warning that +nobody reads. + +## Keeping the ledger current + +`verify` proves the ledger's links resolve, not that the ledger is current. A commit that +adds, changes or removes behaviour updates the ledger in the same commit: the new or changed +claim, its `@status` and its `@implementedBy`. If the commit should not touch the ledger, the +commit message says why. + +## The error codes, and the fix for each + +Match on the stable `code`, never the message text (ADR-0009). The runtime messages already +name the remedy; this table exists so you can act on one without re-deriving the rule. + +| code | what it means | fix | +|---|---|---| +| `ERR_REQUIREMENT_DANGLING_REF` | an `@implementedBy` ref does not resolve in the loaded model, on a status that requires live nodes | the model moved and the claim is stale — repoint the ref, or change the status if the capability really went away. **Check the ref is not merely unloaded**: a source missing from `sources` makes a live node look deleted. | +| `ERR_REQUIREMENT_L5_NOT_MEMBER` | an L5 entry's ref names an OBJECT | move the entry to **L4**, or repoint to a member as `pkg::Owner.member`. Moving the level is usually right — the ref is usually what you meant. | +| `ERR_REQUIREMENT_L4_NOT_OBJECT` | an L4 entry's ref names a MEMBER | the symmetric case — move the entry to **L5**, or repoint at the owning object. | +| `ERR_REQUIREMENT_LINK_ABOVE_FLOOR` | `@implementedBy` on an L1–L3 node | L1–L3 are problem-domain altitude and never reference the model. Push the claim down to the L4/L5 child that actually carries it. | +| `ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS` | a `live`/`partial` `requirement.architectural` that nothing implements | a policy declared and applied to nothing. Claim the nodes it governs, or drop it to `planned` — which is exempt, because it is not applied yet by definition. | +| `ERR_REQUIREMENT_LEVEL_NESTING` | a node's `@level` disagrees with the level of the parent it nests under | nesting IS the hierarchy. Move the node to the right parent rather than editing the level to match where it happens to sit. | +| `ERR_REQUIREMENT_BAD_LEVEL` | `@level` is not an integer inside the allowed range | levels are L1–L5 and nothing else. | +| `ERR_REQUIREMENT_RETIRED_HAS_IMPLEMENTORS` | a `@status: retired` entry declares `@implementedBy` | a retired capability has no implementation by definition. Delete the attribute and put what used to implement it in `notes`; if the nodes are still there, the capability is `live` or `partial`, not retired. | +| `ERR_REQUIREMENT_SUPERSEDED_BY_NOT_RETIRED` | `@supersededBy` on a status other than `retired` | it names what REPLACED a withdrawn capability, so on a live one there is nothing for it to say. | +| `ERR_MISSING_REQUIRED_ATTR` | a required attr is absent | `@statement`, `@status` and `@counterexample` are required on both subtypes; `@level` is required on `functional` and optional on `architectural`. | +| `ERR_BAD_ATTR_VALUE` | a closed-enum attr has an unknown value | `@status` and `@disposition` are enforced by the LOADER, so a typo fails the load in every port rather than passing in some. | + +**A note on the L4/L5 pair.** They are enforced at the same site and fail for opposite +reasons, so reading only the code you hit can send you the wrong way. The question is not +"is this ref valid?" — usually it is — but "does the LEVEL match the shape of the ref?" + +## The authoring lint — a second section, never an error + +The Node `meta verify` also prints an **authoring lint** under its own heading, after the +gate's own warnings. It is TypeScript-only: `metaobjects verify`, `mvn metaobjects:verify` +and `dotnet meta verify` run the gate above and never print these codes, so on those ports +a clean run says nothing about them. + +``` +meta verify — requirements: 6 authoring warning(s) (advisory — does not fail the build): +``` + +Read it as a different claim from everything above. The gate says the ledger **disagrees with +the model**; the lint says it agrees but **records less than its author thinks**. Every finding +is a warning and none can change the exit code, so a lint-only run is a passing run. + +| code | what it means | fix | +|---|---|---| +| `WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE` | the `name` holds a character that breaks the dotted path or the generated stub filename | rename it. A `.` is the common one and the worst: `Orders.Recorded` and `Orders` containing `Recorded` produce the **same** path, so the address stops identifying one node. | +| `WARN_REQUIREMENT_NAME_READS_AS_PROSE` | the `name` is a sentence | the name is an address — make it an identifier and put the prose in `@statement`. | +| `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT` | `name` and `@statement` say the same thing | the claim is written twice. Keep `@statement` (every surface reads it) and shorten the name. | +| `WARN_REQUIREMENT_PROSE_EMPTY` | `@statement` or `@counterexample` is present but blank | the loader requires the attr to EXIST, never to say anything. Write the sentence, or delete the entry. | +| `WARN_REQUIREMENT_PROSE_DUPLICATED` | `description` repeats `@statement` (whole, or as its opening sentence), or `@counterexample` does | `@statement` is already the description. `description` holds the SCOPE; drop it entirely if the scope is obvious. | +| `WARN_REQUIREMENT_INERT_DOC_SLOT` | `summary` is set on a requirement | `@statement` is already the required one-line sentence, so a summary can only repeat it, and nothing reads it. Delete it. (`title` is NOT flagged — it is chartered as the entry's label.) | +| `WARN_REQUIREMENT_TITLE_IS_AN_ID` | `title` holds a catalogue or ticket id | a title is a noun phrase and an id is not a name. **Split** it — the id to `@trackedBy`, the phrase stays the title. Do not move the whole string; that throws the label away. | + +Findings from both sections are addressed by the requirement's **dotted path**, never its +bare name — two branches of a ledger may reuse a name, so a bare one can be ambiguous. + +Mute the lint with `--no-requirement-lint` or `META_NO_REQUIREMENT_LINT=1`. That silences the +advisory half **only** — the gate above still runs and can still exit 1. + +Two deliberate silences. The lint reports only **exact** repeats, never a paraphrase — a +similarity threshold on prose produces findings you can argue with. And it never judges +whether a statement is true or a counterexample sufficient; those are the judgements the +ledger exists to record. + +## `verify` does not look at your tests + +It used to. `@verifiedBy` asked you to name a test, and `verify` checked that the **name** +occurred somewhere in your test sources. That is existence evidence, never proof — an audit of +one real 19-name ledger opened every named test and found **4 that did not verify their +claim**: one matched a comment, one a dependency-injection key, one a real test of a +*different* claim, and one a test of the entry's output where the claim was about its source +text. `verify` reported zero errors throughout. The author picks the string, so the cheapest +way to satisfy the check is to find any name that already exists. + +The attribute is retired. Tying a requirement to a test is the job of a generator that emits +the test **from** the requirement, so the link is structural rather than a name someone chose. +Every port ships one, `requirement-tests` (see the codegen skill's reference for the +project's language). `verify` still never reads test results: the codegen drift gate +(`verify --codegen`) proves the generated tests match the ledger, a live test with nothing +behind it fails in the project's own test run, and a generated test that passes proves its +witness ran, not that the witness tests the claim. + +## What a green run does NOT prove + +It proves **referential integrity**: statuses parse, levels are in range, links sit at or +below the floor, references resolve. + +It cannot prove a status is **true**, or that a node genuinely implements the requirement +claiming it. No test can. That judgement is yours. + +Coverage is also narrower than the name suggests: it is checked at **entity grain only** — +`object.value` and `object.projection` are exempt, and fields, views, validators and +identities are never required to be claimed. Green means "every entity is claimed by +something", not "every node is described". + +Full reference: the repo's `spec/capability-ledger.md`. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/upgrading.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/upgrading.md new file mode 100644 index 0000000000..23ccb45965 --- /dev/null +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.claude/skills/metaobjects-verify/references/upgrading.md @@ -0,0 +1,10 @@ +# Upgrading MetaObjects and keeping the ports in sync + +Read this before you move a project to a new MetaObjects release, and when a project runs more +than one port (a TypeScript client beside a Java backend, say). + +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. + +`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. + +**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md index 48c0a14f43..b700a9d00e 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/AGENTS.md @@ -3,82 +3,105 @@ > Stack: java, kotlin 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/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `mvn metaobjects:generate`. - -## Read the generated reference before you touch a tier -`meta gen` writes four reference files under this project's docs target -(`targets.docs.outDir` in `metaobjects.config.ts`) when `generators` lists the generator -for each — `agentDocsFile()` for the three `agent/` pages, `apiDocsFile()` for the api -reference. There is no separate docs command: documentation is one more entry in the same -list every other generator is in. They are generated from THIS project's metadata, so they -describe what it actually is rather than what the framework does in general — and reading -the relevant one first is how you avoid inferring the answer from generated code, which is -the disposable half. - -- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each - column came from, keys, foreign keys, constraints. -- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter - operators the generators actually emitted. -- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and - filterability per field, and each declared grid. -- **`agent/requirements.md`** — before adding a capability. What is already claimed, and - what was deliberately **retired** — an entry saying a capability must not be rebuilt is - the one thing you cannot learn from the code, because the code is gone. - -Not there? List the missing generator — and a `docs` target, if `targets` has none — in -`generators`, then run `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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. -- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. -- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. -- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. -- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. -- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. -- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. -- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. -- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. -- 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. -- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. - -## Authoring rules you must not violate -- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. -- Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### Java: generate with `mvn metaobjects:generate` + +1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator + list, one `` each, under `` pointing at the metadata. + Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`, + `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and + `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a + list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator` + writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The + full table is in the `metaobjects-codegen` skill's `references/java.md`. +2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java` + copies a generator to change what it emits. + +### Kotlin: generate with `mvn metaobjects:generate` + +1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator + list, one `` each. Nothing is generated until it is listed. The Exposed + and Spring tier is `KotlinEntityGenerator`, `KotlinExposedTableGenerator`, + `KotlinRelationsGenerator`, `KotlinFilterAllowlistGenerator` and + `KotlinSpringControllerGenerator` (package `com.metaobjects.generator.kotlin`). + `KotlinRequirementTestsGenerator` writes one test per requirement; `KotlinApiDocsGenerator` + writes `api/kotlin/AGENT-API.md`. The full table is in the `metaobjects-codegen` skill's + `references/kotlin.md`. +2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=kotlin` + copies a generator to change what it emits. Several Kotlin generators truncate their output + on every run, so never put your own code in a generated file. + +### Web client: generated by the Node `npx meta gen` + +The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` +from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from +`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and +`@metaobjectsdev/tanstack`. Wrap the app in ``: +`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every +hook without an error. The `metaobjects-runtime-ui` skill has the detail. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `mvn metaobjects:verify` (the generated code and the requirements). +- `npx meta verify --codegen` (the generated web client). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md index 48c0a14f43..b700a9d00e 100644 --- a/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/java-kotlin-react-tanstack/expected/.metaobjects/CLAUDE.md @@ -3,82 +3,105 @@ > Stack: java, kotlin 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/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `mvn metaobjects:generate`. - -## Read the generated reference before you touch a tier -`meta gen` writes four reference files under this project's docs target -(`targets.docs.outDir` in `metaobjects.config.ts`) when `generators` lists the generator -for each — `agentDocsFile()` for the three `agent/` pages, `apiDocsFile()` for the api -reference. There is no separate docs command: documentation is one more entry in the same -list every other generator is in. They are generated from THIS project's metadata, so they -describe what it actually is rather than what the framework does in general — and reading -the relevant one first is how you avoid inferring the answer from generated code, which is -the disposable half. - -- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each - column came from, keys, foreign keys, constraints. -- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter - operators the generators actually emitted. -- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and - filterability per field, and each declared grid. -- **`agent/requirements.md`** — before adding a capability. What is already claimed, and - what was deliberately **retired** — an entry saying a capability must not be rebuilt is - the one thing you cannot learn from the code, because the code is gone. - -Not there? List the missing generator — and a `docs` target, if `targets` has none — in -`generators`, then run `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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. -- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. -- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. -- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. -- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. -- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. -- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. -- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. -- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. -- 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. -- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. - -## Authoring rules you must not violate -- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. -- Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### Java: generate with `mvn metaobjects:generate` + +1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator + list, one `` each, under `` pointing at the metadata. + Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`, + `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and + `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a + list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator` + writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The + full table is in the `metaobjects-codegen` skill's `references/java.md`. +2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java` + copies a generator to change what it emits. + +### Kotlin: generate with `mvn metaobjects:generate` + +1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator + list, one `` each. Nothing is generated until it is listed. The Exposed + and Spring tier is `KotlinEntityGenerator`, `KotlinExposedTableGenerator`, + `KotlinRelationsGenerator`, `KotlinFilterAllowlistGenerator` and + `KotlinSpringControllerGenerator` (package `com.metaobjects.generator.kotlin`). + `KotlinRequirementTestsGenerator` writes one test per requirement; `KotlinApiDocsGenerator` + writes `api/kotlin/AGENT-API.md`. The full table is in the `metaobjects-codegen` skill's + `references/kotlin.md`. +2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=kotlin` + copies a generator to change what it emits. Several Kotlin generators truncate their output + on every run, so never put your own code in a generated file. + +### Web client: generated by the Node `npx meta gen` + +The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` +from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from +`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and +`@metaobjectsdev/tanstack`. Wrap the app in ``: +`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every +hook without an error. The `metaobjects-runtime-ui` skill has the detail. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `mvn metaobjects:verify` (the generated code and the requirements). +- `npx meta verify --codegen` (the generated web client). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/references/requirements.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/references/requirements.md new file mode 100644 index 0000000000..2bcebe9ec1 --- /dev/null +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-audit/references/requirements.md @@ -0,0 +1,154 @@ +# Requirements — the audit dimension + +This project declares `requirement.*` nodes, so the ledger is an auditable surface. Audit it +for **truthfulness**, not volume. A large ledger that lies is worse than a small one that +does not, because every later reader trusts it. + +Run `verify` first — the project's own: `meta verify`, `metaobjects verify`, +`mvn metaobjects:verify` or `dotnet meta verify`. Every port runs the same requirement +gate. It settles referential integrity mechanically — do not spend audit effort re-deriving +what a green run already proves. + +## Contents +- What verify has already proven (do not re-check by hand) +- Currency checks — run these before sampling +- What only a human or an agent reading the code can catch +- Scope — do NOT flag these as defects +- Reporting + +## What verify has already proven (do not re-check by hand) + +A run answers two different questions in two separate sections. Both are already-proven +ground; neither is worth an auditor's time. + +**The gate** settles referential integrity: links sit at or below the L4 floor, nesting +agrees with levels, `@status` values are legal, and references resolve (with dangling allowed +on `planned`, whose nodes do not exist yet). + +**The authoring lint** — printed under its own heading, advisory, and unable to change the +exit code — settles the naming and prose defects you would otherwise find by reading every +entry. **Only the Node `meta verify` prints it.** On a Python, Java, Kotlin or C# project +whose pipeline runs that port's `verify` alone, nothing below has been proven: run +the Node `meta verify` over the same metadata, or audit these by hand. + +| code | what a clean run has already proven | +|---|---| +| `WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE` | no `name` is blank, space-padded, or carries a `.` / `/` / `:` that breaks the dotted path or the generated stub's filename | +| `WARN_REQUIREMENT_NAME_READS_AS_PROSE` | no `name` is a sentence sitting in the address slot | +| `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT` | no `name` and `@statement` are the same sentence written twice | +| `WARN_REQUIREMENT_PROSE_EMPTY` | no `@statement` / `@counterexample` is declared-but-blank (the loader enforces presence, never content) | +| `WARN_REQUIREMENT_PROSE_DUPLICATED` | no `description` repeats `@statement` whole or as its opening sentence, and no `@counterexample` repeats it | +| `WARN_REQUIREMENT_INERT_DOC_SLOT` | no `summary` is set — nothing reads it, and `@statement` is already the required one-liner | +| `WARN_REQUIREMENT_TITLE_IS_AN_ID` | no `title` leads with a catalogue or ticket id | + +Three limits, and each puts something back on your list: + +- **It is one port's.** See above: establish which `verify` the project's CI runs before + crediting a green pipeline with the table. +- **The lint is mutable.** `--no-requirement-lint` / `META_NO_REQUIREMENT_LINT=1` silences the + advisory half while the gate still runs. **Establish whether the project mutes it** (§F of + the checklist asks this) — against a muted lint the whole table above proves nothing. +- **It reports only EXACT repeats.** A paraphrase — `description` restating `@statement` in + different words, a `@counterexample` that merely negates it — is invisible to the tool and + is a legitimate hand finding. + +`title` is deliberately NOT flagged as an inert slot; it is chartered as the entry's label and +is rendered (see item 6). Do not report a populated `title` as a defect. + +## Currency checks — run these before sampling + +A green gate proves the ledger's links resolve, not that the ledger keeps up with the code. +These three checks measure that, and the first one chooses the sample for item 1 below. + +**(a) Ledger stale.** From git history, take the commits that add a feature (a `feat:` / +`feat(...)` subject under Conventional Commits; otherwise merged feature branches or PRs +labelled as features) over the last six months or the last 50 such commits, whichever is +fewer. Report two numbers: the share of those commits that also change requirement metadata +(any file holding `requirement.*` nodes), and the count of feature commits since the most +recent change to that metadata. A share **under half**, or **more than ten** feature commits +since the last ledger change, is the finding `ledger-stale`, stating both numbers. The +feature commits that did not touch the ledger are the sample for item 1: read what each one +built and check that a requirement says so with a true status. + +**(b) Nothing implements a live claim.** Run the project's verify with the strict switch: +`meta verify --require-implementers`, `metaobjects verify --require-implementers`, +`mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` or +`dotnet meta verify --require-implementers` (all ports also read +`META_REQUIRE_IMPLEMENTERS=1`). Each `WARN_REQUIREMENT_NOTHING_IMPLEMENTS` on a `live` entry +is a finding (`live-claim-unimplemented`) naming the entry. State whether the project's own +gate passes the switch; if it does not and the ledger has no such entries, recommend adding +it so the ledger cannot fall behind again. + +**(c) Live claim, unwired implementer.** For each `@implementedBy` target of a `live` entry, +find the generated code that target produces and confirm hand-written code imports or calls +it. A live claim whose implementer is generated code nothing uses is the finding +`live-claim-unwired-implementer`: the claim resolves, so verify is satisfied, but the +capability is not reachable. Before raising it, rule out a runtime use: code that reaches the +entity through the runtime `ObjectManager` (or a metadata-driven admin or tool registry) +uses it without importing anything generated, so search for the entity's name in those calls +too. + +## What only a human or an agent reading the code can catch + +**1. Statuses that are false.** The highest-value finding in the whole dimension. A +requirement marked `live` whose implementation was gutted; one marked `partial` that is now +complete; one marked `planned` that was quietly built months ago. Sample the claims +and read the nodes, starting with the feature commits currency check (a) found outside the +ledger. `status` is the only payload with controlled evidence behind it — the +resurrection protection rides entirely on it being true. + +**2. Non-violable statements.** Every requirement must state what breaking it looks like. +*"Every entity has a uuid primary key"* is violable — point at one with a composite key. +*"Things are persisted"* is not; it is a description wearing a requirement's shape. Flag +these: they inflate the ledger while proving nothing. + +**3. Claim-padding.** An entity appended to an existing unrelated requirement's +`implementedBy` purely to silence the coverage warning. The claim resolves, so verify is +happy, but it carries no information about what the entity is *for*. Look for entities whose +only claim is a high-fan-out architectural rule when they plainly have product meaning. + +**4. Misfiled kind.** `functional` is checked by EXISTENCE, `architectural` by UNIVERSALITY — +opposite polarity, and nothing mechanical catches a misfile. The discriminator: did this +exist because someone asked for something (functional), or because every entity here looks +like this (architectural)? An architectural entry with one claimant is usually a misfiled +functional one. + +**5. Levels used as decoration.** L1–L3 are organisational and must never reference the +model. If the tree is flat, or every entry is L3, the levels are carrying no information and +should be simplified rather than defended. + +**6. Titles that are not labels.** `title` is now **rendered** — `meta gen`'s +`requirementsFile()` heads each entry `## checkout.payment — Payment capture`, the path +first and the label after it — so a slot +that used to be inert is read by every reader of the generated page. The lint catches only +the id-shaped case (`FR-448 …`); it cannot tell a *useless* label from a good one. Read the +titles: one that restates `@statement`, or repeats the path in prose, is now visible noise in +a heading rather than a private authoring habit. An absent `title` is NOT a finding — the +entry heads by its path alone, which is what every sibling surface addresses it by. + +**7. Witnesses that do not test the claim.** Where the project generates requirement tests +(the `requirement-tests` generator, in any port), a passing generated test proves only that +its witness — or, in TypeScript, the hand-filled stub — ran. Sample the witnesses and read +them against the counterexample each generated test carries: one that asserts nothing, or asserts +something else, is the same defect `@verifiedBy` had. Check also for witnesses that have +gone stale silently: a Java witness that lacks the override annotation, a C# witness implemented +implicitly, and any Python witness keep existing after their requirement is retired or +deleted, and nothing reports them. + +## Scope — do NOT flag these as defects + +- **Unclaimed `object.value` / `object.projection`.** Exempt by design: a value is a shape, a + projection derives from a claimable entity. +- **Fields, views, validators and identities without their own requirement.** Member-grain + coverage is explicitly rejected — plumbing members are covered by architectural + requirements with high fan-out. L5 exists so a member claim *can* be made where it means + something, never so every member must carry one. +- **Dangling links on `planned`.** Correct — the plan precedes the nodes. +- **A project with no requirements at all.** The feature is opt-in by declaration; absence is + not a finding. + +## Reporting + +Report findings as claims to verify, not as a score. "These 4 requirements are marked `live` +and I could not find their implementation" is actionable; "ledger maturity: 62%" is not, and +invites optimising the number. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md index 1979d161f2..de11bce2cc 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -38,7 +38,7 @@ This file covers what almost every model needs. The topics below live in | `references/inheritance-tph.md` | several entities are variants of one thing sharing a single table (`@discriminator`) | | `references/metadata-dependencies.md` | the project builds on another package's metadata (`dependencies`, cross-package `overlay`) | | `references/reporting.md` | what a declared report gives you: what columns a report gets, time grains, null rules, rows with no facts (`@spine`), defaults (`@default`), engine differences | -| `references/requirements.md` | installed only when the project declares `requirement.*` nodes | +| `references/requirements.md` | you add, change or review a `requirement.*` node | ## The operating principle: model-first, generate-first @@ -483,7 +483,7 @@ Every requirement must be **violable**: if you cannot say what breaking it looks **A commit that adds, changes or removes behaviour updates the ledger in the same commit** (the new or changed claim, its `@status` and `@implementedBy`), or says in the commit message why it does not. `verify` checks that links resolve, not that the ledger is current. -If the project declares any `requirement.*` node, `references/requirements.md` is installed with the full authoring rules. If it does not and you are adding the first one, the shape is: +`references/requirements.md` has the full authoring rules. The shape of one node: ```yaml - requirement.architectural: diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/references/requirements.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/references/requirements.md new file mode 100644 index 0000000000..abd10504a0 --- /dev/null +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-authoring/references/requirements.md @@ -0,0 +1,225 @@ +# Requirements — `requirement.functional` / `requirement.architectural` + +Capabilities are **metadata**, declared in the metadata directory beside the entities they +describe. Read the existing requirement nodes before designing anything. Two rules matter +more than the rest. + +## Contents +- A requirement is PRESCRIPTIVE — it states what should be true, never what happened +- Do not delete a retired capability's entry +- When you add an entity, claim it +- A feature commit updates the ledger in the same commit +- Every requirement states its counterexample +- Four prose slots, and `statement` is the one that means "description" +- `title` is a LABEL and is chartered here; `summary` is not +- Never put a catalogue or ticket id in `title` +- The `name` is an address, so write it as an identifier +- Hierarchy is nesting, and links live at the bottom +- Write an L5 ref as a dotted `pkg::Owner.member` path +- L1–L3 are levels of abstraction and ownership in the problem domain +- Architectural requirements are the other kind +- Record gaps rather than rounding them off +- `status: planned` locks in work you have not started +- Which `verify` says what +- A requirement's name and package end up in generated test names + +**1. A requirement is PRESCRIPTIVE — it states what should be true, never what happened.** +`status` is `planned | live | partial | retired`, and every one of them states something +meant to be true. `retired` is not the exception: it means **"this was built, then +deliberately removed, and must not be rebuilt"** — a prohibition in force, falsifiable by +exactly one observable, the capability reappearing. + +So write a retired entry as a rule, not a diary. `statement: "An unpaid order is never +expired by a wall-clock timer"` is a prohibition; `"We used to expire orders on a timer"` is +history wearing a requirement's shape, and no gate will catch it. On a retired entry +`counterexample` describes **the revival** — the thing a future reader is about to propose. + +**Do not delete a retired capability's entry.** Keeping it is the single most load-bearing +thing this vocabulary does: given a brief for a capability that had been retired, agents +working from the model alone proposed rebuilding it 24 times out of 24. The entry is what +stops that, so a retirement is a status change plus a rewritten statement, never a deletion. + +Three rules the loader enforces on `retired`: + +- **`implementedBy` is REFUSED** — a retired capability has no implementation by definition. + What used to implement it goes in `notes`. If deleting the references feels wrong, the + capability is probably not retired: an entry whose nodes are still there is `live` or + `partial`. +- **`supersededBy` names the requirement that replaced it**, legal here only, and RESOLVED — + so the chain still works when the replacement is itself retired later. +- It never counts toward object coverage, and is exempt from the architectural universality + check. + +Leaving a dangling `implementedBy` is **correct only on `planned`**: the entry precedes the +nodes. On `live` or `partial` the same dangling reference is an error — the model moved and +the requirement went stale. + +**2. When you add an entity, claim it.** Every `object.entity` should appear in some +requirement's `implementedBy`, or `verify` says so. + +**A commit that adds, changes or removes behaviour updates the ledger in the same commit.** +`verify` checks that the ledger's links resolve, never that it is current, so nothing fails +when a feature ships and its requirement does not move. In the same commit, write the new or +changed claim, set its `@status` (`planned` to `live` when it lands, `retired` when it is +removed) and keep its `@implementedBy` pointing at the real nodes (a retired entry carries +none). If the commit should not +touch the ledger, say why in the commit message. + +**Every requirement states its counterexample.** One sentence: what breaking it looks like. +*"Every entity has a uuid primary key"* is violable — point at one with a composite string +key. *"Things are persisted"* is not, and is a description rather than a requirement. Same +rule kills *"the system is reliable"*. If you cannot say what breaking it looks like, +delete it. + +**Four prose slots, and `statement` is the one that means "description".** A requirement can +also carry the common `description` and `notes`, and they overlap badly unless you decide the +split before writing any of them: + +- `statement` — **the claim**. This IS the description of what the requirement is +- `counterexample` — **what would falsify the claim**, which is what makes it checkable +- `description` — **the scope**: what the claim covers, what it deliberately does not, and + which sibling entry owns the rest +- `notes` — **the evidence**: how you know the `status` is true — citations, vocabularies, the + control you ran to prove an absence was real + +**`title` is a LABEL and is chartered here; `summary` is not.** A requirement's `name` is an +identifier and its address renders as a dotted camelCase path, so a short noun-phrase `title` is +what an index shows — the requirement attribute table in `spec/capability-ledger.md` says so by +name, and the generated requirements page renders it in the heading after the path +(`## checkout.payment — Payment capture`). `summary` is different: `statement` is already the +required one-line sentence, so a `summary` can only repeat it and nothing reads it (`verify` warns, +`WARN_REQUIREMENT_INERT_DOC_SLOT`). `notes` is unrendered on purpose — chartered internal-only. + +**Never put a catalogue or ticket id in `title`.** A title is a noun phrase; an id is not a name. +`title: "FR-467 — Order recording"` is two things in one slot — put the id in `trackedBy` and +keep the phrase as the title. `verify` warns (`WARN_REQUIREMENT_TITLE_IS_AN_ID`). + +**The `name` is an address, so write it as an identifier.** It is the segment of the dotted +path (`Ordering.Placement.Recorded`) and the filename of the generated test stub. A `.` in a +name is indistinguishable from nesting — `Orders.Recorded` and `Orders` containing `Recorded` +produce the same path — and a sentence for a name puts the claim somewhere nothing reads it. +Both load; `verify` warns (`WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE`, +`WARN_REQUIREMENT_NAME_READS_AS_PROSE`, `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT`). + +Two failure modes, both of which look like diligence. A `description` that **paraphrases the +statement** is padding, and it makes every later reader trust the ledger less — leave it off +instead, it is optional. A `description` that **narrates the evidence** belongs in `notes`; +the tell is a fact you had to read the implementation to learn. Mechanical test for the last +line: *would this sentence have to change if the code changed but the model did not?* Then it +is `notes`. `verify` warns on the exact repeats (`WARN_REQUIREMENT_PROSE_DUPLICATED`), and on +a `statement` or `counterexample` that is present but blank (`WARN_REQUIREMENT_PROSE_EMPTY`) — +the loader requires those attrs to exist, never to say anything. + +**Hierarchy is nesting, and links live at the bottom.** L1 solution, L2 segment, L3 +service — these never reference the model. **L4** binds a declared top-level node — an +`object.*` **or a `template.*`** — and **L5** binds a member of one: a field, view, +validator, identity, or a template's child. `implementedBy` above L4 is an error. +Regrouping *moves* a node; it does not edit a parent string. + +**Write an L5 ref as a dotted `pkg::Owner.member` path** — the level and the shape of the +ref must agree, and this is the single easiest thing to get wrong: + +```yaml +# L4 binds the OBJECT +level: 4 +implementedBy: ["acme::orders::Order"] + +# L5 binds ONE MEMBER of it — dotted owner.member, not the object +level: 5 +implementedBy: ["acme::orders::Order.placedAt"] +``` + +Both mismatches are caught and they are symmetric: an L5 whose ref names an object is +`ERR_REQUIREMENT_L5_NOT_MEMBER`, an L4 whose ref names a member is +`ERR_REQUIREMENT_L4_NOT_OBJECT`. **In both cases the fix is to move the entry to the other +level, not to rewrite the ref** — the ref is usually right and the level is usually the +mistake. The instinct that produces the error is reaching for the object a rule is *about*; +what L5 wants is the member the rule is *carried by*. Copying an L4 block and changing only +`level: 5` produces exactly this failure. + +Reach for L5 when the statement is about ONE column and its entity's own requirement could +not express it — "this lifecycle is `resolvedAt IS NULL` and never `status`", "this id is +nullable, which is why the parent is `partial`". Do NOT use L5 for a blanket rule that +happens to touch many fields; that is an architectural requirement. + +Claim your prompts. A `template.prompt` is a model node realising a capability exactly as +an entity is, and it is the node whose retirement is hardest to see later — a removed +prompt leaves no table behind. A prompt estate with no requirement entries is the same +blind spot this whole mechanism exists to close. + +**L1–L3 are levels of abstraction and ownership in the problem domain** — whose need is +this, and at what altitude — and are NEVER a directory, package, deployable or module. +Technical constructs appear only at L4/L5, which is the allocation step. Test every node: +*if a refactor that changes no behaviour would force it to move, its level is wrong.* + +**Architectural requirements are the other kind.** `requirement.architectural` is flat by +default — a uuid-PK rule, change attribution, tenant scoping. Its check is *universality* +rather than existence, so one that is `live` and claimed by nothing fails: a policy +declared and applied to nothing. A `@level` is OPTIONAL here and opts the node into a tree +(for organising non-functional requirements under a quality taxonomy); once levelled, the +same nesting and link-floor rules apply as to a functional node. + +`@status` is a closed enum enforced by the loader, so a typo fails the load rather than +silently disabling the entry. + +**Record gaps rather than rounding them off.** `partial` says *this works and here is what +is wrong with it*, and it is the most useful status in the enum — a ledger with none is +usually one nobody read carefully. Then say what was DECIDED, which is a separate question: + +- `@disposition: accepted` — understood, deliberately not being closed +- `@disposition: deferred` — will be closed, not now (name a ticket in `@trackedBy`, or + `verify` warns; deferring without one is how a known problem becomes an unknown one) +- **absent** — undecided, and that is a real state. `verify` counts these, because + *"which gaps has nobody ruled on?"* is the question a review exists to answer. + +A `partial` nobody intends to finish is `partial` + `@disposition: accepted` — the gap is +understood and deliberately not being closed. If the capability itself is gone, delete the +requirement instead. + +**`status: planned` locks in work you have not started.** Its references may dangle (write +the requirement before the entity), and it never counts toward object coverage — otherwise +declaring an intention would clear the unclaimed-entity warning and the gate would measure +ambition rather than work. + +```yaml +- requirement.functional: + name: Pacing + level: 3 + status: live + statement: "Scene pacing follows story beats" + counterexample: "A scene that advances on a clock rather than on the story" + children: + - requirement.functional: + name: BeatProgression + level: 4 + status: planned # not built yet -- refs may dangle + statement: "A scene advances when its beat completes" + counterexample: "A scene that advances with its beat unresolved" + trackedBy: ["acme/game#412"] + implementedBy: ["game::turn::BeatProgression"] # does not exist YET + +- requirement.architectural: + name: UuidPrimaryKeys + status: live + statement: "Every entity has a uuid primary key" + counterexample: "An entity keyed by a composite string" + implementedBy: ["game::turn::Turn", "game::world::Location"] +``` + +**Which `verify` says what.** The gate — dangling references, links above the floor, +level and nesting errors, an unclaimed entity, a live requirement nothing implements — runs +in every port's `verify` (`meta verify`, `metaobjects verify`, `mvn metaobjects:verify`, +`dotnet meta verify`). The authoring warnings named on this page (`WARN_REQUIREMENT_NAME_*`, +`WARN_REQUIREMENT_PROSE_*`, `WARN_REQUIREMENT_INERT_DOC_SLOT`, +`WARN_REQUIREMENT_TITLE_IS_AN_ID`) are printed by the Node `meta verify` only, so on another +port follow these rules without waiting for a warning. + +**A requirement's name and package end up in generated test names.** Every port's +`requirement-tests` generator names a test from the requirement's package and dotted path +(`acme::shop::Orders.Recorded` becomes `req_acme_shop_Orders_Recorded…`), and two +requirements whose names differ only in punctuation (`Orders.Recorded` beside +`Orders_Recorded`) are refused as a collision outside TypeScript. Another reason to keep +names plain identifiers. + +There is **no `satisfies:` on a field or entity** — links live on the requirement node, not +on the nodes it claims. Full reference: the repo's `spec/capability-ledger.md`. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md index 36123e0d8f..a337e663e3 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -35,6 +35,7 @@ exact config, generator names and commands for this project; read it. The other | File | Open it when | |---|---| +| `references/principles.md` | a rule in the always-on file does not settle the case: the full standing principles (adopting onto existing code, the schema and live database, prompts, requirements, owning and writing generators, physical names) with their reasons | | `references/any-stack.md` | **the database, data store, HTTP framework or UI framework is not one a reference generator targets** — MySQL, MongoDB, DynamoDB, Express, NestJS, Next.js, Vue, … | | `references/owning-generators.md` | generated output is wrong; your framework is not the one a reference emits for; you write or review a generator (the `own*()` accessor table) | | `references/choosing-layers.md` | the generated API does not fit, you need joined or derived data, or you are adopting onto existing code | diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md new file mode 100644 index 0000000000..2c2f61d72d --- /dev/null +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -0,0 +1,36 @@ +# Standing principles + +The rules that hold in every MetaObjects project, whatever the stack and whatever the task. The +always-on file in `.metaobjects/` carries the short form; this page is the full text, with the +reasons. Read it when a rule there does not settle the case in front of you. + +## Contents + +- Principles +- Authoring rules you must not violate + +## 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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. +- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. +- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. +- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. +- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. +- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. +- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. +- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. +- 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. +- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. + +## Authoring rules you must not violate + +- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. +- Nodes are fused-key maps: `{".": { ... }}` (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`). diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/SKILL.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/SKILL.md index 57b556e1f6..fcdb550249 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/SKILL.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/SKILL.md @@ -334,4 +334,5 @@ what's wrong. --- -For the migration tooling read `references/migration.md`. +For the migration tooling read `references/migration.md`. Before upgrading MetaObjects, or +when a project runs more than one port, read `references/upgrading.md`. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/migration.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/migration.md index f589708490..ff97eb6b8e 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/migration.md +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/migration.md @@ -18,6 +18,7 @@ stays native to the language. - Install (Node, dev-only) - Output format - The workflow +- Renames and destructive changes - Dialects - Live-DB drift: `meta verify --db` - Index modeling (Postgres) @@ -109,8 +110,42 @@ meta migrate baseline --from-db --db postgresql://... --dialect postgres 2. **Review the SQL.** Read the emitted `up.sql` (forward) and `down.sql` (rollback) before applying. Destructive changes (drop column / drop table) are - opt-in — the engine blocks them unless explicitly allowed, and routes ambiguous - rename-vs-drop+add decisions through a prompt rather than guessing. + opt-in — the engine refuses them until you allow each kind by name (`--allow`, + below), and an ambiguous rename-vs-drop+add aborts unless you declare the rename + or choose with `--on-ambiguous`. It never guesses. + +## Renames and destructive changes + +A renamed field or object is a renamed column or table, and the diff cannot tell that +from a drop plus an add unless the names are close. Declare the rename and the data is +kept: + +```bash +meta migrate --dialect postgres --slug rename-origin --rename-column shipment.origin_city=origin_port +meta migrate --dialect postgres --slug rename-stop --rename-table stop=port_call +``` + +- `--rename-column [schema.]table.old=new` and `--rename-table [schema.]old=new` are + repeatable. For a column, `table` is the table's name in the metadata, so it is the + **new** name when the table is renamed in the same run. A declared rename needs no + `--allow drop-column`, and one that finds nothing to rename is refused rather than + falling back to drop and add. Change a column's type, nullability or default in a + later run, not in the rename's. +- `--allow ` permits a destructive change by kind. A refusal names the token it + needs. The tokens: `drop-column`, `drop-table`, `type-change`, `drop-index`, `drop-fk`, + `drop-check`, `drop-view`, `drop-view-cascade`, `drop-column-read-by-view`, + `adopt-view`, `nullable-to-not-null`, `drop-identity-default`, `drop-comment`, + `drop-unmanaged`. Changing an enum's values replaces its CHECK, so it needs + `--allow drop-check`. `drop-column` deletes the column's data: for a rename use + `--rename-column`, never `--allow drop-column`. +- `--on-ambiguous abort|rename|drop-add` (default `abort`) decides a pair the + heuristic offers as a possible rename. Declaring the rename is clearer than `rename`. +- `migrate.allow` and `migrate.onAmbiguous` in `.metaobjects/config.json` set the same + things for every run. Prefer the flag: a standing allowance hides the next destructive + change from review. + +`docs/features/migrations-and-drift.md`, "Renames and populated tables", covers what a +rename carries with it (constraint names, views) and changes that need a backfill. 3. **Apply** the pending migrations against the DB; migration history is tracked in a ledger table: diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/requirements.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/requirements.md new file mode 100644 index 0000000000..e859076180 --- /dev/null +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/requirements.md @@ -0,0 +1,218 @@ +# Requirements — what `meta verify` checks + +This project declares `requirement.*` nodes, so `verify` checks them. **There is no +subverb**: requirements are metadata, so they are checked on *every* `meta verify` run. + +## Contents +- Which command runs the gate +- The split, and why it matters when you read a failure +- A load failure on RETIRED vocabulary — run `meta upgrade`, do not hand-sweep +- The status asymmetry — the one that surprises people +- Exit codes +- The strict switch — `--require-implementers` +- Keeping the ledger current +- The error codes, and the fix for each +- The authoring lint — a second section, never an error +- `verify` does not look at your tests +- What a green run does NOT prove + +## Which command runs the gate + +Every port's `verify` runs the same gate, with the same codes, severities and message +text (a shared corpus pins them). Run the one for the project's server language: + +| port | command | +|---|---| +| TypeScript | `meta verify` | +| Python | `metaobjects verify` | +| Java, Kotlin | `mvn metaobjects:verify` (one goal for both) | +| C# | `dotnet meta verify` | + +The gate is core: it is not a generator and cannot be ejected or replaced. What differs +by port is the **authoring lint** further down this page, which only the Node `meta verify` +prints. This page writes `meta verify` throughout; read it as the port's own command. + +## The split, and why it matters when you read a failure + +| | owns | +|---|---| +| **loader** | `@status` enum, required attrs, child rules, levels — unconditional | +| **`verify`** | `@implementedBy` resolution — **severity depends on `@status`** | + +A typo'd `@status` fails the **load** ("failed to load metadata"), before verify runs. If you +see that, no other diagnostic in the run is trustworthy — fix it first and re-run. + +## A load failure on RETIRED vocabulary — run `meta upgrade`, do not hand-sweep + +`@status` is not the only way the load fails before any of this runs. **0.24.0** retired +`@violation` (→ `@counterexample`) and `@verifiedBy`; **0.24.1** made an index key `@fields` +**XOR** `@expr`, so a node declaring both is now `ERR_INVALID_INDEX`. **0.24.2 reversed half +of the 0.24.0 cut**: `abandoned` and `superseded` become **`@status: retired`**, and +`@supersededBy` is registered again — this time as a reference the loader resolves. There is no deprecation shim for any of them — +the registry is sealed (ADR-0023), so a legacy document does not load at all and every check on +this page is unreachable until it does. + +``` +meta upgrade # previews every rewrite; writes nothing +meta upgrade --apply # makes them +``` + +It rewrites from the same table the loader's error text is generated from, so the fix you are +told about and the edit the tool makes cannot drift apart. It fixes only what has one correct +answer and **refuses the rest, exiting non-zero** — a partial migration can never be recorded +as finished by CI. Two refusals are expected on a real ledger and both are yours to decide: + +- **`@status: abandoned` / `superseded` are no longer refused** — they rewrite to `retired`, + and the same run drops the `@implementedBy` that a retired entry may not carry. This was the + canonical judgement case in 0.24.0; FR-039 made it determinate. +- **`origin.collection`.** Retired to `origin.aggregate @agg: collect`; the attribute sets + differ, so the tool will not guess. + +`@fields` beside `@expr` **is** rewritten, and the survivor is not a coin toss: that pair loaded +before 0.24.1 with `@fields` silently discarded, so the index already in your database is the +expression one. `upgrade` drops `@fields` — which reproduces the object that exists and emits no +DDL change. Do not hand-pick the other survivor; that invents a new index and migrates live data. + +## The status asymmetry — the one that surprises people + +The **same** unresolved `@implementedBy` reference is: + +- an **error** on `live` / `partial` — the model moved and the requirement is stale; +- **allowed** on `planned` — those nodes do not exist YET. That is + the entry doing its job, not drift. + +So on a `planned` entry a dangling reference is the entry doing its job, not drift — do not +"fix" it by deleting the reference, or you delete the plan. + +## Exit codes + +| situation | exit | +|---|---| +| clean tree, or no `requirement.*` nodes at all | 0 | +| dangling `@implementedBy` on `live`/`partial` | 1 | +| the same reference on `planned` | 0 | +| `@implementedBy` above the L4 link floor | 1 | +| live `requirement.architectural` claimed by nothing | 1 | +| an entity no requirement claims | 0 (warning) | +| a `live`/`partial` functional requirement nothing implements | 0 (warning); **1 under the strict switch** | + +## The strict switch — `--require-implementers` + +`WARN_REQUIREMENT_NOTHING_IMPLEMENTS` (a `live` or `partial` functional requirement where +neither it nor anything nested under it names a node) is a warning by default. A project +whose ledger has caught up with its links can make it fail the build: + +| port | flag | environment | +|---|---|---| +| TypeScript | `meta verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Python | `metaobjects verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Java, Kotlin | `mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` | `META_REQUIRE_IMPLEMENTERS=1` | +| C# | `dotnet meta verify … --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | + +It raises that one finding to an error and **keeps its code**. Every other warning stays a +warning. If CI went red on this code, check whether the switch is set before treating it +as a new defect: the fix is to add the `@implementedBy` link (or drop the entry to +`planned`), not to unset the switch. + +Once the ledger is clean, set the switch where the project already runs `verify`: its verify +script, CI job or pre-push hook. Until then a stale `live` claim is only a warning that +nobody reads. + +## Keeping the ledger current + +`verify` proves the ledger's links resolve, not that the ledger is current. A commit that +adds, changes or removes behaviour updates the ledger in the same commit: the new or changed +claim, its `@status` and its `@implementedBy`. If the commit should not touch the ledger, the +commit message says why. + +## The error codes, and the fix for each + +Match on the stable `code`, never the message text (ADR-0009). The runtime messages already +name the remedy; this table exists so you can act on one without re-deriving the rule. + +| code | what it means | fix | +|---|---|---| +| `ERR_REQUIREMENT_DANGLING_REF` | an `@implementedBy` ref does not resolve in the loaded model, on a status that requires live nodes | the model moved and the claim is stale — repoint the ref, or change the status if the capability really went away. **Check the ref is not merely unloaded**: a source missing from `sources` makes a live node look deleted. | +| `ERR_REQUIREMENT_L5_NOT_MEMBER` | an L5 entry's ref names an OBJECT | move the entry to **L4**, or repoint to a member as `pkg::Owner.member`. Moving the level is usually right — the ref is usually what you meant. | +| `ERR_REQUIREMENT_L4_NOT_OBJECT` | an L4 entry's ref names a MEMBER | the symmetric case — move the entry to **L5**, or repoint at the owning object. | +| `ERR_REQUIREMENT_LINK_ABOVE_FLOOR` | `@implementedBy` on an L1–L3 node | L1–L3 are problem-domain altitude and never reference the model. Push the claim down to the L4/L5 child that actually carries it. | +| `ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS` | a `live`/`partial` `requirement.architectural` that nothing implements | a policy declared and applied to nothing. Claim the nodes it governs, or drop it to `planned` — which is exempt, because it is not applied yet by definition. | +| `ERR_REQUIREMENT_LEVEL_NESTING` | a node's `@level` disagrees with the level of the parent it nests under | nesting IS the hierarchy. Move the node to the right parent rather than editing the level to match where it happens to sit. | +| `ERR_REQUIREMENT_BAD_LEVEL` | `@level` is not an integer inside the allowed range | levels are L1–L5 and nothing else. | +| `ERR_REQUIREMENT_RETIRED_HAS_IMPLEMENTORS` | a `@status: retired` entry declares `@implementedBy` | a retired capability has no implementation by definition. Delete the attribute and put what used to implement it in `notes`; if the nodes are still there, the capability is `live` or `partial`, not retired. | +| `ERR_REQUIREMENT_SUPERSEDED_BY_NOT_RETIRED` | `@supersededBy` on a status other than `retired` | it names what REPLACED a withdrawn capability, so on a live one there is nothing for it to say. | +| `ERR_MISSING_REQUIRED_ATTR` | a required attr is absent | `@statement`, `@status` and `@counterexample` are required on both subtypes; `@level` is required on `functional` and optional on `architectural`. | +| `ERR_BAD_ATTR_VALUE` | a closed-enum attr has an unknown value | `@status` and `@disposition` are enforced by the LOADER, so a typo fails the load in every port rather than passing in some. | + +**A note on the L4/L5 pair.** They are enforced at the same site and fail for opposite +reasons, so reading only the code you hit can send you the wrong way. The question is not +"is this ref valid?" — usually it is — but "does the LEVEL match the shape of the ref?" + +## The authoring lint — a second section, never an error + +The Node `meta verify` also prints an **authoring lint** under its own heading, after the +gate's own warnings. It is TypeScript-only: `metaobjects verify`, `mvn metaobjects:verify` +and `dotnet meta verify` run the gate above and never print these codes, so on those ports +a clean run says nothing about them. + +``` +meta verify — requirements: 6 authoring warning(s) (advisory — does not fail the build): +``` + +Read it as a different claim from everything above. The gate says the ledger **disagrees with +the model**; the lint says it agrees but **records less than its author thinks**. Every finding +is a warning and none can change the exit code, so a lint-only run is a passing run. + +| code | what it means | fix | +|---|---|---| +| `WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE` | the `name` holds a character that breaks the dotted path or the generated stub filename | rename it. A `.` is the common one and the worst: `Orders.Recorded` and `Orders` containing `Recorded` produce the **same** path, so the address stops identifying one node. | +| `WARN_REQUIREMENT_NAME_READS_AS_PROSE` | the `name` is a sentence | the name is an address — make it an identifier and put the prose in `@statement`. | +| `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT` | `name` and `@statement` say the same thing | the claim is written twice. Keep `@statement` (every surface reads it) and shorten the name. | +| `WARN_REQUIREMENT_PROSE_EMPTY` | `@statement` or `@counterexample` is present but blank | the loader requires the attr to EXIST, never to say anything. Write the sentence, or delete the entry. | +| `WARN_REQUIREMENT_PROSE_DUPLICATED` | `description` repeats `@statement` (whole, or as its opening sentence), or `@counterexample` does | `@statement` is already the description. `description` holds the SCOPE; drop it entirely if the scope is obvious. | +| `WARN_REQUIREMENT_INERT_DOC_SLOT` | `summary` is set on a requirement | `@statement` is already the required one-line sentence, so a summary can only repeat it, and nothing reads it. Delete it. (`title` is NOT flagged — it is chartered as the entry's label.) | +| `WARN_REQUIREMENT_TITLE_IS_AN_ID` | `title` holds a catalogue or ticket id | a title is a noun phrase and an id is not a name. **Split** it — the id to `@trackedBy`, the phrase stays the title. Do not move the whole string; that throws the label away. | + +Findings from both sections are addressed by the requirement's **dotted path**, never its +bare name — two branches of a ledger may reuse a name, so a bare one can be ambiguous. + +Mute the lint with `--no-requirement-lint` or `META_NO_REQUIREMENT_LINT=1`. That silences the +advisory half **only** — the gate above still runs and can still exit 1. + +Two deliberate silences. The lint reports only **exact** repeats, never a paraphrase — a +similarity threshold on prose produces findings you can argue with. And it never judges +whether a statement is true or a counterexample sufficient; those are the judgements the +ledger exists to record. + +## `verify` does not look at your tests + +It used to. `@verifiedBy` asked you to name a test, and `verify` checked that the **name** +occurred somewhere in your test sources. That is existence evidence, never proof — an audit of +one real 19-name ledger opened every named test and found **4 that did not verify their +claim**: one matched a comment, one a dependency-injection key, one a real test of a +*different* claim, and one a test of the entry's output where the claim was about its source +text. `verify` reported zero errors throughout. The author picks the string, so the cheapest +way to satisfy the check is to find any name that already exists. + +The attribute is retired. Tying a requirement to a test is the job of a generator that emits +the test **from** the requirement, so the link is structural rather than a name someone chose. +Every port ships one, `requirement-tests` (see the codegen skill's reference for the +project's language). `verify` still never reads test results: the codegen drift gate +(`verify --codegen`) proves the generated tests match the ledger, a live test with nothing +behind it fails in the project's own test run, and a generated test that passes proves its +witness ran, not that the witness tests the claim. + +## What a green run does NOT prove + +It proves **referential integrity**: statuses parse, levels are in range, links sit at or +below the floor, references resolve. + +It cannot prove a status is **true**, or that a node genuinely implements the requirement +claiming it. No test can. That judgement is yours. + +Coverage is also narrower than the name suggests: it is checked at **entity grain only** — +`object.value` and `object.projection` are exempt, and fields, views, validators and +identities are never required to be claimed. Green means "every entity is claimed by +something", not "every node is described". + +Full reference: the repo's `spec/capability-ledger.md`. diff --git a/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/upgrading.md b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/upgrading.md new file mode 100644 index 0000000000..23ccb45965 --- /dev/null +++ b/fixtures/agent-context-conformance/java-react/expected/.claude/skills/metaobjects-verify/references/upgrading.md @@ -0,0 +1,10 @@ +# Upgrading MetaObjects and keeping the ports in sync + +Read this before you move a project to a new MetaObjects release, and when a project runs more +than one port (a TypeScript client beside a Java backend, say). + +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. + +`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. + +**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md index 44eb9f1644..26abb81c10 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/AGENTS.md @@ -3,82 +3,91 @@ > 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/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `mvn metaobjects:generate`. - -## Read the generated reference before you touch a tier -`meta gen` writes four reference files under this project's docs target -(`targets.docs.outDir` in `metaobjects.config.ts`) when `generators` lists the generator -for each — `agentDocsFile()` for the three `agent/` pages, `apiDocsFile()` for the api -reference. There is no separate docs command: documentation is one more entry in the same -list every other generator is in. They are generated from THIS project's metadata, so they -describe what it actually is rather than what the framework does in general — and reading -the relevant one first is how you avoid inferring the answer from generated code, which is -the disposable half. - -- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each - column came from, keys, foreign keys, constraints. -- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter - operators the generators actually emitted. -- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and - filterability per field, and each declared grid. -- **`agent/requirements.md`** — before adding a capability. What is already claimed, and - what was deliberately **retired** — an entry saying a capability must not be rebuilt is - the one thing you cannot learn from the code, because the code is gone. - -Not there? List the missing generator — and a `docs` target, if `targets` has none — in -`generators`, then run `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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. -- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. -- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. -- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. -- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. -- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. -- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. -- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. -- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. -- 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. -- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. - -## Authoring rules you must not violate -- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. -- Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### Java: generate with `mvn metaobjects:generate` + +1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator + list, one `` each, under `` pointing at the metadata. + Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`, + `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and + `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a + list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator` + writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The + full table is in the `metaobjects-codegen` skill's `references/java.md`. +2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java` + copies a generator to change what it emits. + +### Web client: generated by the Node `npx meta gen` + +The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` +from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from +`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and +`@metaobjectsdev/tanstack`. Wrap the app in ``: +`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every +hook without an error. The `metaobjects-runtime-ui` skill has the detail. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `mvn metaobjects:verify` (the generated code and the requirements). +- `npx meta verify --codegen` (the generated web client). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md index 44eb9f1644..26abb81c10 100644 --- a/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/java-react/expected/.metaobjects/CLAUDE.md @@ -3,82 +3,91 @@ > 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/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `mvn metaobjects:generate`. - -## Read the generated reference before you touch a tier -`meta gen` writes four reference files under this project's docs target -(`targets.docs.outDir` in `metaobjects.config.ts`) when `generators` lists the generator -for each — `agentDocsFile()` for the three `agent/` pages, `apiDocsFile()` for the api -reference. There is no separate docs command: documentation is one more entry in the same -list every other generator is in. They are generated from THIS project's metadata, so they -describe what it actually is rather than what the framework does in general — and reading -the relevant one first is how you avoid inferring the answer from generated code, which is -the disposable half. - -- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each - column came from, keys, foreign keys, constraints. -- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter - operators the generators actually emitted. -- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and - filterability per field, and each declared grid. -- **`agent/requirements.md`** — before adding a capability. What is already claimed, and - what was deliberately **retired** — an entry saying a capability must not be rebuilt is - the one thing you cannot learn from the code, because the code is gone. - -Not there? List the missing generator — and a `docs` target, if `targets` has none — in -`generators`, then run `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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. -- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. -- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. -- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. -- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. -- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. -- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. -- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. -- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. -- 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. -- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. - -## Authoring rules you must not violate -- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. -- Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### Java: generate with `mvn metaobjects:generate` + +1. **Config.** The `metaobjects-maven-plugin`'s `` in `pom.xml` is the generator + list, one `` each, under `` pointing at the metadata. + Nothing is generated until it is listed. The Spring REST tier is `SpringDtoGenerator`, + `SpringValueObjectGenerator`, `SpringRepositoryGenerator`, `SpringFilterAllowlistGenerator` and + `SpringControllerGenerator` (package `com.metaobjects.generator.spring`); the build refuses a + list missing a generator another one's output needs, and names it. `JUnitRequirementTestsGenerator` + writes one test per requirement; `JavaApiDocsGenerator` writes `api/java/AGENT-API.md`. The + full table is in the `metaobjects-codegen` skill's `references/java.md`. +2. **Generate.** `mvn metaobjects:generate`. `mvn metaobjects:eject -Dnames= -Dport=java` + copies a generator to change what it emits. + +### Web client: generated by the Node `npx meta gen` + +The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` +from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from +`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and +`@metaobjectsdev/tanstack`. Wrap the app in ``: +`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every +hook without an error. The `metaobjects-runtime-ui` skill has the detail. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `mvn metaobjects:verify` (the generated code and the requirements). +- `npx meta verify --codegen` (the generated web client). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/requirements.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/requirements.md new file mode 100644 index 0000000000..2bcebe9ec1 --- /dev/null +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-audit/references/requirements.md @@ -0,0 +1,154 @@ +# Requirements — the audit dimension + +This project declares `requirement.*` nodes, so the ledger is an auditable surface. Audit it +for **truthfulness**, not volume. A large ledger that lies is worse than a small one that +does not, because every later reader trusts it. + +Run `verify` first — the project's own: `meta verify`, `metaobjects verify`, +`mvn metaobjects:verify` or `dotnet meta verify`. Every port runs the same requirement +gate. It settles referential integrity mechanically — do not spend audit effort re-deriving +what a green run already proves. + +## Contents +- What verify has already proven (do not re-check by hand) +- Currency checks — run these before sampling +- What only a human or an agent reading the code can catch +- Scope — do NOT flag these as defects +- Reporting + +## What verify has already proven (do not re-check by hand) + +A run answers two different questions in two separate sections. Both are already-proven +ground; neither is worth an auditor's time. + +**The gate** settles referential integrity: links sit at or below the L4 floor, nesting +agrees with levels, `@status` values are legal, and references resolve (with dangling allowed +on `planned`, whose nodes do not exist yet). + +**The authoring lint** — printed under its own heading, advisory, and unable to change the +exit code — settles the naming and prose defects you would otherwise find by reading every +entry. **Only the Node `meta verify` prints it.** On a Python, Java, Kotlin or C# project +whose pipeline runs that port's `verify` alone, nothing below has been proven: run +the Node `meta verify` over the same metadata, or audit these by hand. + +| code | what a clean run has already proven | +|---|---| +| `WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE` | no `name` is blank, space-padded, or carries a `.` / `/` / `:` that breaks the dotted path or the generated stub's filename | +| `WARN_REQUIREMENT_NAME_READS_AS_PROSE` | no `name` is a sentence sitting in the address slot | +| `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT` | no `name` and `@statement` are the same sentence written twice | +| `WARN_REQUIREMENT_PROSE_EMPTY` | no `@statement` / `@counterexample` is declared-but-blank (the loader enforces presence, never content) | +| `WARN_REQUIREMENT_PROSE_DUPLICATED` | no `description` repeats `@statement` whole or as its opening sentence, and no `@counterexample` repeats it | +| `WARN_REQUIREMENT_INERT_DOC_SLOT` | no `summary` is set — nothing reads it, and `@statement` is already the required one-liner | +| `WARN_REQUIREMENT_TITLE_IS_AN_ID` | no `title` leads with a catalogue or ticket id | + +Three limits, and each puts something back on your list: + +- **It is one port's.** See above: establish which `verify` the project's CI runs before + crediting a green pipeline with the table. +- **The lint is mutable.** `--no-requirement-lint` / `META_NO_REQUIREMENT_LINT=1` silences the + advisory half while the gate still runs. **Establish whether the project mutes it** (§F of + the checklist asks this) — against a muted lint the whole table above proves nothing. +- **It reports only EXACT repeats.** A paraphrase — `description` restating `@statement` in + different words, a `@counterexample` that merely negates it — is invisible to the tool and + is a legitimate hand finding. + +`title` is deliberately NOT flagged as an inert slot; it is chartered as the entry's label and +is rendered (see item 6). Do not report a populated `title` as a defect. + +## Currency checks — run these before sampling + +A green gate proves the ledger's links resolve, not that the ledger keeps up with the code. +These three checks measure that, and the first one chooses the sample for item 1 below. + +**(a) Ledger stale.** From git history, take the commits that add a feature (a `feat:` / +`feat(...)` subject under Conventional Commits; otherwise merged feature branches or PRs +labelled as features) over the last six months or the last 50 such commits, whichever is +fewer. Report two numbers: the share of those commits that also change requirement metadata +(any file holding `requirement.*` nodes), and the count of feature commits since the most +recent change to that metadata. A share **under half**, or **more than ten** feature commits +since the last ledger change, is the finding `ledger-stale`, stating both numbers. The +feature commits that did not touch the ledger are the sample for item 1: read what each one +built and check that a requirement says so with a true status. + +**(b) Nothing implements a live claim.** Run the project's verify with the strict switch: +`meta verify --require-implementers`, `metaobjects verify --require-implementers`, +`mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` or +`dotnet meta verify --require-implementers` (all ports also read +`META_REQUIRE_IMPLEMENTERS=1`). Each `WARN_REQUIREMENT_NOTHING_IMPLEMENTS` on a `live` entry +is a finding (`live-claim-unimplemented`) naming the entry. State whether the project's own +gate passes the switch; if it does not and the ledger has no such entries, recommend adding +it so the ledger cannot fall behind again. + +**(c) Live claim, unwired implementer.** For each `@implementedBy` target of a `live` entry, +find the generated code that target produces and confirm hand-written code imports or calls +it. A live claim whose implementer is generated code nothing uses is the finding +`live-claim-unwired-implementer`: the claim resolves, so verify is satisfied, but the +capability is not reachable. Before raising it, rule out a runtime use: code that reaches the +entity through the runtime `ObjectManager` (or a metadata-driven admin or tool registry) +uses it without importing anything generated, so search for the entity's name in those calls +too. + +## What only a human or an agent reading the code can catch + +**1. Statuses that are false.** The highest-value finding in the whole dimension. A +requirement marked `live` whose implementation was gutted; one marked `partial` that is now +complete; one marked `planned` that was quietly built months ago. Sample the claims +and read the nodes, starting with the feature commits currency check (a) found outside the +ledger. `status` is the only payload with controlled evidence behind it — the +resurrection protection rides entirely on it being true. + +**2. Non-violable statements.** Every requirement must state what breaking it looks like. +*"Every entity has a uuid primary key"* is violable — point at one with a composite key. +*"Things are persisted"* is not; it is a description wearing a requirement's shape. Flag +these: they inflate the ledger while proving nothing. + +**3. Claim-padding.** An entity appended to an existing unrelated requirement's +`implementedBy` purely to silence the coverage warning. The claim resolves, so verify is +happy, but it carries no information about what the entity is *for*. Look for entities whose +only claim is a high-fan-out architectural rule when they plainly have product meaning. + +**4. Misfiled kind.** `functional` is checked by EXISTENCE, `architectural` by UNIVERSALITY — +opposite polarity, and nothing mechanical catches a misfile. The discriminator: did this +exist because someone asked for something (functional), or because every entity here looks +like this (architectural)? An architectural entry with one claimant is usually a misfiled +functional one. + +**5. Levels used as decoration.** L1–L3 are organisational and must never reference the +model. If the tree is flat, or every entry is L3, the levels are carrying no information and +should be simplified rather than defended. + +**6. Titles that are not labels.** `title` is now **rendered** — `meta gen`'s +`requirementsFile()` heads each entry `## checkout.payment — Payment capture`, the path +first and the label after it — so a slot +that used to be inert is read by every reader of the generated page. The lint catches only +the id-shaped case (`FR-448 …`); it cannot tell a *useless* label from a good one. Read the +titles: one that restates `@statement`, or repeats the path in prose, is now visible noise in +a heading rather than a private authoring habit. An absent `title` is NOT a finding — the +entry heads by its path alone, which is what every sibling surface addresses it by. + +**7. Witnesses that do not test the claim.** Where the project generates requirement tests +(the `requirement-tests` generator, in any port), a passing generated test proves only that +its witness — or, in TypeScript, the hand-filled stub — ran. Sample the witnesses and read +them against the counterexample each generated test carries: one that asserts nothing, or asserts +something else, is the same defect `@verifiedBy` had. Check also for witnesses that have +gone stale silently: a Java witness that lacks the override annotation, a C# witness implemented +implicitly, and any Python witness keep existing after their requirement is retired or +deleted, and nothing reports them. + +## Scope — do NOT flag these as defects + +- **Unclaimed `object.value` / `object.projection`.** Exempt by design: a value is a shape, a + projection derives from a claimable entity. +- **Fields, views, validators and identities without their own requirement.** Member-grain + coverage is explicitly rejected — plumbing members are covered by architectural + requirements with high fan-out. L5 exists so a member claim *can* be made where it means + something, never so every member must carry one. +- **Dangling links on `planned`.** Correct — the plan precedes the nodes. +- **A project with no requirements at all.** The feature is opt-in by declaration; absence is + not a finding. + +## Reporting + +Report findings as claims to verify, not as a score. "These 4 requirements are marked `live` +and I could not find their implementation" is actionable; "ledger maturity: 62%" is not, and +invites optimising the number. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md index 1979d161f2..de11bce2cc 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -38,7 +38,7 @@ This file covers what almost every model needs. The topics below live in | `references/inheritance-tph.md` | several entities are variants of one thing sharing a single table (`@discriminator`) | | `references/metadata-dependencies.md` | the project builds on another package's metadata (`dependencies`, cross-package `overlay`) | | `references/reporting.md` | what a declared report gives you: what columns a report gets, time grains, null rules, rows with no facts (`@spine`), defaults (`@default`), engine differences | -| `references/requirements.md` | installed only when the project declares `requirement.*` nodes | +| `references/requirements.md` | you add, change or review a `requirement.*` node | ## The operating principle: model-first, generate-first @@ -483,7 +483,7 @@ Every requirement must be **violable**: if you cannot say what breaking it looks **A commit that adds, changes or removes behaviour updates the ledger in the same commit** (the new or changed claim, its `@status` and `@implementedBy`), or says in the commit message why it does not. `verify` checks that links resolve, not that the ledger is current. -If the project declares any `requirement.*` node, `references/requirements.md` is installed with the full authoring rules. If it does not and you are adding the first one, the shape is: +`references/requirements.md` has the full authoring rules. The shape of one node: ```yaml - requirement.architectural: diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/references/requirements.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/references/requirements.md new file mode 100644 index 0000000000..abd10504a0 --- /dev/null +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-authoring/references/requirements.md @@ -0,0 +1,225 @@ +# Requirements — `requirement.functional` / `requirement.architectural` + +Capabilities are **metadata**, declared in the metadata directory beside the entities they +describe. Read the existing requirement nodes before designing anything. Two rules matter +more than the rest. + +## Contents +- A requirement is PRESCRIPTIVE — it states what should be true, never what happened +- Do not delete a retired capability's entry +- When you add an entity, claim it +- A feature commit updates the ledger in the same commit +- Every requirement states its counterexample +- Four prose slots, and `statement` is the one that means "description" +- `title` is a LABEL and is chartered here; `summary` is not +- Never put a catalogue or ticket id in `title` +- The `name` is an address, so write it as an identifier +- Hierarchy is nesting, and links live at the bottom +- Write an L5 ref as a dotted `pkg::Owner.member` path +- L1–L3 are levels of abstraction and ownership in the problem domain +- Architectural requirements are the other kind +- Record gaps rather than rounding them off +- `status: planned` locks in work you have not started +- Which `verify` says what +- A requirement's name and package end up in generated test names + +**1. A requirement is PRESCRIPTIVE — it states what should be true, never what happened.** +`status` is `planned | live | partial | retired`, and every one of them states something +meant to be true. `retired` is not the exception: it means **"this was built, then +deliberately removed, and must not be rebuilt"** — a prohibition in force, falsifiable by +exactly one observable, the capability reappearing. + +So write a retired entry as a rule, not a diary. `statement: "An unpaid order is never +expired by a wall-clock timer"` is a prohibition; `"We used to expire orders on a timer"` is +history wearing a requirement's shape, and no gate will catch it. On a retired entry +`counterexample` describes **the revival** — the thing a future reader is about to propose. + +**Do not delete a retired capability's entry.** Keeping it is the single most load-bearing +thing this vocabulary does: given a brief for a capability that had been retired, agents +working from the model alone proposed rebuilding it 24 times out of 24. The entry is what +stops that, so a retirement is a status change plus a rewritten statement, never a deletion. + +Three rules the loader enforces on `retired`: + +- **`implementedBy` is REFUSED** — a retired capability has no implementation by definition. + What used to implement it goes in `notes`. If deleting the references feels wrong, the + capability is probably not retired: an entry whose nodes are still there is `live` or + `partial`. +- **`supersededBy` names the requirement that replaced it**, legal here only, and RESOLVED — + so the chain still works when the replacement is itself retired later. +- It never counts toward object coverage, and is exempt from the architectural universality + check. + +Leaving a dangling `implementedBy` is **correct only on `planned`**: the entry precedes the +nodes. On `live` or `partial` the same dangling reference is an error — the model moved and +the requirement went stale. + +**2. When you add an entity, claim it.** Every `object.entity` should appear in some +requirement's `implementedBy`, or `verify` says so. + +**A commit that adds, changes or removes behaviour updates the ledger in the same commit.** +`verify` checks that the ledger's links resolve, never that it is current, so nothing fails +when a feature ships and its requirement does not move. In the same commit, write the new or +changed claim, set its `@status` (`planned` to `live` when it lands, `retired` when it is +removed) and keep its `@implementedBy` pointing at the real nodes (a retired entry carries +none). If the commit should not +touch the ledger, say why in the commit message. + +**Every requirement states its counterexample.** One sentence: what breaking it looks like. +*"Every entity has a uuid primary key"* is violable — point at one with a composite string +key. *"Things are persisted"* is not, and is a description rather than a requirement. Same +rule kills *"the system is reliable"*. If you cannot say what breaking it looks like, +delete it. + +**Four prose slots, and `statement` is the one that means "description".** A requirement can +also carry the common `description` and `notes`, and they overlap badly unless you decide the +split before writing any of them: + +- `statement` — **the claim**. This IS the description of what the requirement is +- `counterexample` — **what would falsify the claim**, which is what makes it checkable +- `description` — **the scope**: what the claim covers, what it deliberately does not, and + which sibling entry owns the rest +- `notes` — **the evidence**: how you know the `status` is true — citations, vocabularies, the + control you ran to prove an absence was real + +**`title` is a LABEL and is chartered here; `summary` is not.** A requirement's `name` is an +identifier and its address renders as a dotted camelCase path, so a short noun-phrase `title` is +what an index shows — the requirement attribute table in `spec/capability-ledger.md` says so by +name, and the generated requirements page renders it in the heading after the path +(`## checkout.payment — Payment capture`). `summary` is different: `statement` is already the +required one-line sentence, so a `summary` can only repeat it and nothing reads it (`verify` warns, +`WARN_REQUIREMENT_INERT_DOC_SLOT`). `notes` is unrendered on purpose — chartered internal-only. + +**Never put a catalogue or ticket id in `title`.** A title is a noun phrase; an id is not a name. +`title: "FR-467 — Order recording"` is two things in one slot — put the id in `trackedBy` and +keep the phrase as the title. `verify` warns (`WARN_REQUIREMENT_TITLE_IS_AN_ID`). + +**The `name` is an address, so write it as an identifier.** It is the segment of the dotted +path (`Ordering.Placement.Recorded`) and the filename of the generated test stub. A `.` in a +name is indistinguishable from nesting — `Orders.Recorded` and `Orders` containing `Recorded` +produce the same path — and a sentence for a name puts the claim somewhere nothing reads it. +Both load; `verify` warns (`WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE`, +`WARN_REQUIREMENT_NAME_READS_AS_PROSE`, `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT`). + +Two failure modes, both of which look like diligence. A `description` that **paraphrases the +statement** is padding, and it makes every later reader trust the ledger less — leave it off +instead, it is optional. A `description` that **narrates the evidence** belongs in `notes`; +the tell is a fact you had to read the implementation to learn. Mechanical test for the last +line: *would this sentence have to change if the code changed but the model did not?* Then it +is `notes`. `verify` warns on the exact repeats (`WARN_REQUIREMENT_PROSE_DUPLICATED`), and on +a `statement` or `counterexample` that is present but blank (`WARN_REQUIREMENT_PROSE_EMPTY`) — +the loader requires those attrs to exist, never to say anything. + +**Hierarchy is nesting, and links live at the bottom.** L1 solution, L2 segment, L3 +service — these never reference the model. **L4** binds a declared top-level node — an +`object.*` **or a `template.*`** — and **L5** binds a member of one: a field, view, +validator, identity, or a template's child. `implementedBy` above L4 is an error. +Regrouping *moves* a node; it does not edit a parent string. + +**Write an L5 ref as a dotted `pkg::Owner.member` path** — the level and the shape of the +ref must agree, and this is the single easiest thing to get wrong: + +```yaml +# L4 binds the OBJECT +level: 4 +implementedBy: ["acme::orders::Order"] + +# L5 binds ONE MEMBER of it — dotted owner.member, not the object +level: 5 +implementedBy: ["acme::orders::Order.placedAt"] +``` + +Both mismatches are caught and they are symmetric: an L5 whose ref names an object is +`ERR_REQUIREMENT_L5_NOT_MEMBER`, an L4 whose ref names a member is +`ERR_REQUIREMENT_L4_NOT_OBJECT`. **In both cases the fix is to move the entry to the other +level, not to rewrite the ref** — the ref is usually right and the level is usually the +mistake. The instinct that produces the error is reaching for the object a rule is *about*; +what L5 wants is the member the rule is *carried by*. Copying an L4 block and changing only +`level: 5` produces exactly this failure. + +Reach for L5 when the statement is about ONE column and its entity's own requirement could +not express it — "this lifecycle is `resolvedAt IS NULL` and never `status`", "this id is +nullable, which is why the parent is `partial`". Do NOT use L5 for a blanket rule that +happens to touch many fields; that is an architectural requirement. + +Claim your prompts. A `template.prompt` is a model node realising a capability exactly as +an entity is, and it is the node whose retirement is hardest to see later — a removed +prompt leaves no table behind. A prompt estate with no requirement entries is the same +blind spot this whole mechanism exists to close. + +**L1–L3 are levels of abstraction and ownership in the problem domain** — whose need is +this, and at what altitude — and are NEVER a directory, package, deployable or module. +Technical constructs appear only at L4/L5, which is the allocation step. Test every node: +*if a refactor that changes no behaviour would force it to move, its level is wrong.* + +**Architectural requirements are the other kind.** `requirement.architectural` is flat by +default — a uuid-PK rule, change attribution, tenant scoping. Its check is *universality* +rather than existence, so one that is `live` and claimed by nothing fails: a policy +declared and applied to nothing. A `@level` is OPTIONAL here and opts the node into a tree +(for organising non-functional requirements under a quality taxonomy); once levelled, the +same nesting and link-floor rules apply as to a functional node. + +`@status` is a closed enum enforced by the loader, so a typo fails the load rather than +silently disabling the entry. + +**Record gaps rather than rounding them off.** `partial` says *this works and here is what +is wrong with it*, and it is the most useful status in the enum — a ledger with none is +usually one nobody read carefully. Then say what was DECIDED, which is a separate question: + +- `@disposition: accepted` — understood, deliberately not being closed +- `@disposition: deferred` — will be closed, not now (name a ticket in `@trackedBy`, or + `verify` warns; deferring without one is how a known problem becomes an unknown one) +- **absent** — undecided, and that is a real state. `verify` counts these, because + *"which gaps has nobody ruled on?"* is the question a review exists to answer. + +A `partial` nobody intends to finish is `partial` + `@disposition: accepted` — the gap is +understood and deliberately not being closed. If the capability itself is gone, delete the +requirement instead. + +**`status: planned` locks in work you have not started.** Its references may dangle (write +the requirement before the entity), and it never counts toward object coverage — otherwise +declaring an intention would clear the unclaimed-entity warning and the gate would measure +ambition rather than work. + +```yaml +- requirement.functional: + name: Pacing + level: 3 + status: live + statement: "Scene pacing follows story beats" + counterexample: "A scene that advances on a clock rather than on the story" + children: + - requirement.functional: + name: BeatProgression + level: 4 + status: planned # not built yet -- refs may dangle + statement: "A scene advances when its beat completes" + counterexample: "A scene that advances with its beat unresolved" + trackedBy: ["acme/game#412"] + implementedBy: ["game::turn::BeatProgression"] # does not exist YET + +- requirement.architectural: + name: UuidPrimaryKeys + status: live + statement: "Every entity has a uuid primary key" + counterexample: "An entity keyed by a composite string" + implementedBy: ["game::turn::Turn", "game::world::Location"] +``` + +**Which `verify` says what.** The gate — dangling references, links above the floor, +level and nesting errors, an unclaimed entity, a live requirement nothing implements — runs +in every port's `verify` (`meta verify`, `metaobjects verify`, `mvn metaobjects:verify`, +`dotnet meta verify`). The authoring warnings named on this page (`WARN_REQUIREMENT_NAME_*`, +`WARN_REQUIREMENT_PROSE_*`, `WARN_REQUIREMENT_INERT_DOC_SLOT`, +`WARN_REQUIREMENT_TITLE_IS_AN_ID`) are printed by the Node `meta verify` only, so on another +port follow these rules without waiting for a warning. + +**A requirement's name and package end up in generated test names.** Every port's +`requirement-tests` generator names a test from the requirement's package and dotted path +(`acme::shop::Orders.Recorded` becomes `req_acme_shop_Orders_Recorded…`), and two +requirements whose names differ only in punctuation (`Orders.Recorded` beside +`Orders_Recorded`) are refused as a collision outside TypeScript. Another reason to keep +names plain identifiers. + +There is **no `satisfies:` on a field or entity** — links live on the requirement node, not +on the nodes it claims. Full reference: the repo's `spec/capability-ledger.md`. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md index 36123e0d8f..a337e663e3 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -35,6 +35,7 @@ exact config, generator names and commands for this project; read it. The other | File | Open it when | |---|---| +| `references/principles.md` | a rule in the always-on file does not settle the case: the full standing principles (adopting onto existing code, the schema and live database, prompts, requirements, owning and writing generators, physical names) with their reasons | | `references/any-stack.md` | **the database, data store, HTTP framework or UI framework is not one a reference generator targets** — MySQL, MongoDB, DynamoDB, Express, NestJS, Next.js, Vue, … | | `references/owning-generators.md` | generated output is wrong; your framework is not the one a reference emits for; you write or review a generator (the `own*()` accessor table) | | `references/choosing-layers.md` | the generated API does not fit, you need joined or derived data, or you are adopting onto existing code | diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/principles.md new file mode 100644 index 0000000000..2c2f61d72d --- /dev/null +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -0,0 +1,36 @@ +# Standing principles + +The rules that hold in every MetaObjects project, whatever the stack and whatever the task. The +always-on file in `.metaobjects/` carries the short form; this page is the full text, with the +reasons. Read it when a rule there does not settle the case in front of you. + +## Contents + +- Principles +- Authoring rules you must not violate + +## 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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. +- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. +- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. +- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. +- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. +- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. +- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. +- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. +- 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. +- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. + +## Authoring rules you must not violate + +- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. +- Nodes are fused-key maps: `{".": { ... }}` (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`). diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/SKILL.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/SKILL.md index 57b556e1f6..fcdb550249 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/SKILL.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/SKILL.md @@ -334,4 +334,5 @@ what's wrong. --- -For the migration tooling read `references/migration.md`. +For the migration tooling read `references/migration.md`. Before upgrading MetaObjects, or +when a project runs more than one port, read `references/upgrading.md`. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/migration.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/migration.md index f589708490..ff97eb6b8e 100644 --- a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/migration.md +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/migration.md @@ -18,6 +18,7 @@ stays native to the language. - Install (Node, dev-only) - Output format - The workflow +- Renames and destructive changes - Dialects - Live-DB drift: `meta verify --db` - Index modeling (Postgres) @@ -109,8 +110,42 @@ meta migrate baseline --from-db --db postgresql://... --dialect postgres 2. **Review the SQL.** Read the emitted `up.sql` (forward) and `down.sql` (rollback) before applying. Destructive changes (drop column / drop table) are - opt-in — the engine blocks them unless explicitly allowed, and routes ambiguous - rename-vs-drop+add decisions through a prompt rather than guessing. + opt-in — the engine refuses them until you allow each kind by name (`--allow`, + below), and an ambiguous rename-vs-drop+add aborts unless you declare the rename + or choose with `--on-ambiguous`. It never guesses. + +## Renames and destructive changes + +A renamed field or object is a renamed column or table, and the diff cannot tell that +from a drop plus an add unless the names are close. Declare the rename and the data is +kept: + +```bash +meta migrate --dialect postgres --slug rename-origin --rename-column shipment.origin_city=origin_port +meta migrate --dialect postgres --slug rename-stop --rename-table stop=port_call +``` + +- `--rename-column [schema.]table.old=new` and `--rename-table [schema.]old=new` are + repeatable. For a column, `table` is the table's name in the metadata, so it is the + **new** name when the table is renamed in the same run. A declared rename needs no + `--allow drop-column`, and one that finds nothing to rename is refused rather than + falling back to drop and add. Change a column's type, nullability or default in a + later run, not in the rename's. +- `--allow ` permits a destructive change by kind. A refusal names the token it + needs. The tokens: `drop-column`, `drop-table`, `type-change`, `drop-index`, `drop-fk`, + `drop-check`, `drop-view`, `drop-view-cascade`, `drop-column-read-by-view`, + `adopt-view`, `nullable-to-not-null`, `drop-identity-default`, `drop-comment`, + `drop-unmanaged`. Changing an enum's values replaces its CHECK, so it needs + `--allow drop-check`. `drop-column` deletes the column's data: for a rename use + `--rename-column`, never `--allow drop-column`. +- `--on-ambiguous abort|rename|drop-add` (default `abort`) decides a pair the + heuristic offers as a possible rename. Declaring the rename is clearer than `rename`. +- `migrate.allow` and `migrate.onAmbiguous` in `.metaobjects/config.json` set the same + things for every run. Prefer the flag: a standing allowance hides the next destructive + change from review. + +`docs/features/migrations-and-drift.md`, "Renames and populated tables", covers what a +rename carries with it (constraint names, views) and changes that need a backfill. 3. **Apply** the pending migrations against the DB; migration history is tracked in a ledger table: diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/requirements.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/requirements.md new file mode 100644 index 0000000000..e859076180 --- /dev/null +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/requirements.md @@ -0,0 +1,218 @@ +# Requirements — what `meta verify` checks + +This project declares `requirement.*` nodes, so `verify` checks them. **There is no +subverb**: requirements are metadata, so they are checked on *every* `meta verify` run. + +## Contents +- Which command runs the gate +- The split, and why it matters when you read a failure +- A load failure on RETIRED vocabulary — run `meta upgrade`, do not hand-sweep +- The status asymmetry — the one that surprises people +- Exit codes +- The strict switch — `--require-implementers` +- Keeping the ledger current +- The error codes, and the fix for each +- The authoring lint — a second section, never an error +- `verify` does not look at your tests +- What a green run does NOT prove + +## Which command runs the gate + +Every port's `verify` runs the same gate, with the same codes, severities and message +text (a shared corpus pins them). Run the one for the project's server language: + +| port | command | +|---|---| +| TypeScript | `meta verify` | +| Python | `metaobjects verify` | +| Java, Kotlin | `mvn metaobjects:verify` (one goal for both) | +| C# | `dotnet meta verify` | + +The gate is core: it is not a generator and cannot be ejected or replaced. What differs +by port is the **authoring lint** further down this page, which only the Node `meta verify` +prints. This page writes `meta verify` throughout; read it as the port's own command. + +## The split, and why it matters when you read a failure + +| | owns | +|---|---| +| **loader** | `@status` enum, required attrs, child rules, levels — unconditional | +| **`verify`** | `@implementedBy` resolution — **severity depends on `@status`** | + +A typo'd `@status` fails the **load** ("failed to load metadata"), before verify runs. If you +see that, no other diagnostic in the run is trustworthy — fix it first and re-run. + +## A load failure on RETIRED vocabulary — run `meta upgrade`, do not hand-sweep + +`@status` is not the only way the load fails before any of this runs. **0.24.0** retired +`@violation` (→ `@counterexample`) and `@verifiedBy`; **0.24.1** made an index key `@fields` +**XOR** `@expr`, so a node declaring both is now `ERR_INVALID_INDEX`. **0.24.2 reversed half +of the 0.24.0 cut**: `abandoned` and `superseded` become **`@status: retired`**, and +`@supersededBy` is registered again — this time as a reference the loader resolves. There is no deprecation shim for any of them — +the registry is sealed (ADR-0023), so a legacy document does not load at all and every check on +this page is unreachable until it does. + +``` +meta upgrade # previews every rewrite; writes nothing +meta upgrade --apply # makes them +``` + +It rewrites from the same table the loader's error text is generated from, so the fix you are +told about and the edit the tool makes cannot drift apart. It fixes only what has one correct +answer and **refuses the rest, exiting non-zero** — a partial migration can never be recorded +as finished by CI. Two refusals are expected on a real ledger and both are yours to decide: + +- **`@status: abandoned` / `superseded` are no longer refused** — they rewrite to `retired`, + and the same run drops the `@implementedBy` that a retired entry may not carry. This was the + canonical judgement case in 0.24.0; FR-039 made it determinate. +- **`origin.collection`.** Retired to `origin.aggregate @agg: collect`; the attribute sets + differ, so the tool will not guess. + +`@fields` beside `@expr` **is** rewritten, and the survivor is not a coin toss: that pair loaded +before 0.24.1 with `@fields` silently discarded, so the index already in your database is the +expression one. `upgrade` drops `@fields` — which reproduces the object that exists and emits no +DDL change. Do not hand-pick the other survivor; that invents a new index and migrates live data. + +## The status asymmetry — the one that surprises people + +The **same** unresolved `@implementedBy` reference is: + +- an **error** on `live` / `partial` — the model moved and the requirement is stale; +- **allowed** on `planned` — those nodes do not exist YET. That is + the entry doing its job, not drift. + +So on a `planned` entry a dangling reference is the entry doing its job, not drift — do not +"fix" it by deleting the reference, or you delete the plan. + +## Exit codes + +| situation | exit | +|---|---| +| clean tree, or no `requirement.*` nodes at all | 0 | +| dangling `@implementedBy` on `live`/`partial` | 1 | +| the same reference on `planned` | 0 | +| `@implementedBy` above the L4 link floor | 1 | +| live `requirement.architectural` claimed by nothing | 1 | +| an entity no requirement claims | 0 (warning) | +| a `live`/`partial` functional requirement nothing implements | 0 (warning); **1 under the strict switch** | + +## The strict switch — `--require-implementers` + +`WARN_REQUIREMENT_NOTHING_IMPLEMENTS` (a `live` or `partial` functional requirement where +neither it nor anything nested under it names a node) is a warning by default. A project +whose ledger has caught up with its links can make it fail the build: + +| port | flag | environment | +|---|---|---| +| TypeScript | `meta verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Python | `metaobjects verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Java, Kotlin | `mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` | `META_REQUIRE_IMPLEMENTERS=1` | +| C# | `dotnet meta verify … --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | + +It raises that one finding to an error and **keeps its code**. Every other warning stays a +warning. If CI went red on this code, check whether the switch is set before treating it +as a new defect: the fix is to add the `@implementedBy` link (or drop the entry to +`planned`), not to unset the switch. + +Once the ledger is clean, set the switch where the project already runs `verify`: its verify +script, CI job or pre-push hook. Until then a stale `live` claim is only a warning that +nobody reads. + +## Keeping the ledger current + +`verify` proves the ledger's links resolve, not that the ledger is current. A commit that +adds, changes or removes behaviour updates the ledger in the same commit: the new or changed +claim, its `@status` and its `@implementedBy`. If the commit should not touch the ledger, the +commit message says why. + +## The error codes, and the fix for each + +Match on the stable `code`, never the message text (ADR-0009). The runtime messages already +name the remedy; this table exists so you can act on one without re-deriving the rule. + +| code | what it means | fix | +|---|---|---| +| `ERR_REQUIREMENT_DANGLING_REF` | an `@implementedBy` ref does not resolve in the loaded model, on a status that requires live nodes | the model moved and the claim is stale — repoint the ref, or change the status if the capability really went away. **Check the ref is not merely unloaded**: a source missing from `sources` makes a live node look deleted. | +| `ERR_REQUIREMENT_L5_NOT_MEMBER` | an L5 entry's ref names an OBJECT | move the entry to **L4**, or repoint to a member as `pkg::Owner.member`. Moving the level is usually right — the ref is usually what you meant. | +| `ERR_REQUIREMENT_L4_NOT_OBJECT` | an L4 entry's ref names a MEMBER | the symmetric case — move the entry to **L5**, or repoint at the owning object. | +| `ERR_REQUIREMENT_LINK_ABOVE_FLOOR` | `@implementedBy` on an L1–L3 node | L1–L3 are problem-domain altitude and never reference the model. Push the claim down to the L4/L5 child that actually carries it. | +| `ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS` | a `live`/`partial` `requirement.architectural` that nothing implements | a policy declared and applied to nothing. Claim the nodes it governs, or drop it to `planned` — which is exempt, because it is not applied yet by definition. | +| `ERR_REQUIREMENT_LEVEL_NESTING` | a node's `@level` disagrees with the level of the parent it nests under | nesting IS the hierarchy. Move the node to the right parent rather than editing the level to match where it happens to sit. | +| `ERR_REQUIREMENT_BAD_LEVEL` | `@level` is not an integer inside the allowed range | levels are L1–L5 and nothing else. | +| `ERR_REQUIREMENT_RETIRED_HAS_IMPLEMENTORS` | a `@status: retired` entry declares `@implementedBy` | a retired capability has no implementation by definition. Delete the attribute and put what used to implement it in `notes`; if the nodes are still there, the capability is `live` or `partial`, not retired. | +| `ERR_REQUIREMENT_SUPERSEDED_BY_NOT_RETIRED` | `@supersededBy` on a status other than `retired` | it names what REPLACED a withdrawn capability, so on a live one there is nothing for it to say. | +| `ERR_MISSING_REQUIRED_ATTR` | a required attr is absent | `@statement`, `@status` and `@counterexample` are required on both subtypes; `@level` is required on `functional` and optional on `architectural`. | +| `ERR_BAD_ATTR_VALUE` | a closed-enum attr has an unknown value | `@status` and `@disposition` are enforced by the LOADER, so a typo fails the load in every port rather than passing in some. | + +**A note on the L4/L5 pair.** They are enforced at the same site and fail for opposite +reasons, so reading only the code you hit can send you the wrong way. The question is not +"is this ref valid?" — usually it is — but "does the LEVEL match the shape of the ref?" + +## The authoring lint — a second section, never an error + +The Node `meta verify` also prints an **authoring lint** under its own heading, after the +gate's own warnings. It is TypeScript-only: `metaobjects verify`, `mvn metaobjects:verify` +and `dotnet meta verify` run the gate above and never print these codes, so on those ports +a clean run says nothing about them. + +``` +meta verify — requirements: 6 authoring warning(s) (advisory — does not fail the build): +``` + +Read it as a different claim from everything above. The gate says the ledger **disagrees with +the model**; the lint says it agrees but **records less than its author thinks**. Every finding +is a warning and none can change the exit code, so a lint-only run is a passing run. + +| code | what it means | fix | +|---|---|---| +| `WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE` | the `name` holds a character that breaks the dotted path or the generated stub filename | rename it. A `.` is the common one and the worst: `Orders.Recorded` and `Orders` containing `Recorded` produce the **same** path, so the address stops identifying one node. | +| `WARN_REQUIREMENT_NAME_READS_AS_PROSE` | the `name` is a sentence | the name is an address — make it an identifier and put the prose in `@statement`. | +| `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT` | `name` and `@statement` say the same thing | the claim is written twice. Keep `@statement` (every surface reads it) and shorten the name. | +| `WARN_REQUIREMENT_PROSE_EMPTY` | `@statement` or `@counterexample` is present but blank | the loader requires the attr to EXIST, never to say anything. Write the sentence, or delete the entry. | +| `WARN_REQUIREMENT_PROSE_DUPLICATED` | `description` repeats `@statement` (whole, or as its opening sentence), or `@counterexample` does | `@statement` is already the description. `description` holds the SCOPE; drop it entirely if the scope is obvious. | +| `WARN_REQUIREMENT_INERT_DOC_SLOT` | `summary` is set on a requirement | `@statement` is already the required one-line sentence, so a summary can only repeat it, and nothing reads it. Delete it. (`title` is NOT flagged — it is chartered as the entry's label.) | +| `WARN_REQUIREMENT_TITLE_IS_AN_ID` | `title` holds a catalogue or ticket id | a title is a noun phrase and an id is not a name. **Split** it — the id to `@trackedBy`, the phrase stays the title. Do not move the whole string; that throws the label away. | + +Findings from both sections are addressed by the requirement's **dotted path**, never its +bare name — two branches of a ledger may reuse a name, so a bare one can be ambiguous. + +Mute the lint with `--no-requirement-lint` or `META_NO_REQUIREMENT_LINT=1`. That silences the +advisory half **only** — the gate above still runs and can still exit 1. + +Two deliberate silences. The lint reports only **exact** repeats, never a paraphrase — a +similarity threshold on prose produces findings you can argue with. And it never judges +whether a statement is true or a counterexample sufficient; those are the judgements the +ledger exists to record. + +## `verify` does not look at your tests + +It used to. `@verifiedBy` asked you to name a test, and `verify` checked that the **name** +occurred somewhere in your test sources. That is existence evidence, never proof — an audit of +one real 19-name ledger opened every named test and found **4 that did not verify their +claim**: one matched a comment, one a dependency-injection key, one a real test of a +*different* claim, and one a test of the entry's output where the claim was about its source +text. `verify` reported zero errors throughout. The author picks the string, so the cheapest +way to satisfy the check is to find any name that already exists. + +The attribute is retired. Tying a requirement to a test is the job of a generator that emits +the test **from** the requirement, so the link is structural rather than a name someone chose. +Every port ships one, `requirement-tests` (see the codegen skill's reference for the +project's language). `verify` still never reads test results: the codegen drift gate +(`verify --codegen`) proves the generated tests match the ledger, a live test with nothing +behind it fails in the project's own test run, and a generated test that passes proves its +witness ran, not that the witness tests the claim. + +## What a green run does NOT prove + +It proves **referential integrity**: statuses parse, levels are in range, links sit at or +below the floor, references resolve. + +It cannot prove a status is **true**, or that a node genuinely implements the requirement +claiming it. No test can. That judgement is yours. + +Coverage is also narrower than the name suggests: it is checked at **entity grain only** — +`object.value` and `object.projection` are exempt, and fields, views, validators and +identities are never required to be claimed. Green means "every entity is claimed by +something", not "every node is described". + +Full reference: the repo's `spec/capability-ledger.md`. diff --git a/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/upgrading.md b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/upgrading.md new file mode 100644 index 0000000000..23ccb45965 --- /dev/null +++ b/fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-verify/references/upgrading.md @@ -0,0 +1,10 @@ +# Upgrading MetaObjects and keeping the ports in sync + +Read this before you move a project to a new MetaObjects release, and when a project runs more +than one port (a TypeScript client beside a Java backend, say). + +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. + +`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. + +**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md index ece01bdac0..8f925a267a 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/AGENTS.md @@ -3,79 +3,87 @@ > Stack: python server, no 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/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `metaobjects gen`. - -## Read the generated reference before you touch a tier -`metaobjects gen` writes ONE reference file under this project's docs output directory (when a target's generator list declares `api-docs`). It -is generated from THIS project's metadata, so it describes what the project actually is -rather than what the framework does in general — and reading it first is how you avoid -inferring the answer from generated code, which is the disposable half. There is no -separate docs command on this port either: the api reference is one more entry in the same -generator list every other generator is in. - -- **`api/python/AGENT-API.md`** — before calling generated code. The types, endpoints and - filter operators the generators actually emitted. - -Not there? Declare `api-docs` in that target's `generators` in metaobjects.config.yaml if it is missing, then run `metaobjects gen`. - -The other three pages of that reference — `agent/schema.md` (persistence), `agent/ui.md` -(forms and grids) and `agent/requirements.md` (what is already claimed, and what was -deliberately **retired**) — are written by the Node `meta gen` command, from -`agentDocsFile()` in a `metaobjects.config.ts`, which this stack does not have. Until it -does, read the metadata itself for those three: it is the source those pages are generated -from. - -## 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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. -- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. -- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. -- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. -- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. -- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. -- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. -- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. -- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. -- 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. -- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. - -## Authoring rules you must not violate -- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. -- Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### Python: generate with `metaobjects gen` + +1. **Config.** `metaobjects.config.yaml` names each target's `outDir` and its generators. Nothing + is generated until it is listed: + + ```yaml + targets: + app: + outDir: generated + generators: [entity, names] # Pydantic models + physical-name constants + ``` + + `routes` and `filter-allowlist` add FastAPI routers over a repository `Protocol` you + implement; `requirement-tests` writes one pytest file per package of requirements; + `api-docs` writes `api/python/AGENT-API.md`. `metaobjects gen --list` is the catalog. + Install what the output imports: `pip install "pydantic[email]>=2" fastapi`. +2. **Generate.** `metaobjects gen`. `metaobjects eject ` copies a generator into the repo + to change what it emits. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `metaobjects verify` (the generated code and the requirements; `--codegen` is its default). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md index ece01bdac0..8f925a267a 100644 --- a/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/python/expected/.metaobjects/CLAUDE.md @@ -3,79 +3,87 @@ > Stack: python server, no 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/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `metaobjects gen`. - -## Read the generated reference before you touch a tier -`metaobjects gen` writes ONE reference file under this project's docs output directory (when a target's generator list declares `api-docs`). It -is generated from THIS project's metadata, so it describes what the project actually is -rather than what the framework does in general — and reading it first is how you avoid -inferring the answer from generated code, which is the disposable half. There is no -separate docs command on this port either: the api reference is one more entry in the same -generator list every other generator is in. - -- **`api/python/AGENT-API.md`** — before calling generated code. The types, endpoints and - filter operators the generators actually emitted. - -Not there? Declare `api-docs` in that target's `generators` in metaobjects.config.yaml if it is missing, then run `metaobjects gen`. - -The other three pages of that reference — `agent/schema.md` (persistence), `agent/ui.md` -(forms and grids) and `agent/requirements.md` (what is already claimed, and what was -deliberately **retired**) — are written by the Node `meta gen` command, from -`agentDocsFile()` in a `metaobjects.config.ts`, which this stack does not have. Until it -does, read the metadata itself for those three: it is the source those pages are generated -from. - -## 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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. -- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. -- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. -- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. -- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. -- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. -- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. -- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. -- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. -- 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. -- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. - -## Authoring rules you must not violate -- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. -- Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### Python: generate with `metaobjects gen` + +1. **Config.** `metaobjects.config.yaml` names each target's `outDir` and its generators. Nothing + is generated until it is listed: + + ```yaml + targets: + app: + outDir: generated + generators: [entity, names] # Pydantic models + physical-name constants + ``` + + `routes` and `filter-allowlist` add FastAPI routers over a repository `Protocol` you + implement; `requirement-tests` writes one pytest file per package of requirements; + `api-docs` writes `api/python/AGENT-API.md`. `metaobjects gen --list` is the catalog. + Install what the output imports: `pip install "pydantic[email]>=2" fastapi`. +2. **Generate.** `metaobjects gen`. `metaobjects eject ` copies a generator into the repo + to change what it emits. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `metaobjects verify` (the generated code and the requirements; `--codegen` is its default). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/requirements.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/requirements.md new file mode 100644 index 0000000000..2bcebe9ec1 --- /dev/null +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-audit/references/requirements.md @@ -0,0 +1,154 @@ +# Requirements — the audit dimension + +This project declares `requirement.*` nodes, so the ledger is an auditable surface. Audit it +for **truthfulness**, not volume. A large ledger that lies is worse than a small one that +does not, because every later reader trusts it. + +Run `verify` first — the project's own: `meta verify`, `metaobjects verify`, +`mvn metaobjects:verify` or `dotnet meta verify`. Every port runs the same requirement +gate. It settles referential integrity mechanically — do not spend audit effort re-deriving +what a green run already proves. + +## Contents +- What verify has already proven (do not re-check by hand) +- Currency checks — run these before sampling +- What only a human or an agent reading the code can catch +- Scope — do NOT flag these as defects +- Reporting + +## What verify has already proven (do not re-check by hand) + +A run answers two different questions in two separate sections. Both are already-proven +ground; neither is worth an auditor's time. + +**The gate** settles referential integrity: links sit at or below the L4 floor, nesting +agrees with levels, `@status` values are legal, and references resolve (with dangling allowed +on `planned`, whose nodes do not exist yet). + +**The authoring lint** — printed under its own heading, advisory, and unable to change the +exit code — settles the naming and prose defects you would otherwise find by reading every +entry. **Only the Node `meta verify` prints it.** On a Python, Java, Kotlin or C# project +whose pipeline runs that port's `verify` alone, nothing below has been proven: run +the Node `meta verify` over the same metadata, or audit these by hand. + +| code | what a clean run has already proven | +|---|---| +| `WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE` | no `name` is blank, space-padded, or carries a `.` / `/` / `:` that breaks the dotted path or the generated stub's filename | +| `WARN_REQUIREMENT_NAME_READS_AS_PROSE` | no `name` is a sentence sitting in the address slot | +| `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT` | no `name` and `@statement` are the same sentence written twice | +| `WARN_REQUIREMENT_PROSE_EMPTY` | no `@statement` / `@counterexample` is declared-but-blank (the loader enforces presence, never content) | +| `WARN_REQUIREMENT_PROSE_DUPLICATED` | no `description` repeats `@statement` whole or as its opening sentence, and no `@counterexample` repeats it | +| `WARN_REQUIREMENT_INERT_DOC_SLOT` | no `summary` is set — nothing reads it, and `@statement` is already the required one-liner | +| `WARN_REQUIREMENT_TITLE_IS_AN_ID` | no `title` leads with a catalogue or ticket id | + +Three limits, and each puts something back on your list: + +- **It is one port's.** See above: establish which `verify` the project's CI runs before + crediting a green pipeline with the table. +- **The lint is mutable.** `--no-requirement-lint` / `META_NO_REQUIREMENT_LINT=1` silences the + advisory half while the gate still runs. **Establish whether the project mutes it** (§F of + the checklist asks this) — against a muted lint the whole table above proves nothing. +- **It reports only EXACT repeats.** A paraphrase — `description` restating `@statement` in + different words, a `@counterexample` that merely negates it — is invisible to the tool and + is a legitimate hand finding. + +`title` is deliberately NOT flagged as an inert slot; it is chartered as the entry's label and +is rendered (see item 6). Do not report a populated `title` as a defect. + +## Currency checks — run these before sampling + +A green gate proves the ledger's links resolve, not that the ledger keeps up with the code. +These three checks measure that, and the first one chooses the sample for item 1 below. + +**(a) Ledger stale.** From git history, take the commits that add a feature (a `feat:` / +`feat(...)` subject under Conventional Commits; otherwise merged feature branches or PRs +labelled as features) over the last six months or the last 50 such commits, whichever is +fewer. Report two numbers: the share of those commits that also change requirement metadata +(any file holding `requirement.*` nodes), and the count of feature commits since the most +recent change to that metadata. A share **under half**, or **more than ten** feature commits +since the last ledger change, is the finding `ledger-stale`, stating both numbers. The +feature commits that did not touch the ledger are the sample for item 1: read what each one +built and check that a requirement says so with a true status. + +**(b) Nothing implements a live claim.** Run the project's verify with the strict switch: +`meta verify --require-implementers`, `metaobjects verify --require-implementers`, +`mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` or +`dotnet meta verify --require-implementers` (all ports also read +`META_REQUIRE_IMPLEMENTERS=1`). Each `WARN_REQUIREMENT_NOTHING_IMPLEMENTS` on a `live` entry +is a finding (`live-claim-unimplemented`) naming the entry. State whether the project's own +gate passes the switch; if it does not and the ledger has no such entries, recommend adding +it so the ledger cannot fall behind again. + +**(c) Live claim, unwired implementer.** For each `@implementedBy` target of a `live` entry, +find the generated code that target produces and confirm hand-written code imports or calls +it. A live claim whose implementer is generated code nothing uses is the finding +`live-claim-unwired-implementer`: the claim resolves, so verify is satisfied, but the +capability is not reachable. Before raising it, rule out a runtime use: code that reaches the +entity through the runtime `ObjectManager` (or a metadata-driven admin or tool registry) +uses it without importing anything generated, so search for the entity's name in those calls +too. + +## What only a human or an agent reading the code can catch + +**1. Statuses that are false.** The highest-value finding in the whole dimension. A +requirement marked `live` whose implementation was gutted; one marked `partial` that is now +complete; one marked `planned` that was quietly built months ago. Sample the claims +and read the nodes, starting with the feature commits currency check (a) found outside the +ledger. `status` is the only payload with controlled evidence behind it — the +resurrection protection rides entirely on it being true. + +**2. Non-violable statements.** Every requirement must state what breaking it looks like. +*"Every entity has a uuid primary key"* is violable — point at one with a composite key. +*"Things are persisted"* is not; it is a description wearing a requirement's shape. Flag +these: they inflate the ledger while proving nothing. + +**3. Claim-padding.** An entity appended to an existing unrelated requirement's +`implementedBy` purely to silence the coverage warning. The claim resolves, so verify is +happy, but it carries no information about what the entity is *for*. Look for entities whose +only claim is a high-fan-out architectural rule when they plainly have product meaning. + +**4. Misfiled kind.** `functional` is checked by EXISTENCE, `architectural` by UNIVERSALITY — +opposite polarity, and nothing mechanical catches a misfile. The discriminator: did this +exist because someone asked for something (functional), or because every entity here looks +like this (architectural)? An architectural entry with one claimant is usually a misfiled +functional one. + +**5. Levels used as decoration.** L1–L3 are organisational and must never reference the +model. If the tree is flat, or every entry is L3, the levels are carrying no information and +should be simplified rather than defended. + +**6. Titles that are not labels.** `title` is now **rendered** — `meta gen`'s +`requirementsFile()` heads each entry `## checkout.payment — Payment capture`, the path +first and the label after it — so a slot +that used to be inert is read by every reader of the generated page. The lint catches only +the id-shaped case (`FR-448 …`); it cannot tell a *useless* label from a good one. Read the +titles: one that restates `@statement`, or repeats the path in prose, is now visible noise in +a heading rather than a private authoring habit. An absent `title` is NOT a finding — the +entry heads by its path alone, which is what every sibling surface addresses it by. + +**7. Witnesses that do not test the claim.** Where the project generates requirement tests +(the `requirement-tests` generator, in any port), a passing generated test proves only that +its witness — or, in TypeScript, the hand-filled stub — ran. Sample the witnesses and read +them against the counterexample each generated test carries: one that asserts nothing, or asserts +something else, is the same defect `@verifiedBy` had. Check also for witnesses that have +gone stale silently: a Java witness that lacks the override annotation, a C# witness implemented +implicitly, and any Python witness keep existing after their requirement is retired or +deleted, and nothing reports them. + +## Scope — do NOT flag these as defects + +- **Unclaimed `object.value` / `object.projection`.** Exempt by design: a value is a shape, a + projection derives from a claimable entity. +- **Fields, views, validators and identities without their own requirement.** Member-grain + coverage is explicitly rejected — plumbing members are covered by architectural + requirements with high fan-out. L5 exists so a member claim *can* be made where it means + something, never so every member must carry one. +- **Dangling links on `planned`.** Correct — the plan precedes the nodes. +- **A project with no requirements at all.** The feature is opt-in by declaration; absence is + not a finding. + +## Reporting + +Report findings as claims to verify, not as a score. "These 4 requirements are marked `live` +and I could not find their implementation" is actionable; "ledger maturity: 62%" is not, and +invites optimising the number. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md index 1979d161f2..de11bce2cc 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/SKILL.md @@ -38,7 +38,7 @@ This file covers what almost every model needs. The topics below live in | `references/inheritance-tph.md` | several entities are variants of one thing sharing a single table (`@discriminator`) | | `references/metadata-dependencies.md` | the project builds on another package's metadata (`dependencies`, cross-package `overlay`) | | `references/reporting.md` | what a declared report gives you: what columns a report gets, time grains, null rules, rows with no facts (`@spine`), defaults (`@default`), engine differences | -| `references/requirements.md` | installed only when the project declares `requirement.*` nodes | +| `references/requirements.md` | you add, change or review a `requirement.*` node | ## The operating principle: model-first, generate-first @@ -483,7 +483,7 @@ Every requirement must be **violable**: if you cannot say what breaking it looks **A commit that adds, changes or removes behaviour updates the ledger in the same commit** (the new or changed claim, its `@status` and `@implementedBy`), or says in the commit message why it does not. `verify` checks that links resolve, not that the ledger is current. -If the project declares any `requirement.*` node, `references/requirements.md` is installed with the full authoring rules. If it does not and you are adding the first one, the shape is: +`references/requirements.md` has the full authoring rules. The shape of one node: ```yaml - requirement.architectural: diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/requirements.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/requirements.md new file mode 100644 index 0000000000..abd10504a0 --- /dev/null +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-authoring/references/requirements.md @@ -0,0 +1,225 @@ +# Requirements — `requirement.functional` / `requirement.architectural` + +Capabilities are **metadata**, declared in the metadata directory beside the entities they +describe. Read the existing requirement nodes before designing anything. Two rules matter +more than the rest. + +## Contents +- A requirement is PRESCRIPTIVE — it states what should be true, never what happened +- Do not delete a retired capability's entry +- When you add an entity, claim it +- A feature commit updates the ledger in the same commit +- Every requirement states its counterexample +- Four prose slots, and `statement` is the one that means "description" +- `title` is a LABEL and is chartered here; `summary` is not +- Never put a catalogue or ticket id in `title` +- The `name` is an address, so write it as an identifier +- Hierarchy is nesting, and links live at the bottom +- Write an L5 ref as a dotted `pkg::Owner.member` path +- L1–L3 are levels of abstraction and ownership in the problem domain +- Architectural requirements are the other kind +- Record gaps rather than rounding them off +- `status: planned` locks in work you have not started +- Which `verify` says what +- A requirement's name and package end up in generated test names + +**1. A requirement is PRESCRIPTIVE — it states what should be true, never what happened.** +`status` is `planned | live | partial | retired`, and every one of them states something +meant to be true. `retired` is not the exception: it means **"this was built, then +deliberately removed, and must not be rebuilt"** — a prohibition in force, falsifiable by +exactly one observable, the capability reappearing. + +So write a retired entry as a rule, not a diary. `statement: "An unpaid order is never +expired by a wall-clock timer"` is a prohibition; `"We used to expire orders on a timer"` is +history wearing a requirement's shape, and no gate will catch it. On a retired entry +`counterexample` describes **the revival** — the thing a future reader is about to propose. + +**Do not delete a retired capability's entry.** Keeping it is the single most load-bearing +thing this vocabulary does: given a brief for a capability that had been retired, agents +working from the model alone proposed rebuilding it 24 times out of 24. The entry is what +stops that, so a retirement is a status change plus a rewritten statement, never a deletion. + +Three rules the loader enforces on `retired`: + +- **`implementedBy` is REFUSED** — a retired capability has no implementation by definition. + What used to implement it goes in `notes`. If deleting the references feels wrong, the + capability is probably not retired: an entry whose nodes are still there is `live` or + `partial`. +- **`supersededBy` names the requirement that replaced it**, legal here only, and RESOLVED — + so the chain still works when the replacement is itself retired later. +- It never counts toward object coverage, and is exempt from the architectural universality + check. + +Leaving a dangling `implementedBy` is **correct only on `planned`**: the entry precedes the +nodes. On `live` or `partial` the same dangling reference is an error — the model moved and +the requirement went stale. + +**2. When you add an entity, claim it.** Every `object.entity` should appear in some +requirement's `implementedBy`, or `verify` says so. + +**A commit that adds, changes or removes behaviour updates the ledger in the same commit.** +`verify` checks that the ledger's links resolve, never that it is current, so nothing fails +when a feature ships and its requirement does not move. In the same commit, write the new or +changed claim, set its `@status` (`planned` to `live` when it lands, `retired` when it is +removed) and keep its `@implementedBy` pointing at the real nodes (a retired entry carries +none). If the commit should not +touch the ledger, say why in the commit message. + +**Every requirement states its counterexample.** One sentence: what breaking it looks like. +*"Every entity has a uuid primary key"* is violable — point at one with a composite string +key. *"Things are persisted"* is not, and is a description rather than a requirement. Same +rule kills *"the system is reliable"*. If you cannot say what breaking it looks like, +delete it. + +**Four prose slots, and `statement` is the one that means "description".** A requirement can +also carry the common `description` and `notes`, and they overlap badly unless you decide the +split before writing any of them: + +- `statement` — **the claim**. This IS the description of what the requirement is +- `counterexample` — **what would falsify the claim**, which is what makes it checkable +- `description` — **the scope**: what the claim covers, what it deliberately does not, and + which sibling entry owns the rest +- `notes` — **the evidence**: how you know the `status` is true — citations, vocabularies, the + control you ran to prove an absence was real + +**`title` is a LABEL and is chartered here; `summary` is not.** A requirement's `name` is an +identifier and its address renders as a dotted camelCase path, so a short noun-phrase `title` is +what an index shows — the requirement attribute table in `spec/capability-ledger.md` says so by +name, and the generated requirements page renders it in the heading after the path +(`## checkout.payment — Payment capture`). `summary` is different: `statement` is already the +required one-line sentence, so a `summary` can only repeat it and nothing reads it (`verify` warns, +`WARN_REQUIREMENT_INERT_DOC_SLOT`). `notes` is unrendered on purpose — chartered internal-only. + +**Never put a catalogue or ticket id in `title`.** A title is a noun phrase; an id is not a name. +`title: "FR-467 — Order recording"` is two things in one slot — put the id in `trackedBy` and +keep the phrase as the title. `verify` warns (`WARN_REQUIREMENT_TITLE_IS_AN_ID`). + +**The `name` is an address, so write it as an identifier.** It is the segment of the dotted +path (`Ordering.Placement.Recorded`) and the filename of the generated test stub. A `.` in a +name is indistinguishable from nesting — `Orders.Recorded` and `Orders` containing `Recorded` +produce the same path — and a sentence for a name puts the claim somewhere nothing reads it. +Both load; `verify` warns (`WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE`, +`WARN_REQUIREMENT_NAME_READS_AS_PROSE`, `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT`). + +Two failure modes, both of which look like diligence. A `description` that **paraphrases the +statement** is padding, and it makes every later reader trust the ledger less — leave it off +instead, it is optional. A `description` that **narrates the evidence** belongs in `notes`; +the tell is a fact you had to read the implementation to learn. Mechanical test for the last +line: *would this sentence have to change if the code changed but the model did not?* Then it +is `notes`. `verify` warns on the exact repeats (`WARN_REQUIREMENT_PROSE_DUPLICATED`), and on +a `statement` or `counterexample` that is present but blank (`WARN_REQUIREMENT_PROSE_EMPTY`) — +the loader requires those attrs to exist, never to say anything. + +**Hierarchy is nesting, and links live at the bottom.** L1 solution, L2 segment, L3 +service — these never reference the model. **L4** binds a declared top-level node — an +`object.*` **or a `template.*`** — and **L5** binds a member of one: a field, view, +validator, identity, or a template's child. `implementedBy` above L4 is an error. +Regrouping *moves* a node; it does not edit a parent string. + +**Write an L5 ref as a dotted `pkg::Owner.member` path** — the level and the shape of the +ref must agree, and this is the single easiest thing to get wrong: + +```yaml +# L4 binds the OBJECT +level: 4 +implementedBy: ["acme::orders::Order"] + +# L5 binds ONE MEMBER of it — dotted owner.member, not the object +level: 5 +implementedBy: ["acme::orders::Order.placedAt"] +``` + +Both mismatches are caught and they are symmetric: an L5 whose ref names an object is +`ERR_REQUIREMENT_L5_NOT_MEMBER`, an L4 whose ref names a member is +`ERR_REQUIREMENT_L4_NOT_OBJECT`. **In both cases the fix is to move the entry to the other +level, not to rewrite the ref** — the ref is usually right and the level is usually the +mistake. The instinct that produces the error is reaching for the object a rule is *about*; +what L5 wants is the member the rule is *carried by*. Copying an L4 block and changing only +`level: 5` produces exactly this failure. + +Reach for L5 when the statement is about ONE column and its entity's own requirement could +not express it — "this lifecycle is `resolvedAt IS NULL` and never `status`", "this id is +nullable, which is why the parent is `partial`". Do NOT use L5 for a blanket rule that +happens to touch many fields; that is an architectural requirement. + +Claim your prompts. A `template.prompt` is a model node realising a capability exactly as +an entity is, and it is the node whose retirement is hardest to see later — a removed +prompt leaves no table behind. A prompt estate with no requirement entries is the same +blind spot this whole mechanism exists to close. + +**L1–L3 are levels of abstraction and ownership in the problem domain** — whose need is +this, and at what altitude — and are NEVER a directory, package, deployable or module. +Technical constructs appear only at L4/L5, which is the allocation step. Test every node: +*if a refactor that changes no behaviour would force it to move, its level is wrong.* + +**Architectural requirements are the other kind.** `requirement.architectural` is flat by +default — a uuid-PK rule, change attribution, tenant scoping. Its check is *universality* +rather than existence, so one that is `live` and claimed by nothing fails: a policy +declared and applied to nothing. A `@level` is OPTIONAL here and opts the node into a tree +(for organising non-functional requirements under a quality taxonomy); once levelled, the +same nesting and link-floor rules apply as to a functional node. + +`@status` is a closed enum enforced by the loader, so a typo fails the load rather than +silently disabling the entry. + +**Record gaps rather than rounding them off.** `partial` says *this works and here is what +is wrong with it*, and it is the most useful status in the enum — a ledger with none is +usually one nobody read carefully. Then say what was DECIDED, which is a separate question: + +- `@disposition: accepted` — understood, deliberately not being closed +- `@disposition: deferred` — will be closed, not now (name a ticket in `@trackedBy`, or + `verify` warns; deferring without one is how a known problem becomes an unknown one) +- **absent** — undecided, and that is a real state. `verify` counts these, because + *"which gaps has nobody ruled on?"* is the question a review exists to answer. + +A `partial` nobody intends to finish is `partial` + `@disposition: accepted` — the gap is +understood and deliberately not being closed. If the capability itself is gone, delete the +requirement instead. + +**`status: planned` locks in work you have not started.** Its references may dangle (write +the requirement before the entity), and it never counts toward object coverage — otherwise +declaring an intention would clear the unclaimed-entity warning and the gate would measure +ambition rather than work. + +```yaml +- requirement.functional: + name: Pacing + level: 3 + status: live + statement: "Scene pacing follows story beats" + counterexample: "A scene that advances on a clock rather than on the story" + children: + - requirement.functional: + name: BeatProgression + level: 4 + status: planned # not built yet -- refs may dangle + statement: "A scene advances when its beat completes" + counterexample: "A scene that advances with its beat unresolved" + trackedBy: ["acme/game#412"] + implementedBy: ["game::turn::BeatProgression"] # does not exist YET + +- requirement.architectural: + name: UuidPrimaryKeys + status: live + statement: "Every entity has a uuid primary key" + counterexample: "An entity keyed by a composite string" + implementedBy: ["game::turn::Turn", "game::world::Location"] +``` + +**Which `verify` says what.** The gate — dangling references, links above the floor, +level and nesting errors, an unclaimed entity, a live requirement nothing implements — runs +in every port's `verify` (`meta verify`, `metaobjects verify`, `mvn metaobjects:verify`, +`dotnet meta verify`). The authoring warnings named on this page (`WARN_REQUIREMENT_NAME_*`, +`WARN_REQUIREMENT_PROSE_*`, `WARN_REQUIREMENT_INERT_DOC_SLOT`, +`WARN_REQUIREMENT_TITLE_IS_AN_ID`) are printed by the Node `meta verify` only, so on another +port follow these rules without waiting for a warning. + +**A requirement's name and package end up in generated test names.** Every port's +`requirement-tests` generator names a test from the requirement's package and dotted path +(`acme::shop::Orders.Recorded` becomes `req_acme_shop_Orders_Recorded…`), and two +requirements whose names differ only in punctuation (`Orders.Recorded` beside +`Orders_Recorded`) are refused as a collision outside TypeScript. Another reason to keep +names plain identifiers. + +There is **no `satisfies:` on a field or entity** — links live on the requirement node, not +on the nodes it claims. Full reference: the repo's `spec/capability-ledger.md`. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md index 36123e0d8f..a337e663e3 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/SKILL.md @@ -35,6 +35,7 @@ exact config, generator names and commands for this project; read it. The other | File | Open it when | |---|---| +| `references/principles.md` | a rule in the always-on file does not settle the case: the full standing principles (adopting onto existing code, the schema and live database, prompts, requirements, owning and writing generators, physical names) with their reasons | | `references/any-stack.md` | **the database, data store, HTTP framework or UI framework is not one a reference generator targets** — MySQL, MongoDB, DynamoDB, Express, NestJS, Next.js, Vue, … | | `references/owning-generators.md` | generated output is wrong; your framework is not the one a reference emits for; you write or review a generator (the `own*()` accessor table) | | `references/choosing-layers.md` | the generated API does not fit, you need joined or derived data, or you are adopting onto existing code | diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md new file mode 100644 index 0000000000..2c2f61d72d --- /dev/null +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/principles.md @@ -0,0 +1,36 @@ +# Standing principles + +The rules that hold in every MetaObjects project, whatever the stack and whatever the task. The +always-on file in `.metaobjects/` carries the short form; this page is the full text, with the +reasons. Read it when a rule there does not settle the case in front of you. + +## Contents + +- Principles +- Authoring rules you must not violate + +## 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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. +- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. +- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. +- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. +- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. +- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. +- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. +- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. +- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. +- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. +- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. +- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. +- 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. +- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. + +## Authoring rules you must not violate + +- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. +- Nodes are fused-key maps: `{".": { ... }}` (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`). diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md index 57b556e1f6..fcdb550249 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/SKILL.md @@ -334,4 +334,5 @@ what's wrong. --- -For the migration tooling read `references/migration.md`. +For the migration tooling read `references/migration.md`. Before upgrading MetaObjects, or +when a project runs more than one port, read `references/upgrading.md`. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md index f589708490..ff97eb6b8e 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/migration.md @@ -18,6 +18,7 @@ stays native to the language. - Install (Node, dev-only) - Output format - The workflow +- Renames and destructive changes - Dialects - Live-DB drift: `meta verify --db` - Index modeling (Postgres) @@ -109,8 +110,42 @@ meta migrate baseline --from-db --db postgresql://... --dialect postgres 2. **Review the SQL.** Read the emitted `up.sql` (forward) and `down.sql` (rollback) before applying. Destructive changes (drop column / drop table) are - opt-in — the engine blocks them unless explicitly allowed, and routes ambiguous - rename-vs-drop+add decisions through a prompt rather than guessing. + opt-in — the engine refuses them until you allow each kind by name (`--allow`, + below), and an ambiguous rename-vs-drop+add aborts unless you declare the rename + or choose with `--on-ambiguous`. It never guesses. + +## Renames and destructive changes + +A renamed field or object is a renamed column or table, and the diff cannot tell that +from a drop plus an add unless the names are close. Declare the rename and the data is +kept: + +```bash +meta migrate --dialect postgres --slug rename-origin --rename-column shipment.origin_city=origin_port +meta migrate --dialect postgres --slug rename-stop --rename-table stop=port_call +``` + +- `--rename-column [schema.]table.old=new` and `--rename-table [schema.]old=new` are + repeatable. For a column, `table` is the table's name in the metadata, so it is the + **new** name when the table is renamed in the same run. A declared rename needs no + `--allow drop-column`, and one that finds nothing to rename is refused rather than + falling back to drop and add. Change a column's type, nullability or default in a + later run, not in the rename's. +- `--allow ` permits a destructive change by kind. A refusal names the token it + needs. The tokens: `drop-column`, `drop-table`, `type-change`, `drop-index`, `drop-fk`, + `drop-check`, `drop-view`, `drop-view-cascade`, `drop-column-read-by-view`, + `adopt-view`, `nullable-to-not-null`, `drop-identity-default`, `drop-comment`, + `drop-unmanaged`. Changing an enum's values replaces its CHECK, so it needs + `--allow drop-check`. `drop-column` deletes the column's data: for a rename use + `--rename-column`, never `--allow drop-column`. +- `--on-ambiguous abort|rename|drop-add` (default `abort`) decides a pair the + heuristic offers as a possible rename. Declaring the rename is clearer than `rename`. +- `migrate.allow` and `migrate.onAmbiguous` in `.metaobjects/config.json` set the same + things for every run. Prefer the flag: a standing allowance hides the next destructive + change from review. + +`docs/features/migrations-and-drift.md`, "Renames and populated tables", covers what a +rename carries with it (constraint names, views) and changes that need a backfill. 3. **Apply** the pending migrations against the DB; migration history is tracked in a ledger table: diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/requirements.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/requirements.md new file mode 100644 index 0000000000..e859076180 --- /dev/null +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/requirements.md @@ -0,0 +1,218 @@ +# Requirements — what `meta verify` checks + +This project declares `requirement.*` nodes, so `verify` checks them. **There is no +subverb**: requirements are metadata, so they are checked on *every* `meta verify` run. + +## Contents +- Which command runs the gate +- The split, and why it matters when you read a failure +- A load failure on RETIRED vocabulary — run `meta upgrade`, do not hand-sweep +- The status asymmetry — the one that surprises people +- Exit codes +- The strict switch — `--require-implementers` +- Keeping the ledger current +- The error codes, and the fix for each +- The authoring lint — a second section, never an error +- `verify` does not look at your tests +- What a green run does NOT prove + +## Which command runs the gate + +Every port's `verify` runs the same gate, with the same codes, severities and message +text (a shared corpus pins them). Run the one for the project's server language: + +| port | command | +|---|---| +| TypeScript | `meta verify` | +| Python | `metaobjects verify` | +| Java, Kotlin | `mvn metaobjects:verify` (one goal for both) | +| C# | `dotnet meta verify` | + +The gate is core: it is not a generator and cannot be ejected or replaced. What differs +by port is the **authoring lint** further down this page, which only the Node `meta verify` +prints. This page writes `meta verify` throughout; read it as the port's own command. + +## The split, and why it matters when you read a failure + +| | owns | +|---|---| +| **loader** | `@status` enum, required attrs, child rules, levels — unconditional | +| **`verify`** | `@implementedBy` resolution — **severity depends on `@status`** | + +A typo'd `@status` fails the **load** ("failed to load metadata"), before verify runs. If you +see that, no other diagnostic in the run is trustworthy — fix it first and re-run. + +## A load failure on RETIRED vocabulary — run `meta upgrade`, do not hand-sweep + +`@status` is not the only way the load fails before any of this runs. **0.24.0** retired +`@violation` (→ `@counterexample`) and `@verifiedBy`; **0.24.1** made an index key `@fields` +**XOR** `@expr`, so a node declaring both is now `ERR_INVALID_INDEX`. **0.24.2 reversed half +of the 0.24.0 cut**: `abandoned` and `superseded` become **`@status: retired`**, and +`@supersededBy` is registered again — this time as a reference the loader resolves. There is no deprecation shim for any of them — +the registry is sealed (ADR-0023), so a legacy document does not load at all and every check on +this page is unreachable until it does. + +``` +meta upgrade # previews every rewrite; writes nothing +meta upgrade --apply # makes them +``` + +It rewrites from the same table the loader's error text is generated from, so the fix you are +told about and the edit the tool makes cannot drift apart. It fixes only what has one correct +answer and **refuses the rest, exiting non-zero** — a partial migration can never be recorded +as finished by CI. Two refusals are expected on a real ledger and both are yours to decide: + +- **`@status: abandoned` / `superseded` are no longer refused** — they rewrite to `retired`, + and the same run drops the `@implementedBy` that a retired entry may not carry. This was the + canonical judgement case in 0.24.0; FR-039 made it determinate. +- **`origin.collection`.** Retired to `origin.aggregate @agg: collect`; the attribute sets + differ, so the tool will not guess. + +`@fields` beside `@expr` **is** rewritten, and the survivor is not a coin toss: that pair loaded +before 0.24.1 with `@fields` silently discarded, so the index already in your database is the +expression one. `upgrade` drops `@fields` — which reproduces the object that exists and emits no +DDL change. Do not hand-pick the other survivor; that invents a new index and migrates live data. + +## The status asymmetry — the one that surprises people + +The **same** unresolved `@implementedBy` reference is: + +- an **error** on `live` / `partial` — the model moved and the requirement is stale; +- **allowed** on `planned` — those nodes do not exist YET. That is + the entry doing its job, not drift. + +So on a `planned` entry a dangling reference is the entry doing its job, not drift — do not +"fix" it by deleting the reference, or you delete the plan. + +## Exit codes + +| situation | exit | +|---|---| +| clean tree, or no `requirement.*` nodes at all | 0 | +| dangling `@implementedBy` on `live`/`partial` | 1 | +| the same reference on `planned` | 0 | +| `@implementedBy` above the L4 link floor | 1 | +| live `requirement.architectural` claimed by nothing | 1 | +| an entity no requirement claims | 0 (warning) | +| a `live`/`partial` functional requirement nothing implements | 0 (warning); **1 under the strict switch** | + +## The strict switch — `--require-implementers` + +`WARN_REQUIREMENT_NOTHING_IMPLEMENTS` (a `live` or `partial` functional requirement where +neither it nor anything nested under it names a node) is a warning by default. A project +whose ledger has caught up with its links can make it fail the build: + +| port | flag | environment | +|---|---|---| +| TypeScript | `meta verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Python | `metaobjects verify --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | +| Java, Kotlin | `mvn metaobjects:verify -Dmeta.verify.requireImplementers=true` | `META_REQUIRE_IMPLEMENTERS=1` | +| C# | `dotnet meta verify … --require-implementers` | `META_REQUIRE_IMPLEMENTERS=1` | + +It raises that one finding to an error and **keeps its code**. Every other warning stays a +warning. If CI went red on this code, check whether the switch is set before treating it +as a new defect: the fix is to add the `@implementedBy` link (or drop the entry to +`planned`), not to unset the switch. + +Once the ledger is clean, set the switch where the project already runs `verify`: its verify +script, CI job or pre-push hook. Until then a stale `live` claim is only a warning that +nobody reads. + +## Keeping the ledger current + +`verify` proves the ledger's links resolve, not that the ledger is current. A commit that +adds, changes or removes behaviour updates the ledger in the same commit: the new or changed +claim, its `@status` and its `@implementedBy`. If the commit should not touch the ledger, the +commit message says why. + +## The error codes, and the fix for each + +Match on the stable `code`, never the message text (ADR-0009). The runtime messages already +name the remedy; this table exists so you can act on one without re-deriving the rule. + +| code | what it means | fix | +|---|---|---| +| `ERR_REQUIREMENT_DANGLING_REF` | an `@implementedBy` ref does not resolve in the loaded model, on a status that requires live nodes | the model moved and the claim is stale — repoint the ref, or change the status if the capability really went away. **Check the ref is not merely unloaded**: a source missing from `sources` makes a live node look deleted. | +| `ERR_REQUIREMENT_L5_NOT_MEMBER` | an L5 entry's ref names an OBJECT | move the entry to **L4**, or repoint to a member as `pkg::Owner.member`. Moving the level is usually right — the ref is usually what you meant. | +| `ERR_REQUIREMENT_L4_NOT_OBJECT` | an L4 entry's ref names a MEMBER | the symmetric case — move the entry to **L5**, or repoint at the owning object. | +| `ERR_REQUIREMENT_LINK_ABOVE_FLOOR` | `@implementedBy` on an L1–L3 node | L1–L3 are problem-domain altitude and never reference the model. Push the claim down to the L4/L5 child that actually carries it. | +| `ERR_REQUIREMENT_ARCH_NO_IMPLEMENTERS` | a `live`/`partial` `requirement.architectural` that nothing implements | a policy declared and applied to nothing. Claim the nodes it governs, or drop it to `planned` — which is exempt, because it is not applied yet by definition. | +| `ERR_REQUIREMENT_LEVEL_NESTING` | a node's `@level` disagrees with the level of the parent it nests under | nesting IS the hierarchy. Move the node to the right parent rather than editing the level to match where it happens to sit. | +| `ERR_REQUIREMENT_BAD_LEVEL` | `@level` is not an integer inside the allowed range | levels are L1–L5 and nothing else. | +| `ERR_REQUIREMENT_RETIRED_HAS_IMPLEMENTORS` | a `@status: retired` entry declares `@implementedBy` | a retired capability has no implementation by definition. Delete the attribute and put what used to implement it in `notes`; if the nodes are still there, the capability is `live` or `partial`, not retired. | +| `ERR_REQUIREMENT_SUPERSEDED_BY_NOT_RETIRED` | `@supersededBy` on a status other than `retired` | it names what REPLACED a withdrawn capability, so on a live one there is nothing for it to say. | +| `ERR_MISSING_REQUIRED_ATTR` | a required attr is absent | `@statement`, `@status` and `@counterexample` are required on both subtypes; `@level` is required on `functional` and optional on `architectural`. | +| `ERR_BAD_ATTR_VALUE` | a closed-enum attr has an unknown value | `@status` and `@disposition` are enforced by the LOADER, so a typo fails the load in every port rather than passing in some. | + +**A note on the L4/L5 pair.** They are enforced at the same site and fail for opposite +reasons, so reading only the code you hit can send you the wrong way. The question is not +"is this ref valid?" — usually it is — but "does the LEVEL match the shape of the ref?" + +## The authoring lint — a second section, never an error + +The Node `meta verify` also prints an **authoring lint** under its own heading, after the +gate's own warnings. It is TypeScript-only: `metaobjects verify`, `mvn metaobjects:verify` +and `dotnet meta verify` run the gate above and never print these codes, so on those ports +a clean run says nothing about them. + +``` +meta verify — requirements: 6 authoring warning(s) (advisory — does not fail the build): +``` + +Read it as a different claim from everything above. The gate says the ledger **disagrees with +the model**; the lint says it agrees but **records less than its author thinks**. Every finding +is a warning and none can change the exit code, so a lint-only run is a passing run. + +| code | what it means | fix | +|---|---|---| +| `WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE` | the `name` holds a character that breaks the dotted path or the generated stub filename | rename it. A `.` is the common one and the worst: `Orders.Recorded` and `Orders` containing `Recorded` produce the **same** path, so the address stops identifying one node. | +| `WARN_REQUIREMENT_NAME_READS_AS_PROSE` | the `name` is a sentence | the name is an address — make it an identifier and put the prose in `@statement`. | +| `WARN_REQUIREMENT_NAME_RESTATES_STATEMENT` | `name` and `@statement` say the same thing | the claim is written twice. Keep `@statement` (every surface reads it) and shorten the name. | +| `WARN_REQUIREMENT_PROSE_EMPTY` | `@statement` or `@counterexample` is present but blank | the loader requires the attr to EXIST, never to say anything. Write the sentence, or delete the entry. | +| `WARN_REQUIREMENT_PROSE_DUPLICATED` | `description` repeats `@statement` (whole, or as its opening sentence), or `@counterexample` does | `@statement` is already the description. `description` holds the SCOPE; drop it entirely if the scope is obvious. | +| `WARN_REQUIREMENT_INERT_DOC_SLOT` | `summary` is set on a requirement | `@statement` is already the required one-line sentence, so a summary can only repeat it, and nothing reads it. Delete it. (`title` is NOT flagged — it is chartered as the entry's label.) | +| `WARN_REQUIREMENT_TITLE_IS_AN_ID` | `title` holds a catalogue or ticket id | a title is a noun phrase and an id is not a name. **Split** it — the id to `@trackedBy`, the phrase stays the title. Do not move the whole string; that throws the label away. | + +Findings from both sections are addressed by the requirement's **dotted path**, never its +bare name — two branches of a ledger may reuse a name, so a bare one can be ambiguous. + +Mute the lint with `--no-requirement-lint` or `META_NO_REQUIREMENT_LINT=1`. That silences the +advisory half **only** — the gate above still runs and can still exit 1. + +Two deliberate silences. The lint reports only **exact** repeats, never a paraphrase — a +similarity threshold on prose produces findings you can argue with. And it never judges +whether a statement is true or a counterexample sufficient; those are the judgements the +ledger exists to record. + +## `verify` does not look at your tests + +It used to. `@verifiedBy` asked you to name a test, and `verify` checked that the **name** +occurred somewhere in your test sources. That is existence evidence, never proof — an audit of +one real 19-name ledger opened every named test and found **4 that did not verify their +claim**: one matched a comment, one a dependency-injection key, one a real test of a +*different* claim, and one a test of the entry's output where the claim was about its source +text. `verify` reported zero errors throughout. The author picks the string, so the cheapest +way to satisfy the check is to find any name that already exists. + +The attribute is retired. Tying a requirement to a test is the job of a generator that emits +the test **from** the requirement, so the link is structural rather than a name someone chose. +Every port ships one, `requirement-tests` (see the codegen skill's reference for the +project's language). `verify` still never reads test results: the codegen drift gate +(`verify --codegen`) proves the generated tests match the ledger, a live test with nothing +behind it fails in the project's own test run, and a generated test that passes proves its +witness ran, not that the witness tests the claim. + +## What a green run does NOT prove + +It proves **referential integrity**: statuses parse, levels are in range, links sit at or +below the floor, references resolve. + +It cannot prove a status is **true**, or that a node genuinely implements the requirement +claiming it. No test can. That judgement is yours. + +Coverage is also narrower than the name suggests: it is checked at **entity grain only** — +`object.value` and `object.projection` are exempt, and fields, views, validators and +identities are never required to be claimed. Green means "every entity is claimed by +something", not "every node is described". + +Full reference: the repo's `spec/capability-ledger.md`. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/upgrading.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/upgrading.md new file mode 100644 index 0000000000..23ccb45965 --- /dev/null +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-verify/references/upgrading.md @@ -0,0 +1,10 @@ +# Upgrading MetaObjects and keeping the ports in sync + +Read this before you move a project to a new MetaObjects release, and when a project runs more +than one port (a TypeScript client beside a Java backend, say). + +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. + +`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. + +**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md index 2b67e4ec0b..f71ff6288e 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/AGENTS.md @@ -3,82 +3,104 @@ > 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/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `npx meta gen`. - -## Read the generated reference before you touch a tier -`meta gen` writes four reference files under this project's docs target -(`targets.docs.outDir` in `metaobjects.config.ts`) when `generators` lists the generator -for each — `agentDocsFile()` for the three `agent/` pages, `apiDocsFile()` for the api -reference. There is no separate docs command: documentation is one more entry in the same -list every other generator is in. They are generated from THIS project's metadata, so they -describe what it actually is rather than what the framework does in general — and reading -the relevant one first is how you avoid inferring the answer from generated code, which is -the disposable half. - -- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each - column came from, keys, foreign keys, constraints. -- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter - operators the generators actually emitted. -- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and - filterability per field, and each declared grid. -- **`agent/requirements.md`** — before adding a capability. What is already claimed, and - what was deliberately **retired** — an entry saying a capability must not be rebuilt is - the one thing you cannot learn from the code, because the code is gone. - -Not there? List the missing generator — and a `docs` target, if `targets` has none — in -`generators`, then run `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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. -- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. -- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. -- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. -- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. -- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. -- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. -- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. -- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. -- 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. -- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. - -## Authoring rules you must not violate -- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. -- Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### TypeScript: generate with `npx meta gen` + +1. **Config.** In `metaobjects.config.ts` set `dialect` (`"postgres" | "sqlite" | "mysql"`; D1 is + `"sqlite"`) and list the generators. Nothing is generated until it is listed. The usual data + layer: + + ```ts + import { entityFile, namesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; + // generators: [entityFile(), namesFile(), barrel()] + ``` + + Per entity: `.ts` (Drizzle table, row types, Zod schemas, enum types) and + `.names.ts` (physical names), plus an `index.ts` barrel. Install what they import: + `npm i drizzle-orm zod` and your database driver. Every other generator (CRUD `queriesFile()` + / `routesFile()`, docs, `requirementTests()` for a test stub per requirement) is one line too: + + ``` + npx meta gen --list --probe 2>/dev/null | grep -E "^ - name|wouldEmit| import:" | paste - - - + ``` + + `npx meta eject ` copies one into `codegen/generators/` to change it; + `npx meta generator new ` writes a new one. +2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `. +3. **Reference pages.** `agentDocsFile()` and `apiDocsFile()` with `targets: { docs: { outDir: + "docs" } }` write `agent/schema.md`, `agent/ui.md`, `agent/requirements.md` and + `api/AGENT-API.md`. When they exist, read the one for the tier you touch. + +### Web client: generated by the Node `npx meta gen` + +The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` +from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from +`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and +`@metaobjectsdev/tanstack`. Wrap the app in ``: +`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every +hook without an error. The `metaobjects-runtime-ui` skill has the detail. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `npx meta verify --codegen` (the generated code and docs pages; the requirement gate runs on every `meta verify`). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md index 2b67e4ec0b..f71ff6288e 100644 --- a/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md +++ b/fixtures/agent-context-conformance/ts-react-tanstack/expected/.metaobjects/CLAUDE.md @@ -3,82 +3,104 @@ > 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/` unless the project -configures another directory) is the durable spine; generated code is the disposable -artifact. Regenerate with `npx meta gen`. - -## Read the generated reference before you touch a tier -`meta gen` writes four reference files under this project's docs target -(`targets.docs.outDir` in `metaobjects.config.ts`) when `generators` lists the generator -for each — `agentDocsFile()` for the three `agent/` pages, `apiDocsFile()` for the api -reference. There is no separate docs command: documentation is one more entry in the same -list every other generator is in. They are generated from THIS project's metadata, so they -describe what it actually is rather than what the framework does in general — and reading -the relevant one first is how you avoid inferring the answer from generated code, which is -the disposable half. - -- **`agent/schema.md`** — before touching persistence. Tables, columns, the field each - column came from, keys, foreign keys, constraints. -- **`api/AGENT-API.md`** — before calling generated code. The types, endpoints and filter - operators the generators actually emitted. -- **`agent/ui.md`** — before touching a form or a grid. The control, label, rules and - filterability per field, and each declared grid. -- **`agent/requirements.md`** — before adding a capability. What is already claimed, and - what was deliberately **retired** — an entry saying a capability must not be rebuilt is - the one thing you cannot learn from the code, because the code is gone. - -Not there? List the missing generator — and a `docs` target, if `targets` has none — in -`generators`, then run `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`; and the type a JSON column's WRITER declares — `list[str]`, a dataclass, `dict[str, X]` — not an open jsonb bag), 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, GENERATE them — `meta migrate --migration-format flyway` emits the paired `V__`/`U__` files a Flyway runner expects, and leaves applying them to Flyway. The one exception is a schema this project deliberately owns outside MetaObjects (hand-written migrations by rule, or another team's database): when the root `CLAUDE.md` says so, follow it, and keep `meta verify --db` catching drift. -- **Once this project declares any `template.prompt`, every LLM prompt is one** — the text in the template, the data in a payload value object, rendered by the render engine. A structured reply is read through a response model (`@responseRef` and its generated extractor), never a regex or string search. This covers code that does not look like MetaObjects: a format string, text block, template literal or string builder sent to an LLM, and it covers every module, including test harnesses and scripts. Changing a prompt that is still hand-built means migrating it first (snapshot its current output and keep the rendered text byte-identical), not adding a line to it; the `metaobjects-prompts` skill has the steps. -- 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 **Flyway**-owned JVM stack that is `meta migrate --migration-format flyway`, which emits the migration and deliberately refuses `--apply` because Flyway owns applying it. Hand-authoring a migration to match the generated schema is the LAST resort — for a runner MetaObjects has no writer for — never the default for Flyway. 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 — the URL is required, and the bare form exits 2. -- **If this project declares `requirement.*` nodes, a commit that adds, changes or removes behaviour updates them in the same commit** (the claim, its `@status` and `@implementedBy`), or says in the commit message why not. `meta verify` checks that the links resolve, not that the ledger is current. -- Never hand-edit generated **output** — change the metadata and regenerate. (That bounds the files codegen *emits*; it says nothing about the generator that emits them — next bullet.) What survives a regen DIFFERS BY TOOLCHAIN, so never rely on it: the Node/TS `meta gen` path three-way-merges, so an edit inside a generated file is preserved (and is refused rather than guessed at when it cannot tell yours from its own stale output); the JVM generators overwrite, and several Kotlin ones — including `.kt` — truncate unconditionally with no marker check. Put your own code in a subclass or a separate file, never in the generated one. -- **Nothing is generated until you wire it — one generator list per port, documentation included.** No port ships a default generator suite, and there is no separate docs command or docs config block on any port: a docs page is one more entry in the same list as every other generator. The list lives in `generators` in `metaobjects.config.ts` (TypeScript; a fresh `meta init` writes `generators: []`), `codegen/Generators.cs` (C#, scaffolded by `dotnet meta init`), a target's `generators` in `metaobjects.config.yaml` (Python), or `` in the Maven plugin's `` (Java, Kotlin). `meta gen --list --probe` is the catalog — it runs every generator against your model and reports how many files each would emit — and each generator runs as shipped from its package: import it on the Node/TS path (the catalog row prints the line), select it by name on the others. `--generators ` (TypeScript, C#, Python; a second Maven `` of `generate`) NARROWS a run to entries the list already declares — it never adds one, and naming an undeclared entry is refused. Copy nothing you are not going to change. `meta eject ...` copies one into your repo when you need to change what it emits. -- **Need an output the model describes that nothing here emits? WRITE A GENERATOR — that is the primary path, not an escape hatch.** Zod, DTOs, a client, a service layer, docs: MetaObjects is the core (model, loader, `verify`, `migrate`, render, extract), and the generators on it are yours to write. A generator is a name plus a function from the model to `{ path, content }` files, about 20 lines; `verify --codegen` gates it with nothing to register. Read the model through the RESOLVING accessors (`fields()`, `attr()`, `resolvedIsArray()` — never `own*()`), skip abstract objects, and resolve `@objectRef` with the port's helper. On the Node/TS path, `meta generator new ` scaffolds a working one and wires it into `metaobjects.config.ts`; the `metaobjects-codegen` skill has every port's shape. Run a reference generator as shipped when it already emits what you want, and eject one only when it is close and you need to change it; never hand-write output the model fully describes. -- **Database, store or framework not built in? Work it out — that is a supported path, not a dead end.** MySQL is a `dialect`. MongoDB, DynamoDB, Cassandra, Neo4j or a remote API: model each record as a sourceless entity (`identity.primary`, no `source.*`, store settings in your own `@` property bag) and generate its data access with a repository generator you own. Express, NestJS, Next.js, Workers, Vue, Svelte: mount or retarget a reference generator, or write one. Never invent vocabulary for it and never hand-write the layer. The procedure is the `metaobjects-codegen` skill's `references/any-stack.md`. -- **The generators are yours — editing one is ordinary work, not an escalation.** Your generator list, and any generator source in your repo (`codegen/generators/` on the Node/TS path), are your code: no `@generated` header, no upstream ownership, nothing to ask permission for. **A standing rule not to change the MetaObjects repo is not a rule about your generators** — that is a different repository, and generalising the first into the second is how an agent ends up hand-writing the exact layer it was told to generate. When generated output does not fit, in order: change the **metadata** if the model is wrong; else change **your own generator**; only then hand-write. Hand-writing what the metadata already describes is the last resort, not the first. Per-port specifics — including which ports have an eject command and which mean implementing a generator interface — are in the `metaobjects-codegen` skill. -- **A defect in generated code is a defect in YOUR generator — fix it there and keep going.** Output that does not compile, has the wrong shape, or collides with your own code is fixed in the generator that emitted it: your ejected copy on the Node/TS path (eject it first if you import it from the package), your own generator class on the JVM (subclass the reference generator, or copy its source into a codegen module on the plugin's classpath), your ejected copy or your own generator on C# and Python. Do not file it upstream, do not wait for or pin a MetaObjects release, and never branch or patch the MetaObjects repository: the reference generators are helpers you can take a copy of at any time, not a guarantee to wait on. Upstream is only for what you cannot own — the loader and metamodel, the runtime packages, the codegen engine itself (runner, merge, `verify`), and `meta migrate`. The test: if changing a generator fixes it, it is yours. -- Use the generated constants for any string that names metadata — a type, subtype or attribute name, and a **physical table or column name**: those are declared once (`@table` / `@column`) and emitted as the per-object `Names` artifact, so never respell one as a literal outside its declaration. Prefer a typed ORM handle where one exists; the constant is for raw SQL, migrations and logs. -- **Ownership has a converse — wire a generator only for output you will actually consume.** Generated code nothing imports is indistinguishable from generated code that does not COMPILE — one adopter carried 87 uncallable query helpers through two audits because a dead-file census read them as over-generation. Worse, an unused generated file still reads as an invitation: a routes file nobody mounted still says "register this as-is for stock CRUD", so the next reader adopts the thing you decided not to. If an entity needs no REST surface, grid or form, don't wire that generator at all, or narrow it with the generator's own `filter` — don't emit the file and leave it unimported. -- 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. -- **JVM:** serialize a MetaObject-backed instance (a `pojoAware`-flavor generated class, a runtime `ValueObject`, or any `MetaObjectAware` type) through the MetaObjects JSON layer — never hand-configure a Jackson/Gson mapper around the framework fields to make a default mapper cope. - -## Authoring rules you must not violate -- **Search the vocabulary before concluding it cannot be expressed — `meta types `.** An unregistered TYPE or SUBTYPE fails the load in every command. An undeclared `@attr` is different: it fails `meta verify`, which loads strict, and NOT `meta gen`, which does not — so an invented attribute passes codegen silently and is caught by the drift gate rather than by the build. Either way inventing vocabulary is a failure you will hit, not a shortcut. Search names with `meta types `, names and descriptions with `--all` ("find it by what it does"), and one construct's valid `@attrs` with `meta types . --detail`; `--format json` returns the same answer as one machine-readable document, each attr with its `allowedValues`. Rows marked `[ts; pending elsewhere]` (or `[java; …]`) are cross-port vocabulary only that port implements so far: every port loads it, the others ignore it. `[-only]` (`[csharp-only]`; several ports: `[java+kotlin]`) marks vocabulary only those ports ever act on. It is the Node `meta` CLI and, like `meta migrate`, works whatever your backend — it reads the registry, so it needs no project. Ask for NEW vocabulary only once that search comes up empty. -- Nodes are fused-key maps: `{".": { ... }}` (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. - -`metamodelVersion` is also the number that tells you whether YOUR METADATA needs work, separately from the package version, which tells you whether your BUILD does. A package major means imports/CLI/generated-code shape may need attention; a metamodel major means your model may. A release can move one without the other — so when you upgrade, read the changelog for a metamodel-version move, not just the package number. For a 1.0.x → 1.0.x upgrade, follow https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-within-1.0.md: it covers resyncing the generators you own (`meta eject --list`), which a plain `meta gen` does not do. - -**Upgrading from 1.0 to 1.1:** read https://github.com/metaobjectsdev/metaobjects/blob/main/docs/features/migrations/upgrading-1.0-to-1.1.md before you run `gen`, because the build can stay green while the result is wrong. It is the one page for every port and lists the steps in order. The changes that fail nothing when skipped: the default column and table names are now `snake_case` in every port, so a C#, Java, Kotlin or Python database whose columns are named like its fields needs `"naming": { "columns": "literal" }` in `.metaobjects/config.json`, or the regenerated code queries columns that do not exist; and a fractional validator `@min`/`@max` is refused. Upgrade to 1.1.2 or later: with no `wire` setting each port writes its language's standard JSON, and a project whose clients expect 1.1.0's canonical decimal strings declares `"wire": { "decimals": "string", "nullMembers": "omit" }` in `.metaobjects/config.json`. Documentation is a generator in the project's list, and the docs commands are removed. Then resync every generator you own (`meta eject --list` reports each copy that differs from the reference; `meta eject --force` takes the new one, and a plain `meta gen` does not): an owned copy keeps the logic it was copied with. - -## 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/.md` fragment -installed for this project's stack. Read only the skill your task needs, and inside it the -`SKILL.md` plus the reference file it points you to; the other `references/` files are topics -to open when they come up, not reading to do up front. A skill that `meta init` wrote during -the current session may not load by name until the next one; read -`.claude/skills//SKILL.md` directly. - -## 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 `.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 `.new`: diff it against ``, **keep the hand-edits and take -the upstream changes**, write the merged result back to ``, then delete `.new`. +> **root `CLAUDE.md`**, not here — this file is regenerated. + +The metadata (`metaobjects/` unless `.metaobjects/config.json` says otherwise) is the source of +truth. Code, schema and migrations are generated from it and are disposable. + +## Start here: the whole loop, in order + +### TypeScript: generate with `npx meta gen` + +1. **Config.** In `metaobjects.config.ts` set `dialect` (`"postgres" | "sqlite" | "mysql"`; D1 is + `"sqlite"`) and list the generators. Nothing is generated until it is listed. The usual data + layer: + + ```ts + import { entityFile, namesFile, barrel } from "@metaobjectsdev/codegen-ts/generators"; + // generators: [entityFile(), namesFile(), barrel()] + ``` + + Per entity: `.ts` (Drizzle table, row types, Zod schemas, enum types) and + `.names.ts` (physical names), plus an `index.ts` barrel. Install what they import: + `npm i drizzle-orm zod` and your database driver. Every other generator (CRUD `queriesFile()` + / `routesFile()`, docs, `requirementTests()` for a test stub per requirement) is one line too: + + ``` + npx meta gen --list --probe 2>/dev/null | grep -E "^ - name|wouldEmit| import:" | paste - - - + ``` + + `npx meta eject ` copies one into `codegen/generators/` to change it; + `npx meta generator new ` writes a new one. +2. **Generate.** `npx meta gen`. See a file's exports with `grep -n "^export" `. +3. **Reference pages.** `agentDocsFile()` and `apiDocsFile()` with `targets: { docs: { outDir: + "docs" } }` write `agent/schema.md`, `agent/ui.md`, `agent/requirements.md` and + `api/AGENT-API.md`. When they exist, read the one for the tier you touch. + +### Web client: generated by the Node `npx meta gen` + +The client tiers are generators in `metaobjects.config.ts`, whatever the backend: `formFile()` +from `@metaobjectsdev/codegen-ts-react`, `tanstackQuery()` and `tanstackGrid()` from +`@metaobjectsdev/codegen-ts-tanstack`, with the runtime packages `@metaobjectsdev/react` and +`@metaobjectsdev/tanstack`. Wrap the app in ``: +`baseUrl` is where the server mounts its routes, and leaving it out drops that prefix from every +hook without an error. The `metaobjects-runtime-ui` skill has the detail. + +### Schema (every stack: the Node `meta` CLI owns it) + +Never hand-write DDL, and never `ALTER` a live database by hand. + +- First migration (needs a reachable, empty database): + `npx meta migrate --from-db --db --dialect --slug init` writes + `.metaobjects/migrations/-init/up.sql` and `down.sql`. +- At boot or deploy: `npx meta migrate apply-pending --db `. +- A later change: edit the metadata, regenerate, then + `npx meta migrate --dialect --slug ` (offline, from the committed snapshot). + For a renamed field add `--rename-column
.=` (a renamed object: + `--rename-table =`), which keeps the data. A destructive change is refused until you + allow its kind by name, and the refusal names it: `--allow drop-check` for a changed enum, + `--allow drop-column` only when the data should go. + +### Requirements, when the metadata declares `requirement.*` nodes + +They list what the software must do. A commit that changes behaviour updates them in the same +commit: `@status: live` once a claim is built (never before), a new node for new behaviour. +`meta verify` checks that links resolve, not that a status is true. + +### Before you call it done + +- `npx meta verify --codegen` (the generated code and docs pages; the requirement gate runs on every `meta verify`). +- `npx meta verify --db ` (the live schema matches the metadata). + +Commit the metadata, the generated code, `.metaobjects/migrations/` and +`.metaobjects/.gen-state/.hashes.json`. + +## Rules + +- What the metadata describes is generated, never hand-written (tables, types, validators, + foreign keys, schema, migrations). Hand-write only what it cannot describe, on top. +- Never hand-edit generated output: change the metadata and regenerate. Output wrong for you, or + broken? Fix the generator: eject it and it is yours, no permission needed, nothing to wait on. +- Never spell a table or column name as a string; use the generated table or names constants. +- Wire a generator only for output you will import. +- Once the project declares a `template.prompt`, every LLM prompt is one: the text in a + template, the data in a payload, a structured reply read through its generated extractor + (`metaobjects-prompts`). +- Never invent metadata vocabulary. Search it: `npx meta types `, and + `npx meta types . --detail` for one construct's valid attributes. +- Metadata nodes are fused-key maps, `{"field.string": {"name": "email"}}`. Attribute names are + unique within a node, and package paths use `::`. + +## Going deeper + +The loop above needs no skill. The `metaobjects-*` skills hold the detail for less common work: +unusual shapes or adopting existing code (`metaobjects-authoring`), owning or writing a +generator and the full principles (`metaobjects-codegen`), REST routes and the web client +(`metaobjects-runtime-ui`), LLM prompts (`metaobjects-prompts`), migration edge cases, drift and +upgrades (`metaobjects-verify`), an adoption review (`metaobjects-audit`). Load one by name when +its task comes up; it names the one reference page to open. + +This file and the skills are regenerated by `meta init --refresh-docs`; put project guidance in +the root `CLAUDE.md`, or copy a skill to a new name to override it. A refresh never overwrites a +file you edited (only `--force` does, so never `--force` one with local edits): it writes +`.new` beside it. Merge that into ``, keeping your edits, and delete the `.new`. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md deleted file mode 100644 index cada45c525..0000000000 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-audit/references/typescript.md +++ /dev/null @@ -1,218 +0,0 @@ -# TypeScript port — audit reference - -TypeScript is the **reference implementation**. Codegen, schema migration, docs, -and live-DB drift all run through the Node `meta` CLI (`@metaobjectsdev/cli`, -binary `meta`). The full feature set ships here; other ports are measured against it. - ---- - -## Contents -- Finding generated files -- Run codegen + verify -- Drift signatures (what to grep for) -- Owned generators — scaffold-and-own (ADR-0034) -- Version-skew check -- Calibration gaps (do NOT flag these) - -## Finding generated files - -Generated files carry the header: - -``` -// @generated by @metaobjectsdev/codegen-ts -``` - -The header is **informational on this port** — the write decision never reads it. What -the runner overwrites is decided by `.metaobjects/.gen-state/`: the snapshot body if the -machine has one (three-way merge), otherwise the committed `.hashes.json` (byte-for-byte -what it wrote ⇒ overwrite; anything else ⇒ refused, exit 1). A sibling file you created -(`.extra.ts` by convention) is never touched because it is not a path the runner -records — not because of its name, and not because of the header. Locate generated output at the -`outDir` (and per-target `outDir` values) declared in `metaobjects.config.ts`. With -the default single-target config, generated files land at the one configured -`outDir` (e.g. `src/generated/`). - ---- - -## Run codegen + verify - -```bash -meta gen # render → 3-way merge → write (overwrites @generated files only) -meta gen --dry-run # preview without writing -meta gen Author Post # scope to named entities -meta verify --codegen # regenerate to temp, diff vs committed output -meta verify --templates # check {{field}} refs vs payload VO (a bare `meta verify` runs this and `--codegen`) -meta verify --db # schema drift — live DB vs metadata (requires DB connection) -meta verify --codegen --templates --db # all three drift checks in one pass -``` - -`meta gen` and a real `meta verify` run also emit an **advisory anti-pattern pass** -(never fails the build) flagging hand-rolled aggregates, money-as-float, and CHECK -value-sets against the constructs that replace them. - ---- - -## Drift signatures (what to grep for) - -Rows are shapes worth a grep. Signature 11's rule row is applied to any shape that is not -here — the table is its second pass, not its definition. - -| Signature | What it means | -|---|---| -| `pgTable(` / `sqliteTable(` outside `src/generated/` | hand-authored Drizzle table — candidate to generate | -| `z.object({` / `z.string()` in service/route files | hand-built Zod schema — compare to generated `InsertSchema`/`UpdateSchema` | -| `FilterAllowlist` defined by hand | should be the generated `FilterAllowlist` | -| `// keep in sync with` / `// mirrors the` | second-source-of-truth comment — always a finding | -| camelCase↔snake_case mapping tables | generated views handle this; diff field by field | -| `@generated` file with hand edits | three-way merge preserves them; review at codegen; never delete | -| **Signature 11, the rule — apply it before any row below:** a literal in non-generated code or config that must equal a value the metadata declares or a generator derives, at a site regeneration would NOT break | hunt VALUE-first: build one alternation from the values in `.names.ts` + `.meta.ts` (`$entity` / `$table` / `$path`; per-field `name` / `label` / `rules` / `options`) and grep the non-generated tree once, then trace each hit to its consumer. The rows below are the three commonest shapes, kept for the second pass — an already-drifted, concatenated or case-transformed literal a value grep cannot see. A shape not listed (a GraphQL field, a queue topic, a cache key, a `COPY` header) is scored by the rule, not skipped | -| a table / column / index string in a `sql` fragment, a Kysely identifier, or a migration/seed script outside the generated dir | second spelling of a declared physical name — reference `.names.ts` (`ProgramNames.fields..column`, `.indexes..index`): `generators: [...]` is the COMPLETE list and there is no default suite, so a project scaffolded before 1.0 emits NO names artifact and the missing `namesFile()` is the finding first; a Drizzle column object in its place is resolved, not a finding | -| the resource path in an API call or mount — `app.get("/subscribers")`, the `/subscribers` inside `fetch("/api/subscribers")`, a TanStack `queryKey` path | second spelling of `$path` on the entity module and its `.meta.ts` twin. The `/api` prefix is NOT part of the finding: the constants surface deliberately carries no prefix member — it is the provider's `baseUrl`, a deployment fact — so the prefix has no constant by design and is never a gap either | -| a field name passed as text where the parameter's type is a bare `string` — `sort: "created_at"`, `filter["email"]`, a column-id string in a grid config | the per-field constants object carries `name`. Check the parameter type first: where the generated hook types it as a union of field names it is resolved and this row does not apply, exactly as a typed property access does not | -| a form label or a validation limit/message re-typed in hand JSX or a hand validator (`maxLength={255}`, `"Email is required"`) | derived from `@title` and the field's validator children into `label` / `rules`; the hand copy is a second source of truth | -| a member of a generated enum typed as a hand string where the file already imports its constant (`status: "error"` beside `CouncilStatusEnum.enum.complete`), in a string-typed position, raw SQL, a JSON payload or an event name | **signature 13.** Value-first: grep the non-generated tree for each enum's `@values`. Check the declared type at the site first: a position narrowed by the generated `export type ` union is resolved and exempt; a `Record`, a `string` parameter or a `sql` fragment is compared and scores | -| a status-like column written with `.set({ status })` and no condition on the current value, or a `sql` `UPDATE ... SET status` | **axis G3.** Guard it through Drizzle / Kysely: `.where(and(eq(t.id, id), eq(t.status, )))` with the generated constant, and check the affected-row count. Never raw SQL | -| **NOT a finding — the rule's consequences, not a list:** anything RESOLVED (a typed handle, a typed property access, a typed filter key, an enum member narrowed by the generated `export type ` union — measured: 332 such literals on one estate, 0 real) and anything the metadata does not OWN (a page route or link, `` / `` — 11 apparent `$path` hits on another estate, 0 real; the deployment prefix above) | rewriting a resolved reference to a string lookup is a regression; converting a page route couples the site's URL structure to the API's. Emitted `options: [...] as const` populates a dropdown, not replaces checked literals. A shape that is neither resolved nor unowned and has a constant → score it, whatever it is | - ---- - -## Owned generators — scaffold-and-own (ADR-0034) - -`meta eject ...` copies the reference generator templates a project chooses into -`codegen/generators/*.ts`. Since 1.0.4 `meta init` scaffolds that directory EMPTY with -`generators: []` (a project initialised on an earlier CLI got four wired copies, so both -shapes are normal). A generator reaches `metaobjects.config.ts` one of two ways, and both -are supported from 1.1: - -```ts -// run as shipped — nothing copied, fixes arrive by upgrading -import { entityFile, queriesFile } from "@metaobjectsdev/codegen-ts/generators"; -// owned — a local copy the project can change -import { routesFile } from "./codegen/generators/routes"; -``` - -**Audit the owned copies first.** A copy exists to carry a change. One that carries none -is duplication, and it falls behind the reference: - -```bash -meta eject --list --format text -``` - -Pass `--format text`. Away from a terminal the default is a structured document, and -that document holds the generator rows only: it has no `codegen/runtime/` rows at all. - -The text listing marks each owned copy `identical to the reference`, `same content as the -reference, your formatting`, or `DIFFERS: N line(s) behind, M line(s) of your own`. Those -are the `--format text` labels. In `--format json` or `toon` the same row carries -`status` (`identical`, `reformatted` or `differs`), `behind` and `own`. - -**Report a copy as a finding only when you can prove it unmodified.** - -- `identical to the reference` (`status: identical`) is the only mark that proves it. That - copy is a **finding**: recommend replacing its local import with the package import and - deleting the file, then `meta gen` and a review of the diff. -- `same content as the reference, your formatting` (`reformatted`) and `DIFFERS` with - `0 line(s) of your own` (`differs`, `own: 0`) do not prove it. The comparison ignores - line order, and a line the adopter deleted counts as "behind" exactly like a line - upstream added, so `own: 0` rules out added or edited lines, not removed or reordered - ones. Confirm from the file's history - (`git log -p -- codegen/generators/.ts` shows no edit after the eject) or from - `diff -u node_modules/@metaobjectsdev/codegen-ts/src/reference/.ts codegen/generators/.ts` - showing only upstream changes. Confirmed, it is the same finding. Not confirmed, report - it as **possibly customized: check its history**, never as a copy to drop. -- A copy with lines of its own (`M > 0`) is **not** thereby an intentional customization. The - listing prints `DIFFERS` for both a deliberate customization and a copy gone stale by - version, and `M line(s) of your own` counts every line absent from today's reference: after - the reference moved on, a line it removed or rewrote reads as "yours" and is an older - upstream line. Run the same history check on any `DIFFERS` row, `M > 0` included - (`git log -p -- codegen/generators/.ts`): lines the copy's later commits added are the - project's own; lines already in the eject commit are upstream's, and count as behind. Report - ` lines behind the reference` and ` lines of the project's own (confirmed from - history)` as two figures. A copy whose own lines history does not confirm is **possibly - customized or stale: check its history**; a copy that is stale by version is a finding - (`owned-generator-stale`): re-eject at the current version or return to the package import. - -Count lines for the Phase 0 total: lines in copies proven unmodified, with the -`codegen/runtime/` files they reach, over all lines the project owns under `codegen/` -(`generators/`, `runtime/`, and the `templates/` modules an eject copies). -`meta eject --undo` removes every identical copy and prints the config edit. - -**`entity`, `routes` and `routes-hono` own more than the generator file.** Ejecting one -also copies the HTTP adapter its output imports into `codegen/runtime/`, and a route fix -is often made there with the generator file left untouched. Read the state of those files -from one of two places: - -- `meta eject --list --format text` prints a block of `codegen/runtime/` rows after the - generators, each marked `identical to the package`, `same content as the package, your - formatting`, `DIFFERS: N line(s) only in the package, M line(s) only in yours`, or - `not in the package`. -- `meta gen --list --format json` puts them on the generator's own row, inside its - `source` object: an owned row has `owned: true` there and a `runtimeCopy` array, one - entry per adapter file that generator reaches, with `path` and `verdict` (`identical`, - `reformatted` or `differs`). - -**A listing with no runtime rows is not evidence that the adapter is unmodified.** If -`codegen/runtime/` exists and the output you read shows no row for its files, you have -not checked: run one of the two commands above. - -For these three, the copy is unmodified only when the generator file passes the test -above AND every `codegen/runtime/` file it reaches is marked identical. If any of those files differs or -is not in the package, the copy carries a customization: it is **not a finding**, and do -not recommend the package import. The packaged generator imports -`@metaobjectsdev/runtime-ts`, so swapping it in would leave the edited adapter unused with -nothing reporting it. The owned generator is what points the output at that adapter (its -`runtimeImport` option), so it stays. - -**Do NOT flag the subpath itself.** `@metaobjectsdev/codegen-ts/generators` is the supported -home of every generator factory: the entity tier (`entityFile`, `queriesFile`, `routesFile`, -`routesFileHono`, `namesFile`, `barrel`), the prompt/output tier (`promptRender`, -`outputParser`, `outputPrompt`, `extractor`, `renderHelper`), `traceHelperFile`, -`callableFile` and the rest. The CLI's own prompt-gate warning tells adopters to import -`promptRender` from exactly this path. A project importing from it is CORRECT, and -reporting it as un-adopted scaffold-and-own is a false finding. - -**The one version where an import is a blocker.** `entityFile` / `queriesFile` / -`routesFile` / `barrel` were removed from that subpath at 1.0 (ADR-0035 A3) and restored in -1.1 (ADR-0034 Amendment 5). On a 1.0.x install a config importing them fails to load with -a message naming `meta eject`. Check the installed version before reading a hit as a -finding, and match the four NAMES, never the path alone: - -```bash -npm list @metaobjectsdev/codegen-ts -grep -rn '@metaobjectsdev/codegen-ts/generators' . \ - | grep -E 'entityFile|queriesFile|routesFile|barrel' -``` - -On 1.0.x a hit is an upgrade blocker: recommend upgrading to 1.1, or `meta eject ` -and importing the copies locally. On 1.1 and later a hit is correct. - ---- - -## Version-skew check - -The resolved CLI version is what actually ran codegen — check `node_modules`, not -`package.json` declared ranges: - -```bash -npm list @metaobjectsdev/cli @metaobjectsdev/codegen-ts @metaobjectsdev/metadata -``` - -All `@metaobjectsdev/*` packages publish in lockstep. A mixed resolved version -(e.g. `codegen-ts@0.12.0` alongside `metadata@0.11.9`) is an intra-port skew -finding. A runtime package (`@metaobjectsdev/runtime-ts`) in `devDependencies` -rather than `dependencies` is also a finding — it ships to the deployed app. -This intra-port skew is npm-vs-npm; **cross-language** skew (a stale npm line next to -a newer Maven/PyPI/NuGet line) is invisible in the package numbers and must be checked -via `metamodelVersion` (see the Phase 0 cross-language consistency item). - ---- - -## Calibration gaps (do NOT flag these) - -TypeScript is the reference implementation — it ships the full feature set: - -- Filter-operator codegen: **complete** — the core `?filter[field][op]` grammar (all 9 operators `eq/ne/gt/gte/lt/lte/in/like/isNull`, `FilterAllowlist`) generates in every port; TS additionally ships the richer surface (`?search=`, `filter[or][N]` / `filter[and][N]` combinators, leading-wildcard gating). -- Output-parser codegen: **complete** (`outputParser()` generator; `parse*`/`safeParse*`/`extract*` emitted as `.response.ts` per **responding `template.prompt`** — one carrying `@responseRef`; ADR-0052). -- ObjectManager: **complete** (`@metaobjectsdev/runtime-ts`; `kyselyDriver` / `inMemoryDriver`). -- Schema migrate / `meta verify --db`: **complete** (TS owns the shared migrate engine — ADR-0015). -- D1 dialect: **TS-only by design** — `migrate.dialect: "d1"` (and `meta migrate --dialect d1`) targets Cloudflare D1 via the Wrangler CLI layout; codegen's `dialect` in `metaobjects.config.ts` is `"sqlite"` for D1 and `"d1"` there is refused. Do NOT recommend porting D1 support to other languages. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/typescript-providers.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/typescript-providers.md deleted file mode 100644 index 5db6706b4f..0000000000 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-authoring/references/typescript-providers.md +++ /dev/null @@ -1,74 +0,0 @@ -# Your own vocabulary in TypeScript — a provider - -> Part of the `metaobjects-authoring` skill, "Your own vocabulary". Read it when the project -> needs a subtype or an attribute the core does not ship, and its TypeScript tools load the -> metadata. - -A provider is an object implementing `MetaDataTypeProvider`: an `id`, the provider ids it -depends on, and a `registerTypes(registry)` body that calls `register` (a new subtype) and -`extend` (attributes on an existing one). This one registers `layout.picker` and adds -`@searchable` and `@valueLabels` to `field.string`: - -```ts -// codegen/vocabulary.ts -import { TypeId, MetaLayout, type MetaDataTypeProvider } from "@metaobjectsdev/metadata"; -import { - TYPE_FIELD, TYPE_LAYOUT, FIELD_SUBTYPE_STRING, - ATTR_SUBTYPE_STRING, ATTR_SUBTYPE_BOOLEAN, -} from "@metaobjectsdev/metadata/constants"; - -export const vocabulary: MetaDataTypeProvider = { - id: "acme-vocabulary", - dependencies: ["metaobjects-core-types"], - registerTypes(registry) { - // A new subtype, with its own attribute (an own attribute may be required). - registry.register({ - typeId: new TypeId(TYPE_LAYOUT, "picker"), - description: "A picker over the owning object.", - factory: (typeId, name) => new MetaLayout(typeId, name), - childRules: [], - attributes: [ - { name: "displayField", valueType: ATTR_SUBTYPE_STRING, required: false, description: "Field shown per choice." }, - ], - }); - // Attributes on a type another provider owns: always optional. - registry.extend(TYPE_FIELD, FIELD_SUBTYPE_STRING, { - attributes: [ - { name: "searchable", valueType: ATTR_SUBTYPE_BOOLEAN, required: false, description: "Offer in search." }, - { name: "valueLabels", valueType: ATTR_SUBTYPE_STRING, isArray: true, required: false, description: "Display labels." }, - ], - }); - }, -}; -``` - -Wire it once, in `metaobjects.config.ts`: - -```ts -import { defineConfig } from "@metaobjectsdev/cli"; -import { vocabulary } from "./codegen/vocabulary"; - -export default defineConfig({ - generators: [/* ... */], - providers: [vocabulary], -}); -``` - -Every `meta` command that loads metadata reads that list: `gen` (with the docs generators and -the docs site), `verify`, `migrate`, `fmt`, `types`, `export`, `deps`, `upgrade`. `meta types -picker` then lists the new subtype. A config that fails to load is reported as its own error, -never as an unknown subtype. Code that loads the model itself passes the same provider: -`loadMemory(root, { providers: [vocabulary] })`. - -The metadata uses it like core vocabulary: - - -```json -{ "field.string": { "name": "name", "@searchable": true } } -{ "layout.picker": { "name": "contactPicker", "@displayField": "name" } } -``` - -Without the provider the same file fails `meta verify` with `ERR_UNKNOWN_SUBTYPE` on -`layout.picker` and `ERR_UNKNOWN_ATTR` on `@searchable`. If another port's tools load this -metadata too, that port needs its own copy of the provider (same `id`, same vocabulary). Each port's `verify` loads strict, so a clean run there proves that port's copy is complete. -Full contract: `docs/features/extending-with-providers.md`. diff --git a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md b/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md deleted file mode 100644 index ff88a88e8a..0000000000 --- a/fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript-docs.md +++ /dev/null @@ -1,112 +0,0 @@ -# TypeScript docs — generator entries, not a separate command - -> Part of the `metaobjects-codegen` skill's TypeScript reference. Read it when generating or wiring documentation. - -## Contents -- Six docs factories, each one more entry in `generators` -- Give them a `docs` target -- An owned docs generator goes in `generators`, like any other owned copy -- There is no `mermaidErDiagram()` generator -- Before calling any generated code, read `/api/AGENT-API.md` -- Ejecting routes hands over the adapter too -- Wire a generator only for output you consume, and narrow it with its `filter` - -**Documentation is `meta gen` generators — there is no separate `meta docs` command and -no `docs` config block** (both removed in 1.1.0, ADR-0034 Amendment 9: one way to -generate, docs included). Each factory is an ordinary entry in `generators`, from -`@metaobjectsdev/codegen-ts/generators`: - -| Factory | Registry name | Writes | -|---|---|---| -| `docsFile()` | `docs` | the neutral model surface — one page per entity and per template (`.md`, `