intentdna 1.6.5 → 1.7.4
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/README.md +26 -15
- 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 +3 -2
- package/dist/cli/commands/feedback.js +16 -5
- 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 +191 -186
- 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.d.ts +32 -0
- package/dist/cli/commands/verify.js +270 -31
- package/dist/cli/index.js +78 -11
- package/dist/compiler/activate.js +11 -6
- package/dist/compiler/cascade.d.ts +5 -1
- package/dist/compiler/cascade.js +74 -1
- package/dist/compiler/compile.js +38 -0
- package/dist/compiler/diagnostics.d.ts +17 -0
- package/dist/compiler/diagnostics.js +30 -0
- package/dist/compiler/index.d.ts +7 -0
- package/dist/compiler/index.js +13 -13
- package/dist/compiler/input-resolver.d.ts +32 -0
- package/dist/compiler/input-resolver.js +281 -0
- package/dist/compiler/provenance.d.ts +8 -0
- package/dist/compiler/provenance.js +127 -0
- package/dist/governance/index.d.ts +4 -3
- package/dist/governance/index.js +3 -4
- package/dist/governance/runtime-decision-event.d.ts +85 -0
- package/dist/governance/runtime-decision-event.js +231 -0
- package/dist/governance/types.d.ts +3 -3
- package/dist/governance/types.js +3 -3
- package/dist/hooks/cli.d.ts +10 -1
- package/dist/hooks/cli.js +199 -35
- package/dist/hooks/state.d.ts +5 -10
- package/dist/hooks/state.js +170 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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 +19 -49
- package/dist/mcp/tools-observability.js +26 -2
- package/dist/mcp/tools-state.d.ts +1 -1
- package/dist/mcp/tools-state.js +17 -7
- package/dist/report/kernel-report.d.ts +27 -0
- package/dist/report/kernel-report.js +60 -19
- package/dist/report/kernel-signals.d.ts +5 -2
- package/dist/report/kernel-signals.js +87 -2
- package/dist/report/report-package.d.ts +64 -0
- package/dist/report/report-package.js +90 -0
- package/dist/runtime/agent-md.d.ts +1 -0
- package/dist/runtime/agent-md.js +21 -3
- package/dist/runtime/claude-sdk.d.ts +9 -4
- package/dist/runtime/claude-sdk.js +9 -0
- package/dist/runtime/context-sources.d.ts +14 -0
- package/dist/runtime/context-sources.js +60 -0
- package/dist/runtime/plugin-adapter.d.ts +5 -1
- package/dist/runtime/plugin-adapter.js +2 -0
- package/dist/runtime/skill-adapter.d.ts +32 -4
- package/dist/runtime/skill-adapter.js +184 -9
- package/dist/runtime/workflow-runner.d.ts +1 -1
- package/dist/runtime/workflow-runner.js +1 -1
- package/dist/schema/types.d.ts +84 -0
- package/dist/schema/validate.js +156 -2
- package/dist/schema/validators/controllers.js +16 -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/package.json +1 -1
- package/spec/README.md +1 -1
- package/spec/foundation-hardening.md +2 -1
- package/spec/schema-spec.md +78 -1
package/dist/schema/validate.js
CHANGED
|
@@ -7,7 +7,25 @@
|
|
|
7
7
|
* - Codon values in valid ranges
|
|
8
8
|
* - No circular inheritance
|
|
9
9
|
*/
|
|
10
|
+
import { isAbsolute } from "node:path";
|
|
10
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
|
+
}
|
|
11
29
|
function isLegibilityAssetArray(value) {
|
|
12
30
|
return Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null &&
|
|
13
31
|
typeof item.type === "string" &&
|
|
@@ -72,6 +90,126 @@ function validateLegibilityAssets(dna, roleNamesSet, errors, warnings) {
|
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
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
|
+
}
|
|
75
213
|
function validateCodon(codon, path) {
|
|
76
214
|
const errors = [];
|
|
77
215
|
switch (codon.type) {
|
|
@@ -150,6 +288,9 @@ function validateContext(name, ctx, geneNames) {
|
|
|
150
288
|
function validateRole(name, role, geneNames) {
|
|
151
289
|
const errors = [];
|
|
152
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
|
+
}
|
|
153
294
|
// description required
|
|
154
295
|
if (!role.description) {
|
|
155
296
|
errors.push({ path: `${path}.description`, message: "role requires 'description'" });
|
|
@@ -245,8 +386,10 @@ function validateRole(name, role, geneNames) {
|
|
|
245
386
|
if (os.converter !== undefined && (typeof os.converter !== "string" || !os.converter)) {
|
|
246
387
|
errors.push({ path: `${osPath}.converter`, message: "must be a non-empty string" });
|
|
247
388
|
}
|
|
248
|
-
if (os.path !== undefined
|
|
249
|
-
|
|
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
|
+
}
|
|
250
393
|
}
|
|
251
394
|
}
|
|
252
395
|
return errors;
|
|
@@ -307,6 +450,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
|
307
450
|
if (!workflow.name) {
|
|
308
451
|
errors.push({ path: `${path}.name`, message: "workflow requires 'name'" });
|
|
309
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
|
+
}
|
|
310
456
|
// steps required and non-empty
|
|
311
457
|
if (!workflow.steps || workflow.steps.length === 0) {
|
|
312
458
|
errors.push({ path: `${path}.steps`, message: "workflow requires at least one step" });
|
|
@@ -322,6 +468,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
|
322
468
|
if (!step.id) {
|
|
323
469
|
errors.push({ path: `${stepPath}.id`, message: "step requires 'id'" });
|
|
324
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
|
+
}
|
|
325
474
|
else if (seenStepIds.has(step.id)) {
|
|
326
475
|
errors.push({ path: `${stepPath}.id`, message: `duplicate step id '${step.id}'` });
|
|
327
476
|
}
|
|
@@ -333,6 +482,9 @@ function validateWorkflow(workflow, roleNames, pathPrefix) {
|
|
|
333
482
|
if (!step.role) {
|
|
334
483
|
errors.push({ path: `${stepPath}.role`, message: "step requires 'role'" });
|
|
335
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
|
+
}
|
|
336
488
|
else if (roleNames.size > 0 && !roleNames.has(step.role)) {
|
|
337
489
|
errors.push({
|
|
338
490
|
path: `${stepPath}.role`,
|
|
@@ -583,6 +735,7 @@ export function validateDNA(dna) {
|
|
|
583
735
|
errors.push(...validateWorkflow(wfDef, roleNamesSet, `workflows.${wfName}`));
|
|
584
736
|
}
|
|
585
737
|
}
|
|
738
|
+
validateContextSourceHandoffOverlap(dna, errors);
|
|
586
739
|
if (dna.controllers) {
|
|
587
740
|
const workflowsForControllers = { ...(dna.workflows ?? {}) };
|
|
588
741
|
if (dna.workflow)
|
|
@@ -621,6 +774,7 @@ export function validateDNA(dna) {
|
|
|
621
774
|
}
|
|
622
775
|
}
|
|
623
776
|
validateLegibilityAssets(dna, roleNamesSet, errors, warnings);
|
|
777
|
+
validateContextSources(dna, errors);
|
|
624
778
|
// Validate context_files
|
|
625
779
|
if (dna.context_files) {
|
|
626
780
|
const cf = dna.context_files;
|
|
@@ -8,6 +8,12 @@ function isSafeControllerArtifactPath(path) {
|
|
|
8
8
|
!path.includes("}}") &&
|
|
9
9
|
path.includes("$ARGUMENTS");
|
|
10
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
|
+
}
|
|
11
17
|
function validateStringList(value, path, errors) {
|
|
12
18
|
if (!Array.isArray(value)) {
|
|
13
19
|
errors.push({ path, message: "must be an array of non-empty strings" });
|
|
@@ -69,6 +75,13 @@ export function validateController(controller, roleNames, workflows, path) {
|
|
|
69
75
|
if (!controller.name) {
|
|
70
76
|
errors.push({ path: `${path}.name`, message: "controller requires 'name'" });
|
|
71
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
|
+
}
|
|
72
85
|
const kindMetadata = isSupportedControllerKind(controller.kind)
|
|
73
86
|
? getControllerKindMetadata(controller.kind)
|
|
74
87
|
: undefined;
|
|
@@ -121,6 +134,9 @@ export function validateController(controller, roleNames, workflows, path) {
|
|
|
121
134
|
if (typeof roleName !== "string" || !roleName) {
|
|
122
135
|
errors.push({ path: `${path}.roles.${roleKey}`, message: "must be a non-empty role name" });
|
|
123
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
|
+
}
|
|
124
140
|
else if (!roleNames.has(roleName)) {
|
|
125
141
|
errors.push({ path: `${path}.roles.${roleKey}`, message: `references unknown role '${roleName}'` });
|
|
126
142
|
}
|
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;
|
package/dist/signals/index.js
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
import { extractGeneFromReason } from "../evolution/trace-bridge.js";
|
|
2
|
+
const SAFE_EXTERNAL_GENE = /^[A-Za-z0-9:_-]+$/;
|
|
3
|
+
function hasSafeExternalGene(gene) {
|
|
4
|
+
return typeof gene === "string" && SAFE_EXTERNAL_GENE.test(gene);
|
|
5
|
+
}
|
|
6
|
+
function hasNonBlankText(value) {
|
|
7
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
8
|
+
}
|
|
9
|
+
function isSignalStatus(value) {
|
|
10
|
+
return value === "positive" || value === "negative" || value === "neutral";
|
|
11
|
+
}
|
|
12
|
+
function isExternalSignalClassification(value) {
|
|
13
|
+
return value === undefined || value === "protective" || value === "friction" || value === "unknown";
|
|
14
|
+
}
|
|
15
|
+
function hasFiniteConfidence(value) {
|
|
16
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
17
|
+
}
|
|
18
|
+
function hasExternalSignalShape(signal) {
|
|
19
|
+
return isSignalStatus(signal.status) &&
|
|
20
|
+
hasNonBlankText(signal.source) &&
|
|
21
|
+
isExternalSignalClassification(signal.classification) &&
|
|
22
|
+
(signal.confidence === undefined || hasFiniteConfidence(signal.confidence));
|
|
23
|
+
}
|
|
24
|
+
function isRecord(value) {
|
|
25
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
26
|
+
}
|
|
27
|
+
function asString(value) {
|
|
28
|
+
return typeof value === "string" ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
function asNumber(value) {
|
|
31
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
32
|
+
}
|
|
2
33
|
function toSignalEvidence(result) {
|
|
3
34
|
const payload = {
|
|
4
35
|
target: result.target,
|
|
@@ -6,23 +37,75 @@ function toSignalEvidence(result) {
|
|
|
6
37
|
exit_code: result.exit_code,
|
|
7
38
|
artifact: result.artifact,
|
|
8
39
|
message: result.message,
|
|
40
|
+
evidence_ref: "evidence_ref" in result ? result.evidence_ref : undefined,
|
|
9
41
|
};
|
|
10
42
|
return Object.values(payload).some((value) => value !== undefined) ? payload : undefined;
|
|
11
43
|
}
|
|
44
|
+
function hasExternalSignalInputContract(signal) {
|
|
45
|
+
return (hasExternalSignalShape(signal) &&
|
|
46
|
+
hasFiniteConfidence(signal.confidence) &&
|
|
47
|
+
hasNonBlankText(signal.mapping_policy) &&
|
|
48
|
+
hasNonBlankText(signal.evidence_ref) &&
|
|
49
|
+
hasSafeExternalGene(signal.gene));
|
|
50
|
+
}
|
|
51
|
+
function hasExternalSignalEnvelopeContract(signal) {
|
|
52
|
+
return (isSignalStatus(signal.status) &&
|
|
53
|
+
hasNonBlankText(signal.source) &&
|
|
54
|
+
isExternalSignalClassification(signal.classification) &&
|
|
55
|
+
hasFiniteConfidence(signal.confidence) &&
|
|
56
|
+
hasNonBlankText(signal.mapping_policy) &&
|
|
57
|
+
hasNonBlankText(signal.evidence_payload?.evidence_ref) &&
|
|
58
|
+
hasSafeExternalGene(signal.gene));
|
|
59
|
+
}
|
|
12
60
|
function normalizeExternalSignals(adapterName, payload) {
|
|
13
61
|
const items = Array.isArray(payload) ? payload : [payload];
|
|
14
|
-
return items.map((
|
|
15
|
-
const timestamp =
|
|
62
|
+
return items.map((raw, index) => {
|
|
63
|
+
const timestamp = isRecord(raw) ? asString(raw.timestamp) ?? new Date().toISOString() : new Date().toISOString();
|
|
64
|
+
if (!isRecord(raw)) {
|
|
65
|
+
return {
|
|
66
|
+
id: `${adapterName}:invalid:${timestamp}:${index}`,
|
|
67
|
+
kind: "external",
|
|
68
|
+
status: "neutral",
|
|
69
|
+
timestamp,
|
|
70
|
+
source: `${adapterName}.invalid`,
|
|
71
|
+
adapter: adapterName,
|
|
72
|
+
classification: "unknown",
|
|
73
|
+
evolution_eligible: false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const item = {
|
|
77
|
+
id: asString(raw.id),
|
|
78
|
+
status: raw.status,
|
|
79
|
+
timestamp,
|
|
80
|
+
gene: asString(raw.gene),
|
|
81
|
+
source: asString(raw.source) ?? "",
|
|
82
|
+
detail: asString(raw.detail),
|
|
83
|
+
target: asString(raw.target),
|
|
84
|
+
evidence: asString(raw.evidence),
|
|
85
|
+
exit_code: asNumber(raw.exit_code),
|
|
86
|
+
artifact: asString(raw.artifact),
|
|
87
|
+
message: asString(raw.message),
|
|
88
|
+
evidence_ref: asString(raw.evidence_ref),
|
|
89
|
+
classification: raw.classification,
|
|
90
|
+
confidence: typeof raw.confidence === "number" ? raw.confidence : undefined,
|
|
91
|
+
mapping_policy: asString(raw.mapping_policy),
|
|
92
|
+
};
|
|
93
|
+
const shapeValid = hasExternalSignalShape(item);
|
|
94
|
+
const complete = hasExternalSignalInputContract(item);
|
|
16
95
|
return {
|
|
17
|
-
id: item.id ?? `${adapterName}:${item.source}:${timestamp}:${index}`,
|
|
96
|
+
id: item.id ?? `${adapterName}:${shapeValid ? item.source : "invalid"}:${timestamp}:${index}`,
|
|
18
97
|
kind: "external",
|
|
19
|
-
status: item.status,
|
|
98
|
+
status: shapeValid ? item.status : "neutral",
|
|
20
99
|
timestamp,
|
|
21
100
|
gene: item.gene,
|
|
22
|
-
source: item.source
|
|
101
|
+
source: hasNonBlankText(item.source) ? item.source : `${adapterName}.invalid`,
|
|
23
102
|
detail: item.detail ?? item.message ?? item.evidence,
|
|
24
103
|
adapter: adapterName,
|
|
25
104
|
evidence_payload: toSignalEvidence(item),
|
|
105
|
+
classification: isExternalSignalClassification(item.classification) ? (item.classification ?? "unknown") : "unknown",
|
|
106
|
+
confidence: hasFiniteConfidence(item.confidence) ? item.confidence : undefined,
|
|
107
|
+
mapping_policy: item.mapping_policy,
|
|
108
|
+
evolution_eligible: complete,
|
|
26
109
|
};
|
|
27
110
|
});
|
|
28
111
|
}
|
|
@@ -85,6 +168,8 @@ export function signalsToOutcomes(signals, dnaId = "default") {
|
|
|
85
168
|
for (const signal of signals) {
|
|
86
169
|
if (!signal.gene)
|
|
87
170
|
continue;
|
|
171
|
+
if (signal.kind === "external" && !hasExternalSignalEnvelopeContract(signal))
|
|
172
|
+
continue;
|
|
88
173
|
const effect = signal.status === "positive"
|
|
89
174
|
? "helped"
|
|
90
175
|
: signal.status === "negative"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const BUILTIN_TEMPLATES_DIR: string;
|
|
2
|
+
export declare const PROJECT_TEMPLATES_DIR = ".dna/templates";
|
|
3
|
+
export interface TemplateCatalogEntry {
|
|
4
|
+
name: string;
|
|
5
|
+
path: string;
|
|
6
|
+
namespace?: string;
|
|
7
|
+
displayName?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
contentHash: string;
|
|
10
|
+
source: "builtin" | "project";
|
|
11
|
+
keywords: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function calculateTemplateContentHash(content: string): string;
|
|
14
|
+
export declare function listTemplateCatalog(options?: {
|
|
15
|
+
projectDir?: string;
|
|
16
|
+
}): Promise<TemplateCatalogEntry[]>;
|
|
17
|
+
export declare function findTemplateCatalogEntry(name: string, options?: {
|
|
18
|
+
projectDir?: string;
|
|
19
|
+
}): Promise<TemplateCatalogEntry | undefined>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseYAML } from "../schema/yaml-parser.js";
|
|
6
|
+
import { getTemplateMatchMetadata } from "./metadata.js";
|
|
7
|
+
export const BUILTIN_TEMPLATES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates");
|
|
8
|
+
export const PROJECT_TEMPLATES_DIR = ".dna/templates";
|
|
9
|
+
function calculateContentHash(content) {
|
|
10
|
+
return createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 16);
|
|
11
|
+
}
|
|
12
|
+
async function readTemplateEntry(path, source) {
|
|
13
|
+
const content = await readFile(path, "utf-8");
|
|
14
|
+
const data = parseYAML(content);
|
|
15
|
+
const fileName = path.split("/").pop() ?? path;
|
|
16
|
+
const name = fileName.replace(/\.dna\.ya?ml$/, "");
|
|
17
|
+
const matchMetadata = getTemplateMatchMetadata(name);
|
|
18
|
+
return {
|
|
19
|
+
name,
|
|
20
|
+
path,
|
|
21
|
+
namespace: typeof data.namespace === "string" ? data.namespace : undefined,
|
|
22
|
+
displayName: typeof data.name === "string" ? data.name : undefined,
|
|
23
|
+
version: typeof data.version === "string" ? data.version : undefined,
|
|
24
|
+
contentHash: calculateContentHash(content),
|
|
25
|
+
source,
|
|
26
|
+
keywords: matchMetadata?.keywords ?? [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function collectTemplates(dir, source) {
|
|
30
|
+
try {
|
|
31
|
+
const files = await readdir(dir);
|
|
32
|
+
const templateFiles = files
|
|
33
|
+
.filter((file) => file.endsWith(".dna.yaml") || file.endsWith(".dna.yml"))
|
|
34
|
+
.sort();
|
|
35
|
+
const entries = [];
|
|
36
|
+
for (const file of templateFiles) {
|
|
37
|
+
entries.push(await readTemplateEntry(resolve(dir, file), source));
|
|
38
|
+
}
|
|
39
|
+
return entries;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function calculateTemplateContentHash(content) {
|
|
46
|
+
return calculateContentHash(content);
|
|
47
|
+
}
|
|
48
|
+
export async function listTemplateCatalog(options) {
|
|
49
|
+
const projectDir = options?.projectDir ?? process.cwd();
|
|
50
|
+
const builtin = await collectTemplates(BUILTIN_TEMPLATES_DIR, "builtin");
|
|
51
|
+
const project = await collectTemplates(resolve(projectDir, PROJECT_TEMPLATES_DIR), "project");
|
|
52
|
+
return [...builtin, ...project];
|
|
53
|
+
}
|
|
54
|
+
export async function findTemplateCatalogEntry(name, options) {
|
|
55
|
+
const entries = await listTemplateCatalog(options);
|
|
56
|
+
return entries.find((entry) => entry.name === name || entry.displayName === name || entry.namespace === name);
|
|
57
|
+
}
|
|
@@ -335,8 +335,8 @@ controllers:
|
|
|
335
335
|
- "- MAX_ROUNDS: stop, report max rounds reached and next focus"
|
|
336
336
|
stop_report:
|
|
337
337
|
- "On REQUEST_CHANGES, BLOCKED, or MAX_ROUNDS, output exactly these Chinese sections:"
|
|
338
|
-
- "结论:state the direct stop trigger, current round, and current commit if one exists."
|
|
339
|
-
- "为什么停止:explain the workflow principle in plain language; if this is a current-round new red, explain that continuing would build on a bad baseline."
|
|
338
|
+
- "结论:state the direct stop trigger, current round, and current commit if one exists; cite verifier result_id, trace_id, or ArtifactManifest artifact id."
|
|
339
|
+
- "为什么停止:explain the workflow principle in plain language; if this is a current-round new red, explain that continuing would build on a bad baseline; prose-only claims are not sufficient."
|
|
340
340
|
- "是否回滚:say whether rollback/revert is recommended. If the commit direction looks wrong or the blast radius is unclear, recommend revert; if the failure is narrow and fixable, recommend fix-forward."
|
|
341
341
|
- "下一步:give one concrete next action. If recommending fix-forward, include a copy-paste single-fix prompt with current commit, exact failure, allowed scope, forbidden actions, verification command, commit expectation, and stop conditions."
|
|
342
342
|
constraints:
|
package/package.json
CHANGED
package/spec/README.md
CHANGED
|
@@ -15,7 +15,7 @@ This directory contains engineering specs. Use [ROADMAP.md](../ROADMAP.md) as th
|
|
|
15
15
|
|
|
16
16
|
| Spec | Status | Purpose |
|
|
17
17
|
|------|--------|---------|
|
|
18
|
-
| [schema-spec.md](schema-spec.md) | Canonical | Intent DNA document structure, genes, roles, workflows, cascade, and IR contract. |
|
|
18
|
+
| [schema-spec.md](schema-spec.md) | Canonical | Intent DNA document structure, genes, roles, workflows, cascade, Enterprise bundle inheritance, RuntimeDecisionEvent, and IR contract. |
|
|
19
19
|
| [foundation-hardening.md](foundation-hardening.md) | Active reference | Reliability, governance, MCP, and tool-experience hardening plan. |
|
|
20
20
|
| [hooks-infra-harness-hardening.md](hooks-infra-harness-hardening.md) | Active reference | Hook boundary schema, enforcement result types, state manager, context/reflection/workflow gates. |
|
|
21
21
|
| [workflow-pipeline.md](workflow-pipeline.md) | Active reference | Workflow pipeline design and state management unification. |
|
package/spec/schema-spec.md
CHANGED
|
@@ -386,7 +386,7 @@ Or shorthand: `max_rounds: 3` (equivalent to max_retries: 2)
|
|
|
386
386
|
|
|
387
387
|
## Cascade (Inheritance)
|
|
388
388
|
|
|
389
|
-
DNA configs inherit from parent layers using CSS-like cascade.
|
|
389
|
+
DNA configs inherit from parent layers using CSS-like cascade. The enterprise safety cascade order is Species → Enterprise → Project → Personal → Role → Context → Task.
|
|
390
390
|
|
|
391
391
|
```yaml
|
|
392
392
|
cascade:
|
|
@@ -404,6 +404,83 @@ cascade:
|
|
|
404
404
|
### Threshold Protection
|
|
405
405
|
Threshold codons from lower-priority layers **cannot be removed** by higher-priority layers. This ensures safety invariants propagate up the chain.
|
|
406
406
|
|
|
407
|
+
Species and Enterprise thresholds are organization red lines. Project, Personal, Role, Context, and Task layers may add stricter thresholds, but they cannot relax, remove, bypass, or downgrade Species/Enterprise thresholds.
|
|
408
|
+
|
|
409
|
+
### Enterprise Bundle Inheritance (v1.7.x local/shared slice)
|
|
410
|
+
|
|
411
|
+
The v1.7.x Enterprise Safety Slice is scoped to local and shared policy bundle references only; it does not implement remote registry, signing, push/pull, or SaaS permissions. In v1.7.1 this section freezes the contract and resolver boundary; the concrete `dna sync` / `dna verify` resolver implementation is planned for v1.7.2.
|
|
412
|
+
|
|
413
|
+
Candidate bundle locations:
|
|
414
|
+
|
|
415
|
+
- `INTENTDNA_ENTERPRISE_POLICY_DIR` — admin/team-controlled shared root for authoritative local dogfood
|
|
416
|
+
- `.dna/policies/enterprise/*.dna.yaml` — project-local bundle for development and fixture use
|
|
417
|
+
- `~/.intentdna/policies/enterprise/*.dna.yaml` — user-local bundle for development and personal dogfood
|
|
418
|
+
- `test/fixtures/enterprise-policies/` — test-only fixture root
|
|
419
|
+
|
|
420
|
+
Project-local and user-local bundles are not organization-authoritative by themselves. They must be treated as local/dev provenance unless the future v1.7.2 resolver can tie them to an admin-controlled root or pinned fingerprint.
|
|
421
|
+
|
|
422
|
+
Supported reference forms:
|
|
423
|
+
|
|
424
|
+
```yaml
|
|
425
|
+
cascade:
|
|
426
|
+
inherits:
|
|
427
|
+
- "enterprise:baseline" # resolves baseline.dna.yaml from candidate dirs
|
|
428
|
+
- "file:../shared/baseline.dna.yaml" # explicit local/shared file
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
For v1.7.2, `dna sync` must materialize the resolved bundle id/version, source path, fingerprint, cascade layer fingerprints, and compiled IR hash into provenance. `dna verify` must reject unresolved references, inheritance cycles, missing bundle metadata, and attempts by lower layers to relax Species/Enterprise thresholds.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Enterprise Provenance Contract (v1.7.1)
|
|
436
|
+
|
|
437
|
+
v1.7.1 freezes the enterprise evidence contract before the full implementation lands. `RuntimeDecisionEvent` is required for enterprise governance evidence; legacy trace/audit/state records without this provenance remain diagnostic only.
|
|
438
|
+
|
|
439
|
+
### PolicyBundle
|
|
440
|
+
|
|
441
|
+
A policy bundle records the policy source that produced enforcement-relevant constraints:
|
|
442
|
+
|
|
443
|
+
| Field | Required | Description |
|
|
444
|
+
|-------|----------|-------------|
|
|
445
|
+
| `policy_bundle_id` | yes | Stable bundle id, e.g. `enterprise_baseline` |
|
|
446
|
+
| `policy_bundle_version` | yes | Bundle version used for the decision |
|
|
447
|
+
| `source_ref` | yes | `enterprise:baseline`, `file:...`, or a local source path |
|
|
448
|
+
| `fingerprint` | yes | Stable content/source fingerprint |
|
|
449
|
+
|
|
450
|
+
### CascadeLayerFingerprint
|
|
451
|
+
|
|
452
|
+
`cascade_layers` must include every layer: `species`, `enterprise`, `project`, `personal`, `role`, `context`, and `task`.
|
|
453
|
+
|
|
454
|
+
| Field | Required | Description |
|
|
455
|
+
|-------|----------|-------------|
|
|
456
|
+
| `layer` | yes | One of the fixed cascade layer names |
|
|
457
|
+
| `source_id` | yes | DNA or placeholder source id |
|
|
458
|
+
| `source_ref` | yes | Resolved source path/reference or `unknown` / `unsupported` |
|
|
459
|
+
| `fingerprint` | yes | Layer fingerprint or `unknown` / `unsupported` |
|
|
460
|
+
| `status` | yes | `active`, `unknown`, or `unsupported` |
|
|
461
|
+
|
|
462
|
+
### RuntimeDecisionEvent
|
|
463
|
+
|
|
464
|
+
Every enterprise runtime decision must include these required fields:
|
|
465
|
+
|
|
466
|
+
```text
|
|
467
|
+
schema_version, event_id, timestamp,
|
|
468
|
+
org_id, team_id, user_id, project_id,
|
|
469
|
+
agent_id, agent_role, agent_type, session_agent,
|
|
470
|
+
harness_adapter, runtime,
|
|
471
|
+
policy_bundle_id, policy_bundle_version,
|
|
472
|
+
cascade_layers, winning_constraint, compiled_ir_hash,
|
|
473
|
+
decision, decision_reason,
|
|
474
|
+
enforcement_point, harness_runtime_context,
|
|
475
|
+
session_id, run_id, step_id,
|
|
476
|
+
tool_name, action_kind, resource_ref,
|
|
477
|
+
evidence_refs
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
`decision` is one of `allow`, `warn`, `block`, `escalate`, or `validate`. `enforcement_point` is one of `hook`, `sdk`, `ci`, `plugin_runtime`, `mcp_external_write`, `unknown`, or `unsupported`.
|
|
481
|
+
|
|
482
|
+
The first implementation may fill unavailable dimensions with explicit `unknown` or `unsupported` values, but it must not omit required fields. Placeholder values make the event syntactically valid for phased rollout, not enterprise-authoritative: any required provenance field that remains `unknown` / `unsupported`, or any `legacy` / placeholder `evidence_refs` entry, keeps the event classified as `legacy_diagnostic`. Events missing required fields or classified as diagnostic cannot enter enterprise compliance statistics. MCP, reports, dashboards, and evolution consume this event as evidence; they do not create policy truth.
|
|
483
|
+
|
|
407
484
|
---
|
|
408
485
|
|
|
409
486
|
## Variables
|