// tests/lib/skills-annotating-prose-pins.test.mjs // Wave 4, order B: the `annotating` skill and its wiring into the three // producing commands (/trekbrief, /trekplan, /trekreview). // // What is pinned, and why: // • the skill's form is not pinned here: the form check // (lib/skills/skill-lint.mjs) and the routing test (lib/skills/routing.mjs) // run over every skill in `npm test` and are the authority (contract S1); // • the three commands ask for `voyage:annotating` by name (contract R3); // • the long explanation of the page and the loop left the commands: the // skill is the one source. What each command keeps is the call to // scripts/annotate.mjs, the "not a gate" line and the printed report, // which tests/lib/doc-consistency.test.mjs also pins; // • /trekresearch gets no annotation step (decision 62): the skill says it // can be used on a research document when the user asks; // • the loop the skill carries, aimed at what the red baseline showed; // • the skill says plainly what is deterministic and what is prose only. // Counted as doc pins by lib/util/test-census.mjs (file name *-prose-pins). import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const read = (rel) => readFileSync(join(ROOT, rel), 'utf8'); const flat = (t) => t.replace(/\s+/g, ' '); const SKILL = 'skills/annotating/SKILL.md'; const skill = () => { assert.ok(existsSync(join(ROOT, SKILL)), `${SKILL} missing`); return read(SKILL); }; /** Text from `## ` up to the next `## ` heading. */ function section(text, heading) { const start = text.indexOf(`\n## ${heading}`); assert.ok(start >= 0, `missing ## ${heading}`); const next = text.indexOf('\n## ', start + 4); return text.slice(start, next < 0 ? undefined : next); } const PRODUCERS = ['trekbrief.md', 'trekplan.md', 'trekreview.md']; // ---- the commands ask for the skill, and keep only the call and the report ---- test('pin: the three producing commands ask for voyage:annotating by name', () => { for (const f of PRODUCERS) { assert.ok(read(`commands/${f}`).includes('`voyage:annotating`'), `commands/${f} must name voyage:annotating`); } }); test('pin: the explanation the skill took over is gone from the commands', () => { // The sentences that described the page and the loop in each command // (1b61342: trekbrief.md 833-842, trekplan.md 1189-1197, trekreview.md // 694-699). The page is described in the skill and the README; the loop // ("revises freehand") is what the skill replaces. const moved = [ /lets the operator click any line to attach/i, /keeps a sidebar of all notes/i, /persists state in localStorage/i, /exposes a "Copy Prompt" button/i, /freehand from the notes/i, ]; for (const f of PRODUCERS) { const t = flat(read(`commands/${f}`)); for (const re of moved) assert.doesNotMatch(t, re, `commands/${f} still carries: ${re}`); } }); test('pin: each command keeps the call, the not-a-gate line and the printed links', () => { for (const f of PRODUCERS) { const t = read(`commands/${f}`); assert.match(t, /ANNOT_HTML=\$\(node \$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/annotate\.mjs "/, `${f}: the call`); assert.match(flat(t), /convenience, not a gate/, `${f}: a failed page never blocks`); assert.match(t, /open file:\/\/\{\$ANNOT_HTML\}/, `${f}: the open line`); assert.match(t, /YOUR OWN/, `${f}: the operator writes the notes`); } for (const f of ['trekbrief.md', 'trekplan.md']) { assert.match(read(`commands/${f}`), /Annotation HTML:\s+file:\/\/\{\$ANNOT_HTML\}/, `${f}: the report line`); } }); test('pin: /trekresearch has no annotation step (decision 62)', () => { const t = read('commands/trekresearch.md'); assert.ok(!t.includes('annotate.mjs'), 'trekresearch must not run the annotation script'); }); // ---- the skill carries the loop, aimed at what the red baseline showed ---- // Baseline (order B, 3 runs per scenario, no skill): 3 of 3 changed the plan // because of a Question; 3 of 3 applied a Fix to steps the note did not name; // 2 of 3 edited brief text no note asked for; 1 of 3 wrote its own critique // next to the page; 0 of 3 named the stale approval before the edit. test('pin: annotating builds the page with the script and leaves every note to the person', () => { const p = flat(section(skill(), 'Process')); assert.match(p, /node \$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/annotate\.mjs/); assert.match(p, /The person writes the notes\. Claude writes none\./); assert.match(p, /convenience, never a gate/); }); test('pin: annotating — a Question is answered and the text stays as it was', () => { const p = flat(section(skill(), 'Process')); assert.match(p, /\*\*Question: answer it in the reply\. The text stays as it was\.\*\*/); assert.match(p, /Only their yes turns a Question into a change/); }); test('pin: annotating — Fix and Change edit only what the note points at; the rest is asked', () => { const p = flat(section(skill(), 'Process')); assert.match(p, /\*\*Fix and Change: edit the text the note points at, and only that\.\*\*/); assert.match(p, /the same gap in another step/); assert.match(p, /already there before the notes is reported, not fixed in passing/); }); test('pin: annotating — the approval an edit costs is said before the edit', () => { const p = flat(section(skill(), 'Process')); assert.match(p, /`## Intent` or `## Goal`[^.]*before the edit, that the edit makes the approval stale/); assert.match(p, /new sha256[^.]*`\/trekexecute` refuses it until the plan is approved again\. Say that before the edit\./); assert.match(p, /Never stamp, log or commit an approval in this loop/); }); test('pin: annotating — the page is built again, and a research document is covered on request', () => { assert.match(flat(section(skill(), 'Process')), /\*\*Build the page again\*\* on the revised file/); assert.match(flat(section(skill(), 'When to use')), /research document under `research\/`\. `\/trekresearch` does not build the page itself/); assert.match(flat(skill()), /"Clear all"/); }); test('pin: annotating says what is enforced and what is prose only', () => { const e = flat(section(skill(), 'Enforced by')); for (const p of ['tests/lib/doc-consistency.test.mjs', 'scripts/annotate.mjs', 'lib/validators/intent-approval.mjs', 'lib/util/approval-log.mjs']) { assert.ok(e.includes(`\`${p}\``), `Enforced by must name ${p}`); } assert.match(e, /prose only: that the person writes every note and Claude none, that a Question changes nothing/); }); test("pin: annotating is Voyage's own content", () => { assert.match(flat(section(skill(), 'Credits')), /Voyage's own content: [\s\S]*Nothing here is borrowed\./); });