The three producing commands name voyage:annotating where they build the page; the description of the page and the freehand revision left them. The skill: the person writes every note, Fix/Change edit only the quoted text, a Question is answered and changes nothing, the approval an edit costs is said before the edit, the page is rebuilt. Research documents on request (decision 62). Red baseline 3 runs per scenario without and with the skill; six eval cases; 11 pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140 lines
6.9 KiB
JavaScript
140 lines
6.9 KiB
JavaScript
// 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 `## <heading>` 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\./);
|
|
});
|