intentdna 1.6.4 → 1.7.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/compile.js +2 -47
- package/dist/cli/commands/context.d.ts +8 -0
- package/dist/cli/commands/context.js +63 -0
- package/dist/cli/commands/feedback.d.ts +1 -0
- package/dist/cli/commands/feedback.js +11 -0
- package/dist/cli/commands/generate.js +5 -33
- package/dist/cli/commands/init.js +11 -63
- package/dist/cli/commands/run.js +5 -4
- package/dist/cli/commands/show.js +2 -38
- package/dist/cli/commands/sync.d.ts +9 -6
- package/dist/cli/commands/sync.js +209 -190
- package/dist/cli/commands/templates.d.ts +10 -1
- package/dist/cli/commands/templates.js +50 -1
- package/dist/cli/commands/validate.js +15 -9
- package/dist/cli/commands/verify.js +97 -25
- package/dist/cli/index.js +76 -11
- package/dist/compiler/cascade.d.ts +3 -1
- package/dist/compiler/cascade.js +51 -0
- package/dist/compiler/compile.js +37 -0
- package/dist/compiler/diagnostics.d.ts +17 -0
- package/dist/compiler/diagnostics.js +30 -0
- package/dist/compiler/index.d.ts +3 -0
- package/dist/compiler/index.js +8 -11
- package/dist/compiler/input-resolver.d.ts +25 -0
- package/dist/compiler/input-resolver.js +175 -0
- package/dist/hooks/cli.d.ts +10 -1
- package/dist/hooks/cli.js +39 -26
- 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/hooks/state.d.ts +2 -0
- package/dist/hooks/state.js +23 -2
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/tools-compile.js +18 -49
- package/dist/mcp/tools-context.d.ts +2 -0
- package/dist/mcp/tools-context.js +85 -0
- package/dist/mcp/tools-enforce.d.ts +2 -2
- package/dist/mcp/tools-enforce.js +21 -53
- package/dist/mcp/tools-observability.js +24 -0
- package/dist/report/kernel-signals.js +3 -0
- package/dist/report/report-package.d.ts +56 -0
- package/dist/report/report-package.js +85 -0
- package/dist/runtime/agent-md.d.ts +1 -0
- package/dist/runtime/agent-md.js +21 -3
- package/dist/runtime/context-sources.d.ts +14 -0
- package/dist/runtime/context-sources.js +60 -0
- 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.d.ts +32 -4
- package/dist/runtime/skill-adapter.js +187 -10
- package/dist/runtime/workflow-runner.d.ts +1 -1
- package/dist/runtime/workflow-runner.js +1 -1
- package/dist/schema/controller-registry.d.ts +29 -0
- package/dist/schema/controller-registry.js +35 -0
- package/dist/schema/types.d.ts +33 -0
- package/dist/schema/validate.js +157 -138
- package/dist/schema/validators/controllers.d.ts +3 -0
- package/dist/schema/validators/controllers.js +156 -0
- package/dist/signals/index.d.ts +10 -0
- package/dist/signals/index.js +90 -5
- package/dist/templates/catalog.d.ts +19 -0
- package/dist/templates/catalog.js +57 -0
- package/dist/templates/flutter-rewrite.dna.yaml +2 -2
- package/dist/templates/metadata.d.ts +6 -0
- package/dist/templates/metadata.js +32 -0
- package/package.json +1 -1
- package/spec/foundation-hardening.md +2 -1
|
@@ -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
|
@@ -295,6 +295,35 @@ export interface LegibilityAssetMap {
|
|
|
295
295
|
per_role?: Record<string, LegibilityAsset[]>;
|
|
296
296
|
per_workflow?: Record<string, LegibilityAsset[]>;
|
|
297
297
|
}
|
|
298
|
+
export type AdvisoryContextSourceType = "file" | "url" | "doc" | "note";
|
|
299
|
+
export interface AdvisoryContextSource {
|
|
300
|
+
id: string;
|
|
301
|
+
type: AdvisoryContextSourceType;
|
|
302
|
+
path?: string;
|
|
303
|
+
url?: string;
|
|
304
|
+
title?: string;
|
|
305
|
+
description?: string;
|
|
306
|
+
tags?: string[];
|
|
307
|
+
}
|
|
308
|
+
export interface PlanningContext {
|
|
309
|
+
sources: string[];
|
|
310
|
+
instructions?: string[];
|
|
311
|
+
require_quotes?: boolean;
|
|
312
|
+
require_attribution?: boolean;
|
|
313
|
+
}
|
|
314
|
+
export interface CompiledAdvisoryContextSource extends AdvisoryContextSource {
|
|
315
|
+
usage: "planning_only";
|
|
316
|
+
non_enforcing: true;
|
|
317
|
+
requires_quotes: boolean;
|
|
318
|
+
requires_attribution: boolean;
|
|
319
|
+
}
|
|
320
|
+
export interface CompiledPlanningContext {
|
|
321
|
+
sources: string[];
|
|
322
|
+
instructions?: string[];
|
|
323
|
+
require_quotes: boolean;
|
|
324
|
+
require_attribution: boolean;
|
|
325
|
+
non_enforcing: true;
|
|
326
|
+
}
|
|
298
327
|
export interface IntentDNA {
|
|
299
328
|
$schema?: string;
|
|
300
329
|
version: string;
|
|
@@ -311,6 +340,8 @@ export interface IntentDNA {
|
|
|
311
340
|
variables?: Record<string, string | VariableDef>;
|
|
312
341
|
mcp?: Record<string, MCPServerDef>;
|
|
313
342
|
legibility_assets?: LegibilityAssetMap;
|
|
343
|
+
context_sources?: AdvisoryContextSource[];
|
|
344
|
+
planning_context?: PlanningContext;
|
|
314
345
|
context_files?: {
|
|
315
346
|
mandatory?: string[];
|
|
316
347
|
per_role?: Record<string, string[]>;
|
|
@@ -439,6 +470,8 @@ export interface ConstraintIR {
|
|
|
439
470
|
post_execution_validators: PostExecutionValidator[];
|
|
440
471
|
verifier_specs?: VerifierSpec[];
|
|
441
472
|
context_injections: ContextInjection[];
|
|
473
|
+
context_sources?: CompiledAdvisoryContextSource[];
|
|
474
|
+
planning_context?: CompiledPlanningContext;
|
|
442
475
|
source_dna_ids: string[];
|
|
443
476
|
compiled_at: string;
|
|
444
477
|
active_context?: string;
|
package/dist/schema/validate.js
CHANGED
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
* - Codon values in valid ranges
|
|
8
8
|
* - No circular inheritance
|
|
9
9
|
*/
|
|
10
|
+
import { isAbsolute } from "node:path";
|
|
11
|
+
import { validateController } from "./validators/controllers.js";
|
|
12
|
+
const SAFE_GENERATED_IDENTIFIER = /^[A-Za-z0-9_-]+$/;
|
|
13
|
+
const SAFE_GENERATED_DISPLAY_NAME = /^[A-Za-z0-9][A-Za-z0-9 _-]*$/;
|
|
14
|
+
function isSafeGeneratedIdentifier(value) {
|
|
15
|
+
return SAFE_GENERATED_IDENTIFIER.test(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\");
|
|
16
|
+
}
|
|
17
|
+
function isSafeGeneratedDisplayName(value) {
|
|
18
|
+
return SAFE_GENERATED_DISPLAY_NAME.test(value) && !value.includes("..");
|
|
19
|
+
}
|
|
20
|
+
function isSafeProjectTemplatePath(value) {
|
|
21
|
+
const withoutPlaceholders = value.replace(/\{\{[A-Za-z_][A-Za-z0-9_]*\}\}/g, "placeholder");
|
|
22
|
+
return value.trim().length > 0 &&
|
|
23
|
+
!isAbsolute(value) &&
|
|
24
|
+
!value.split(/[\\/]+/).includes("..") &&
|
|
25
|
+
!/[*?[\]{}]/.test(withoutPlaceholders) &&
|
|
26
|
+
!value.includes("${") &&
|
|
27
|
+
!value.includes("$(`");
|
|
28
|
+
}
|
|
10
29
|
function isLegibilityAssetArray(value) {
|
|
11
30
|
return Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null &&
|
|
12
31
|
typeof item.type === "string" &&
|
|
@@ -71,6 +90,126 @@ function validateLegibilityAssets(dna, roleNamesSet, errors, warnings) {
|
|
|
71
90
|
}
|
|
72
91
|
}
|
|
73
92
|
}
|
|
93
|
+
function validateContextSources(dna, errors) {
|
|
94
|
+
const sources = dna.context_sources;
|
|
95
|
+
if (sources !== undefined) {
|
|
96
|
+
if (!Array.isArray(sources)) {
|
|
97
|
+
errors.push({ path: "context_sources", message: "must be an array" });
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (let i = 0; i < sources.length; i++) {
|
|
102
|
+
const path = `context_sources[${i}]`;
|
|
103
|
+
if (typeof sources[i] !== "object" || sources[i] === null || Array.isArray(sources[i])) {
|
|
104
|
+
errors.push({ path, message: "context source must be an object" });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const source = sources[i];
|
|
108
|
+
if (!source.id || typeof source.id !== "string") {
|
|
109
|
+
errors.push({ path: `${path}.id`, message: "context source requires non-empty id" });
|
|
110
|
+
}
|
|
111
|
+
else if (seen.has(source.id)) {
|
|
112
|
+
errors.push({ path: `${path}.id`, message: `duplicate context source id '${source.id}'` });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
seen.add(source.id);
|
|
116
|
+
}
|
|
117
|
+
if (!["file", "url", "doc", "note"].includes(source.type)) {
|
|
118
|
+
errors.push({ path: `${path}.type`, message: "must be one of: file, url, doc, note" });
|
|
119
|
+
}
|
|
120
|
+
if (source.type === "file" || source.type === "doc") {
|
|
121
|
+
if (typeof source.path !== "string" || !source.path) {
|
|
122
|
+
errors.push({ path: `${path}.path`, message: `${source.type} context source requires non-empty path` });
|
|
123
|
+
}
|
|
124
|
+
else if (isAbsolute(source.path) || source.path.split(/[\\/]+/).includes("..")) {
|
|
125
|
+
errors.push({ path: `${path}.path`, message: "context source path must stay within the project" });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (source.type === "url" && (typeof source.url !== "string" || !source.url)) {
|
|
129
|
+
errors.push({ path: `${path}.url`, message: "url context source requires non-empty url" });
|
|
130
|
+
}
|
|
131
|
+
if (source.tags !== undefined && (!Array.isArray(source.tags) || source.tags.some((tag) => typeof tag !== "string" || !tag))) {
|
|
132
|
+
errors.push({ path: `${path}.tags`, message: "tags must be an array of non-empty strings" });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (dna.planning_context) {
|
|
138
|
+
const pc = dna.planning_context;
|
|
139
|
+
if (!Array.isArray(pc.sources) || pc.sources.some((sourceId) => typeof sourceId !== "string" || !sourceId)) {
|
|
140
|
+
errors.push({ path: "planning_context.sources", message: "must be an array of non-empty context source ids" });
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const sourceList = Array.isArray(sources) ? sources : [];
|
|
144
|
+
const declared = new Set(sourceList.map((source) => source.id));
|
|
145
|
+
for (const sourceId of pc.sources) {
|
|
146
|
+
if (!declared.has(sourceId)) {
|
|
147
|
+
errors.push({ path: "planning_context.sources", message: `references unknown context source '${sourceId}'` });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (pc.instructions !== undefined && (!Array.isArray(pc.instructions) || pc.instructions.some((instruction) => typeof instruction !== "string" || !instruction))) {
|
|
152
|
+
errors.push({ path: "planning_context.instructions", message: "must be an array of non-empty strings" });
|
|
153
|
+
}
|
|
154
|
+
if (pc.require_quotes !== undefined && typeof pc.require_quotes !== "boolean") {
|
|
155
|
+
errors.push({ path: "planning_context.require_quotes", message: "must be a boolean" });
|
|
156
|
+
}
|
|
157
|
+
if (pc.require_attribution !== undefined && typeof pc.require_attribution !== "boolean") {
|
|
158
|
+
errors.push({ path: "planning_context.require_attribution", message: "must be a boolean" });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function contextSourceIdentities(source) {
|
|
163
|
+
const identities = [source.id];
|
|
164
|
+
if (source.type === "file" || source.type === "doc")
|
|
165
|
+
identities.push(source.path);
|
|
166
|
+
if (source.type === "url")
|
|
167
|
+
identities.push(source.url);
|
|
168
|
+
return identities.filter((identity) => typeof identity === "string" && identity.length > 0);
|
|
169
|
+
}
|
|
170
|
+
function handoffArtifactIdentities(artifact) {
|
|
171
|
+
return [artifact.artifact_id, artifact.name, artifact.path, artifact.type === "git_commit" ? "git_commit" : undefined]
|
|
172
|
+
.filter((identity) => typeof identity === "string" && identity.length > 0);
|
|
173
|
+
}
|
|
174
|
+
function validateContextSourceHandoffOverlap(dna, errors) {
|
|
175
|
+
const contextSources = Array.isArray(dna.context_sources) ? dna.context_sources : [];
|
|
176
|
+
if (contextSources.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
const advisoryIdentities = new Set(contextSources.flatMap(contextSourceIdentities));
|
|
179
|
+
if (advisoryIdentities.size === 0)
|
|
180
|
+
return;
|
|
181
|
+
const checkArtifact = (artifact, path) => {
|
|
182
|
+
if (artifact.required === false)
|
|
183
|
+
return;
|
|
184
|
+
for (const identity of handoffArtifactIdentities(artifact)) {
|
|
185
|
+
if (!advisoryIdentities.has(identity))
|
|
186
|
+
continue;
|
|
187
|
+
errors.push({
|
|
188
|
+
path,
|
|
189
|
+
message: `planning-only context source '${identity}' cannot be used as a required handoff artifact`,
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const checkWorkflow = (workflow, path) => {
|
|
195
|
+
for (let i = 0; i < (workflow.steps ?? []).length; i++) {
|
|
196
|
+
const step = workflow.steps[i];
|
|
197
|
+
for (const direction of ["consumes", "produces"]) {
|
|
198
|
+
const artifacts = step.handoff?.[direction] ?? [];
|
|
199
|
+
for (let j = 0; j < artifacts.length; j++) {
|
|
200
|
+
checkArtifact(artifacts[j], `${path}.steps[${i}].handoff.${direction}[${j}]`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
if (dna.workflow)
|
|
206
|
+
checkWorkflow(dna.workflow, "workflow");
|
|
207
|
+
if (dna.workflows) {
|
|
208
|
+
for (const [wfName, wfDef] of Object.entries(dna.workflows)) {
|
|
209
|
+
checkWorkflow(wfDef, `workflows.${wfName}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
74
213
|
function validateCodon(codon, path) {
|
|
75
214
|
const errors = [];
|
|
76
215
|
switch (codon.type) {
|
|
@@ -149,6 +288,9 @@ function validateContext(name, ctx, geneNames) {
|
|
|
149
288
|
function validateRole(name, role, geneNames) {
|
|
150
289
|
const errors = [];
|
|
151
290
|
const path = `roles.${name}`;
|
|
291
|
+
if (!isSafeGeneratedIdentifier(name)) {
|
|
292
|
+
errors.push({ path, message: "role name must contain only letters, digits, underscores, and hyphens" });
|
|
293
|
+
}
|
|
152
294
|
// description required
|
|
153
295
|
if (!role.description) {
|
|
154
296
|
errors.push({ path: `${path}.description`, message: "role requires 'description'" });
|
|
@@ -244,8 +386,10 @@ function validateRole(name, role, geneNames) {
|
|
|
244
386
|
if (os.converter !== undefined && (typeof os.converter !== "string" || !os.converter)) {
|
|
245
387
|
errors.push({ path: `${osPath}.converter`, message: "must be a non-empty string" });
|
|
246
388
|
}
|
|
247
|
-
if (os.path !== undefined
|
|
248
|
-
|
|
389
|
+
if (os.path !== undefined) {
|
|
390
|
+
if (typeof os.path !== "string" || !isSafeProjectTemplatePath(os.path)) {
|
|
391
|
+
errors.push({ path: `${osPath}.path`, message: "must be a safe project-relative path template" });
|
|
392
|
+
}
|
|
249
393
|
}
|
|
250
394
|
}
|
|
251
395
|
return errors;
|
|
@@ -299,142 +443,6 @@ function validateCompletionCheck(check, path) {
|
|
|
299
443
|
}
|
|
300
444
|
return errors;
|
|
301
445
|
}
|
|
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 validateStringList(value, path, errors) {
|
|
312
|
-
if (!Array.isArray(value)) {
|
|
313
|
-
errors.push({ path, message: "must be an array of non-empty strings" });
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
value.forEach((item, index) => {
|
|
317
|
-
if (typeof item !== "string" || item.trim() === "") {
|
|
318
|
-
errors.push({ path: `${path}.${index}`, message: "must be a non-empty string" });
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
function validateControllerPolicy(policy, path, errors) {
|
|
323
|
-
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
324
|
-
errors.push({ path, message: "policy must be an object" });
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
const record = policy;
|
|
328
|
-
const listFields = [
|
|
329
|
-
"diagnosis_reuse_criteria",
|
|
330
|
-
"progress_provenance_checks",
|
|
331
|
-
"verdict_branches",
|
|
332
|
-
"verdict_mapping",
|
|
333
|
-
"stop_report",
|
|
334
|
-
"constraints",
|
|
335
|
-
];
|
|
336
|
-
const allowedFields = new Set([...listFields, "progress_provenance_failure"]);
|
|
337
|
-
for (const field of Object.keys(record)) {
|
|
338
|
-
if (!allowedFields.has(field)) {
|
|
339
|
-
errors.push({ path: `${path}.${field}`, message: "unknown controller policy field" });
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
for (const field of listFields) {
|
|
343
|
-
if (record[field] !== undefined)
|
|
344
|
-
validateStringList(record[field], `${path}.${field}`, errors);
|
|
345
|
-
}
|
|
346
|
-
if (record.progress_provenance_failure !== undefined &&
|
|
347
|
-
(typeof record.progress_provenance_failure !== "string" || record.progress_provenance_failure.trim() === "")) {
|
|
348
|
-
errors.push({ path: `${path}.progress_provenance_failure`, message: "must be a non-empty string" });
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
function validateController(controller, roleNames, workflows, path) {
|
|
352
|
-
const errors = [];
|
|
353
|
-
if (!controller.name) {
|
|
354
|
-
errors.push({ path: `${path}.name`, message: "controller requires 'name'" });
|
|
355
|
-
}
|
|
356
|
-
if (controller.kind !== "fix_loop" && controller.kind !== "flutter_rewrite_fix_loop") {
|
|
357
|
-
errors.push({ path: `${path}.kind`, message: "unsupported controller kind" });
|
|
358
|
-
}
|
|
359
|
-
if (!Number.isInteger(controller.max_rounds) || controller.max_rounds < 1 || controller.max_rounds > 20) {
|
|
360
|
-
errors.push({ path: `${path}.max_rounds`, message: "max_rounds must be an integer in 1..20" });
|
|
361
|
-
}
|
|
362
|
-
const requiredStringFields = [
|
|
363
|
-
"progress_artifact_path",
|
|
364
|
-
"diagnosis_workflow",
|
|
365
|
-
"fix_workflow",
|
|
366
|
-
"diagnosis_artifact_path",
|
|
367
|
-
"diagnosis_review_artifact_path",
|
|
368
|
-
"review_artifact_path",
|
|
369
|
-
];
|
|
370
|
-
for (const field of requiredStringFields) {
|
|
371
|
-
if (typeof controller[field] !== "string" || !controller[field]) {
|
|
372
|
-
errors.push({ path: `${path}.${field}`, message: "must be a non-empty string" });
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
const artifactPathFields = [
|
|
376
|
-
"progress_artifact_path",
|
|
377
|
-
"diagnosis_artifact_path",
|
|
378
|
-
"diagnosis_review_artifact_path",
|
|
379
|
-
"review_artifact_path",
|
|
380
|
-
];
|
|
381
|
-
for (const field of artifactPathFields) {
|
|
382
|
-
if (typeof controller[field] === "string" && !isSafeControllerArtifactPath(controller[field])) {
|
|
383
|
-
errors.push({ path: `${path}.${field}`, message: "must be a relative .dna/ artifact template containing $ARGUMENTS without glob, brace, absolute, or parent traversal syntax" });
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
const diagnosisWorkflow = controller.diagnosis_workflow ? workflows[controller.diagnosis_workflow] : undefined;
|
|
387
|
-
const fixWorkflow = controller.fix_workflow ? workflows[controller.fix_workflow] : undefined;
|
|
388
|
-
if (controller.diagnosis_workflow && !diagnosisWorkflow) {
|
|
389
|
-
errors.push({ path: `${path}.diagnosis_workflow`, message: `references unknown workflow '${controller.diagnosis_workflow}'` });
|
|
390
|
-
}
|
|
391
|
-
if (controller.fix_workflow && !fixWorkflow) {
|
|
392
|
-
errors.push({ path: `${path}.fix_workflow`, message: `references unknown workflow '${controller.fix_workflow}'` });
|
|
393
|
-
}
|
|
394
|
-
const roleMap = controller.roles;
|
|
395
|
-
const requiredRoles = ["analyzer", "analysis_reviewer", "surgeon", "fix_reviewer", "test_runner"];
|
|
396
|
-
if (!roleMap || typeof roleMap !== "object") {
|
|
397
|
-
errors.push({ path: `${path}.roles`, message: "controller requires roles" });
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
for (const roleKey of requiredRoles) {
|
|
401
|
-
const roleName = roleMap[roleKey];
|
|
402
|
-
if (typeof roleName !== "string" || !roleName) {
|
|
403
|
-
errors.push({ path: `${path}.roles.${roleKey}`, message: "must be a non-empty role name" });
|
|
404
|
-
}
|
|
405
|
-
else if (!roleNames.has(roleName)) {
|
|
406
|
-
errors.push({ path: `${path}.roles.${roleKey}`, message: `references unknown role '${roleName}'` });
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
if (roleMap && typeof roleMap === "object") {
|
|
411
|
-
validateControllerWorkflowRoles(diagnosisWorkflow, ["analyzer", "analysis_reviewer"], roleMap, `${path}.diagnosis_workflow`, errors);
|
|
412
|
-
validateControllerWorkflowRoles(fixWorkflow, ["surgeon", "fix_reviewer", "test_runner"], roleMap, `${path}.fix_workflow`, errors);
|
|
413
|
-
}
|
|
414
|
-
if (controller.stop_format !== "configured_stop_report" && controller.stop_format !== "zh_three_part") {
|
|
415
|
-
errors.push({ path: `${path}.stop_format`, message: "stop_format must be 'configured_stop_report' or legacy 'zh_three_part'" });
|
|
416
|
-
}
|
|
417
|
-
if (controller.policy !== undefined) {
|
|
418
|
-
validateControllerPolicy(controller.policy, `${path}.policy`, errors);
|
|
419
|
-
}
|
|
420
|
-
return errors;
|
|
421
|
-
}
|
|
422
|
-
function validateControllerWorkflowRoles(workflow, requiredRoleKeys, roleMap, path, errors) {
|
|
423
|
-
if (!workflow)
|
|
424
|
-
return;
|
|
425
|
-
const workflowRoles = new Set(workflow.steps.map((step) => step.role));
|
|
426
|
-
for (const roleKey of requiredRoleKeys) {
|
|
427
|
-
const roleName = roleMap[roleKey];
|
|
428
|
-
if (typeof roleName !== "string" || !roleName)
|
|
429
|
-
continue;
|
|
430
|
-
if (!workflowRoles.has(roleName)) {
|
|
431
|
-
errors.push({
|
|
432
|
-
path: `${path}.${roleKey}`,
|
|
433
|
-
message: `workflow '${workflow.name}' must include a step with controller role '${roleName}'`,
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
446
|
function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
439
447
|
const errors = [];
|
|
440
448
|
const path = pathPrefix ?? "workflow";
|
|
@@ -442,6 +450,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
|
442
450
|
if (!workflow.name) {
|
|
443
451
|
errors.push({ path: `${path}.name`, message: "workflow requires 'name'" });
|
|
444
452
|
}
|
|
453
|
+
else if (!isSafeGeneratedDisplayName(workflow.name)) {
|
|
454
|
+
errors.push({ path: `${path}.name`, message: "workflow name must contain only letters, digits, spaces, underscores, and hyphens" });
|
|
455
|
+
}
|
|
445
456
|
// steps required and non-empty
|
|
446
457
|
if (!workflow.steps || workflow.steps.length === 0) {
|
|
447
458
|
errors.push({ path: `${path}.steps`, message: "workflow requires at least one step" });
|
|
@@ -457,6 +468,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
|
457
468
|
if (!step.id) {
|
|
458
469
|
errors.push({ path: `${stepPath}.id`, message: "step requires 'id'" });
|
|
459
470
|
}
|
|
471
|
+
else if (!isSafeGeneratedIdentifier(step.id)) {
|
|
472
|
+
errors.push({ path: `${stepPath}.id`, message: "step id must contain only letters, digits, underscores, and hyphens" });
|
|
473
|
+
}
|
|
460
474
|
else if (seenStepIds.has(step.id)) {
|
|
461
475
|
errors.push({ path: `${stepPath}.id`, message: `duplicate step id '${step.id}'` });
|
|
462
476
|
}
|
|
@@ -468,6 +482,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
|
468
482
|
if (!step.role) {
|
|
469
483
|
errors.push({ path: `${stepPath}.role`, message: "step requires 'role'" });
|
|
470
484
|
}
|
|
485
|
+
else if (!isSafeGeneratedIdentifier(step.role)) {
|
|
486
|
+
errors.push({ path: `${stepPath}.role`, message: "step role must contain only letters, digits, underscores, and hyphens" });
|
|
487
|
+
}
|
|
471
488
|
else if (roleNames.size > 0 && !roleNames.has(step.role)) {
|
|
472
489
|
errors.push({
|
|
473
490
|
path: `${stepPath}.role`,
|
|
@@ -718,6 +735,7 @@ export function validateDNA(dna) {
|
|
|
718
735
|
errors.push(...validateWorkflow(wfDef, roleNamesSet, `workflows.${wfName}`));
|
|
719
736
|
}
|
|
720
737
|
}
|
|
738
|
+
validateContextSourceHandoffOverlap(dna, errors);
|
|
721
739
|
if (dna.controllers) {
|
|
722
740
|
const workflowsForControllers = { ...(dna.workflows ?? {}) };
|
|
723
741
|
if (dna.workflow)
|
|
@@ -756,6 +774,7 @@ export function validateDNA(dna) {
|
|
|
756
774
|
}
|
|
757
775
|
}
|
|
758
776
|
validateLegibilityAssets(dna, roleNamesSet, errors, warnings);
|
|
777
|
+
validateContextSources(dna, errors);
|
|
759
778
|
// Validate context_files
|
|
760
779
|
if (dna.context_files) {
|
|
761
780
|
const cf = dna.context_files;
|
|
@@ -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,156 @@
|
|
|
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 isSafeGeneratedIdentifier(value) {
|
|
12
|
+
return /^[A-Za-z0-9_-]+$/.test(value) && !value.includes("..") && !value.includes("/") && !value.includes("\\");
|
|
13
|
+
}
|
|
14
|
+
function isSafeGeneratedDisplayName(value) {
|
|
15
|
+
return /^[A-Za-z0-9][A-Za-z0-9 _-]*$/.test(value) && !value.includes("..");
|
|
16
|
+
}
|
|
17
|
+
function validateStringList(value, path, errors) {
|
|
18
|
+
if (!Array.isArray(value)) {
|
|
19
|
+
errors.push({ path, message: "must be an array of non-empty strings" });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
value.forEach((item, index) => {
|
|
23
|
+
if (typeof item !== "string" || item.trim() === "") {
|
|
24
|
+
errors.push({ path: `${path}.${index}`, message: "must be a non-empty string" });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function validateControllerPolicy(policy, path, errors) {
|
|
29
|
+
if (typeof policy !== "object" || policy === null || Array.isArray(policy)) {
|
|
30
|
+
errors.push({ path, message: "policy must be an object" });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const record = policy;
|
|
34
|
+
const listFields = [
|
|
35
|
+
"diagnosis_reuse_criteria",
|
|
36
|
+
"progress_provenance_checks",
|
|
37
|
+
"verdict_branches",
|
|
38
|
+
"verdict_mapping",
|
|
39
|
+
"stop_report",
|
|
40
|
+
"constraints",
|
|
41
|
+
];
|
|
42
|
+
const allowedFields = new Set([...listFields, "progress_provenance_failure"]);
|
|
43
|
+
for (const field of Object.keys(record)) {
|
|
44
|
+
if (!allowedFields.has(field)) {
|
|
45
|
+
errors.push({ path: `${path}.${field}`, message: "unknown controller policy field" });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const field of listFields) {
|
|
49
|
+
if (record[field] !== undefined)
|
|
50
|
+
validateStringList(record[field], `${path}.${field}`, errors);
|
|
51
|
+
}
|
|
52
|
+
if (record.progress_provenance_failure !== undefined &&
|
|
53
|
+
(typeof record.progress_provenance_failure !== "string" || record.progress_provenance_failure.trim() === "")) {
|
|
54
|
+
errors.push({ path: `${path}.progress_provenance_failure`, message: "must be a non-empty string" });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function validateControllerWorkflowRoles(workflow, requiredRoleKeys, roleMap, path, errors) {
|
|
58
|
+
if (!workflow)
|
|
59
|
+
return;
|
|
60
|
+
const workflowRoles = new Set(workflow.steps.map((step) => step.role));
|
|
61
|
+
for (const roleKey of requiredRoleKeys) {
|
|
62
|
+
const roleName = roleMap[roleKey];
|
|
63
|
+
if (typeof roleName !== "string" || !roleName)
|
|
64
|
+
continue;
|
|
65
|
+
if (!workflowRoles.has(roleName)) {
|
|
66
|
+
errors.push({
|
|
67
|
+
path: `${path}.${roleKey}`,
|
|
68
|
+
message: `workflow '${workflow.name}' must include a step with controller role '${roleName}'`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function validateController(controller, roleNames, workflows, path) {
|
|
74
|
+
const errors = [];
|
|
75
|
+
if (!controller.name) {
|
|
76
|
+
errors.push({ path: `${path}.name`, message: "controller requires 'name'" });
|
|
77
|
+
}
|
|
78
|
+
else if (!isSafeGeneratedDisplayName(controller.name)) {
|
|
79
|
+
errors.push({ path: `${path}.name`, message: "controller name must contain only letters, digits, spaces, underscores, and hyphens" });
|
|
80
|
+
}
|
|
81
|
+
const controllerKey = path.startsWith("controllers.") ? path.slice("controllers.".length) : "";
|
|
82
|
+
if (controllerKey && !isSafeGeneratedIdentifier(controllerKey)) {
|
|
83
|
+
errors.push({ path, message: "controller key must contain only letters, digits, underscores, and hyphens" });
|
|
84
|
+
}
|
|
85
|
+
const kindMetadata = isSupportedControllerKind(controller.kind)
|
|
86
|
+
? getControllerKindMetadata(controller.kind)
|
|
87
|
+
: undefined;
|
|
88
|
+
if (!kindMetadata) {
|
|
89
|
+
errors.push({ path: `${path}.kind`, message: "unsupported controller kind" });
|
|
90
|
+
}
|
|
91
|
+
if (!Number.isInteger(controller.max_rounds) || controller.max_rounds < 1 || controller.max_rounds > 20) {
|
|
92
|
+
errors.push({ path: `${path}.max_rounds`, message: "max_rounds must be an integer in 1..20" });
|
|
93
|
+
}
|
|
94
|
+
const requiredStringFields = [
|
|
95
|
+
"progress_artifact_path",
|
|
96
|
+
"diagnosis_workflow",
|
|
97
|
+
"fix_workflow",
|
|
98
|
+
"diagnosis_artifact_path",
|
|
99
|
+
"diagnosis_review_artifact_path",
|
|
100
|
+
"review_artifact_path",
|
|
101
|
+
];
|
|
102
|
+
for (const field of requiredStringFields) {
|
|
103
|
+
if (typeof controller[field] !== "string" || !controller[field]) {
|
|
104
|
+
errors.push({ path: `${path}.${field}`, message: "must be a non-empty string" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const artifactPathFields = [
|
|
108
|
+
"progress_artifact_path",
|
|
109
|
+
"diagnosis_artifact_path",
|
|
110
|
+
"diagnosis_review_artifact_path",
|
|
111
|
+
"review_artifact_path",
|
|
112
|
+
];
|
|
113
|
+
for (const field of artifactPathFields) {
|
|
114
|
+
if (typeof controller[field] === "string" && !isSafeControllerArtifactPath(controller[field])) {
|
|
115
|
+
errors.push({ path: `${path}.${field}`, message: "must be a relative .dna/ artifact template containing $ARGUMENTS without glob, brace, absolute, or parent traversal syntax" });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const diagnosisWorkflow = controller.diagnosis_workflow ? workflows[controller.diagnosis_workflow] : undefined;
|
|
119
|
+
const fixWorkflow = controller.fix_workflow ? workflows[controller.fix_workflow] : undefined;
|
|
120
|
+
if (controller.diagnosis_workflow && !diagnosisWorkflow) {
|
|
121
|
+
errors.push({ path: `${path}.diagnosis_workflow`, message: `references unknown workflow '${controller.diagnosis_workflow}'` });
|
|
122
|
+
}
|
|
123
|
+
if (controller.fix_workflow && !fixWorkflow) {
|
|
124
|
+
errors.push({ path: `${path}.fix_workflow`, message: `references unknown workflow '${controller.fix_workflow}'` });
|
|
125
|
+
}
|
|
126
|
+
const roleMap = controller.roles;
|
|
127
|
+
const requiredRoles = kindMetadata?.requiredRoleKeys ?? CONTROLLER_ROLE_KEYS;
|
|
128
|
+
if (!roleMap || typeof roleMap !== "object") {
|
|
129
|
+
errors.push({ path: `${path}.roles`, message: "controller requires roles" });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
for (const roleKey of requiredRoles) {
|
|
133
|
+
const roleName = roleMap[roleKey];
|
|
134
|
+
if (typeof roleName !== "string" || !roleName) {
|
|
135
|
+
errors.push({ path: `${path}.roles.${roleKey}`, message: "must be a non-empty role name" });
|
|
136
|
+
}
|
|
137
|
+
else if (!isSafeGeneratedIdentifier(roleName)) {
|
|
138
|
+
errors.push({ path: `${path}.roles.${roleKey}`, message: "role name must contain only letters, digits, underscores, and hyphens" });
|
|
139
|
+
}
|
|
140
|
+
else if (!roleNames.has(roleName)) {
|
|
141
|
+
errors.push({ path: `${path}.roles.${roleKey}`, message: `references unknown role '${roleName}'` });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (roleMap && typeof roleMap === "object") {
|
|
146
|
+
validateControllerWorkflowRoles(diagnosisWorkflow, kindMetadata?.diagnosisRoleKeys ?? ["analyzer", "analysis_reviewer"], roleMap, `${path}.diagnosis_workflow`, errors);
|
|
147
|
+
validateControllerWorkflowRoles(fixWorkflow, kindMetadata?.fixRoleKeys ?? ["surgeon", "fix_reviewer", "test_runner"], roleMap, `${path}.fix_workflow`, errors);
|
|
148
|
+
}
|
|
149
|
+
if (controller.stop_format !== "configured_stop_report" && controller.stop_format !== "zh_three_part") {
|
|
150
|
+
errors.push({ path: `${path}.stop_format`, message: "stop_format must be 'configured_stop_report' or legacy 'zh_three_part'" });
|
|
151
|
+
}
|
|
152
|
+
if (controller.policy !== undefined) {
|
|
153
|
+
validateControllerPolicy(controller.policy, `${path}.policy`, errors);
|
|
154
|
+
}
|
|
155
|
+
return errors;
|
|
156
|
+
}
|
package/dist/signals/index.d.ts
CHANGED
|
@@ -2,12 +2,14 @@ import type { TraceEntry, VerifierResultEntry } from "../hooks/state.js";
|
|
|
2
2
|
import type { ExecutionOutcome } from "../evolution/types.js";
|
|
3
3
|
export type SignalKind = "trace" | "verifier" | "external";
|
|
4
4
|
export type SignalStatus = "positive" | "negative" | "neutral";
|
|
5
|
+
export type ExternalSignalClassification = "protective" | "friction" | "unknown";
|
|
5
6
|
export interface SignalEvidence {
|
|
6
7
|
target?: string;
|
|
7
8
|
evidence?: string;
|
|
8
9
|
exit_code?: number;
|
|
9
10
|
artifact?: string;
|
|
10
11
|
message?: string;
|
|
12
|
+
evidence_ref?: string;
|
|
11
13
|
}
|
|
12
14
|
export interface SignalEnvelope {
|
|
13
15
|
id: string;
|
|
@@ -19,6 +21,10 @@ export interface SignalEnvelope {
|
|
|
19
21
|
detail?: string;
|
|
20
22
|
adapter?: string;
|
|
21
23
|
evidence_payload?: SignalEvidence;
|
|
24
|
+
classification?: ExternalSignalClassification;
|
|
25
|
+
confidence?: number;
|
|
26
|
+
mapping_policy?: string;
|
|
27
|
+
evolution_eligible?: boolean;
|
|
22
28
|
}
|
|
23
29
|
export interface ExternalSignalInput {
|
|
24
30
|
id?: string;
|
|
@@ -32,6 +38,10 @@ export interface ExternalSignalInput {
|
|
|
32
38
|
exit_code?: number;
|
|
33
39
|
artifact?: string;
|
|
34
40
|
message?: string;
|
|
41
|
+
evidence_ref?: string;
|
|
42
|
+
classification?: ExternalSignalClassification;
|
|
43
|
+
confidence?: number;
|
|
44
|
+
mapping_policy?: string;
|
|
35
45
|
}
|
|
36
46
|
export interface SignalSourceAdapter<T = unknown> {
|
|
37
47
|
name: string;
|