Skip to content

Commit 8a371a7

Browse files
dmealingclaude
andcommitted
fix(cli): verify --templates reported a different denominator passing vs failing
The same project, the same run: "11 drift error(s) across 29 template(s)" while red, "22 template(s) clean" once fixed. Seven templates appear to vanish on the way to green. Both numbers were real; neither line named its unit. The failure line divided by every `template.*` node found — INCLUDING every one the loop `continue`s past, which on a real project means each project-local template subtype from the adopter's own provider. The pass line divided by BODIES verified, and an `@kind: email` template has up to three bodies while being one template. So the red line took credit for work it had not done, and the two lines could not be compared with each other. Both now count templates at least one body of which was actually examined. Found in the drift-gate demo receipt on the public reference app — the artifact whose entire purpose is to be checked by a skeptical reader, and the asset the launch plan names for the Show HN capstone. Every existing assertion matched on a substring and both phrasings contain "template(s)", so nothing could see it. The new test asserts the PAIR — same fixture, same number passing and failing — because the failure half alone was already correct for a single-body template. Confirmed non-vacuous by reverting the reporting and watching it go red. cli suite green: 678 pass, 0 fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NynrRND6ZUwGvq3ZUTCfxG
1 parent 11af128 commit 8a371a7

3 files changed

Lines changed: 97 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
### Fixed — `meta verify --templates` reported a different denominator depending on whether it passed
11+
12+
The same project, the same run: **"11 drift error(s) across 29 template(s)"** while red, and
13+
**"22 template(s) clean"** once fixed. Seven templates appear to vanish on the way to green.
14+
Both numbers were real and neither line named its actual unit — the failure line divided by every
15+
`template.*` node found, **including every one the loop skips** (a subtype it does not check —
16+
a project-local `template.*` from an adopter's own provider), while the pass line divided by
17+
**bodies verified** (an `@kind: email` template has up to three and is one template). So the red
18+
line claimed a denominator of work it had not done, and the two lines could not be compared to
19+
each other at all. Both now count templates at least one body of which was actually examined.
20+
21+
Found in the drift-gate demo receipt on the public reference app — the artifact whose whole
22+
purpose is to be checked by a skeptical reader. The existing tests could not see it: every
23+
assertion matched on a substring, and both phrasings contain the word `template(s)`. The new
24+
test asserts the **pair** — the same fixture reports the same number passing and failing —
25+
because the failure half alone was already correct for a single-body template.
26+
1027
### Fixed — three first-touch defects, found running the quickstart cold on the published `0.24.3`
1128

1229
`0.24.3` made the cold quickstart part of the release procedure. Run against the published

server/typescript/packages/cli/src/commands/verify.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,18 @@ export async function verifyCommand(
581581

582582
let errorCount = 0;
583583
let warnCount = 0;
584-
let checked = 0;
584+
// Both report lines below must divide by the SAME thing, and it must be
585+
// something that was actually examined. They used to disagree: the failure line
586+
// divided by `templates.length` — every node found, INCLUDING every one the loop
587+
// `continue`s past (unknown subtype, no renderable body ref) — while the pass
588+
// line divided by a count of bodies verified. On a real project the same run read
589+
// "11 drift error(s) across 29 template(s)" while failing and "22 template(s)
590+
// clean" once fixed: seven templates apparently vanishing on the way to green,
591+
// and a failure line claiming a denominator of work that had not been done.
592+
// `checkedTemplates` is now the single unit — templates at least one body of
593+
// which was verified. (An @kind=email template has up to three bodies and still
594+
// counts once; the line says "template(s)", so it counts templates.)
595+
let checkedTemplates = 0;
585596

586597
for (const tmpl of templates) {
587598
// ADR-0039: effective attrs — @payloadRef may be inherited via an abstract template.
@@ -645,6 +656,7 @@ export async function verifyCommand(
645656
const requiredSlots = promptRules ? attrAsStringArray(tmpl.attr(TEMPLATE_ATTR_REQUIRED_SLOTS)) : [];
646657
const requiredTags = promptRules ? attrAsStringArray(tmpl.attr(TEMPLATE_ATTR_REQUIRED_TAGS)) : [];
647658

659+
let anyBodyChecked = false;
648660
for (const { label, ref } of refs) {
649661
// Render-engine drift check: mustache variables ↔ payload field names.
650662
const text = provider.resolve(ref);
@@ -656,7 +668,7 @@ export async function verifyCommand(
656668
continue;
657669
}
658670
const drift = verify(text, fieldTree, { provider, requiredSlots, requiredTags });
659-
checked++;
671+
anyBodyChecked = true;
660672
for (const e of drift) {
661673
if (e.code === ERR_REQUIRED_SLOT_UNUSED) {
662674
log.warn(`[${tmpl.name}] (${label}) ${e.code}: ${e.path}`);
@@ -667,16 +679,17 @@ export async function verifyCommand(
667679
}
668680
}
669681
}
682+
if (anyBodyChecked) checkedTemplates++;
670683
}
671684

672685
if (errorCount > 0) {
673686
log.error(
674-
`meta verify — ${errorCount} drift error(s) across ${templates.length} template(s).`,
687+
`meta verify — ${errorCount} drift error(s) across ${checkedTemplates} template(s).`,
675688
);
676689
return 1;
677690
}
678691
log.info(
679-
`meta verify — ${checked} template(s) clean${warnCount > 0 ? ` (${warnCount} warning(s))` : ""}.`,
692+
`meta verify — ${checkedTemplates} template(s) clean${warnCount > 0 ? ` (${warnCount} warning(s))` : ""}.`,
680693
);
681694
return 0;
682695
}

server/typescript/packages/cli/test/integration/verify.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,69 @@ describe("meta verify", () => {
178178
}
179179
});
180180

181+
// The two report lines must divide by the SAME thing for the SAME project. They
182+
// did not: the pass line counted BODIES verified and the failure line counted every
183+
// template NODE found, skipped ones included. An @kind=email template has two or
184+
// three bodies and is one template, so it separates the units on its own — and a
185+
// real project read "11 drift error(s) across 29 template(s)" while red and
186+
// "22 template(s) clean" once green, seven templates apparently vanishing.
187+
//
188+
// Asserting the PAIR is what makes this non-vacuous: the failure half alone was
189+
// already 1 under the old code. (The other half of the old inflation — a template
190+
// the loop skips — needs a project-local `template.*` subtype from a provider, the
191+
// shape that produced the 29-vs-22 above; core vocabulary requires a body on every
192+
// prompt and email, so it cannot be built from core alone.)
193+
describe("both report lines divide by the same denominator", () => {
194+
function scaffoldEmail(htmlBody: string): string {
195+
const tmp = mkdtempSync(join(tmpdir(), "metaobjects-verify-denom-"));
196+
mkdirSync(join(tmp, "metaobjects"), { recursive: true });
197+
writeFileSync(join(tmp, "metaobjects", "meta.ai.json"), JSON.stringify({
198+
"metadata.root": {
199+
package: "acme::ai",
200+
children: [
201+
{ "object.value": { name: "P", children: [{ "field.string": { name: "name" } }] } },
202+
{
203+
// ONE template, TWO renderable bodies — the shape that separates the units.
204+
"template.output": {
205+
name: "Welcome",
206+
"@kind": "email",
207+
"@payloadRef": "P",
208+
"@subjectRef": "e/subj",
209+
"@htmlBodyRef": "e/html",
210+
},
211+
},
212+
],
213+
},
214+
}), "utf8");
215+
mkdirSync(join(tmp, "prompts", "e"), { recursive: true });
216+
writeFileSync(join(tmp, "prompts", "e", "subj"), "Hello {{name}}", "utf8");
217+
writeFileSync(join(tmp, "prompts", "e", "html"), htmlBody, "utf8");
218+
return tmp;
219+
}
220+
221+
test("counts TEMPLATES, not bodies, when it passes", async () => {
222+
const tmp = scaffoldEmail("<p>Hi {{name}}</p>");
223+
try {
224+
expect(await run(["verify", "--cwd", tmp])).toBe(0);
225+
const all = [...out, ...err].join("\n");
226+
expect(all).toContain("1 template(s) clean");
227+
expect(all).not.toContain("2 template(s) clean"); // two bodies, one template
228+
} finally {
229+
rmSync(tmp, { recursive: true, force: true });
230+
}
231+
});
232+
233+
test("and reports that same 1 when it fails", async () => {
234+
const tmp = scaffoldEmail("<p>Hi {{nonExistentField}}</p>");
235+
try {
236+
expect(await run(["verify", "--cwd", tmp])).toBe(1);
237+
expect([...out, ...err].join("\n")).toContain("across 1 template(s)");
238+
} finally {
239+
rmSync(tmp, { recursive: true, force: true });
240+
}
241+
});
242+
});
243+
181244
test("a custom --prompts dir is honored", async () => {
182245
const tmp = mkdtempSync(join(tmpdir(), "metaobjects-verify-custom-"));
183246
mkdirSync(join(tmp, "metaobjects"), { recursive: true });

0 commit comments

Comments
 (0)