Back to Conway's Game of Life
SOURCE / PINNED RELEASE

Made of little things.

Conway's Game of Life

Release
230955df1900…
Author-recorded commit
c08196c2b050…
License
LICENSE
Author’s source reference
nostr://npub16nzc4ql748r2kv9202rgzrt0fmtk4n3tzsjl86yf6ty9agqe9u0qjcc7hc/wss%3A%2F%2Fgit.napplet.soy%2F/n-644c45e22c7

Archive hash verified: 2627ce678935a213…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/guidance.test.mjs
import assert from 'node:assert/strict';
import { readdir, readFile } from 'node:fs/promises';
import { dirname, extname, join, relative } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { runInNewContext } from 'node:vm';
import ts from 'typescript';

const root = dirname(dirname(fileURLToPath(import.meta.url)));
const textExtensions = new Set(['.css', '.html', '.json', '.md', '.mjs', '.ts']);
const skippedDirectories = new Set(['.git', 'dist', 'node_modules', '.agents', '.claude', '.napplet-space']);

async function collectTextFiles(directory = root) {
  const files = [];
  for (const entry of await readdir(directory, { withFileTypes: true })) {
    if (entry.isDirectory() && skippedDirectories.has(entry.name)) continue;
    const path = join(directory, entry.name);
    if (entry.isDirectory()) files.push(...await collectTextFiles(path));
    else if (entry.isFile() && textExtensions.has(extname(entry.name))) files.push(path);
  }
  return files;
}

const files = await collectTextFiles();
const sources = new Map(
  await Promise.all(files.map(async (path) => [relative(root, path), await readFile(path, 'utf8')])),
);

test('rejects retired app-owned bootstrap and probing APIs', () => {
  const joined = (...parts) => parts.join('');
  const shimSpecifier = ['@napplet', 'shim'].join('/');
  const patterns = [
    new RegExp(`import\\s+(?:[^'\"]+\\s+from\\s+)?['\"]${shimSpecifier}['\"]`),
    new RegExp(`shell(?:\\.|\\?\\.)${joined('rea', 'dy')}\\s*\\(`),
    new RegExp(`shell(?:\\.|\\?\\.)${joined('in', 'it')}\\s*\\(`),
    new RegExp(`shell(?:\\.|\\?\\.)${joined('supp', 'orts')}\\s*\\(`),
    new RegExp(`window(?:\\.|\\?\\.)napplet(?:\\.|\\?\\.)${joined('sh', 'ell')}`),
    new RegExp(`window\\s*\\[\\s*['\"]napplet['\"]\\s*\\]\\s*(?:\\.|\\?\\.)${joined('sh', 'ell')}`),
    new RegExp(`window\\s*\\[\\s*['\"]napplet['\"]\\s*\\]\\s*\\[\\s*['\"]${joined('sh', 'ell')}['\"]\\s*\\]`),
    new RegExp(`${joined('discover', 'Services')}\\s*\\(`),
    new RegExp(`${joined('has', 'Service')}(?:Version)?\\s*\\(`),
  ];
  const retiredExamples = [
    `import { install } from '${shimSpecifier}';`,
    `shell?.${joined('rea', 'dy')}()`,
    `shell.${joined('in', 'it')}()`,
    `shell.${joined('supp', 'orts')}('storage')`,
    `window?.napplet?.${joined('sh', 'ell')}`,
    `window['napplet']?.${joined('sh', 'ell')}`,
    `window['napplet']['${joined('sh', 'ell')}']`,
    `${joined('discover', 'Services')}()`,
    `${joined('has', 'Service', 'Version')}('relay', '1')`,
  ];

  for (const [index, example] of retiredExamples.entries()) {
    assert.match(example, patterns[index]);
  }

  for (const [path, source] of sources) {
    for (const pattern of patterns) assert.doesNotMatch(source, pattern, path);
  }
});

test('keeps normal Nostr examples OUTBOX-first', () => {
  const main = sources.get('src/main.ts');
  const patterns = ['query', 'subscribe', 'publish'].map(
    (operation) => new RegExp(['relay', operation].join('\\.') + '\\s*\\('),
  );
  for (const pattern of patterns) assert.doesNotMatch(main, pattern);

  const joined = (...parts) => parts.join('');
  const directCallPatterns = [
    new RegExp(`window(?:\\.|\\?\\.)napplet(?:\\.|\\?\\.)[A-Za-z_$][\\w$]*(?:\\.|\\?\\.)[A-Za-z_$][\\w$]*\\s*\\(`),
    new RegExp(`window\\s*\\[\\s*['\"]napplet['\"]\\s*\\]\\s*\\[\\s*['\"][^'\"]+['\"]\\s*\\]\\s*(?:\\.|\\?\\.)[A-Za-z_$][\\w$]*\\s*\\(`),
  ];
  const directCallExamples = [
    `window?.napplet?.storage.${joined('get', 'Item')}('key')`,
    `window['napplet']['outbox'].${joined('qu', 'ery')}([])`,
  ];
  for (const [index, example] of directCallExamples.entries()) {
    assert.match(example, directCallPatterns[index]);
  }
  for (const [path, source] of sources) {
    if (!path.startsWith('src/')) continue;
    for (const pattern of directCallPatterns) assert.doesNotMatch(source, pattern, path);
  }

  const designPatterns = sources.get('docs/design-patterns.md');
  assert.match(designPatterns, /OUTBOX-first/i);
  assert.match(designPatterns, /relay-local escape hatch/i);
});

test('ships no forked skill body, points agents at skills.sh, and keeps manifest config on documented surface', () => {
  const skillBodies = [...sources.keys()].filter((path) => /(?:^|\/)SKILL\.md$/.test(path));
  assert.deepEqual(skillBodies, []);
  for (const path of ['AGENTS.md', 'README.md']) {
    assert.match(sources.get(path), /docs\/napplet-space\.md/, path);
  }
  const retiredInstallers = new RegExp(
    [['@napplet', 'skills'].join('/'), ['napplet skills', 'install'].join(' '), ['.codex', 'skills'].join('/')]
      .map((text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
      .join('|'),
  );
  for (const [path, source] of sources) {
    assert.doesNotMatch(source, retiredInstallers, path);
  }

  const viteConfig = sources.get('vite.config.ts');
  assert.match(viteConfig, /artifactMode:\s*'single-file'/);
  // `requires` is allowed once a product has a core task that cannot run
  // without a domain, but it must stay a list of bare domain names.
  const requires = viteConfig.match(/\brequires\s*:\s*\[([^\]]*)\]/);
  if (requires) {
    const entries = requires[1].split(',').map((entry) => entry.trim()).filter(Boolean);
    assert.ok(entries.length > 0, 'requires must not be an empty list; omit it instead');
    for (const entry of entries) {
      assert.match(entry, /^['"][a-z][a-z0-9-]*['"]$/, `requires entry ${entry} must be a bare domain name`);
    }
  }
  assert.doesNotMatch(viteConfig, /\bconfigSchema\b/);
  assert.equal(JSON.parse(sources.get('config.schema.json')).type, 'object');
  const retiredSingleFilePlugin = new RegExp(['vite-plugin', 'singlefile'].join('-'));
  for (const source of sources.values()) assert.doesNotMatch(source, retiredSingleFilePlugin);
});

test('keeps deferred domains out of active package surfaces', () => {
  const joined = (...parts) => parts.join('');
  const domains = [joined('con', 'nect'), joined('cl', 'ass')];

  for (const domain of domains) {
    const patterns = [
      new RegExp(`@napplet/nap/${domain}\\b`),
      new RegExp(`window(?:\\.|\\?\\.)napplet(?:\\.|\\?\\.)${domain}\\b`),
      new RegExp(`window\\s*\\[\\s*['\"]napplet['\"]\\s*\\]\\s*\\[\\s*['\"]${domain}['\"]\\s*\\]`),
      new RegExp(`import\\s*\\{[^}]*\\b${domain}\\b[^}]*\\}\\s*from\\s*['\"]@napplet/sdk['\"]`),
      new RegExp(`\\b${domain.toUpperCase()}_DOMAIN\\b`),
      new RegExp(`\\b${domain}\\s*(?:\\.|\\?\\.)[A-Za-z]\\w*\\s*\\(`),
      new RegExp(`\\brequires\\s*:\\s*\\[[^\\]]*['\"]${domain}['\"]`),
    ];
    const examples = [
      `@napplet/nap/${domain}`,
      `window?.napplet?.${domain}`,
      `window['napplet']['${domain}']`,
      `import { ${domain} } from '@napplet/sdk'`,
      `${domain.toUpperCase()}_DOMAIN`,
      `${domain}.request()`,
      `requires: ['${domain}']`,
    ];

    for (const [index, example] of examples.entries()) {
      assert.match(example, patterns[index]);
    }
    for (const [path, source] of sources) {
      for (const pattern of patterns) assert.doesNotMatch(source, pattern, path);
    }
  }

  for (const label of [joined('NAP-', 'CON', 'NECT'), joined('NAP-', 'CLASS')]) {
    for (const [path, source] of sources) {
      if (source.includes(label)) assert.match(source, /deferred|retired/i, path);
    }
  }
});

test('treats injected optional-domain absence as a normal state', async () => {
  const helperSource = await readFile(join(root, 'src/domain-availability.ts'), 'utf8');
  const compiled = ts.transpileModule(helperSource, {
    compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
  }).outputText;
  const module = { exports: {} };
  runInNewContext(compiled, { module, exports: module.exports });
  const { hasDomain } = module.exports;

  assert.equal(hasDomain(undefined, 'outbox'), false);
  assert.equal(hasDomain({}, 'outbox'), false);
  assert.equal(hasDomain({ outbox: null }, 'outbox'), false);
  assert.equal(hasDomain({ outbox: {} }, 'outbox'), true);
  assert.equal(hasDomain(Object.create({ outbox: {} }), 'outbox'), true);

  // Product code may replace every demo control; it must keep gating optional
  // domains through the injected-namespace check instead of a probe API.
  const main = sources.get('src/main.ts');
  assert.match(main, /from '\.\/domain-availability\.js'/);
  assert.match(main, /runtimeHasDomain\(/);
});

test('keeps the applet layout contract: no title header, frame-filling root', () => {
  const html = sources.get('index.html');
  const title = html.match(/<title>([^<]*)<\/title>/)?.[1]?.trim();
  assert.ok(title, 'index.html keeps a <title> for metadata');
  const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  // The runtime shows the napplet's name; do not repeat it as a heading.
  assert.doesNotMatch(html, new RegExp(`<h[1-6][^>]*>\\s*${escaped}\\s*<`));
  assert.doesNotMatch(html, /class="[^"]*\b(?:masthead|eyebrow|hero)\b/);

  const css = sources.get('src/styles.css');
  assert.doesNotMatch(css, /\bbody\s*\{[^}]*min-width/);
  assert.match(css, /html,\s*body,\s*#app\s*\{[^}]*height:\s*100%/);
});

// --- Project pins (build plan Phase 6) ---------------------------------------
// Listing strings are frozen at Phase 9 with the player; until then these
// guards keep the two sources of truth from drifting and pin the codename.

test('keeps listing metadata single-sourced: napplet.json == index.html head', () => {
  const project = JSON.parse(sources.get('napplet.json'));
  const html = sources.get('index.html');

  assert.equal(project.name, 'game-of-life', 'codename is load-bearing (identity/previewId)');
  assert.equal(JSON.parse(sources.get('package.json')).name, project.name,
    'package.json name tracks the codename');

  assert.ok(project.title && project.title.trim() === project.title, 'manifest title nonempty, no edge whitespace');
  const title = html.match(/<title>([^<]*)<\/title>/)?.[1]?.trim();
  assert.equal(title, project.title, '<title> == manifest title');

  assert.ok(project.description && project.description.trim() === project.description,
    'manifest description nonempty, no edge whitespace');
  const meta = html.match(/<meta\s+name="description"\s+content="([^"]*)"/)?.[1];
  assert.ok(meta, 'index.html carries a description meta (newer plugin/CLI reads it)');
  assert.equal(meta, project.description, 'meta description byte-identical to manifest description');

  assert.ok(Array.isArray(project.topics) && project.topics.length > 0, 'topics listed');
  assert.equal(project.license, 'MIT');
});

test('QA hook is quarantined to QA surfaces (no cheat-surface exposure in player code)', () => {
  // window.game_of_life may be touched ONLY by the browser suite; product code
  // must never read it back (self-feeding cheat surface), and listing copy
  // (title/description/topics) must never mention it.
  const main = sources.get('src/main.ts');
  assert.match(main, /window\.game_of_life\s*=/, 'QA hook stays exported for the suite');
  assert.doesNotMatch(main.replace(/window\.game_of_life\s*=/g, ''), /window\.game_of_life/,
    'main.ts never reads its own QA hook back');
  const project = JSON.parse(sources.get('napplet.json'));
  const listingText = [project.title, project.description, ...project.topics].join(' ');
  assert.doesNotMatch(listingText, /QA|hook|game_of_life/i, 'listing copy stays clean of QA affordances');
});

test('save wire declares host storage as core and keeps browser storage APIs out (D6)', () => {
  const project = JSON.parse(sources.get('napplet.json'));
  assert.ok(project.requires?.includes('storage'), 'napplet.json lists storage in requires');
  assert.match(sources.get('vite.config.ts'), /requires:\s*\[\s*'storage'\s*\]/,
    'vite plugin manifest lists storage too (published as R)');
  const wire = sources.get('src/savewire.ts');
  assert.match(wire, /runtimeHasDomain\('storage'\)/, 'save wire gates every call on the storage domain');
  assert.match(wire, /'game-of-life:board'/, "save keys carry the plan's game-of-life: prefix");
  for (const [path, source] of sources) {
    if (!path.startsWith('src/')) continue;
    assert.doesNotMatch(source, /\blocal\s*Storage\s*\.|\bsession\s*Storage\s*\./,
      `${path}: durable state goes through the host storage wire, never browser storage (D6)`);
  }
});

test('counter cues stay CSS-only with a reduced-motion guard, and the rejected seam decoration stays out (Phase 8, spec §12)', () => {
  const css = sources.get('src/styles.css');
  const main = sources.get('src/main.ts');
  // Cue mechanism law: transient classes + animationend self-removal, no
  // canvas fx layer introduced by Phase 8.
  assert.match(main, /animationend/, 'cues self-remove on animationend (one persistent listener per element)');
  assert.match(css, /@keyframes gen-tick/);
  assert.match(css, /@keyframes pop-pulse/);
  assert.match(css, /\.num\.cue-gen\s*\{/);
  assert.match(css, /\.num\.cue-pop\s*\{/);
  // Reduced-motion guard must FLATTEN (animation-duration), never kill the
  // animation: animation:none would stop animationend and strand the class.
  assert.match(css, /@media\s*\(prefers-reduced-motion:\s*reduce\)/);
  assert.doesNotMatch(css, /animation:\s*none/);
  // Player ruling 2026-10-30: wrap seam cues rejected for this game; the
  // sparkline candidate likewise never lands. Reopen = ruling loop, not drift.
  assert.doesNotMatch(css, /\.board\s*::\s*(?:before|after)/);
  assert.doesNotMatch(css, /seam/i);
  assert.doesNotMatch(main, /sparkline/i);
});