fix(gate): repair the count I clobbered, and close the drift check's blind spot

Two defects an independent review of the previous two commits found.

`4fec1423` changed `claudeCode.commands!.length` to `claudeCode.skills!.length` in
the commands loop's summary line, so the gate reported the skill count under a
"manifest commands" label — 26 where 17 were checked. The edit was an anchoring
slip: the replacement text carried `skills`, and it was accepted against a line
reading `commands`. Restored, keeping `?? []` so a manifest without a commands
array reports 0 rather than throwing on a non-null assertion.

The drift check was one-directional. It iterated only the committed array, so a
skill present on disk but absent from the manifest — adding a suite and forgetting
to regenerate, the same mistake pointing the other way — passed `npm test` while
the unit suite caught it. It now compares the name sets in both directions, and
flags a manifest entry with no skill behind it as well.

Verified in four states: description drift, a skill missing from the manifest, a
manifest entry with no skill on disk, and clean. Each injecting case is caught and
names the offending entry; the clean case reports 26 descriptions matching.

Verified: npm test (static clean), npm run test:unit (524/524).
This commit is contained in:
Charles Wiltgen
2026-09-17 10:59:43 -07:00
parent 5ed60b9e05
commit ae70a4d240
+42 -16
View File
@@ -352,7 +352,7 @@ if (claudeCode) {
}
}
console.log(
`${claudeCode.skills!.length} manifest commands checked against filesystem`,
`${(claudeCode.commands ?? []).length} manifest commands checked against filesystem`,
);
// Existence is not parity. `skills[]` is generated from each SKILL.md's
@@ -363,24 +363,50 @@ if (claudeCode) {
// manifest, and in the /axiom:ask generated from it, through a green local
// gate and four red CI runs. Compare the text here too, so the cheap local
// command catches what CI catches.
const expectedDescriptions: Record<string, string> = Object.fromEntries(
manifestSkillsFromDisk(
pluginDir,
(claudeCode.skills ?? []).map((s: { name: string }) => s.name),
).map((e) => [e.name, e.description]),
const committedSkills: { name: string; description: string }[] =
claudeCode.skills ?? [];
const generated = manifestSkillsFromDisk(
pluginDir,
committedSkills.map((s) => s.name),
);
const drifted = (claudeCode.skills ?? []).filter((skill: { name: string; description: string }) => {
const want = expectedDescriptions[skill.name];
return want !== undefined && want !== skill.description;
});
if (drifted.length > 0) {
error(
"manifest-drift",
`${drifted.length} manifest description(s) drifted from SKILL.md frontmatter, starting with "${drifted[0].name}" — regenerate with \`npm run build:manifest\``,
);
const expectedDescriptions: Record<string, string> = Object.fromEntries(
generated.map((e) => [e.name, e.description]),
);
const drifted = committedSkills.filter(
(skill) =>
expectedDescriptions[skill.name] !== undefined &&
expectedDescriptions[skill.name] !== skill.description,
);
// Both directions. Iterating only the committed array is blind to a skill that
// is on disk but missing from the manifest — adding a suite and forgetting to
// regenerate, the same mistake pointing the other way — and to an entry whose
// skill is gone or manifest-excluded. The unit suite's deepEqual catches those;
// this is the cheap command, so it has to as well.
const committedNames = new Set(committedSkills.map((s) => s.name));
const absent = generated.filter((e) => !committedNames.has(e.name)).map((e) => e.name);
const extra = committedSkills
.map((s) => s.name)
.filter((name) => expectedDescriptions[name] === undefined);
if (drifted.length > 0 || absent.length > 0 || extra.length > 0) {
const detail = [
drifted.length > 0
? `${drifted.length} description(s) drifted from SKILL.md frontmatter, starting with "${drifted[0].name}"`
: "",
absent.length > 0
? `${absent.length} skill(s) on disk are missing from the manifest, starting with "${absent[0]}"`
: "",
extra.length > 0
? `${extra.length} manifest entr(ies) have no corresponding skill on disk, starting with "${extra[0]}"`
: "",
]
.filter(Boolean)
.join("; ");
error("manifest-drift", `${detail} — regenerate with \`npm run build:manifest\``);
} else {
console.log(
`${claudeCode.skills!.length} manifest descriptions match SKILL.md frontmatter`,
`${committedSkills.length} manifest descriptions match SKILL.md frontmatter`,
);
}
}