chore(voyage): release v5.0.1 — drop standalone HTML render; print literal /playground document-critique invocation
The v5.0.0 stop-gap had /trekbrief, /trekplan, and /trekreview each render
a read-only {artifact}.html (via scripts/render-artifact.mjs) AND print a
vague "run the /playground plugin" instruction. In practice the read-only
HTML was redundant with what /playground produces and the instruction
wasn't copy-paste-ready — the operator had to guess the right invocation.
v5.0.1 deletes scripts/render-artifact.mjs + its test + npm run render,
and makes each producing command end with a single boxed, literal,
copy-paste-ready line:
/playground build a document-critique playground for {artifact_path}
One paste from the operator launches the official playground skill's
document-critique template, which builds an interactive HTML — artifact
on the left, per-line Approve/Reject/Comment cards on the right, Copy
Prompt button at the bottom. Mark suggestions, click Copy Prompt, paste
back, Claude revises the .md. Doc-consistency test pins the literal
invocation so the prose cannot soften back into vagueness.
npm test green: 503 tests, 501 pass, 0 fail, 2 skipped.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
916d30f63e
commit
2e0892cdaf
15 changed files with 206 additions and 563 deletions
|
|
@ -400,13 +400,13 @@ test('commands/trekplan.md Phase 8 seals Opus-4.7 schema-drift defense', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// --- v5.0.0 — bespoke playground + /trekrevise + Handover 8 removed ---
|
||||
// --- v5.0.0 / v5.0.1 — bespoke playground removed; /playground invocation explicit ---
|
||||
//
|
||||
// The v4.2/v4.3 bespoke playground SPA, the /trekrevise command, and
|
||||
// Handover 8 (annotation → revision) were removed in v5.0.0. Producing
|
||||
// commands now render artifacts to self-contained HTML via
|
||||
// scripts/render-artifact.mjs and direct operators at the official
|
||||
// `/playground` plugin for annotation. These pins lock the removal in.
|
||||
// v5.0.0 removed the bespoke playground SPA, /trekrevise, and Handover 8.
|
||||
// v5.0.1 dropped the v5.0.0 stop-gap (scripts/render-artifact.mjs) and made
|
||||
// the producing commands print a literal, copy-paste-ready /playground
|
||||
// document-critique invocation instead. These pins lock both removals in
|
||||
// AND pin the new copy-paste invocation as the operator-facing contract.
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
|
|
@ -430,36 +430,55 @@ 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 exists (v5.0.0 render-and-link step)', () => {
|
||||
test('scripts/render-artifact.mjs no longer exists (removed in v5.0.1)', () => {
|
||||
assert.ok(
|
||||
existsSync(join(ROOT, 'scripts/render-artifact.mjs')),
|
||||
'scripts/render-artifact.mjs is required — producing commands call it to render artifacts to HTML',
|
||||
!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',
|
||||
);
|
||||
});
|
||||
|
||||
test('producing commands reference render-artifact.mjs (render-and-link step)', () => {
|
||||
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';
|
||||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||||
assert.ok(
|
||||
read(`commands/${f}`).includes('render-artifact.mjs'),
|
||||
`commands/${f} must wire the render-artifact.mjs render-and-link step (v5.0.0)`,
|
||||
read(`commands/${f}`).includes(REQUIRED),
|
||||
`commands/${f} must include the literal invocation "${REQUIRED}" so the operator copy-pastes it directly (v5.0.1)`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('producing commands point operators at the /playground plugin for annotation', () => {
|
||||
test('producing commands no longer reference the removed scripts/render-artifact.mjs', () => {
|
||||
for (const f of ['trekbrief.md', 'trekplan.md', 'trekreview.md']) {
|
||||
assert.ok(
|
||||
read(`commands/${f}`).includes('/playground'),
|
||||
`commands/${f} must mention the /playground plugin as the annotation path (v5.0.0)`,
|
||||
!read(`commands/${f}`).includes('render-artifact.mjs'),
|
||||
`commands/${f} still references scripts/render-artifact.mjs — that script was removed in v5.0.1`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('package.json no longer has an "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',
|
||||
);
|
||||
});
|
||||
|
||||
test('CHANGELOG.md has v5.0.0 entry', () => {
|
||||
const cl = read('CHANGELOG.md');
|
||||
assert.match(cl, /## v5\.0\.0\b/, 'CHANGELOG.md must include "## v5.0.0" entry');
|
||||
});
|
||||
|
||||
test('CHANGELOG.md has v5.0.1 entry', () => {
|
||||
const cl = read('CHANGELOG.md');
|
||||
assert.match(cl, /## v5\.0\.1\b/, 'CHANGELOG.md must include "## v5.0.1" 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');
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
// tests/scripts/render-artifact.test.mjs
|
||||
// Covers scripts/render-artifact.mjs — the v5.0.0 self-contained HTML
|
||||
// renderer that /trekbrief, /trekplan, /trekreview call at the end of their
|
||||
// run to produce a browser-readable view of the just-written artifact.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { buildHtml, renderMarkdown, render, parseArgs } from '../../scripts/render-artifact.mjs';
|
||||
|
||||
const SAMPLE = `---
|
||||
type: trekplan
|
||||
plan_version: "1.7"
|
||||
task: "Render-artifact smoke test"
|
||||
slug: render-smoke
|
||||
---
|
||||
|
||||
# Render-artifact smoke test
|
||||
|
||||
A paragraph with **bold**, \`inline code\`, and a [link](https://example.com).
|
||||
|
||||
## Steps
|
||||
|
||||
- top item
|
||||
- nested item
|
||||
- second top item
|
||||
|
||||
1. ordered one
|
||||
2. ordered two
|
||||
|
||||
\`\`\`js
|
||||
const x = 1;
|
||||
\`\`\`
|
||||
|
||||
> a blockquote line
|
||||
|
||||
| Col A | Col B |
|
||||
|-------|-------|
|
||||
| 1 | 2 |
|
||||
`;
|
||||
|
||||
test('buildHtml produces a complete self-contained HTML document', () => {
|
||||
const html = buildHtml('plan.md', SAMPLE);
|
||||
assert.ok(html.startsWith('<!DOCTYPE html>'), 'must start with doctype');
|
||||
assert.ok(html.includes('</html>'), 'must close html');
|
||||
assert.ok(html.includes('<style>'), 'must inline a stylesheet');
|
||||
// Zero external network references.
|
||||
assert.ok(!/<link[^>]+href=/i.test(html), 'no external <link> stylesheets');
|
||||
assert.ok(!/<script[^>]+src=/i.test(html), 'no external <script src>');
|
||||
assert.ok(!/https?:\/\/(?!example\.com)/.test(html.replace(/<style>[\s\S]*?<\/style>/, '')), 'no unexpected http(s) URLs outside example link');
|
||||
});
|
||||
|
||||
test('buildHtml folds frontmatter into a <details> block', () => {
|
||||
const html = buildHtml('plan.md', SAMPLE);
|
||||
assert.ok(html.includes('<details class="frontmatter">'), 'frontmatter wrapped in <details>');
|
||||
assert.ok(html.includes('plan_version'), 'frontmatter content preserved');
|
||||
// Frontmatter must NOT leak into the rendered body as a literal "---" rule.
|
||||
const bodyOnly = html.split('</details>')[1] || '';
|
||||
assert.ok(!bodyOnly.startsWith('\n<hr>'), 'frontmatter fence should not become an <hr>');
|
||||
});
|
||||
|
||||
test('buildHtml derives the <title> from frontmatter task', () => {
|
||||
const html = buildHtml('plan.md', SAMPLE);
|
||||
assert.match(html, /<title>Render-artifact smoke test<\/title>/);
|
||||
});
|
||||
|
||||
test('renderMarkdown renders headings, code fences, lists, tables, blockquotes', () => {
|
||||
const out = renderMarkdown(SAMPLE.split('---\n').slice(2).join('---\n'));
|
||||
assert.match(out, /<h1>Render-artifact smoke test<\/h1>/);
|
||||
assert.match(out, /<h2>Steps<\/h2>/);
|
||||
assert.match(out, /<pre><code class="language-js">/);
|
||||
assert.ok(out.includes('const x = 1;'), 'code fence body preserved');
|
||||
assert.match(out, /<ul><li>top item<ul><li>nested item<\/li><\/ul><\/li>/);
|
||||
assert.match(out, /<ol><li>ordered one<\/li><li>ordered two<\/li><\/ol>/);
|
||||
assert.match(out, /<blockquote>a blockquote line<\/blockquote>/);
|
||||
assert.match(out, /<table>[\s\S]*<th>Col A<\/th>[\s\S]*<td>1<\/td>[\s\S]*<\/table>/);
|
||||
assert.match(out, /<strong>bold<\/strong>/);
|
||||
assert.match(out, /<code>inline code<\/code>/);
|
||||
assert.match(out, /<a href="https:\/\/example\.com">link<\/a>/);
|
||||
});
|
||||
|
||||
test('renderMarkdown escapes HTML in body and code', () => {
|
||||
const out = renderMarkdown('A <tag> & "quote".\n\n```\n<script>alert(1)</script>\n```\n');
|
||||
assert.ok(!out.includes('<tag>'), 'raw tag escaped');
|
||||
assert.ok(out.includes('<tag>'), 'tag rendered as entity');
|
||||
assert.ok(out.includes('<script>alert(1)</script>'), 'code-fence content escaped');
|
||||
});
|
||||
|
||||
test('render() is deterministic — two runs byte-identical', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'render-artifact-'));
|
||||
try {
|
||||
const md = join(dir, 'plan.md');
|
||||
writeFileSync(md, SAMPLE);
|
||||
const out1 = render(md, join(dir, 'a.html'));
|
||||
const out2 = render(md, join(dir, 'b.html'));
|
||||
assert.ok(existsSync(out1) && existsSync(out2));
|
||||
assert.equal(readFileSync(out1, 'utf-8'), readFileSync(out2, 'utf-8'));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('render() defaults output to <input-basename>.html', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'render-artifact-'));
|
||||
try {
|
||||
const md = join(dir, 'review.md');
|
||||
writeFileSync(md, '# Review\n\nok\n');
|
||||
const out = render(md);
|
||||
assert.equal(out, join(dir, 'review.html'));
|
||||
assert.ok(existsSync(out));
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('parseArgs handles --out and positional input', () => {
|
||||
assert.deepEqual(parseArgs(['x.md']), { input: 'x.md', out: null, help: false });
|
||||
assert.deepEqual(parseArgs(['x.md', '--out', 'y.html']), { input: 'x.md', out: 'y.html', help: false });
|
||||
assert.equal(parseArgs(['--help']).help, true);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue