diff --git a/hooks/ponytail-instructions.js b/hooks/ponytail-instructions.js index f3da002..3ec3980 100644 --- a/hooks/ponytail-instructions.js +++ b/hooks/ponytail-instructions.js @@ -25,7 +25,11 @@ function filterSkillBodyForMode(body, mode) { if (labelMode) return labelMode === effectiveMode; } - const exampleLabel = line.match(/^-\s*([^:]+):\s*/); + // Require a quoted value: every worked example is `- lite: "..."`. Without + // this, an ordinary rule bullet that happens to start with a mode word + // (e.g. "- Full: ...") is silently dropped in every other mode — it looks + // like a worked example but is really prose meant to survive verbatim. + const exampleLabel = line.match(/^-\s*([^:]+):\s*"/); if (exampleLabel) { const labelMode = normalizeMode(exampleLabel[1].trim()); if (labelMode) return labelMode === effectiveMode; diff --git a/pi-extension/test/helpers.test.js b/pi-extension/test/helpers.test.js index 12bf722..00258e2 100644 --- a/pi-extension/test/helpers.test.js +++ b/pi-extension/test/helpers.test.js @@ -104,7 +104,9 @@ test("readQuietStartup resolves env var, config file, and default in that order" }); test("filterSkillBodyForMode keeps only requested intensity examples and rows", () => { - const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n| **ultra** | keep ultra |\n- lite: Lite example\n- full: Full example\n- ultra: Ultra example\nOther line`; + // Examples are quoted in the real SKILL.md (`- lite: "..."`) — match that + // shape here too; see the next test for why the quote is load-bearing. + const body = `---\nname: ponytail\n---\n| **lite** | keep lite |\n| **full** | keep full |\n| **ultra** | keep ultra |\n- lite: "Lite example"\n- full: "Full example"\n- ultra: "Ultra example"\nOther line`; const filtered = filterSkillBodyForMode(body, "ultra"); @@ -116,6 +118,20 @@ test("filterSkillBodyForMode keeps only requested intensity examples and rows", assert.ok(filtered.includes("Other line")); }); +test("filterSkillBodyForMode does not drop a rule bullet whose label matches a mode name", () => { + // A rule bullet like "- Full: ..." has the same "label: text" shape as a + // worked example, but isn't one — it must survive in every mode. Only the + // quoted, `- lite: "..."`-style bullets are real per-mode examples. + const body = `- Full: do not confuse this rule label with the mode name.\n- Lite: same risk, this is a real rule bullet.\n- lite: "real worked example"\n- ultra: "real worked example"`; + + const filtered = filterSkillBodyForMode(body, "ultra"); + + assert.ok(filtered.includes("Full: do not confuse"), "an unquoted rule bullet must not be treated as a mode example"); + assert.ok(filtered.includes("Lite: same risk"), "an unquoted rule bullet must not be treated as a mode example"); + assert.ok(!filtered.includes("- lite:"), "the real quoted lite example must still be filtered out in ultra mode"); + assert.ok(filtered.includes('ultra: "real worked example"')); +}); + test("filterSkillBodyForMode keeps rule bullets that contain a colon", () => { // Regression: rule bullets outside the Intensity section (e.g. the // "No unrequested abstractions:" rule or the `ponytail:` comment convention)