@kbediako/codex-orchestrator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +238 -0
- package/dist/bin/codex-orchestrator.js +507 -0
- package/dist/orchestrator/src/agents/builder.js +16 -0
- package/dist/orchestrator/src/agents/index.js +4 -0
- package/dist/orchestrator/src/agents/planner.js +17 -0
- package/dist/orchestrator/src/agents/reviewer.js +13 -0
- package/dist/orchestrator/src/agents/tester.js +13 -0
- package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +20 -0
- package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +164 -0
- package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +32 -0
- package/dist/orchestrator/src/cli/adapters/CommandTester.js +33 -0
- package/dist/orchestrator/src/cli/adapters/index.js +4 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +28 -0
- package/dist/orchestrator/src/cli/doctor.js +48 -0
- package/dist/orchestrator/src/cli/events/runEvents.js +84 -0
- package/dist/orchestrator/src/cli/exec/command.js +56 -0
- package/dist/orchestrator/src/cli/exec/context.js +108 -0
- package/dist/orchestrator/src/cli/exec/experience.js +77 -0
- package/dist/orchestrator/src/cli/exec/finalization.js +140 -0
- package/dist/orchestrator/src/cli/exec/learning.js +62 -0
- package/dist/orchestrator/src/cli/exec/stageRunner.js +71 -0
- package/dist/orchestrator/src/cli/exec/summary.js +109 -0
- package/dist/orchestrator/src/cli/exec/telemetry.js +18 -0
- package/dist/orchestrator/src/cli/exec/tfgrpo.js +200 -0
- package/dist/orchestrator/src/cli/exec/tfgrpoArtifacts.js +19 -0
- package/dist/orchestrator/src/cli/exec/types.js +1 -0
- package/dist/orchestrator/src/cli/init.js +64 -0
- package/dist/orchestrator/src/cli/mcp.js +124 -0
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +404 -0
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +138 -0
- package/dist/orchestrator/src/cli/orchestrator.js +554 -0
- package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +32 -0
- package/dist/orchestrator/src/cli/pipelines/designReference.js +72 -0
- package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +71 -0
- package/dist/orchestrator/src/cli/pipelines/index.js +34 -0
- package/dist/orchestrator/src/cli/run/environment.js +24 -0
- package/dist/orchestrator/src/cli/run/manifest.js +367 -0
- package/dist/orchestrator/src/cli/run/manifestPersister.js +88 -0
- package/dist/orchestrator/src/cli/run/runPaths.js +30 -0
- package/dist/orchestrator/src/cli/selfCheck.js +12 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +420 -0
- package/dist/orchestrator/src/cli/services/controlPlaneService.js +107 -0
- package/dist/orchestrator/src/cli/services/execRuntime.js +69 -0
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +47 -0
- package/dist/orchestrator/src/cli/services/runPreparation.js +82 -0
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
- package/dist/orchestrator/src/cli/services/schedulerService.js +42 -0
- package/dist/orchestrator/src/cli/tasks/taskMetadata.js +19 -0
- package/dist/orchestrator/src/cli/telemetry/schema.js +8 -0
- package/dist/orchestrator/src/cli/types.js +1 -0
- package/dist/orchestrator/src/cli/ui/HudApp.js +112 -0
- package/dist/orchestrator/src/cli/ui/controller.js +26 -0
- package/dist/orchestrator/src/cli/ui/store.js +240 -0
- package/dist/orchestrator/src/cli/utils/enforcementMode.js +12 -0
- package/dist/orchestrator/src/cli/utils/fs.js +8 -0
- package/dist/orchestrator/src/cli/utils/interactive.js +25 -0
- package/dist/orchestrator/src/cli/utils/jsonlWriter.js +10 -0
- package/dist/orchestrator/src/cli/utils/optionalDeps.js +30 -0
- package/dist/orchestrator/src/cli/utils/packageInfo.js +25 -0
- package/dist/orchestrator/src/cli/utils/planFormatter.js +49 -0
- package/dist/orchestrator/src/cli/utils/runId.js +7 -0
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +26 -0
- package/dist/orchestrator/src/cli/utils/strings.js +8 -0
- package/dist/orchestrator/src/cli/utils/time.js +6 -0
- package/dist/orchestrator/src/control-plane/drift-reporter.js +109 -0
- package/dist/orchestrator/src/control-plane/index.js +3 -0
- package/dist/orchestrator/src/control-plane/request-builder.js +217 -0
- package/dist/orchestrator/src/control-plane/types.js +1 -0
- package/dist/orchestrator/src/control-plane/validator.js +50 -0
- package/dist/orchestrator/src/credentials/CredentialBroker.js +1 -0
- package/dist/orchestrator/src/events/EventBus.js +25 -0
- package/dist/orchestrator/src/learning/crystalizer.js +108 -0
- package/dist/orchestrator/src/learning/harvester.js +146 -0
- package/dist/orchestrator/src/learning/manifest.js +56 -0
- package/dist/orchestrator/src/learning/runner.js +177 -0
- package/dist/orchestrator/src/learning/validator.js +164 -0
- package/dist/orchestrator/src/logger.js +20 -0
- package/dist/orchestrator/src/manager.js +388 -0
- package/dist/orchestrator/src/persistence/ArtifactStager.js +95 -0
- package/dist/orchestrator/src/persistence/ExperienceStore.js +210 -0
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +65 -0
- package/dist/orchestrator/src/persistence/RunManifestWriter.js +23 -0
- package/dist/orchestrator/src/persistence/TaskStateStore.js +172 -0
- package/dist/orchestrator/src/persistence/identifierGuards.js +1 -0
- package/dist/orchestrator/src/persistence/lockFile.js +26 -0
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +26 -0
- package/dist/orchestrator/src/persistence/sanitizeRunId.js +8 -0
- package/dist/orchestrator/src/persistence/sanitizeTaskId.js +8 -0
- package/dist/orchestrator/src/persistence/writeAtomicFile.js +4 -0
- package/dist/orchestrator/src/privacy/guard.js +111 -0
- package/dist/orchestrator/src/scheduler/index.js +1 -0
- package/dist/orchestrator/src/scheduler/plan.js +171 -0
- package/dist/orchestrator/src/scheduler/types.js +1 -0
- package/dist/orchestrator/src/sync/CloudRunsClient.js +1 -0
- package/dist/orchestrator/src/sync/CloudRunsHttpClient.js +82 -0
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +206 -0
- package/dist/orchestrator/src/sync/createCloudSyncWorker.js +15 -0
- package/dist/orchestrator/src/types.js +1 -0
- package/dist/orchestrator/src/utils/atomicWrite.js +15 -0
- package/dist/orchestrator/src/utils/errorMessage.js +14 -0
- package/dist/orchestrator/src/utils/executionMode.js +69 -0
- package/dist/packages/control-plane-schemas/src/index.js +1 -0
- package/dist/packages/control-plane-schemas/src/run-request.js +548 -0
- package/dist/packages/orchestrator/src/exec/handle-service.js +203 -0
- package/dist/packages/orchestrator/src/exec/session-manager.js +147 -0
- package/dist/packages/orchestrator/src/exec/unified-exec.js +432 -0
- package/dist/packages/orchestrator/src/index.js +3 -0
- package/dist/packages/orchestrator/src/instructions/loader.js +101 -0
- package/dist/packages/orchestrator/src/instructions/promptPacks.js +151 -0
- package/dist/packages/orchestrator/src/notifications/index.js +74 -0
- package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +142 -0
- package/dist/packages/orchestrator/src/tool-orchestrator.js +161 -0
- package/dist/packages/sdk-node/src/orchestrator.js +195 -0
- package/dist/packages/shared/config/designConfig.js +495 -0
- package/dist/packages/shared/config/env.js +37 -0
- package/dist/packages/shared/config/index.js +2 -0
- package/dist/packages/shared/design-artifacts/writer.js +221 -0
- package/dist/packages/shared/events/serializer.js +84 -0
- package/dist/packages/shared/events/types.js +1 -0
- package/dist/packages/shared/manifest/artifactUtils.js +36 -0
- package/dist/packages/shared/manifest/designArtifacts.js +665 -0
- package/dist/packages/shared/manifest/fileIO.js +29 -0
- package/dist/packages/shared/manifest/toolRuns.js +78 -0
- package/dist/packages/shared/manifest/toolkitArtifacts.js +223 -0
- package/dist/packages/shared/manifest/types.js +5 -0
- package/dist/packages/shared/manifest/validator.js +73 -0
- package/dist/packages/shared/manifest/writer.js +2 -0
- package/dist/packages/shared/streams/stdio.js +112 -0
- package/dist/scripts/design/pipeline/advanced-assets.js +466 -0
- package/dist/scripts/design/pipeline/componentize.js +74 -0
- package/dist/scripts/design/pipeline/context.js +34 -0
- package/dist/scripts/design/pipeline/extract.js +249 -0
- package/dist/scripts/design/pipeline/optionalDeps.js +107 -0
- package/dist/scripts/design/pipeline/prepare.js +46 -0
- package/dist/scripts/design/pipeline/reference.js +94 -0
- package/dist/scripts/design/pipeline/state.js +206 -0
- package/dist/scripts/design/pipeline/toolkit/common.js +94 -0
- package/dist/scripts/design/pipeline/toolkit/extract.js +258 -0
- package/dist/scripts/design/pipeline/toolkit/publish.js +202 -0
- package/dist/scripts/design/pipeline/toolkit/publishActions.js +12 -0
- package/dist/scripts/design/pipeline/toolkit/reference.js +846 -0
- package/dist/scripts/design/pipeline/toolkit/snapshot.js +882 -0
- package/dist/scripts/design/pipeline/toolkit/tokens.js +456 -0
- package/dist/scripts/design/pipeline/visual-regression.js +137 -0
- package/dist/scripts/design/pipeline/write-artifacts.js +61 -0
- package/package.json +97 -0
- package/schemas/manifest.json +1064 -0
- package/templates/README.md +12 -0
- package/templates/codex/mcp-client.json +8 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
import { access, cp, mkdir, readFile, readdir, symlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { loadPixelmatch, loadPlaywright, loadPngjs } from '../optionalDeps.js';
|
|
6
|
+
import { loadDesignContext } from '../context.js';
|
|
7
|
+
import { appendApprovals, appendToolkitArtifacts, ensureToolkitState, loadDesignRunState, saveDesignRunState, upsertStage, upsertToolkitContext } from '../state.js';
|
|
8
|
+
import { stageArtifacts } from '../../../../orchestrator/src/persistence/ArtifactStager.js';
|
|
9
|
+
import { buildRetentionMetadata } from './common.js';
|
|
10
|
+
import { normalizeSentenceSpacing, runDefaultInteractions } from './snapshot.js';
|
|
11
|
+
const TOOLKIT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
12
|
+
const DEFAULT_SELF_CORRECTION_THRESHOLD = 1.5;
|
|
13
|
+
const MISMATCH_PADDING_COLOR = { r: 255, g: 0, b: 255, a: 255 };
|
|
14
|
+
async function resolveDesignDeps() {
|
|
15
|
+
const [playwright, pngModule, pixelmatchModule] = await Promise.all([
|
|
16
|
+
loadPlaywright(),
|
|
17
|
+
loadPngjs(),
|
|
18
|
+
loadPixelmatch()
|
|
19
|
+
]);
|
|
20
|
+
const pixelmatchFn = pixelmatchModule.default ??
|
|
21
|
+
pixelmatchModule;
|
|
22
|
+
return {
|
|
23
|
+
playwright,
|
|
24
|
+
png: pngModule.PNG,
|
|
25
|
+
pixelmatch: pixelmatchFn
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function main() {
|
|
29
|
+
const context = await loadDesignContext();
|
|
30
|
+
const state = await loadDesignRunState(context.statePath);
|
|
31
|
+
const stageId = 'design-toolkit-reference';
|
|
32
|
+
const toolkitState = ensureToolkitState(state);
|
|
33
|
+
const contexts = toolkitState.contexts;
|
|
34
|
+
if (contexts.length === 0) {
|
|
35
|
+
upsertStage(state, {
|
|
36
|
+
id: stageId,
|
|
37
|
+
title: 'Toolkit reference + self-correction',
|
|
38
|
+
status: 'skipped',
|
|
39
|
+
notes: ['No toolkit contexts available. Run previous stages first.']
|
|
40
|
+
});
|
|
41
|
+
await saveDesignRunState(context.statePath, state);
|
|
42
|
+
console.log('[design-toolkit-reference] skipped — no contexts');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const retention = toolkitState.retention ?? {
|
|
46
|
+
days: state.retention?.days ?? 30,
|
|
47
|
+
autoPurge: state.retention?.autoPurge ?? false,
|
|
48
|
+
policy: state.retention?.policy ?? 'design.config.retention'
|
|
49
|
+
};
|
|
50
|
+
const selfCorrection = context.config.config.pipelines.hiFiDesignToolkit.selfCorrection;
|
|
51
|
+
const advanced = context.config.config.advanced;
|
|
52
|
+
const designDeps = selfCorrection.enabled ? await resolveDesignDeps() : null;
|
|
53
|
+
const tmpRoot = join(tmpdir(), `design-toolkit-reference-${Date.now()}`);
|
|
54
|
+
await mkdir(tmpRoot, { recursive: true });
|
|
55
|
+
const artifacts = [];
|
|
56
|
+
const approvals = [];
|
|
57
|
+
const failures = [];
|
|
58
|
+
let processed = 0;
|
|
59
|
+
for (const entry of contexts) {
|
|
60
|
+
try {
|
|
61
|
+
const outputs = await buildReferenceOutputs(entry, context.repoRoot, tmpRoot);
|
|
62
|
+
const retentionMetadata = buildRetentionMetadata(retention, new Date());
|
|
63
|
+
const [referenceArtifact] = await stageArtifacts({
|
|
64
|
+
taskId: context.taskId,
|
|
65
|
+
runId: context.runId,
|
|
66
|
+
artifacts: [
|
|
67
|
+
{
|
|
68
|
+
path: relative(process.cwd(), outputs.referencePath),
|
|
69
|
+
description: `Reference page for ${entry.slug}`
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
options: {
|
|
73
|
+
relativeDir: `design-toolkit/reference/${entry.slug}`,
|
|
74
|
+
overwrite: true
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
await mirrorReferenceAssets({
|
|
78
|
+
entry,
|
|
79
|
+
repoRoot: context.repoRoot,
|
|
80
|
+
referenceArtifactPath: referenceArtifact.path
|
|
81
|
+
});
|
|
82
|
+
artifacts.push({
|
|
83
|
+
id: `${entry.slug}-reference`,
|
|
84
|
+
stage: 'reference',
|
|
85
|
+
status: 'succeeded',
|
|
86
|
+
relative_path: referenceArtifact.path,
|
|
87
|
+
description: `Reference implementation for ${entry.slug}`,
|
|
88
|
+
retention: retentionMetadata,
|
|
89
|
+
metrics: {
|
|
90
|
+
section_count: outputs.sectionCount
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
upsertToolkitContext(state, {
|
|
94
|
+
...entry,
|
|
95
|
+
referencePath: referenceArtifact.path
|
|
96
|
+
});
|
|
97
|
+
if (selfCorrection.enabled) {
|
|
98
|
+
const correction = await runSelfCorrection({
|
|
99
|
+
entry,
|
|
100
|
+
repoRoot: context.repoRoot,
|
|
101
|
+
tmpRoot,
|
|
102
|
+
referenceArtifactPath: referenceArtifact.path,
|
|
103
|
+
pipelineBreakpoints: context.config.config.pipelines.hiFiDesignToolkit.breakpoints,
|
|
104
|
+
threshold: selfCorrection.threshold ?? DEFAULT_SELF_CORRECTION_THRESHOLD,
|
|
105
|
+
maxIterations: selfCorrection.maxIterations ?? 1,
|
|
106
|
+
deps: designDeps
|
|
107
|
+
});
|
|
108
|
+
const correctionArtifacts = [
|
|
109
|
+
{
|
|
110
|
+
path: relative(process.cwd(), correction.path),
|
|
111
|
+
description: `Self-correction report for ${entry.slug}`
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
if (correction.settlingLogPath) {
|
|
115
|
+
correctionArtifacts.push({
|
|
116
|
+
path: relative(process.cwd(), correction.settlingLogPath),
|
|
117
|
+
description: `Counter settling log for ${entry.slug}`
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const assetOffset = correctionArtifacts.length;
|
|
121
|
+
correction.assets.forEach((asset) => correctionArtifacts.push({
|
|
122
|
+
path: relative(process.cwd(), asset.path),
|
|
123
|
+
description: asset.description
|
|
124
|
+
}));
|
|
125
|
+
const stagedCorrection = await stageArtifacts({
|
|
126
|
+
taskId: context.taskId,
|
|
127
|
+
runId: context.runId,
|
|
128
|
+
artifacts: correctionArtifacts,
|
|
129
|
+
options: {
|
|
130
|
+
relativeDir: `design-toolkit/diffs/${entry.slug}`,
|
|
131
|
+
overwrite: true
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const diffArtifact = stagedCorrection[0];
|
|
135
|
+
const settlingArtifact = correction.settlingLogPath ? stagedCorrection[1] : undefined;
|
|
136
|
+
const evidenceArtifacts = stagedCorrection.slice(assetOffset);
|
|
137
|
+
artifacts.push({
|
|
138
|
+
id: `${entry.slug}-self-correct`,
|
|
139
|
+
stage: 'self-correct',
|
|
140
|
+
status: 'succeeded',
|
|
141
|
+
relative_path: diffArtifact.path,
|
|
142
|
+
description: `Self-correction summary for ${entry.slug}`,
|
|
143
|
+
retention: retentionMetadata,
|
|
144
|
+
metrics: {
|
|
145
|
+
iterations: correction.iterations,
|
|
146
|
+
final_iteration: correction.finalIteration,
|
|
147
|
+
final_error_rate: correction.finalErrorRate,
|
|
148
|
+
threshold: selfCorrection.threshold ?? DEFAULT_SELF_CORRECTION_THRESHOLD,
|
|
149
|
+
threshold_passed: correction.finalErrorRate <= (selfCorrection.threshold ?? DEFAULT_SELF_CORRECTION_THRESHOLD)
|
|
150
|
+
? 1
|
|
151
|
+
: 0
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
if (settlingArtifact) {
|
|
155
|
+
artifacts.push({
|
|
156
|
+
id: `${entry.slug}-self-correct-settle`,
|
|
157
|
+
stage: 'self-correct',
|
|
158
|
+
status: 'succeeded',
|
|
159
|
+
relative_path: settlingArtifact.path,
|
|
160
|
+
description: `Counter settling log for ${entry.slug}`,
|
|
161
|
+
retention: retentionMetadata,
|
|
162
|
+
metrics: {
|
|
163
|
+
wait_ms: correction.settlingWaitMs ?? 0,
|
|
164
|
+
baseline_error: correction.baselineError,
|
|
165
|
+
stabilized_error: correction.finalErrorRate,
|
|
166
|
+
final_iteration: correction.finalIteration,
|
|
167
|
+
threshold: selfCorrection.threshold ?? DEFAULT_SELF_CORRECTION_THRESHOLD
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (evidenceArtifacts.length > 0) {
|
|
172
|
+
evidenceArtifacts.forEach((artifactRecord, index) => {
|
|
173
|
+
const asset = correction.assets[index];
|
|
174
|
+
artifacts.push({
|
|
175
|
+
id: `${entry.slug}-self-correct-${asset?.role ?? 'asset'}-${asset?.breakpoint ?? index + 1}`,
|
|
176
|
+
stage: 'self-correct',
|
|
177
|
+
status: 'succeeded',
|
|
178
|
+
relative_path: artifactRecord.path,
|
|
179
|
+
description: asset?.description ?? artifactRecord.description ?? 'Self-correction asset',
|
|
180
|
+
retention: retentionMetadata,
|
|
181
|
+
metrics: asset?.breakpoint ? { breakpoint: asset.breakpoint } : undefined
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
approvals.push({
|
|
186
|
+
id: `self-correct-${entry.slug}`,
|
|
187
|
+
actor: selfCorrection.provider ?? metadataApprover(context),
|
|
188
|
+
reason: `Self-correction provider ${selfCorrection.provider ?? 'local'} approved`,
|
|
189
|
+
timestamp: new Date().toISOString()
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (advanced.ffmpeg.enabled) {
|
|
193
|
+
approvals.push({
|
|
194
|
+
id: `ffmpeg-${entry.slug}`,
|
|
195
|
+
actor: advanced.ffmpeg.approver ?? metadataApprover(context),
|
|
196
|
+
reason: 'FFmpeg diff rendering approved',
|
|
197
|
+
timestamp: new Date().toISOString()
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
processed += 1;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
204
|
+
failures.push(`${entry.slug}: ${message}`);
|
|
205
|
+
console.error(`[design-toolkit-reference] failed for ${entry.slug}: ${message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (artifacts.length > 0) {
|
|
209
|
+
appendToolkitArtifacts(state, artifacts);
|
|
210
|
+
}
|
|
211
|
+
if (approvals.length > 0) {
|
|
212
|
+
appendApprovals(state, approvals);
|
|
213
|
+
}
|
|
214
|
+
const status = failures.length === 0 && processed > 0 ? 'succeeded' : 'failed';
|
|
215
|
+
upsertStage(state, {
|
|
216
|
+
id: stageId,
|
|
217
|
+
title: 'Toolkit reference + self-correction',
|
|
218
|
+
status,
|
|
219
|
+
notes: failures.length > 0 ? failures : undefined,
|
|
220
|
+
metrics: {
|
|
221
|
+
processed,
|
|
222
|
+
self_correction_enabled: selfCorrection.enabled
|
|
223
|
+
},
|
|
224
|
+
artifacts: artifacts.map((artifact) => ({
|
|
225
|
+
relative_path: artifact.relative_path,
|
|
226
|
+
stage: artifact.stage,
|
|
227
|
+
status: artifact.status,
|
|
228
|
+
description: artifact.description ?? undefined
|
|
229
|
+
}))
|
|
230
|
+
});
|
|
231
|
+
await saveDesignRunState(context.statePath, state);
|
|
232
|
+
if (status === 'failed') {
|
|
233
|
+
throw new Error('Reference or self-correction stage failed.');
|
|
234
|
+
}
|
|
235
|
+
console.log(`[design-toolkit-reference] produced references for ${processed} contexts`);
|
|
236
|
+
}
|
|
237
|
+
async function buildReferenceOutputs(entry, repoRoot, tmpRoot) {
|
|
238
|
+
const referenceDir = join(tmpRoot, entry.slug, 'reference');
|
|
239
|
+
await mkdir(referenceDir, { recursive: true });
|
|
240
|
+
const referencePath = join(referenceDir, 'index.html');
|
|
241
|
+
const sections = await loadSections(entry, repoRoot);
|
|
242
|
+
const sectionCount = sections.length;
|
|
243
|
+
const html = await buildReferenceHtml(entry, repoRoot, sections);
|
|
244
|
+
await writeFile(referencePath, html, 'utf8');
|
|
245
|
+
return { referencePath, sectionCount: sectionCount > 0 ? sectionCount : 3 };
|
|
246
|
+
}
|
|
247
|
+
async function runSelfCorrection(options) {
|
|
248
|
+
const { entry, repoRoot, tmpRoot, referenceArtifactPath, pipelineBreakpoints, threshold } = options;
|
|
249
|
+
const correctionDir = join(tmpRoot, entry.slug, 'diffs');
|
|
250
|
+
const screenshotsDir = join(correctionDir, 'screens');
|
|
251
|
+
await mkdir(correctionDir, { recursive: true });
|
|
252
|
+
await mkdir(screenshotsDir, { recursive: true });
|
|
253
|
+
const sourceUrl = entry.referenceUrl ?? entry.url;
|
|
254
|
+
const cloneUrl = resolveCloneUrl(entry, repoRoot, referenceArtifactPath);
|
|
255
|
+
const breakpoints = resolveBreakpointTargets(entry, pipelineBreakpoints);
|
|
256
|
+
const macro = await loadInteractionMacroForCapture(entry, repoRoot);
|
|
257
|
+
const browser = await options.deps.playwright.chromium.launch({ headless: true });
|
|
258
|
+
const results = [];
|
|
259
|
+
const iterationHistory = [];
|
|
260
|
+
let lastResults = [];
|
|
261
|
+
const maxIterations = Math.max(1, options.maxIterations ?? 1);
|
|
262
|
+
let bestResults = null;
|
|
263
|
+
let bestWorstMismatch = Number.POSITIVE_INFINITY;
|
|
264
|
+
let bestIteration = 0;
|
|
265
|
+
let iterationsRun = 0;
|
|
266
|
+
let settlingLogPath;
|
|
267
|
+
let settlingWaitMs;
|
|
268
|
+
try {
|
|
269
|
+
for (let iteration = 1; iteration <= maxIterations; iteration += 1) {
|
|
270
|
+
const iterationResults = [];
|
|
271
|
+
for (const breakpoint of breakpoints) {
|
|
272
|
+
const result = await captureBreakpointDiff({
|
|
273
|
+
browser,
|
|
274
|
+
deps: options.deps,
|
|
275
|
+
breakpoint,
|
|
276
|
+
screenshotsDir,
|
|
277
|
+
sourceUrl,
|
|
278
|
+
cloneUrl,
|
|
279
|
+
macro,
|
|
280
|
+
waitMs: entry.interactionWaitMs ?? null,
|
|
281
|
+
suffix: iteration > 1 ? `iter-${iteration}` : undefined,
|
|
282
|
+
iteration
|
|
283
|
+
});
|
|
284
|
+
iterationResults.push(result);
|
|
285
|
+
if (result.clipped) {
|
|
286
|
+
console.warn(`[design-toolkit-reference] Detected clipped screenshot for ${entry.slug} at ${breakpoint.id}: reference ${result.referenceDimensions.width}x${result.referenceDimensions.height}, clone ${result.cloneDimensions.width}x${result.cloneDimensions.height}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (iterationResults.length === 0) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
iterationsRun = iteration;
|
|
293
|
+
lastResults = iterationResults;
|
|
294
|
+
const worstMismatch = Math.max(...iterationResults.map((item) => item.mismatchPercentage ?? 0));
|
|
295
|
+
const averageMismatch = iterationResults.reduce((sum, item) => sum + (item.mismatchPercentage ?? 0), 0) / iterationResults.length;
|
|
296
|
+
iterationHistory.push({
|
|
297
|
+
iteration,
|
|
298
|
+
worstMismatch: roundPercent(worstMismatch),
|
|
299
|
+
averageMismatch: roundPercent(averageMismatch)
|
|
300
|
+
});
|
|
301
|
+
const improved = worstMismatch + 0.001 < bestWorstMismatch;
|
|
302
|
+
if (improved) {
|
|
303
|
+
bestResults = iterationResults;
|
|
304
|
+
bestWorstMismatch = worstMismatch;
|
|
305
|
+
bestIteration = iteration;
|
|
306
|
+
}
|
|
307
|
+
if (threshold > 0 && worstMismatch <= threshold) {
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
if (iteration >= maxIterations) {
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
if (!improved && iteration >= 1) {
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const finalResults = bestResults ?? lastResults ?? results;
|
|
318
|
+
if (finalResults.length > 0) {
|
|
319
|
+
results.splice(0, results.length, ...finalResults);
|
|
320
|
+
const worst = results.reduce((previous, current) => (current.mismatchPercentage ?? 0) >= (previous.mismatchPercentage ?? 0) ? current : previous);
|
|
321
|
+
const settleWait = Math.max(entry.interactionWaitMs ?? 0, 650);
|
|
322
|
+
const settled = await captureBreakpointDiff({
|
|
323
|
+
browser,
|
|
324
|
+
deps: options.deps,
|
|
325
|
+
breakpoint: breakpoints.find((bp) => bp.id === worst.breakpoint) ?? breakpoints[0],
|
|
326
|
+
screenshotsDir,
|
|
327
|
+
sourceUrl,
|
|
328
|
+
cloneUrl,
|
|
329
|
+
macro,
|
|
330
|
+
waitMs: settleWait,
|
|
331
|
+
suffix: `iter-${bestIteration || iterationsRun}-settled`,
|
|
332
|
+
iteration: bestIteration || iterationsRun
|
|
333
|
+
});
|
|
334
|
+
worst.settledMismatch = settled.mismatchPercentage;
|
|
335
|
+
worst.settledDiffPath = settled.diffPath;
|
|
336
|
+
const settlingLog = await recordSettlingLog({
|
|
337
|
+
slug: entry.slug,
|
|
338
|
+
correctionDir,
|
|
339
|
+
baselineError: worst.mismatchPercentage,
|
|
340
|
+
stabilizedError: settled.mismatchPercentage,
|
|
341
|
+
waitMs: settleWait,
|
|
342
|
+
breakpoint: settled.breakpoint,
|
|
343
|
+
iteration: bestIteration || iterationsRun
|
|
344
|
+
});
|
|
345
|
+
await writeFile(settlingLog.path, JSON.stringify(settlingLog.payload, null, 2), 'utf8');
|
|
346
|
+
settlingLogPath = settlingLog.path;
|
|
347
|
+
settlingWaitMs = settleWait;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
finally {
|
|
351
|
+
await browser.close();
|
|
352
|
+
}
|
|
353
|
+
const baselineMismatch = iterationHistory.length > 0
|
|
354
|
+
? iterationHistory[0].worstMismatch
|
|
355
|
+
: (results[0]?.mismatchPercentage ?? 0);
|
|
356
|
+
return await assembleSelfCorrectionResult({
|
|
357
|
+
correctionDir,
|
|
358
|
+
results,
|
|
359
|
+
threshold,
|
|
360
|
+
sourceUrl,
|
|
361
|
+
cloneUrl,
|
|
362
|
+
slug: entry.slug,
|
|
363
|
+
settlingLogPath,
|
|
364
|
+
settlingWaitMs,
|
|
365
|
+
iterationsRun,
|
|
366
|
+
finalIteration: bestIteration || iterationsRun || 1,
|
|
367
|
+
history: iterationHistory,
|
|
368
|
+
baselineError: baselineMismatch
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async function captureBreakpointDiff(options) {
|
|
372
|
+
const label = options.suffix ? `${options.breakpoint.id}-${options.suffix}` : options.breakpoint.id;
|
|
373
|
+
const referencePath = join(options.screenshotsDir, `reference-${label}.png`);
|
|
374
|
+
const clonePath = join(options.screenshotsDir, `clone-${label}.png`);
|
|
375
|
+
const diffPath = join(options.screenshotsDir, `diff-${label}.png`);
|
|
376
|
+
const viewport = normalizeViewportConfig(options.breakpoint);
|
|
377
|
+
const referenceShot = await captureScreenshot({
|
|
378
|
+
browser: options.browser,
|
|
379
|
+
deps: options.deps,
|
|
380
|
+
targetUrl: options.sourceUrl,
|
|
381
|
+
outputPath: referencePath,
|
|
382
|
+
viewport,
|
|
383
|
+
macro: options.macro,
|
|
384
|
+
waitMs: options.waitMs,
|
|
385
|
+
blockNetwork: false
|
|
386
|
+
});
|
|
387
|
+
const cloneShot = await captureScreenshot({
|
|
388
|
+
browser: options.browser,
|
|
389
|
+
deps: options.deps,
|
|
390
|
+
targetUrl: options.cloneUrl,
|
|
391
|
+
outputPath: clonePath,
|
|
392
|
+
viewport,
|
|
393
|
+
macro: options.macro,
|
|
394
|
+
waitMs: options.waitMs,
|
|
395
|
+
blockNetwork: true
|
|
396
|
+
});
|
|
397
|
+
const mismatch = await computeMismatch(referenceShot.image, cloneShot.image, diffPath, options.deps);
|
|
398
|
+
return {
|
|
399
|
+
breakpoint: options.breakpoint.id,
|
|
400
|
+
width: mismatch.width,
|
|
401
|
+
height: mismatch.height,
|
|
402
|
+
referencePath,
|
|
403
|
+
clonePath,
|
|
404
|
+
diffPath,
|
|
405
|
+
mismatchPercentage: mismatch.percent,
|
|
406
|
+
clipped: mismatch.clipped,
|
|
407
|
+
referenceDimensions: mismatch.referenceDimensions,
|
|
408
|
+
cloneDimensions: mismatch.cloneDimensions,
|
|
409
|
+
iteration: options.iteration
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
async function captureScreenshot(options) {
|
|
413
|
+
const context = await options.browser.newContext({
|
|
414
|
+
viewport: { width: options.viewport.width, height: options.viewport.height },
|
|
415
|
+
deviceScaleFactor: options.viewport.deviceScaleFactor,
|
|
416
|
+
userAgent: TOOLKIT_USER_AGENT
|
|
417
|
+
});
|
|
418
|
+
const enforceOffline = Boolean(options.blockNetwork && options.targetUrl.startsWith('file:'));
|
|
419
|
+
if (enforceOffline) {
|
|
420
|
+
await context.route('**/*', (route) => {
|
|
421
|
+
const requestUrl = route.request().url();
|
|
422
|
+
if (requestUrl.startsWith('file:') || requestUrl.startsWith('data:')) {
|
|
423
|
+
return route.continue();
|
|
424
|
+
}
|
|
425
|
+
return route.abort();
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
if (options.macro?.contextScript) {
|
|
429
|
+
await context.addInitScript({ content: options.macro.contextScript });
|
|
430
|
+
}
|
|
431
|
+
const page = await context.newPage();
|
|
432
|
+
await page.goto(options.targetUrl, { waitUntil: 'networkidle', timeout: 120_000 });
|
|
433
|
+
if (options.macro?.script) {
|
|
434
|
+
await page.addScriptTag({ content: options.macro.script }).catch(() => { });
|
|
435
|
+
}
|
|
436
|
+
const waitMs = options.waitMs ?? 800;
|
|
437
|
+
if (waitMs > 0) {
|
|
438
|
+
await page.waitForTimeout(waitMs);
|
|
439
|
+
}
|
|
440
|
+
await runDefaultInteractions(page);
|
|
441
|
+
const buffer = await page.screenshot({ fullPage: true, path: options.outputPath });
|
|
442
|
+
await context.close();
|
|
443
|
+
return { image: options.deps.png.sync.read(buffer) };
|
|
444
|
+
}
|
|
445
|
+
function normalizeViewportConfig(breakpoint) {
|
|
446
|
+
return {
|
|
447
|
+
width: breakpoint.width,
|
|
448
|
+
height: breakpoint.height,
|
|
449
|
+
deviceScaleFactor: breakpoint.deviceScaleFactor
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
async function computeMismatch(reference, candidate, diffPath, deps) {
|
|
453
|
+
const { png, pixelmatch } = deps;
|
|
454
|
+
const width = Math.max(reference.width, candidate.width);
|
|
455
|
+
const height = Math.max(reference.height, candidate.height);
|
|
456
|
+
if (width === 0 || height === 0) {
|
|
457
|
+
const placeholder = new png({ width: 1, height: 1 });
|
|
458
|
+
await writeFile(diffPath, png.sync.write(placeholder));
|
|
459
|
+
return {
|
|
460
|
+
percent: 100,
|
|
461
|
+
width: 1,
|
|
462
|
+
height: 1,
|
|
463
|
+
clipped: true,
|
|
464
|
+
referenceDimensions: { width: reference.width, height: reference.height },
|
|
465
|
+
cloneDimensions: { width: candidate.width, height: candidate.height }
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const normalizedReference = normalizePngDimensions(png, reference, width, height);
|
|
469
|
+
const normalizedCandidate = normalizePngDimensions(png, candidate, width, height);
|
|
470
|
+
const diff = new png({ width, height });
|
|
471
|
+
const mismatchedPixels = pixelmatch(normalizedReference.data, normalizedCandidate.data, diff.data, width, height, { threshold: 0.1 });
|
|
472
|
+
const percent = (mismatchedPixels / (width * height)) * 100;
|
|
473
|
+
await writeFile(diffPath, png.sync.write(diff));
|
|
474
|
+
return {
|
|
475
|
+
percent: roundPercent(percent),
|
|
476
|
+
width,
|
|
477
|
+
height,
|
|
478
|
+
clipped: reference.width !== candidate.width || reference.height !== candidate.height,
|
|
479
|
+
referenceDimensions: { width: reference.width, height: reference.height },
|
|
480
|
+
cloneDimensions: { width: candidate.width, height: candidate.height }
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function normalizePngDimensions(png, image, targetWidth, targetHeight) {
|
|
484
|
+
if (image.width === targetWidth && image.height === targetHeight) {
|
|
485
|
+
return image;
|
|
486
|
+
}
|
|
487
|
+
const output = new png({ width: targetWidth, height: targetHeight });
|
|
488
|
+
for (let i = 0; i < output.data.length; i += 4) {
|
|
489
|
+
output.data[i] = MISMATCH_PADDING_COLOR.r;
|
|
490
|
+
output.data[i + 1] = MISMATCH_PADDING_COLOR.g;
|
|
491
|
+
output.data[i + 2] = MISMATCH_PADDING_COLOR.b;
|
|
492
|
+
output.data[i + 3] = MISMATCH_PADDING_COLOR.a;
|
|
493
|
+
}
|
|
494
|
+
const copyWidth = Math.min(image.width, targetWidth);
|
|
495
|
+
const copyHeight = Math.min(image.height, targetHeight);
|
|
496
|
+
for (let y = 0; y < copyHeight; y += 1) {
|
|
497
|
+
const sourceStart = y * image.width * 4;
|
|
498
|
+
const targetStart = y * targetWidth * 4;
|
|
499
|
+
image.data.copy(output.data, targetStart, sourceStart, sourceStart + copyWidth * 4);
|
|
500
|
+
}
|
|
501
|
+
return output;
|
|
502
|
+
}
|
|
503
|
+
function roundPercent(value) {
|
|
504
|
+
return Number.isFinite(value) ? Number(value.toFixed(3)) : 0;
|
|
505
|
+
}
|
|
506
|
+
async function loadInteractionMacroForCapture(entry, repoRoot) {
|
|
507
|
+
if (!entry.interactionScriptPath) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const absolute = join(repoRoot, entry.interactionScriptPath);
|
|
512
|
+
const script = await readFile(absolute, 'utf8');
|
|
513
|
+
return {
|
|
514
|
+
script: script.trim(),
|
|
515
|
+
contextScript: buildMacroContextScript(entry)
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
console.warn(`[design-toolkit-reference] Failed to load interaction macro for ${entry.slug}:`, error);
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
function buildMacroContextScript(entry) {
|
|
524
|
+
const payload = {
|
|
525
|
+
slug: entry.slug,
|
|
526
|
+
url: entry.url,
|
|
527
|
+
waitMs: entry.interactionWaitMs ?? null,
|
|
528
|
+
runtimeCanvasColors: entry.runtimeCanvasColors ?? [],
|
|
529
|
+
resolvedFonts: entry.resolvedFonts ?? []
|
|
530
|
+
};
|
|
531
|
+
return `(function(){window.macroContext=Object.assign({},window.macroContext||{},${JSON.stringify(payload)});})();`;
|
|
532
|
+
}
|
|
533
|
+
function resolveBreakpointTargets(entry, pipeline) {
|
|
534
|
+
const map = new Map(pipeline.map((bp) => [bp.id, bp]));
|
|
535
|
+
const resolved = [];
|
|
536
|
+
for (const id of entry.breakpoints) {
|
|
537
|
+
const match = map.get(id);
|
|
538
|
+
if (match) {
|
|
539
|
+
resolved.push(match);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (resolved.length > 0) {
|
|
543
|
+
return resolved;
|
|
544
|
+
}
|
|
545
|
+
if (pipeline.length > 0) {
|
|
546
|
+
return [pipeline[0]];
|
|
547
|
+
}
|
|
548
|
+
return [{ id: 'desktop', width: 1440, height: 900 }];
|
|
549
|
+
}
|
|
550
|
+
function resolveCloneUrl(entry, repoRoot, referenceArtifactPath) {
|
|
551
|
+
const absoluteReference = referenceArtifactPath
|
|
552
|
+
? isAbsolute(referenceArtifactPath)
|
|
553
|
+
? referenceArtifactPath
|
|
554
|
+
: join(repoRoot, referenceArtifactPath)
|
|
555
|
+
: entry.snapshotHtmlPath
|
|
556
|
+
? isAbsolute(entry.snapshotHtmlPath)
|
|
557
|
+
? entry.snapshotHtmlPath
|
|
558
|
+
: join(repoRoot, entry.snapshotHtmlPath)
|
|
559
|
+
: null;
|
|
560
|
+
if (absoluteReference) {
|
|
561
|
+
return pathToFileURL(absoluteReference).toString();
|
|
562
|
+
}
|
|
563
|
+
return entry.referenceUrl ?? entry.url;
|
|
564
|
+
}
|
|
565
|
+
async function recordSettlingLog(options) {
|
|
566
|
+
const waitMs = Math.max(0, options.waitMs);
|
|
567
|
+
const path = join(options.correctionDir, 'counter-settling.json');
|
|
568
|
+
const payload = {
|
|
569
|
+
slug: options.slug,
|
|
570
|
+
breakpoint: options.breakpoint,
|
|
571
|
+
wait_ms: waitMs,
|
|
572
|
+
baseline_error: roundPercent(options.baselineError),
|
|
573
|
+
stabilized_error: roundPercent(options.stabilizedError),
|
|
574
|
+
recorded_at: new Date().toISOString(),
|
|
575
|
+
iteration: options.iteration ?? 1
|
|
576
|
+
};
|
|
577
|
+
return { path, payload };
|
|
578
|
+
}
|
|
579
|
+
async function assembleSelfCorrectionResult(options) {
|
|
580
|
+
const { correctionDir, results, threshold, sourceUrl, cloneUrl, slug, settlingLogPath, settlingWaitMs, iterationsRun, finalIteration, history, baselineError } = options;
|
|
581
|
+
const reportPath = join(correctionDir, 'self-correction.json');
|
|
582
|
+
const finalValues = results.map((result) => result.settledMismatch ?? result.mismatchPercentage);
|
|
583
|
+
const finalErrorRate = finalValues.length > 0 ? roundPercent(Math.max(...finalValues)) : 0;
|
|
584
|
+
const averageErrorRate = finalValues.length > 0 ? roundPercent(finalValues.reduce((sum, value) => sum + value, 0) / finalValues.length) : 0;
|
|
585
|
+
const fallbackBaseline = results.length > 0 ? roundPercent(Math.max(...results.map((result) => result.mismatchPercentage))) : 0;
|
|
586
|
+
const normalizedBaseline = baselineError > 0 ? roundPercent(baselineError) : fallbackBaseline;
|
|
587
|
+
const iterations = iterationsRun > 0 ? iterationsRun : results[0]?.iteration ?? 0;
|
|
588
|
+
const payload = {
|
|
589
|
+
slug,
|
|
590
|
+
reference: sourceUrl,
|
|
591
|
+
clone: cloneUrl,
|
|
592
|
+
threshold,
|
|
593
|
+
iterations,
|
|
594
|
+
final_iteration: finalIteration,
|
|
595
|
+
iteration_history: history,
|
|
596
|
+
threshold_passed: threshold > 0 ? finalErrorRate <= threshold : null,
|
|
597
|
+
average_error_rate: averageErrorRate,
|
|
598
|
+
final_error_rate: finalErrorRate,
|
|
599
|
+
baseline_error_rate: normalizedBaseline,
|
|
600
|
+
breakpoints: results.map((result) => ({
|
|
601
|
+
id: result.breakpoint,
|
|
602
|
+
width: result.width,
|
|
603
|
+
height: result.height,
|
|
604
|
+
mismatch_percent: result.mismatchPercentage,
|
|
605
|
+
settled_mismatch_percent: result.settledMismatch ?? null,
|
|
606
|
+
reference_dimensions: result.referenceDimensions,
|
|
607
|
+
clone_dimensions: result.cloneDimensions,
|
|
608
|
+
clipped: Boolean(result.clipped),
|
|
609
|
+
iteration: result.iteration ?? null,
|
|
610
|
+
reference_image: result.referencePath,
|
|
611
|
+
clone_image: result.clonePath,
|
|
612
|
+
diff_image: result.diffPath,
|
|
613
|
+
settled_diff_image: result.settledDiffPath ?? null
|
|
614
|
+
})),
|
|
615
|
+
recorded_at: new Date().toISOString(),
|
|
616
|
+
settling_log: settlingLogPath ?? null
|
|
617
|
+
};
|
|
618
|
+
await writeFile(reportPath, JSON.stringify(payload, null, 2), 'utf8');
|
|
619
|
+
return {
|
|
620
|
+
path: reportPath,
|
|
621
|
+
iterations,
|
|
622
|
+
finalErrorRate,
|
|
623
|
+
settlingLogPath,
|
|
624
|
+
settlingWaitMs,
|
|
625
|
+
baselineError: normalizedBaseline,
|
|
626
|
+
history,
|
|
627
|
+
finalIteration,
|
|
628
|
+
assets: buildSelfCorrectionAssets(results)
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function buildSelfCorrectionAssets(results) {
|
|
632
|
+
const assets = [];
|
|
633
|
+
for (const result of results) {
|
|
634
|
+
assets.push({
|
|
635
|
+
path: result.referencePath,
|
|
636
|
+
description: `Reference screenshot (${result.breakpoint})`,
|
|
637
|
+
role: 'reference',
|
|
638
|
+
breakpoint: result.breakpoint
|
|
639
|
+
});
|
|
640
|
+
assets.push({
|
|
641
|
+
path: result.clonePath,
|
|
642
|
+
description: `Clone screenshot (${result.breakpoint})`,
|
|
643
|
+
role: 'clone',
|
|
644
|
+
breakpoint: result.breakpoint
|
|
645
|
+
});
|
|
646
|
+
assets.push({
|
|
647
|
+
path: result.diffPath,
|
|
648
|
+
description: `Diff heatmap (${result.breakpoint})`,
|
|
649
|
+
role: 'diff',
|
|
650
|
+
breakpoint: result.breakpoint
|
|
651
|
+
});
|
|
652
|
+
if (result.settledDiffPath) {
|
|
653
|
+
assets.push({
|
|
654
|
+
path: result.settledDiffPath,
|
|
655
|
+
description: `Settled diff (${result.breakpoint})`,
|
|
656
|
+
role: 'settled-diff',
|
|
657
|
+
breakpoint: result.breakpoint
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return assets;
|
|
662
|
+
}
|
|
663
|
+
function metadataApprover(context) {
|
|
664
|
+
return context.config.config.metadata.design.privacy.approver ?? 'design-reviewer';
|
|
665
|
+
}
|
|
666
|
+
async function loadSections(entry, repoRoot) {
|
|
667
|
+
if (!entry.sectionsPath) {
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
const absolute = join(repoRoot, entry.sectionsPath);
|
|
672
|
+
const raw = await readFile(absolute, 'utf8');
|
|
673
|
+
const parsed = JSON.parse(raw);
|
|
674
|
+
return parsed
|
|
675
|
+
.map((section) => ({
|
|
676
|
+
title: normalizeSentenceSpacing(section.title ?? 'Section')
|
|
677
|
+
.replace(/\s+/g, ' ')
|
|
678
|
+
.trim() || 'Section',
|
|
679
|
+
description: normalizeSentenceSpacing(section.description ?? '')
|
|
680
|
+
.replace(/\s+/g, ' ')
|
|
681
|
+
.trim()
|
|
682
|
+
}))
|
|
683
|
+
.filter((section) => section.description.length > 0);
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
console.warn(`[design-toolkit-reference] Failed to read sections for ${entry.slug}:`, error);
|
|
687
|
+
}
|
|
688
|
+
return [];
|
|
689
|
+
}
|
|
690
|
+
async function buildReferenceHtml(entry, repoRoot, sections) {
|
|
691
|
+
if (!entry.snapshotHtmlPath) {
|
|
692
|
+
return fallbackReference(entry.slug, entry.url);
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
const absolute = join(repoRoot, entry.snapshotHtmlPath);
|
|
696
|
+
const snapshotHtml = await readFile(absolute, 'utf8');
|
|
697
|
+
const overlayEnabled = process.env.HI_FI_TOOLKIT_DEBUG_OVERLAY === '1';
|
|
698
|
+
const scrollUnlockEnabled = process.env.HI_FI_TOOLKIT_SCROLL_UNLOCK === '1';
|
|
699
|
+
const overlay = overlayEnabled ? buildOverlay(entry.url, sections) : '';
|
|
700
|
+
const styleBlock = overlayEnabled ? buildOverlayStyles() : '';
|
|
701
|
+
const scrollScript = scrollUnlockEnabled ? buildScrollFallbackScript() : '';
|
|
702
|
+
const interactionScript = await buildInteractionMacro(entry, repoRoot);
|
|
703
|
+
const withStyles = overlay ? injectIntoHead(snapshotHtml, styleBlock) : snapshotHtml;
|
|
704
|
+
const macroBundle = [overlay, scrollScript, interactionScript].filter(Boolean).join('\n');
|
|
705
|
+
return macroBundle ? injectAfterBodyOpen(withStyles, macroBundle) : withStyles;
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
console.warn(`[design-toolkit-reference] Failed to read snapshot for ${entry.slug}:`, error);
|
|
709
|
+
return fallbackReference(entry.slug, entry.url);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function fallbackReference(slug, url) {
|
|
713
|
+
return `<!doctype html>\n<html lang="en">\n<head>\n <meta charset="utf-8"/>\n <title>${slug} Reference</title>\n <style>body{font-family:system-ui;padding:2rem;}section{margin-bottom:2rem;}</style>\n</head>\n<body>\n <section aria-label="fallback"><h2>Snapshot unavailable</h2><p>Original capture for ${escapeHtml(url)} missing; showing placeholder.</p></section>\n</body>\n</html>`;
|
|
714
|
+
}
|
|
715
|
+
function buildOverlay(sourceUrl, sections) {
|
|
716
|
+
const list = sections.length > 0
|
|
717
|
+
? `<ol>${sections
|
|
718
|
+
.map((section) => `<li><strong>${escapeHtml(section.title)}</strong><p>${escapeHtml(section.description)}</p></li>`)
|
|
719
|
+
.join('')}</ol>`
|
|
720
|
+
: '<p>No sections detected.</p>';
|
|
721
|
+
return `<div class="codex-clone-overlay"><div class="codex-clone-banner"><strong>Hi-Fi Toolkit Clone</strong><p>Source: <a href="${escapeHtml(sourceUrl)}" target="_blank" rel="noreferrer">${escapeHtml(sourceUrl)}</a></p></div><div class="codex-section-outline"><h2>Captured Sections</h2>${list}</div></div>`;
|
|
722
|
+
}
|
|
723
|
+
function buildOverlayStyles() {
|
|
724
|
+
return `<style id="codex-clone-style">\n .codex-clone-overlay { position: fixed; top: 1rem; right: 1rem; width: 320px; max-height: 90vh; overflow-y: auto; z-index: 9999; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #f2f2f2; }\n .codex-clone-banner { background: rgba(7, 7, 12, 0.95); padding: 1rem; border-radius: 0.75rem 0.75rem 0 0; box-shadow: 0 8px 20px rgba(0,0,0,0.4); }\n .codex-clone-banner a { color: #7dd3ff; text-decoration: none; }\n .codex-section-outline { background: rgba(12, 12, 18, 0.92); padding: 0.75rem 1rem 1rem; border-radius: 0 0 0.75rem 0.75rem; font-size: 0.85rem; line-height: 1.4; }\n .codex-section-outline ol { margin: 0; padding-left: 1.25rem; }\n .codex-section-outline li { margin-bottom: 0.75rem; }\n .codex-section-outline p { margin: 0.25rem 0 0; color: #cbd5f5; }\n @media (max-width: 900px) { .codex-clone-overlay { position: static; width: auto; max-height: none; } }\n</style>`;
|
|
725
|
+
}
|
|
726
|
+
function buildScrollFallbackScript() {
|
|
727
|
+
return `<script id="codex-scroll-fallback">(function(){\n const html = document.documentElement;\n const body = document.body;\n if (!html || !body) { return; }\n const MAX_ATTEMPTS = 8;\n let attempts = 0;\n\n function isLocked() {\n if (!body) { return false; }\n if (body.hasAttribute('data-lenis-prevent')) { return true; }\n const bodyOverflow = window.getComputedStyle(body).overflowY;\n const htmlOverflow = window.getComputedStyle(html).overflowY;\n return bodyOverflow === 'hidden' || htmlOverflow === 'hidden';\n }\n\n function unlock() {\n body.removeAttribute('data-lenis-prevent');\n body.style.overflowY = 'auto';\n body.style.removeProperty('height');\n body.style.removeProperty('min-height');\n html.style.overflowY = 'auto';\n html.style.removeProperty('--vh-in-px');\n }\n\n function tryUnlock() {\n if (!isLocked()) { return; }\n attempts += 1;\n unlock();\n if (attempts < MAX_ATTEMPTS) {\n setTimeout(tryUnlock, 500);\n }\n }\n\n window.addEventListener('load', () => {\n setTimeout(tryUnlock, 1200);\n });\n document.addEventListener('visibilitychange', () => {\n if (document.visibilityState === 'visible') {\n setTimeout(tryUnlock, 600);\n }\n });\n})();</script>`;
|
|
728
|
+
}
|
|
729
|
+
function injectIntoHead(html, snippet) {
|
|
730
|
+
if (html.includes('</head>')) {
|
|
731
|
+
return html.replace('</head>', `${snippet}\n</head>`);
|
|
732
|
+
}
|
|
733
|
+
return `${snippet}\n${html}`;
|
|
734
|
+
}
|
|
735
|
+
function injectAfterBodyOpen(html, snippet) {
|
|
736
|
+
const match = html.match(/<body[^>]*>/i);
|
|
737
|
+
if (match) {
|
|
738
|
+
return html.replace(match[0], `${match[0]}\n${snippet}\n`);
|
|
739
|
+
}
|
|
740
|
+
return `${snippet}\n${html}`;
|
|
741
|
+
}
|
|
742
|
+
function escapeHtml(value) {
|
|
743
|
+
return value
|
|
744
|
+
.replace(/&/g, '&')
|
|
745
|
+
.replace(/</g, '<')
|
|
746
|
+
.replace(/>/g, '>')
|
|
747
|
+
.replace(/"/g, '"')
|
|
748
|
+
.replace(/'/g, ''');
|
|
749
|
+
}
|
|
750
|
+
async function mirrorReferenceAssets(options) {
|
|
751
|
+
const { entry, repoRoot, referenceArtifactPath } = options;
|
|
752
|
+
const contextDir = join(repoRoot, entry.relativeDir);
|
|
753
|
+
if (!(await directoryExists(contextDir))) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const referenceDir = dirname(join(repoRoot, referenceArtifactPath));
|
|
757
|
+
await mkdir(referenceDir, { recursive: true });
|
|
758
|
+
const entries = await readdir(contextDir, { withFileTypes: true });
|
|
759
|
+
let copiedAssets = false;
|
|
760
|
+
let copiedVideo = false;
|
|
761
|
+
for (const entryDir of entries) {
|
|
762
|
+
if (!entryDir.isDirectory()) {
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
const sourcePath = join(contextDir, entryDir.name);
|
|
766
|
+
const destinationPath = join(referenceDir, entryDir.name);
|
|
767
|
+
await cp(sourcePath, destinationPath, { recursive: true, force: true });
|
|
768
|
+
if (entryDir.name === 'assets') {
|
|
769
|
+
copiedAssets = true;
|
|
770
|
+
await mirrorTopLevelShortcuts(destinationPath, referenceDir);
|
|
771
|
+
}
|
|
772
|
+
if (entryDir.name === 'video') {
|
|
773
|
+
copiedVideo = true;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (!copiedAssets) {
|
|
777
|
+
const assetsPath = join(contextDir, 'assets');
|
|
778
|
+
if (await directoryExists(assetsPath)) {
|
|
779
|
+
await mkdir(join(referenceDir, 'assets'), { recursive: true });
|
|
780
|
+
await cp(assetsPath, join(referenceDir, 'assets'), { recursive: true, force: true });
|
|
781
|
+
await mirrorTopLevelShortcuts(join(referenceDir, 'assets'), referenceDir);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (!copiedVideo) {
|
|
785
|
+
const videoPath = join(contextDir, 'video');
|
|
786
|
+
if (await directoryExists(videoPath)) {
|
|
787
|
+
await cp(videoPath, join(referenceDir, 'video'), { recursive: true, force: true });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async function buildInteractionMacro(entry, repoRoot) {
|
|
792
|
+
if (!entry.interactionScriptPath) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
const absolute = join(repoRoot, entry.interactionScriptPath);
|
|
797
|
+
const script = await readFile(absolute, 'utf8');
|
|
798
|
+
const contextScript = `<script id="codex-interaction-context">${buildMacroContextScript(entry)}</script>`;
|
|
799
|
+
return `${contextScript}\n<script id="codex-interaction-macro">${script.trim()}\n</script>`;
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
console.warn(`[design-toolkit-reference] Failed to load interaction macro for ${entry.slug}:`, error);
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async function directoryExists(path) {
|
|
807
|
+
try {
|
|
808
|
+
await access(path);
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
if (error.code === 'ENOENT') {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
async function mirrorTopLevelShortcuts(sourceAssetsDir, referenceDir) {
|
|
819
|
+
const entries = await readdir(sourceAssetsDir, { withFileTypes: true });
|
|
820
|
+
for (const entry of entries) {
|
|
821
|
+
if (!entry.isDirectory()) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const shortcutName = entry.name;
|
|
825
|
+
// Preserve "assets" prefix; only mirror top-level roots (e.g., wp-content, wp-includes).
|
|
826
|
+
if (!/^wp-/.test(shortcutName)) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const shortcutPath = join(referenceDir, shortcutName);
|
|
830
|
+
try {
|
|
831
|
+
await symlink(join(sourceAssetsDir, shortcutName), shortcutPath);
|
|
832
|
+
}
|
|
833
|
+
catch (error) {
|
|
834
|
+
const nodeError = error;
|
|
835
|
+
if (nodeError.code === 'EEXIST') {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
main().catch((error) => {
|
|
843
|
+
console.error('[design-toolkit-reference] failed to build references');
|
|
844
|
+
console.error(error instanceof Error ? error.stack ?? error.message : error);
|
|
845
|
+
process.exitCode = 1;
|
|
846
|
+
});
|