@nyxa/nyx-agent 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/README.md +15 -0
- package/dist/cli.js +35 -0
- package/dist/commands/init.js +265 -0
- package/dist/commands/run.js +8 -0
- package/dist/config/loadConfig.js +8 -0
- package/dist/config/schema.js +97 -0
- package/dist/runtime/buildPrompt.js +57 -0
- package/dist/runtime/effectiveConfig.js +14 -0
- package/dist/runtime/files.js +25 -0
- package/dist/runtime/git.js +24 -0
- package/dist/runtime/parseResult.js +38 -0
- package/dist/runtime/paths.js +19 -0
- package/dist/runtime/renderTemplate.js +28 -0
- package/dist/runtime/runPhase.js +244 -0
- package/dist/runtime/runWorkflow.js +158 -0
- package/dist/runtime/time.js +3 -0
- package/dist/runtime/validateResult.js +15 -0
- package/docs/nyxagent-v0-spec.md +488 -0
- package/package.json +37 -0
- package/templates/default/prompts/closure.md +11 -0
- package/templates/default/prompts/execution.md +11 -0
- package/templates/default/prompts/repair-result.md +29 -0
- package/templates/default/prompts/review.md +18 -0
- package/templates/default/prompts/selection.md +19 -0
- package/templates/default/schemas/review.schema.json +60 -0
- package/templates/default/schemas/selection.schema.json +74 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
import { buildPhasePrompt, phaseRequiresStructuredResult } from "./buildPrompt.js";
|
|
4
|
+
import { getEffectiveHarness, getEffectiveModel } from "./effectiveConfig.js";
|
|
5
|
+
import { ensureDir, readText, writeJson, writeText } from "./files.js";
|
|
6
|
+
import { getGitSnapshot } from "./git.js";
|
|
7
|
+
import { parseNyxAgentResult, getOutcome } from "./parseResult.js";
|
|
8
|
+
import { resolveNyxPath } from "./paths.js";
|
|
9
|
+
import { renderTemplate } from "./renderTemplate.js";
|
|
10
|
+
import { validateAgainstSchema } from "./validateResult.js";
|
|
11
|
+
export async function runPhase(input) {
|
|
12
|
+
const phaseDir = path.join(input.iterationDir, "phases", input.phase.id);
|
|
13
|
+
await ensureDir(phaseDir);
|
|
14
|
+
const context = buildContext({ ...input, phaseDir });
|
|
15
|
+
const prompt = await buildPhasePrompt({
|
|
16
|
+
projectRoot: input.projectRoot,
|
|
17
|
+
runDir: input.runDir,
|
|
18
|
+
iterationDir: input.iterationDir,
|
|
19
|
+
phaseDir,
|
|
20
|
+
stateFile: input.stateFile,
|
|
21
|
+
config: input.config,
|
|
22
|
+
phase: input.phase,
|
|
23
|
+
context
|
|
24
|
+
});
|
|
25
|
+
const attempt = await runHarnessAttempt({
|
|
26
|
+
attemptDir: path.join(phaseDir, "attempt-001"),
|
|
27
|
+
prompt,
|
|
28
|
+
context,
|
|
29
|
+
config: input.config,
|
|
30
|
+
phase: input.phase,
|
|
31
|
+
projectRoot: input.projectRoot
|
|
32
|
+
});
|
|
33
|
+
if (attempt.exitCode !== 0) {
|
|
34
|
+
await writeAttemptMeta(attempt, {
|
|
35
|
+
failure: `Harness exited with code ${attempt.exitCode}`
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: `Phase "${input.phase.id}" failed with exit code ${attempt.exitCode}`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!phaseRequiresStructuredResult(input.phase)) {
|
|
43
|
+
await writeAttemptMeta(attempt);
|
|
44
|
+
return { ok: true };
|
|
45
|
+
}
|
|
46
|
+
const parsed = await parseAndValidatePhaseResult({
|
|
47
|
+
input,
|
|
48
|
+
phaseDir,
|
|
49
|
+
attempt
|
|
50
|
+
});
|
|
51
|
+
if (parsed.ok) {
|
|
52
|
+
return parsed;
|
|
53
|
+
}
|
|
54
|
+
await writeAttemptMeta(attempt, {
|
|
55
|
+
result_error: parsed.error
|
|
56
|
+
});
|
|
57
|
+
const repaired = await repairStructuredResult({
|
|
58
|
+
input,
|
|
59
|
+
phaseDir,
|
|
60
|
+
originalPrompt: prompt,
|
|
61
|
+
originalAttempt: attempt,
|
|
62
|
+
validationError: parsed.error
|
|
63
|
+
});
|
|
64
|
+
if (repaired.ok) {
|
|
65
|
+
return repaired;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: repaired.error
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function buildContext(input) {
|
|
73
|
+
const model = getEffectiveModel(input.config, input.phase);
|
|
74
|
+
const harness = getEffectiveHarness(input.config, input.phase);
|
|
75
|
+
return {
|
|
76
|
+
project_root: input.projectRoot,
|
|
77
|
+
run_dir: input.runDir,
|
|
78
|
+
iteration_dir: input.iterationDir,
|
|
79
|
+
phase_dir: input.phaseDir,
|
|
80
|
+
state_file: input.stateFile,
|
|
81
|
+
phase: input.phase,
|
|
82
|
+
workflow: input.config.workflow,
|
|
83
|
+
work_items: input.config.work_items ?? {},
|
|
84
|
+
work_item: input.state.work_item ?? {},
|
|
85
|
+
seen_work_item_keys: input.state.seen_work_item_keys ?? [],
|
|
86
|
+
phase_results: input.state.phase_results ?? {},
|
|
87
|
+
state: input.state,
|
|
88
|
+
model,
|
|
89
|
+
harness
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function runHarnessAttempt(input) {
|
|
93
|
+
await ensureDir(input.attemptDir);
|
|
94
|
+
await writeText(path.join(input.attemptDir, "prompt.md"), input.prompt);
|
|
95
|
+
const harness = getEffectiveHarness(input.config, input.phase);
|
|
96
|
+
if (harness.prompt_input !== "stdin") {
|
|
97
|
+
throw new Error(`Unsupported prompt_input "${harness.prompt_input}"`);
|
|
98
|
+
}
|
|
99
|
+
const command = renderTemplate(harness.command, input.context);
|
|
100
|
+
const args = harness.args.map((arg) => renderTemplate(arg, input.context));
|
|
101
|
+
const startedAt = new Date().toISOString();
|
|
102
|
+
const started = Date.now();
|
|
103
|
+
const gitBefore = await getGitSnapshot(input.projectRoot);
|
|
104
|
+
let stdout = "";
|
|
105
|
+
let stderr = "";
|
|
106
|
+
let exitCode = 0;
|
|
107
|
+
try {
|
|
108
|
+
const result = await execa(command, args, {
|
|
109
|
+
cwd: input.projectRoot,
|
|
110
|
+
input: input.prompt,
|
|
111
|
+
reject: false
|
|
112
|
+
});
|
|
113
|
+
stdout = result.stdout;
|
|
114
|
+
stderr = result.stderr;
|
|
115
|
+
exitCode = result.exitCode ?? 0;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
exitCode = 127;
|
|
119
|
+
stderr = error instanceof Error ? error.message : String(error);
|
|
120
|
+
}
|
|
121
|
+
const endedAt = new Date().toISOString();
|
|
122
|
+
const durationMs = Date.now() - started;
|
|
123
|
+
const gitAfter = await getGitSnapshot(input.projectRoot);
|
|
124
|
+
await writeText(path.join(input.attemptDir, "stdout.log"), stdout);
|
|
125
|
+
await writeText(path.join(input.attemptDir, "stderr.log"), stderr);
|
|
126
|
+
const attempt = {
|
|
127
|
+
attemptDir: input.attemptDir,
|
|
128
|
+
metaPath: path.join(input.attemptDir, "meta.json"),
|
|
129
|
+
stdout,
|
|
130
|
+
stderr,
|
|
131
|
+
exitCode,
|
|
132
|
+
command,
|
|
133
|
+
args,
|
|
134
|
+
startedAt,
|
|
135
|
+
endedAt,
|
|
136
|
+
durationMs,
|
|
137
|
+
gitBefore,
|
|
138
|
+
gitAfter
|
|
139
|
+
};
|
|
140
|
+
await writeAttemptMeta(attempt);
|
|
141
|
+
return attempt;
|
|
142
|
+
}
|
|
143
|
+
async function writeAttemptMeta(attempt, extra = {}) {
|
|
144
|
+
await writeJson(attempt.metaPath, {
|
|
145
|
+
command: attempt.command,
|
|
146
|
+
args: attempt.args,
|
|
147
|
+
started_at: attempt.startedAt,
|
|
148
|
+
ended_at: attempt.endedAt,
|
|
149
|
+
duration_ms: attempt.durationMs,
|
|
150
|
+
exit_code: attempt.exitCode,
|
|
151
|
+
git_before: attempt.gitBefore,
|
|
152
|
+
git_after: attempt.gitAfter,
|
|
153
|
+
...extra
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function parseAndValidatePhaseResult(input) {
|
|
157
|
+
const parsed = parseNyxAgentResult(input.attempt.stdout);
|
|
158
|
+
if (!parsed.ok) {
|
|
159
|
+
return parsed;
|
|
160
|
+
}
|
|
161
|
+
if (input.input.phase.output_schema) {
|
|
162
|
+
const schemaPath = resolveNyxPath(input.input.projectRoot, input.input.phase.output_schema, `output_schema for phase "${input.input.phase.id}"`);
|
|
163
|
+
const validation = await validateAgainstSchema(schemaPath, parsed.value);
|
|
164
|
+
if (!validation.ok) {
|
|
165
|
+
return validation;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (input.input.phase.transitions && !getOutcome(parsed.value)) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: `Phase "${input.input.phase.id}" has transitions but result has no string outcome`
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
await writeJson(path.join(input.phaseDir, "result.json"), parsed.value);
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
result: parsed.value,
|
|
178
|
+
outcome: getOutcome(parsed.value)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function repairStructuredResult(input) {
|
|
182
|
+
const maxAttempts = input.input.config.repair.max_attempts;
|
|
183
|
+
if (maxAttempts <= 0) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
error: input.validationError
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const repairPromptTemplate = await readText(resolveNyxPath(input.input.projectRoot, input.input.config.repair.prompt, "repair prompt"));
|
|
190
|
+
let lastError = input.validationError;
|
|
191
|
+
for (let attemptNumber = 1; attemptNumber <= maxAttempts; attemptNumber += 1) {
|
|
192
|
+
const repairContext = buildContext({
|
|
193
|
+
...input.input,
|
|
194
|
+
phaseDir: input.phaseDir
|
|
195
|
+
});
|
|
196
|
+
const renderedRepairPrompt = renderTemplate(repairPromptTemplate, {
|
|
197
|
+
...repairContext,
|
|
198
|
+
original_prompt: input.originalPrompt,
|
|
199
|
+
original_stdout: input.originalAttempt.stdout,
|
|
200
|
+
original_stderr: input.originalAttempt.stderr,
|
|
201
|
+
validation_error: lastError
|
|
202
|
+
});
|
|
203
|
+
const repairPrompt = [
|
|
204
|
+
"# NyxAgent Result Repair",
|
|
205
|
+
"",
|
|
206
|
+
"Repair only the structured result for the previous phase attempt.",
|
|
207
|
+
"Do not modify project files. Do not redo the phase work.",
|
|
208
|
+
"Return a valid result inside the final <nyxagent_result> block.",
|
|
209
|
+
"",
|
|
210
|
+
renderedRepairPrompt
|
|
211
|
+
].join("\n");
|
|
212
|
+
const repairAttempt = await runHarnessAttempt({
|
|
213
|
+
attemptDir: path.join(input.phaseDir, `repair-${String(attemptNumber).padStart(3, "0")}`),
|
|
214
|
+
prompt: repairPrompt,
|
|
215
|
+
context: repairContext,
|
|
216
|
+
config: input.input.config,
|
|
217
|
+
phase: input.input.phase,
|
|
218
|
+
projectRoot: input.input.projectRoot
|
|
219
|
+
});
|
|
220
|
+
if (repairAttempt.exitCode !== 0) {
|
|
221
|
+
lastError = `Repair harness exited with code ${repairAttempt.exitCode}`;
|
|
222
|
+
await writeAttemptMeta(repairAttempt, {
|
|
223
|
+
failure: lastError
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const repaired = await parseAndValidatePhaseResult({
|
|
228
|
+
input: input.input,
|
|
229
|
+
phaseDir: input.phaseDir,
|
|
230
|
+
attempt: repairAttempt
|
|
231
|
+
});
|
|
232
|
+
if (repaired.ok) {
|
|
233
|
+
return repaired;
|
|
234
|
+
}
|
|
235
|
+
lastError = repaired.error;
|
|
236
|
+
await writeAttemptMeta(repairAttempt, {
|
|
237
|
+
result_error: repaired.error
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: lastError
|
|
243
|
+
};
|
|
244
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { loadConfig } from "../config/loadConfig.js";
|
|
4
|
+
import { ensureDir, writeJson } from "./files.js";
|
|
5
|
+
import { getGitSnapshot } from "./git.js";
|
|
6
|
+
import { getNyxDir, relativeToProject } from "./paths.js";
|
|
7
|
+
import { runPhase } from "./runPhase.js";
|
|
8
|
+
import { createRunId } from "./time.js";
|
|
9
|
+
export async function runWorkflow(options) {
|
|
10
|
+
const projectRoot = path.resolve(options.projectRoot);
|
|
11
|
+
const nyxDir = getNyxDir(projectRoot);
|
|
12
|
+
const configPath = options.configPath ?? path.join(nyxDir, "config.toml");
|
|
13
|
+
const config = await loadConfig(configPath);
|
|
14
|
+
const runId = createRunId();
|
|
15
|
+
const runDir = path.join(nyxDir, "runs", runId);
|
|
16
|
+
await ensureDir(runDir);
|
|
17
|
+
const git = await getGitSnapshot(projectRoot);
|
|
18
|
+
const runState = {
|
|
19
|
+
run_id: runId,
|
|
20
|
+
status: "running",
|
|
21
|
+
current_iteration: 0,
|
|
22
|
+
completed_iterations: 0,
|
|
23
|
+
seen_work_item_keys: []
|
|
24
|
+
};
|
|
25
|
+
await writeJson(path.join(runDir, "run.json"), {
|
|
26
|
+
run_id: runId,
|
|
27
|
+
project_root: projectRoot,
|
|
28
|
+
started_at: new Date().toISOString(),
|
|
29
|
+
config_path: relativeToProject(projectRoot, configPath),
|
|
30
|
+
harness: {
|
|
31
|
+
preset: config.harness.preset,
|
|
32
|
+
command: config.harness.command
|
|
33
|
+
},
|
|
34
|
+
git
|
|
35
|
+
});
|
|
36
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
37
|
+
console.log(pc.bold(`NyxAgent run ${runId}`));
|
|
38
|
+
console.log(`Project: ${projectRoot}`);
|
|
39
|
+
console.log(`Harness: ${config.harness.preset ?? "custom"} (${config.harness.command})`);
|
|
40
|
+
console.log(`Model: ${config.model.name}`);
|
|
41
|
+
console.log("");
|
|
42
|
+
const phasesById = new Map(config.phases.map((phase) => [phase.id, phase]));
|
|
43
|
+
for (let iterationNumber = 1; iterationNumber <= config.workflow.max_iterations; iterationNumber += 1) {
|
|
44
|
+
runState.current_iteration = iterationNumber;
|
|
45
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
46
|
+
const iterationDir = path.join(runDir, "iterations", String(iterationNumber).padStart(3, "0"));
|
|
47
|
+
await ensureDir(iterationDir);
|
|
48
|
+
const iterationState = {
|
|
49
|
+
iteration: iterationNumber,
|
|
50
|
+
status: "running",
|
|
51
|
+
seen_work_item_keys: [...runState.seen_work_item_keys],
|
|
52
|
+
phase_results: {},
|
|
53
|
+
phase_visit_counts: {}
|
|
54
|
+
};
|
|
55
|
+
const iterationStateFile = path.join(iterationDir, "state.json");
|
|
56
|
+
await writeJson(iterationStateFile, iterationState);
|
|
57
|
+
let currentPhaseId = config.workflow.entry_phase;
|
|
58
|
+
while (currentPhaseId) {
|
|
59
|
+
const phase = phasesById.get(currentPhaseId);
|
|
60
|
+
if (!phase) {
|
|
61
|
+
throw new Error(`Unknown phase "${currentPhaseId}"`);
|
|
62
|
+
}
|
|
63
|
+
const visits = (iterationState.phase_visit_counts[phase.id] ?? 0) + 1;
|
|
64
|
+
iterationState.phase_visit_counts[phase.id] = visits;
|
|
65
|
+
if (visits > phase.max_visits_per_iteration) {
|
|
66
|
+
iterationState.status = "failed";
|
|
67
|
+
runState.status = "failed";
|
|
68
|
+
await writeJson(iterationStateFile, iterationState);
|
|
69
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
70
|
+
throw new Error(`Phase "${phase.id}" exceeded max_visits_per_iteration=${phase.max_visits_per_iteration}`);
|
|
71
|
+
}
|
|
72
|
+
process.stdout.write(`[${iterationNumber}/${config.workflow.max_iterations}] ${phase.id} ... `);
|
|
73
|
+
await writeJson(iterationStateFile, iterationState);
|
|
74
|
+
const phaseResult = await runPhase({
|
|
75
|
+
projectRoot,
|
|
76
|
+
runDir,
|
|
77
|
+
iterationDir,
|
|
78
|
+
stateFile: iterationStateFile,
|
|
79
|
+
config,
|
|
80
|
+
phase,
|
|
81
|
+
state: iterationState
|
|
82
|
+
});
|
|
83
|
+
if (!phaseResult.ok) {
|
|
84
|
+
console.log(pc.red("failed"));
|
|
85
|
+
iterationState.status = "failed";
|
|
86
|
+
iterationState.phase_results[phase.id] = {
|
|
87
|
+
status: "failed",
|
|
88
|
+
error: phaseResult.error
|
|
89
|
+
};
|
|
90
|
+
runState.status = "failed";
|
|
91
|
+
await writeJson(iterationStateFile, iterationState);
|
|
92
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
93
|
+
throw new Error(phaseResult.error);
|
|
94
|
+
}
|
|
95
|
+
iterationState.phase_results[phase.id] =
|
|
96
|
+
phaseResult.result ?? { status: "ok" };
|
|
97
|
+
const workItem = readObjectProperty(phaseResult.result, "work_item");
|
|
98
|
+
if (workItem !== undefined) {
|
|
99
|
+
iterationState.work_item = workItem;
|
|
100
|
+
const key = readObjectProperty(workItem, "key");
|
|
101
|
+
if (typeof key === "string" && !runState.seen_work_item_keys.includes(key)) {
|
|
102
|
+
runState.seen_work_item_keys.push(key);
|
|
103
|
+
iterationState.seen_work_item_keys = [...runState.seen_work_item_keys];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const nextTarget = resolveNextTarget(phase, phaseResult.outcome);
|
|
107
|
+
const label = phaseResult.outcome ?? "ok";
|
|
108
|
+
console.log(pc.green(label));
|
|
109
|
+
await writeJson(iterationStateFile, iterationState);
|
|
110
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
111
|
+
if (nextTarget === "stop_run") {
|
|
112
|
+
iterationState.status = phaseResult.outcome === "no_work" ? "no_work" : "stopped";
|
|
113
|
+
runState.status =
|
|
114
|
+
phaseResult.outcome === "no_work" ? "completed_no_work" : "stopped";
|
|
115
|
+
await writeJson(iterationStateFile, iterationState);
|
|
116
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
117
|
+
console.log("");
|
|
118
|
+
console.log(`Done: ${runState.status}`);
|
|
119
|
+
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (nextTarget === "stop_iteration" || nextTarget === "next_iteration") {
|
|
123
|
+
iterationState.status = "completed";
|
|
124
|
+
runState.completed_iterations += 1;
|
|
125
|
+
await writeJson(iterationStateFile, iterationState);
|
|
126
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
currentPhaseId = nextTarget;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
runState.status = "completed_max_iterations";
|
|
133
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
134
|
+
console.log("");
|
|
135
|
+
console.log(`Done: ${runState.status}`);
|
|
136
|
+
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
137
|
+
}
|
|
138
|
+
function resolveNextTarget(phase, outcome) {
|
|
139
|
+
if (phase.transitions) {
|
|
140
|
+
if (!outcome) {
|
|
141
|
+
throw new Error(`Phase "${phase.id}" requires an outcome`);
|
|
142
|
+
}
|
|
143
|
+
const target = phase.transitions[outcome];
|
|
144
|
+
if (!target) {
|
|
145
|
+
throw new Error(`Phase "${phase.id}" returned unknown outcome "${outcome}"`);
|
|
146
|
+
}
|
|
147
|
+
return target;
|
|
148
|
+
}
|
|
149
|
+
return phase.next;
|
|
150
|
+
}
|
|
151
|
+
function readObjectProperty(value, key) {
|
|
152
|
+
if (value &&
|
|
153
|
+
typeof value === "object" &&
|
|
154
|
+
Object.prototype.hasOwnProperty.call(value, key)) {
|
|
155
|
+
return value[key];
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
3
|
+
export async function validateAgainstSchema(schemaPath, value) {
|
|
4
|
+
const schema = JSON.parse(await readFile(schemaPath, "utf8"));
|
|
5
|
+
const ajv = new Ajv2020({ allErrors: true });
|
|
6
|
+
const validate = ajv.compile(schema);
|
|
7
|
+
const valid = validate(value);
|
|
8
|
+
if (valid) {
|
|
9
|
+
return { ok: true };
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
error: ajv.errorsText(validate.errors, { separator: "\n" })
|
|
14
|
+
};
|
|
15
|
+
}
|