@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,665 @@
|
|
|
1
|
+
import { loadJsonManifest, resolveIO, writeJsonManifest } from './fileIO.js';
|
|
2
|
+
import { mergeToolkitArtifacts, sanitizeToolkitSummary } from './toolkitArtifacts.js';
|
|
3
|
+
import { coerceNonNegativeInteger, isIsoDate, sanitizeRelativeArtifactPath } from './artifactUtils.js';
|
|
4
|
+
const MAX_DESIGN_ARTIFACTS = 200;
|
|
5
|
+
const DESIGN_STAGE_SET = new Set([
|
|
6
|
+
'extract',
|
|
7
|
+
'reference',
|
|
8
|
+
'components',
|
|
9
|
+
'motion',
|
|
10
|
+
'video',
|
|
11
|
+
'visual-regression',
|
|
12
|
+
'style-ingestion',
|
|
13
|
+
'design-brief',
|
|
14
|
+
'aesthetic-plan',
|
|
15
|
+
'implementation',
|
|
16
|
+
'guardrail',
|
|
17
|
+
'design-history'
|
|
18
|
+
]);
|
|
19
|
+
export async function persistDesignManifest(manifestPath, update, options = {}) {
|
|
20
|
+
const io = resolveIO(options);
|
|
21
|
+
const manifest = (await loadJsonManifest(manifestPath, io)) ?? {};
|
|
22
|
+
const merged = mergeDesignManifest(manifest, update, options);
|
|
23
|
+
await writeJsonManifest(manifestPath, merged, io);
|
|
24
|
+
return merged;
|
|
25
|
+
}
|
|
26
|
+
function mergeDesignManifest(manifest, update, options) {
|
|
27
|
+
let merged = { ...manifest };
|
|
28
|
+
if (update.configSnapshot !== undefined) {
|
|
29
|
+
merged = {
|
|
30
|
+
...merged,
|
|
31
|
+
design_config_snapshot: sanitizeConfigSnapshot(update.configSnapshot)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (update.artifacts && update.artifacts.length > 0) {
|
|
35
|
+
merged = {
|
|
36
|
+
...merged,
|
|
37
|
+
design_artifacts: mergeDesignArtifacts(Array.isArray(manifest.design_artifacts) ? manifest.design_artifacts : [], update.artifacts, options)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (update.summary !== undefined) {
|
|
41
|
+
merged = {
|
|
42
|
+
...merged,
|
|
43
|
+
design_artifacts_summary: update.summary === null ? undefined : sanitizeDesignArtifactsSummary(update.summary)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (update.toolkitArtifacts && update.toolkitArtifacts.length > 0) {
|
|
47
|
+
merged = {
|
|
48
|
+
...merged,
|
|
49
|
+
design_toolkit_artifacts: mergeToolkitArtifacts(Array.isArray(manifest.design_toolkit_artifacts) ? manifest.design_toolkit_artifacts : [], update.toolkitArtifacts)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (update.toolkitSummary !== undefined) {
|
|
53
|
+
merged = {
|
|
54
|
+
...merged,
|
|
55
|
+
design_toolkit_summary: update.toolkitSummary === null ? undefined : sanitizeToolkitSummary(update.toolkitSummary)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (update.designPlan !== undefined) {
|
|
59
|
+
merged = {
|
|
60
|
+
...merged,
|
|
61
|
+
design_plan: update.designPlan === null ? undefined : sanitizeDesignPlan(update.designPlan)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (update.designGuardrail !== undefined) {
|
|
65
|
+
merged = {
|
|
66
|
+
...merged,
|
|
67
|
+
design_guardrail: update.designGuardrail === null ? undefined : sanitizeDesignGuardrail(update.designGuardrail)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (update.designHistory !== undefined) {
|
|
71
|
+
merged = {
|
|
72
|
+
...merged,
|
|
73
|
+
design_history: update.designHistory === null ? undefined : sanitizeDesignHistory(update.designHistory)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (update.designStyleProfile !== undefined) {
|
|
77
|
+
merged = {
|
|
78
|
+
...merged,
|
|
79
|
+
design_style_profile: update.designStyleProfile === null ? undefined : sanitizeDesignStyleProfile(update.designStyleProfile, options)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (update.designMetrics !== undefined) {
|
|
83
|
+
merged = {
|
|
84
|
+
...merged,
|
|
85
|
+
design_metrics: update.designMetrics === null ? undefined : sanitizeDesignMetrics(update.designMetrics)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return merged;
|
|
89
|
+
}
|
|
90
|
+
function mergeDesignArtifacts(existing, incoming, options) {
|
|
91
|
+
const sanitizedExisting = existing
|
|
92
|
+
.map((entry) => {
|
|
93
|
+
try {
|
|
94
|
+
return sanitizeDesignArtifactRecord(entry, options, false);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.filter((entry) => entry !== null);
|
|
101
|
+
const merged = [...sanitizedExisting];
|
|
102
|
+
for (const record of incoming) {
|
|
103
|
+
const sanitized = sanitizeDesignArtifactRecord(record, options, true);
|
|
104
|
+
const index = merged.findIndex((entry) => entry.stage === sanitized.stage &&
|
|
105
|
+
entry.relative_path === sanitized.relative_path &&
|
|
106
|
+
(sanitized.type ? entry.type === sanitized.type : true));
|
|
107
|
+
if (index >= 0) {
|
|
108
|
+
merged[index] = { ...merged[index], ...sanitized };
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
merged.push(sanitized);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (merged.length > MAX_DESIGN_ARTIFACTS) {
|
|
115
|
+
return merged.slice(-MAX_DESIGN_ARTIFACTS);
|
|
116
|
+
}
|
|
117
|
+
return merged;
|
|
118
|
+
}
|
|
119
|
+
function sanitizeDesignArtifactRecord(record, options, applyDefaults) {
|
|
120
|
+
if (!record || typeof record !== 'object') {
|
|
121
|
+
throw new Error('design artifact record must be an object');
|
|
122
|
+
}
|
|
123
|
+
const input = record;
|
|
124
|
+
const stage = input.stage;
|
|
125
|
+
if (typeof stage !== 'string' || !DESIGN_STAGE_SET.has(stage)) {
|
|
126
|
+
throw new Error(`invalid design artifact stage '${String(stage)}'`);
|
|
127
|
+
}
|
|
128
|
+
const status = input.status;
|
|
129
|
+
if (status !== 'succeeded' && status !== 'skipped' && status !== 'failed') {
|
|
130
|
+
throw new Error(`invalid design artifact status '${String(status)}'`);
|
|
131
|
+
}
|
|
132
|
+
const relativePathRaw = input.relative_path;
|
|
133
|
+
if (typeof relativePathRaw !== 'string') {
|
|
134
|
+
throw new Error('design artifact must include a relative_path string');
|
|
135
|
+
}
|
|
136
|
+
const sanitized = {
|
|
137
|
+
stage: stage,
|
|
138
|
+
status: status,
|
|
139
|
+
relative_path: sanitizeRelativeArtifactPath(relativePathRaw)
|
|
140
|
+
};
|
|
141
|
+
if (typeof input.type === 'string' && input.type.trim().length > 0) {
|
|
142
|
+
sanitized.type = input.type.trim();
|
|
143
|
+
}
|
|
144
|
+
if (typeof input.description === 'string' && input.description.trim().length > 0) {
|
|
145
|
+
sanitized.description = input.description.trim();
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(input.privacy_notes)) {
|
|
148
|
+
const notes = input.privacy_notes
|
|
149
|
+
.map((note) => (typeof note === 'string' ? note.trim() : ''))
|
|
150
|
+
.filter((note) => note.length > 0);
|
|
151
|
+
if (notes.length > 0) {
|
|
152
|
+
sanitized.privacy_notes = notes.slice(0, 20);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (typeof input.config_hash === 'string' && input.config_hash.trim().length > 0) {
|
|
156
|
+
sanitized.config_hash = input.config_hash.trim();
|
|
157
|
+
}
|
|
158
|
+
if (input.metadata && typeof input.metadata === 'object') {
|
|
159
|
+
sanitized.metadata = JSON.parse(JSON.stringify(input.metadata));
|
|
160
|
+
}
|
|
161
|
+
if (Array.isArray(input.approvals)) {
|
|
162
|
+
const approvals = input.approvals
|
|
163
|
+
.map((entry) => sanitizeApprovalRecord(entry))
|
|
164
|
+
.filter((entry) => entry !== null);
|
|
165
|
+
if (approvals.length > 0) {
|
|
166
|
+
sanitized.approvals = approvals;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (input.quota && typeof input.quota === 'object') {
|
|
170
|
+
const quota = sanitizeQuotaRecord(input.quota);
|
|
171
|
+
if (quota) {
|
|
172
|
+
sanitized.quota = quota;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (input.expiry && typeof input.expiry === 'object') {
|
|
176
|
+
const expiry = sanitizeExpiryRecord(input.expiry);
|
|
177
|
+
if (expiry) {
|
|
178
|
+
sanitized.expiry = expiry;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else if (applyDefaults && options.retentionDays !== undefined) {
|
|
182
|
+
sanitized.expiry = computeExpiryRecord(options.retentionDays, options.retentionPolicy, options.now);
|
|
183
|
+
}
|
|
184
|
+
return sanitized;
|
|
185
|
+
}
|
|
186
|
+
function sanitizeDesignArtifactsSummary(summary) {
|
|
187
|
+
if (!summary || typeof summary !== 'object') {
|
|
188
|
+
throw new Error('design artifacts summary must be an object');
|
|
189
|
+
}
|
|
190
|
+
const total = typeof summary.total_artifacts === 'number' ? summary.total_artifacts : Number(summary.total_artifacts ?? NaN);
|
|
191
|
+
if (!Number.isFinite(total) || total < 0) {
|
|
192
|
+
throw new Error('design artifacts summary requires a non-negative total_artifacts');
|
|
193
|
+
}
|
|
194
|
+
const generatedAt = typeof summary.generated_at === 'string' ? summary.generated_at : '';
|
|
195
|
+
if (!isIsoDate(generatedAt)) {
|
|
196
|
+
throw new Error('design artifacts summary generated_at must be ISO-8601 formatted');
|
|
197
|
+
}
|
|
198
|
+
const stages = Array.isArray(summary.stages)
|
|
199
|
+
? summary.stages
|
|
200
|
+
.map((stage) => sanitizeSummaryStage(stage))
|
|
201
|
+
.filter((entry) => entry !== null)
|
|
202
|
+
: [];
|
|
203
|
+
const sanitized = {
|
|
204
|
+
total_artifacts: Math.floor(total),
|
|
205
|
+
generated_at: generatedAt,
|
|
206
|
+
stages
|
|
207
|
+
};
|
|
208
|
+
if (typeof summary.storage_bytes === 'number') {
|
|
209
|
+
sanitized.storage_bytes = summary.storage_bytes < 0 ? 0 : Math.floor(summary.storage_bytes);
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(summary.errors)) {
|
|
212
|
+
const errors = summary.errors
|
|
213
|
+
.map((err) => (typeof err === 'string' ? err.trim() : ''))
|
|
214
|
+
.filter((err) => err.length > 0);
|
|
215
|
+
if (errors.length > 0) {
|
|
216
|
+
sanitized.errors = errors;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return sanitized;
|
|
220
|
+
}
|
|
221
|
+
function sanitizeSummaryStage(entry) {
|
|
222
|
+
if (!entry || typeof entry !== 'object') {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const record = entry;
|
|
226
|
+
const stage = record.stage;
|
|
227
|
+
if (typeof stage !== 'string' || !DESIGN_STAGE_SET.has(stage)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const succeededValue = coerceNonNegativeInteger(record.succeeded) ?? 0;
|
|
231
|
+
const failedValue = coerceNonNegativeInteger(record.failed) ?? 0;
|
|
232
|
+
const skippedValue = coerceNonNegativeInteger(record.skipped) ?? 0;
|
|
233
|
+
const artifactsValue = coerceNonNegativeInteger(record.artifacts);
|
|
234
|
+
const sanitized = {
|
|
235
|
+
stage: stage,
|
|
236
|
+
succeeded: succeededValue,
|
|
237
|
+
failed: failedValue,
|
|
238
|
+
skipped: skippedValue
|
|
239
|
+
};
|
|
240
|
+
if (artifactsValue !== null) {
|
|
241
|
+
sanitized.artifacts = artifactsValue;
|
|
242
|
+
}
|
|
243
|
+
if (Array.isArray(record.notes)) {
|
|
244
|
+
const notes = record.notes
|
|
245
|
+
.map((note) => (typeof note === 'string' ? note.trim() : ''))
|
|
246
|
+
.filter((note) => note.length > 0);
|
|
247
|
+
if (notes.length > 0) {
|
|
248
|
+
sanitized.notes = notes;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return sanitized;
|
|
252
|
+
}
|
|
253
|
+
function sanitizeApprovalRecord(entry) {
|
|
254
|
+
if (!entry || typeof entry !== 'object') {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
const record = entry;
|
|
258
|
+
const id = typeof record.id === 'string' ? record.id.trim() : '';
|
|
259
|
+
const actor = typeof record.actor === 'string' ? record.actor.trim() : '';
|
|
260
|
+
const reason = typeof record.reason === 'string' ? record.reason.trim() : '';
|
|
261
|
+
const timestamp = typeof record.timestamp === 'string' ? record.timestamp.trim() : '';
|
|
262
|
+
if (!id || !actor || !timestamp) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (!isIsoDate(timestamp)) {
|
|
266
|
+
throw new Error(`approval timestamp '${timestamp}' is not a valid ISO date`);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
id,
|
|
270
|
+
actor,
|
|
271
|
+
reason,
|
|
272
|
+
timestamp
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function sanitizeQuotaRecord(entry) {
|
|
276
|
+
if (!entry || typeof entry !== 'object') {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const record = entry;
|
|
280
|
+
const type = record.type === 'storage' || record.type === 'runtime' ? record.type : null;
|
|
281
|
+
const unit = record.unit === 'MB' || record.unit === 'seconds' ? record.unit : null;
|
|
282
|
+
const limit = typeof record.limit === 'number' ? record.limit : Number(record.limit ?? NaN);
|
|
283
|
+
if (!type || !unit || !Number.isFinite(limit)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const sanitized = {
|
|
287
|
+
type,
|
|
288
|
+
unit,
|
|
289
|
+
limit: limit < 0 ? 0 : limit
|
|
290
|
+
};
|
|
291
|
+
const consumedValue = typeof record.consumed === 'number' ? record.consumed : Number(record.consumed ?? NaN);
|
|
292
|
+
if (Number.isFinite(consumedValue)) {
|
|
293
|
+
sanitized.consumed = consumedValue < 0 ? 0 : consumedValue;
|
|
294
|
+
}
|
|
295
|
+
return sanitized;
|
|
296
|
+
}
|
|
297
|
+
function sanitizeExpiryRecord(entry) {
|
|
298
|
+
if (!entry || typeof entry !== 'object') {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const record = entry;
|
|
302
|
+
const date = typeof record.date === 'string' ? record.date.trim() : '';
|
|
303
|
+
const policy = typeof record.policy === 'string' ? record.policy.trim() : '';
|
|
304
|
+
if (!date || !policy) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
if (!isIsoDate(date)) {
|
|
308
|
+
throw new Error(`expiry date '${date}' must be ISO-8601 formatted`);
|
|
309
|
+
}
|
|
310
|
+
return { date, policy };
|
|
311
|
+
}
|
|
312
|
+
function computeExpiryRecord(retentionDays, policy, now) {
|
|
313
|
+
const base = now ?? new Date();
|
|
314
|
+
const ms = base.getTime() + Math.max(0, retentionDays) * 24 * 60 * 60 * 1000;
|
|
315
|
+
return {
|
|
316
|
+
date: new Date(ms).toISOString(),
|
|
317
|
+
policy: policy ?? 'design.config.retention'
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function sanitizeConfigSnapshot(snapshot) {
|
|
321
|
+
if (snapshot === null) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
325
|
+
throw new Error('design config snapshot must be an object or null');
|
|
326
|
+
}
|
|
327
|
+
return JSON.parse(JSON.stringify(snapshot));
|
|
328
|
+
}
|
|
329
|
+
function sanitizeDesignPlan(plan) {
|
|
330
|
+
if (!plan || typeof plan !== 'object') {
|
|
331
|
+
throw new Error('design plan must be an object');
|
|
332
|
+
}
|
|
333
|
+
const record = plan;
|
|
334
|
+
const mode = record.mode;
|
|
335
|
+
if (mode !== 'fresh' && mode !== 'clone-informed') {
|
|
336
|
+
throw new Error(`design plan mode must be 'fresh' or 'clone-informed' (received '${String(mode)}')`);
|
|
337
|
+
}
|
|
338
|
+
const brief = record.brief;
|
|
339
|
+
if (!brief || typeof brief !== 'object') {
|
|
340
|
+
throw new Error('design plan requires a brief descriptor');
|
|
341
|
+
}
|
|
342
|
+
const briefRecord = brief;
|
|
343
|
+
const briefPath = sanitizeRelativeArtifactPath(String(briefRecord.path ?? ''));
|
|
344
|
+
const aestheticPlanRaw = record.aesthetic_plan;
|
|
345
|
+
const implementationRaw = record.implementation;
|
|
346
|
+
const sanitized = {
|
|
347
|
+
mode,
|
|
348
|
+
brief: {
|
|
349
|
+
path: briefPath
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
if (typeof briefRecord.hash === 'string' && briefRecord.hash.trim().length > 0) {
|
|
353
|
+
sanitized.brief.hash = briefRecord.hash.trim();
|
|
354
|
+
}
|
|
355
|
+
if (typeof briefRecord.id === 'string' && briefRecord.id.trim().length > 0) {
|
|
356
|
+
sanitized.brief.id = briefRecord.id.trim();
|
|
357
|
+
}
|
|
358
|
+
if (aestheticPlanRaw && typeof aestheticPlanRaw === 'object') {
|
|
359
|
+
const aestheticPlan = aestheticPlanRaw;
|
|
360
|
+
sanitized.aesthetic_plan = {
|
|
361
|
+
path: sanitizeRelativeArtifactPath(String(aestheticPlan.path ?? ''))
|
|
362
|
+
};
|
|
363
|
+
if (typeof aestheticPlan.snippet_version === 'string' && aestheticPlan.snippet_version.trim().length > 0) {
|
|
364
|
+
sanitized.aesthetic_plan.snippet_version = aestheticPlan.snippet_version.trim();
|
|
365
|
+
}
|
|
366
|
+
if (typeof aestheticPlan.id === 'string' && aestheticPlan.id.trim().length > 0) {
|
|
367
|
+
sanitized.aesthetic_plan.id = aestheticPlan.id.trim();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (implementationRaw && typeof implementationRaw === 'object') {
|
|
371
|
+
const implementation = implementationRaw;
|
|
372
|
+
sanitized.implementation = {
|
|
373
|
+
path: sanitizeRelativeArtifactPath(String(implementation.path ?? ''))
|
|
374
|
+
};
|
|
375
|
+
if (typeof implementation.complexity === 'string' && implementation.complexity.trim().length > 0) {
|
|
376
|
+
sanitized.implementation.complexity = implementation.complexity.trim();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (record.reference_style_id !== undefined && record.reference_style_id !== null) {
|
|
380
|
+
const value = typeof record.reference_style_id === 'string' ? record.reference_style_id.trim() : '';
|
|
381
|
+
if (value.length > 0) {
|
|
382
|
+
sanitized.reference_style_id = value;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
sanitized.reference_style_id = null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (record.style_profile_id !== undefined && record.style_profile_id !== null) {
|
|
389
|
+
const value = typeof record.style_profile_id === 'string' ? record.style_profile_id.trim() : '';
|
|
390
|
+
if (value.length > 0) {
|
|
391
|
+
sanitized.style_profile_id = value;
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
sanitized.style_profile_id = null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (typeof record.generated_at === 'string' && isIsoDate(record.generated_at)) {
|
|
398
|
+
sanitized.generated_at = record.generated_at;
|
|
399
|
+
}
|
|
400
|
+
return sanitized;
|
|
401
|
+
}
|
|
402
|
+
function sanitizeDesignGuardrail(guardrail) {
|
|
403
|
+
if (!guardrail || typeof guardrail !== 'object') {
|
|
404
|
+
throw new Error('design guardrail must be an object');
|
|
405
|
+
}
|
|
406
|
+
const record = guardrail;
|
|
407
|
+
const status = record.status;
|
|
408
|
+
if (status !== 'pass' && status !== 'fail') {
|
|
409
|
+
throw new Error(`design guardrail status must be 'pass' or 'fail' (received '${String(status)}')`);
|
|
410
|
+
}
|
|
411
|
+
const reportPath = sanitizeRelativeArtifactPath(String(record.report_path ?? ''));
|
|
412
|
+
const sanitized = {
|
|
413
|
+
report_path: reportPath,
|
|
414
|
+
status
|
|
415
|
+
};
|
|
416
|
+
if (typeof record.snippet_version === 'string' && record.snippet_version.trim().length > 0) {
|
|
417
|
+
sanitized.snippet_version = record.snippet_version.trim();
|
|
418
|
+
}
|
|
419
|
+
if (typeof record.strictness === 'string' && ['low', 'medium', 'high'].includes(record.strictness.trim())) {
|
|
420
|
+
sanitized.strictness = record.strictness.trim();
|
|
421
|
+
}
|
|
422
|
+
if (record.slop_threshold !== undefined) {
|
|
423
|
+
const threshold = clampScore(record.slop_threshold, false);
|
|
424
|
+
if (threshold !== null) {
|
|
425
|
+
sanitized.slop_threshold = threshold;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (record.mode === 'fresh' || record.mode === 'clone-informed') {
|
|
429
|
+
sanitized.mode = record.mode;
|
|
430
|
+
}
|
|
431
|
+
if (record.scores && typeof record.scores === 'object') {
|
|
432
|
+
sanitized.scores = sanitizeScoreMap(record.scores);
|
|
433
|
+
}
|
|
434
|
+
if (Array.isArray(record.recommendations)) {
|
|
435
|
+
const recommendations = record.recommendations
|
|
436
|
+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
437
|
+
.filter((entry) => entry.length > 0);
|
|
438
|
+
if (recommendations.length > 0) {
|
|
439
|
+
sanitized.recommendations = recommendations.slice(0, 20);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (Array.isArray(record.notes)) {
|
|
443
|
+
const notes = record.notes
|
|
444
|
+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
445
|
+
.filter((entry) => entry.length > 0);
|
|
446
|
+
if (notes.length > 0) {
|
|
447
|
+
sanitized.notes = notes.slice(0, 20);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (record.style_overlap && typeof record.style_overlap === 'object') {
|
|
451
|
+
sanitized.style_overlap = sanitizeStyleOverlap(record.style_overlap);
|
|
452
|
+
}
|
|
453
|
+
return sanitized;
|
|
454
|
+
}
|
|
455
|
+
function sanitizeDesignHistory(history) {
|
|
456
|
+
if (!history || typeof history !== 'object') {
|
|
457
|
+
throw new Error('design history must be an object');
|
|
458
|
+
}
|
|
459
|
+
const record = history;
|
|
460
|
+
const path = sanitizeRelativeArtifactPath(String(record.path ?? ''));
|
|
461
|
+
const sanitized = { path };
|
|
462
|
+
if (record.mirror_path !== undefined) {
|
|
463
|
+
const mirrorPathRaw = typeof record.mirror_path === 'string' ? record.mirror_path.trim() : '';
|
|
464
|
+
if (mirrorPathRaw.length > 0) {
|
|
465
|
+
sanitized.mirror_path = sanitizeRelativeArtifactPath(mirrorPathRaw);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const entries = coerceNonNegativeInteger(record.entries);
|
|
469
|
+
if (entries !== null) {
|
|
470
|
+
sanitized.entries = entries;
|
|
471
|
+
}
|
|
472
|
+
const maxEntries = coerceNonNegativeInteger(record.max_entries);
|
|
473
|
+
if (maxEntries !== null) {
|
|
474
|
+
sanitized.max_entries = maxEntries;
|
|
475
|
+
}
|
|
476
|
+
if (typeof record.updated_at === 'string' && isIsoDate(record.updated_at)) {
|
|
477
|
+
sanitized.updated_at = record.updated_at;
|
|
478
|
+
}
|
|
479
|
+
if (record.mode === 'fresh' || record.mode === 'clone-informed') {
|
|
480
|
+
sanitized.mode = record.mode;
|
|
481
|
+
}
|
|
482
|
+
return sanitized;
|
|
483
|
+
}
|
|
484
|
+
function sanitizeDesignStyleProfile(profile, options) {
|
|
485
|
+
if (!profile || typeof profile !== 'object') {
|
|
486
|
+
throw new Error('design style profile must be an object');
|
|
487
|
+
}
|
|
488
|
+
const record = profile;
|
|
489
|
+
const id = typeof record.id === 'string' ? record.id.trim() : '';
|
|
490
|
+
if (!id) {
|
|
491
|
+
throw new Error('design style profile requires an id');
|
|
492
|
+
}
|
|
493
|
+
const relativePath = sanitizeRelativeArtifactPath(String(record.relative_path ?? ''));
|
|
494
|
+
const sanitized = {
|
|
495
|
+
id,
|
|
496
|
+
relative_path: relativePath
|
|
497
|
+
};
|
|
498
|
+
if (typeof record.source_url === 'string' && record.source_url.trim().length > 0) {
|
|
499
|
+
sanitized.source_url = record.source_url.trim();
|
|
500
|
+
}
|
|
501
|
+
if (typeof record.ingestion_run === 'string' && record.ingestion_run.trim().length > 0) {
|
|
502
|
+
sanitized.ingestion_run = record.ingestion_run.trim();
|
|
503
|
+
}
|
|
504
|
+
if (typeof record.similarity_level === 'string' &&
|
|
505
|
+
['low', 'medium', 'high'].includes(record.similarity_level.trim())) {
|
|
506
|
+
sanitized.similarity_level = record.similarity_level.trim();
|
|
507
|
+
}
|
|
508
|
+
if (record.do_not_copy && typeof record.do_not_copy === 'object') {
|
|
509
|
+
const doNotCopy = record.do_not_copy;
|
|
510
|
+
const doNotCopyValues = {
|
|
511
|
+
logos: sanitizeStringList(doNotCopy.logos, 20),
|
|
512
|
+
wordmarks: sanitizeStringList(doNotCopy.wordmarks, 20),
|
|
513
|
+
unique_shapes: sanitizeStringList(doNotCopy.unique_shapes, 20),
|
|
514
|
+
unique_illustrations: sanitizeStringList(doNotCopy.unique_illustrations, 20),
|
|
515
|
+
other: sanitizeStringList(doNotCopy.other, 20)
|
|
516
|
+
};
|
|
517
|
+
const hasDoNotCopyValues = Object.values(doNotCopyValues).some((entry) => Array.isArray(entry) && entry.length > 0);
|
|
518
|
+
if (hasDoNotCopyValues) {
|
|
519
|
+
sanitized.do_not_copy = doNotCopyValues;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const retentionDays = coerceNonNegativeInteger(record.retention_days);
|
|
523
|
+
if (retentionDays !== null) {
|
|
524
|
+
sanitized.retention_days = retentionDays;
|
|
525
|
+
}
|
|
526
|
+
else if (options.retentionDays !== undefined) {
|
|
527
|
+
sanitized.retention_days = Math.max(0, options.retentionDays);
|
|
528
|
+
}
|
|
529
|
+
if (record.expiry && typeof record.expiry === 'object') {
|
|
530
|
+
const expiry = sanitizeExpiryRecord(record.expiry);
|
|
531
|
+
if (expiry) {
|
|
532
|
+
sanitized.expiry = expiry;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
const retentionForExpiry = sanitized.retention_days ?? options.retentionDays;
|
|
537
|
+
if (retentionForExpiry !== undefined) {
|
|
538
|
+
sanitized.expiry = computeExpiryRecord(retentionForExpiry, options.retentionPolicy, options.now);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (Array.isArray(record.approvals)) {
|
|
542
|
+
const approvals = record.approvals
|
|
543
|
+
.map((entry) => sanitizeApprovalRecord(entry))
|
|
544
|
+
.filter((entry) => entry !== null);
|
|
545
|
+
if (approvals.length > 0) {
|
|
546
|
+
sanitized.approvals = approvals;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (Array.isArray(record.notes)) {
|
|
550
|
+
const notes = sanitizeStringList(record.notes, 20);
|
|
551
|
+
if (notes && notes.length > 0) {
|
|
552
|
+
sanitized.notes = notes;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return sanitized;
|
|
556
|
+
}
|
|
557
|
+
function sanitizeDesignMetrics(metrics) {
|
|
558
|
+
if (!metrics || typeof metrics !== 'object') {
|
|
559
|
+
throw new Error('design metrics must be an object');
|
|
560
|
+
}
|
|
561
|
+
const record = metrics;
|
|
562
|
+
const sanitized = {};
|
|
563
|
+
const fields = [
|
|
564
|
+
'aesthetic_axes_completeness',
|
|
565
|
+
'originality_score',
|
|
566
|
+
'accessibility_score',
|
|
567
|
+
'brief_alignment_score',
|
|
568
|
+
'slop_risk',
|
|
569
|
+
'diversity_penalty',
|
|
570
|
+
'similarity_to_reference',
|
|
571
|
+
'style_overlap'
|
|
572
|
+
];
|
|
573
|
+
for (const field of fields) {
|
|
574
|
+
const value = clampScore(record[field]);
|
|
575
|
+
if (value !== null) {
|
|
576
|
+
sanitized[field] = value;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (record.style_overlap_gate === 'pass' || record.style_overlap_gate === 'fail') {
|
|
580
|
+
sanitized.style_overlap_gate = record.style_overlap_gate;
|
|
581
|
+
}
|
|
582
|
+
if (typeof record.snippet_version === 'string' && record.snippet_version.trim().length > 0) {
|
|
583
|
+
sanitized.snippet_version = record.snippet_version.trim();
|
|
584
|
+
}
|
|
585
|
+
return sanitized;
|
|
586
|
+
}
|
|
587
|
+
function sanitizeStyleOverlap(entry) {
|
|
588
|
+
const palette = clampScore(entry.palette);
|
|
589
|
+
const typography = clampScore(entry.typography);
|
|
590
|
+
const motion = clampScore(entry.motion);
|
|
591
|
+
const spacing = clampScore(entry.spacing);
|
|
592
|
+
const threshold = clampScore(entry.threshold);
|
|
593
|
+
const overall = clampScore(entry.overall) ??
|
|
594
|
+
Math.max(palette ?? 0, typography ?? 0, motion ?? 0, spacing ?? 0);
|
|
595
|
+
const overlap = {
|
|
596
|
+
overall
|
|
597
|
+
};
|
|
598
|
+
if (palette !== null)
|
|
599
|
+
overlap.palette = palette;
|
|
600
|
+
if (typography !== null)
|
|
601
|
+
overlap.typography = typography;
|
|
602
|
+
if (motion !== null)
|
|
603
|
+
overlap.motion = motion;
|
|
604
|
+
if (spacing !== null)
|
|
605
|
+
overlap.spacing = spacing;
|
|
606
|
+
if (threshold !== null)
|
|
607
|
+
overlap.threshold = threshold;
|
|
608
|
+
if (entry.gate === 'pass' || entry.gate === 'fail') {
|
|
609
|
+
overlap.gate = entry.gate;
|
|
610
|
+
}
|
|
611
|
+
if (Array.isArray(entry.comparison_window)) {
|
|
612
|
+
const windowIds = entry.comparison_window
|
|
613
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
614
|
+
.filter((value) => value.length > 0);
|
|
615
|
+
if (windowIds.length > 0) {
|
|
616
|
+
overlap.comparison_window = windowIds.slice(0, 20);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (entry.reference_style_id !== undefined && entry.reference_style_id !== null) {
|
|
620
|
+
const value = typeof entry.reference_style_id === 'string' ? entry.reference_style_id.trim() : '';
|
|
621
|
+
overlap.reference_style_id = value.length > 0 ? value : null;
|
|
622
|
+
}
|
|
623
|
+
return overlap;
|
|
624
|
+
}
|
|
625
|
+
function sanitizeScoreMap(record) {
|
|
626
|
+
const scores = {};
|
|
627
|
+
for (const [key, value] of Object.entries(record)) {
|
|
628
|
+
if (!key) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const numeric = clampScore(value);
|
|
632
|
+
if (numeric !== null) {
|
|
633
|
+
scores[key] = numeric;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return scores;
|
|
637
|
+
}
|
|
638
|
+
function clampScore(value, allowAboveOne = false) {
|
|
639
|
+
if (typeof value === 'number' || typeof value === 'string') {
|
|
640
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
641
|
+
if (Number.isFinite(numeric)) {
|
|
642
|
+
if (allowAboveOne) {
|
|
643
|
+
return numeric;
|
|
644
|
+
}
|
|
645
|
+
if (numeric < 0)
|
|
646
|
+
return 0;
|
|
647
|
+
if (numeric > 1)
|
|
648
|
+
return 1;
|
|
649
|
+
return numeric;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
function sanitizeStringList(value, maxEntries) {
|
|
655
|
+
if (!Array.isArray(value)) {
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
const entries = value
|
|
659
|
+
.map((item) => (typeof item === 'string' ? item.trim() : ''))
|
|
660
|
+
.filter((item) => item.length > 0);
|
|
661
|
+
if (entries.length === 0) {
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
return entries.slice(0, maxEntries);
|
|
665
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
export function resolveIO(options = {}) {
|
|
4
|
+
const read = options.read ?? ((path, encoding) => readFile(path, { encoding }));
|
|
5
|
+
const write = options.write ?? ((path, data) => writeFile(path, data, { encoding: 'utf-8' }));
|
|
6
|
+
const ensureDir = options.ensureDir ??
|
|
7
|
+
(async (path) => {
|
|
8
|
+
await mkdir(path, { recursive: true });
|
|
9
|
+
});
|
|
10
|
+
return { read, write, ensureDir };
|
|
11
|
+
}
|
|
12
|
+
export async function loadJsonManifest(manifestPath, io) {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await io.read(manifestPath, 'utf-8');
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const err = error;
|
|
19
|
+
if (!err || err.code !== 'ENOENT') {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
await io.ensureDir(dirname(manifestPath));
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function writeJsonManifest(manifestPath, manifest, io) {
|
|
27
|
+
const serialized = JSON.stringify(manifest, null, 2);
|
|
28
|
+
await io.write(manifestPath, `${serialized}\n`);
|
|
29
|
+
}
|