@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.
Files changed (150) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +238 -0
  3. package/dist/bin/codex-orchestrator.js +507 -0
  4. package/dist/orchestrator/src/agents/builder.js +16 -0
  5. package/dist/orchestrator/src/agents/index.js +4 -0
  6. package/dist/orchestrator/src/agents/planner.js +17 -0
  7. package/dist/orchestrator/src/agents/reviewer.js +13 -0
  8. package/dist/orchestrator/src/agents/tester.js +13 -0
  9. package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +20 -0
  10. package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +164 -0
  11. package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +32 -0
  12. package/dist/orchestrator/src/cli/adapters/CommandTester.js +33 -0
  13. package/dist/orchestrator/src/cli/adapters/index.js +4 -0
  14. package/dist/orchestrator/src/cli/config/userConfig.js +28 -0
  15. package/dist/orchestrator/src/cli/doctor.js +48 -0
  16. package/dist/orchestrator/src/cli/events/runEvents.js +84 -0
  17. package/dist/orchestrator/src/cli/exec/command.js +56 -0
  18. package/dist/orchestrator/src/cli/exec/context.js +108 -0
  19. package/dist/orchestrator/src/cli/exec/experience.js +77 -0
  20. package/dist/orchestrator/src/cli/exec/finalization.js +140 -0
  21. package/dist/orchestrator/src/cli/exec/learning.js +62 -0
  22. package/dist/orchestrator/src/cli/exec/stageRunner.js +71 -0
  23. package/dist/orchestrator/src/cli/exec/summary.js +109 -0
  24. package/dist/orchestrator/src/cli/exec/telemetry.js +18 -0
  25. package/dist/orchestrator/src/cli/exec/tfgrpo.js +200 -0
  26. package/dist/orchestrator/src/cli/exec/tfgrpoArtifacts.js +19 -0
  27. package/dist/orchestrator/src/cli/exec/types.js +1 -0
  28. package/dist/orchestrator/src/cli/init.js +64 -0
  29. package/dist/orchestrator/src/cli/mcp.js +124 -0
  30. package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +404 -0
  31. package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +138 -0
  32. package/dist/orchestrator/src/cli/orchestrator.js +554 -0
  33. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +32 -0
  34. package/dist/orchestrator/src/cli/pipelines/designReference.js +72 -0
  35. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +71 -0
  36. package/dist/orchestrator/src/cli/pipelines/index.js +34 -0
  37. package/dist/orchestrator/src/cli/run/environment.js +24 -0
  38. package/dist/orchestrator/src/cli/run/manifest.js +367 -0
  39. package/dist/orchestrator/src/cli/run/manifestPersister.js +88 -0
  40. package/dist/orchestrator/src/cli/run/runPaths.js +30 -0
  41. package/dist/orchestrator/src/cli/selfCheck.js +12 -0
  42. package/dist/orchestrator/src/cli/services/commandRunner.js +420 -0
  43. package/dist/orchestrator/src/cli/services/controlPlaneService.js +107 -0
  44. package/dist/orchestrator/src/cli/services/execRuntime.js +69 -0
  45. package/dist/orchestrator/src/cli/services/pipelineResolver.js +47 -0
  46. package/dist/orchestrator/src/cli/services/runPreparation.js +82 -0
  47. package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
  48. package/dist/orchestrator/src/cli/services/schedulerService.js +42 -0
  49. package/dist/orchestrator/src/cli/tasks/taskMetadata.js +19 -0
  50. package/dist/orchestrator/src/cli/telemetry/schema.js +8 -0
  51. package/dist/orchestrator/src/cli/types.js +1 -0
  52. package/dist/orchestrator/src/cli/ui/HudApp.js +112 -0
  53. package/dist/orchestrator/src/cli/ui/controller.js +26 -0
  54. package/dist/orchestrator/src/cli/ui/store.js +240 -0
  55. package/dist/orchestrator/src/cli/utils/enforcementMode.js +12 -0
  56. package/dist/orchestrator/src/cli/utils/fs.js +8 -0
  57. package/dist/orchestrator/src/cli/utils/interactive.js +25 -0
  58. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +10 -0
  59. package/dist/orchestrator/src/cli/utils/optionalDeps.js +30 -0
  60. package/dist/orchestrator/src/cli/utils/packageInfo.js +25 -0
  61. package/dist/orchestrator/src/cli/utils/planFormatter.js +49 -0
  62. package/dist/orchestrator/src/cli/utils/runId.js +7 -0
  63. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +26 -0
  64. package/dist/orchestrator/src/cli/utils/strings.js +8 -0
  65. package/dist/orchestrator/src/cli/utils/time.js +6 -0
  66. package/dist/orchestrator/src/control-plane/drift-reporter.js +109 -0
  67. package/dist/orchestrator/src/control-plane/index.js +3 -0
  68. package/dist/orchestrator/src/control-plane/request-builder.js +217 -0
  69. package/dist/orchestrator/src/control-plane/types.js +1 -0
  70. package/dist/orchestrator/src/control-plane/validator.js +50 -0
  71. package/dist/orchestrator/src/credentials/CredentialBroker.js +1 -0
  72. package/dist/orchestrator/src/events/EventBus.js +25 -0
  73. package/dist/orchestrator/src/learning/crystalizer.js +108 -0
  74. package/dist/orchestrator/src/learning/harvester.js +146 -0
  75. package/dist/orchestrator/src/learning/manifest.js +56 -0
  76. package/dist/orchestrator/src/learning/runner.js +177 -0
  77. package/dist/orchestrator/src/learning/validator.js +164 -0
  78. package/dist/orchestrator/src/logger.js +20 -0
  79. package/dist/orchestrator/src/manager.js +388 -0
  80. package/dist/orchestrator/src/persistence/ArtifactStager.js +95 -0
  81. package/dist/orchestrator/src/persistence/ExperienceStore.js +210 -0
  82. package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +65 -0
  83. package/dist/orchestrator/src/persistence/RunManifestWriter.js +23 -0
  84. package/dist/orchestrator/src/persistence/TaskStateStore.js +172 -0
  85. package/dist/orchestrator/src/persistence/identifierGuards.js +1 -0
  86. package/dist/orchestrator/src/persistence/lockFile.js +26 -0
  87. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +26 -0
  88. package/dist/orchestrator/src/persistence/sanitizeRunId.js +8 -0
  89. package/dist/orchestrator/src/persistence/sanitizeTaskId.js +8 -0
  90. package/dist/orchestrator/src/persistence/writeAtomicFile.js +4 -0
  91. package/dist/orchestrator/src/privacy/guard.js +111 -0
  92. package/dist/orchestrator/src/scheduler/index.js +1 -0
  93. package/dist/orchestrator/src/scheduler/plan.js +171 -0
  94. package/dist/orchestrator/src/scheduler/types.js +1 -0
  95. package/dist/orchestrator/src/sync/CloudRunsClient.js +1 -0
  96. package/dist/orchestrator/src/sync/CloudRunsHttpClient.js +82 -0
  97. package/dist/orchestrator/src/sync/CloudSyncWorker.js +206 -0
  98. package/dist/orchestrator/src/sync/createCloudSyncWorker.js +15 -0
  99. package/dist/orchestrator/src/types.js +1 -0
  100. package/dist/orchestrator/src/utils/atomicWrite.js +15 -0
  101. package/dist/orchestrator/src/utils/errorMessage.js +14 -0
  102. package/dist/orchestrator/src/utils/executionMode.js +69 -0
  103. package/dist/packages/control-plane-schemas/src/index.js +1 -0
  104. package/dist/packages/control-plane-schemas/src/run-request.js +548 -0
  105. package/dist/packages/orchestrator/src/exec/handle-service.js +203 -0
  106. package/dist/packages/orchestrator/src/exec/session-manager.js +147 -0
  107. package/dist/packages/orchestrator/src/exec/unified-exec.js +432 -0
  108. package/dist/packages/orchestrator/src/index.js +3 -0
  109. package/dist/packages/orchestrator/src/instructions/loader.js +101 -0
  110. package/dist/packages/orchestrator/src/instructions/promptPacks.js +151 -0
  111. package/dist/packages/orchestrator/src/notifications/index.js +74 -0
  112. package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +142 -0
  113. package/dist/packages/orchestrator/src/tool-orchestrator.js +161 -0
  114. package/dist/packages/sdk-node/src/orchestrator.js +195 -0
  115. package/dist/packages/shared/config/designConfig.js +495 -0
  116. package/dist/packages/shared/config/env.js +37 -0
  117. package/dist/packages/shared/config/index.js +2 -0
  118. package/dist/packages/shared/design-artifacts/writer.js +221 -0
  119. package/dist/packages/shared/events/serializer.js +84 -0
  120. package/dist/packages/shared/events/types.js +1 -0
  121. package/dist/packages/shared/manifest/artifactUtils.js +36 -0
  122. package/dist/packages/shared/manifest/designArtifacts.js +665 -0
  123. package/dist/packages/shared/manifest/fileIO.js +29 -0
  124. package/dist/packages/shared/manifest/toolRuns.js +78 -0
  125. package/dist/packages/shared/manifest/toolkitArtifacts.js +223 -0
  126. package/dist/packages/shared/manifest/types.js +5 -0
  127. package/dist/packages/shared/manifest/validator.js +73 -0
  128. package/dist/packages/shared/manifest/writer.js +2 -0
  129. package/dist/packages/shared/streams/stdio.js +112 -0
  130. package/dist/scripts/design/pipeline/advanced-assets.js +466 -0
  131. package/dist/scripts/design/pipeline/componentize.js +74 -0
  132. package/dist/scripts/design/pipeline/context.js +34 -0
  133. package/dist/scripts/design/pipeline/extract.js +249 -0
  134. package/dist/scripts/design/pipeline/optionalDeps.js +107 -0
  135. package/dist/scripts/design/pipeline/prepare.js +46 -0
  136. package/dist/scripts/design/pipeline/reference.js +94 -0
  137. package/dist/scripts/design/pipeline/state.js +206 -0
  138. package/dist/scripts/design/pipeline/toolkit/common.js +94 -0
  139. package/dist/scripts/design/pipeline/toolkit/extract.js +258 -0
  140. package/dist/scripts/design/pipeline/toolkit/publish.js +202 -0
  141. package/dist/scripts/design/pipeline/toolkit/publishActions.js +12 -0
  142. package/dist/scripts/design/pipeline/toolkit/reference.js +846 -0
  143. package/dist/scripts/design/pipeline/toolkit/snapshot.js +882 -0
  144. package/dist/scripts/design/pipeline/toolkit/tokens.js +456 -0
  145. package/dist/scripts/design/pipeline/visual-regression.js +137 -0
  146. package/dist/scripts/design/pipeline/write-artifacts.js +61 -0
  147. package/package.json +97 -0
  148. package/schemas/manifest.json +1064 -0
  149. package/templates/README.md +12 -0
  150. 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, '&amp;')
745
+ .replace(/</g, '&lt;')
746
+ .replace(/>/g, '&gt;')
747
+ .replace(/"/g, '&quot;')
748
+ .replace(/'/g, '&#39;');
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
+ });