// tests/lib/source-annotations.test.mjs // Additive-field invariant for source_annotations: array (Handover 8). // // Mirrors tests/lib/source-findings.test.mjs:9-13 — the structural three-part // contract that v4.2 brief-validator + plan-validator + review-validator must // uphold for the new optional source_annotations frontmatter field: // // 1. validators accept an artifact with source_annotations (additive optional) // 2. frontmatter parser extracts source_annotations as an array // 3. each entry has the documented annotation shape // ({id, target_artifact, target_anchor, intent, ...}) // // LLM behavior (the planner actually emitting source_annotations) is // non-testable without live invocation — this test only covers the schema // half. See Step 12 doc-pin for the operator-level contract. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { parseDocument } from '../../lib/util/frontmatter.mjs'; import { validateBrief } from '../../lib/validators/brief-validator.mjs'; import { validatePlan } from '../../lib/validators/plan-validator.mjs'; import { validateReview } from '../../lib/validators/review-validator.mjs'; const ID_RE = /^ANN-\d{4}$/; const VALID_INTENT = new Set(['fix', 'change', 'question', 'block']); function makeFixture(name, body) { const dir = mkdtempSync(join(tmpdir(), 'voyage-source-ann-')); const path = join(dir, name); writeFileSync(path, body); return { dir, path }; } const BRIEF_WITH_SOURCE_ANNOTATIONS = `--- type: trekbrief brief_version: "1.0" task: Demo brief with source_annotations slug: source-annotations-demo-brief research_topics: 0 research_status: complete revision: 1 annotation_digest: deadbeefcafe1234 source_annotations: - id: ANN-0001 target_artifact: brief.md target_anchor: goal line: 20 intent: change - id: ANN-0002 target_artifact: brief.md target_anchor: success-criteria line: 30 intent: fix --- # Demo ## Intent Test fixture. ## Goal Test fixture. ## Success Criteria - It validates. `; const PLAN_WITH_SOURCE_ANNOTATIONS = `--- plan_version: 1.7 profile: balanced revision: 2 annotation_digest: cafebabe98765432 source_annotations: - id: ANN-0001 target_artifact: plan.md target_anchor: step-1 line: 25 intent: fix --- # Demo plan ## Implementation Plan ### Step 1: Sentinel - **Files:** \`tmp/x.txt\` (new) - **Changes:** Touch. - **Verify:** \`test -f tmp/x.txt\` - **On failure:** revert. - **Checkpoint:** \`git commit -m "chore: x"\` - **Manifest:** \`\`\`yaml manifest: expected_paths: - tmp/x.txt min_file_count: 1 commit_message_pattern: "^chore: x" bash_syntax_check: [] forbidden_paths: [] must_contain: [] \`\`\` ## Verification - It validates. `; const REVIEW_WITH_SOURCE_ANNOTATIONS = `--- type: trekreview review_version: "1.0" task: Demo review with source_annotations slug: source-annotations-demo-review project_dir: .claude/projects/2026-05-09-demo brief_path: .claude/projects/2026-05-09-demo/brief.md scope_sha_end: 0000000000000000000000000000000000000000 reviewed_files_count: 0 findings: [] revision: 1 annotation_digest: 0123456789abcdef source_annotations: - id: ANN-0001 target_artifact: review.md target_anchor: executive-summary line: 18 intent: question --- # Demo ## Executive Summary Verdict: ALLOW. ## Coverage | File | Treatment | |------|-----------| | _none_ | _no diff_ | ## Remediation Summary ALLOW. `; test('validators accept artifacts with source_annotations field (additive optional, brief)', () => { const { dir, path } = makeFixture('brief.md', BRIEF_WITH_SOURCE_ANNOTATIONS); try { const r = validateBrief(path, { strict: true }); assert.ok( r.valid, `brief-validator rejected synthetic brief with source_annotations: ` + `${(r.errors || []).map(e => `[${e.code}] ${e.message}`).join('; ')}`, ); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('validators accept artifacts with source_annotations field (additive optional, plan)', () => { const { dir, path } = makeFixture('plan.md', PLAN_WITH_SOURCE_ANNOTATIONS); try { const r = validatePlan(path, { strict: true }); assert.ok( r.valid, `plan-validator rejected synthetic plan with source_annotations: ` + `${(r.errors || []).map(e => `[${e.code}] ${e.message}`).join('; ')}`, ); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('validators accept artifacts with source_annotations field (additive optional, review)', () => { const { dir, path } = makeFixture('review.md', REVIEW_WITH_SOURCE_ANNOTATIONS); try { const r = validateReview(path, { strict: true }); assert.ok( r.valid, `review-validator rejected synthetic review with source_annotations: ` + `${(r.errors || []).map(e => `[${e.code}] ${e.message}`).join('; ')}`, ); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('frontmatter parser extracts source_annotations as array of dicts (per artifact)', () => { const cases = [ { name: 'brief.md', body: BRIEF_WITH_SOURCE_ANNOTATIONS, expected: 2 }, { name: 'plan.md', body: PLAN_WITH_SOURCE_ANNOTATIONS, expected: 1 }, { name: 'review.md', body: REVIEW_WITH_SOURCE_ANNOTATIONS, expected: 1 }, ]; for (const c of cases) { const doc = parseDocument(c.body); assert.ok(doc.valid, `${c.name}: frontmatter did not parse: ${(doc.errors || []).map(e => e.message).join(', ')}`); const sa = doc.parsed.frontmatter && doc.parsed.frontmatter.source_annotations; assert.ok(Array.isArray(sa), `${c.name}: frontmatter.source_annotations is not an array (got ${typeof sa})`); assert.strictEqual(sa.length, c.expected, `${c.name}: expected ${c.expected} entries, got ${sa.length}`); } }); test('source_annotations entries match documented annotation shape', () => { const doc = parseDocument(BRIEF_WITH_SOURCE_ANNOTATIONS); const entries = doc.parsed.frontmatter.source_annotations; for (const e of entries) { assert.strictEqual(typeof e, 'object', `source_annotations entry is not an object: ${JSON.stringify(e)}`); assert.ok(typeof e.id === 'string' && ID_RE.test(e.id), `source_annotations[*].id must match /^ANN-\\d{4}$/, got ${JSON.stringify(e.id)}`); assert.ok(typeof e.target_artifact === 'string' && e.target_artifact.endsWith('.md'), `source_annotations[*].target_artifact must be a *.md path, got ${JSON.stringify(e.target_artifact)}`); assert.ok(typeof e.target_anchor === 'string' && e.target_anchor.length > 0, `source_annotations[*].target_anchor must be a non-empty string, got ${JSON.stringify(e.target_anchor)}`); if (e.intent !== undefined && e.intent !== null) { assert.ok(VALID_INTENT.has(e.intent), `source_annotations[*].intent must be in {fix|change|question|block}, got ${JSON.stringify(e.intent)}`); } } }); test('artifacts WITHOUT source_annotations still validate (forward-compat baseline)', () => { // Forward-compat: artifacts that predate v4.2 must still validate. Fall back // to an artifact with neither revision nor source_annotations. const baseline = BRIEF_WITH_SOURCE_ANNOTATIONS .replace(/^revision:.*\n/m, '') .replace(/^annotation_digest:.*\n/m, '') .replace(/^source_annotations:[\s\S]*?(?=^---$|^[A-Za-z])/m, ''); const { dir, path } = makeFixture('brief.md', baseline); try { const r = validateBrief(path, { strict: true }); assert.ok( r.valid, `brief-validator must accept artifacts WITHOUT source_annotations (forward-compat baseline): ` + `${(r.errors || []).map(e => `[${e.code}] ${e.message}`).join('; ')}`, ); } finally { rmSync(dir, { recursive: true, force: true }); } });