@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,249 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { loadDesignContext } from './context.js';
|
|
5
|
+
import { appendArtifacts, loadDesignRunState, saveDesignRunState, upsertStage } from './state.js';
|
|
6
|
+
import { stageArtifacts } from '../../../orchestrator/src/persistence/ArtifactStager.js';
|
|
7
|
+
import { loadPlaywright } from './optionalDeps.js';
|
|
8
|
+
async function main() {
|
|
9
|
+
const context = await loadDesignContext();
|
|
10
|
+
const state = await loadDesignRunState(context.statePath);
|
|
11
|
+
const stageId = 'design-extract';
|
|
12
|
+
const metadata = context.config.config.metadata.design;
|
|
13
|
+
if (metadata.captureUrls.length === 0) {
|
|
14
|
+
upsertStage(state, {
|
|
15
|
+
id: stageId,
|
|
16
|
+
title: 'Playwright extractor',
|
|
17
|
+
status: 'skipped',
|
|
18
|
+
notes: ['No capture URLs configured.']
|
|
19
|
+
});
|
|
20
|
+
await saveDesignRunState(context.statePath, state);
|
|
21
|
+
console.log('[design-extract] skipped — no capture URLs configured');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const breakpoints = metadata.breakpoints.length > 0 ? metadata.breakpoints : defaultBreakpoints();
|
|
25
|
+
const records = [];
|
|
26
|
+
const stageSummaries = [];
|
|
27
|
+
const notes = [];
|
|
28
|
+
let successCount = 0;
|
|
29
|
+
const playwright = await loadPlaywright();
|
|
30
|
+
const browser = await playwright.chromium.launch({ headless: true });
|
|
31
|
+
const tmpRoot = join(tmpdir(), `design-extract-${Date.now()}`);
|
|
32
|
+
await mkdir(tmpRoot, { recursive: true });
|
|
33
|
+
try {
|
|
34
|
+
for (const url of metadata.captureUrls) {
|
|
35
|
+
for (const breakpoint of breakpoints) {
|
|
36
|
+
try {
|
|
37
|
+
const result = await captureWithPlaywright({
|
|
38
|
+
browser,
|
|
39
|
+
url,
|
|
40
|
+
breakpoint,
|
|
41
|
+
tmpRoot,
|
|
42
|
+
taskId: context.taskId,
|
|
43
|
+
runId: context.runId,
|
|
44
|
+
allowThirdParty: metadata.privacy.allowThirdParty,
|
|
45
|
+
maskSelectors: metadata.maskSelectors ?? []
|
|
46
|
+
});
|
|
47
|
+
records.push(...result.records);
|
|
48
|
+
stageSummaries.push(...result.stageSummaries);
|
|
49
|
+
successCount += 1;
|
|
50
|
+
console.log(`[design-extract] staged assets for ${url} @ ${breakpoint.id}`);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
notes.push(`${url} (${breakpoint.id}): ${message}`);
|
|
55
|
+
console.error(`[design-extract] capture failed for ${url} @ ${breakpoint.id}: ${message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
await browser.close();
|
|
62
|
+
}
|
|
63
|
+
appendArtifacts(state, records);
|
|
64
|
+
const stageStatus = successCount === 0 ? 'failed' : notes.length > 0 ? 'failed' : 'succeeded';
|
|
65
|
+
upsertStage(state, {
|
|
66
|
+
id: stageId,
|
|
67
|
+
title: 'Playwright extractor',
|
|
68
|
+
status: stageStatus,
|
|
69
|
+
notes: notes.length > 0 ? notes : undefined,
|
|
70
|
+
metrics: {
|
|
71
|
+
capture_count: successCount,
|
|
72
|
+
failure_count: notes.length,
|
|
73
|
+
breakpoint_count: breakpoints.length
|
|
74
|
+
},
|
|
75
|
+
artifacts: stageSummaries
|
|
76
|
+
});
|
|
77
|
+
await saveDesignRunState(context.statePath, state);
|
|
78
|
+
if (stageStatus === 'failed') {
|
|
79
|
+
throw new Error('Playwright extraction encountered failures. See stage notes for details.');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function captureWithPlaywright(options) {
|
|
83
|
+
const { browser, url, breakpoint, tmpRoot, taskId, runId, allowThirdParty, maskSelectors } = options;
|
|
84
|
+
const context = await browser.newContext({
|
|
85
|
+
viewport: { width: breakpoint.width, height: breakpoint.height },
|
|
86
|
+
deviceScaleFactor: breakpoint.deviceScaleFactor
|
|
87
|
+
});
|
|
88
|
+
const baseOrigin = new URL(url).origin;
|
|
89
|
+
if (!allowThirdParty) {
|
|
90
|
+
await context.route('**/*', async (route) => {
|
|
91
|
+
const requestUrl = route.request().url();
|
|
92
|
+
if (isSameOrigin(baseOrigin, requestUrl)) {
|
|
93
|
+
await route.continue();
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
await route.abort();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const page = await context.newPage();
|
|
101
|
+
try {
|
|
102
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: 60_000 });
|
|
103
|
+
if (maskSelectors.length > 0) {
|
|
104
|
+
await page.addStyleTag({ content: createMaskStyles(maskSelectors) });
|
|
105
|
+
}
|
|
106
|
+
const slug = slugify(`${url}-${breakpoint.id}`);
|
|
107
|
+
const captureDir = join(tmpRoot, slug);
|
|
108
|
+
await mkdir(captureDir, { recursive: true });
|
|
109
|
+
const domPath = join(captureDir, 'dom.html');
|
|
110
|
+
const cssPath = join(captureDir, 'styles.css');
|
|
111
|
+
const screenshotPath = join(captureDir, 'screenshot.png');
|
|
112
|
+
const metadataPath = join(captureDir, 'metadata.json');
|
|
113
|
+
const html = await page.content();
|
|
114
|
+
const css = await collectStyles(page);
|
|
115
|
+
const screenshot = await page.screenshot({ fullPage: true, type: 'png' });
|
|
116
|
+
const metadata = {
|
|
117
|
+
url,
|
|
118
|
+
breakpoint,
|
|
119
|
+
capturedAt: new Date().toISOString(),
|
|
120
|
+
assets: {
|
|
121
|
+
dom: 'dom.html',
|
|
122
|
+
css: 'styles.css',
|
|
123
|
+
screenshot: 'screenshot.png'
|
|
124
|
+
},
|
|
125
|
+
privacy: {
|
|
126
|
+
allowThirdParty,
|
|
127
|
+
maskSelectors
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
await Promise.all([
|
|
131
|
+
writeFile(domPath, html, 'utf8'),
|
|
132
|
+
writeFile(cssPath, css, 'utf8'),
|
|
133
|
+
writeFile(screenshotPath, screenshot),
|
|
134
|
+
writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf8')
|
|
135
|
+
]);
|
|
136
|
+
const relativeDir = `design/reference/playwright/${sanitizeSegment(breakpoint.id)}/${slug}`;
|
|
137
|
+
const staged = await stageArtifacts({
|
|
138
|
+
taskId,
|
|
139
|
+
runId,
|
|
140
|
+
artifacts: [
|
|
141
|
+
{ path: relative(process.cwd(), domPath), description: `DOM snapshot for ${url}` },
|
|
142
|
+
{ path: relative(process.cwd(), cssPath), description: `CSS snapshot for ${url}` },
|
|
143
|
+
{ path: relative(process.cwd(), screenshotPath), description: `Screenshot for ${url}` },
|
|
144
|
+
{ path: relative(process.cwd(), metadataPath), description: `Capture metadata for ${url}` }
|
|
145
|
+
],
|
|
146
|
+
options: {
|
|
147
|
+
relativeDir,
|
|
148
|
+
overwrite: true
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
const [domArtifact, cssArtifact, screenshotArtifact, metadataArtifact] = staged;
|
|
152
|
+
const records = [
|
|
153
|
+
buildDesignRecord('dom', domArtifact.path, url, breakpoint.id),
|
|
154
|
+
buildDesignRecord('css', cssArtifact.path, url, breakpoint.id),
|
|
155
|
+
buildDesignRecord('screenshot', screenshotArtifact.path, url, breakpoint.id),
|
|
156
|
+
{
|
|
157
|
+
stage: 'extract',
|
|
158
|
+
status: 'succeeded',
|
|
159
|
+
relative_path: metadataArtifact.path,
|
|
160
|
+
type: 'metadata',
|
|
161
|
+
description: `${url} (${breakpoint.id}) metadata`,
|
|
162
|
+
metadata
|
|
163
|
+
}
|
|
164
|
+
];
|
|
165
|
+
const stageSummaries = [
|
|
166
|
+
summaryEntry(domArtifact.path, 'DOM snapshot'),
|
|
167
|
+
summaryEntry(cssArtifact.path, 'CSS snapshot'),
|
|
168
|
+
summaryEntry(screenshotArtifact.path, 'Screenshot'),
|
|
169
|
+
summaryEntry(metadataArtifact.path, 'Metadata')
|
|
170
|
+
];
|
|
171
|
+
return { records, stageSummaries };
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await context.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function summaryEntry(path, description) {
|
|
178
|
+
return {
|
|
179
|
+
relative_path: path,
|
|
180
|
+
stage: 'extract',
|
|
181
|
+
status: 'succeeded',
|
|
182
|
+
description
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function buildDesignRecord(type, path, url, breakpointId) {
|
|
186
|
+
return {
|
|
187
|
+
stage: 'extract',
|
|
188
|
+
status: 'succeeded',
|
|
189
|
+
relative_path: path,
|
|
190
|
+
type,
|
|
191
|
+
description: `${url} (${breakpointId})`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function createMaskStyles(selectors) {
|
|
195
|
+
const rules = selectors
|
|
196
|
+
.map((selector) => `${selector} { filter: blur(10px) !important; }`)
|
|
197
|
+
.join('\n');
|
|
198
|
+
return `${rules}\n`;
|
|
199
|
+
}
|
|
200
|
+
function isSameOrigin(baseOrigin, requestUrl) {
|
|
201
|
+
try {
|
|
202
|
+
const requestOrigin = new URL(requestUrl).origin;
|
|
203
|
+
return requestOrigin === baseOrigin;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async function collectStyles(page) {
|
|
210
|
+
return (await page.evaluate(() => {
|
|
211
|
+
const outputs = [];
|
|
212
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
213
|
+
try {
|
|
214
|
+
const rules = sheet.cssRules ? Array.from(sheet.cssRules).map((rule) => rule.cssText).join('\n') : '';
|
|
215
|
+
outputs.push(rules);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
const href = sheet.href ?? 'inline';
|
|
219
|
+
outputs.push(`/* Unable to read stylesheet: ${href} (${String(error)}) */`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return outputs.join('\n\n');
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
function defaultBreakpoints() {
|
|
226
|
+
return [
|
|
227
|
+
{
|
|
228
|
+
id: 'default',
|
|
229
|
+
width: 1280,
|
|
230
|
+
height: 720
|
|
231
|
+
}
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
function slugify(value) {
|
|
235
|
+
return value
|
|
236
|
+
.toLowerCase()
|
|
237
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
238
|
+
.replace(/^-+|-+$/g, '')
|
|
239
|
+
.slice(0, 60) || 'capture';
|
|
240
|
+
}
|
|
241
|
+
function sanitizeSegment(value) {
|
|
242
|
+
const slug = slugify(value);
|
|
243
|
+
return slug.length > 0 ? slug : 'default';
|
|
244
|
+
}
|
|
245
|
+
main().catch((error) => {
|
|
246
|
+
console.error('[design-extract] failed to stage assets');
|
|
247
|
+
console.error(error instanceof Error ? error.stack ?? error.message : error);
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
const DESIGN_SETUP_HINT = 'Run "npm run setup:design-tools" and "npx playwright install" to enable design tooling.';
|
|
6
|
+
function isModuleNotFound(error) {
|
|
7
|
+
const candidate = error;
|
|
8
|
+
if (!candidate) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const message = candidate.message ?? '';
|
|
12
|
+
return (candidate.code === 'ERR_MODULE_NOT_FOUND' ||
|
|
13
|
+
candidate.code === 'MODULE_NOT_FOUND' ||
|
|
14
|
+
message.includes('Cannot find package') ||
|
|
15
|
+
message.includes('Cannot find module'));
|
|
16
|
+
}
|
|
17
|
+
function missingDependency(specifier) {
|
|
18
|
+
return new Error(`[design-tools] Missing optional dependency "${specifier}". ${DESIGN_SETUP_HINT}`);
|
|
19
|
+
}
|
|
20
|
+
function resolveWithRequire(specifier, base) {
|
|
21
|
+
try {
|
|
22
|
+
const resolver = createRequire(base);
|
|
23
|
+
return resolver.resolve(specifier);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function resolveWithImportMeta(specifier, parentUrl) {
|
|
30
|
+
const resolver = import.meta.resolve;
|
|
31
|
+
if (typeof resolver !== 'function') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return resolver(specifier, parentUrl);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function resolveOptionalDependency(specifier, cwd = process.cwd()) {
|
|
42
|
+
const cwdPackage = join(cwd, 'package.json');
|
|
43
|
+
if (existsSync(cwdPackage)) {
|
|
44
|
+
const cwdRequireResolved = resolveWithRequire(specifier, cwdPackage);
|
|
45
|
+
if (cwdRequireResolved) {
|
|
46
|
+
return cwdRequireResolved;
|
|
47
|
+
}
|
|
48
|
+
const cwdImportResolved = resolveWithImportMeta(specifier, pathToFileURL(cwdPackage).href);
|
|
49
|
+
if (cwdImportResolved) {
|
|
50
|
+
return cwdImportResolved;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const selfRequireResolved = resolveWithRequire(specifier, import.meta.url);
|
|
54
|
+
if (selfRequireResolved) {
|
|
55
|
+
return selfRequireResolved;
|
|
56
|
+
}
|
|
57
|
+
return resolveWithImportMeta(specifier, import.meta.url);
|
|
58
|
+
}
|
|
59
|
+
function toModuleUrl(resolved) {
|
|
60
|
+
if (resolved.startsWith('file://') || resolved.startsWith('node:')) {
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
63
|
+
return pathToFileURL(resolved).href;
|
|
64
|
+
}
|
|
65
|
+
async function loadOptionalDependency(specifier) {
|
|
66
|
+
const resolved = resolveOptionalDependency(specifier);
|
|
67
|
+
if (!resolved) {
|
|
68
|
+
throw missingDependency(specifier);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return (await import(toModuleUrl(resolved)));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (isModuleNotFound(error)) {
|
|
75
|
+
throw missingDependency(specifier);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let playwrightPromise = null;
|
|
81
|
+
let pngPromise = null;
|
|
82
|
+
let pixelmatchPromise = null;
|
|
83
|
+
let cheerioPromise = null;
|
|
84
|
+
export async function loadPlaywright() {
|
|
85
|
+
if (!playwrightPromise) {
|
|
86
|
+
playwrightPromise = loadOptionalDependency('playwright');
|
|
87
|
+
}
|
|
88
|
+
return playwrightPromise;
|
|
89
|
+
}
|
|
90
|
+
export async function loadPngjs() {
|
|
91
|
+
if (!pngPromise) {
|
|
92
|
+
pngPromise = loadOptionalDependency('pngjs');
|
|
93
|
+
}
|
|
94
|
+
return pngPromise;
|
|
95
|
+
}
|
|
96
|
+
export async function loadPixelmatch() {
|
|
97
|
+
if (!pixelmatchPromise) {
|
|
98
|
+
pixelmatchPromise = loadOptionalDependency('pixelmatch');
|
|
99
|
+
}
|
|
100
|
+
return pixelmatchPromise;
|
|
101
|
+
}
|
|
102
|
+
export async function loadCheerio() {
|
|
103
|
+
if (!cheerioPromise) {
|
|
104
|
+
cheerioPromise = loadOptionalDependency('cheerio');
|
|
105
|
+
}
|
|
106
|
+
return cheerioPromise;
|
|
107
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { loadDesignContext, resolveDesignPipelineId } from './context.js';
|
|
2
|
+
import { loadDesignRunState, mergeMetrics, saveDesignRunState, upsertStage } from './state.js';
|
|
3
|
+
async function main() {
|
|
4
|
+
const context = await loadDesignContext();
|
|
5
|
+
const state = await loadDesignRunState(context.statePath);
|
|
6
|
+
state.configSnapshot = JSON.parse(JSON.stringify(context.config.config));
|
|
7
|
+
const retention = context.config.config.metadata.design.retention;
|
|
8
|
+
state.retention = {
|
|
9
|
+
days: retention.days,
|
|
10
|
+
autoPurge: Boolean(retention.autoPurge),
|
|
11
|
+
policy: 'design.config.retention'
|
|
12
|
+
};
|
|
13
|
+
const designMetadata = context.config.config.metadata.design;
|
|
14
|
+
const privacy = designMetadata.privacy;
|
|
15
|
+
state.privacy = {
|
|
16
|
+
allowThirdParty: Boolean(privacy.allowThirdParty),
|
|
17
|
+
requireApproval: Boolean(privacy.requireApproval),
|
|
18
|
+
maskSelectors: [...designMetadata.maskSelectors],
|
|
19
|
+
approver: privacy.approver ?? null
|
|
20
|
+
};
|
|
21
|
+
mergeMetrics(state, {
|
|
22
|
+
design_pipeline_id: resolveDesignPipelineId(context.config),
|
|
23
|
+
design_pipeline_enabled: designMetadata.enabled,
|
|
24
|
+
capture_url_count: designMetadata.captureUrls.length,
|
|
25
|
+
breakpoint_count: designMetadata.breakpoints.length
|
|
26
|
+
});
|
|
27
|
+
const stageNotes = context.config.warnings.length > 0 ? context.config.warnings : undefined;
|
|
28
|
+
upsertStage(state, {
|
|
29
|
+
id: 'design-config',
|
|
30
|
+
title: 'Resolve design configuration',
|
|
31
|
+
status: 'succeeded',
|
|
32
|
+
notes: stageNotes,
|
|
33
|
+
metrics: {
|
|
34
|
+
config_path: context.designConfigPath
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
await saveDesignRunState(context.statePath, state);
|
|
38
|
+
console.log(`[design-config] task=${context.taskId} run=${context.runId} designEnabled=${context.config.config.metadata.design.enabled}`);
|
|
39
|
+
console.log(`[design-config] retention=${state.retention?.days ?? retention.days}d autoPurge=${state.retention?.autoPurge ?? retention.autoPurge}`);
|
|
40
|
+
console.log(`[design-config] privacy allowThirdParty=${state.privacy?.allowThirdParty} requireApproval=${state.privacy?.requireApproval}`);
|
|
41
|
+
}
|
|
42
|
+
main().catch((error) => {
|
|
43
|
+
console.error('[design-config] failed to prepare design context');
|
|
44
|
+
console.error(error instanceof Error ? error.stack ?? error.message : error);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { loadDesignContext } from './context.js';
|
|
5
|
+
import { appendArtifacts, loadDesignRunState, saveDesignRunState, upsertStage } from './state.js';
|
|
6
|
+
import { stageArtifacts } from '../../../orchestrator/src/persistence/ArtifactStager.js';
|
|
7
|
+
async function main() {
|
|
8
|
+
const context = await loadDesignContext();
|
|
9
|
+
const state = await loadDesignRunState(context.statePath);
|
|
10
|
+
const stageId = 'design-reference';
|
|
11
|
+
const html = buildReferencePage(context.config.config.metadata.design.captureUrls, state.artifacts.filter((artifact) => artifact.stage === 'extract'));
|
|
12
|
+
const tmpRoot = join(tmpdir(), `design-reference-${Date.now()}`);
|
|
13
|
+
await mkdir(tmpRoot, { recursive: true });
|
|
14
|
+
const tempFile = join(tmpRoot, 'motherduck.html');
|
|
15
|
+
await writeFile(tempFile, html, 'utf8');
|
|
16
|
+
const [staged] = await stageArtifacts({
|
|
17
|
+
taskId: context.taskId,
|
|
18
|
+
runId: context.runId,
|
|
19
|
+
artifacts: [
|
|
20
|
+
{
|
|
21
|
+
path: relative(process.cwd(), tempFile),
|
|
22
|
+
description: 'Design reference page'
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
options: {
|
|
26
|
+
relativeDir: 'design/reference',
|
|
27
|
+
overwrite: true
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const artifact = {
|
|
31
|
+
stage: 'reference',
|
|
32
|
+
status: 'succeeded',
|
|
33
|
+
relative_path: staged.path,
|
|
34
|
+
type: 'motherduck-html',
|
|
35
|
+
description: 'Aggregated reference page'
|
|
36
|
+
};
|
|
37
|
+
appendArtifacts(state, [artifact]);
|
|
38
|
+
upsertStage(state, {
|
|
39
|
+
id: stageId,
|
|
40
|
+
title: 'Build motherduck reference page',
|
|
41
|
+
status: 'succeeded',
|
|
42
|
+
artifacts: [
|
|
43
|
+
{
|
|
44
|
+
relative_path: staged.path,
|
|
45
|
+
stage: 'reference',
|
|
46
|
+
status: 'succeeded',
|
|
47
|
+
description: 'motherduck.html'
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
await saveDesignRunState(context.statePath, state);
|
|
52
|
+
console.log(`[design-reference] staged reference page: ${staged.path}`);
|
|
53
|
+
}
|
|
54
|
+
function buildReferencePage(urls, extractArtifacts) {
|
|
55
|
+
const captures = extractArtifacts.map((artifact) => artifact.relative_path);
|
|
56
|
+
const listItems = urls
|
|
57
|
+
.map((url) => `<li><a href="${url}">${url}</a></li>`)
|
|
58
|
+
.join('\n');
|
|
59
|
+
const captureList = captures
|
|
60
|
+
.map((path) => `<li><code>${path}</code></li>`)
|
|
61
|
+
.join('\n');
|
|
62
|
+
return `<!doctype html>
|
|
63
|
+
<html lang="en">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="utf-8" />
|
|
66
|
+
<title>Design Reference</title>
|
|
67
|
+
<style>
|
|
68
|
+
body { font-family: system-ui, sans-serif; margin: 2rem; }
|
|
69
|
+
h1 { margin-bottom: 1rem; }
|
|
70
|
+
section { margin-bottom: 2rem; }
|
|
71
|
+
</style>
|
|
72
|
+
</head>
|
|
73
|
+
<body>
|
|
74
|
+
<h1>Design Reference Overview</h1>
|
|
75
|
+
<section>
|
|
76
|
+
<h2>Capture Targets</h2>
|
|
77
|
+
<ol>
|
|
78
|
+
${listItems}
|
|
79
|
+
</ol>
|
|
80
|
+
</section>
|
|
81
|
+
<section>
|
|
82
|
+
<h2>Staged Artifacts</h2>
|
|
83
|
+
<ul>
|
|
84
|
+
${captureList}
|
|
85
|
+
</ul>
|
|
86
|
+
</section>
|
|
87
|
+
</body>
|
|
88
|
+
</html>`;
|
|
89
|
+
}
|
|
90
|
+
main().catch((error) => {
|
|
91
|
+
console.error('[design-reference] failed to generate reference page');
|
|
92
|
+
console.error(error instanceof Error ? error.stack ?? error.message : error);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
export async function loadDesignRunState(path) {
|
|
3
|
+
try {
|
|
4
|
+
const raw = await readFile(path, 'utf8');
|
|
5
|
+
const parsed = JSON.parse(raw);
|
|
6
|
+
return sanitizeState(parsed);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
const nodeError = error;
|
|
10
|
+
if (nodeError && nodeError.code === 'ENOENT') {
|
|
11
|
+
return createEmptyState();
|
|
12
|
+
}
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function saveDesignRunState(path, state) {
|
|
17
|
+
const payload = JSON.stringify(state, null, 2);
|
|
18
|
+
await writeFile(path, `${payload}\n`, 'utf8');
|
|
19
|
+
}
|
|
20
|
+
export function upsertStage(state, stage) {
|
|
21
|
+
const existingIndex = state.stages.findIndex((entry) => entry.id === stage.id);
|
|
22
|
+
const normalized = sanitizeStage(stage);
|
|
23
|
+
if (existingIndex >= 0) {
|
|
24
|
+
state.stages[existingIndex] = {
|
|
25
|
+
...state.stages[existingIndex],
|
|
26
|
+
...normalized,
|
|
27
|
+
notes: mergeNotes(state.stages[existingIndex].notes, normalized.notes),
|
|
28
|
+
metrics: { ...state.stages[existingIndex].metrics, ...normalized.metrics },
|
|
29
|
+
artifacts: mergeStageArtifacts(state.stages[existingIndex].artifacts, normalized.artifacts)
|
|
30
|
+
};
|
|
31
|
+
return state.stages[existingIndex];
|
|
32
|
+
}
|
|
33
|
+
state.stages.push(normalized);
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
export function appendArtifacts(state, artifacts) {
|
|
37
|
+
for (const artifact of artifacts) {
|
|
38
|
+
const existingIndex = state.artifacts.findIndex((entry) => entry.stage === artifact.stage && entry.relative_path === artifact.relative_path && entry.type === artifact.type);
|
|
39
|
+
if (existingIndex >= 0) {
|
|
40
|
+
state.artifacts[existingIndex] = { ...state.artifacts[existingIndex], ...artifact };
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
state.artifacts.push(artifact);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function appendToolkitArtifacts(state, artifacts) {
|
|
48
|
+
const toolkit = ensureToolkitState(state);
|
|
49
|
+
for (const artifact of artifacts) {
|
|
50
|
+
const index = toolkit.artifacts.findIndex((entry) => entry.id === artifact.id &&
|
|
51
|
+
entry.stage === artifact.stage &&
|
|
52
|
+
entry.relative_path === artifact.relative_path);
|
|
53
|
+
if (index >= 0) {
|
|
54
|
+
toolkit.artifacts[index] = {
|
|
55
|
+
...toolkit.artifacts[index],
|
|
56
|
+
...artifact
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
toolkit.artifacts.push({ ...artifact });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function appendApprovals(state, approvals) {
|
|
65
|
+
for (const approval of approvals) {
|
|
66
|
+
const exists = state.approvals.some((entry) => entry.id === approval.id);
|
|
67
|
+
if (!exists) {
|
|
68
|
+
state.approvals.push(approval);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function mergeMetrics(state, metrics) {
|
|
73
|
+
state.metrics = {
|
|
74
|
+
...state.metrics,
|
|
75
|
+
...metrics
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function ensureToolkitState(state) {
|
|
79
|
+
if (!state.toolkit) {
|
|
80
|
+
state.toolkit = {
|
|
81
|
+
contexts: [],
|
|
82
|
+
artifacts: []
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return state.toolkit;
|
|
86
|
+
}
|
|
87
|
+
export function upsertToolkitContext(state, contextRecord) {
|
|
88
|
+
const toolkit = ensureToolkitState(state);
|
|
89
|
+
const index = toolkit.contexts.findIndex((entry) => entry.id === contextRecord.id);
|
|
90
|
+
if (index >= 0) {
|
|
91
|
+
toolkit.contexts[index] = {
|
|
92
|
+
...toolkit.contexts[index],
|
|
93
|
+
...contextRecord
|
|
94
|
+
};
|
|
95
|
+
return toolkit.contexts[index];
|
|
96
|
+
}
|
|
97
|
+
toolkit.contexts.push({ ...contextRecord });
|
|
98
|
+
return toolkit.contexts[toolkit.contexts.length - 1];
|
|
99
|
+
}
|
|
100
|
+
function sanitizeState(state) {
|
|
101
|
+
if (!state || typeof state !== 'object') {
|
|
102
|
+
return createEmptyState();
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
configSnapshot: state.configSnapshot ?? null,
|
|
106
|
+
retention: state.retention,
|
|
107
|
+
privacy: state.privacy,
|
|
108
|
+
approvals: Array.isArray(state.approvals) ? [...state.approvals] : [],
|
|
109
|
+
artifacts: Array.isArray(state.artifacts) ? [...state.artifacts] : [],
|
|
110
|
+
toolkit: sanitizeToolkitState(state.toolkit),
|
|
111
|
+
stages: Array.isArray(state.stages) ? state.stages.map(sanitizeStage) : [],
|
|
112
|
+
metrics: state.metrics && typeof state.metrics === 'object' ? { ...state.metrics } : {},
|
|
113
|
+
designPlan: cloneDesignValue(state.designPlan),
|
|
114
|
+
designGuardrail: cloneDesignValue(state.designGuardrail),
|
|
115
|
+
designHistory: cloneDesignValue(state.designHistory),
|
|
116
|
+
designStyleProfile: cloneDesignValue(state.designStyleProfile),
|
|
117
|
+
designMetrics: cloneDesignValue(state.designMetrics)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function createEmptyState() {
|
|
121
|
+
return {
|
|
122
|
+
approvals: [],
|
|
123
|
+
artifacts: [],
|
|
124
|
+
toolkit: {
|
|
125
|
+
contexts: [],
|
|
126
|
+
artifacts: []
|
|
127
|
+
},
|
|
128
|
+
stages: [],
|
|
129
|
+
metrics: {},
|
|
130
|
+
designPlan: null,
|
|
131
|
+
designGuardrail: null,
|
|
132
|
+
designHistory: null,
|
|
133
|
+
designStyleProfile: null,
|
|
134
|
+
designMetrics: null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function sanitizeStage(stage) {
|
|
138
|
+
return {
|
|
139
|
+
id: stage.id,
|
|
140
|
+
title: stage.title,
|
|
141
|
+
status: stage.status,
|
|
142
|
+
notes: stage.notes ? [...stage.notes] : [],
|
|
143
|
+
metrics: stage.metrics ? { ...stage.metrics } : undefined,
|
|
144
|
+
artifacts: mergeStageArtifacts([], stage.artifacts)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function mergeNotes(existing, next) {
|
|
148
|
+
const combined = [...(existing ?? []), ...(next ?? [])];
|
|
149
|
+
if (combined.length === 0) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
return Array.from(new Set(combined));
|
|
153
|
+
}
|
|
154
|
+
function mergeStageArtifacts(existing, incoming) {
|
|
155
|
+
if ((!existing || existing.length === 0) && (!incoming || incoming.length === 0)) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const records = [...(existing ?? [])];
|
|
159
|
+
for (const artifact of incoming ?? []) {
|
|
160
|
+
const index = records.findIndex((entry) => entry.relative_path === artifact.relative_path);
|
|
161
|
+
if (index >= 0) {
|
|
162
|
+
records[index] = { ...records[index], ...artifact };
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
records.push({ ...artifact });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return records;
|
|
169
|
+
}
|
|
170
|
+
function cloneDesignValue(value) {
|
|
171
|
+
if (value === undefined) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
if (value === null) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return JSON.parse(JSON.stringify(value));
|
|
178
|
+
}
|
|
179
|
+
function sanitizeToolkitState(toolkit) {
|
|
180
|
+
if (!toolkit || typeof toolkit !== 'object') {
|
|
181
|
+
return {
|
|
182
|
+
contexts: [],
|
|
183
|
+
artifacts: []
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const contexts = Array.isArray(toolkit.contexts)
|
|
187
|
+
? toolkit.contexts.map((context) => ({ ...context }))
|
|
188
|
+
: [];
|
|
189
|
+
const artifacts = Array.isArray(toolkit.artifacts)
|
|
190
|
+
? toolkit.artifacts.map((artifact) => ({ ...artifact }))
|
|
191
|
+
: [];
|
|
192
|
+
const retention = toolkit.retention
|
|
193
|
+
? {
|
|
194
|
+
days: Number(toolkit.retention.days) || 0,
|
|
195
|
+
autoPurge: Boolean(toolkit.retention.autoPurge),
|
|
196
|
+
policy: toolkit.retention.policy
|
|
197
|
+
}
|
|
198
|
+
: undefined;
|
|
199
|
+
const summary = toolkit.summary && typeof toolkit.summary === 'object' ? { ...toolkit.summary } : undefined;
|
|
200
|
+
return {
|
|
201
|
+
contexts,
|
|
202
|
+
artifacts,
|
|
203
|
+
retention,
|
|
204
|
+
summary: summary ?? null
|
|
205
|
+
};
|
|
206
|
+
}
|