chore(voyage): release v5.0.2 — operator-driven annotation HTML (scripts/annotate.mjs)

v5.0.0 added a read-only HTML render. v5.0.1 deleted that and pointed at
/playground document-critique, which pre-generates Claude's suggestions
and asks the operator to approve/reject them. The operator asked for the
opposite — a surface where THEY drive every annotation. v5.0.2 lands it.

scripts/annotate.mjs (~430 lines, zero deps) takes any artifact .md and
writes a self-contained HTML next to it. The HTML renders the document
with line numbers, lets the operator click any line to add their own
note (inline textarea, save with Cmd+Enter or button), keeps a sidebar
of all notes (editable + deletable + persisted in localStorage per
artifact path), and exposes Copy Prompt to gather every note into one
structured prompt. Operator copies, pastes back, Claude revises the .md.

The three producing commands now run annotate.mjs at their last step and
print the file:// link with explicit "Click any line to add YOUR OWN note"
instructions. The v5.0.1 /playground document-critique line is gone.

npm test green: 516 tests, 514 pass, 0 fail, 2 skipped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-13 14:04:28 +02:00
commit 8ea692bc60
15 changed files with 995 additions and 118 deletions

View file

@ -430,42 +430,67 @@ test('Handover 8 deleted from HANDOVER-CONTRACTS.md (back to seven handovers)',
assert.ok(text.includes('## Handover 7'), 'Handover 7 must remain');
});
test('scripts/render-artifact.mjs no longer exists (removed in v5.0.1)', () => {
test('scripts/render-artifact.mjs is still removed (v5.0.1 + v5.0.2)', () => {
assert.ok(
!existsSync(join(ROOT, 'scripts/render-artifact.mjs')),
'scripts/render-artifact.mjs should be deleted — v5.0.1 drops the redundant standalone HTML render in favour of the /playground document-critique invocation printed by the producing commands',
'scripts/render-artifact.mjs should be deleted — v5.0.1 dropped the standalone HTML render; v5.0.2 kept it removed (annotate.mjs is the replacement)',
);
});
test('producing commands print a literal /playground document-critique invocation', () => {
// The exact substring must appear in each producing command's prose so the
// operator copy-pastes a verbatim line. Drift on this is the friction point
// that motivated v5.0.1 — fail loudly if the prose softens back to "run the
// /playground plugin" without the literal command.
const REQUIRED = '/playground build a document-critique playground for';
test('scripts/annotate.mjs exists (v5.0.2 operator-annotation HTML generator)', () => {
assert.ok(
existsSync(join(ROOT, 'scripts/annotate.mjs')),
'scripts/annotate.mjs is required — producing commands call it to build the operator-annotation HTML',
);
});
test('producing commands reference scripts/annotate.mjs (v5.0.2 render-and-link step)', () => {
// v5.0.0 → v5.0.1 → v5.0.2 chain: v5.0.0 added an HTML render that didn't
// afford annotation; v5.0.1 pointed at /playground document-critique (which
// pre-generates Claude's suggestions, not operator-driven annotation); v5.0.2
// ships scripts/annotate.mjs — an operator-driven annotation surface where
// the OPERATOR clicks lines and writes their own notes. Pin the wiring.
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
assert.ok(
read(`commands/${f}`).includes(REQUIRED),
`commands/${f} must include the literal invocation "${REQUIRED}" so the operator copy-pastes it directly (v5.0.1)`,
read(`commands/${f}`).includes('scripts/annotate.mjs'),
`commands/${f} must invoke scripts/annotate.mjs to build the operator-annotation HTML (v5.0.2)`,
);
}
});
test('producing commands no longer reference the removed scripts/render-artifact.mjs', () => {
test('producing commands no longer print the v5.0.1 /playground document-critique line', () => {
// v5.0.1 told operators to copy-paste "/playground build a document-critique
// playground for X" — but that flow pre-generates Claude's suggestions. The
// operator asked for their own annotations, not a critique of Claude's.
// v5.0.2 removes that line from the producing commands' final report.
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
assert.ok(
!read(`commands/${f}`).includes('render-artifact.mjs'),
`commands/${f} still references scripts/render-artifact.mjs — that script was removed in v5.0.1`,
!read(`commands/${f}`).includes('/playground build a document-critique'),
`commands/${f} must not print the v5.0.1 /playground document-critique invocation — v5.0.2 replaces it with annotate.mjs`,
);
}
});
test('package.json no longer has an "npm run render" script (removed in v5.0.1)', () => {
test('producing commands tell the operator the flow is THEIR own annotations', () => {
// Pin language: every producing command's prose must mention that the
// OPERATOR drives annotation, not Claude. Phrase variants are allowed
// ("YOUR OWN note", "operator drives", etc.) — we look for the operator-
// ownership signal.
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
const text = read(`commands/${f}`);
assert.ok(
/YOUR OWN|operator drives|your own/i.test(text),
`commands/${f} must signal that the operator drives annotation (v5.0.2 contract)`,
);
}
});
test('package.json still has no "npm run render" script (removed in v5.0.1)', () => {
const pkg = JSON.parse(read('package.json'));
assert.equal(
pkg.scripts && pkg.scripts.render,
undefined,
'package.json scripts.render should be gone in v5.0.1',
'package.json scripts.render should remain gone',
);
});
@ -479,6 +504,11 @@ test('CHANGELOG.md has v5.0.1 entry', () => {
assert.match(cl, /## v5\.0\.1\b/, 'CHANGELOG.md must include "## v5.0.1" entry');
});
test('CHANGELOG.md has v5.0.2 entry', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v5\.0\.2\b/, 'CHANGELOG.md must include "## v5.0.2" entry');
});
test('CHANGELOG.md retains v4.2.0 entry (history is not rewritten)', () => {
const cl = read('CHANGELOG.md');
assert.match(cl, /## v4\.2\.0\b/, 'CHANGELOG.md must keep the historical "## v4.2.0" entry');