orizu 0.3.5 → 0.3.6
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/README.md +25 -3
- package/dist/help.js +611 -0
- package/dist/index.js +268 -15
- package/dist/skill-installer.js +180 -0
- package/package.json +3 -3
- package/scripts/vendor-orizu-skill.mjs +54 -0
- package/src/help.ts +676 -0
- package/src/index.ts +353 -15
- package/src/skill-installer.ts +260 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/cli.py +14 -0
- package/vendor/orizu-gepa-python/src/orizu_gepa/optimizer.py +46 -0
- package/vendor/skills/orizu-cli/SKILL.md +185 -0
- package/vendor/skills/orizu-cli/agents/openai.yaml +4 -0
- package/vendor/skills/orizu-cli/references/building-apps.md +1461 -0
- package/vendor/skills/orizu-cli/references/building-judges.md +218 -0
- package/vendor/skills/orizu-cli/references/cli-reference.md +324 -0
- package/vendor/skills/orizu-cli/references/optimization-reports.md +174 -0
- package/vendor/skills/orizu-cli/references/optimization-with-gepa.md +291 -0
- package/vendor/skills/orizu-cli/references/primer.md +245 -0
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +830 -0
- package/vendor/skills/orizu-cli/scripts/test-app.mjs +492 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Offline smoke test for an Orizu labeler app.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node test-app.mjs <App.tsx> <input.json> <output.json> [sample-payload.json]
|
|
7
|
+
*
|
|
8
|
+
* Checks:
|
|
9
|
+
* 1. App.tsx has a single named default export (function or class).
|
|
10
|
+
* 2. Default export's first parameter destructures inputData, onComplete,
|
|
11
|
+
* initialValues (and not the deprecated data, onSubmit).
|
|
12
|
+
* 3. Imports resolve against the platform-provided component registry.
|
|
13
|
+
* 4. input.json and output.json use only the supported JSON Schema subset:
|
|
14
|
+
* type, required, properties, items, enum.
|
|
15
|
+
* 5. (Optional) sample-payload.json validates against output.json.
|
|
16
|
+
*
|
|
17
|
+
* Exits 0 on pass, 1 on any failure. Pure ES module — runs on node 18+
|
|
18
|
+
* with no dependencies. Also runs under bun unchanged.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
22
|
+
import process from "node:process";
|
|
23
|
+
|
|
24
|
+
function printLine(message = "") {
|
|
25
|
+
process.stdout.write(`${message}\n`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ALLOWED_SCHEMA_KEYS = new Set([
|
|
29
|
+
"type",
|
|
30
|
+
"required",
|
|
31
|
+
"properties",
|
|
32
|
+
"items",
|
|
33
|
+
"enum",
|
|
34
|
+
// Permitted purely as documentation; ignored by the platform.
|
|
35
|
+
"title",
|
|
36
|
+
"description",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const ALLOWED_TYPES = new Set([
|
|
40
|
+
"object",
|
|
41
|
+
"string",
|
|
42
|
+
"number",
|
|
43
|
+
"integer",
|
|
44
|
+
"boolean",
|
|
45
|
+
"array",
|
|
46
|
+
"null",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const REQUIRED_PROPS = ["inputData", "onComplete"];
|
|
50
|
+
const OPTIONAL_PROPS = ["initialValues"];
|
|
51
|
+
const DEPRECATED_PROPS = ["data", "onSubmit"];
|
|
52
|
+
|
|
53
|
+
// Generated from lib/available-components.ts by scripts/sync-agent-doc.mjs.
|
|
54
|
+
// BEGIN ORIZU_AUTO_TEST_APP_IMPORTS
|
|
55
|
+
const AVAILABLE_IMPORTS = Object.freeze({
|
|
56
|
+
"@/components/ui/button": ["Button"],
|
|
57
|
+
"@/components/ui/card": ["Card"],
|
|
58
|
+
"@/components/ui/carousel": ["Carousel"],
|
|
59
|
+
"@/components/ui/checkbox": ["Checkbox"],
|
|
60
|
+
"@/components/ui/form": ["Form"],
|
|
61
|
+
"@/components/ui/input": ["Input"],
|
|
62
|
+
"@/components/ui/label": ["Label"],
|
|
63
|
+
"@/components/ui/progress": ["Progress"],
|
|
64
|
+
"@/components/ui/radio-group": ["RadioGroup","RadioGroupItem"],
|
|
65
|
+
"@/components/ui/scroll-area": ["ScrollArea"],
|
|
66
|
+
"@/components/ui/select": ["Select","SelectContent","SelectItem","SelectTrigger","SelectValue"],
|
|
67
|
+
"@/components/ui/separator": ["Separator"],
|
|
68
|
+
"@/components/ui/switch": ["Switch"],
|
|
69
|
+
"@/components/ui/table": ["Table","TableBody","TableCell","TableHead","TableHeader","TableRow","TableCaption","TableFooter"],
|
|
70
|
+
"@/components/ui/tabs": ["Tabs","TabsContent","TabsList","TabsTrigger"],
|
|
71
|
+
"@/components/ui/textarea": ["Textarea"],
|
|
72
|
+
"@/components/ui/toggle": ["Toggle"],
|
|
73
|
+
"@/components/ui/toggle-group": ["ToggleGroup","ToggleGroupItem"],
|
|
74
|
+
"@/components/ui/tooltip": ["Tooltip","TooltipContent","TooltipProvider","TooltipTrigger"],
|
|
75
|
+
"@/components/base/content/TextContent": ["TextContent","default"],
|
|
76
|
+
"@/components/base/content/CodeBlock": ["CodeBlock","default"],
|
|
77
|
+
"@/components/base/content/ConversationView": ["AssistantMessageBlock","ContextMessageBlock","ConversationMessageBlock","ConversationView","ReasoningMessageBlock","SystemMessageBlock","ToolCallBlock","ToolResultBlock","UserMessageBlock","default"],
|
|
78
|
+
"@/components/base/content/ContentRenderer": ["ContentRenderer","default"],
|
|
79
|
+
"@/components/base/content/Prose": ["Prose","default"],
|
|
80
|
+
"@/components/base/behaviors/Annotatable": ["Annotatable","default"],
|
|
81
|
+
"@/components/base/behaviors/Reactable": ["Reactable","default"],
|
|
82
|
+
"@/components/base/input/CommentBox": ["CommentBox"],
|
|
83
|
+
"@/components/base/input/CriterionRating": ["CriterionRating"],
|
|
84
|
+
"@/components/base/input/LikertScale": ["LikertScale"],
|
|
85
|
+
"@/components/base/input/NumericRating": ["NumericRating"],
|
|
86
|
+
"@/components/base/input/RatingSelector": ["RatingSelector"],
|
|
87
|
+
"@/components/base/input/StarRating": ["StarRating"],
|
|
88
|
+
"@/components/base/input/TagPicker": ["TagPicker"],
|
|
89
|
+
"@/components/base/input/ThumbsRating": ["ThumbsRating"],
|
|
90
|
+
"@/components/base/ui/ComparisonPanel": ["ComparisonPanel"],
|
|
91
|
+
"@/components/base/ui/DraggableItem": ["DraggableItem"],
|
|
92
|
+
"@/components/templates/classification/TagSelector": ["TagSelector"],
|
|
93
|
+
"@/components/templates/comparison/SideBySideComparison": ["SideBySideComparison"],
|
|
94
|
+
"@/components/templates/correction/CorrectionTask": ["CorrectionTask"],
|
|
95
|
+
"@/components/templates/code/CodeComparison": ["CodeComparison"],
|
|
96
|
+
"@/components/templates/qa/ContextualQA": ["ContextualQA"],
|
|
97
|
+
"@/components/templates/ranking/RankingList": ["RankingList"],
|
|
98
|
+
"@/components/templates/rating/SingleItemRater": ["SingleItemRater"],
|
|
99
|
+
});
|
|
100
|
+
// END ORIZU_AUTO_TEST_APP_IMPORTS
|
|
101
|
+
|
|
102
|
+
const LEGACY_IMPORT_SUGGESTIONS = {
|
|
103
|
+
"@/components/prose": "@/components/base/content/Prose",
|
|
104
|
+
"@/components/ui/text-content": "@/components/base/content/TextContent",
|
|
105
|
+
"@/components/ui/comment-box": "@/components/base/input/CommentBox",
|
|
106
|
+
"@/components/ui/tag-picker": "@/components/base/input/TagPicker",
|
|
107
|
+
"@/components/criterion-rating": "@/components/base/input/CriterionRating",
|
|
108
|
+
"@/components/annotation/annotatable":
|
|
109
|
+
"@/components/base/behaviors/Annotatable",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const issues = [];
|
|
113
|
+
|
|
114
|
+
function err(message) {
|
|
115
|
+
issues.push({ level: "error", message });
|
|
116
|
+
}
|
|
117
|
+
function warn(message) {
|
|
118
|
+
issues.push({ level: "warn", message });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkAppFile(path) {
|
|
122
|
+
if (!existsSync(path)) {
|
|
123
|
+
err(`App file not found: ${path}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const src = readFileSync(path, "utf8");
|
|
127
|
+
|
|
128
|
+
// Strip line comments and block comments to avoid false matches.
|
|
129
|
+
const stripped = src
|
|
130
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
131
|
+
.replace(/(^|[^:])\/\/[^\n]*/g, "$1");
|
|
132
|
+
|
|
133
|
+
// 1. Find a named default export.
|
|
134
|
+
const fnMatch = stripped.match(
|
|
135
|
+
/export\s+default\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)/
|
|
136
|
+
);
|
|
137
|
+
const classMatch = stripped.match(
|
|
138
|
+
/export\s+default\s+class\s+([A-Za-z_$][\w$]*)/
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (!fnMatch && !classMatch) {
|
|
142
|
+
if (
|
|
143
|
+
/export\s+default\s+(?:\(|function\s*\(|class\s*\{|memo|forwardRef)/.test(
|
|
144
|
+
stripped
|
|
145
|
+
)
|
|
146
|
+
) {
|
|
147
|
+
err(
|
|
148
|
+
"Default export is anonymous or wrapped (e.g. memo/forwardRef). Use a named function or class declaration."
|
|
149
|
+
);
|
|
150
|
+
} else {
|
|
151
|
+
err(
|
|
152
|
+
"No default export found. App.tsx must export a named function or class as default."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const exportName = (fnMatch && fnMatch[1]) || (classMatch && classMatch[1]);
|
|
159
|
+
if (exportName === "GeneratedComponent") {
|
|
160
|
+
warn(
|
|
161
|
+
`Default export is named "GeneratedComponent" — consider a more descriptive name (e.g. SupportLabeler).`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 2. For function exports, check the parameter destructure.
|
|
166
|
+
if (fnMatch) {
|
|
167
|
+
const paramSrc = fnMatch[2] || "";
|
|
168
|
+
if (!paramSrc.trim()) {
|
|
169
|
+
err(
|
|
170
|
+
`Default export "${exportName}" takes no parameters. It must accept { inputData, onComplete, initialValues }.`
|
|
171
|
+
);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const destructure = paramSrc.match(/\{([^}]*)\}/);
|
|
176
|
+
if (!destructure) {
|
|
177
|
+
err(
|
|
178
|
+
`Default export "${exportName}" does not destructure props. Use { inputData, onComplete, initialValues }.`
|
|
179
|
+
);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const names = destructure[1]
|
|
184
|
+
.split(",")
|
|
185
|
+
.map((s) => s.replace(/[:=].*/, "").trim())
|
|
186
|
+
.filter(Boolean);
|
|
187
|
+
|
|
188
|
+
for (const required of REQUIRED_PROPS) {
|
|
189
|
+
if (!names.includes(required)) {
|
|
190
|
+
err(`Missing required prop "${required}" in default export signature.`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const optional of OPTIONAL_PROPS) {
|
|
195
|
+
if (!names.includes(optional)) {
|
|
196
|
+
warn(
|
|
197
|
+
`Optional prop "${optional}" is not destructured — pre-fill on resume will be unavailable.`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const deprecated of DEPRECATED_PROPS) {
|
|
203
|
+
if (names.includes(deprecated)) {
|
|
204
|
+
err(
|
|
205
|
+
`Deprecated prop "${deprecated}" used. Rename: data→inputData, onSubmit→onComplete.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const known = new Set([
|
|
211
|
+
...REQUIRED_PROPS,
|
|
212
|
+
...OPTIONAL_PROPS,
|
|
213
|
+
...DEPRECATED_PROPS,
|
|
214
|
+
]);
|
|
215
|
+
for (const name of names) {
|
|
216
|
+
if (!known.has(name)) {
|
|
217
|
+
warn(
|
|
218
|
+
`Unknown prop "${name}" in default export signature — will be undefined at runtime.`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 3. Sanity check: onComplete is referenced somewhere in the body.
|
|
225
|
+
if (!/onComplete\s*\(/.test(stripped)) {
|
|
226
|
+
warn(
|
|
227
|
+
"onComplete is destructured but never called. The annotator won't be able to submit."
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 4. Validate imports against the same platform-provided registry used by
|
|
232
|
+
// server-side compile validation.
|
|
233
|
+
checkImports(stripped);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function extractImports(src) {
|
|
237
|
+
const imports = [];
|
|
238
|
+
const importRegex =
|
|
239
|
+
/import\s+(?:type\s+)?([\s\S]*?)\s+from\s+["']([^"']+)["']/g;
|
|
240
|
+
let match;
|
|
241
|
+
|
|
242
|
+
while ((match = importRegex.exec(src)) !== null) {
|
|
243
|
+
const specifier = match[1].trim();
|
|
244
|
+
const source = match[2];
|
|
245
|
+
const line = src.slice(0, match.index).split("\n").length;
|
|
246
|
+
const namedMatch = specifier.match(/\{([\s\S]*?)\}/);
|
|
247
|
+
const namedImports = namedMatch
|
|
248
|
+
? namedMatch[1]
|
|
249
|
+
.split(",")
|
|
250
|
+
.map((part) =>
|
|
251
|
+
part
|
|
252
|
+
.trim()
|
|
253
|
+
.replace(/^type\s+/, "")
|
|
254
|
+
.replace(/\s+as\s+[A-Za-z_$][\w$]*/, "")
|
|
255
|
+
)
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
: [];
|
|
258
|
+
const defaultImport = specifier
|
|
259
|
+
.replace(/\{[\s\S]*?\}/, "")
|
|
260
|
+
.split(",")[0]
|
|
261
|
+
.trim()
|
|
262
|
+
.replace(/^type\s+/, "");
|
|
263
|
+
|
|
264
|
+
imports.push({
|
|
265
|
+
defaultImport: defaultImport || null,
|
|
266
|
+
namedImports,
|
|
267
|
+
source,
|
|
268
|
+
line,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return imports;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function suggestImportPath(source) {
|
|
276
|
+
if (LEGACY_IMPORT_SUGGESTIONS[source]) {
|
|
277
|
+
return LEGACY_IMPORT_SUGGESTIONS[source];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const tail = source.split("/").pop() || "";
|
|
281
|
+
const normalizedTail = tail.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
282
|
+
return Object.keys(AVAILABLE_IMPORTS).find((candidate) => {
|
|
283
|
+
const candidateTail = candidate.split("/").pop() || "";
|
|
284
|
+
return (
|
|
285
|
+
candidateTail.toLowerCase().replace(/[^a-z0-9]/g, "") === normalizedTail
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function checkImports(src) {
|
|
291
|
+
for (const imp of extractImports(src)) {
|
|
292
|
+
if (imp.source === "react") {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (imp.source.startsWith(".")) {
|
|
297
|
+
warn(
|
|
298
|
+
`Relative import from "${imp.source}" (line ${imp.line}) — make sure the referenced file is uploaded or inline.`
|
|
299
|
+
);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const availableNames = AVAILABLE_IMPORTS[imp.source];
|
|
304
|
+
if (!availableNames) {
|
|
305
|
+
const suggestion = suggestImportPath(imp.source);
|
|
306
|
+
err(
|
|
307
|
+
suggestion
|
|
308
|
+
? `Module "${imp.source}" not found (line ${imp.line}). Use "${suggestion}".`
|
|
309
|
+
: `Module "${imp.source}" not found (line ${imp.line}). Only platform-provided registry imports are supported.`
|
|
310
|
+
);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (imp.defaultImport && !availableNames.includes("default")) {
|
|
315
|
+
if (availableNames.includes(imp.defaultImport)) {
|
|
316
|
+
err(
|
|
317
|
+
`Default import "${imp.defaultImport}" not available from "${imp.source}" (line ${imp.line}). Use: import { ${imp.defaultImport} } from "${imp.source}".`
|
|
318
|
+
);
|
|
319
|
+
} else {
|
|
320
|
+
err(
|
|
321
|
+
`Default import "${imp.defaultImport}" not available from "${imp.source}" (line ${imp.line}). Available named exports: ${availableNames.join(", ")}.`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const name of imp.namedImports) {
|
|
327
|
+
if (!availableNames.includes(name)) {
|
|
328
|
+
err(
|
|
329
|
+
`Named import "${name}" not available from "${imp.source}" (line ${imp.line}). Available exports: ${availableNames.join(", ")}.`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function walkSchema(node, path) {
|
|
337
|
+
if (node === null || typeof node !== "object") return;
|
|
338
|
+
if (Array.isArray(node)) {
|
|
339
|
+
node.forEach((item, i) => walkSchema(item, `${path}[${i}]`));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
for (const key of Object.keys(node)) {
|
|
343
|
+
if (!ALLOWED_SCHEMA_KEYS.has(key)) {
|
|
344
|
+
err(
|
|
345
|
+
`Schema at ${path || "<root>"} uses unsupported keyword "${key}". Allowed: ${[
|
|
346
|
+
...ALLOWED_SCHEMA_KEYS,
|
|
347
|
+
].join(", ")}.`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (typeof node.type === "string" && !ALLOWED_TYPES.has(node.type)) {
|
|
352
|
+
err(`Schema at ${path || "<root>"} uses unsupported type "${node.type}".`);
|
|
353
|
+
}
|
|
354
|
+
if (node.properties && typeof node.properties === "object") {
|
|
355
|
+
for (const [k, v] of Object.entries(node.properties)) {
|
|
356
|
+
walkSchema(v, `${path}.properties.${k}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (node.items) {
|
|
360
|
+
walkSchema(node.items, `${path}.items`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function checkSchemaFile(path, label) {
|
|
365
|
+
if (!existsSync(path)) {
|
|
366
|
+
err(`${label} schema not found: ${path}`);
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
let parsed;
|
|
370
|
+
try {
|
|
371
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
372
|
+
} catch (e) {
|
|
373
|
+
err(`${label} schema is not valid JSON: ${e.message}`);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
walkSchema(parsed, "");
|
|
377
|
+
return parsed;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function validatePayload(schema, payload, path) {
|
|
381
|
+
if (schema === null || typeof schema !== "object") return;
|
|
382
|
+
|
|
383
|
+
if (typeof schema.type === "string") {
|
|
384
|
+
const t = schema.type;
|
|
385
|
+
const ok =
|
|
386
|
+
(t === "object" &&
|
|
387
|
+
payload !== null &&
|
|
388
|
+
typeof payload === "object" &&
|
|
389
|
+
!Array.isArray(payload)) ||
|
|
390
|
+
(t === "array" && Array.isArray(payload)) ||
|
|
391
|
+
(t === "string" && typeof payload === "string") ||
|
|
392
|
+
(t === "number" && typeof payload === "number") ||
|
|
393
|
+
(t === "integer" &&
|
|
394
|
+
typeof payload === "number" &&
|
|
395
|
+
Number.isInteger(payload)) ||
|
|
396
|
+
(t === "boolean" && typeof payload === "boolean") ||
|
|
397
|
+
(t === "null" && payload === null);
|
|
398
|
+
if (!ok) {
|
|
399
|
+
const got = Array.isArray(payload)
|
|
400
|
+
? "array"
|
|
401
|
+
: payload === null
|
|
402
|
+
? "null"
|
|
403
|
+
: typeof payload;
|
|
404
|
+
err(`Payload at ${path || "<root>"} expected type ${t}, got ${got}.`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(payload)) {
|
|
410
|
+
err(
|
|
411
|
+
`Payload at ${path || "<root>"} value ${JSON.stringify(payload)} not in enum ${JSON.stringify(schema.enum)}.`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (schema.type === "object" && payload && typeof payload === "object") {
|
|
416
|
+
if (Array.isArray(schema.required)) {
|
|
417
|
+
for (const key of schema.required) {
|
|
418
|
+
if (!(key in payload)) {
|
|
419
|
+
err(`Payload at ${path || "<root>"} missing required key "${key}".`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
424
|
+
for (const [k, sub] of Object.entries(schema.properties)) {
|
|
425
|
+
if (k in payload) validatePayload(sub, payload[k], `${path}.${k}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (schema.type === "array" && Array.isArray(payload) && schema.items) {
|
|
431
|
+
payload.forEach((item, i) =>
|
|
432
|
+
validatePayload(schema.items, item, `${path}[${i}]`)
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function main() {
|
|
438
|
+
const [, , appPath, inputSchemaPath, outputSchemaPath, payloadPath] =
|
|
439
|
+
process.argv;
|
|
440
|
+
|
|
441
|
+
if (!appPath || !inputSchemaPath || !outputSchemaPath) {
|
|
442
|
+
console.error(
|
|
443
|
+
"Usage: node test-app.mjs <App.tsx> <input.json> <output.json> [sample-payload.json]"
|
|
444
|
+
);
|
|
445
|
+
process.exit(2);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
printLine(`Checking app: ${appPath}`);
|
|
449
|
+
checkAppFile(appPath);
|
|
450
|
+
|
|
451
|
+
printLine(`Checking input: ${inputSchemaPath}`);
|
|
452
|
+
checkSchemaFile(inputSchemaPath, "Input");
|
|
453
|
+
|
|
454
|
+
printLine(`Checking output: ${outputSchemaPath}`);
|
|
455
|
+
const outputSchema = checkSchemaFile(outputSchemaPath, "Output");
|
|
456
|
+
|
|
457
|
+
if (payloadPath) {
|
|
458
|
+
printLine(`Validating payload: ${payloadPath}`);
|
|
459
|
+
if (!existsSync(payloadPath)) {
|
|
460
|
+
err(`Sample payload not found: ${payloadPath}`);
|
|
461
|
+
} else {
|
|
462
|
+
try {
|
|
463
|
+
const payload = JSON.parse(readFileSync(payloadPath, "utf8"));
|
|
464
|
+
if (outputSchema) validatePayload(outputSchema, payload, "");
|
|
465
|
+
} catch (e) {
|
|
466
|
+
err(`Sample payload is not valid JSON: ${e.message}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
472
|
+
const warnings = issues.filter((i) => i.level === "warn");
|
|
473
|
+
|
|
474
|
+
if (warnings.length) {
|
|
475
|
+
printLine("");
|
|
476
|
+
for (const w of warnings) printLine(`warn: ${w.message}`);
|
|
477
|
+
}
|
|
478
|
+
if (errors.length) {
|
|
479
|
+
printLine("");
|
|
480
|
+
for (const e of errors) printLine(`error: ${e.message}`);
|
|
481
|
+
printLine(
|
|
482
|
+
`\n${errors.length} error(s), ${warnings.length} warning(s).`
|
|
483
|
+
);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
printLine(
|
|
488
|
+
`\nOK — ${warnings.length} warning(s). Smoke test matches the platform import registry; keep the first label round small.`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
main();
|