import { readFileSync, readdirSync } from 'node:fs'; const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8'); const built = (path) => read(`dist/${path}`); const rendered = (value) => value .replace(//gi, '') .replace(//gi, '') .replace(//g, '') .replace(/<[^>]+>/g, ' ') .replace(/&/g, '&') .replace( /&(lt|gt|quot|nbsp);/g, (_, entity) => ({ lt: '<', gt: '>', quot: '"', nbsp: ' ' })[entity], ) .replace(/�?39;/g, "'") .replace(/\s+/g, ' ') .trim(); const normalize = (value) => value.replace(/\s+/g, ' ').trim(); const html = { landing: built('index.html'), guide: built('full-guide/index.html'), summary: built('summary/index.html'), models: built('models/index.html'), agents: built('agents/index.html'), skills: built('skills/index.html'), rules: built('rules/index.html'), review: built('skills-review/index.html'), starter: built('hands-on/starter/index.html'), labRules: built('hands-on/rules/index.html'), }; const snapshot = (path) => normalize(read(`.agents/snapshots/${path}`)); const reviewAssetSrc = html.review.match(/src="([^"]+\.[a-zA-Z0-9_-]{8,}\.js)"/)?.[1]; const reviewAsset = reviewAssetSrc ? read(`dist/_astro/${reviewAssetSrc.split('/').at(-1)}`) : ''; const builtCss = readdirSync(new URL('../dist/_astro/', import.meta.url)) .filter((file) => file.endsWith('.css')) .map((file) => read(`dist/_astro/${file}`)) .join('\n'); const source = { references: read('docs/references/README.md'), additionalReading: read('docs/references/additional-reading.md'), skillSources: read('src/content/skillSources/ponytail.json'), providers: [ read('src/content/providers/openai.json'), read('src/content/providers/claude.json'), read('src/content/providers/gemini.json'), ].join('\n'), starter: read('public/hands-on/starter/app.js'), ndoReview: read('src/content/reviews/ndo-repro.md'), }; // Rendered prose catches a deleted paragraph that string-token checks miss. if (rendered(html.landing) !== snapshot('index.txt')) throw new Error('landing rendered-text snapshot changed'); if (rendered(html.guide) !== snapshot('full-guide.txt')) throw new Error('full-guide rendered-text snapshot changed'); if (rendered(html.summary) !== snapshot('summary.txt')) throw new Error('summary rendered-text snapshot changed'); if (rendered(html.models) !== snapshot('models.txt')) throw new Error('models rendered-text snapshot changed'); if (rendered(html.agents) !== snapshot('agents.txt')) throw new Error('agents rendered-text snapshot changed'); if (rendered(html.skills) !== snapshot('skills.txt')) throw new Error('skills rendered-text snapshot changed'); if (rendered(html.rules) !== snapshot('rules.txt')) throw new Error('rules rendered-text snapshot changed'); if (rendered(html.review) !== snapshot('skills-review.txt')) throw new Error('skills-review rendered-text snapshot changed'); if (rendered(html.starter) !== snapshot('hands-on/starter.txt')) throw new Error('starter rendered-text snapshot changed'); if (rendered(html.labRules) !== snapshot('hands-on/rules.txt')) throw new Error('rules lab rendered-text snapshot changed'); // The 102 Portuguese strings were extracted verbatim from the legacy // `app.js` `translations.pt` object at cutover, before that file was deleted. // This is a legacy capture, not a snapshot of the Astro build: it still asserts // against an independent source, which is the whole point of the check. const portuguese = JSON.parse(read('.agents/snapshots/full-guide-pt.json')).map(rendered); if (portuguese.length !== 102) throw new Error('full-guide Portuguese snapshot no longer has 102 translated strings'); // Without this, trimming the snapshot would make the check below pass vacuously. if (portuguese.some((value) => !value.trim())) throw new Error('full-guide Portuguese snapshot has an empty entry'); if (!portuguese.every((value) => rendered(html.guide).includes(value))) throw new Error('built full-guide lost a Portuguese translation'); const has = (page, tokens) => tokens.every((token) => page.includes(token)); if (!has(html.guide, ['data-phase="plan"', 'data-phase="build"', 'data-phase="review"'])) throw new Error('built full-guide lost its phase selector'); if (!has(html.guide, ['data-tree="main"', 'data-tree="ui"', 'data-worker="ui"'])) throw new Error('built full-guide lost its worker or worktree selector'); if ( !has(html.guide, [ 'data-route="plan"', 'data-model-provider="openai"', 'data-model-provider="claude"', 'data-model-provider="gemini"', ]) ) throw new Error('built full-guide lost its model-routing controls'); if (!has(html.guide, ['data-effort="low"', 'data-effort="medium"', 'data-effort="high"'])) throw new Error('built full-guide lost its reasoning-effort controls'); if ( !has(html.guide, [ 'data-skill-file="skill"', 'data-skill-step="observe"', 'data-skill-step="validate"', ]) ) throw new Error('built full-guide lost its skill anatomy controls'); if ( !has(html.guide, [ 'data-common-skill="ponytail"', 'data-common-skill="caveman"', 'data-common-skill="unlazy"', ]) ) throw new Error('built full-guide lost its common-skill controls'); if ( !has(html.guide, [ 'id="hands-on"', 'data-copy-target="prompt-install-skills"', 'data-copy-target="prompt-basic"', 'data-copy-target="prompt-skills"', ]) ) throw new Error('built full-guide lost its hands-on prompts'); if (!has(html.guide, ['hands-on/starter/', 'additional-reading.md', 'role="tablist"', '')) throw new Error('built full-guide lost its interaction module'); if ( !has(html.rules, [ 'id="pipeline"', 'id="skills"', 'id="examples"', 'data-stage="context"', 'data-stage="cli"', 'data-stage="commit"', 'data-stage="review"', ]) ) throw new Error('built rules page lost its enforcement pipeline'); if ( !has(html.rules, [ 'gate-discipline', 'parallel-agents', 'repo-db', 'tech-debt', 'skill-writer', 'scripts/check-ui-contract.mjs', '.husky/pre-commit', '.pr-review.json', '.agents/skills', 'netcracker/interview', ]) ) throw new Error('built rules page lost its case-study content'); if ( !has(html.rules, [ 'data-lang="en"', 'data-lang="pt"', 'data-copy-prompt', 'aria-live="polite"', 'role="tablist"', ]) ) throw new Error('built rules page lost its accessible controls'); if (!html.rules.includes('data-rules-copy') || !html.rules.includes('"pt"')) throw new Error('built rules page lost its Portuguese interaction data'); if ( !has(html.review, [ 'id="catalog"', 'id="skill-filter"', 'id="skill-list"', 'id="detail"', 'Preview Markdown', '?author=Name&skill=skill-id&view=improved', 'SKILLS_REVIEW_VOTE_API', ]) ) throw new Error('built review desk lost its catalog or deep-link contract'); if ( !has(html.review, [ 'ndo-repro', 'gfiber-logging', 'confluence-page', 'diagram-plantuml', 'page-reviewer', 'unslop', 'spanish-naturalizer', 'draft-mr', 'semantic-diff-review', ]) ) throw new Error('built review desk lost submitted-skill coverage'); if ( !html.review.includes('Anonymous%20Operational%20Submission') || !has(reviewAsset, ['[REDACTED]', '[REDACTED LOCAL USER]', '[REDACTED USER]']) ) throw new Error('built review desk lost operational-submission privacy redactions'); if (!reviewAssetSrc) throw new Error('built review desk does not load its hashed island asset'); if ( ![html.summary, html.models, html.agents, html.skills].every((page) => page.includes('ROUTE MAP')) ) throw new Error('built chapter pages lost shared navigation'); if ( !has(html.landing, [ 'The short route', 'full-guide/', 'models/', 'agents/', 'skills/', 'rules/', 'hands-on/starter/', 'skills-review/', ]) ) throw new Error('built landing page lost a route-map destination'); if (/ page.includes('name="viewport"'))) throw new Error('a built route lacks a viewport declaration'); if (allPages.some(([, page]) => /<(script|link)[^>]+(src|href)="https?:[^\"]+"/i.test(page))) throw new Error('a built route has an external runtime dependency'); if ( !read('package.json').includes( '"verify": "node scripts/verify.mjs && node scripts/audit-ui.mjs && node .agents/scripts/check-tokens.mjs"', ) ) throw new Error('pnpm verify no longer runs audit-ui and check-tokens'); if (!read('.agents/scripts/gate.sh').includes('pnpm run verify')) throw new Error('the gate no longer runs the output contract'); if (!read('docs/references/README.md').includes('https://developers.openai.com/codex/skills')) throw new Error('primary reference list lost Codex skills'); if ((read('docs/references/additional-reading.md').match(/^### \d+\./gm) || []).length < 5) throw new Error('additional reading list has fewer than five entries'); if ( /Pedro\s+Aranha|pedro\.aranha|git\.netcracker\.com|artifactorycn|managed\.netcracker\.cloud/i.test( read('src/content/reviews/ndo-repro.md'), ) ) throw new Error('operational review content has a privacy leak'); // These retain the pre-Astro contract one fact at a time. Visible facts read // from dist; source-only contracts remain at their authoritative boundary. if ( ![ 'https://code.claude.com/docs/en/sub-agents', 'https://code.claude.com/docs/en/skills', 'https://code.claude.com/docs/en/worktrees', 'https://git-scm.com/docs/git-worktree.html', 'https://developers.openai.com/codex/skills', ].every((url) => source.references.includes(url)) ) throw new Error('missing reference contract'); if (!has(html.guide, ['data-phase="plan"', 'data-copy-target="prompt-basic"'])) throw new Error('missing full-guide content contract'); if (!has(html.guide, ['data-route="plan"', 'data-skill-file="skill"', '