sourceFiles(MetaDataLoader loader) {
* @return the number of errors found; the caller fails the build once its own gate has reported
*/
private int runRequirementGate(MetaDataLoader loader) {
+ // A default-set run reaches here once per gate; the ledger is one report, not two.
+ if (advisoriesDone) return 0;
+ advisoriesDone = true;
RequirementCheck.Scan scan = RequirementCheck.scan(loader.getRoot(),
new RequirementCheck.Options(null,
requireImplementers || "1".equals(getEnv(ENV_REQUIRE_IMPLEMENTERS))));
@@ -323,7 +371,7 @@ private void verifyTemplates() throws MojoExecutionException, MojoFailureExcepti
}
// ------------------------------------------------------------------------
- // mode=codegen (default) — regenerate-to-temp + diff vs committed (UNCHANGED)
+ // mode=codegen — regenerate-to-temp + diff vs committed (UNCHANGED)
// ------------------------------------------------------------------------
private void verifyCodegen() throws MojoExecutionException, MojoFailureException {
diff --git a/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/MetaDataVerifyTemplatesModeTest.java b/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/MetaDataVerifyTemplatesModeTest.java
index 633bcc6db..11fba6440 100644
--- a/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/MetaDataVerifyTemplatesModeTest.java
+++ b/server/java/maven-plugin/src/test/java/com/metaobjects/mojo/MetaDataVerifyTemplatesModeTest.java
@@ -14,7 +14,7 @@
/**
* Tests {@code meta:verify -Dmeta.verify.mode=templates} — the NEW template/prompt
* drift mode (ADR-0021 D2 fan-out) — plus the {@code mode} dispatch itself
- * (unknown / {@code db} → clear failure; default stays {@code codegen}).
+ * (unknown / {@code db} → clear failure; unset runs the codegen and template gates).
*
* The Mojo's {@code project}/{@code execution} fields are left null so
* {@code createProjectClassLoader()} falls back to the test classloader (which can
@@ -126,6 +126,42 @@ public void dbModeFailsAsUnsupported() throws Exception {
}
}
+ // === the default gate set (mode unset) ==================================
+
+ @Test
+ public void defaultModeRunsTheTemplateGateWhenATemplateRootIsSet() throws Exception {
+ Path templateRoot = writeTemplate("ai/greeting", "Hello {{title}} — {{subject}}");
+
+ MetaDataVerifyMojo verify = new MetaDataVerifyMojo();
+ configureTemplates(verify, templateRoot);
+ verify.setMode("");
+ try {
+ verify.execute();
+ fail("expected the default gate set to fail on template drift");
+ } catch (MojoFailureException e) {
+ assertTrue(e.getMessage().contains("ERR_VAR_NOT_ON_PAYLOAD"));
+ }
+ }
+
+ @Test
+ public void defaultModePassesWhenBothGatesAreClean() throws Exception {
+ MetaDataVerifyMojo verify = new MetaDataVerifyMojo();
+ configureTemplates(verify, writeTemplate("ai/greeting", "Hello {{title}} — {{body}}"));
+ verify.setMode("");
+ verify.execute();
+ }
+
+ @Test
+ public void defaultModeSkipsTheTemplateGateWithoutATemplateRoot() throws Exception {
+ // Not configured is not failed: the codegen gate (no generators here) still runs and passes.
+ MetaDataVerifyMojo verify = new MetaDataVerifyMojo();
+ configureTemplates(verify, Files.createTempDirectory("verify-no-root"));
+ verify.setMode("");
+ verify.setTemplateRoot(null);
+ verify.execute();
+ assertTrue(new MetaDataVerifyMojo().getMode().isEmpty());
+ }
+
// === helpers ============================================================
private static Path writeTemplate(String ref, String body) throws IOException {
diff --git a/server/python/src/metaobjects/cli.py b/server/python/src/metaobjects/cli.py
index bdbf69870..e23551624 100644
--- a/server/python/src/metaobjects/cli.py
+++ b/server/python/src/metaobjects/cli.py
@@ -21,7 +21,7 @@
across ports):
--codegen regenerate into a temp dir + diff against the committed
- ``--out`` tree (Python's historical default behavior).
+ ``--out`` tree.
The column naming comes from ``naming.columns`` in
``.metaobjects/config.json`` (ADR-0058), the same setting
``gen`` reads, so the ``names`` generator's constants match
@@ -38,9 +38,9 @@
migrate engine (ADR-0015), not this CLI.
Combining flags runs each requested mode and aggregates the exit code
- (non-zero if ANY mode drifts). A bare ``verify`` keeps the historical
- default = ``--codegen`` (back-compat) and prints a one-line note that the
- explicit subverbs exist.
+ (non-zero if ANY mode drifts). A bare ``verify`` runs ``--codegen`` and
+ ``--templates`` (the cross-port default gate set) and prints a one-line
+ note that the explicit subverbs exist.
Named ``metaobjects``, NOT ``meta``: ``meta`` is the Node schema CLI. This CLI
intentionally has NO ``migrate`` subcommand — schema is owned by the Node
@@ -2668,8 +2668,11 @@ def _verify_requirements(args: argparse.Namespace, model: "_VerifyModel | None")
def _cmd_verify(args: argparse.Namespace) -> int:
"""Subverb dispatch (ADR-0021 D2). Run each requested mode; aggregate exit =
- max (non-zero if ANY mode drifts). Bare ``verify`` (no subverb) keeps the
- historical default = ``--codegen`` + a one-line note advertising the subverbs.
+ max (non-zero if ANY mode drifts). Bare ``verify`` (no subverb) runs the
+ cross-port default gate set, ``--codegen`` + ``--templates`` (the requirement
+ ledger runs on every verify) + a one-line note advertising the subverbs.
+ ``--templates`` is skipped with a note when the run names no config and no
+ ``--prompts`` root, since there is then no template directory to resolve.
"""
naming_refusal = _refuse_column_naming_flag(args)
if naming_refusal is not None:
@@ -2684,12 +2687,24 @@ def _cmd_verify(args: argparse.Namespace) -> int:
if not any_explicit:
print(
- "metaobjects verify — running --codegen (default). Explicit "
+ "metaobjects verify — running --codegen --templates (default). Explicit "
"subverbs: --codegen (codegen drift), --templates (template/prompt "
"drift), --db (schema drift — not supported in Python).",
file=sys.stderr,
)
run_codegen = True
+ if args.metadata_dir is None:
+ has_template_root = _find_config(args) is not None
+ else:
+ has_template_root = bool(getattr(args, "templates_root", None))
+ if not has_template_root:
+ print(
+ "metaobjects verify — template gate skipped: no metaobjects.config.yaml "
+ "and no --prompts directory, so there is no template root to resolve against.",
+ file=sys.stderr,
+ )
+ else:
+ run_templates = True
exit_code = 0
gates_stderr = _RecordedStderr(sys.stderr)
@@ -2850,7 +2865,7 @@ def _build_parser() -> argparse.ArgumentParser:
"verify",
help=(
"drift gate — explicit subverbs --codegen / --templates / --db "
- "(ADR-0021 D2); bare verify defaults to --codegen"
+ "(ADR-0021 D2); bare verify runs --codegen and --templates"
),
)
verify.add_argument(
diff --git a/server/python/tests/codegen/test_cli_config_verify.py b/server/python/tests/codegen/test_cli_config_verify.py
index fb947c362..a49ff6512 100644
--- a/server/python/tests/codegen/test_cli_config_verify.py
+++ b/server/python/tests/codegen/test_cli_config_verify.py
@@ -61,7 +61,7 @@ def test_verify_codegen_bare_defaults_to_codegen(tmp_path: Path, monkeypatch) ->
_project(tmp_path)
monkeypatch.chdir(tmp_path)
assert main(["gen"]) == 0
- assert main(["verify"]) == 0 # bare verify → --codegen default, config-driven
+ assert main(["verify"]) == 0 # bare verify → --codegen (config-driven), plus --templates
def test_verify_codegen_detects_drift_in_one_target(tmp_path: Path, capsys) -> None:
diff --git a/server/python/tests/codegen/test_cli_verify_subverbs.py b/server/python/tests/codegen/test_cli_verify_subverbs.py
index c732f89c0..d73bbbae9 100644
--- a/server/python/tests/codegen/test_cli_verify_subverbs.py
+++ b/server/python/tests/codegen/test_cli_verify_subverbs.py
@@ -10,8 +10,10 @@
verify --db REJECTED in the Python port (exit 2): schema verify is the
migrate engine (ADR-0015), not this CLI.
-A bare `verify` keeps the historical default = codegen (back-compat) and prints
-a one-line note advertising the explicit subverbs. Combining flags runs each and
+A bare `verify` runs the cross-port default gate set, codegen + templates (the
+requirement ledger runs on every verify), and prints a one-line note advertising
+the explicit subverbs. `--templates` is skipped with a note when the run names no
+config and no template root. Combining flags runs each and
aggregates the exit code (non-zero if ANY drift).
"""
@@ -272,6 +274,32 @@ def test_bare_verify_detects_codegen_drift(tmp_path: Path) -> None:
assert main(as_config(["verify", "--generators", GEN_SUITE, meta_dir, "--out", str(out)])) != 0
+def test_bare_verify_runs_the_template_gate(tmp_path: Path, capsys) -> None:
+ """The headline gap: bare verify used to be codegen only, so prompt drift passed CI."""
+ meta_dir = _meta_dir_with(tmp_path, _META_CLEAN)
+ troot = _templates_dir(tmp_path, "Hello {{missing}}")
+ out = tmp_path / "out"
+ out.mkdir()
+ rc = main(as_config(["verify", "--generators", GEN_SUITE, meta_dir, "--out", str(out), "--templates-root", troot]))
+ assert rc != 0
+ assert "missing" in capsys.readouterr().err
+
+
+def test_bare_verify_template_gate_passes_when_clean(tmp_path: Path) -> None:
+ meta_dir = _meta_dir_with(tmp_path, _META_CLEAN)
+ troot = _templates_dir(tmp_path, "Hello {{name}}")
+ out = tmp_path / "out"
+ assert main(as_config(["gen", "--generators", GEN_SUITE, meta_dir, "--out", str(out)])) == 0
+ assert main(as_config(["verify", "--generators", GEN_SUITE, meta_dir, "--out", str(out), "--templates-root", troot])) == 0
+
+
+def test_bare_verify_skips_templates_with_a_note_when_no_root_is_known(tmp_path: Path, capsys) -> None:
+ """Named and no --prompts: nothing to resolve refs against, so skip, do not fail."""
+ meta_dir = _meta_dir_with(tmp_path, _META_CLEAN)
+ main(["verify", "--generators", GEN_SUITE, meta_dir, "--out", str(tmp_path / "out")])
+ assert "template gate skipped" in capsys.readouterr().err
+
+
# --- 4. --db is rejected in the Python port (exit 2) ------------------------
diff --git a/server/typescript/packages/cli/src/commands/verify.ts b/server/typescript/packages/cli/src/commands/verify.ts
index 0bc013a8c..c027beae4 100644
--- a/server/typescript/packages/cli/src/commands/verify.ts
+++ b/server/typescript/packages/cli/src/commands/verify.ts
@@ -7,6 +7,7 @@
// silently break a prompt" guarantee, enforced at the last fixed point before
// the text ships. Required-slot misses are warnings (don't fail the build).
+import { existsSync } from "node:fs";
import { join, relative, resolve as resolvePath } from "node:path";
import { parseVerifyArgs, type MigrateFlags } from "../lib/args.js";
import { log } from "../lib/log.js";
@@ -199,20 +200,24 @@ export async function verifyCommand(
// ADR-0021 D2 — explicit verify subverbs. Each flag selects one drift mode;
// any combination runs each and the overall exit code is the MAX (non-zero on
- // any drift). A bare `verify` (no explicit subverb) keeps its documented
- // back-compat default: the template/prompt drift gate — plus a one-line note
- // advertising the explicit subverbs.
+ // any drift). A bare `verify` (no explicit subverb) runs the cross-port default
+ // gate set: codegen drift, the template/prompt drift gate and the requirement
+ // ledger — plus a one-line note advertising the explicit subverbs. `--db`,
+ // `--deps` and `--replay` stay opt-in.
const runTemplates = flags.templates || !flags.anyExplicit;
// The schema gate is selected by the presence of --db, or --dialect d1 (#225 —
// D1 has no URL connection); that check lives inside runSchemaVerify.
- const runCodegen = flags.codegen;
+ // Selected by `--codegen`, or by a bare run that has a `metaobjects.config.ts` to
+ // regenerate from (decided below, once the config has been read). A bare run with no
+ // config skips the gate with a note instead of failing it.
+ let runCodegen = flags.codegen;
// Task 15 — same shape as --codegen: selected ONLY by its own flag,
// never folded into the bare-verify default (it needs the publisher
// reachable, which CI may not have — see the VerifyFlags doc on `deps`).
const runDeps = flags.deps;
if (!flags.anyExplicit) {
say(
- "meta verify — running --templates (default). Explicit subverbs: " +
+ "meta verify — running --codegen --templates (default). Explicit subverbs: " +
"--templates (prompt drift), --db/--dialect d1 (schema drift), " +
"--codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), " +
"--replay/--replay-snapshot (the committed migration chain replays from empty).",
@@ -280,6 +285,7 @@ export async function verifyCommand(
// If absent/invalid we fall back to defaults; `--codegen` then reports a clear
// error (it can't diff without knowing where the committed output lives).
let forgeConfig: MetaobjectsGenConfig | undefined;
+ let configLoadError: unknown;
try {
forgeConfig = await loadMetaobjectsConfig(genConfigDir);
} catch (e) {
@@ -290,12 +296,27 @@ export async function verifyCommand(
return 2;
}
forgeConfig = undefined;
+ configLoadError = e;
}
// Both of the gen config's contributions to the load, together — see
// `loadMemoryOptionsFrom`. Threading `providers` and forgetting `libraries` is how
// a shipped library became unloadable through the CLI (#333).
const configLoadOptions = loadMemoryOptionsFrom(forgeConfig);
+ // The bare run's codegen gate: on whenever there is a config to diff against. An
+ // adopter with metadata but no generators (nothing generated, nothing to go stale)
+ // keeps a passing bare verify; the note says what was skipped and why.
+ if (!flags.anyExplicit) {
+ if (forgeConfig !== undefined) runCodegen = true;
+ else if (existsSync(join(genConfigDir, "metaobjects.config.ts"))) {
+ log.error(
+ "meta verify: metaobjects.config.ts exists but failed to load, so the codegen gate cannot run: " +
+ (configLoadError instanceof Error ? configLoadError.message : String(configLoadError)),
+ );
+ return 2;
+ } else say("meta verify — codegen gate skipped: no metaobjects.config.ts found, so there is no generated output to diff against");
+ }
+
// ADR-0023 strict-by-default (#96): verify loads strict unless --lax is passed,
// so an undeclared/typo'd own @attr fails verify (matching Java's Maven goal).
let root: Awaited>;
@@ -1964,7 +1985,7 @@ export function notRunNote(
return flag !== undefined ? `${g.gate} (${flag})` : g.gate;
});
const tail = opts.bare === true
- ? " — a bare 'meta verify' runs only the template gate (plus the requirement ledger); " +
+ ? " — a bare 'meta verify' runs the codegen and template gates (plus the requirement ledger); " +
"name each gate to run it"
: "";
return `meta verify — not run: ${named.join(", ")}${tail}`;
diff --git a/server/typescript/packages/cli/src/index.ts b/server/typescript/packages/cli/src/index.ts
index 3f1b29b15..fb3327cec 100644
--- a/server/typescript/packages/cli/src/index.ts
+++ b/server/typescript/packages/cli/src/index.ts
@@ -42,7 +42,7 @@ COMMANDS:
types [query] Search the metadata vocabulary (types, subtypes, @attrs) by name or description
export Flatten loaded metadata to one canonical JSON artifact
fmt [...] Rewrite metadata files into canonical form; --check lists drift, exits non-zero
- verify Drift gate — subverbs: --templates / --db / --codegen / --deps (bare = --templates)
+ verify Drift gate — subverbs: --templates / --db / --codegen / --deps (bare = --codegen --templates)
upgrade Rewrite retired metadata vocabulary (previews; --apply writes)
prompt-snapshot Snapshot rendered template.* output; --check gates drift
migrate Diff metadata vs live DB; emit migration SQL files
diff --git a/server/typescript/packages/cli/src/lib/args.ts b/server/typescript/packages/cli/src/lib/args.ts
index 52be9ee20..80f626008 100644
--- a/server/typescript/packages/cli/src/lib/args.ts
+++ b/server/typescript/packages/cli/src/lib/args.ts
@@ -357,7 +357,7 @@ export interface VerifyFlags {
// ADR-0021 D2 — explicit verify subverbs. Each selects one drift mode; any
// combination may be passed and the exit code aggregates (non-zero on any
// drift). The boolean flags record which modes were explicitly requested; the
- // command layer applies the bare-verify default (= --templates) when none are.
+ // command layer applies the bare-verify default (= --codegen --templates) when none are.
/** Run the template/prompt {{field}}↔payload drift gate. */
templates: boolean;
/** Run the codegen-drift gate: is the GENERATED contribution of each file current? */
diff --git a/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap b/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap
index 9d5be11f7..cb6959984 100644
--- a/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap
+++ b/server/typescript/packages/cli/test/__snapshots__/cli.test.ts.snap
@@ -22,7 +22,7 @@ COMMANDS:
types [query] Search the metadata vocabulary (types, subtypes, @attrs) by name or description
export Flatten loaded metadata to one canonical JSON artifact
fmt [...] Rewrite metadata files into canonical form; --check lists drift, exits non-zero
- verify Drift gate — subverbs: --templates / --db / --codegen / --deps (bare = --templates)
+ verify Drift gate — subverbs: --templates / --db / --codegen / --deps (bare = --codegen --templates)
upgrade Rewrite retired metadata vocabulary (previews; --apply writes)
prompt-snapshot Snapshot rendered template.* output; --check gates drift
migrate Diff metadata vs live DB; emit migration SQL files
diff --git a/server/typescript/packages/cli/test/advisory-structured-output.test.ts b/server/typescript/packages/cli/test/advisory-structured-output.test.ts
index b10f72f22..6aa739cce 100644
--- a/server/typescript/packages/cli/test/advisory-structured-output.test.ts
+++ b/server/typescript/packages/cli/test/advisory-structured-output.test.ts
@@ -112,6 +112,18 @@ async function capture(argv: string[]): Promise<{ exit: number; out: string; err
return { exit, out: outLines.join("\n"), err: errLines.join("\n") };
}
+/**
+ * `project()` with its generated output on disk. A bare `meta verify` diffs that output
+ * against a fresh regen, so a verify test that is about the advisory sections needs the
+ * codegen gate to find nothing stale.
+ */
+async function generatedProject(extraConfig = "", baseUrlSites = 0): Promise {
+ const dir = project(extraConfig, baseUrlSites);
+ const { exit } = await capture(["gen", "--cwd", dir]);
+ if (exit !== 0) throw new Error(`fixture gen failed with exit ${exit}`);
+ return dir;
+}
+
interface FindingRow {
file: string; line: number; rule: string; construct: string; message: string;
}
@@ -179,7 +191,7 @@ describe("meta gen --format json carries the advisory findings", () => {
describe("meta verify --format json emits a structured result", () => {
test("stdout is one JSON document — gates, findings, and what is NOT represented", async () => {
- const dir = project();
+ const dir = await generatedProject();
try {
const { exit, out, err } = await capture(["verify", "--format", "json", "--cwd", dir]);
@@ -217,14 +229,14 @@ describe("meta verify --format json emits a structured result", () => {
// Narration moved to stderr so it cannot corrupt the document.
expect(err).toContain("meta verify");
- expect(out).not.toContain("running --templates");
+ expect(out).not.toContain("running --codegen --templates");
} finally {
rmSync(dir, { recursive: true, force: true });
}
}, TIMEOUT_MS);
test("--format toon emits a TOON document with a tabular findings block", async () => {
- const dir = project();
+ const dir = await generatedProject();
try {
const { exit, out } = await capture(["verify", "--format", "toon", "--cwd", dir]);
expect(exit).toBe(0);
@@ -257,7 +269,7 @@ describe("text mode caps, --limit raises it, and every site honors one constant"
}, TIMEOUT_MS);
test("meta verify text output caps at the same default — one constant, not one per site", async () => {
- const dir = project();
+ const dir = await generatedProject();
try {
const { exit, err } = await capture(["verify", "--format", "text", "--cwd", dir]);
expect(exit).toBe(0);
@@ -300,7 +312,7 @@ describe("text mode caps, --limit raises it, and every site honors one constant"
// one that silently drops the API prefix from every generated hook. Starving the small
// list is the worse failure, so each list gets its own budget and the section total can
// reach a multiple of the cap.
- const dir = project(`\n apiPrefix: "/api",`, 5);
+ const dir = await generatedProject(`\n apiPrefix: "/api",`, 5);
try {
const { exit, err } = await capture(["verify", "--format", "text", "--limit", "3", "--cwd", dir]);
expect(exit).toBe(0);
@@ -318,7 +330,7 @@ describe("text mode caps, --limit raises it, and every site honors one constant"
}, TIMEOUT_MS);
test("...and the structured payload still carries both lists in ONE section, uncapped", async () => {
- const dir = project(`\n apiPrefix: "/api",`, 5);
+ const dir = await generatedProject(`\n apiPrefix: "/api",`, 5);
try {
const { exit, out } = await capture(["verify", "--format", "json", "--limit", "1", "--cwd", dir]);
expect(exit).toBe(0);
@@ -333,7 +345,7 @@ describe("text mode caps, --limit raises it, and every site honors one constant"
}, TIMEOUT_MS);
test("--limit never truncates a structured payload, whatever it is set to", async () => {
- const dir = project();
+ const dir = await generatedProject();
try {
const { exit, out } = await capture(["verify", "--format", "json", "--limit", "1", "--cwd", dir]);
expect(exit).toBe(0);
@@ -597,14 +609,14 @@ describe("verify.antiPatternIgnore reaches the scan from metaobjects.config.ts",
describe("verify wires the base-URL advisory", () => {
const PROVIDER = `\n \n\n`;
- function withProvider(apiPrefix: string): string {
- const dir = project(`\n apiPrefix: ${JSON.stringify(apiPrefix)},`);
+ async function withProvider(apiPrefix: string): Promise {
+ const dir = await generatedProject(`\n apiPrefix: ${JSON.stringify(apiPrefix)},`);
writeFileSync(join(dir, "src", "main.tsx"), PROVIDER);
return dir;
}
test("a non-empty apiPrefix + a provider with no baseUrl reaches the payload", async () => {
- const dir = withProvider("/api");
+ const dir = await withProvider("/api");
try {
const { exit, out } = await capture(["verify", "--cwd", dir, "--format", "json"]);
const payload = JSON.parse(out) as { antiPatterns: AdvisoryBlock };
@@ -619,7 +631,7 @@ describe("verify wires the base-URL advisory", () => {
});
test("the text header names the count and the prefix", async () => {
- const dir = withProvider("/api");
+ const dir = await withProvider("/api");
try {
const { err } = await capture(["verify", "--cwd", dir]);
expect(err).toContain("entity-fetcher provider(s) mounted with no baseUrl");
@@ -628,7 +640,7 @@ describe("verify wires the base-URL advisory", () => {
});
test("an empty apiPrefix emits nothing — the meta init scaffold is not nagged", async () => {
- const dir = withProvider("");
+ const dir = await withProvider("");
try {
const { out } = await capture(["verify", "--cwd", dir, "--format", "json"]);
const payload = JSON.parse(out) as { antiPatterns: AdvisoryBlock };
@@ -637,7 +649,7 @@ describe("verify wires the base-URL advisory", () => {
});
test("--no-antipatterns suppresses it, which is why the migration note says so", async () => {
- const dir = withProvider("/api");
+ const dir = await withProvider("/api");
try {
const { out } = await capture(["verify", "--cwd", dir, "--format", "json", "--no-antipatterns"]);
const payload = JSON.parse(out) as { antiPatterns: AdvisoryBlock };
diff --git a/server/typescript/packages/cli/test/integration/verify-subverbs.test.ts b/server/typescript/packages/cli/test/integration/verify-subverbs.test.ts
index 315553cb2..b8cf8ce3a 100644
--- a/server/typescript/packages/cli/test/integration/verify-subverbs.test.ts
+++ b/server/typescript/packages/cli/test/integration/verify-subverbs.test.ts
@@ -3,7 +3,7 @@
*
* Asserts the explicit-subverb contract:
* - `verify --templates` runs the template/prompt drift gate (clean → 0, drift → 1).
- * - bare `verify` is back-compat (= --templates) and prints the subverb note.
+ * - bare `verify` runs the default gate set (--codegen --templates) and prints the subverb note.
* - `verify --codegen` regenerates to a temp dir and diffs the committed output:
* committed == fresh regen → 0; generated content that has gone STALE against
* the metadata → 1, naming the drifted file. A hand-edited generated file is
@@ -124,8 +124,8 @@ describe("meta verify — subverbs (ADR-0021 D2)", () => {
}
});
- // 3. Bare verify = --templates (back-compat) + prints the subverb note.
- test("bare verify behaves as --templates and prints the subverb note", async () => {
+ // 3. Bare verify = the default gate set + prints the subverb note.
+ test("bare verify runs the template gate and prints the subverb note", async () => {
const tmp = setupTemplateRepo("Hi {{notARealField}}.");
try {
expect(await run(["verify", "--cwd", tmp])).toBe(1);
diff --git a/server/typescript/packages/cli/test/verify-bare-default-gates.test.ts b/server/typescript/packages/cli/test/verify-bare-default-gates.test.ts
new file mode 100644
index 000000000..bb63a91ab
--- /dev/null
+++ b/server/typescript/packages/cli/test/verify-bare-default-gates.test.ts
@@ -0,0 +1,124 @@
+/**
+ * A bare `meta verify` runs the cross-port default gate set — codegen drift, prompt
+ * templates, the requirement ledger — so "run `meta verify` in CI" catches a model that
+ * changed without `meta gen` being re-run. `--db`, `--deps` and `--replay` stay opt-in; a
+ * project with no `metaobjects.config.ts` has nothing generated to diff, so the codegen
+ * gate is skipped with a note rather than failed. Naming a gate still runs only that gate.
+ */
+import { describe, test, expect, beforeEach, afterEach } from "bun:test";
+import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { run } from "../src/index.js";
+
+const entity = (extra: unknown[] = []): string => JSON.stringify({
+ "metadata.root": {
+ package: "app",
+ children: [
+ {
+ "object.entity": {
+ name: "Order",
+ children: [
+ { "source.rdb": { "@table": "orders" } },
+ { "field.long": { name: "id" } },
+ ...extra,
+ { "identity.primary": { name: "pk", "@fields": ["id"] } },
+ ],
+ },
+ },
+ ],
+ },
+});
+
+function setupRepo(opts: { withConfig: boolean }): string {
+ const root = mkdtempSync(join(tmpdir(), "verify-bare-default-"));
+ mkdirSync(join(root, "metaobjects"), { recursive: true });
+ writeFileSync(join(root, "metaobjects", "meta.app.json"), entity(), "utf8");
+ if (opts.withConfig) {
+ writeFileSync(
+ join(root, "metaobjects.config.ts"),
+ `import { defineConfig } from "@metaobjectsdev/codegen-ts";
+export default defineConfig({
+ outDir: ${JSON.stringify(join(root, "generated", "db"))},
+ dialect: "sqlite",
+ dbImport: "~/db",
+ extStyle: "none",
+ generators: ["entity"],
+});
+`,
+ );
+ }
+ return root;
+}
+
+/** Change the model after `meta gen`, leaving the generated output stale. */
+function staleTheModel(root: string): void {
+ writeFileSync(
+ join(root, "metaobjects", "meta.app.json"),
+ entity([{ "field.string": { name: "note" } }]),
+ "utf8",
+ );
+}
+
+let out: string[];
+let origLog: typeof console.log;
+let origErr: typeof console.error;
+beforeEach(() => {
+ out = [];
+ origLog = console.log;
+ origErr = console.error;
+ console.log = (...a: unknown[]) => { out.push(a.map(String).join(" ")); };
+ console.error = (...a: unknown[]) => { out.push(a.map(String).join(" ")); };
+});
+afterEach(() => {
+ console.log = origLog;
+ console.error = origErr;
+});
+
+describe("meta verify — the bare run's default gate set", () => {
+ test("bare verify fails on stale generated code", async () => {
+ const root = setupRepo({ withConfig: true });
+ try {
+ expect(await run(["gen", "--cwd", root])).toBe(0);
+ expect(await run(["verify", "--cwd", root])).toBe(0);
+ staleTheModel(root);
+ out = [];
+ expect(await run(["verify", "--cwd", root])).toBe(1);
+ expect(out.join("\n")).toContain("Order.ts");
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ });
+
+ test("naming --templates still runs only the template gate", async () => {
+ const root = setupRepo({ withConfig: true });
+ try {
+ expect(await run(["gen", "--cwd", root])).toBe(0);
+ staleTheModel(root);
+ expect(await run(["verify", "--cwd", root, "--templates"])).toBe(0);
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ });
+
+ test("a bare run with no metaobjects.config.ts skips codegen with a note and passes", async () => {
+ const root = setupRepo({ withConfig: false });
+ try {
+ expect(await run(["verify", "--cwd", root])).toBe(0);
+ expect(out.join("\n")).toContain("codegen gate skipped: no metaobjects.config.ts");
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ });
+
+ test("a bare run with a metaobjects.config.ts that fails to load exits 2 with the load error", async () => {
+ const root = setupRepo({ withConfig: false });
+ try {
+ writeFileSync(join(root, "metaobjects.config.ts"), `export default { this is not valid typescript`, "utf8");
+ expect(await run(["verify", "--cwd", root])).toBe(2);
+ expect(out.join("\n")).toContain("failed to load");
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+ });
+});
diff --git a/server/typescript/packages/cli/test/verify-not-run-note.test.ts b/server/typescript/packages/cli/test/verify-not-run-note.test.ts
index fc9d18d3c..86bfe0a68 100644
--- a/server/typescript/packages/cli/test/verify-not-run-note.test.ts
+++ b/server/typescript/packages/cli/test/verify-not-run-note.test.ts
@@ -84,6 +84,6 @@ describe("meta verify — a partial run names the gates that did not run", () =>
for (const gate of ["codegen (--codegen)", "schema (--db )", "deps (--deps)", "replay (--replay)"]) {
expect(line).toContain(gate);
}
- expect(line).toContain("a bare 'meta verify' runs only the template gate");
+ expect(line).toContain("a bare 'meta verify' runs the codegen and template gates");
});
});
diff --git a/spec/decisions/ADR-0021-codegen-surface-coherence.md b/spec/decisions/ADR-0021-codegen-surface-coherence.md
index a2a75f87a..0cc15365b 100644
--- a/spec/decisions/ADR-0021-codegen-surface-coherence.md
+++ b/spec/decisions/ADR-0021-codegen-surface-coherence.md
@@ -57,6 +57,10 @@ A bare `verify` keeps a documented default per port (back-compat) but the
explicit subverbs are the contract. C#'s current `verify` (template drift)
becomes `verify --templates`; ports add `--codegen` for parity over time.
+*Amendment (unreleased): a bare `verify` no longer has a per-port default. Every port runs the
+same set (codegen drift, templates, the requirement ledger) and skips a gate it has no
+configuration for, with a note. See [cli.md](../../docs/features/cli.md).*
+
### D3 — Uniform generator selection by STABLE NAME + a registry + `--list`
Generators are selected by a **stable string id** (e.g. `entity`, `routes`,
`render-helper`, `extractor`), not by language-specific factory imports / FQNs /