intentdna 1.6.3 → 1.6.5
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/generate.js +5 -33
- package/dist/cli/commands/sync.js +27 -10
- package/dist/hooks/cli.js +2 -4
- package/dist/hooks/event-registry.d.ts +17 -0
- package/dist/hooks/event-registry.js +89 -0
- package/dist/hooks/protocol.d.ts +1 -1
- package/dist/hooks/schema.js +5 -1
- package/dist/mcp/tools-enforce.js +2 -4
- package/dist/runtime/markdown-target-registry.d.ts +17 -0
- package/dist/runtime/markdown-target-registry.js +37 -0
- package/dist/runtime/markdown.d.ts +2 -1
- package/dist/runtime/markdown.js +9 -12
- package/dist/runtime/output-artifact-registry.d.ts +11 -0
- package/dist/runtime/output-artifact-registry.js +30 -0
- package/dist/runtime/settings-adapter.d.ts +2 -1
- package/dist/runtime/settings-adapter.js +4 -12
- package/dist/runtime/skill-adapter.js +105 -37
- package/dist/schema/controller-registry.d.ts +29 -0
- package/dist/schema/controller-registry.js +35 -0
- package/dist/schema/types.d.ts +12 -2
- package/dist/schema/validate.js +1 -93
- package/dist/schema/validators/controllers.d.ts +3 -0
- package/dist/schema/validators/controllers.js +140 -0
- package/dist/templates/flutter-rewrite.dna.yaml +53 -4
- package/dist/templates/metadata.d.ts +6 -0
- package/dist/templates/metadata.js +32 -0
- package/package.json +1 -1
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { mkdir, writeFile, readdir, readFile, rm } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { getControllerKindMetadata } from "../schema/controller-registry.js";
|
|
11
12
|
import { toKebabCase } from "./agent-md.js";
|
|
12
13
|
const SENTINEL = "<!-- intentdna:managed — do not edit manually -->";
|
|
13
14
|
// ── Compile Controller → Skill ─────────────────────────────
|
|
14
15
|
export function compileControllerToSkill(controllerKey, controller, variables, workflows) {
|
|
15
|
-
|
|
16
|
+
const kindMetadata = getControllerKindMetadata(controller.kind);
|
|
17
|
+
if (!kindMetadata?.supportsSkillGeneration) {
|
|
16
18
|
throw new Error(`Unsupported controller kind: ${controller.kind}`);
|
|
17
19
|
}
|
|
18
20
|
const skillName = `dna-${toKebabCase(controllerKey)}`;
|
|
@@ -29,6 +31,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
29
31
|
const reviewChangesStep = requireStepByRole(fixWorkflow, controller.roles.fix_reviewer, controller.fix_workflow);
|
|
30
32
|
const verifyReportStep = requireStepByRole(fixWorkflow, controller.roles.test_runner, controller.fix_workflow);
|
|
31
33
|
const stepPrompt = (step) => step.prompt || step.description || "";
|
|
34
|
+
const policy = normalizeControllerPolicy(controller);
|
|
32
35
|
const lines = [];
|
|
33
36
|
lines.push("---");
|
|
34
37
|
lines.push(`name: ${skillName}`);
|
|
@@ -49,7 +52,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
49
52
|
lines.push("</Purpose>");
|
|
50
53
|
lines.push("");
|
|
51
54
|
lines.push("<Controller>");
|
|
52
|
-
lines.push(
|
|
55
|
+
lines.push(`kind: ${controller.kind}`);
|
|
53
56
|
lines.push(`max_rounds: ${controller.max_rounds}`);
|
|
54
57
|
lines.push(`diagnosis_workflow: ${controller.diagnosis_workflow}`);
|
|
55
58
|
lines.push(`fix_workflow: ${controller.fix_workflow}`);
|
|
@@ -59,7 +62,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
59
62
|
lines.push("<Steps>");
|
|
60
63
|
lines.push("0. Validate the module argument before using it in any artifact path.");
|
|
61
64
|
lines.push(" - The module argument must match /^[A-Za-z0-9_-]+$/.");
|
|
62
|
-
lines.push(" - If it contains '/', '.', '..', spaces, braces, glob characters, or path separators, stop with the
|
|
65
|
+
lines.push(" - If it contains '/', '.', '..', spaces, braces, glob characters, or path separators, stop with the configured stop report.");
|
|
63
66
|
lines.push(" - Do not read or write controller artifacts until the module argument is safe.");
|
|
64
67
|
lines.push("1. Check diagnosis artifact freshness before any fix round.");
|
|
65
68
|
lines.push(` - Diagnosis artifact: ${controller.diagnosis_artifact_path}`);
|
|
@@ -68,12 +71,9 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
68
71
|
lines.push(" - Reuse the same controller_run_id in every child Agent prompt and in every progress validation.");
|
|
69
72
|
lines.push(" - Do not regenerate controller_run_id inside fix rounds.");
|
|
70
73
|
lines.push("3. Reuse diagnosis only when all diagnosis reuse criteria pass:");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
lines.push(" - baseline artifact is not newer than diagnosis spec");
|
|
75
|
-
lines.push(" - user did not explicitly pass reanalyze or scope changed");
|
|
76
|
-
lines.push(" - progress JSON does not say diagnosis_invalid");
|
|
74
|
+
for (const criterion of policy.diagnosis_reuse_criteria) {
|
|
75
|
+
lines.push(` - ${criterion}`);
|
|
76
|
+
}
|
|
77
77
|
lines.push(`4. If diagnosis is missing, invalid, stale, or scope-changed, run diagnosis once with fresh context and sequential gates:`);
|
|
78
78
|
lines.push(" a. Spawn fresh analyzer agent with controller_run_id, then wait for completion.");
|
|
79
79
|
lines.push(" ```");
|
|
@@ -91,7 +91,7 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
91
91
|
lines.push(" )");
|
|
92
92
|
lines.push(" ```");
|
|
93
93
|
lines.push(` d. Diagnosis review JSON gate: read ${controller.diagnosis_review_artifact_path}; require existing valid JSON with verdict APPROVE before fix rounds.`);
|
|
94
|
-
lines.push(" If the review JSON is missing, invalid, or verdict is not APPROVE, stop immediately with the
|
|
94
|
+
lines.push(" If the review JSON is missing, invalid, or verdict is not APPROVE, stop immediately with the configured stop report; do NOT enter fix rounds.");
|
|
95
95
|
lines.push(`5. For round 1..${controller.max_rounds}, set current_round to the loop number and run one fresh context fix round with sequential gates:`);
|
|
96
96
|
lines.push(" a. Spawn fresh surgeon agent with controller_run_id and current_round, then wait for completion.");
|
|
97
97
|
lines.push(" ```");
|
|
@@ -118,43 +118,33 @@ export function compileControllerToSkill(controllerKey, controller, variables, w
|
|
|
118
118
|
lines.push(" ```");
|
|
119
119
|
lines.push(` f. Progress JSON read/provenance validation: read ${controller.progress_artifact_path}. The controller must read this JSON sidecar, not Markdown prose.`);
|
|
120
120
|
lines.push(" g. Verify progress JSON provenance before branching:");
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
lines.push(` -
|
|
125
|
-
lines.push(` - review_artifact equals ${controller.review_artifact_path}`);
|
|
126
|
-
lines.push(" - git_head or last_fix_commit references the current round's fix commit when available");
|
|
127
|
-
lines.push(" - updated_at is not earlier than the current round start time");
|
|
128
|
-
lines.push(" - if provenance is missing, stale, or mismatched, stop with Verdict BLOCKED and failure_reason PROGRESS_PROVENANCE_MISMATCH");
|
|
121
|
+
for (const check of policy.progress_provenance_checks) {
|
|
122
|
+
lines.push(` - ${check}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push(` - ${policy.progress_provenance_failure}`);
|
|
129
125
|
lines.push(" h. Branch only from the JSON verdict:");
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
lines.push(
|
|
134
|
-
lines.push(`6. If max ${controller.max_rounds} rounds is reached, stop with Chinese report.`);
|
|
126
|
+
for (const branch of policy.verdict_branches) {
|
|
127
|
+
lines.push(` - ${branch}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push(`6. If max ${controller.max_rounds} rounds is reached, stop with the configured stop report.`);
|
|
135
130
|
lines.push("</Steps>");
|
|
136
131
|
lines.push("");
|
|
137
132
|
lines.push("<Verdict_Mapping>");
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
lines.push("- REQUEST_CHANGES: stop, report reviewer/verifier requested changes");
|
|
142
|
-
lines.push("- BLOCKED: stop, report blocker");
|
|
143
|
-
lines.push("- MAX_ROUNDS: stop, report max rounds reached and next focus");
|
|
133
|
+
for (const line of policy.verdict_mapping) {
|
|
134
|
+
lines.push(line);
|
|
135
|
+
}
|
|
144
136
|
lines.push("</Verdict_Mapping>");
|
|
145
137
|
lines.push("");
|
|
146
138
|
lines.push("<Stop_Report>");
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
lines.push("下一步:...");
|
|
139
|
+
for (const line of policy.stop_report) {
|
|
140
|
+
lines.push(line);
|
|
141
|
+
}
|
|
151
142
|
lines.push("</Stop_Report>");
|
|
152
143
|
lines.push("");
|
|
153
144
|
lines.push("<Constraints>");
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
lines.push("- Do not use the generic workflow policy block; branch only from the progress JSON verdict.");
|
|
145
|
+
for (const constraint of policy.constraints) {
|
|
146
|
+
lines.push(`- ${constraint}`);
|
|
147
|
+
}
|
|
158
148
|
lines.push("</Constraints>");
|
|
159
149
|
lines.push("");
|
|
160
150
|
let content = lines.join("\n") + "\n";
|
|
@@ -174,6 +164,84 @@ function requireStepByRole(workflow, role, workflowName) {
|
|
|
174
164
|
}
|
|
175
165
|
return step;
|
|
176
166
|
}
|
|
167
|
+
function normalizeControllerPolicy(controller) {
|
|
168
|
+
const defaults = defaultControllerPolicy(controller);
|
|
169
|
+
const policy = {
|
|
170
|
+
diagnosis_reuse_criteria: controller.policy?.diagnosis_reuse_criteria ?? defaults.diagnosis_reuse_criteria,
|
|
171
|
+
progress_provenance_checks: controller.policy?.progress_provenance_checks ?? defaults.progress_provenance_checks,
|
|
172
|
+
progress_provenance_failure: controller.policy?.progress_provenance_failure ?? defaults.progress_provenance_failure,
|
|
173
|
+
verdict_branches: controller.policy?.verdict_branches ?? defaults.verdict_branches,
|
|
174
|
+
verdict_mapping: controller.policy?.verdict_mapping ?? defaults.verdict_mapping,
|
|
175
|
+
stop_report: controller.policy?.stop_report ?? defaults.stop_report,
|
|
176
|
+
constraints: controller.policy?.constraints ?? defaults.constraints,
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
diagnosis_reuse_criteria: renderControllerPolicyLines(policy.diagnosis_reuse_criteria, controller),
|
|
180
|
+
progress_provenance_checks: renderControllerPolicyLines(policy.progress_provenance_checks, controller),
|
|
181
|
+
progress_provenance_failure: renderControllerPolicyText(policy.progress_provenance_failure, controller),
|
|
182
|
+
verdict_branches: renderControllerPolicyLines(policy.verdict_branches, controller),
|
|
183
|
+
verdict_mapping: renderControllerPolicyLines(policy.verdict_mapping, controller),
|
|
184
|
+
stop_report: renderControllerPolicyLines(policy.stop_report, controller),
|
|
185
|
+
constraints: renderControllerPolicyLines(policy.constraints, controller),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function renderControllerPolicyLines(lines, controller) {
|
|
189
|
+
return lines.map((line) => renderControllerPolicyText(line, controller));
|
|
190
|
+
}
|
|
191
|
+
function renderControllerPolicyText(text, controller) {
|
|
192
|
+
const replacements = {
|
|
193
|
+
"$progress_artifact_path": controller.progress_artifact_path,
|
|
194
|
+
"$diagnosis_artifact_path": controller.diagnosis_artifact_path,
|
|
195
|
+
"$diagnosis_review_artifact_path": controller.diagnosis_review_artifact_path,
|
|
196
|
+
"$review_artifact_path": controller.review_artifact_path,
|
|
197
|
+
};
|
|
198
|
+
let rendered = text;
|
|
199
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
200
|
+
rendered = rendered.replaceAll(token, value);
|
|
201
|
+
}
|
|
202
|
+
return rendered;
|
|
203
|
+
}
|
|
204
|
+
function defaultControllerPolicy(controller) {
|
|
205
|
+
return {
|
|
206
|
+
diagnosis_reuse_criteria: [
|
|
207
|
+
`${controller.diagnosis_artifact_path} exists`,
|
|
208
|
+
`${controller.diagnosis_review_artifact_path} exists and verdict is APPROVE`,
|
|
209
|
+
"the user did not explicitly request reanalysis",
|
|
210
|
+
"progress JSON does not invalidate the diagnosis",
|
|
211
|
+
],
|
|
212
|
+
progress_provenance_checks: [
|
|
213
|
+
"module equals the safe module argument",
|
|
214
|
+
"round equals the provided current_round",
|
|
215
|
+
"run_id equals the provided controller_run_id",
|
|
216
|
+
`diagnosis_artifact equals ${controller.diagnosis_artifact_path}`,
|
|
217
|
+
`review_artifact equals ${controller.review_artifact_path}`,
|
|
218
|
+
"git_head or last_fix_commit references the current round's fix commit when available",
|
|
219
|
+
"updated_at is not earlier than the current round start time",
|
|
220
|
+
],
|
|
221
|
+
progress_provenance_failure: "if provenance is missing, stale, or mismatched, stop with Verdict BLOCKED and failure_reason PROGRESS_PROVENANCE_MISMATCH",
|
|
222
|
+
verdict_branches: [
|
|
223
|
+
"PASS -> success stop",
|
|
224
|
+
"CONTINUE -> next fresh context fix round",
|
|
225
|
+
"REQUEST_CHANGES -> stop with the configured stop report",
|
|
226
|
+
"BLOCKED -> stop with the configured stop report",
|
|
227
|
+
],
|
|
228
|
+
verdict_mapping: [
|
|
229
|
+
"PASS / CONTINUE / REQUEST_CHANGES / BLOCKED mapping:",
|
|
230
|
+
"- PASS: success stop",
|
|
231
|
+
"- CONTINUE: start the next fresh context round",
|
|
232
|
+
"- REQUEST_CHANGES: stop and report the requested changes",
|
|
233
|
+
"- BLOCKED: stop and report the blocker",
|
|
234
|
+
"- MAX_ROUNDS: stop and report max rounds reached",
|
|
235
|
+
],
|
|
236
|
+
stop_report: [
|
|
237
|
+
"On REQUEST_CHANGES, BLOCKED, or MAX_ROUNDS, explain the stop reason, the evidence, and the next action.",
|
|
238
|
+
],
|
|
239
|
+
constraints: [
|
|
240
|
+
`The controller must schedule only from ${controller.progress_artifact_path}.`,
|
|
241
|
+
"Branch only from the progress JSON verdict.",
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
177
245
|
// ── Compile Workflow → Skill ───────────────────────────────
|
|
178
246
|
/**
|
|
179
247
|
* Compile a WorkflowPlan + roles + IR into a SKILL.md file.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ControllerKind } from "./types.js";
|
|
2
|
+
export declare const CONTROLLER_ROLE_KEYS: readonly ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
|
|
3
|
+
export type ControllerRoleKey = typeof CONTROLLER_ROLE_KEYS[number];
|
|
4
|
+
export interface ControllerKindMetadata {
|
|
5
|
+
kind: ControllerKind;
|
|
6
|
+
label: string;
|
|
7
|
+
supportsSkillGeneration: boolean;
|
|
8
|
+
requiredRoleKeys: readonly ControllerRoleKey[];
|
|
9
|
+
diagnosisRoleKeys: readonly ControllerRoleKey[];
|
|
10
|
+
fixRoleKeys: readonly ControllerRoleKey[];
|
|
11
|
+
}
|
|
12
|
+
export declare const CONTROLLER_KIND_REGISTRY: readonly [{
|
|
13
|
+
readonly kind: "fix_loop";
|
|
14
|
+
readonly label: "Fix loop";
|
|
15
|
+
readonly supportsSkillGeneration: true;
|
|
16
|
+
readonly requiredRoleKeys: readonly ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
|
|
17
|
+
readonly diagnosisRoleKeys: readonly ["analyzer", "analysis_reviewer"];
|
|
18
|
+
readonly fixRoleKeys: readonly ["surgeon", "fix_reviewer", "test_runner"];
|
|
19
|
+
}, {
|
|
20
|
+
readonly kind: "flutter_rewrite_fix_loop";
|
|
21
|
+
readonly label: "Flutter rewrite fix loop";
|
|
22
|
+
readonly supportsSkillGeneration: true;
|
|
23
|
+
readonly requiredRoleKeys: readonly ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
|
|
24
|
+
readonly diagnosisRoleKeys: readonly ["analyzer", "analysis_reviewer"];
|
|
25
|
+
readonly fixRoleKeys: readonly ["surgeon", "fix_reviewer", "test_runner"];
|
|
26
|
+
}];
|
|
27
|
+
export declare function getControllerKindMetadata(kind: unknown): ControllerKindMetadata | undefined;
|
|
28
|
+
export declare function isSupportedControllerKind(kind: unknown): kind is ControllerKind;
|
|
29
|
+
export declare function supportedControllerKinds(): ControllerKind[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const CONTROLLER_ROLE_KEYS = [
|
|
2
|
+
"analyzer",
|
|
3
|
+
"analysis_reviewer",
|
|
4
|
+
"surgeon",
|
|
5
|
+
"fix_reviewer",
|
|
6
|
+
"test_runner",
|
|
7
|
+
];
|
|
8
|
+
export const CONTROLLER_KIND_REGISTRY = [
|
|
9
|
+
{
|
|
10
|
+
kind: "fix_loop",
|
|
11
|
+
label: "Fix loop",
|
|
12
|
+
supportsSkillGeneration: true,
|
|
13
|
+
requiredRoleKeys: CONTROLLER_ROLE_KEYS,
|
|
14
|
+
diagnosisRoleKeys: ["analyzer", "analysis_reviewer"],
|
|
15
|
+
fixRoleKeys: ["surgeon", "fix_reviewer", "test_runner"],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
kind: "flutter_rewrite_fix_loop",
|
|
19
|
+
label: "Flutter rewrite fix loop",
|
|
20
|
+
supportsSkillGeneration: true,
|
|
21
|
+
requiredRoleKeys: CONTROLLER_ROLE_KEYS,
|
|
22
|
+
diagnosisRoleKeys: ["analyzer", "analysis_reviewer"],
|
|
23
|
+
fixRoleKeys: ["surgeon", "fix_reviewer", "test_runner"],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const CONTROLLER_KIND_BY_ID = new Map(CONTROLLER_KIND_REGISTRY.map((entry) => [entry.kind, entry]));
|
|
27
|
+
export function getControllerKindMetadata(kind) {
|
|
28
|
+
return typeof kind === "string" ? CONTROLLER_KIND_BY_ID.get(kind) : undefined;
|
|
29
|
+
}
|
|
30
|
+
export function isSupportedControllerKind(kind) {
|
|
31
|
+
return getControllerKindMetadata(kind) !== undefined;
|
|
32
|
+
}
|
|
33
|
+
export function supportedControllerKinds() {
|
|
34
|
+
return CONTROLLER_KIND_REGISTRY.map((entry) => entry.kind);
|
|
35
|
+
}
|
package/dist/schema/types.d.ts
CHANGED
|
@@ -200,7 +200,16 @@ export interface WorkflowDef {
|
|
|
200
200
|
default_isolation?: "none" | "worktree" | "auto";
|
|
201
201
|
merge_strategy?: "escalate";
|
|
202
202
|
}
|
|
203
|
-
export type ControllerKind = "flutter_rewrite_fix_loop";
|
|
203
|
+
export type ControllerKind = "fix_loop" | "flutter_rewrite_fix_loop";
|
|
204
|
+
export interface ControllerPolicyDef {
|
|
205
|
+
diagnosis_reuse_criteria?: string[];
|
|
206
|
+
progress_provenance_checks?: string[];
|
|
207
|
+
progress_provenance_failure?: string;
|
|
208
|
+
verdict_branches?: string[];
|
|
209
|
+
verdict_mapping?: string[];
|
|
210
|
+
stop_report?: string[];
|
|
211
|
+
constraints?: string[];
|
|
212
|
+
}
|
|
204
213
|
export interface ControllerDef {
|
|
205
214
|
name: string;
|
|
206
215
|
description?: string;
|
|
@@ -219,7 +228,8 @@ export interface ControllerDef {
|
|
|
219
228
|
fix_reviewer: string;
|
|
220
229
|
test_runner: string;
|
|
221
230
|
};
|
|
222
|
-
stop_format: "zh_three_part";
|
|
231
|
+
stop_format: "configured_stop_report" | "zh_three_part";
|
|
232
|
+
policy?: ControllerPolicyDef;
|
|
223
233
|
}
|
|
224
234
|
/** Parallel block syntax sugar — expanded by compiler into steps with depends_on */
|
|
225
235
|
export interface ParallelBlockDef {
|
package/dist/schema/validate.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - Codon values in valid ranges
|
|
8
8
|
* - No circular inheritance
|
|
9
9
|
*/
|
|
10
|
+
import { validateController } from "./validators/controllers.js";
|
|
10
11
|
function isLegibilityAssetArray(value) {
|
|
11
12
|
return Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null &&
|
|
12
13
|
typeof item.type === "string" &&
|
|
@@ -299,99 +300,6 @@ function validateCompletionCheck(check, path) {
|
|
|
299
300
|
}
|
|
300
301
|
return errors;
|
|
301
302
|
}
|
|
302
|
-
function isSafeControllerArtifactPath(path) {
|
|
303
|
-
return path.startsWith(".dna/") &&
|
|
304
|
-
!path.startsWith("/") &&
|
|
305
|
-
!path.includes("..") &&
|
|
306
|
-
!/[*?[\]{}]/.test(path) &&
|
|
307
|
-
!path.includes("{{") &&
|
|
308
|
-
!path.includes("}}") &&
|
|
309
|
-
path.includes("$ARGUMENTS");
|
|
310
|
-
}
|
|
311
|
-
function validateController(controller, roleNames, workflows, path) {
|
|
312
|
-
const errors = [];
|
|
313
|
-
if (!controller.name) {
|
|
314
|
-
errors.push({ path: `${path}.name`, message: "controller requires 'name'" });
|
|
315
|
-
}
|
|
316
|
-
if (controller.kind !== "flutter_rewrite_fix_loop") {
|
|
317
|
-
errors.push({ path: `${path}.kind`, message: "unsupported controller kind" });
|
|
318
|
-
}
|
|
319
|
-
if (!Number.isInteger(controller.max_rounds) || controller.max_rounds < 1 || controller.max_rounds > 20) {
|
|
320
|
-
errors.push({ path: `${path}.max_rounds`, message: "max_rounds must be an integer in 1..20" });
|
|
321
|
-
}
|
|
322
|
-
const requiredStringFields = [
|
|
323
|
-
"progress_artifact_path",
|
|
324
|
-
"diagnosis_workflow",
|
|
325
|
-
"fix_workflow",
|
|
326
|
-
"diagnosis_artifact_path",
|
|
327
|
-
"diagnosis_review_artifact_path",
|
|
328
|
-
"review_artifact_path",
|
|
329
|
-
];
|
|
330
|
-
for (const field of requiredStringFields) {
|
|
331
|
-
if (typeof controller[field] !== "string" || !controller[field]) {
|
|
332
|
-
errors.push({ path: `${path}.${field}`, message: "must be a non-empty string" });
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
const artifactPathFields = [
|
|
336
|
-
"progress_artifact_path",
|
|
337
|
-
"diagnosis_artifact_path",
|
|
338
|
-
"diagnosis_review_artifact_path",
|
|
339
|
-
"review_artifact_path",
|
|
340
|
-
];
|
|
341
|
-
for (const field of artifactPathFields) {
|
|
342
|
-
if (typeof controller[field] === "string" && !isSafeControllerArtifactPath(controller[field])) {
|
|
343
|
-
errors.push({ path: `${path}.${field}`, message: "must be a relative .dna/ artifact template containing $ARGUMENTS without glob, brace, absolute, or parent traversal syntax" });
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
const diagnosisWorkflow = controller.diagnosis_workflow ? workflows[controller.diagnosis_workflow] : undefined;
|
|
347
|
-
const fixWorkflow = controller.fix_workflow ? workflows[controller.fix_workflow] : undefined;
|
|
348
|
-
if (controller.diagnosis_workflow && !diagnosisWorkflow) {
|
|
349
|
-
errors.push({ path: `${path}.diagnosis_workflow`, message: `references unknown workflow '${controller.diagnosis_workflow}'` });
|
|
350
|
-
}
|
|
351
|
-
if (controller.fix_workflow && !fixWorkflow) {
|
|
352
|
-
errors.push({ path: `${path}.fix_workflow`, message: `references unknown workflow '${controller.fix_workflow}'` });
|
|
353
|
-
}
|
|
354
|
-
const roleMap = controller.roles;
|
|
355
|
-
const requiredRoles = ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
|
|
356
|
-
if (!roleMap || typeof roleMap !== "object") {
|
|
357
|
-
errors.push({ path: `${path}.roles`, message: "controller requires roles" });
|
|
358
|
-
}
|
|
359
|
-
else {
|
|
360
|
-
for (const roleKey of requiredRoles) {
|
|
361
|
-
const roleName = roleMap[roleKey];
|
|
362
|
-
if (typeof roleName !== "string" || !roleName) {
|
|
363
|
-
errors.push({ path: `${path}.roles.${roleKey}`, message: "must be a non-empty role name" });
|
|
364
|
-
}
|
|
365
|
-
else if (!roleNames.has(roleName)) {
|
|
366
|
-
errors.push({ path: `${path}.roles.${roleKey}`, message: `references unknown role '${roleName}'` });
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (roleMap && typeof roleMap === "object") {
|
|
371
|
-
validateControllerWorkflowRoles(diagnosisWorkflow, ["analyzer", "analysis_reviewer"], roleMap, `${path}.diagnosis_workflow`, errors);
|
|
372
|
-
validateControllerWorkflowRoles(fixWorkflow, ["surgeon", "fix_reviewer", "test_runner"], roleMap, `${path}.fix_workflow`, errors);
|
|
373
|
-
}
|
|
374
|
-
if (controller.stop_format !== "zh_three_part") {
|
|
375
|
-
errors.push({ path: `${path}.stop_format`, message: "stop_format must be 'zh_three_part'" });
|
|
376
|
-
}
|
|
377
|
-
return errors;
|
|
378
|
-
}
|
|
379
|
-
function validateControllerWorkflowRoles(workflow, requiredRoleKeys, roleMap, path, errors) {
|
|
380
|
-
if (!workflow)
|
|
381
|
-
return;
|
|
382
|
-
const workflowRoles = new Set(workflow.steps.map((step) => step.role));
|
|
383
|
-
for (const roleKey of requiredRoleKeys) {
|
|
384
|
-
const roleName = roleMap[roleKey];
|
|
385
|
-
if (typeof roleName !== "string" || !roleName)
|
|
386
|
-
continue;
|
|
387
|
-
if (!workflowRoles.has(roleName)) {
|
|
388
|
-
errors.push({
|
|
389
|
-
path: `${path}.${roleKey}`,
|
|
390
|
-
message: `workflow '${workflow.name}' must include a step with controller role '${roleName}'`,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
303
|
function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
396
304
|
const errors = [];
|
|
397
305
|
const path = pathPrefix ?? "workflow";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ControllerDef, WorkflowDef } from "../types.js";
|
|
2
|
+
import type { ValidationError } from "../validate.js";
|
|
3
|
+
export declare function validateController(controller: ControllerDef, roleNames: Set<string>, workflows: Record<string, WorkflowDef>, path: string): ValidationError[];
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { CONTROLLER_ROLE_KEYS, getControllerKindMetadata, isSupportedControllerKind } from "../controller-registry.js";
|
|
2
|
+
function isSafeControllerArtifactPath(path) {
|
|
3
|
+
return path.startsWith(".dna/") &&
|
|
4
|
+
!path.startsWith("/") &&
|
|
5
|
+
!path.includes("..") &&
|
|
6
|
+
!/[*?[\]{}]/.test(path) &&
|
|
7
|
+
!path.includes("{{") &&
|
|
8
|
+
!path.includes("}}") &&
|
|
9
|
+
path.includes("$ARGUMENTS");
|
|
10
|
+
}
|
|
11
|
+
function validateStringList(value, path, errors) {
|
|
12
|
+
if (!Array.isArray(value)) {
|
|
13
|
+
errors.push({ path, message: "must be an array of non-empty strings" });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
value.forEach((item, index) => {
|
|
17
|
+
if (typeof item !== "string" || item.trim() === "") {
|
|
18
|
+
errors.push({ path: `${path}.${index}`, message: "must be a non-empty string" });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function validateControllerPolicy(policy, path, errors) {
|
|
23
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
24
|
+
errors.push({ path, message: "policy must be an object" });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const record = policy;
|
|
28
|
+
const listFields = [
|
|
29
|
+
"diagnosis_reuse_criteria",
|
|
30
|
+
"progress_provenance_checks",
|
|
31
|
+
"verdict_branches",
|
|
32
|
+
"verdict_mapping",
|
|
33
|
+
"stop_report",
|
|
34
|
+
"constraints",
|
|
35
|
+
];
|
|
36
|
+
const allowedFields = new Set([...listFields, "progress_provenance_failure"]);
|
|
37
|
+
for (const field of Object.keys(record)) {
|
|
38
|
+
if (!allowedFields.has(field)) {
|
|
39
|
+
errors.push({ path: `${path}.${field}`, message: "unknown controller policy field" });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const field of listFields) {
|
|
43
|
+
if (record[field] !== undefined)
|
|
44
|
+
validateStringList(record[field], `${path}.${field}`, errors);
|
|
45
|
+
}
|
|
46
|
+
if (record.progress_provenance_failure !== undefined &&
|
|
47
|
+
(typeof record.progress_provenance_failure !== "string" || record.progress_provenance_failure.trim() === "")) {
|
|
48
|
+
errors.push({ path: `${path}.progress_provenance_failure`, message: "must be a non-empty string" });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function validateControllerWorkflowRoles(workflow, requiredRoleKeys, roleMap, path, errors) {
|
|
52
|
+
if (!workflow)
|
|
53
|
+
return;
|
|
54
|
+
const workflowRoles = new Set(workflow.steps.map((step) => step.role));
|
|
55
|
+
for (const roleKey of requiredRoleKeys) {
|
|
56
|
+
const roleName = roleMap[roleKey];
|
|
57
|
+
if (typeof roleName !== "string" || !roleName)
|
|
58
|
+
continue;
|
|
59
|
+
if (!workflowRoles.has(roleName)) {
|
|
60
|
+
errors.push({
|
|
61
|
+
path: `${path}.${roleKey}`,
|
|
62
|
+
message: `workflow '${workflow.name}' must include a step with controller role '${roleName}'`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function validateController(controller, roleNames, workflows, path) {
|
|
68
|
+
const errors = [];
|
|
69
|
+
if (!controller.name) {
|
|
70
|
+
errors.push({ path: `${path}.name`, message: "controller requires 'name'" });
|
|
71
|
+
}
|
|
72
|
+
const kindMetadata = isSupportedControllerKind(controller.kind)
|
|
73
|
+
? getControllerKindMetadata(controller.kind)
|
|
74
|
+
: undefined;
|
|
75
|
+
if (!kindMetadata) {
|
|
76
|
+
errors.push({ path: `${path}.kind`, message: "unsupported controller kind" });
|
|
77
|
+
}
|
|
78
|
+
if (!Number.isInteger(controller.max_rounds) || controller.max_rounds < 1 || controller.max_rounds > 20) {
|
|
79
|
+
errors.push({ path: `${path}.max_rounds`, message: "max_rounds must be an integer in 1..20" });
|
|
80
|
+
}
|
|
81
|
+
const requiredStringFields = [
|
|
82
|
+
"progress_artifact_path",
|
|
83
|
+
"diagnosis_workflow",
|
|
84
|
+
"fix_workflow",
|
|
85
|
+
"diagnosis_artifact_path",
|
|
86
|
+
"diagnosis_review_artifact_path",
|
|
87
|
+
"review_artifact_path",
|
|
88
|
+
];
|
|
89
|
+
for (const field of requiredStringFields) {
|
|
90
|
+
if (typeof controller[field] !== "string" || !controller[field]) {
|
|
91
|
+
errors.push({ path: `${path}.${field}`, message: "must be a non-empty string" });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const artifactPathFields = [
|
|
95
|
+
"progress_artifact_path",
|
|
96
|
+
"diagnosis_artifact_path",
|
|
97
|
+
"diagnosis_review_artifact_path",
|
|
98
|
+
"review_artifact_path",
|
|
99
|
+
];
|
|
100
|
+
for (const field of artifactPathFields) {
|
|
101
|
+
if (typeof controller[field] === "string" && !isSafeControllerArtifactPath(controller[field])) {
|
|
102
|
+
errors.push({ path: `${path}.${field}`, message: "must be a relative .dna/ artifact template containing $ARGUMENTS without glob, brace, absolute, or parent traversal syntax" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const diagnosisWorkflow = controller.diagnosis_workflow ? workflows[controller.diagnosis_workflow] : undefined;
|
|
106
|
+
const fixWorkflow = controller.fix_workflow ? workflows[controller.fix_workflow] : undefined;
|
|
107
|
+
if (controller.diagnosis_workflow && !diagnosisWorkflow) {
|
|
108
|
+
errors.push({ path: `${path}.diagnosis_workflow`, message: `references unknown workflow '${controller.diagnosis_workflow}'` });
|
|
109
|
+
}
|
|
110
|
+
if (controller.fix_workflow && !fixWorkflow) {
|
|
111
|
+
errors.push({ path: `${path}.fix_workflow`, message: `references unknown workflow '${controller.fix_workflow}'` });
|
|
112
|
+
}
|
|
113
|
+
const roleMap = controller.roles;
|
|
114
|
+
const requiredRoles = kindMetadata?.requiredRoleKeys ?? CONTROLLER_ROLE_KEYS;
|
|
115
|
+
if (!roleMap || typeof roleMap !== "object") {
|
|
116
|
+
errors.push({ path: `${path}.roles`, message: "controller requires roles" });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
for (const roleKey of requiredRoles) {
|
|
120
|
+
const roleName = roleMap[roleKey];
|
|
121
|
+
if (typeof roleName !== "string" || !roleName) {
|
|
122
|
+
errors.push({ path: `${path}.roles.${roleKey}`, message: "must be a non-empty role name" });
|
|
123
|
+
}
|
|
124
|
+
else if (!roleNames.has(roleName)) {
|
|
125
|
+
errors.push({ path: `${path}.roles.${roleKey}`, message: `references unknown role '${roleName}'` });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (roleMap && typeof roleMap === "object") {
|
|
130
|
+
validateControllerWorkflowRoles(diagnosisWorkflow, kindMetadata?.diagnosisRoleKeys ?? ["analyzer", "analysis_reviewer"], roleMap, `${path}.diagnosis_workflow`, errors);
|
|
131
|
+
validateControllerWorkflowRoles(fixWorkflow, kindMetadata?.fixRoleKeys ?? ["surgeon", "fix_reviewer", "test_runner"], roleMap, `${path}.fix_workflow`, errors);
|
|
132
|
+
}
|
|
133
|
+
if (controller.stop_format !== "configured_stop_report" && controller.stop_format !== "zh_three_part") {
|
|
134
|
+
errors.push({ path: `${path}.stop_format`, message: "stop_format must be 'configured_stop_report' or legacy 'zh_three_part'" });
|
|
135
|
+
}
|
|
136
|
+
if (controller.policy !== undefined) {
|
|
137
|
+
validateControllerPolicy(controller.policy, `${path}.policy`, errors);
|
|
138
|
+
}
|
|
139
|
+
return errors;
|
|
140
|
+
}
|