/** * lib/pagespec-blocks — the fenced-JSON grammar has ONE home (audit 2026-09-24, lot 15). * * Until lot 15 the tree carried 17 private spellings of the ```json fence regex * (`JSON_BLOCK_RE`, `PAGESPEC_JSON_BLOCK_RE`, a second `FENCED_JSON_RE`, inline * `md.match(/```json…/)`), all UNANCHORED — a prose mention or an indented fence * opened a phantom block, and the write-back helpers (`renamePagespecRefs`, * `rewriteMachineBlock`) located the span to rewrite with that phantom. This * gate walks every source `.ts` under `templates/skills` and refuses any regex * literal that spells the fence outside `lib/pagespec-blocks.ts`. * * FIRES on the historical spelling and on the `/g` and LF-only variants; SILENT * on a template string that merely EMITS a fence (the scaffolders write * ```json blocks — that is not a grammar) and on the SSOT file itself. */ import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { resolve, relative, dirname } from 'node:path' import { fileURLToPath } from 'node:url' import { listFiles } from './helpers/cli-options.js' const SKILLS_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..') /** The one file allowed to spell the grammar. */ const SSOT = 'lib/pagespec-blocks.ts' /** * A regex LITERAL whose body opens with the fence: `/```json…/flags`. A template * string or a plain string containing ```json is an emitter, not a grammar, and * is not matched (the literal must start with `/` and the fence). */ const FENCE_REGEX_LITERAL = /\/\^?```json[^\n/]*\/[a-z]*/ // `^` optional: the SSOT itself is anchored export function fenceSpellings(source: string): string[] { const out: string[] = [] const lines = source.split(/\r?\n/) lines.forEach((line, i) => { if (FENCE_REGEX_LITERAL.test(line)) out.push(`${i + 1}: ${line.trim()}`) }) return out } describe('fence grammar SSOT — no private spelling of the ```json regex outside lib/pagespec-blocks.ts', () => { it('FIRES on the historical spellings', () => { expect(fenceSpellings('const JSON_BLOCK_RE = /```json\\s*\\r?\\n([\\s\\S]*?)\\r?\\n```/')).toHaveLength(1) expect(fenceSpellings(' const blockRe = /```json\\s*\\n([\\s\\S]*?)\\n```/g;')).toHaveLength(1) expect(fenceSpellings(' const m = md.match(/```json\\s*\\r?\\n([\\s\\S]*?)\\r?\\n```/)')).toHaveLength(1) }) it('SILENT on an emitter (template string writing a fence) and on plain prose', () => { expect(fenceSpellings(' lines.push("```json", JSON.stringify(spec, null, 2), "```")')).toEqual([]) expect(fenceSpellings(' return `## Lifecycle\\n\\n```json\\n${body}\\n```\\n`')).toEqual([]) expect(fenceSpellings(' * the block is fenced ```json … ``` at column 0')).toEqual([]) }) it('the tree carries the grammar in ONE file', () => { const files = listFiles(SKILLS_ROOT, (p) => p.endsWith('.ts') && !p.includes('__tests__') && !p.includes('node_modules')) expect(files.length).toBeGreaterThan(400) const offenders: string[] = [] for (const abs of files) { const rel = relative(SKILLS_ROOT, abs).replace(/\\/g, '/') if (rel === SSOT) continue const hits = fenceSpellings(readFileSync(abs, 'utf8')) for (const h of hits) offenders.push(`${rel}:${h}`) } expect(offenders, `private fence spellings — import FENCED_JSON_RE / fencedJsonRe() / extractJsonBlock(s) from lib/pagespec-blocks instead:\n${offenders.join('\n')}`).toEqual([]) // The SSOT itself still spells it (the gate is not green because nothing spells the fence at all). expect(fenceSpellings(readFileSync(resolve(SKILLS_ROOT, SSOT), 'utf8')).length).toBeGreaterThan(0) }) })