Files
tt-a1iandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> 397b28a4cf fix(viewer): address PR #566 review findings
- Node index: a Route pick the Finder refuses no longer falls back to
  focus, which cleared the active Route.
- Schemas keep accepting retired meta.views (ignored) so existing
  schema 1/2 files still validate, per the 2.x compatibility contract.
- Rail comfort check measures node label sizes directly, so the 12px
  default-collapse rule covers all five diagram types.
- Rail choices live in memory first; blocked localStorage no longer
  freezes collapse/expand/placement.
- Escape-collapsing the overlay rail returns focus to the reveal control.
- Oversized raster exports downscale below 1x instead of allocating a
  canvas over the pixel cap.
- README and Gallery drop removed chapter-playback links and copy;
  README demo captures and live-proof GIF re-recorded on the new shell.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-25 19:05:46 +08:00

140 lines
5.2 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { copySiteAssets } from './copy-site-assets.mjs';
import { diagramTypeCopyReplacements } from './site-copy.mjs';
import { CASES, renderCard } from '../website/src/data/gallery-presentation.mjs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, '..');
const skillRoot = path.join(repoRoot, 'archify');
const outputRoot = path.resolve(process.argv[2] || path.join(repoRoot, 'docs'));
const artifactsRoot = path.join(outputRoot, 'gallery', 'artifacts');
const sourcesRoot = path.join(outputRoot, 'gallery', 'sources');
const templatePath = path.join(__dirname, 'gallery-template.html');
const packageJson = JSON.parse(fs.readFileSync(path.join(skillRoot, 'package.json'), 'utf8'));
const SHAPES = {
architecture: ['components', 'connections'],
workflow: ['nodes', 'edges'],
sequence: ['participants', 'messages'],
dataflow: ['nodes', 'flows'],
lifecycle: ['states', 'transitions'],
};
function digest(buffer) {
return crypto.createHash('sha256').update(buffer).digest('hex');
}
fs.rmSync(artifactsRoot, { recursive: true, force: true });
fs.rmSync(sourcesRoot, { recursive: true, force: true });
fs.mkdirSync(artifactsRoot, { recursive: true });
fs.mkdirSync(sourcesRoot, { recursive: true });
const entries = [];
for (const item of CASES) {
const inputPath = path.join(skillRoot, 'examples', item.input);
const sourceBuffer = fs.readFileSync(inputPath);
const source = JSON.parse(sourceBuffer.toString('utf8'));
const artifactPath = path.join(artifactsRoot, item.output);
const sourcePath = path.join(sourcesRoot, item.input);
execFileSync(process.execPath, [
path.join(skillRoot, 'renderers', item.type, `render-${item.type}.mjs`),
inputPath,
artifactPath,
], { stdio: ['ignore', 'ignore', 'pipe'] });
fs.copyFileSync(inputPath, sourcePath);
const checkOutput = execFileSync(process.execPath, [
path.join(skillRoot, 'scripts', 'check-render-output.mjs'),
artifactPath,
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
const validation = JSON.parse(checkOutput);
const artifactBuffer = fs.readFileSync(artifactPath);
const [nodeKey, edgeKey] = SHAPES[item.type];
const checksPassed = validation.checks.filter((check) => check.ok).length;
entries.push({
...item,
title: source.meta.title,
subtitle: source.meta.subtitle || '',
schemaVersion: source.schema_version,
visualPreset: source.meta.visual_preset || 'classic',
animation: source.meta.animation || 'static',
engineeringProfile: source.meta.engineering_profile || null,
nodeCount: Array.isArray(source[nodeKey]) ? source[nodeKey].length : 0,
edgeCount: Array.isArray(source[edgeKey]) ? source[edgeKey].length : 0,
artifactBytes: artifactBuffer.byteLength,
sourceBytes: sourceBuffer.byteLength,
artifactSha256: digest(artifactBuffer),
sourceSha256: digest(sourceBuffer),
checkCount: validation.checks.length,
checksPassed,
checks: validation.checks.map((check) => ({ name: check.name, ok: check.ok })),
composition: validation.composition,
});
}
const manifest = {
schemaVersion: 1,
generator: 'scripts/build-gallery.mjs',
archifyVersion: packageJson.version,
entryCount: entries.length,
checkCount: entries.reduce((sum, entry) => sum + entry.checkCount, 0),
entries: entries.map((entry) => ({
id: entry.id,
type: entry.type,
title: entry.title,
subtitle: entry.subtitle,
input: `gallery/sources/${entry.input}`,
artifact: `gallery/artifacts/${entry.output}`,
focus: entry.focus,
schemaVersion: entry.schemaVersion,
visualPreset: entry.visualPreset,
animation: entry.animation,
engineeringProfile: entry.engineeringProfile,
nodeCount: entry.nodeCount,
edgeCount: entry.edgeCount,
artifactBytes: entry.artifactBytes,
artifactSha256: entry.artifactSha256,
sourceBytes: entry.sourceBytes,
sourceSha256: entry.sourceSha256,
checks: entry.checks,
composition: entry.composition,
})),
};
const manifestJson = JSON.stringify(manifest, null, 2);
fs.writeFileSync(path.join(outputRoot, 'gallery', 'manifest.json'), `${manifestJson}\n`);
const replacements = {
...diagramTypeCopyReplacements(),
'[[ARCHIFY_VERSION]]': packageJson.version,
'[[ENTRY_COUNT]]': String(manifest.entryCount),
'[[CHECK_COUNT]]': String(manifest.checkCount),
'[[GALLERY_CARDS]]': entries.map(renderCard).join('\n'),
'[[MANIFEST_JSON]]': manifestJson.replace(/<\/script/gi, '<\\/script'),
};
let html = fs.readFileSync(templatePath, 'utf8');
for (const [placeholder, value] of Object.entries(replacements)) {
html = html.split(placeholder).join(value);
}
if (/\[\[[A-Z0-9_]+\]\]/.test(html)) {
throw new Error('Gallery template contains unresolved placeholders');
}
const galleryPath = path.join(outputRoot, 'gallery.html');
copySiteAssets(galleryPath);
fs.writeFileSync(galleryPath, html);
console.log(`gallery ${manifest.entryCount} artifacts / ${manifest.checkCount} checks`);
console.log(path.join(outputRoot, 'gallery.html'));