chore(voyage): release v5.0.0 — remove bespoke playground + /trekrevise + Handover 8; render produced artifacts to HTML + link, annotate via /playground
The v4.2/v4.3 bespoke playground SPA (~388 KB), the /trekrevise command, Handover 8 (annotation → revision), the supporting lib/ modules (anchor-parser, annotation-digest, markdown-write, revision-guard), the Playwright e2e suite, and the @playwright/test / @axe-core/playwright devDeps are removed. A browser walkthrough found the playground borderline unusable, and it duplicated the official /playground plugin's document-critique / diff-review templates. In their place: scripts/render-artifact.mjs — a small, zero-dependency renderer that turns a brief/plan/review .md into a self-contained, design-system-styled, zero-network .html (frontmatter folded into a <details> block). /trekbrief, /trekplan, and /trekreview call it on their last step and print the file:// link; to annotate, run /playground (document-critique) on the .md and paste the generated prompt back. Resolves the v4.3.1-deferred findings as moot (their target files are deleted). npm test green: 509 tests, 507 pass, 0 fail, 2 skipped. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0f197f6ff6
commit
916d30f63e
96 changed files with 620 additions and 14716 deletions
|
|
@ -1,130 +0,0 @@
|
|||
// tests/parsers/anchor-parser.test.mjs
|
||||
// Unit tests for lib/parsers/anchor-parser.mjs (v4.2)
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
parseAnchors,
|
||||
addAnchors,
|
||||
stripAnchors,
|
||||
validateAnchorPlacement,
|
||||
} from '../../lib/parsers/anchor-parser.mjs';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const EXAMPLE_PATH = resolve(__dirname, '..', 'fixtures', 'annotation', 'annotation-example.md');
|
||||
|
||||
const PLAIN = `# Title
|
||||
|
||||
A normal paragraph.
|
||||
|
||||
## Section
|
||||
|
||||
More text.
|
||||
`;
|
||||
|
||||
test('parseAnchors — empty array on plain markdown without anchors', () => {
|
||||
const r = parseAnchors(PLAIN);
|
||||
assert.equal(r.valid, true);
|
||||
assert.deepEqual(r.parsed, []);
|
||||
});
|
||||
|
||||
test('parseAnchors — extracts id/target/line/intent from valid anchor', () => {
|
||||
const md = readFileSync(EXAMPLE_PATH, 'utf-8');
|
||||
const r = parseAnchors(md);
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
assert.equal(r.parsed.length, 1);
|
||||
assert.equal(r.parsed[0].id, 'ANN-0001');
|
||||
assert.equal(r.parsed[0].target, 'section-b');
|
||||
assert.equal(r.parsed[0].line, 20);
|
||||
assert.equal(r.parsed[0].intent, 'change');
|
||||
});
|
||||
|
||||
test('parseAnchors — rejects ID not matching ANN-NNNN', () => {
|
||||
const md = `# X\n\n<!-- voyage:anchor id="X-001" target="foo" line="3" -->\n`;
|
||||
const r = parseAnchors(md);
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'ANCHOR_BAD_ID'));
|
||||
});
|
||||
|
||||
test('parseAnchors — rejects malformed (missing id)', () => {
|
||||
const md = `# X\n\n<!-- voyage:anchor target="foo" line="3" -->\n`;
|
||||
const r = parseAnchors(md);
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'ANCHOR_MALFORMED'));
|
||||
});
|
||||
|
||||
test('parseAnchors — rejects duplicate IDs', () => {
|
||||
const md = `# X\n\n<!-- voyage:anchor id="ANN-0001" target="a" line="3" -->\n\nFoo.\n\n<!-- voyage:anchor id="ANN-0001" target="b" line="9" -->\n`;
|
||||
const r = parseAnchors(md);
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'ANCHOR_DUPLICATE_ID'));
|
||||
});
|
||||
|
||||
test('parseAnchors — ignores anchors inside fenced code blocks', () => {
|
||||
const md = `# X\n\n\`\`\`yaml\n<!-- voyage:anchor id="ANN-0001" target="a" line="4" -->\n\`\`\`\n`;
|
||||
const r = parseAnchors(md);
|
||||
assert.equal(r.valid, true);
|
||||
assert.deepEqual(r.parsed, []);
|
||||
});
|
||||
|
||||
test('addAnchors — empty list returns input byte-identical', () => {
|
||||
const r = addAnchors(PLAIN, []);
|
||||
assert.equal(r, PLAIN);
|
||||
});
|
||||
|
||||
test('addAnchors — inserts anchor on its own line with blank-line separation', () => {
|
||||
const md = `# Title\n\nLine 3.\n`;
|
||||
const result = addAnchors(md, [{ id: 'ANN-0001', target: 'title', line: 3, intent: 'change' }]);
|
||||
assert.match(result, /<!-- voyage:anchor id="ANN-0001" target="title" line="3" intent="change" -->/);
|
||||
// Anchor inserted above target line
|
||||
const lines = result.split('\n');
|
||||
const anchorIdx = lines.findIndex(l => l.startsWith('<!-- voyage:anchor'));
|
||||
assert.ok(anchorIdx >= 0);
|
||||
});
|
||||
|
||||
test('addAnchors -> stripAnchors round-trips byte-identical', () => {
|
||||
const md = `# Title\n\nLine 3.\n\nLine 5.\n`;
|
||||
const withAnchors = addAnchors(md, [
|
||||
{ id: 'ANN-0001', target: 'title', line: 3 },
|
||||
{ id: 'ANN-0002', target: 'title', line: 5 },
|
||||
]);
|
||||
const stripped = stripAnchors(withAnchors);
|
||||
assert.equal(stripped, md, 'addAnchors then stripAnchors must round-trip byte-identical');
|
||||
});
|
||||
|
||||
test('parseAnchors(stripAnchors(addAnchors(md, []))) returns []', () => {
|
||||
const md = `# Title\n\nBody.\n`;
|
||||
const result = parseAnchors(stripAnchors(addAnchors(md, [])));
|
||||
assert.equal(result.valid, true);
|
||||
assert.deepEqual(result.parsed, []);
|
||||
});
|
||||
|
||||
test('validateAnchorPlacement — rejects anchor in list-item', () => {
|
||||
const md = `# X\n\n- item\n <!-- voyage:anchor id="ANN-0001" target="x" line="4" -->\n- next\n`;
|
||||
const r = validateAnchorPlacement(md, []);
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'ANCHOR_IN_LIST_ITEM'));
|
||||
});
|
||||
|
||||
test('validateAnchorPlacement — rejects anchor inside fenced yaml block', () => {
|
||||
const md = `# X\n\n\`\`\`yaml\nfoo: bar\n<!-- voyage:anchor id="ANN-0001" target="x" line="5" -->\n\`\`\`\n`;
|
||||
const r = validateAnchorPlacement(md, []);
|
||||
assert.equal(r.valid, false);
|
||||
assert.ok(r.errors.find(e => e.code === 'ANCHOR_IN_FENCED_BLOCK'));
|
||||
});
|
||||
|
||||
test('validateAnchorPlacement — accepts anchor in body paragraph', () => {
|
||||
const md = readFileSync(EXAMPLE_PATH, 'utf-8');
|
||||
const r = validateAnchorPlacement(md, []);
|
||||
assert.equal(r.valid, true, JSON.stringify(r.errors));
|
||||
});
|
||||
|
||||
test('parseAnchors — anchor with intent block sets intent field', () => {
|
||||
const md = `# X\n\n<!-- voyage:anchor id="ANN-0001" target="x" line="3" intent="block" -->\n`;
|
||||
const r = parseAnchors(md);
|
||||
assert.equal(r.valid, true);
|
||||
assert.equal(r.parsed[0].intent, 'block');
|
||||
});
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
// tests/parsers/annotation-digest.test.mjs
|
||||
// Unit tests for lib/parsers/annotation-digest.mjs (v4.2)
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { computeAnnotationDigest } from '../../lib/parsers/annotation-digest.mjs';
|
||||
|
||||
test('computeAnnotationDigest — empty array yields deterministic 16-char hex', () => {
|
||||
const d = computeAnnotationDigest([]);
|
||||
assert.equal(typeof d, 'string');
|
||||
assert.equal(d.length, 16);
|
||||
assert.match(d, /^[0-9a-f]{16}$/);
|
||||
// Empty-array digest is a known constant (sha256 of empty string)
|
||||
assert.equal(d, 'e3b0c44298fc1c14');
|
||||
});
|
||||
|
||||
test('computeAnnotationDigest — array order does not affect digest', () => {
|
||||
const a = [
|
||||
{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: 'one', timestamp: 't1' },
|
||||
{ id: 'ANN-0002', target_artifact: 'plan.md', target_anchor: 'b', intent: 'change', comment: 'two', timestamp: 't2' },
|
||||
];
|
||||
const b = [a[1], a[0]]; // reversed
|
||||
assert.equal(computeAnnotationDigest(a), computeAnnotationDigest(b));
|
||||
});
|
||||
|
||||
test('computeAnnotationDigest — different intent produces different digest', () => {
|
||||
const a = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: '', timestamp: '' }];
|
||||
const b = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'change', comment: '', timestamp: '' }];
|
||||
assert.notEqual(computeAnnotationDigest(a), computeAnnotationDigest(b));
|
||||
});
|
||||
|
||||
test('computeAnnotationDigest — output is exactly 16 lowercase hex chars', () => {
|
||||
const a = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: 'x', timestamp: 't' }];
|
||||
const d = computeAnnotationDigest(a);
|
||||
assert.equal(d.length, 16);
|
||||
assert.match(d, /^[0-9a-f]{16}$/);
|
||||
});
|
||||
|
||||
test('computeAnnotationDigest — single annotation produces fixed golden value', () => {
|
||||
// This pins the canonicalization. Changing the format will break this test.
|
||||
const a = [{
|
||||
id: 'ANN-0001',
|
||||
target_artifact: 'plan.md',
|
||||
target_anchor: 'step-3',
|
||||
intent: 'change',
|
||||
comment: 'reorder',
|
||||
timestamp: '2026-05-09T10:00:00Z',
|
||||
}];
|
||||
const d = computeAnnotationDigest(a);
|
||||
// Canonical: "ANN-0001|plan.md|step-3|change|reorder|2026-05-09T10:00:00Z"
|
||||
// Computed once and pinned here:
|
||||
assert.equal(d.length, 16);
|
||||
assert.match(d, /^[0-9a-f]{16}$/);
|
||||
// Recompute deterministically — same input must always give same output
|
||||
const d2 = computeAnnotationDigest(a);
|
||||
assert.equal(d, d2);
|
||||
});
|
||||
|
||||
test('computeAnnotationDigest — undefined optional fields treated identically to empty string', () => {
|
||||
const a = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix' }]; // no comment, no timestamp
|
||||
const b = [{ id: 'ANN-0001', target_artifact: 'plan.md', target_anchor: 'a', intent: 'fix', comment: '', timestamp: '' }];
|
||||
assert.equal(computeAnnotationDigest(a), computeAnnotationDigest(b));
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue