mova-claude-import 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -29
- package/control_surface_exclusions_v0.json +8 -0
- package/dist/anthropic_profile_v0.d.ts +1 -1
- package/dist/anthropic_profile_v0.js +1 -0
- package/dist/cli.js +206 -23
- package/dist/control_apply_v0.d.ts +5 -1
- package/dist/control_apply_v0.js +125 -8
- package/dist/control_check_v0.d.ts +1 -0
- package/dist/control_check_v0.js +194 -23
- package/dist/control_prefill_v0.js +128 -9
- package/dist/control_surface_coverage_v0.d.ts +22 -0
- package/dist/control_surface_coverage_v0.js +128 -0
- package/dist/control_v0.d.ts +149 -0
- package/dist/control_v0.js +360 -0
- package/dist/control_v0_schema.d.ts +6 -0
- package/dist/control_v0_schema.js +19 -0
- package/dist/init_v0.d.ts +6 -1
- package/dist/init_v0.js +41 -1
- package/dist/observability_writer_v0.d.ts +1 -0
- package/dist/observability_writer_v0.js +157 -0
- package/dist/observe_v0.d.ts +8 -0
- package/dist/observe_v0.js +57 -0
- package/dist/presets_v0.d.ts +11 -0
- package/dist/presets_v0.js +52 -0
- package/dist/redaction.js +5 -1
- package/dist/run_import.js +111 -26
- package/docs/CLAUDE_CONTROL_SURFACE_MAP_v0.md +78 -0
- package/docs/OPERATOR_GUIDE_v0.md +11 -0
- package/fixtures/pos/basic/mova/control_v0.json +93 -0
- package/fixtures/pos/claude_code_demo_full/.claude/agents/code-reviewer.md +13 -0
- package/fixtures/pos/claude_code_demo_full/.claude/agents/github-workflow.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/code-quality.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/docs-sync.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/onboard.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-review.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/pr-summary.md +8 -0
- package/fixtures/pos/claude_code_demo_full/.claude/commands/ticket.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.js +13 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-eval.sh +15 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.json +21 -0
- package/fixtures/pos/claude_code_demo_full/.claude/hooks/skill-rules.schema.json +24 -0
- package/fixtures/pos/claude_code_demo_full/.claude/rules/code-style.md +5 -0
- package/fixtures/pos/claude_code_demo_full/.claude/rules/security.md +5 -0
- package/fixtures/pos/claude_code_demo_full/.claude/settings.json +102 -0
- package/fixtures/pos/claude_code_demo_full/.claude/settings.md +6 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/core-components/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/formik-patterns/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/graphql-schema/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/react-ui-patterns/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/systematic-debugging/SKILL.md +9 -0
- package/fixtures/pos/claude_code_demo_full/.claude/skills/testing-patterns/SKILL.md +10 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/pr-claude-code-review.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-dependency-audit.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-docs-sync.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.github/workflows/scheduled-claude-code-quality.yml +14 -0
- package/fixtures/pos/claude_code_demo_full/.mcp.json +44 -0
- package/fixtures/pos/claude_code_demo_full/CLAUDE.md +28 -0
- package/fixtures/pos/control_basic_project/mova/control_v0.json +93 -0
- package/fixtures/pos/observability_basic/CLAUDE.md +3 -0
- package/{.tmp_test_zip/out1 → fixtures/pos/preset_safe_observable_v0}/.mcp.json +3 -3
- package/fixtures/pos/preset_safe_observable_v0/CLAUDE.md +3 -0
- package/package.json +2 -1
- package/presets/safe_observable_v0/assets/.claude/agents/code-reviewer.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/commands/finish.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/commands/start.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.js +15 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-eval.sh +17 -0
- package/presets/safe_observable_v0/assets/.claude/hooks/skill-rules.json +26 -0
- package/presets/safe_observable_v0/assets/.claude/rules/code-style.md +6 -0
- package/presets/safe_observable_v0/assets/.claude/rules/security.md +6 -0
- package/presets/safe_observable_v0/assets/.claude/skills/git-workflow/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/security-basics/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/systematic-debugging/SKILL.md +9 -0
- package/presets/safe_observable_v0/assets/.claude/skills/testing-patterns/SKILL.md +9 -0
- package/presets/safe_observable_v0/control_v0.json +214 -0
- package/schemas/mova.control_v0.schema.json +252 -0
- package/src/anthropic_profile_v0.ts +1 -0
- package/src/cli.ts +194 -23
- package/src/control_apply_v0.ts +131 -8
- package/src/control_check_v0.ts +203 -23
- package/src/control_prefill_v0.ts +136 -8
- package/src/control_surface_coverage_v0.ts +164 -0
- package/src/control_v0.ts +808 -0
- package/src/control_v0_schema.ts +26 -0
- package/src/init_v0.ts +48 -1
- package/src/observability_writer_v0.ts +157 -0
- package/src/observe_v0.ts +64 -0
- package/src/presets_v0.ts +58 -0
- package/src/redaction.ts +6 -1
- package/src/run_import.ts +132 -26
- package/test/control_demo_full_roundtrip.test.js +92 -0
- package/test/control_surface_coverage_v0.test.js +36 -0
- package/test/control_v0_schema_validation.test.js +69 -0
- package/test/init_v0.test.js +9 -0
- package/test/observability_writer_v0.test.js +59 -0
- package/test/preset_safe_observable_v0.test.js +55 -0
- package/test/profile_v0_output.test.js +1 -0
- package/test/scaffold_v0_output.test.js +1 -0
- package/tools/control_surface_coverage_v0.mjs +27 -0
- package/tools/smoke_v0.mjs +33 -0
- package/.tmp_test_control_apply/proj/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/commands/example_command.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_control_apply/proj/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_control_apply/proj/.claude/settings.json +0 -30
- package/.tmp_test_control_apply/proj/.claude/settings.local.example.json +0 -3
- package/.tmp_test_control_apply/proj/.mcp.json +0 -3
- package/.tmp_test_control_apply/proj/CLAUDE.md +0 -13
- package/.tmp_test_control_apply/proj/MOVA.md +0 -3
- package/.tmp_test_control_check/proj/.mcp.json +0 -1
- package/.tmp_test_control_check/proj/CLAUDE.md +0 -1
- package/.tmp_test_control_prefill/out1/claude_control_profile_v0.json +0 -114
- package/.tmp_test_control_prefill/out1/prefill_report_v0.json +0 -13
- package/.tmp_test_control_prefill/out2/claude_control_profile_v0.json +0 -114
- package/.tmp_test_control_prefill/out2/prefill_report_v0.json +0 -13
- package/.tmp_test_overlay/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_overlay/proj/.mcp.json +0 -1
- package/.tmp_test_overlay/proj/CLAUDE.md +0 -1
- package/.tmp_test_profile/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_profile/proj/.mcp.json +0 -1
- package/.tmp_test_profile/proj/CLAUDE.md +0 -1
- package/.tmp_test_scaffold_apply/proj/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/commands/example_command.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_scaffold_apply/proj/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_scaffold_apply/proj/.claude/settings.json +0 -30
- package/.tmp_test_scaffold_apply/proj/.claude/settings.local.example.json +0 -3
- package/.tmp_test_scaffold_apply/proj/.mcp.json +0 -3
- package/.tmp_test_scaffold_apply/proj/CLAUDE.md +0 -13
- package/.tmp_test_scaffold_apply/proj/MOVA.md +0 -3
- package/.tmp_test_strict/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_strict/mova/claude_import/v0/episode_import_run.json +0 -20
- package/.tmp_test_strict/mova/claude_import/v0/import_manifest.json +0 -20
- package/.tmp_test_strict/mova/claude_import/v0/input_policy_report_v0.json +0 -32
- package/.tmp_test_zip/out1/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_zip/out1/.claude/commands/example_command.md +0 -3
- package/.tmp_test_zip/out1/.claude/commands/mova_context.md +0 -4
- package/.tmp_test_zip/out1/.claude/commands/mova_lint.md +0 -4
- package/.tmp_test_zip/out1/.claude/commands/mova_proof.md +0 -6
- package/.tmp_test_zip/out1/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_zip/out1/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_zip/out1/.claude/settings.json +0 -30
- package/.tmp_test_zip/out1/.claude/settings.local.example.json +0 -3
- package/.tmp_test_zip/out1/.claude/skills/a/SKILL.md +0 -1
- package/.tmp_test_zip/out1/.claude/skills/mova-control-v0/SKILL.md +0 -11
- package/.tmp_test_zip/out1/.claude/skills/mova-layer-v0/SKILL.md +0 -8
- package/.tmp_test_zip/out1/CLAUDE.md +0 -4
- package/.tmp_test_zip/out1/MOVA.md +0 -10
- package/.tmp_test_zip/out1/export.zip +0 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
- package/.tmp_test_zip/out1/mova/claude_import/v0/episode_import_run.json +0 -80
- package/.tmp_test_zip/out1/mova/claude_import/v0/export_manifest_v0.json +0 -32
- package/.tmp_test_zip/out1/mova/claude_import/v0/import_manifest.json +0 -33
- package/.tmp_test_zip/out1/mova/claude_import/v0/input_policy_report_v0.json +0 -38
- package/.tmp_test_zip/out1/mova/claude_import/v0/lint_report_v0.json +0 -6
- package/.tmp_test_zip/out1/mova/claude_import/v0/redaction_report.json +0 -4
- package/.tmp_test_zip/out2/.claude/agents/example_agent.md +0 -3
- package/.tmp_test_zip/out2/.claude/commands/example_command.md +0 -3
- package/.tmp_test_zip/out2/.claude/commands/mova_context.md +0 -4
- package/.tmp_test_zip/out2/.claude/commands/mova_lint.md +0 -4
- package/.tmp_test_zip/out2/.claude/commands/mova_proof.md +0 -6
- package/.tmp_test_zip/out2/.claude/hooks/example_hook.sh +0 -2
- package/.tmp_test_zip/out2/.claude/output-styles/example_style.md +0 -3
- package/.tmp_test_zip/out2/.claude/settings.json +0 -30
- package/.tmp_test_zip/out2/.claude/settings.local.example.json +0 -3
- package/.tmp_test_zip/out2/.claude/skills/a/SKILL.md +0 -1
- package/.tmp_test_zip/out2/.claude/skills/mova-control-v0/SKILL.md +0 -11
- package/.tmp_test_zip/out2/.claude/skills/mova-layer-v0/SKILL.md +0 -8
- package/.tmp_test_zip/out2/.mcp.json +0 -3
- package/.tmp_test_zip/out2/CLAUDE.md +0 -4
- package/.tmp_test_zip/out2/MOVA.md +0 -10
- package/.tmp_test_zip/out2/export.zip +0 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/VERSION.json +0 -10
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/instruction_profile_v0.json +0 -8
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/mcp_servers_v0.json +0 -4
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/skills_catalog_v0.json +0 -11
- package/.tmp_test_zip/out2/mova/claude_import/v0/episode_import_run.json +0 -80
- package/.tmp_test_zip/out2/mova/claude_import/v0/export_manifest_v0.json +0 -32
- package/.tmp_test_zip/out2/mova/claude_import/v0/import_manifest.json +0 -33
- package/.tmp_test_zip/out2/mova/claude_import/v0/input_policy_report_v0.json +0 -38
- package/.tmp_test_zip/out2/mova/claude_import/v0/lint_report_v0.json +0 -6
- package/.tmp_test_zip/out2/mova/claude_import/v0/redaction_report.json +0 -4
- package/.tmp_test_zip/proj/.claude/skills/a.md +0 -1
- package/.tmp_test_zip/proj/.mcp.json +0 -1
- package/.tmp_test_zip/proj/CLAUDE.md +0 -1
package/dist/control_check_v0.js
CHANGED
|
@@ -3,6 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { stableStringify } from "./stable_json.js";
|
|
4
4
|
import { stableSha256 } from "./redaction.js";
|
|
5
5
|
import { buildMovaControlEntryV0, MOVA_CONTROL_ENTRY_MARKER } from "./mova_overlay_v0.js";
|
|
6
|
+
import { controlToMcpJson, controlToSettingsV0, normalizeControlV0 } from "./control_v0.js";
|
|
7
|
+
import { validateControlV0Schema } from "./control_v0_schema.js";
|
|
6
8
|
async function exists(p) {
|
|
7
9
|
try {
|
|
8
10
|
await fs.stat(p);
|
|
@@ -23,13 +25,49 @@ async function writeJson(p, obj) {
|
|
|
23
25
|
function computeRunId(parts) {
|
|
24
26
|
return stableSha256(parts.join("|")).slice(0, 16);
|
|
25
27
|
}
|
|
28
|
+
function normalizeJson(obj) {
|
|
29
|
+
return stableStringify(obj);
|
|
30
|
+
}
|
|
31
|
+
function isPlaceholder(value) {
|
|
32
|
+
return /^\$\{[A-Z0-9_]+(?::-?[^}]+)?\}$/.test(value.trim());
|
|
33
|
+
}
|
|
34
|
+
function validateMcpEnvValues(servers) {
|
|
35
|
+
if (!servers || typeof servers !== "object")
|
|
36
|
+
return [];
|
|
37
|
+
const issues = [];
|
|
38
|
+
const entries = Array.isArray(servers)
|
|
39
|
+
? servers.map((server, idx) => [server?.name ?? `index_${idx}`, server])
|
|
40
|
+
: Object.entries(servers);
|
|
41
|
+
for (const [name, server] of entries) {
|
|
42
|
+
if (!server || typeof server !== "object")
|
|
43
|
+
continue;
|
|
44
|
+
const env = server.env;
|
|
45
|
+
if (!env || typeof env !== "object")
|
|
46
|
+
continue;
|
|
47
|
+
for (const [key, value] of Object.entries(env)) {
|
|
48
|
+
if (typeof value !== "string")
|
|
49
|
+
continue;
|
|
50
|
+
if (!value.includes("${"))
|
|
51
|
+
continue;
|
|
52
|
+
if (!isPlaceholder(value)) {
|
|
53
|
+
issues.push({ server: String(name), key, value });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return issues;
|
|
58
|
+
}
|
|
26
59
|
export async function controlCheckV0(projectDir, profilePath, outDir) {
|
|
27
60
|
const profile = await readJson(profilePath);
|
|
28
61
|
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
29
62
|
const mcpPath = path.join(projectDir, ".mcp.json");
|
|
63
|
+
const settingsPath = path.join(projectDir, ".claude", "settings.json");
|
|
30
64
|
const claudeExists = await exists(claudePath);
|
|
31
65
|
const mcpExists = await exists(mcpPath);
|
|
32
|
-
const
|
|
66
|
+
const settingsExists = await exists(settingsPath);
|
|
67
|
+
const isControlV0 = profile?.version === "control_v0";
|
|
68
|
+
const marker = isControlV0
|
|
69
|
+
? profile?.claude_md?.marker ?? MOVA_CONTROL_ENTRY_MARKER
|
|
70
|
+
: profile?.anthropic?.claude_md?.marker ?? MOVA_CONTROL_ENTRY_MARKER;
|
|
33
71
|
const runId = computeRunId([profilePath, claudeExists ? "claude" : "", mcpExists ? "mcp" : "", marker]);
|
|
34
72
|
const runBase = path.join(outDir, "mova", "claude_control", "v0", "runs", runId);
|
|
35
73
|
const overlayParams = {
|
|
@@ -42,39 +80,172 @@ export async function controlCheckV0(projectDir, profilePath, outDir) {
|
|
|
42
80
|
qualityReportFile: "quality_report_v0.json",
|
|
43
81
|
exportManifestFile: "export_manifest_v0.json",
|
|
44
82
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
83
|
+
if (!isControlV0) {
|
|
84
|
+
const actions = [];
|
|
85
|
+
if (profile?.anthropic?.claude_md?.inject_control_entry) {
|
|
86
|
+
actions.push({
|
|
87
|
+
target: "CLAUDE.md",
|
|
88
|
+
action: "insert_or_update_control_entry",
|
|
89
|
+
marker,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (profile?.anthropic?.mcp?.servers && mcpExists) {
|
|
93
|
+
actions.push({
|
|
94
|
+
target: ".mcp.json",
|
|
95
|
+
action: "merge_servers",
|
|
96
|
+
summary: "merge profile servers with project mcp.json",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const plan = {
|
|
100
|
+
profile_version: "v0",
|
|
101
|
+
run_id: runId,
|
|
102
|
+
project_dir: projectDir,
|
|
103
|
+
profile_path: profilePath,
|
|
104
|
+
actions,
|
|
105
|
+
};
|
|
106
|
+
const summary = {
|
|
107
|
+
run_id: runId,
|
|
108
|
+
outcome_code: "PREVIEW",
|
|
109
|
+
actions_count: actions.length,
|
|
110
|
+
control_entry_preview: profile?.anthropic?.claude_md?.inject_control_entry
|
|
111
|
+
? buildMovaControlEntryV0(overlayParams)
|
|
112
|
+
: null,
|
|
113
|
+
};
|
|
114
|
+
const planPath = path.join(runBase, "control_plan_v0.json");
|
|
115
|
+
const summaryPath = path.join(runBase, "control_summary_v0.json");
|
|
116
|
+
await writeJson(planPath, plan);
|
|
117
|
+
await writeJson(summaryPath, summary);
|
|
118
|
+
return { run_id: runId, plan_path: planPath, summary_path: summaryPath };
|
|
119
|
+
}
|
|
120
|
+
const schemaValidation = await validateControlV0Schema(profile);
|
|
121
|
+
const control = normalizeControlV0(profile).control;
|
|
122
|
+
const planPath = path.join(runBase, "control_plan_v0.json");
|
|
123
|
+
const summaryPath = path.join(runBase, "control_summary_v0.json");
|
|
124
|
+
if (!schemaValidation.ok) {
|
|
125
|
+
const summary = {
|
|
126
|
+
run_id: runId,
|
|
127
|
+
outcome_code: "INVALID_SCHEMA",
|
|
128
|
+
errors: schemaValidation.errors ?? [],
|
|
129
|
+
};
|
|
130
|
+
await writeJson(planPath, { profile_version: "v0", run_id: runId, project_dir: projectDir, profile_path: profilePath, actions: [] });
|
|
131
|
+
await writeJson(summaryPath, summary);
|
|
132
|
+
return { run_id: runId, plan_path: planPath, summary_path: summaryPath, exit_code: 2 };
|
|
133
|
+
}
|
|
134
|
+
const mcpJsonExpected = controlToMcpJson(control);
|
|
135
|
+
const settingsExpected = controlToSettingsV0(control);
|
|
136
|
+
const missing = [];
|
|
137
|
+
const drift = [];
|
|
138
|
+
if (!claudeExists)
|
|
139
|
+
missing.push("CLAUDE.md");
|
|
140
|
+
if (!settingsExists)
|
|
141
|
+
missing.push(".claude/settings.json");
|
|
142
|
+
if (!mcpExists)
|
|
143
|
+
missing.push(".mcp.json");
|
|
144
|
+
if (claudeExists && control.claude_md.inject_control_entry) {
|
|
145
|
+
const claude = await fs.readFile(claudePath, "utf8");
|
|
146
|
+
if (!claude.includes(control.claude_md.marker)) {
|
|
147
|
+
drift.push({ path: "CLAUDE.md", expected: `marker:${control.claude_md.marker}`, actual: "missing_marker" });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (settingsExists) {
|
|
151
|
+
const actual = normalizeJson(await readJson(settingsPath));
|
|
152
|
+
const expected = normalizeJson(settingsExpected);
|
|
153
|
+
if (actual !== expected) {
|
|
154
|
+
drift.push({ path: ".claude/settings.json", expected, actual });
|
|
155
|
+
}
|
|
52
156
|
}
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
157
|
+
if (mcpExists) {
|
|
158
|
+
const actual = normalizeJson(await readJson(mcpPath));
|
|
159
|
+
const expected = normalizeJson(mcpJsonExpected);
|
|
160
|
+
if (actual !== expected) {
|
|
161
|
+
drift.push({ path: ".mcp.json", expected, actual });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (control.lsp.managed) {
|
|
165
|
+
const lspPath = control.lsp.config_path;
|
|
166
|
+
const abs = path.join(projectDir, lspPath);
|
|
167
|
+
if (!(await exists(abs))) {
|
|
168
|
+
missing.push(lspPath);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const actual = normalizeJson(await readJson(abs));
|
|
172
|
+
const expected = normalizeJson({ enabled_plugins: control.lsp.enabled_plugins });
|
|
173
|
+
if (actual !== expected) {
|
|
174
|
+
drift.push({ path: lspPath, expected, actual });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (control.observability.enable && control.observability.writer?.script_path) {
|
|
179
|
+
const obsPath = path.join(projectDir, control.observability.writer.script_path);
|
|
180
|
+
if (!(await exists(obsPath))) {
|
|
181
|
+
missing.push(control.observability.writer.script_path);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const assets = [
|
|
185
|
+
...control.assets.skills,
|
|
186
|
+
...control.assets.agents,
|
|
187
|
+
...control.assets.commands,
|
|
188
|
+
...control.assets.rules,
|
|
189
|
+
...control.assets.hooks,
|
|
190
|
+
...control.assets.workflows,
|
|
191
|
+
...control.assets.docs,
|
|
192
|
+
...control.assets.dotfiles,
|
|
193
|
+
...control.assets.schemas,
|
|
194
|
+
];
|
|
195
|
+
for (const asset of assets) {
|
|
196
|
+
const abs = path.join(projectDir, asset.path);
|
|
197
|
+
if (!(await exists(abs))) {
|
|
198
|
+
missing.push(asset.path);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const envIssues = validateMcpEnvValues(control.mcp.servers);
|
|
202
|
+
let outcome = "OK";
|
|
203
|
+
let exitCode = 0;
|
|
204
|
+
if (envIssues.length > 0) {
|
|
205
|
+
outcome = "INVALID_CONTROL";
|
|
206
|
+
exitCode = 2;
|
|
207
|
+
}
|
|
208
|
+
else if (missing.length > 0) {
|
|
209
|
+
outcome = "MISSING_REQUIRED_FILES";
|
|
210
|
+
exitCode = 4;
|
|
211
|
+
}
|
|
212
|
+
else if (drift.length > 0) {
|
|
213
|
+
outcome = "DRIFT";
|
|
214
|
+
exitCode = 3;
|
|
59
215
|
}
|
|
60
216
|
const plan = {
|
|
61
217
|
profile_version: "v0",
|
|
62
218
|
run_id: runId,
|
|
63
219
|
project_dir: projectDir,
|
|
64
220
|
profile_path: profilePath,
|
|
65
|
-
actions
|
|
221
|
+
actions: [
|
|
222
|
+
{
|
|
223
|
+
target: "CLAUDE.md",
|
|
224
|
+
action: "ensure_control_entry",
|
|
225
|
+
marker: control.claude_md.marker,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
target: ".claude/settings.json",
|
|
229
|
+
action: "overwrite",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
target: ".mcp.json",
|
|
233
|
+
action: "overwrite",
|
|
234
|
+
},
|
|
235
|
+
],
|
|
66
236
|
};
|
|
67
237
|
const summary = {
|
|
68
238
|
run_id: runId,
|
|
69
|
-
outcome_code:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
239
|
+
outcome_code: outcome,
|
|
240
|
+
exit_code: exitCode,
|
|
241
|
+
missing,
|
|
242
|
+
drift_count: drift.length,
|
|
243
|
+
invalid_mcp_env: envIssues,
|
|
244
|
+
control_entry_preview: control.claude_md.inject_control_entry ? buildMovaControlEntryV0(overlayParams) : null,
|
|
74
245
|
};
|
|
75
|
-
const
|
|
76
|
-
const summaryPath = path.join(runBase, "control_summary_v0.json");
|
|
246
|
+
const reportPath = path.join(runBase, "control_check_report_v0.json");
|
|
77
247
|
await writeJson(planPath, plan);
|
|
78
248
|
await writeJson(summaryPath, summary);
|
|
79
|
-
|
|
249
|
+
await writeJson(reportPath, { summary, drift, missing, invalid_mcp_env: envIssues });
|
|
250
|
+
return { run_id: runId, plan_path: planPath, summary_path: summaryPath, exit_code: exitCode };
|
|
80
251
|
}
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { stableStringify } from "./stable_json.js";
|
|
4
4
|
import { loadControlContractsV0 } from "./control_contracts_v0.js";
|
|
5
|
+
import { controlFromSettingsV0, normalizeControlV0 } from "./control_v0.js";
|
|
5
6
|
async function exists(p) {
|
|
6
7
|
try {
|
|
7
8
|
await fs.stat(p);
|
|
@@ -11,6 +12,22 @@ async function exists(p) {
|
|
|
11
12
|
return false;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
async function listFilesRec(dir) {
|
|
16
|
+
const out = [];
|
|
17
|
+
const stack = [dir];
|
|
18
|
+
while (stack.length) {
|
|
19
|
+
const current = stack.pop();
|
|
20
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
21
|
+
for (const e of entries) {
|
|
22
|
+
const abs = path.join(current, e.name);
|
|
23
|
+
if (e.isDirectory())
|
|
24
|
+
stack.push(abs);
|
|
25
|
+
else if (e.isFile())
|
|
26
|
+
out.push(abs);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
14
31
|
async function readJson(p) {
|
|
15
32
|
const raw = await fs.readFile(p, "utf8");
|
|
16
33
|
return JSON.parse(raw);
|
|
@@ -25,27 +42,117 @@ export async function controlPrefillV0(projectDir, outDir) {
|
|
|
25
42
|
const mcpPath = path.join(projectDir, ".mcp.json");
|
|
26
43
|
const settingsPath = path.join(projectDir, ".claude", "settings.json");
|
|
27
44
|
const settingsLocalPath = path.join(projectDir, ".claude", "settings.local.json");
|
|
28
|
-
let
|
|
45
|
+
let mcpParsed;
|
|
29
46
|
let mcpFound = false;
|
|
30
47
|
if (await exists(mcpPath)) {
|
|
31
|
-
|
|
32
|
-
if (Array.isArray(parsed?.servers)) {
|
|
33
|
-
mcpServers = { servers: parsed.servers };
|
|
34
|
-
}
|
|
35
|
-
else if (parsed?.servers && typeof parsed.servers === "object") {
|
|
36
|
-
mcpServers = { servers: parsed.servers };
|
|
37
|
-
}
|
|
48
|
+
mcpParsed = await readJson(mcpPath);
|
|
38
49
|
mcpFound = true;
|
|
39
50
|
}
|
|
40
51
|
if (template?.anthropic?.mcp) {
|
|
41
|
-
|
|
52
|
+
const servers = mcpParsed?.mcpServers ?? mcpParsed?.servers ?? {};
|
|
53
|
+
template.anthropic.mcp.servers = servers;
|
|
54
|
+
}
|
|
55
|
+
let settingsParsed;
|
|
56
|
+
if (await exists(settingsPath)) {
|
|
57
|
+
settingsParsed = await readJson(settingsPath);
|
|
58
|
+
}
|
|
59
|
+
const controlDerived = controlFromSettingsV0(settingsParsed, mcpParsed);
|
|
60
|
+
const control = normalizeControlV0(controlDerived.control).control;
|
|
61
|
+
const skills = [];
|
|
62
|
+
const skillsRoot = path.join(projectDir, ".claude", "skills");
|
|
63
|
+
if (await exists(skillsRoot)) {
|
|
64
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
if (!entry.isDirectory())
|
|
67
|
+
continue;
|
|
68
|
+
const rel = `.claude/skills/${entry.name}/SKILL.md`;
|
|
69
|
+
const abs = path.join(skillsRoot, entry.name, "SKILL.md");
|
|
70
|
+
if (await exists(abs)) {
|
|
71
|
+
skills.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const assetDirs = [
|
|
76
|
+
{ key: "agents", root: path.join(projectDir, ".claude", "agents"), prefix: ".claude/agents" },
|
|
77
|
+
{ key: "commands", root: path.join(projectDir, ".claude", "commands"), prefix: ".claude/commands" },
|
|
78
|
+
{ key: "rules", root: path.join(projectDir, ".claude", "rules"), prefix: ".claude/rules" },
|
|
79
|
+
{ key: "hooks", root: path.join(projectDir, ".claude", "hooks"), prefix: ".claude/hooks" },
|
|
80
|
+
];
|
|
81
|
+
const assetMap = {
|
|
82
|
+
agents: [],
|
|
83
|
+
commands: [],
|
|
84
|
+
rules: [],
|
|
85
|
+
hooks: [],
|
|
86
|
+
workflows: [],
|
|
87
|
+
docs: [],
|
|
88
|
+
dotfiles: [],
|
|
89
|
+
schemas: [],
|
|
90
|
+
};
|
|
91
|
+
for (const dir of assetDirs) {
|
|
92
|
+
if (!(await exists(dir.root)))
|
|
93
|
+
continue;
|
|
94
|
+
const files = await listFilesRec(dir.root);
|
|
95
|
+
for (const abs of files) {
|
|
96
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
97
|
+
assetMap[dir.key].push({ path: rel, mode: "copy_through", source_path: rel });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const docsCandidates = [
|
|
101
|
+
path.join(projectDir, ".claude", "settings.md"),
|
|
102
|
+
path.join(projectDir, ".claude", "skills", "README.md"),
|
|
103
|
+
];
|
|
104
|
+
for (const abs of docsCandidates) {
|
|
105
|
+
if (await exists(abs)) {
|
|
106
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
107
|
+
assetMap.docs.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const dotfileCandidates = [path.join(projectDir, ".claude", ".gitignore")];
|
|
111
|
+
for (const abs of dotfileCandidates) {
|
|
112
|
+
if (await exists(abs)) {
|
|
113
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
114
|
+
assetMap.dotfiles.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const schemasRoot = path.join(projectDir, ".claude");
|
|
118
|
+
if (await exists(schemasRoot)) {
|
|
119
|
+
const files = await listFilesRec(schemasRoot);
|
|
120
|
+
for (const abs of files) {
|
|
121
|
+
if (!abs.endsWith(".schema.json"))
|
|
122
|
+
continue;
|
|
123
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
124
|
+
assetMap.schemas.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const workflowsRoot = path.join(projectDir, ".github", "workflows");
|
|
128
|
+
if (await exists(workflowsRoot)) {
|
|
129
|
+
const files = await listFilesRec(workflowsRoot);
|
|
130
|
+
for (const abs of files) {
|
|
131
|
+
const rel = path.relative(projectDir, abs).replace(/\\/g, "/");
|
|
132
|
+
assetMap.workflows.push({ path: rel, mode: "copy_through", source_path: rel });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
control.assets.skills = skills.sort((a, b) => a.path.localeCompare(b.path));
|
|
136
|
+
control.assets.agents = assetMap.agents.sort((a, b) => a.path.localeCompare(b.path));
|
|
137
|
+
control.assets.commands = assetMap.commands.sort((a, b) => a.path.localeCompare(b.path));
|
|
138
|
+
control.assets.rules = assetMap.rules.sort((a, b) => a.path.localeCompare(b.path));
|
|
139
|
+
control.assets.hooks = assetMap.hooks.sort((a, b) => a.path.localeCompare(b.path));
|
|
140
|
+
control.assets.workflows = assetMap.workflows.sort((a, b) => a.path.localeCompare(b.path));
|
|
141
|
+
control.assets.docs = assetMap.docs.sort((a, b) => a.path.localeCompare(b.path));
|
|
142
|
+
control.assets.dotfiles = assetMap.dotfiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
143
|
+
control.assets.schemas = assetMap.schemas.sort((a, b) => a.path.localeCompare(b.path));
|
|
144
|
+
const hasSkillEval = control.assets.hooks.some((h) => h.path.endsWith("skill-eval.sh") || h.path.endsWith("skill-eval.js"));
|
|
145
|
+
if (hasSkillEval) {
|
|
146
|
+
control.skill_eval.enable = true;
|
|
42
147
|
}
|
|
43
148
|
const profilePath = path.join(outDir, "claude_control_profile_v0.json");
|
|
44
149
|
const reportPath = path.join(outDir, "prefill_report_v0.json");
|
|
150
|
+
const controlPath = path.join(outDir, "mova", "control_v0.json");
|
|
45
151
|
const report = {
|
|
46
152
|
profile_version: "v0",
|
|
47
153
|
project_dir: projectDir,
|
|
48
154
|
profile_path: profilePath,
|
|
155
|
+
control_path: controlPath,
|
|
49
156
|
found: {
|
|
50
157
|
mcp_json: mcpFound,
|
|
51
158
|
settings_json: await exists(settingsPath),
|
|
@@ -53,9 +160,21 @@ export async function controlPrefillV0(projectDir, outDir) {
|
|
|
53
160
|
},
|
|
54
161
|
applied: {
|
|
55
162
|
mcp_servers: mcpFound,
|
|
163
|
+
assets: {
|
|
164
|
+
skills: control.assets.skills.length,
|
|
165
|
+
agents: control.assets.agents.length,
|
|
166
|
+
commands: control.assets.commands.length,
|
|
167
|
+
rules: control.assets.rules.length,
|
|
168
|
+
hooks: control.assets.hooks.length,
|
|
169
|
+
workflows: control.assets.workflows.length,
|
|
170
|
+
docs: control.assets.docs.length,
|
|
171
|
+
dotfiles: control.assets.dotfiles.length,
|
|
172
|
+
schemas: control.assets.schemas.length,
|
|
173
|
+
},
|
|
56
174
|
},
|
|
57
175
|
};
|
|
58
176
|
await writeJson(profilePath, template);
|
|
177
|
+
await writeJson(controlPath, control);
|
|
59
178
|
await writeJson(reportPath, report);
|
|
60
179
|
return { profile_path: profilePath, report_path: reportPath };
|
|
61
180
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type CoverageReport = {
|
|
2
|
+
profile_version: "v0";
|
|
3
|
+
showcase_root: string;
|
|
4
|
+
control_path: string;
|
|
5
|
+
total_surface_files: number;
|
|
6
|
+
excluded: Array<{
|
|
7
|
+
pattern: string;
|
|
8
|
+
reason: string;
|
|
9
|
+
matches: number;
|
|
10
|
+
}>;
|
|
11
|
+
covered_count: number;
|
|
12
|
+
coverage_percent: number;
|
|
13
|
+
missing: string[];
|
|
14
|
+
covered: string[];
|
|
15
|
+
};
|
|
16
|
+
export declare function runControlSurfaceCoverageV0(params: {
|
|
17
|
+
showcaseRoot: string;
|
|
18
|
+
controlPath: string;
|
|
19
|
+
exclusionsPath: string;
|
|
20
|
+
reportPath: string;
|
|
21
|
+
}): Promise<CoverageReport>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { stableStringify } from "./stable_json.js";
|
|
4
|
+
import { normalizeControlV0 } from "./control_v0.js";
|
|
5
|
+
function toPosix(p) {
|
|
6
|
+
return p.replace(/\\/g, "/");
|
|
7
|
+
}
|
|
8
|
+
async function exists(p) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.stat(p);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function listFilesRec(dir) {
|
|
18
|
+
const out = [];
|
|
19
|
+
const stack = [dir];
|
|
20
|
+
while (stack.length) {
|
|
21
|
+
const current = stack.pop();
|
|
22
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const abs = path.join(current, entry.name);
|
|
25
|
+
if (entry.isDirectory())
|
|
26
|
+
stack.push(abs);
|
|
27
|
+
else if (entry.isFile())
|
|
28
|
+
out.push(abs);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function globToRegex(pattern) {
|
|
34
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
35
|
+
const regex = escaped.replace(/\\\*\\\*/g, ".*").replace(/\\\*/g, "[^/]*");
|
|
36
|
+
return new RegExp(`^${regex}$`);
|
|
37
|
+
}
|
|
38
|
+
function applyExclusions(files, exclusions) {
|
|
39
|
+
const excludedMatches = [];
|
|
40
|
+
const keep = new Set(files);
|
|
41
|
+
for (const ex of exclusions) {
|
|
42
|
+
const regex = globToRegex(ex.pattern);
|
|
43
|
+
let matches = 0;
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
if (regex.test(file)) {
|
|
46
|
+
if (keep.delete(file))
|
|
47
|
+
matches++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
excludedMatches.push({ pattern: ex.pattern, reason: ex.reason, matches });
|
|
51
|
+
}
|
|
52
|
+
return { remaining: Array.from(keep).sort(), excludedMatches };
|
|
53
|
+
}
|
|
54
|
+
async function readJson(p) {
|
|
55
|
+
const raw = await fs.readFile(p, "utf8");
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
export async function runControlSurfaceCoverageV0(params) {
|
|
59
|
+
const showcaseRoot = path.resolve(params.showcaseRoot);
|
|
60
|
+
const controlPath = path.resolve(params.controlPath);
|
|
61
|
+
const exclusionsPath = path.resolve(params.exclusionsPath);
|
|
62
|
+
const reportPath = path.resolve(params.reportPath);
|
|
63
|
+
const roots = [
|
|
64
|
+
path.join(showcaseRoot, "CLAUDE.md"),
|
|
65
|
+
path.join(showcaseRoot, ".mcp.json"),
|
|
66
|
+
path.join(showcaseRoot, ".claude"),
|
|
67
|
+
path.join(showcaseRoot, ".github", "workflows"),
|
|
68
|
+
];
|
|
69
|
+
const surfaceFiles = [];
|
|
70
|
+
for (const root of roots) {
|
|
71
|
+
if (!(await exists(root)))
|
|
72
|
+
continue;
|
|
73
|
+
const stat = await fs.stat(root);
|
|
74
|
+
if (stat.isFile()) {
|
|
75
|
+
surfaceFiles.push(root);
|
|
76
|
+
}
|
|
77
|
+
else if (stat.isDirectory()) {
|
|
78
|
+
const files = await listFilesRec(root);
|
|
79
|
+
surfaceFiles.push(...files);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const surfaceRel = surfaceFiles.map((p) => toPosix(path.relative(showcaseRoot, p))).sort();
|
|
83
|
+
const controlRaw = await readJson(controlPath);
|
|
84
|
+
const control = normalizeControlV0(controlRaw).control;
|
|
85
|
+
const covered = new Set([
|
|
86
|
+
"CLAUDE.md",
|
|
87
|
+
".mcp.json",
|
|
88
|
+
".claude/settings.json",
|
|
89
|
+
]);
|
|
90
|
+
const assets = [
|
|
91
|
+
...control.assets.skills,
|
|
92
|
+
...control.assets.agents,
|
|
93
|
+
...control.assets.commands,
|
|
94
|
+
...control.assets.rules,
|
|
95
|
+
...control.assets.hooks,
|
|
96
|
+
...control.assets.workflows,
|
|
97
|
+
...control.assets.docs,
|
|
98
|
+
...control.assets.dotfiles,
|
|
99
|
+
...control.assets.schemas,
|
|
100
|
+
];
|
|
101
|
+
for (const asset of assets) {
|
|
102
|
+
covered.add(toPosix(asset.path));
|
|
103
|
+
}
|
|
104
|
+
if (control.lsp.managed) {
|
|
105
|
+
covered.add(toPosix(control.lsp.config_path));
|
|
106
|
+
}
|
|
107
|
+
const exclusions = (await readJson(exclusionsPath))?.exclusions ?? [];
|
|
108
|
+
const { remaining, excludedMatches } = applyExclusions(surfaceRel, exclusions);
|
|
109
|
+
const missing = remaining.filter((p) => !covered.has(p));
|
|
110
|
+
const coveredList = remaining.filter((p) => covered.has(p));
|
|
111
|
+
const total = remaining.length;
|
|
112
|
+
const coveredCount = coveredList.length;
|
|
113
|
+
const coveragePercent = total === 0 ? 100 : Math.round((coveredCount / total) * 100);
|
|
114
|
+
const report = {
|
|
115
|
+
profile_version: "v0",
|
|
116
|
+
showcase_root: showcaseRoot,
|
|
117
|
+
control_path: controlPath,
|
|
118
|
+
total_surface_files: total,
|
|
119
|
+
excluded: excludedMatches,
|
|
120
|
+
covered_count: coveredCount,
|
|
121
|
+
coverage_percent: coveragePercent,
|
|
122
|
+
missing,
|
|
123
|
+
covered: coveredList,
|
|
124
|
+
};
|
|
125
|
+
await fs.mkdir(path.dirname(reportPath), { recursive: true });
|
|
126
|
+
await fs.writeFile(reportPath, stableStringify(report) + "\n", "utf8");
|
|
127
|
+
return report;
|
|
128
|
+
}
|