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
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dna templates
|
|
2
|
+
* dna templates — template catalog and deployment commands.
|
|
3
3
|
*/
|
|
4
4
|
import { readdirSync, copyFileSync, existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
5
5
|
import { resolve, join } from "node:path";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { findTemplateCatalogEntry, listTemplateCatalog } from "../../templates/catalog.js";
|
|
7
9
|
/**
|
|
8
10
|
* Resolve the global intentdna templates directory.
|
|
9
11
|
* Strategy: npm root -g + /intentdna/dist/templates
|
|
@@ -45,6 +47,53 @@ function verifyProjectRoot(cwd) {
|
|
|
45
47
|
}
|
|
46
48
|
return cwd;
|
|
47
49
|
}
|
|
50
|
+
export async function runTemplatesList(opts = {}) {
|
|
51
|
+
const templates = await listTemplateCatalog({ projectDir: opts.projectDir });
|
|
52
|
+
if (templates.length === 0) {
|
|
53
|
+
process.stderr.write("No templates found.\n");
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
process.stderr.write(`Available templates (${templates.length}):\n`);
|
|
57
|
+
for (const template of templates) {
|
|
58
|
+
const ns = template.namespace ? ` ns=${template.namespace}` : "";
|
|
59
|
+
const version = template.version ? ` v${template.version}` : "";
|
|
60
|
+
process.stderr.write(` - ${template.name} [${template.source}]${ns}${version}`);
|
|
61
|
+
if (template.displayName)
|
|
62
|
+
process.stderr.write(` — ${template.displayName}`);
|
|
63
|
+
process.stderr.write("\n");
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
export async function runTemplatesShow(opts) {
|
|
68
|
+
if (!opts.name) {
|
|
69
|
+
process.stderr.write("Usage: dna templates show <name>\n");
|
|
70
|
+
return 2;
|
|
71
|
+
}
|
|
72
|
+
const template = await findTemplateCatalogEntry(opts.name, { projectDir: opts.projectDir });
|
|
73
|
+
if (!template) {
|
|
74
|
+
process.stderr.write(`Unknown template: ${opts.name}\n`);
|
|
75
|
+
return 2;
|
|
76
|
+
}
|
|
77
|
+
const content = await readFile(template.path, "utf-8");
|
|
78
|
+
process.stderr.write(`Template: ${template.name}\n`);
|
|
79
|
+
process.stderr.write(`Source: ${template.source}\n`);
|
|
80
|
+
process.stderr.write(`Path: ${template.path}\n`);
|
|
81
|
+
if (template.displayName)
|
|
82
|
+
process.stderr.write(`Name: ${template.displayName}\n`);
|
|
83
|
+
if (template.namespace)
|
|
84
|
+
process.stderr.write(`Namespace: ${template.namespace}\n`);
|
|
85
|
+
if (template.version)
|
|
86
|
+
process.stderr.write(`Version: ${template.version}\n`);
|
|
87
|
+
process.stderr.write(`Content hash: ${template.contentHash}\n`);
|
|
88
|
+
if (template.keywords.length > 0) {
|
|
89
|
+
process.stderr.write(`Match keywords: ${template.keywords.join(", ")}\n`);
|
|
90
|
+
}
|
|
91
|
+
process.stderr.write("\n---\n");
|
|
92
|
+
process.stdout.write(content);
|
|
93
|
+
if (!content.endsWith("\n"))
|
|
94
|
+
process.stdout.write("\n");
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
48
97
|
export async function runTemplatesDeploy(opts = {}) {
|
|
49
98
|
try {
|
|
50
99
|
const cwd = process.cwd();
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Validate DNA files for structural correctness.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { formatDiagnostic, loadDNAWithDiagnostics } from "../../compiler/index.js";
|
|
7
7
|
export async function runValidate(opts) {
|
|
8
8
|
if (opts.files.length === 0) {
|
|
9
9
|
process.stderr.write("Error: no files specified\n");
|
|
@@ -11,17 +11,23 @@ export async function runValidate(opts) {
|
|
|
11
11
|
}
|
|
12
12
|
let allValid = true;
|
|
13
13
|
for (const file of opts.files) {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const result = await loadDNAWithDiagnostics(file);
|
|
15
|
+
const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
16
|
+
const warnings = result.diagnostics.filter((diagnostic) => diagnostic.severity === "warning");
|
|
17
|
+
if (errors.length > 0 || !result.dna) {
|
|
18
|
+
allValid = false;
|
|
19
|
+
process.stderr.write(`invalid: ${file}\n`);
|
|
16
20
|
if (!opts.quiet) {
|
|
17
|
-
|
|
21
|
+
for (const diagnostic of result.diagnostics) {
|
|
22
|
+
process.stderr.write(`${formatDiagnostic(diagnostic)}\n`);
|
|
23
|
+
}
|
|
18
24
|
}
|
|
25
|
+
continue;
|
|
19
26
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
process.stderr.write(`${err.message}\n`);
|
|
27
|
+
if (!opts.quiet) {
|
|
28
|
+
process.stderr.write(`valid: ${file}\n`);
|
|
29
|
+
for (const warning of warnings) {
|
|
30
|
+
process.stderr.write(`${formatDiagnostic(warning)}\n`);
|
|
25
31
|
}
|
|
26
32
|
}
|
|
27
33
|
}
|
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* 1 — drift detected (modified or missing files)
|
|
10
10
|
* 2 — usage error (e.g. lock file not found)
|
|
11
11
|
*/
|
|
12
|
+
import type { CompiledProvenanceManifest } from "../../schema/types.js";
|
|
12
13
|
export interface VerifyOptions {
|
|
13
14
|
lockFile: string;
|
|
14
15
|
stats?: boolean;
|
|
15
16
|
health?: boolean;
|
|
17
|
+
strict?: boolean;
|
|
18
|
+
projectDir?: string;
|
|
16
19
|
}
|
|
17
20
|
export interface LockFileEntry {
|
|
18
21
|
sha256: string;
|
|
@@ -21,6 +24,8 @@ export interface LockFile {
|
|
|
21
24
|
synced_at: string;
|
|
22
25
|
dna_sources: string[];
|
|
23
26
|
outputs: Record<string, LockFileEntry>;
|
|
27
|
+
compiled_ir_hash?: string;
|
|
28
|
+
provenance?: CompiledProvenanceManifest;
|
|
24
29
|
}
|
|
25
30
|
export interface VerifyResult {
|
|
26
31
|
file: string;
|
|
@@ -28,9 +33,36 @@ export interface VerifyResult {
|
|
|
28
33
|
expected?: string;
|
|
29
34
|
actual?: string;
|
|
30
35
|
}
|
|
36
|
+
export interface ProvenanceVerifyResult {
|
|
37
|
+
source_id: string;
|
|
38
|
+
file: string;
|
|
39
|
+
status: "match" | "mismatch" | "missing";
|
|
40
|
+
expected?: string;
|
|
41
|
+
actual?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface InheritanceGraphVerifyResult {
|
|
44
|
+
source_id: string;
|
|
45
|
+
file: string;
|
|
46
|
+
status: "match" | "mismatch" | "missing" | "extra";
|
|
47
|
+
expected?: string;
|
|
48
|
+
actual?: string;
|
|
49
|
+
}
|
|
31
50
|
export declare function sha256(content: string): string;
|
|
32
51
|
export declare function parseLockFile(raw: string): LockFile;
|
|
33
52
|
export declare function verifyLock(lock: LockFile): Promise<VerifyResult[]>;
|
|
53
|
+
export declare function verifyInheritanceGraph(lock: LockFile): Promise<InheritanceGraphVerifyResult[]>;
|
|
54
|
+
export declare function verifyProvenance(provenance?: CompiledProvenanceManifest): Promise<ProvenanceVerifyResult[]>;
|
|
55
|
+
export interface StrictRuntimeEvidenceExpectation {
|
|
56
|
+
compiledIrHash?: string;
|
|
57
|
+
policyBundleId?: string;
|
|
58
|
+
policyBundleVersion?: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function verifyStrictRuntimeEvidence(projectDir: string, expected?: StrictRuntimeEvidenceExpectation): Promise<{
|
|
61
|
+
ok: boolean;
|
|
62
|
+
total: number;
|
|
63
|
+
enterprise: number;
|
|
64
|
+
errors: string[];
|
|
65
|
+
}>;
|
|
34
66
|
export declare function runVerify(opts: VerifyOptions): Promise<number>;
|
|
35
67
|
export interface HealthCheckResult {
|
|
36
68
|
name: string;
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { readFile } from "node:fs/promises";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
|
-
import {
|
|
14
|
+
import { dirname, resolve } from "node:path";
|
|
15
|
+
import { readRuntimeDecisionEvents, readTraces } from "../../hooks/state.js";
|
|
16
|
+
import { expandDNAInputs, formatDiagnostics } from "../../compiler/index.js";
|
|
17
|
+
import { DNADiagnosticsError } from "../../compiler/diagnostics.js";
|
|
18
|
+
import { validateRuntimeDecisionEvent } from "../../governance/index.js";
|
|
15
19
|
// ── Helpers ────────────────────────────────────────────────
|
|
16
20
|
export function sha256(content) {
|
|
17
21
|
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
@@ -52,6 +56,108 @@ export async function verifyLock(lock) {
|
|
|
52
56
|
}
|
|
53
57
|
return results;
|
|
54
58
|
}
|
|
59
|
+
function enterpriseDirsFromLock(lock) {
|
|
60
|
+
const dirs = new Set();
|
|
61
|
+
for (const source of lock.provenance?.sources ?? []) {
|
|
62
|
+
if (source.source_ref.startsWith("enterprise:"))
|
|
63
|
+
dirs.add(dirname(source.resolved_path));
|
|
64
|
+
}
|
|
65
|
+
return [...dirs];
|
|
66
|
+
}
|
|
67
|
+
function sourceGraphKey(source) {
|
|
68
|
+
return JSON.stringify([source.source_id, source.source_ref, source.inherited_by ?? ""]);
|
|
69
|
+
}
|
|
70
|
+
function comparableSource(source) {
|
|
71
|
+
return JSON.stringify({
|
|
72
|
+
source_ref: source.source_ref,
|
|
73
|
+
resolved_path: source.resolved_path,
|
|
74
|
+
fingerprint: source.fingerprint,
|
|
75
|
+
inherited_by: source.inherited_by,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function verifyInheritanceGraph(lock) {
|
|
79
|
+
if (!lock.provenance)
|
|
80
|
+
return [];
|
|
81
|
+
const expanded = await expandDNAInputs(lock.dna_sources, { enterprisePolicyDirs: enterpriseDirsFromLock(lock) });
|
|
82
|
+
const current = new Map(expanded.provenance.map((source) => [sourceGraphKey(source), source]));
|
|
83
|
+
const locked = new Map(lock.provenance.sources.map((source) => [sourceGraphKey(source), source]));
|
|
84
|
+
const results = [];
|
|
85
|
+
for (const [key, expected] of locked) {
|
|
86
|
+
const actual = current.get(key);
|
|
87
|
+
if (!actual) {
|
|
88
|
+
results.push({ source_id: expected.source_id, file: expected.resolved_path, status: "missing", expected: comparableSource(expected) });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const expectedComparable = comparableSource(expected);
|
|
92
|
+
const actualComparable = comparableSource(actual);
|
|
93
|
+
results.push({
|
|
94
|
+
source_id: expected.source_id,
|
|
95
|
+
file: expected.resolved_path,
|
|
96
|
+
status: expectedComparable === actualComparable ? "match" : "mismatch",
|
|
97
|
+
expected: expectedComparable,
|
|
98
|
+
actual: actualComparable,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
for (const [key, actual] of current) {
|
|
102
|
+
if (!locked.has(key)) {
|
|
103
|
+
results.push({ source_id: actual.source_id, file: actual.resolved_path, status: "extra", actual: comparableSource(actual) });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return results;
|
|
107
|
+
}
|
|
108
|
+
export async function verifyProvenance(provenance) {
|
|
109
|
+
if (!provenance)
|
|
110
|
+
return [];
|
|
111
|
+
const results = [];
|
|
112
|
+
for (const source of provenance.sources) {
|
|
113
|
+
try {
|
|
114
|
+
const content = await readFile(source.resolved_path, "utf-8");
|
|
115
|
+
const actual = `sha256:${sha256(content)}`;
|
|
116
|
+
results.push({
|
|
117
|
+
source_id: source.source_id,
|
|
118
|
+
file: source.resolved_path,
|
|
119
|
+
status: actual === source.fingerprint ? "match" : "mismatch",
|
|
120
|
+
expected: source.fingerprint,
|
|
121
|
+
actual,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
results.push({
|
|
126
|
+
source_id: source.source_id,
|
|
127
|
+
file: source.resolved_path,
|
|
128
|
+
status: "missing",
|
|
129
|
+
expected: source.fingerprint,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
export async function verifyStrictRuntimeEvidence(projectDir, expected = {}) {
|
|
136
|
+
const events = await readRuntimeDecisionEvents(projectDir, 7);
|
|
137
|
+
const errors = [];
|
|
138
|
+
let enterprise = 0;
|
|
139
|
+
for (const event of events) {
|
|
140
|
+
const result = validateRuntimeDecisionEvent(event);
|
|
141
|
+
if (!result.valid)
|
|
142
|
+
errors.push(...result.errors.map((error) => `${event.event_id ?? "unknown"}: ${error}`));
|
|
143
|
+
if (expected.compiledIrHash && event.compiled_ir_hash !== expected.compiledIrHash) {
|
|
144
|
+
errors.push(`${event.event_id ?? "unknown"}: compiled_ir_hash does not match current lock`);
|
|
145
|
+
}
|
|
146
|
+
if (expected.policyBundleId && event.policy_bundle_id !== expected.policyBundleId) {
|
|
147
|
+
errors.push(`${event.event_id ?? "unknown"}: policy_bundle_id does not match current lock`);
|
|
148
|
+
}
|
|
149
|
+
if (expected.policyBundleVersion && event.policy_bundle_version !== expected.policyBundleVersion) {
|
|
150
|
+
errors.push(`${event.event_id ?? "unknown"}: policy_bundle_version does not match current lock`);
|
|
151
|
+
}
|
|
152
|
+
if (result.classification === "enterprise_evidence")
|
|
153
|
+
enterprise++;
|
|
154
|
+
else
|
|
155
|
+
errors.push(`${event.event_id ?? "unknown"}: RuntimeDecisionEvent is diagnostic-only, not enterprise evidence`);
|
|
156
|
+
}
|
|
157
|
+
if (events.length === 0)
|
|
158
|
+
errors.push("no RuntimeDecisionEvent evidence found");
|
|
159
|
+
return { ok: errors.length === 0, total: events.length, enterprise, errors };
|
|
160
|
+
}
|
|
55
161
|
// ── CLI entry ──────────────────────────────────────────────
|
|
56
162
|
export async function runVerify(opts) {
|
|
57
163
|
// Handle --health mode
|
|
@@ -79,8 +185,24 @@ export async function runVerify(opts) {
|
|
|
79
185
|
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
80
186
|
return 2;
|
|
81
187
|
}
|
|
188
|
+
let inheritanceResults = [];
|
|
189
|
+
if (lock.provenance) {
|
|
190
|
+
try {
|
|
191
|
+
inheritanceResults = await verifyInheritanceGraph(lock);
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
if (err instanceof DNADiagnosticsError) {
|
|
195
|
+
process.stderr.write(`Error: DNA inheritance graph is invalid:\n${formatDiagnostics(err.diagnostics)}\n`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
process.stderr.write(`Error: DNA inheritance graph is invalid: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
199
|
+
}
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
82
203
|
// Verify
|
|
83
204
|
const results = await verifyLock(lock);
|
|
205
|
+
const provenanceResults = await verifyProvenance(lock.provenance);
|
|
84
206
|
// Report
|
|
85
207
|
let hasFailure = false;
|
|
86
208
|
for (const r of results) {
|
|
@@ -96,17 +218,62 @@ export async function runVerify(opts) {
|
|
|
96
218
|
hasFailure = true;
|
|
97
219
|
}
|
|
98
220
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
221
|
+
for (const r of inheritanceResults) {
|
|
222
|
+
if (r.status === "match") {
|
|
223
|
+
process.stderr.write(` ok inheritance ${r.source_id}: ${r.file}\n`);
|
|
224
|
+
}
|
|
225
|
+
else if (r.status === "mismatch") {
|
|
226
|
+
process.stderr.write(` MISMATCH inheritance ${r.source_id}: ${r.file}\n`);
|
|
227
|
+
hasFailure = true;
|
|
228
|
+
}
|
|
229
|
+
else if (r.status === "missing") {
|
|
230
|
+
process.stderr.write(` MISSING inheritance ${r.source_id}: ${r.file}\n`);
|
|
231
|
+
hasFailure = true;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
process.stderr.write(` EXTRA inheritance ${r.source_id}: ${r.file}\n`);
|
|
235
|
+
hasFailure = true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const r of provenanceResults) {
|
|
239
|
+
if (r.status === "match") {
|
|
240
|
+
process.stderr.write(` ok provenance ${r.source_id}: ${r.file}\n`);
|
|
241
|
+
}
|
|
242
|
+
else if (r.status === "mismatch") {
|
|
243
|
+
process.stderr.write(` MISMATCH provenance ${r.source_id}: ${r.file}\n`);
|
|
244
|
+
hasFailure = true;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
process.stderr.write(` MISSING provenance ${r.source_id}: ${r.file}\n`);
|
|
248
|
+
hasFailure = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (lock.provenance?.compiled_ir_hash && lock.compiled_ir_hash && lock.provenance.compiled_ir_hash !== lock.compiled_ir_hash) {
|
|
252
|
+
process.stderr.write(" MISMATCH compiled_ir_hash differs between lock and provenance\n");
|
|
253
|
+
hasFailure = true;
|
|
254
|
+
}
|
|
255
|
+
const total = results.length + inheritanceResults.length + provenanceResults.length;
|
|
256
|
+
const ok = results.filter((r) => r.status === "match").length + inheritanceResults.filter((r) => r.status === "match").length + provenanceResults.filter((r) => r.status === "match").length;
|
|
101
257
|
const failed = total - ok;
|
|
102
258
|
if (hasFailure) {
|
|
103
259
|
process.stderr.write(`\nDrift detected: ${failed}/${total} file(s) changed since last sync\n`);
|
|
104
260
|
return 1;
|
|
105
261
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
262
|
+
if (opts.strict) {
|
|
263
|
+
const strictProjectDir = opts.projectDir ?? dirname(dirname(resolve(opts.lockFile)));
|
|
264
|
+
const strict = await verifyStrictRuntimeEvidence(strictProjectDir, {
|
|
265
|
+
compiledIrHash: lock.compiled_ir_hash,
|
|
266
|
+
policyBundleId: lock.provenance?.policy_bundle_id,
|
|
267
|
+
policyBundleVersion: lock.provenance?.policy_bundle_version,
|
|
268
|
+
});
|
|
269
|
+
if (!strict.ok) {
|
|
270
|
+
process.stderr.write(`\nStrict runtime evidence failed: ${strict.errors.join("; ")}\n`);
|
|
271
|
+
return 1;
|
|
272
|
+
}
|
|
273
|
+
process.stderr.write(`\nStrict runtime evidence: ${strict.enterprise}/${strict.total} enterprise event(s) valid\n`);
|
|
109
274
|
}
|
|
275
|
+
process.stderr.write(`\nAll ${total} file(s) verified — no drift\n`);
|
|
276
|
+
return 0;
|
|
110
277
|
}
|
|
111
278
|
// ── Stats Mode ────────────────────────────────────────────
|
|
112
279
|
async function runStats() {
|
|
@@ -218,6 +385,18 @@ function truncate(s, max) {
|
|
|
218
385
|
return s;
|
|
219
386
|
return s.slice(0, max - 3) + "...";
|
|
220
387
|
}
|
|
388
|
+
async function checkTemplateVersionsQuiet(configs) {
|
|
389
|
+
const stderr = process.stderr;
|
|
390
|
+
const originalWrite = stderr.write;
|
|
391
|
+
stderr.write = (() => true);
|
|
392
|
+
try {
|
|
393
|
+
const { checkTemplateVersions } = await import("./sync.js");
|
|
394
|
+
return await checkTemplateVersions(configs);
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
stderr.write = originalWrite;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
221
400
|
/**
|
|
222
401
|
* U3: Comprehensive health report for DNA installation.
|
|
223
402
|
* Checks IR, configs, templates, hooks, and trace system.
|
|
@@ -225,7 +404,7 @@ function truncate(s, max) {
|
|
|
225
404
|
export async function runHealth() {
|
|
226
405
|
const cwd = process.cwd();
|
|
227
406
|
const checks = [];
|
|
228
|
-
const { resolve } = await import("node:path");
|
|
407
|
+
const { resolve, basename } = await import("node:path");
|
|
229
408
|
const { stat, readFile: rf, readdir } = await import("node:fs/promises");
|
|
230
409
|
// 1. Check compiled IR
|
|
231
410
|
const irPath = resolve(cwd, ".dna", "compiled", "ir.json");
|
|
@@ -245,15 +424,15 @@ export async function runHealth() {
|
|
|
245
424
|
checks.push({ name: "Compiled IR", status: "warn", detail: "Legacy format — run `dna sync` to upgrade" });
|
|
246
425
|
}
|
|
247
426
|
else {
|
|
248
|
-
checks.push({ name: "Compiled IR", status: "fail", detail: "Unrecognized format" });
|
|
427
|
+
checks.push({ name: "Compiled IR", status: "fail", detail: "Unrecognized format — run `dna sync` to regenerate" });
|
|
249
428
|
}
|
|
250
429
|
}
|
|
251
430
|
catch {
|
|
252
431
|
checks.push({ name: "Compiled IR", status: "fail", detail: "Not found — run `dna sync`" });
|
|
253
432
|
}
|
|
254
433
|
// 2. Check DNA config files
|
|
255
|
-
const {
|
|
256
|
-
const configs = await
|
|
434
|
+
const { detectDNAConfigs } = await import("../../compiler/index.js");
|
|
435
|
+
const configs = await detectDNAConfigs(cwd);
|
|
257
436
|
if (configs.length > 0) {
|
|
258
437
|
checks.push({ name: "DNA configs", status: "pass", detail: `${configs.length} config(s) found` });
|
|
259
438
|
}
|
|
@@ -262,51 +441,72 @@ export async function runHealth() {
|
|
|
262
441
|
}
|
|
263
442
|
// 3. Check config validity
|
|
264
443
|
if (configs.length > 0) {
|
|
265
|
-
const {
|
|
444
|
+
const { loadDNAWithDiagnostics, formatDiagnostic } = await import("../../compiler/index.js");
|
|
266
445
|
let valid = 0;
|
|
267
446
|
let invalid = 0;
|
|
447
|
+
let warningCount = 0;
|
|
268
448
|
const errors = [];
|
|
269
449
|
for (const configPath of configs) {
|
|
270
|
-
|
|
271
|
-
|
|
450
|
+
const result = await loadDNAWithDiagnostics(configPath);
|
|
451
|
+
const errorDiagnostics = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
452
|
+
const warningDiagnostics = result.diagnostics.filter((diagnostic) => diagnostic.severity === "warning");
|
|
453
|
+
warningCount += warningDiagnostics.length;
|
|
454
|
+
if (errorDiagnostics.length === 0 && result.dna) {
|
|
272
455
|
valid++;
|
|
273
456
|
}
|
|
274
|
-
|
|
457
|
+
else {
|
|
275
458
|
invalid++;
|
|
276
|
-
|
|
277
|
-
errors.push(`${basename(configPath)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
459
|
+
errors.push(`${basename(configPath)}: ${errorDiagnostics.map(formatDiagnostic).join(", ")}`);
|
|
278
460
|
}
|
|
279
461
|
}
|
|
280
462
|
if (invalid === 0) {
|
|
281
|
-
|
|
463
|
+
const detail = warningCount > 0 ? `${valid} config(s) valid, ${warningCount} warning(s)` : `${valid} config(s) valid`;
|
|
464
|
+
checks.push({ name: "Config validity", status: warningCount > 0 ? "warn" : "pass", detail });
|
|
282
465
|
}
|
|
283
466
|
else {
|
|
284
|
-
checks.push({ name: "Config validity", status: "fail", detail: errors.join("; ") });
|
|
467
|
+
checks.push({ name: "Config validity", status: "fail", detail: `${errors.join("; ")} — fix listed config errors, then run \`dna validate\`` });
|
|
285
468
|
}
|
|
286
469
|
}
|
|
287
470
|
// 4. Check template versions
|
|
288
471
|
if (configs.length > 0) {
|
|
289
|
-
const { checkTemplateVersions: checkVersions } = await import("./sync.js");
|
|
290
|
-
// Suppress stderr output from checkTemplateVersions — we'll report ourselves
|
|
291
|
-
const origWrite = process.stderr.write;
|
|
292
|
-
process.stderr.write = (() => true);
|
|
293
472
|
try {
|
|
294
|
-
const upgrades = await
|
|
473
|
+
const upgrades = await checkTemplateVersionsQuiet(configs);
|
|
295
474
|
if (upgrades.length === 0) {
|
|
296
475
|
checks.push({ name: "Template versions", status: "pass", detail: "All up to date" });
|
|
297
476
|
}
|
|
298
477
|
else {
|
|
299
|
-
const
|
|
300
|
-
|
|
478
|
+
const versionUpdates = upgrades.filter(u => u.reason === "version-update");
|
|
479
|
+
const missingHash = upgrades.filter(u => u.reason === "missing-hash");
|
|
480
|
+
const contentDrift = upgrades.filter(u => u.reason === "content-drift");
|
|
481
|
+
const parts = [];
|
|
482
|
+
if (versionUpdates.length > 0) {
|
|
483
|
+
const names = versionUpdates.map(u => `${u.templateName} (${u.currentVersion} → ${u.availableVersion})`);
|
|
484
|
+
parts.push(`Upgrade available: ${names.join(", ")}`);
|
|
485
|
+
}
|
|
486
|
+
if (missingHash.length > 0) {
|
|
487
|
+
const names = missingHash.map(u => `${u.templateName} (${u.currentVersion})`);
|
|
488
|
+
parts.push(`Template metadata incomplete: ${names.join(", ")} — run dna init --upgrade <template>`);
|
|
489
|
+
}
|
|
490
|
+
if (contentDrift.length > 0) {
|
|
491
|
+
const names = contentDrift.map(u => `${u.templateName} (${u.currentVersion})`);
|
|
492
|
+
parts.push(`Template content drift: ${names.join(", ")} — run dna init --upgrade <template>`);
|
|
493
|
+
}
|
|
494
|
+
checks.push({ name: "Template versions", status: "warn", detail: parts.join("; ") });
|
|
301
495
|
}
|
|
302
496
|
}
|
|
303
497
|
catch {
|
|
304
|
-
checks.push({ name: "Template versions", status: "warn", detail: "Could not check versions" });
|
|
305
|
-
}
|
|
306
|
-
finally {
|
|
307
|
-
process.stderr.write = origWrite;
|
|
498
|
+
checks.push({ name: "Template versions", status: "warn", detail: "Could not check versions — run `dna templates list` and retry `dna verify --health`" });
|
|
308
499
|
}
|
|
309
500
|
}
|
|
501
|
+
// 4.5 Check sync mode detection
|
|
502
|
+
try {
|
|
503
|
+
const { detectMode } = await import("./sync.js");
|
|
504
|
+
const mode = await detectMode();
|
|
505
|
+
checks.push({ name: "Sync mode", status: "pass", detail: `auto resolves to ${mode}` });
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
checks.push({ name: "Sync mode", status: "warn", detail: "Could not auto-detect plugin/bin mode — use `dna sync --mode bin` if plugin mode is unavailable" });
|
|
509
|
+
}
|
|
310
510
|
// 5. Check .claude settings or plugin registration
|
|
311
511
|
const settingsPath = resolve(cwd, ".claude", "settings.json");
|
|
312
512
|
try {
|
|
@@ -319,7 +519,7 @@ export async function runHealth() {
|
|
|
319
519
|
checks.push({ name: "Hook registration", status: "pass", detail: `${eventCount} event(s) in settings.json` });
|
|
320
520
|
}
|
|
321
521
|
else {
|
|
322
|
-
checks.push({ name: "Hook registration", status: "warn", detail: "
|
|
522
|
+
checks.push({ name: "Hook registration", status: "warn", detail: "settings.json has no hooks — run `dna sync --mode bin` or use plugin-managed hooks" });
|
|
323
523
|
}
|
|
324
524
|
}
|
|
325
525
|
catch {
|
|
@@ -332,13 +532,52 @@ export async function runHealth() {
|
|
|
332
532
|
checks.push({ name: "MCP server", status: "pass", detail: "dna-mcp registered in .mcp.json" });
|
|
333
533
|
}
|
|
334
534
|
else {
|
|
335
|
-
checks.push({ name: "Hook registration", status: "warn", detail: "No hooks or MCP server found" });
|
|
535
|
+
checks.push({ name: "Hook registration", status: "warn", detail: "No hooks or MCP server found — run `dna sync`" });
|
|
336
536
|
}
|
|
337
537
|
}
|
|
338
538
|
catch {
|
|
339
539
|
checks.push({ name: "Hook registration", status: "warn", detail: "No settings.json or .mcp.json — run `dna setup` or `dna sync`" });
|
|
340
540
|
}
|
|
341
541
|
}
|
|
542
|
+
// 5.5 Check generated skill map/body references
|
|
543
|
+
const skillsDir = resolve(cwd, ".claude", "skills");
|
|
544
|
+
const skillMapPath = resolve(skillsDir, ".intentdna-skill-map.json");
|
|
545
|
+
try {
|
|
546
|
+
const raw = await rf(skillMapPath, "utf-8");
|
|
547
|
+
const map = JSON.parse(raw);
|
|
548
|
+
const skills = Array.isArray(map.skills) ? map.skills : [];
|
|
549
|
+
const missingBodies = [];
|
|
550
|
+
for (const skill of skills) {
|
|
551
|
+
if (!skill.bodyPath)
|
|
552
|
+
continue;
|
|
553
|
+
try {
|
|
554
|
+
await stat(resolve(skillsDir, skill.bodyPath));
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
missingBodies.push(`${skill.name ?? "unknown"}: ${skill.bodyPath}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (missingBodies.length === 0) {
|
|
561
|
+
checks.push({ name: "Generated skills", status: "pass", detail: `${skills.length} skill map entr${skills.length === 1 ? "y" : "ies"} with valid bodies` });
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
checks.push({ name: "Generated skills", status: "fail", detail: `Missing body files: ${missingBodies.join(", ")} — run \`dna sync --skills .claude/skills\`` });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
try {
|
|
569
|
+
const entries = await readdir(skillsDir);
|
|
570
|
+
const hasSkillDirs = entries.some((entry) => entry.startsWith("dna-"));
|
|
571
|
+
checks.push({
|
|
572
|
+
name: "Generated skills",
|
|
573
|
+
status: hasSkillDirs ? "warn" : "pass",
|
|
574
|
+
detail: hasSkillDirs ? "Skill dirs exist without .intentdna-skill-map.json — run `dna sync`" : "No generated skill dirs found",
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
checks.push({ name: "Generated skills", status: "pass", detail: "No skills directory" });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
342
581
|
// 6. Check trace system
|
|
343
582
|
const traceDir = resolve(cwd, ".dna", "state", "trace");
|
|
344
583
|
try {
|