feat(skills): voyage:annotating, the operator-annotation loop as one skill (order B)
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>
This commit is contained in:
parent
1b613421eb
commit
843897ae33
25 changed files with 550 additions and 46 deletions
140
tests/lib/skills-annotating-prose-pins.test.mjs
Normal file
140
tests/lib/skills-annotating-prose-pins.test.mjs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// 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\./);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue