1// The art critic, as the site uses it: recorded per revision in each SPEC 2// (tools/critic/) and asked live by the REPL's "is it art?" about whatever 3// is in the editor. The rubric itself (the criteria, the request, reading 4// the answers, the mean of runs) is worker/src/art-rubric.mjs, which the 5// Worker also scores listeners' songs with; it is re-exported here so every 6// caller keeps one import. This file adds recording verdicts in SPEC.md 7// and reading a song the way the critic does. 8import { CRITERIA, CRITIC_MODEL, RUBRIC } from '../../../worker/src/art-rubric.mjs'; 9 10export { artOf, artRequest, CRITERIA, CRITIC_MODEL, meanVerdict, RUBRIC, songFacts } from '../../../worker/src/art-rubric.mjs'; 11 12// Recording a verdict in SPEC.md frontmatter (tools/critic/), as text 13// edits, so the rest of the frontmatter keeps its quoting and comments. 14// `art:` goes on the revision after its output line, with `artRuns:` under 15// it; an unscorable revision is `art: null # not scored: <reason>`. 16// 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} 29 30// The song-level `critic:` block, for the current revision's verdict, 31// 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} 42 43const runsLine = (runs) => `[${runs.join(', ')}]`; 44 45// 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)]}`; 47 48// The spec as the critic reads it: SPEC.md without its frontmatter, split 49// as the first critic did (`text.split('---', 2)`). 50export function specBody(text) { 51 const [, , ...rest] = text.split('---'); 52 return rest.join('---').trim(); 53} 54 55// The code as the critic reads it: song.js itself, without the `// @title` 56// line the site adds (songs.mjs). 57export const songCode = (code) => code.replace(/^\/\/ @title [^\n]*\n/, '');