@polderlabs/bizar 3.16.0 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  /**
2
- * plan.mjs tests — uses Node's built-in node:test (Node 20+)
2
+ * artifact.mjs tests — uses Node's built-in node:test (Node 20+)
3
3
  * Tests: slug validation, new, list, delete, export, server routes
4
4
  *
5
- * Note: These tests import from plan.mjs directly, so they test internal
5
+ * Note: These tests import from artifact.mjs directly, so they test internal
6
6
  * functions via their named exports. The server runs in the test process
7
7
  * but is shut down after each test.
8
8
  */
@@ -20,25 +20,25 @@ import {
20
20
  import { join, resolve, dirname } from 'node:path';
21
21
  import { fileURLToPath } from 'node:url';
22
22
 
23
- // Resolve plan.mjs from this test file's location
23
+ // Resolve artifact.mjs from this test file's location
24
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
25
  const PROJECT_ROOT = resolve(__dirname, '..');
26
- const TEMPLATES_DIR = join(PROJECT_ROOT, 'templates', 'plan');
27
- const PLANS_DIR = join(PROJECT_ROOT, 'plans');
26
+ const TEMPLATES_DIR = join(PROJECT_ROOT, 'templates', 'artifact');
27
+ const PLANS_DIR = join(PROJECT_ROOT, 'artifacts');
28
28
 
29
- // ── Named imports from plan.mjs ──────────────────────────────────────────────
30
- const { runPlan, startServer, regenerateHtml } = await import('./plan.mjs');
29
+ // ── Named imports from artifact.mjs ──────────────────────────────────────────────
30
+ const { runPlan, startServer, regenerateHtml } = await import('./artifact.mjs');
31
31
 
32
32
  // Suppress MaxListenersWarning (each server adds SIGINT+SIGTERM listeners)
33
33
  process.setMaxListeners(64);
34
34
 
35
35
  // ── Test helpers ─────────────────────────────────────────────────────────────
36
36
 
37
- /** Slug validation regex (mirrors plan.mjs) */
38
- // Must match plan.mjs SLUG_REGEX: ^[a-z0-9][a-z0-9-]{0,63}$
37
+ /** Slug validation regex (mirrors artifact.mjs) */
38
+ // Must match artifact.mjs SLUG_REGEX: ^[a-z0-9][a-z0-9-]{0,63}$
39
39
  const SLUG_REGEX = /^[a-z0-9][a-z0-9-]{0,63}$/;
40
40
 
41
- /** Create a temp plan directory for testing */
41
+ /** Create a temp artifact directory for testing */
42
42
  function createTempPlan(slug, overrides = {}) {
43
43
  const planDir = join(PLANS_DIR, slug);
44
44
  mkdirSync(planDir, { recursive: true });
@@ -56,35 +56,35 @@ function createTempPlan(slug, overrides = {}) {
56
56
 
57
57
  writeFileSync(join(planDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8');
58
58
  const mdxContent = overrides.mdx || `# ${meta.title}\n\nContent here.\n`;
59
- writeFileSync(join(planDir, 'plan.mdx'), mdxContent, 'utf-8');
59
+ writeFileSync(join(planDir, 'artifact.mdx'), mdxContent, 'utf-8');
60
60
  writeFileSync(
61
61
  join(planDir, 'comments.json'),
62
62
  overrides.comments || '[]',
63
63
  'utf-8'
64
64
  );
65
65
 
66
- // Generate plan.html
67
- const planMdx = readFileSync(join(planDir, 'plan.mdx'), 'utf-8');
66
+ // Generate artifact.html
67
+ const planMdx = readFileSync(join(planDir, 'artifact.mdx'), 'utf-8');
68
68
  const commentsJson = readFileSync(join(planDir, 'comments.json'), 'utf-8');
69
69
  const metaJson = readFileSync(join(planDir, 'meta.json'), 'utf-8');
70
70
  const planJson = JSON.stringify(planMdx);
71
71
  const htmlContent = `<!doctype html>
72
72
  <html>
73
- <head><title>${meta.title} — Bizar Plan</title></head>
73
+ <head><title>${meta.title} — Bizar Artifact</title></head>
74
74
  <body>
75
75
  <header><h1>${meta.title}</h1></header>
76
76
  <main><pre>${planMdx.replace(/</g, '&lt;')}</pre></main>
77
77
  <script>
78
- const INITIAL_STATE = { plan: ${planJson}, comments: ${commentsJson}, meta: ${metaJson} };
78
+ const INITIAL_STATE = { artifact: ${planJson}, comments: ${commentsJson}, meta: ${metaJson} };
79
79
  </script>
80
80
  </body>
81
81
  </html>`;
82
- writeFileSync(join(planDir, 'plan.html'), htmlContent, 'utf-8');
82
+ writeFileSync(join(planDir, 'artifact.html'), htmlContent, 'utf-8');
83
83
 
84
84
  return planDir;
85
85
  }
86
86
 
87
- /** Clean up a temp plan */
87
+ /** Clean up a temp artifact */
88
88
  function cleanupPlan(slug) {
89
89
  const planDir = join(PLANS_DIR, slug);
90
90
  if (existsSync(planDir)) rmSync(planDir, { recursive: true });
@@ -129,18 +129,18 @@ describe('Slug validation', () => {
129
129
 
130
130
  // ── createPlan / new flow tests ───────────────────────────────────────────────
131
131
  // Note: runPlan(['new', slug]) starts a blocking server so can't be tested directly.
132
- // The file creation is tested in 'Plan file creation' suite below.
132
+ // The file creation is tested in 'Artifact file creation' suite below.
133
133
  // Slug validation is tested independently above.
134
134
 
135
135
  // Test createPlan by checking the files it creates
136
- describe('Plan file creation', () => {
136
+ describe('Artifact file creation', () => {
137
137
  const TEST_SLUG = 'test-files-' + Date.now();
138
138
 
139
139
  afterEach(() => {
140
140
  cleanupPlan(TEST_SLUG);
141
141
  });
142
142
 
143
- test('runPlan new creates plan.mdx, meta.json, comments.json', async () => {
143
+ test('runPlan new creates artifact.mdx, meta.json, comments.json', async () => {
144
144
  // We'll use a subprocess to run just the file creation part
145
145
  // For unit testing, we directly verify the file operations work
146
146
  mkdirSync(join(PLANS_DIR, TEST_SLUG), { recursive: true });
@@ -150,11 +150,11 @@ describe('Plan file creation', () => {
150
150
  const meta = { title, slug: TEST_SLUG, status: 'draft', author: 'tester', created: now, lastEdited: now };
151
151
 
152
152
  writeFileSync(join(PLANS_DIR, TEST_SLUG, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8');
153
- writeFileSync(join(PLANS_DIR, TEST_SLUG, 'plan.mdx'), `# ${title}\n\nTest content.\n`, 'utf-8');
153
+ writeFileSync(join(PLANS_DIR, TEST_SLUG, 'artifact.mdx'), `# ${title}\n\nTest content.\n`, 'utf-8');
154
154
  writeFileSync(join(PLANS_DIR, TEST_SLUG, 'comments.json'), '[]', 'utf-8');
155
155
 
156
156
  // Verify files
157
- assert.equal(existsSync(join(PLANS_DIR, TEST_SLUG, 'plan.mdx')), true);
157
+ assert.equal(existsSync(join(PLANS_DIR, TEST_SLUG, 'artifact.mdx')), true);
158
158
  assert.equal(existsSync(join(PLANS_DIR, TEST_SLUG, 'meta.json')), true);
159
159
  assert.equal(existsSync(join(PLANS_DIR, TEST_SLUG, 'comments.json')), true);
160
160
 
@@ -184,7 +184,7 @@ describe('list flow', () => {
184
184
  cleanupPlan(SLUG2);
185
185
  });
186
186
 
187
- test('reads plans directory and meta.json correctly', async () => {
187
+ test('reads artifacts directory and meta.json correctly', async () => {
188
188
  const planDir1 = join(PLANS_DIR, SLUG1);
189
189
  const planDir2 = join(PLANS_DIR, SLUG2);
190
190
 
@@ -206,17 +206,17 @@ describe('list flow', () => {
206
206
  // ── delete flow tests ────────────────────────────────────────────────────────
207
207
 
208
208
  describe('delete flow', () => {
209
- const TEST_SLUG = 'test-delete-plan-' + Date.now();
209
+ const TEST_SLUG = 'test-delete-artifact-' + Date.now();
210
210
 
211
211
  beforeEach(() => {
212
- createTempPlan(TEST_SLUG, { title: 'Delete Test Plan' });
212
+ createTempPlan(TEST_SLUG, { title: 'Delete Test Artifact' });
213
213
  });
214
214
 
215
215
  afterEach(() => {
216
216
  cleanupPlan(TEST_SLUG);
217
217
  });
218
218
 
219
- test('removes plan directory', async () => {
219
+ test('removes artifact directory', async () => {
220
220
  const planDir = join(PLANS_DIR, TEST_SLUG);
221
221
  assert.equal(existsSync(planDir), true);
222
222
 
@@ -234,8 +234,8 @@ describe('delete flow', () => {
234
234
  // ── export flow tests ───────────────────────────────────────────────────────
235
235
 
236
236
  describe('export flow', () => {
237
- const TEST_SLUG = 'test-export-plan-' + Date.now();
238
- const EXPECTED_CONTENT = '# Export Test Plan\n\nSome content here.\n';
237
+ const TEST_SLUG = 'test-export-artifact-' + Date.now();
238
+ const EXPECTED_CONTENT = '# Export Test Artifact\n\nSome content here.\n';
239
239
 
240
240
  beforeEach(() => {
241
241
  createTempPlan(TEST_SLUG, { mdx: EXPECTED_CONTENT });
@@ -245,9 +245,9 @@ describe('export flow', () => {
245
245
  cleanupPlan(TEST_SLUG);
246
246
  });
247
247
 
248
- test('reads plan.mdx content', async () => {
248
+ test('reads artifact.mdx content', async () => {
249
249
  const planDir = join(PLANS_DIR, TEST_SLUG);
250
- const content = readFileSync(join(planDir, 'plan.mdx'), 'utf-8');
250
+ const content = readFileSync(join(planDir, 'artifact.mdx'), 'utf-8');
251
251
  assert.equal(content, EXPECTED_CONTENT);
252
252
  });
253
253
  });
@@ -265,14 +265,14 @@ describe('help flow', () => {
265
265
  // These start a real HTTP server on a random port and make actual requests.
266
266
 
267
267
  describe('Local HTTP server', () => {
268
- const TEST_SLUG = 'test-server-plan-' + Date.now();
268
+ const TEST_SLUG = 'test-server-artifact-' + Date.now();
269
269
  let serverInfo;
270
270
  let baseUrl;
271
271
 
272
272
  beforeEach(async () => {
273
- // Create a plan for the server to serve
273
+ // Create a artifact for the server to serve
274
274
  createTempPlan(TEST_SLUG, {
275
- mdx: '# Test Server Plan\n\nServer test content.\n',
275
+ mdx: '# Test Server Artifact\n\nServer test content.\n',
276
276
  comments: JSON.stringify([
277
277
  {
278
278
  id: '1',
@@ -299,15 +299,15 @@ describe('Local HTTP server', () => {
299
299
  assert.equal(res.status, 200);
300
300
  assert.equal(res.headers.get('content-type').includes('text/html'), true);
301
301
  const html = await res.text();
302
- assert.equal(html.includes('Test Server Plan'), true);
302
+ assert.equal(html.includes('Test Server Artifact'), true);
303
303
  });
304
304
 
305
- test('GET /api/plan returns MDX', async () => {
306
- const res = await fetch(`${baseUrl}/api/plan`);
305
+ test('GET /api/artifact returns MDX', async () => {
306
+ const res = await fetch(`${baseUrl}/api/artifact`);
307
307
  assert.equal(res.status, 200);
308
308
  assert.equal(res.headers.get('content-type').includes('text/plain'), true);
309
309
  const text = await res.text();
310
- assert.equal(text.includes('Test Server Plan'), true);
310
+ assert.equal(text.includes('Test Server Artifact'), true);
311
311
  });
312
312
 
313
313
  test('GET /api/comments returns JSON', async () => {
@@ -320,9 +320,9 @@ describe('Local HTTP server', () => {
320
320
  assert.equal(comments.length > 0, true);
321
321
  });
322
322
 
323
- test('PUT /api/plan saves MDX', async () => {
324
- const newContent = '# Updated Plan\n\nUpdated content.\n';
325
- const res = await fetch(`${baseUrl}/api/plan`, {
323
+ test('PUT /api/artifact saves MDX', async () => {
324
+ const newContent = '# Updated Artifact\n\nUpdated content.\n';
325
+ const res = await fetch(`${baseUrl}/api/artifact`, {
326
326
  method: 'PUT',
327
327
  body: newContent,
328
328
  headers: { 'Content-Type': 'text/plain' },
@@ -330,7 +330,7 @@ describe('Local HTTP server', () => {
330
330
  assert.equal(res.status, 200);
331
331
 
332
332
  // Verify file was updated
333
- const saved = readFileSync(join(PLANS_DIR, TEST_SLUG, 'plan.mdx'), 'utf-8');
333
+ const saved = readFileSync(join(PLANS_DIR, TEST_SLUG, 'artifact.mdx'), 'utf-8');
334
334
  assert.equal(saved, newContent);
335
335
  });
336
336
 
@@ -386,7 +386,7 @@ describe('Local HTTP server', () => {
386
386
  });
387
387
 
388
388
  test('CORS headers are set', async () => {
389
- const res = await fetch(`${baseUrl}/api/plan`, { method: 'OPTIONS' });
389
+ const res = await fetch(`${baseUrl}/api/artifact`, { method: 'OPTIONS' });
390
390
  assert.equal(res.headers.get('access-control-allow-origin'), '*');
391
391
  assert.equal(
392
392
  res.headers.get('access-control-allow-methods').includes('GET'),
@@ -417,7 +417,7 @@ describe('Local HTTP server', () => {
417
417
  });
418
418
 
419
419
  // ── Template library tests ─────────────────────────────────────────────────────
420
- // Tests for plan-templates.mjs — getTemplate, getTemplateNames, printTemplates, etc.
420
+ // Tests for artifact-templates.mjs — getTemplate, getTemplateNames, printTemplates, etc.
421
421
 
422
422
  import {
423
423
  getTemplate,
@@ -426,7 +426,7 @@ import {
426
426
  printTemplates,
427
427
  substitute,
428
428
  buildVars,
429
- } from './plan-templates.mjs';
429
+ } from './artifact-templates.mjs';
430
430
 
431
431
  describe('Template library — getTemplate', () => {
432
432
  test('getTemplate("feature-design") returns content', () => {
@@ -568,7 +568,7 @@ describe('Template library — buildVars', () => {
568
568
  assert.ok(vars.author, 'author should be set');
569
569
  assert.ok(vars.created, 'created should be set');
570
570
  assert.ok(vars.lastEdited, 'lastEdited should be set');
571
- assert.equal(vars.lastEdited, vars.created, 'created and lastEdited should match for a new plan');
571
+ assert.equal(vars.lastEdited, vars.created, 'created and lastEdited should match for a new artifact');
572
572
  });
573
573
 
574
574
  test('auto-generates title from slug when not provided', () => {
@@ -587,7 +587,7 @@ describe('Template library — template content via runPlan', () => {
587
587
  const TEST_SLUG = 'test-tpl-new-' + Date.now();
588
588
 
589
589
  afterEach(() => {
590
- // Clean up plan directory if created
590
+ // Clean up artifact directory if created
591
591
  const planDir = join(PLANS_DIR, TEST_SLUG);
592
592
  if (existsSync(planDir)) rmSync(planDir, { recursive: true, force: true });
593
593
  });
@@ -599,14 +599,14 @@ describe('Template library — template content via runPlan', () => {
599
599
  );
600
600
  assert.equal(result, false, 'should return false for invalid template');
601
601
 
602
- // Verify no plan directory was left behind
602
+ // Verify no artifact directory was left behind
603
603
  const planDir = join(PLANS_DIR, TEST_SLUG);
604
604
  assert.equal(existsSync(planDir), false, 'should clean up on error');
605
605
  });
606
606
  });
607
607
 
608
608
  // ── Comment regression: createPlan → POST /api/comments ──────────────────────
609
- // This regression test ensures that a freshly created plan (with comments.json
609
+ // This regression test ensures that a freshly created artifact (with comments.json
610
610
  // as an empty array []) can receive the first comment via POST without crashing.
611
611
  // Previously, createPlan wrote {schemaVersion: 2, threads: []} which caused
612
612
  // JSON.parse + .push() to fail on the first comment.
@@ -621,8 +621,8 @@ describe('Comment regression: createPlan → POST /api/comments', () => {
621
621
  cleanupPlan(TEST_SLUG);
622
622
  });
623
623
 
624
- test('first comment on freshly created plan works (array shape)', async () => {
625
- // Create a plan directory — replicating what createPlan() does after the fix
624
+ test('first comment on freshly created artifact works (array shape)', async () => {
625
+ // Create a artifact directory — replicating what createPlan() does after the fix
626
626
  const planDir = join(PLANS_DIR, TEST_SLUG);
627
627
  mkdirSync(planDir, { recursive: true });
628
628
 
@@ -636,7 +636,7 @@ describe('Comment regression: createPlan → POST /api/comments', () => {
636
636
  lastEdited: now,
637
637
  };
638
638
  writeFileSync(join(planDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8');
639
- writeFileSync(join(planDir, 'plan.mdx'), '# Comment Regression Test\n\nTest.\n', 'utf-8');
639
+ writeFileSync(join(planDir, 'artifact.mdx'), '# Comment Regression Test\n\nTest.\n', 'utf-8');
640
640
  // Write as [] — same shape as the fixed createPlan() now produces
641
641
  writeFileSync(join(planDir, 'comments.json'), '[]', 'utf-8');
642
642
 
@@ -650,7 +650,7 @@ describe('Comment regression: createPlan → POST /api/comments', () => {
650
650
  headers: { 'Content-Type': 'application/json' },
651
651
  body: JSON.stringify({
652
652
  sectionId: 'regression-test',
653
- text: 'First comment on a fresh plan',
653
+ text: 'First comment on a fresh artifact',
654
654
  author: 'test-gate',
655
655
  }),
656
656
  });
@@ -661,7 +661,7 @@ describe('Comment regression: createPlan → POST /api/comments', () => {
661
661
  assert.equal(Array.isArray(updated), true, 'comments should be an array');
662
662
  assert.equal(updated.length, 1, 'should have exactly 1 comment');
663
663
  assert.equal(updated[0].sectionId, 'regression-test');
664
- assert.equal(updated[0].text, 'First comment on a fresh plan');
664
+ assert.equal(updated[0].text, 'First comment on a fresh artifact');
665
665
  assert.equal(updated[0].author, 'test-gate');
666
666
 
667
667
  // Verify the file is still valid JSON
@@ -700,8 +700,8 @@ describe('Comment regression: createPlan → POST /api/comments', () => {
700
700
 
701
701
  // ── htmx-friendly RESTful routes ───────────────────────────────────────────────
702
702
  // New slug-scoped routes designed for htmx (and the new HTML template).
703
- // GET /api/<slug>/plan → MDX text/plain
704
- // PUT /api/<slug>/plan → save MDX (form data or raw body)
703
+ // GET /api/<slug>/artifact → MDX text/plain
704
+ // PUT /api/<slug>/artifact → save MDX (form data or raw body)
705
705
  // GET /api/<slug>/comments → JSON (default) or HTML (?format=html)
706
706
  // POST /api/<slug>/comments → add comment, returns <li> HTML
707
707
  // PUT /api/<slug>/comments → replace comments array (JSON)
@@ -709,7 +709,7 @@ describe('Comment regression: createPlan → POST /api/comments', () => {
709
709
  // GET /htmx.min.js → self-hosted htmx library
710
710
  // The cross-slug protection: any path with a different slug in the URL
711
711
  // returns 403 to prevent the server from being tricked into serving
712
- // data for a different plan.
712
+ // data for a different artifact.
713
713
 
714
714
  describe('htmx-friendly RESTful routes', () => {
715
715
  const TEST_SLUG = 'test-htmx-routes-' + Date.now();
@@ -746,45 +746,45 @@ describe('htmx-friendly RESTful routes', () => {
746
746
  cleanupPlan(TEST_SLUG);
747
747
  });
748
748
 
749
- // ── Plan routes ────────────────────────────────────────────────────
750
- test('GET /api/<slug>/plan returns MDX as text/plain', async () => {
751
- const res = await fetch(`${baseUrl}/api/${TEST_SLUG}/plan`);
749
+ // ── Artifact routes ────────────────────────────────────────────────────
750
+ test('GET /api/<slug>/artifact returns MDX as text/plain', async () => {
751
+ const res = await fetch(`${baseUrl}/api/${TEST_SLUG}/artifact`);
752
752
  assert.equal(res.status, 200);
753
753
  assert.equal(res.headers.get('content-type').includes('text/plain'), true);
754
754
  const text = await res.text();
755
755
  assert.equal(text.includes('Htmx Routes Test'), true);
756
756
  });
757
757
 
758
- test('PUT /api/<slug>/plan with form data saves MDX', async () => {
759
- const newContent = '# Updated Htmx Plan\n\nNew content.\n';
760
- const res = await fetch(`${baseUrl}/api/${TEST_SLUG}/plan`, {
758
+ test('PUT /api/<slug>/artifact with form data saves MDX', async () => {
759
+ const newContent = '# Updated Htmx Artifact\n\nNew content.\n';
760
+ const res = await fetch(`${baseUrl}/api/${TEST_SLUG}/artifact`, {
761
761
  method: 'PUT',
762
762
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
763
763
  body: 'content=' + encodeURIComponent(newContent),
764
764
  });
765
765
  assert.equal(res.status, 200);
766
- const saved = readFileSync(join(PLANS_DIR, TEST_SLUG, 'plan.mdx'), 'utf-8');
766
+ const saved = readFileSync(join(PLANS_DIR, TEST_SLUG, 'artifact.mdx'), 'utf-8');
767
767
  assert.equal(saved, newContent);
768
768
  });
769
769
 
770
- test('PUT /api/<slug>/plan with raw text body saves MDX', async () => {
771
- const newContent = '# Raw Body Plan\n\nRaw body content.\n';
772
- const res = await fetch(`${baseUrl}/api/${TEST_SLUG}/plan`, {
770
+ test('PUT /api/<slug>/artifact with raw text body saves MDX', async () => {
771
+ const newContent = '# Raw Body Artifact\n\nRaw body content.\n';
772
+ const res = await fetch(`${baseUrl}/api/${TEST_SLUG}/artifact`, {
773
773
  method: 'PUT',
774
774
  headers: { 'Content-Type': 'text/plain' },
775
775
  body: newContent,
776
776
  });
777
777
  assert.equal(res.status, 200);
778
- const saved = readFileSync(join(PLANS_DIR, TEST_SLUG, 'plan.mdx'), 'utf-8');
778
+ const saved = readFileSync(join(PLANS_DIR, TEST_SLUG, 'artifact.mdx'), 'utf-8');
779
779
  assert.equal(saved, newContent);
780
780
  });
781
781
 
782
- test('PUT /api/<slug>/plan updates lastEdited in meta.json', async () => {
782
+ test('PUT /api/<slug>/artifact updates lastEdited in meta.json', async () => {
783
783
  const metaPath = join(PLANS_DIR, TEST_SLUG, 'meta.json');
784
784
  const before = JSON.parse(readFileSync(metaPath, 'utf-8'));
785
785
  // Wait a tick so the timestamp actually advances
786
786
  await new Promise((r) => setTimeout(r, 10));
787
- await fetch(`${baseUrl}/api/${TEST_SLUG}/plan`, {
787
+ await fetch(`${baseUrl}/api/${TEST_SLUG}/artifact`, {
788
788
  method: 'PUT',
789
789
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
790
790
  body: 'content=' + encodeURIComponent('# updated\n'),
@@ -825,7 +825,7 @@ describe('htmx-friendly RESTful routes', () => {
825
825
  });
826
826
 
827
827
  test('GET /api/<slug>/comments?format=html with no comments returns the empty marker', async () => {
828
- // Make a fresh plan with no comments
828
+ // Make a fresh artifact with no comments
829
829
  cleanupPlan(TEST_SLUG);
830
830
  createTempPlan(TEST_SLUG + '-empty', { mdx: '# Empty\n', comments: '[]' });
831
831
  const info = await startServer(TEST_SLUG + '-empty', join(PLANS_DIR, TEST_SLUG + '-empty'), 0);
@@ -968,7 +968,7 @@ describe('htmx-friendly RESTful routes', () => {
968
968
  });
969
969
 
970
970
  test('cross-slug PUT also returns 403', async () => {
971
- const res = await fetch(`${baseUrl}/api/wrong-slug/plan`, {
971
+ const res = await fetch(`${baseUrl}/api/wrong-slug/artifact`, {
972
972
  method: 'PUT',
973
973
  body: 'evil=true',
974
974
  });
@@ -983,7 +983,7 @@ describe('htmx-friendly RESTful routes', () => {
983
983
  });
984
984
 
985
985
  // ── HTML template smoke tests ─────────────────────────────────────────────────
986
- // These tests regenerate plan.html and verify the resulting HTML
986
+ // These tests regenerate artifact.html and verify the resulting HTML
987
987
  // uses htmx attributes (and does NOT use fetch() in JS for the
988
988
  // save/comment flows). The script tag for htmx should reference
989
989
  // the local /htmx.min.js path.
@@ -995,11 +995,11 @@ describe('HTML template uses htmx', () => {
995
995
  cleanupPlan(TEST_SLUG);
996
996
  });
997
997
 
998
- // Test the v1 template (plan.html.template) directly. The v1 template is
999
- // htmx-based; the v2 canvas template (plan.canvas.template) is a separate
998
+ // Test the v1 template (artifact.html.template) directly. The v1 template is
999
+ // htmx-based; the v2 canvas template (artifact.canvas.template) is a separate
1000
1000
  // file with its own tests below.
1001
- test('v1 plan.html.template uses htmx (read directly)', () => {
1002
- const tplPath = join(TEMPLATES_DIR, 'plan.html.template');
1001
+ test('v1 artifact.html.template uses htmx (read directly)', () => {
1002
+ const tplPath = join(TEMPLATES_DIR, 'artifact.html.template');
1003
1003
  const tpl = readFileSync(tplPath, 'utf-8');
1004
1004
 
1005
1005
  // Should load htmx from the local path
@@ -1033,7 +1033,7 @@ describe('HTML template uses htmx', () => {
1033
1033
  assert.ok(!/fetch\s*\(/.test(noCommentFetch), 'v1 non-comment code should not call fetch() directly');
1034
1034
  });
1035
1035
 
1036
- test('regenerated plan.html uses the v2 canvas template by default', async () => {
1036
+ test('regenerated artifact.html uses the v2 canvas template by default', async () => {
1037
1037
  const planDir = join(PLANS_DIR, TEST_SLUG);
1038
1038
  mkdirSync(planDir, { recursive: true });
1039
1039
  writeFileSync(
@@ -1047,17 +1047,17 @@ describe('HTML template uses htmx', () => {
1047
1047
  lastEdited: '2026-06-01T00:00:00.000Z',
1048
1048
  })
1049
1049
  );
1050
- writeFileSync(join(planDir, 'plan.mdx'), '# V2 Default Test\n\n## Section\n\nHello.\n');
1050
+ writeFileSync(join(planDir, 'artifact.mdx'), '# V2 Default Test\n\n## Section\n\nHello.\n');
1051
1051
  writeFileSync(join(planDir, 'comments.json'), '[]');
1052
1052
 
1053
1053
  await regenerateHtml(TEST_SLUG);
1054
1054
 
1055
- const html = readFileSync(join(planDir, 'plan.html'), 'utf-8');
1055
+ const html = readFileSync(join(planDir, 'artifact.html'), 'utf-8');
1056
1056
 
1057
- // The default regenerated plan.html should be the v2 canvas view
1058
- assert.ok(html.includes('id="canvas"'), 'default plan.html should be the v2 canvas view');
1057
+ // The default regenerated artifact.html should be the v2 canvas view
1058
+ assert.ok(html.includes('id="canvas"'), 'default artifact.html should be the v2 canvas view');
1059
1059
  assert.ok(html.includes('id="connections-layer"'),
1060
- 'default plan.html should have the SVG connections layer');
1060
+ 'default artifact.html should have the SVG connections layer');
1061
1061
 
1062
1062
  // Should have all placeholders substituted
1063
1063
  assert.ok(!html.includes('{{slug}}'), 'slug placeholder should be substituted');
@@ -1073,7 +1073,7 @@ describe('HTML template uses htmx', () => {
1073
1073
  });
1074
1074
 
1075
1075
  test('template is smaller than the pre-htmx version (regression check)', () => {
1076
- const tplPath = join(TEMPLATES_DIR, 'plan.html.template');
1076
+ const tplPath = join(TEMPLATES_DIR, 'artifact.html.template');
1077
1077
  const lines = readFileSync(tplPath, 'utf-8').split('\n').length;
1078
1078
  assert.ok(lines < 1039, `template should be smaller than the original 1039 lines, got ${lines}`);
1079
1079
  });
@@ -1105,7 +1105,7 @@ import {
1105
1105
  makeConnectionId,
1106
1106
  makeCommentId,
1107
1107
  makeReplyId,
1108
- } from './plan.mjs';
1108
+ } from './artifact.mjs';
1109
1109
 
1110
1110
  describe('Canvas helpers (pure functions)', () => {
1111
1111
  test('emptyCanvas returns a v2 schema with empty arrays', () => {
@@ -1118,9 +1118,9 @@ describe('Canvas helpers (pure functions)', () => {
1118
1118
  assert.deepEqual(c.viewport, { x: 0, y: 0, zoom: 1 });
1119
1119
  });
1120
1120
 
1121
- test('emptyCanvas with no title uses "Untitled plan"', () => {
1121
+ test('emptyCanvas with no title uses "Untitled artifact"', () => {
1122
1122
  const c = emptyCanvas();
1123
- assert.equal(c.title, 'Untitled plan');
1123
+ assert.equal(c.title, 'Untitled artifact');
1124
1124
  });
1125
1125
 
1126
1126
  test('id generators produce namespaced ids', () => {
@@ -1209,24 +1209,24 @@ describe('loadOrMigrateCanvas', () => {
1209
1209
  cleanupPlan(TEST_SLUG);
1210
1210
  });
1211
1211
 
1212
- test('returns existing plan.json if present', () => {
1212
+ test('returns existing artifact.json if present', () => {
1213
1213
  const planDir = join(PLANS_DIR, TEST_SLUG);
1214
1214
  mkdirSync(planDir, { recursive: true });
1215
1215
  const canvas = {
1216
1216
  schemaVersion: 2, title: 'Existing', elements: [], connections: [], comments: [],
1217
1217
  viewport: { x: 0, y: 0, zoom: 1 },
1218
1218
  };
1219
- writeFileSync(join(planDir, 'plan.json'), JSON.stringify(canvas));
1219
+ writeFileSync(join(planDir, 'artifact.json'), JSON.stringify(canvas));
1220
1220
 
1221
1221
  const result = loadOrMigrateCanvas(planDir, 'Existing');
1222
1222
  assert.equal(result.title, 'Existing');
1223
1223
  });
1224
1224
 
1225
- test('migrates plan.mdx to canvas on first read', () => {
1225
+ test('migrates artifact.mdx to canvas on first read', () => {
1226
1226
  const planDir = join(PLANS_DIR, TEST_SLUG);
1227
1227
  mkdirSync(planDir, { recursive: true });
1228
1228
  const mdx = '# Hello\n\n## World\n\nFoo bar\n';
1229
- writeFileSync(join(planDir, 'plan.mdx'), mdx);
1229
+ writeFileSync(join(planDir, 'artifact.mdx'), mdx);
1230
1230
 
1231
1231
  const result = loadOrMigrateCanvas(planDir, 'Migrated');
1232
1232
  assert.equal(result.schemaVersion, 2);
@@ -1234,11 +1234,11 @@ describe('loadOrMigrateCanvas', () => {
1234
1234
  assert.equal(result.elements[0].type, 'text');
1235
1235
  assert.ok(result.elements[0].content.includes('Foo bar'));
1236
1236
 
1237
- // After migration, plan.json should be on disk.
1238
- assert.equal(existsSync(join(planDir, 'plan.json')), true);
1237
+ // After migration, artifact.json should be on disk.
1238
+ assert.equal(existsSync(join(planDir, 'artifact.json')), true);
1239
1239
  });
1240
1240
 
1241
- test('returns empty canvas if no plan.json and no plan.mdx', () => {
1241
+ test('returns empty canvas if no artifact.json and no artifact.mdx', () => {
1242
1242
  const planDir = join(PLANS_DIR, TEST_SLUG);
1243
1243
  mkdirSync(planDir, { recursive: true });
1244
1244
 
@@ -1246,15 +1246,15 @@ describe('loadOrMigrateCanvas', () => {
1246
1246
  assert.equal(result.title, 'Brand New');
1247
1247
  assert.equal(result.elements.length, 0);
1248
1248
 
1249
- // Should have created plan.json.
1250
- assert.equal(existsSync(join(planDir, 'plan.json')), true);
1249
+ // Should have created artifact.json.
1250
+ assert.equal(existsSync(join(planDir, 'artifact.json')), true);
1251
1251
  });
1252
1252
 
1253
1253
  test('backfills missing arrays on a partial canvas', () => {
1254
1254
  const planDir = join(PLANS_DIR, TEST_SLUG);
1255
1255
  mkdirSync(planDir, { recursive: true });
1256
1256
  // Write a partial canvas (missing comments)
1257
- writeFileSync(join(planDir, 'plan.json'), JSON.stringify({
1257
+ writeFileSync(join(planDir, 'artifact.json'), JSON.stringify({
1258
1258
  schemaVersion: 2, title: 'Partial',
1259
1259
  elements: [], connections: [],
1260
1260
  }));
@@ -1305,8 +1305,8 @@ describe('Canvas HTTP endpoints', () => {
1305
1305
 
1306
1306
  test('GET /api/<slug>/canvas auto-migrates from mdx on first call', async () => {
1307
1307
  const planDir = join(PLANS_DIR, TEST_SLUG);
1308
- // First, clear the auto-generated plan.json so we test the migration path
1309
- const jsonPath = join(planDir, 'plan.json');
1308
+ // First, clear the auto-generated artifact.json so we test the migration path
1309
+ const jsonPath = join(planDir, 'artifact.json');
1310
1310
  if (existsSync(jsonPath)) rmSync(jsonPath);
1311
1311
  assert.equal(existsSync(jsonPath), false);
1312
1312
 
@@ -1316,7 +1316,7 @@ describe('Canvas HTTP endpoints', () => {
1316
1316
  // The migration creates a text element from the mdx
1317
1317
  assert.equal(data.elements.length, 1);
1318
1318
  assert.equal(data.elements[0].type, 'text');
1319
- // After this call, plan.json should exist
1319
+ // After this call, artifact.json should exist
1320
1320
  assert.equal(existsSync(jsonPath), true);
1321
1321
  });
1322
1322
 
@@ -1340,7 +1340,7 @@ describe('Canvas HTTP endpoints', () => {
1340
1340
  assert.equal(res.status, 200);
1341
1341
 
1342
1342
  // Verify the file was written
1343
- const saved = JSON.parse(readFileSync(join(PLANS_DIR, TEST_SLUG, 'plan.json'), 'utf-8'));
1343
+ const saved = JSON.parse(readFileSync(join(PLANS_DIR, TEST_SLUG, 'artifact.json'), 'utf-8'));
1344
1344
  assert.equal(saved.title, 'Updated');
1345
1345
  assert.equal(saved.elements.length, 2);
1346
1346
  assert.equal(saved.viewport.zoom, 1.5);
@@ -1355,7 +1355,7 @@ describe('Canvas HTTP endpoints', () => {
1355
1355
  body: JSON.stringify(partial),
1356
1356
  });
1357
1357
  assert.equal(res.status, 200);
1358
- const saved = JSON.parse(readFileSync(join(PLANS_DIR, TEST_SLUG, 'plan.json'), 'utf-8'));
1358
+ const saved = JSON.parse(readFileSync(join(PLANS_DIR, TEST_SLUG, 'artifact.json'), 'utf-8'));
1359
1359
  assert.deepEqual(saved.elements, []);
1360
1360
  assert.deepEqual(saved.connections, []);
1361
1361
  assert.deepEqual(saved.comments, []);
@@ -1738,7 +1738,7 @@ import {
1738
1738
  renderCommentPinHTML,
1739
1739
  renderCommentThreadHTML,
1740
1740
  renderReplyHTML,
1741
- } from './plan.mjs';
1741
+ } from './artifact.mjs';
1742
1742
 
1743
1743
  describe('htmx content negotiation on canvas endpoints', () => {
1744
1744
  const TEST_SLUG = 'test-htmx-negotiate-' + Date.now();
@@ -2082,7 +2082,7 @@ describe('htmx content negotiation on canvas endpoints', () => {
2082
2082
  assert.equal(res.status, 200);
2083
2083
 
2084
2084
  // Verify the saved state has proper arrays/objects, not JSON strings
2085
- const saved = JSON.parse(readFileSync(join(PLANS_DIR, TEST_SLUG, 'plan.json'), 'utf-8'));
2085
+ const saved = JSON.parse(readFileSync(join(PLANS_DIR, TEST_SLUG, 'artifact.json'), 'utf-8'));
2086
2086
  assert.equal(saved.title, 'Form Encoded');
2087
2087
  assert.ok(Array.isArray(saved.elements));
2088
2088
  assert.equal(saved.elements.length, 1);
@@ -2211,17 +2211,17 @@ describe('htmx content negotiation on canvas endpoints', () => {
2211
2211
  });
2212
2212
 
2213
2213
  // ── Canvas template structure tests ────────────────────────────────────────
2214
- // Verify the new plan.canvas.template has the right shape:
2214
+ // Verify the new artifact.canvas.template has the right shape:
2215
2215
  // - pan/zoom controls
2216
2216
  // - SVG layer for connections
2217
2217
  // - comment pins
2218
2218
  // - toolbar with element-type buttons
2219
2219
 
2220
- describe('plan.canvas.template structure', () => {
2221
- const tplPath = join(TEMPLATES_DIR, 'plan.canvas.template');
2220
+ describe('artifact.canvas.template structure', () => {
2221
+ const tplPath = join(TEMPLATES_DIR, 'artifact.canvas.template');
2222
2222
 
2223
2223
  test('template file exists', () => {
2224
- assert.ok(existsSync(tplPath), 'plan.canvas.template should exist');
2224
+ assert.ok(existsSync(tplPath), 'artifact.canvas.template should exist');
2225
2225
  });
2226
2226
 
2227
2227
  test('template declares {{canvasJson}} placeholder', () => {
@@ -2328,4 +2328,4 @@ describe('plan.canvas.template structure', () => {
2328
2328
 
2329
2329
  // ── End of template tests ──────────────────────────────────────────────────────
2330
2330
 
2331
- console.log(' plan.mjs tests loaded — run with: node --test cli/plan.test.mjs');
2331
+ console.log(' artifact.mjs tests loaded — run with: node --test cli/artifact.test.mjs');