The art critic, as the site uses it: recorded per revision in each SPEC (tools/critic/) and asked live by the REPL's "is it art?" about whatever is in the editor. The rubric itself (the criteria, the request, reading the answers, the mean of runs) is worker/src/art-rubric.mjs, which the Worker also scores listeners' songs with; it is re-exported here so every caller keeps one import. This file adds recording verdicts in SPEC.md and reading a song the way the critic does.
8import { CRITERIA, CRITIC_MODEL, RUBRIC } from '../../../worker/src/art-rubric.mjs';
10export { artOf, artRequest, CRITERIA, CRITIC_MODEL, meanVerdict, RUBRIC, songFacts } from '../../../worker/src/art-rubric.mjs';
Recording a verdict in SPEC.md frontmatter (tools/critic/), as text
edits, so the rest of the frontmatter keeps its quoting and comments.
art: goes on the revision after its output line, with artRuns: under
it; an unscorable revision is art: null # not scored: <reason>.
Either replaces whatever the entry held before.
17export function setRevisionArt(text, rev, verdict) { 18 const entry = new RegExp(`^ {2}- rev: ${rev}\\s*\\n((?:^ {4}.*\\n)*)`, 'm'); 19 const m = text.match(entry); 20 if (!m) throw new Error(`no rev ${rev} entry`); 21 const lines = m[1].split('\n').filter((l) => l && !/^ {4}art(?:Runs)?:/.test(l)); 22 const at = lines.findIndex((l) => /^ {4}output:/.test(l)); 23 const art = verdict.reason 24 ? [` art: null # not scored: ${verdict.reason}`] 25 : [` art: ${verdict.art}`, ` artRuns: ${runsLine(verdict.artRuns)}`]; 26 lines.splice(at < 0 ? lines.length : at + 1, 0, ...art); 27 return text.replace(entry, ` - rev: ${rev}\n${lines.join('\n')}\n`); 28}
The song-level critic: block, for the current revision's verdict,
replacing any; a new one goes after song:.
32export function setCritic(text, { art, artRuns, criteria }, date) { 33 const scores = Object.entries(criteria) 34 .map(([k, v]) => ` ${k}: ${v}\n`) 35 .join(''); 36 const block = 37 `critic:\n date: ${date}\n model: ${CRITIC_MODEL}\n method: ${RUBRIC}\n art: ${art}\n` + 38 ` artRuns: ${runsLine(artRuns)}\n criteria:\n${scores}`; 39 if (/^critic:\n(?: {2}.*\n)*/m.test(text)) return text.replace(/^critic:\n(?: {2}.*\n)*/m, block); 40 return text.replace(/^(song:.*\n)/m, `$1${block}`); 41}
43const runsLine = (runs) => `[${runs.join(', ')}]`;
The step a score lands on, for the hover: "3/5: a hook that sticks…".
46export const levelText = (key, score) => `${score.toFixed(1)}/5: ${CRITERIA[key].levels[Math.round(score)]}`;
The spec as the critic reads it: SPEC.md without its frontmatter, split
as the first critic did (text.split('---', 2)).
The code as the critic reads it: song.js itself, without the // @title
line the site adds (songs.mjs).
57export const songCode = (code) => code.replace(/^\/\/ @title [^\n]*\n/, '');