mova-claude-import 0.1.1
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/.github/workflows/ci.yml +42 -0
- package/.github/workflows/release-dry-run.yml +40 -0
- package/.github/workflows/release-publish.yml +43 -0
- package/.tmp_test_control_apply/proj/.claude/agents/example_agent.md +3 -0
- package/.tmp_test_control_apply/proj/.claude/commands/example_command.md +3 -0
- package/.tmp_test_control_apply/proj/.claude/hooks/example_hook.sh +2 -0
- package/.tmp_test_control_apply/proj/.claude/output-styles/example_style.md +3 -0
- package/.tmp_test_control_apply/proj/.claude/settings.json +30 -0
- package/.tmp_test_control_apply/proj/.claude/settings.local.example.json +3 -0
- package/.tmp_test_control_apply/proj/.mcp.json +3 -0
- package/.tmp_test_control_apply/proj/CLAUDE.md +13 -0
- package/.tmp_test_control_apply/proj/MOVA.md +3 -0
- package/.tmp_test_control_check/proj/.mcp.json +1 -0
- package/.tmp_test_control_check/proj/CLAUDE.md +1 -0
- package/.tmp_test_control_prefill/out1/claude_control_profile_v0.json +114 -0
- package/.tmp_test_control_prefill/out1/prefill_report_v0.json +13 -0
- package/.tmp_test_control_prefill/out2/claude_control_profile_v0.json +114 -0
- package/.tmp_test_control_prefill/out2/prefill_report_v0.json +13 -0
- package/.tmp_test_overlay/proj/.claude/skills/a.md +1 -0
- package/.tmp_test_overlay/proj/.mcp.json +1 -0
- package/.tmp_test_overlay/proj/CLAUDE.md +1 -0
- package/.tmp_test_profile/proj/.claude/skills/a.md +1 -0
- package/.tmp_test_profile/proj/.mcp.json +1 -0
- package/.tmp_test_profile/proj/CLAUDE.md +1 -0
- package/.tmp_test_scaffold_apply/proj/.claude/agents/example_agent.md +3 -0
- package/.tmp_test_scaffold_apply/proj/.claude/commands/example_command.md +3 -0
- package/.tmp_test_scaffold_apply/proj/.claude/hooks/example_hook.sh +2 -0
- package/.tmp_test_scaffold_apply/proj/.claude/output-styles/example_style.md +3 -0
- package/.tmp_test_scaffold_apply/proj/.claude/settings.json +30 -0
- package/.tmp_test_scaffold_apply/proj/.claude/settings.local.example.json +3 -0
- package/.tmp_test_scaffold_apply/proj/.mcp.json +3 -0
- package/.tmp_test_scaffold_apply/proj/CLAUDE.md +13 -0
- package/.tmp_test_scaffold_apply/proj/MOVA.md +3 -0
- package/.tmp_test_strict/mova/claude_import/v0/VERSION.json +10 -0
- package/.tmp_test_strict/mova/claude_import/v0/episode_import_run.json +20 -0
- package/.tmp_test_strict/mova/claude_import/v0/import_manifest.json +20 -0
- package/.tmp_test_strict/mova/claude_import/v0/input_policy_report_v0.json +32 -0
- package/.tmp_test_zip/out1/.claude/agents/example_agent.md +3 -0
- package/.tmp_test_zip/out1/.claude/commands/example_command.md +3 -0
- package/.tmp_test_zip/out1/.claude/commands/mova_context.md +4 -0
- package/.tmp_test_zip/out1/.claude/commands/mova_lint.md +4 -0
- package/.tmp_test_zip/out1/.claude/commands/mova_proof.md +6 -0
- package/.tmp_test_zip/out1/.claude/hooks/example_hook.sh +2 -0
- package/.tmp_test_zip/out1/.claude/output-styles/example_style.md +3 -0
- package/.tmp_test_zip/out1/.claude/settings.json +30 -0
- package/.tmp_test_zip/out1/.claude/settings.local.example.json +3 -0
- package/.tmp_test_zip/out1/.claude/skills/a/SKILL.md +1 -0
- package/.tmp_test_zip/out1/.claude/skills/mova-control-v0/SKILL.md +11 -0
- package/.tmp_test_zip/out1/.claude/skills/mova-layer-v0/SKILL.md +8 -0
- package/.tmp_test_zip/out1/.mcp.json +3 -0
- package/.tmp_test_zip/out1/CLAUDE.md +4 -0
- package/.tmp_test_zip/out1/MOVA.md +10 -0
- package/.tmp_test_zip/out1/export.zip +0 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/VERSION.json +10 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/instruction_profile_v0.json +8 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/mcp_servers_v0.json +4 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/skills_catalog_v0.json +11 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/episode_import_run.json +80 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/export_manifest_v0.json +32 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/import_manifest.json +33 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/input_policy_report_v0.json +38 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/lint_report_v0.json +6 -0
- package/.tmp_test_zip/out1/mova/claude_import/v0/redaction_report.json +4 -0
- package/.tmp_test_zip/out2/.claude/agents/example_agent.md +3 -0
- package/.tmp_test_zip/out2/.claude/commands/example_command.md +3 -0
- package/.tmp_test_zip/out2/.claude/commands/mova_context.md +4 -0
- package/.tmp_test_zip/out2/.claude/commands/mova_lint.md +4 -0
- package/.tmp_test_zip/out2/.claude/commands/mova_proof.md +6 -0
- package/.tmp_test_zip/out2/.claude/hooks/example_hook.sh +2 -0
- package/.tmp_test_zip/out2/.claude/output-styles/example_style.md +3 -0
- package/.tmp_test_zip/out2/.claude/settings.json +30 -0
- package/.tmp_test_zip/out2/.claude/settings.local.example.json +3 -0
- package/.tmp_test_zip/out2/.claude/skills/a/SKILL.md +1 -0
- package/.tmp_test_zip/out2/.claude/skills/mova-control-v0/SKILL.md +11 -0
- package/.tmp_test_zip/out2/.claude/skills/mova-layer-v0/SKILL.md +8 -0
- package/.tmp_test_zip/out2/.mcp.json +3 -0
- package/.tmp_test_zip/out2/CLAUDE.md +4 -0
- package/.tmp_test_zip/out2/MOVA.md +10 -0
- package/.tmp_test_zip/out2/export.zip +0 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/VERSION.json +10 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/instruction_profile_v0.json +8 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/mcp_servers_v0.json +4 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/skills_catalog_v0.json +11 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/episode_import_run.json +80 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/export_manifest_v0.json +32 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/import_manifest.json +33 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/input_policy_report_v0.json +38 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/lint_report_v0.json +6 -0
- package/.tmp_test_zip/out2/mova/claude_import/v0/redaction_report.json +4 -0
- package/.tmp_test_zip/proj/.claude/skills/a.md +1 -0
- package/.tmp_test_zip/proj/.mcp.json +1 -0
- package/.tmp_test_zip/proj/CLAUDE.md +1 -0
- package/README.md +86 -0
- package/create_files.js +52 -0
- package/dist/anthropic_profile_v0.d.ts +2 -0
- package/dist/anthropic_profile_v0.js +66 -0
- package/dist/claude_profile_scaffold_v0.d.ts +2 -0
- package/dist/claude_profile_scaffold_v0.js +110 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +163 -0
- package/dist/cli_entry.d.ts +1 -0
- package/dist/cli_entry.js +1 -0
- package/dist/control_apply_v0.d.ts +6 -0
- package/dist/control_apply_v0.js +86 -0
- package/dist/control_check_v0.d.ts +7 -0
- package/dist/control_check_v0.js +80 -0
- package/dist/control_contracts_v0.d.ts +8 -0
- package/dist/control_contracts_v0.js +17 -0
- package/dist/control_prefill_v0.d.ts +6 -0
- package/dist/control_prefill_v0.js +61 -0
- package/dist/export_zip_v0.d.ts +8 -0
- package/dist/export_zip_v0.js +79 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +2 -0
- package/dist/init_v0.d.ts +7 -0
- package/dist/init_v0.js +47 -0
- package/dist/input_policy_v0.d.ts +26 -0
- package/dist/input_policy_v0.js +76 -0
- package/dist/lint_v0.d.ts +18 -0
- package/dist/lint_v0.js +131 -0
- package/dist/mova_overlay_v0.d.ts +14 -0
- package/dist/mova_overlay_v0.js +65 -0
- package/dist/mova_spec_bindings_v0.d.ts +5 -0
- package/dist/mova_spec_bindings_v0.js +5 -0
- package/dist/quality_v0.d.ts +1 -0
- package/dist/quality_v0.js +223 -0
- package/dist/redaction.d.ts +14 -0
- package/dist/redaction.js +52 -0
- package/dist/run_import.d.ts +2 -0
- package/dist/run_import.js +479 -0
- package/dist/stable_json.d.ts +1 -0
- package/dist/stable_json.js +15 -0
- package/docs/ANTHROPIC_PROFILE_v0.md +38 -0
- package/docs/COMPATIBILITY_MATRIX.md +25 -0
- package/docs/CONTROL_PROFILE_GUIDE_v0.md +40 -0
- package/docs/IMPORT_SPEC_v0.md +30 -0
- package/docs/MOVA_SPEC_BINDINGS.json +21 -0
- package/docs/MOVA_SPEC_BINDINGS.md +11 -0
- package/docs/OPERATOR_GUIDE_v0.md +43 -0
- package/docs/SECURITY_MODEL_v0.md +20 -0
- package/examples/control_profile_min.json +37 -0
- package/examples/control_profile_standard.json +81 -0
- package/examples/control_profile_strict.json +68 -0
- package/fixtures/neg/bad_skill_structure/.claude/skills/bad/README.md +3 -0
- package/fixtures/neg/bad_skill_structure/CLAUDE.md +3 -0
- package/fixtures/neg/local_settings_present/.claude/settings.local.json +3 -0
- package/fixtures/neg/local_settings_present/.claude/skills/alpha/SKILL.md +6 -0
- package/fixtures/neg/local_settings_present/CLAUDE.md +3 -0
- package/fixtures/neg/secret_leak/.claude/skills/alpha/SKILL.md +6 -0
- package/fixtures/neg/secret_leak/.mcp.json +3 -0
- package/fixtures/neg/secret_leak/CLAUDE.md +3 -0
- package/fixtures/neg/strict_denied_local/.claude/settings.local.json +3 -0
- package/fixtures/neg/strict_denied_local/CLAUDE.md +3 -0
- package/fixtures/pos/basic/.claude/skills/alpha/SKILL.md +8 -0
- package/fixtures/pos/basic/.mcp.json +3 -0
- package/fixtures/pos/basic/CLAUDE.md +3 -0
- package/fixtures/pos/control_basic_project/.mcp.json +3 -0
- package/fixtures/pos/control_basic_project/CLAUDE.md +3 -0
- package/fixtures/pos/control_profile_filled/claude_control_profile_v0.json +18 -0
- package/fixtures/pos/full_scaffold_roundtrip/README.md +1 -0
- package/package.json +39 -0
- package/schemas/claude_control/v0/ds/ds.claude_control_mapping_v0.json +227 -0
- package/schemas/claude_control/v0/ds/ds.claude_control_profile_v0.json +114 -0
- package/schemas/claude_control/v0/env/env.claude_control_apply_v0.json +170 -0
- package/schemas/claude_control/v0/env/env.claude_control_import_prefill_v0.json +171 -0
- package/schemas/claude_control/v0/global/global.claude_control_precedence_v0.json +58 -0
- package/schemas/claude_control/v0/global/global.claude_control_vocab_v0.json +98 -0
- package/schemas/ds.claude_import.instruction_profile_v0.schema.json +31 -0
- package/schemas/ds.claude_import.mcp_servers_v0.schema.json +47 -0
- package/schemas/ds.claude_import.skills_catalog_v0.schema.json +48 -0
- package/src/anthropic_profile_v0.ts +68 -0
- package/src/claude_profile_scaffold_v0.ts +117 -0
- package/src/cli.ts +160 -0
- package/src/cli_entry.ts +1 -0
- package/src/control_apply_v0.ts +108 -0
- package/src/control_check_v0.ts +98 -0
- package/src/control_contracts_v0.ts +26 -0
- package/src/control_prefill_v0.ts +74 -0
- package/src/export_zip_v0.ts +90 -0
- package/src/index.ts +29 -0
- package/src/init_v0.ts +59 -0
- package/src/input_policy_v0.ts +103 -0
- package/src/lint_v0.ts +151 -0
- package/src/mova_overlay_v0.ts +79 -0
- package/src/mova_spec_bindings_v0.ts +5 -0
- package/src/quality_v0.ts +264 -0
- package/src/redaction.ts +63 -0
- package/src/run_import.ts +526 -0
- package/src/stable_json.ts +15 -0
- package/test/control_apply_apply.test.js +40 -0
- package/test/control_check_preview.test.js +38 -0
- package/test/control_prefill.test.js +30 -0
- package/test/demo_v0_smoke.test.js +37 -0
- package/test/export_zip_determinism.test.js +41 -0
- package/test/import_determinism.test.js +53 -0
- package/test/init_v0.test.js +37 -0
- package/test/overlay_v0_output.test.js +38 -0
- package/test/profile_v0_output.test.js +44 -0
- package/test/scaffold_v0_output.test.js +64 -0
- package/test/strict_input_policy.test.js +45 -0
- package/tools/demo_v0.mjs +98 -0
- package/tools/deps_audit_v0.mjs +123 -0
- package/tools/write_mova_spec_bindings_v0.mjs +122 -0
- package/tsconfig.json +13 -0
package/dist/lint_v0.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { anthropicProfileV0RequiredFiles } from "./anthropic_profile_v0.js";
|
|
4
|
+
import { stableStringify } from "./stable_json.js";
|
|
5
|
+
async function exists(p) {
|
|
6
|
+
try {
|
|
7
|
+
await fs.stat(p);
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function listFilesRec(dir) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const stack = [dir];
|
|
17
|
+
while (stack.length) {
|
|
18
|
+
const current = stack.pop();
|
|
19
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
20
|
+
for (const e of entries) {
|
|
21
|
+
const abs = path.join(current, e.name);
|
|
22
|
+
if (e.isDirectory())
|
|
23
|
+
stack.push(abs);
|
|
24
|
+
else if (e.isFile())
|
|
25
|
+
out.push(abs);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function extractFrontmatterName(body) {
|
|
31
|
+
const match = body.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
|
|
32
|
+
if (!match)
|
|
33
|
+
return null;
|
|
34
|
+
const lines = match[1].split(/\r?\n/);
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const m = line.match(/^name:\s*(.+)\s*$/);
|
|
37
|
+
if (m)
|
|
38
|
+
return m[1].trim();
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
export async function lintV0(opts) {
|
|
43
|
+
const issues = [];
|
|
44
|
+
const required = [
|
|
45
|
+
...(opts.emitProfile ? anthropicProfileV0RequiredFiles : []),
|
|
46
|
+
"mova/claude_import/v0/import_manifest.json",
|
|
47
|
+
"mova/claude_import/v0/redaction_report.json",
|
|
48
|
+
"mova/claude_import/v0/contracts/instruction_profile_v0.json",
|
|
49
|
+
"mova/claude_import/v0/contracts/skills_catalog_v0.json",
|
|
50
|
+
"mova/claude_import/v0/contracts/mcp_servers_v0.json",
|
|
51
|
+
"mova/claude_import/v0/episode_import_run.json",
|
|
52
|
+
];
|
|
53
|
+
if (opts.mcpExpected && opts.emitProfile) {
|
|
54
|
+
required.push(".mcp.json");
|
|
55
|
+
}
|
|
56
|
+
for (const rel of required) {
|
|
57
|
+
const abs = path.join(opts.outRoot, rel);
|
|
58
|
+
if (!(await exists(abs))) {
|
|
59
|
+
issues.push({ code: "missing_file", message: "Required file is missing.", path: rel });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const settingsLocal = path.join(opts.outRoot, ".claude", "settings.local.json");
|
|
63
|
+
if (await exists(settingsLocal)) {
|
|
64
|
+
issues.push({ code: "settings_local_present", message: "settings.local.json must not be written.", path: ".claude/settings.local.json" });
|
|
65
|
+
}
|
|
66
|
+
const skillsRoot = path.join(opts.outRoot, ".claude", "skills");
|
|
67
|
+
if (await exists(skillsRoot)) {
|
|
68
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
69
|
+
for (const e of entries) {
|
|
70
|
+
const abs = path.join(skillsRoot, e.name);
|
|
71
|
+
if (e.isFile()) {
|
|
72
|
+
issues.push({ code: "skill_root_file", message: "Files are not allowed directly under .claude/skills.", path: `.claude/skills/${e.name}` });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (e.isDirectory()) {
|
|
76
|
+
const skillMd = path.join(abs, "SKILL.md");
|
|
77
|
+
if (!(await exists(skillMd))) {
|
|
78
|
+
issues.push({ code: "missing_skill_md", message: "Skill directory is missing SKILL.md.", path: `.claude/skills/${e.name}/SKILL.md` });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const skillNameCounts = new Map();
|
|
84
|
+
if (await exists(skillsRoot)) {
|
|
85
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
86
|
+
for (const e of entries) {
|
|
87
|
+
if (!e.isDirectory())
|
|
88
|
+
continue;
|
|
89
|
+
const skillMd = path.join(skillsRoot, e.name, "SKILL.md");
|
|
90
|
+
if (!(await exists(skillMd)))
|
|
91
|
+
continue;
|
|
92
|
+
const body = await fs.readFile(skillMd, "utf8");
|
|
93
|
+
const name = extractFrontmatterName(body);
|
|
94
|
+
if (!name)
|
|
95
|
+
continue;
|
|
96
|
+
skillNameCounts.set(name, (skillNameCounts.get(name) ?? 0) + 1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const [name, count] of skillNameCounts.entries()) {
|
|
100
|
+
if (count > 1) {
|
|
101
|
+
issues.push({ code: "duplicate_skill_name", message: `Duplicate skill name in frontmatter: ${name}`, path: ".claude/skills" });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const jsonFiles = (await exists(opts.outRoot)) ? (await listFilesRec(opts.outRoot)).filter((p) => p.toLowerCase().endsWith(".json")) : [];
|
|
105
|
+
for (const abs of jsonFiles) {
|
|
106
|
+
if (abs.endsWith(`${path.sep}lint_report_v0.json`))
|
|
107
|
+
continue;
|
|
108
|
+
const rel = path.relative(opts.outRoot, abs).replace(/\\/g, "/");
|
|
109
|
+
const raw = await fs.readFile(abs, "utf8");
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(raw);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
issues.push({ code: "invalid_json", message: "JSON file failed to parse.", path: rel });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const normalized = stableStringify(parsed) + "\n";
|
|
119
|
+
if (raw !== normalized) {
|
|
120
|
+
issues.push({ code: "json_not_normalized", message: "JSON file is not normalized (sorted keys, 2 spaces, newline).", path: rel });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const ok = issues.length === 0;
|
|
124
|
+
const summary = ok ? "lint_v0: ok" : `lint_v0: ${issues.length} issue(s)`;
|
|
125
|
+
return {
|
|
126
|
+
profile_version: "v0",
|
|
127
|
+
ok,
|
|
128
|
+
issues,
|
|
129
|
+
summary,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type OverlayParams = {
|
|
2
|
+
contractsDir: string;
|
|
3
|
+
artifactsDir: string;
|
|
4
|
+
instructionProfileFile: string;
|
|
5
|
+
skillsCatalogFile: string;
|
|
6
|
+
mcpServersFile: string;
|
|
7
|
+
lintReportFile: string;
|
|
8
|
+
qualityReportFile: string;
|
|
9
|
+
exportManifestFile: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const MOVA_CONTROL_ENTRY_MARKER = "<!-- MOVA_CONTROL_ENTRY_V0 -->";
|
|
12
|
+
export declare function buildMovaOverlayV0(params: OverlayParams): Record<string, string>;
|
|
13
|
+
export declare function buildMovaControlEntryV0(params: OverlayParams): string;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export const MOVA_CONTROL_ENTRY_MARKER = "<!-- MOVA_CONTROL_ENTRY_V0 -->";
|
|
2
|
+
export function buildMovaOverlayV0(params) {
|
|
3
|
+
const contractsDir = params.contractsDir;
|
|
4
|
+
const artifactsDir = params.artifactsDir;
|
|
5
|
+
const instruction = `${contractsDir}${params.instructionProfileFile}`;
|
|
6
|
+
const skills = `${contractsDir}${params.skillsCatalogFile}`;
|
|
7
|
+
const mcp = `${contractsDir}${params.mcpServersFile}`;
|
|
8
|
+
const lint = `${artifactsDir}${params.lintReportFile}`;
|
|
9
|
+
const quality = `${artifactsDir}${params.qualityReportFile}`;
|
|
10
|
+
const exportManifest = `${artifactsDir}${params.exportManifestFile}`;
|
|
11
|
+
return {
|
|
12
|
+
".claude/commands/mova_context.md": [
|
|
13
|
+
"# mova_context",
|
|
14
|
+
"",
|
|
15
|
+
"Use MOVA contracts as source of truth:",
|
|
16
|
+
`- ${instruction}`,
|
|
17
|
+
`- ${skills}`,
|
|
18
|
+
`- ${mcp}`,
|
|
19
|
+
"",
|
|
20
|
+
"Then use CLAUDE.md and MOVA.md as narrative guides.",
|
|
21
|
+
"",
|
|
22
|
+
].join("\n"),
|
|
23
|
+
".claude/commands/mova_proof.md": [
|
|
24
|
+
"# mova_proof",
|
|
25
|
+
"",
|
|
26
|
+
"Proof/evidence files:",
|
|
27
|
+
`- ${lint}`,
|
|
28
|
+
`- ${quality}`,
|
|
29
|
+
`- ${exportManifest}`,
|
|
30
|
+
"",
|
|
31
|
+
].join("\n"),
|
|
32
|
+
".claude/skills/mova-control-v0/SKILL.md": [
|
|
33
|
+
"---",
|
|
34
|
+
"name: mova-control-v0",
|
|
35
|
+
"version: v0",
|
|
36
|
+
"---",
|
|
37
|
+
"",
|
|
38
|
+
"# mova-control-v0",
|
|
39
|
+
"",
|
|
40
|
+
"Rules:",
|
|
41
|
+
"- Use MOVA contracts first.",
|
|
42
|
+
"- Use evidence files for verification.",
|
|
43
|
+
"- Do not invent missing data.",
|
|
44
|
+
"",
|
|
45
|
+
].join("\n"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function buildMovaControlEntryV0(params) {
|
|
49
|
+
const contractsDir = params.contractsDir;
|
|
50
|
+
const artifactsDir = params.artifactsDir;
|
|
51
|
+
return [
|
|
52
|
+
MOVA_CONTROL_ENTRY_MARKER,
|
|
53
|
+
"## MOVA Control Entry (v0)",
|
|
54
|
+
"",
|
|
55
|
+
"Source of truth (do this first):",
|
|
56
|
+
`- ${contractsDir}${params.instructionProfileFile}`,
|
|
57
|
+
`- ${contractsDir}${params.skillsCatalogFile}`,
|
|
58
|
+
`- ${contractsDir}${params.mcpServersFile}`,
|
|
59
|
+
"",
|
|
60
|
+
"Proof / evidence:",
|
|
61
|
+
`- ${artifactsDir}${params.qualityReportFile}`,
|
|
62
|
+
`- ${artifactsDir}${params.exportManifestFile}`,
|
|
63
|
+
"",
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const MOVA_SPEC_BINDINGS_V0: {
|
|
2
|
+
readonly instruction_profile_id: "https://mova.dev/schemas/ds.instruction_profile_core_v1.schema.json";
|
|
3
|
+
readonly mcp_servers_id: "https://mova.dev/schemas/ds.runtime_binding_core_v1.schema.json";
|
|
4
|
+
readonly core_schema_id: "https://mova.dev/schemas/ds.mova_schema_core_v1.schema.json";
|
|
5
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const MOVA_SPEC_BINDINGS_V0 = {
|
|
2
|
+
instruction_profile_id: "https://mova.dev/schemas/ds.instruction_profile_core_v1.schema.json",
|
|
3
|
+
mcp_servers_id: "https://mova.dev/schemas/ds.runtime_binding_core_v1.schema.json",
|
|
4
|
+
core_schema_id: "https://mova.dev/schemas/ds.mova_schema_core_v1.schema.json",
|
|
5
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { runImport } from "./run_import.js";
|
|
6
|
+
import { stableStringify } from "./stable_json.js";
|
|
7
|
+
import { controlCheckV0 } from "./control_check_v0.js";
|
|
8
|
+
import { controlPrefillV0 } from "./control_prefill_v0.js";
|
|
9
|
+
import { controlApplyV0 } from "./control_apply_v0.js";
|
|
10
|
+
import { writeCleanClaudeProfileScaffoldV0 } from "./claude_profile_scaffold_v0.js";
|
|
11
|
+
const execFileP = promisify(execFile);
|
|
12
|
+
function getArg(name) {
|
|
13
|
+
const idx = process.argv.indexOf(name);
|
|
14
|
+
if (idx === -1)
|
|
15
|
+
return undefined;
|
|
16
|
+
return process.argv[idx + 1];
|
|
17
|
+
}
|
|
18
|
+
async function exists(p) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.stat(p);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readJson(p) {
|
|
28
|
+
const raw = await fs.readFile(p, "utf8");
|
|
29
|
+
return JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
async function runDepsAudit() {
|
|
32
|
+
try {
|
|
33
|
+
await execFileP("node", ["tools/deps_audit_v0.mjs"], { cwd: process.cwd() });
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const code = err?.code ?? 1;
|
|
37
|
+
if (code === 2) {
|
|
38
|
+
throw new Error("deps_audit_failed");
|
|
39
|
+
}
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function writeJson(p, obj) {
|
|
44
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
45
|
+
await fs.writeFile(p, stableStringify(obj) + "\n", "utf8");
|
|
46
|
+
}
|
|
47
|
+
async function checkSkillStructure(projectDir) {
|
|
48
|
+
const issues = [];
|
|
49
|
+
const skillsRoot = path.join(projectDir, ".claude", "skills");
|
|
50
|
+
if (!(await exists(skillsRoot)))
|
|
51
|
+
return { ok: true, issues };
|
|
52
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
const rel = `.claude/skills/${entry.name}`;
|
|
55
|
+
if (entry.isFile()) {
|
|
56
|
+
issues.push(`skill_root_file:${rel}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
const skillMd = path.join(skillsRoot, entry.name, "SKILL.md");
|
|
61
|
+
if (!(await exists(skillMd))) {
|
|
62
|
+
issues.push(`missing_skill_md:${rel}/SKILL.md`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { ok: issues.length === 0, issues };
|
|
67
|
+
}
|
|
68
|
+
async function runCase(suite, caseId, fixturesRoot) {
|
|
69
|
+
const repoRoot = process.cwd();
|
|
70
|
+
const projectDir = path.join(fixturesRoot, caseId);
|
|
71
|
+
const outRoot = path.join(repoRoot, ".tmp_test", "quality", suite, caseId, "out");
|
|
72
|
+
await fs.rm(outRoot, { recursive: true, force: true });
|
|
73
|
+
await fs.mkdir(outRoot, { recursive: true });
|
|
74
|
+
const strict = suite === "neg" && caseId === "strict_denied_local";
|
|
75
|
+
const result = await runImport({
|
|
76
|
+
projectDir,
|
|
77
|
+
outDir: outRoot,
|
|
78
|
+
includeLocal: false,
|
|
79
|
+
includeUserSettings: false,
|
|
80
|
+
dryRun: false,
|
|
81
|
+
strict,
|
|
82
|
+
emitProfile: true,
|
|
83
|
+
emitOverlay: true,
|
|
84
|
+
emitZip: true,
|
|
85
|
+
zipName: "export.zip",
|
|
86
|
+
});
|
|
87
|
+
const movaBase = path.join(outRoot, "mova", "claude_import", "v0");
|
|
88
|
+
const manifestPath = path.join(movaBase, "import_manifest.json");
|
|
89
|
+
const lintPath = path.join(movaBase, "lint_report_v0.json");
|
|
90
|
+
const redactionPath = path.join(movaBase, "redaction_report.json");
|
|
91
|
+
const exportManifestPath = path.join(movaBase, "export_manifest_v0.json");
|
|
92
|
+
const inputPolicyPath = path.join(movaBase, "input_policy_report_v0.json");
|
|
93
|
+
const failures = [];
|
|
94
|
+
const inputPolicyReport = await readJson(inputPolicyPath);
|
|
95
|
+
const lintReport = await exists(lintPath) ? await readJson(lintPath) : null;
|
|
96
|
+
const redactionReport = await exists(redactionPath) ? await readJson(redactionPath) : { hits: [] };
|
|
97
|
+
const exportManifestExists = await exists(exportManifestPath);
|
|
98
|
+
const exportManifest = exportManifestExists ? await readJson(exportManifestPath) : null;
|
|
99
|
+
const settingsLocalInput = await exists(path.join(projectDir, ".claude", "settings.local.json"));
|
|
100
|
+
const skillStructure = await checkSkillStructure(projectDir);
|
|
101
|
+
const zipRelPath = exportManifest?.zip_rel_path;
|
|
102
|
+
const zipPresent = typeof zipRelPath === "string" && (await exists(path.join(outRoot, zipRelPath)));
|
|
103
|
+
const exportFilesCountMatch = typeof exportManifest?.files_count === "number" &&
|
|
104
|
+
Array.isArray(exportManifest?.files) &&
|
|
105
|
+
exportManifest.files_count === exportManifest.files.length;
|
|
106
|
+
if (!(await exists(manifestPath)))
|
|
107
|
+
failures.push("missing_import_manifest");
|
|
108
|
+
if (!inputPolicyReport?.ok && suite === "pos")
|
|
109
|
+
failures.push("input_policy_not_ok");
|
|
110
|
+
if (!lintReport?.ok && !strict)
|
|
111
|
+
failures.push("lint_not_ok");
|
|
112
|
+
if (Array.isArray(redactionReport?.hits) && redactionReport.hits.length > 0)
|
|
113
|
+
failures.push("redaction_hits_present");
|
|
114
|
+
if (settingsLocalInput)
|
|
115
|
+
failures.push("settings_local_input_present");
|
|
116
|
+
if (!skillStructure.ok)
|
|
117
|
+
failures.push("skill_structure_invalid");
|
|
118
|
+
if (!strict) {
|
|
119
|
+
if (!exportManifestExists)
|
|
120
|
+
failures.push("missing_export_manifest");
|
|
121
|
+
if (!zipPresent)
|
|
122
|
+
failures.push("zip_missing");
|
|
123
|
+
if (!exportFilesCountMatch)
|
|
124
|
+
failures.push("export_files_count_mismatch");
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
if (result.exit_code !== 2)
|
|
128
|
+
failures.push("strict_exit_code_not_2");
|
|
129
|
+
}
|
|
130
|
+
const ok = failures.length === 0;
|
|
131
|
+
const report = {
|
|
132
|
+
profile_version: "v0",
|
|
133
|
+
suite,
|
|
134
|
+
case_id: caseId,
|
|
135
|
+
run_id: result.run_id,
|
|
136
|
+
ok,
|
|
137
|
+
failures,
|
|
138
|
+
checks: {
|
|
139
|
+
input_policy_ok: Boolean(inputPolicyReport?.ok),
|
|
140
|
+
lint_ok: Boolean(lintReport?.ok),
|
|
141
|
+
redaction_hits: Array.isArray(redactionReport?.hits) ? redactionReport.hits.length : 0,
|
|
142
|
+
settings_local_input: settingsLocalInput,
|
|
143
|
+
skill_structure_ok: skillStructure.ok,
|
|
144
|
+
skill_structure_issues: skillStructure.issues,
|
|
145
|
+
export_manifest_present: exportManifestExists,
|
|
146
|
+
zip_present: zipPresent,
|
|
147
|
+
export_files_count_match: exportFilesCountMatch,
|
|
148
|
+
},
|
|
149
|
+
exit_code: result.exit_code,
|
|
150
|
+
};
|
|
151
|
+
const reportPath = path.join(repoRoot, "artifacts", "quality_v0", result.run_id, "quality_report_v0.json");
|
|
152
|
+
await writeJson(reportPath, report);
|
|
153
|
+
return report;
|
|
154
|
+
}
|
|
155
|
+
async function main() {
|
|
156
|
+
const suiteArg = getArg("--suite") ?? "pos";
|
|
157
|
+
if (suiteArg !== "pos" && suiteArg !== "neg") {
|
|
158
|
+
console.error(`Unknown suite: ${suiteArg}`);
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
const suite = suiteArg;
|
|
162
|
+
if (suite === "pos") {
|
|
163
|
+
await runDepsAudit();
|
|
164
|
+
}
|
|
165
|
+
const fixturesRoot = path.join(process.cwd(), "fixtures", suite);
|
|
166
|
+
const entries = await fs.readdir(fixturesRoot, { withFileTypes: true });
|
|
167
|
+
const cases = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
168
|
+
if (!cases.length) {
|
|
169
|
+
console.error(`No fixtures found in ${fixturesRoot}`);
|
|
170
|
+
process.exit(2);
|
|
171
|
+
}
|
|
172
|
+
const reports = [];
|
|
173
|
+
for (const caseId of cases) {
|
|
174
|
+
reports.push(await runCase(suite, caseId, fixturesRoot));
|
|
175
|
+
}
|
|
176
|
+
if (suite === "pos") {
|
|
177
|
+
const projectDir = path.join(fixturesRoot, "control_basic_project");
|
|
178
|
+
const profilePath = path.join(process.cwd(), "fixtures", "pos", "control_profile_filled", "claude_control_profile_v0.json");
|
|
179
|
+
const outDir = path.join(process.cwd(), ".tmp_test", "quality", "control_check");
|
|
180
|
+
await controlCheckV0(projectDir, profilePath, outDir);
|
|
181
|
+
const roundtripDir = path.join(process.cwd(), ".tmp_test", "quality", "full_scaffold_roundtrip");
|
|
182
|
+
const profileOut = path.join(roundtripDir, "profile");
|
|
183
|
+
const runOut = path.join(roundtripDir, "out");
|
|
184
|
+
await fs.rm(roundtripDir, { recursive: true, force: true });
|
|
185
|
+
await fs.mkdir(roundtripDir, { recursive: true });
|
|
186
|
+
await writeCleanClaudeProfileScaffoldV0(roundtripDir);
|
|
187
|
+
const prefill = await controlPrefillV0(roundtripDir, profileOut);
|
|
188
|
+
await controlCheckV0(roundtripDir, prefill.profile_path, runOut);
|
|
189
|
+
await controlApplyV0(roundtripDir, prefill.profile_path, runOut, "apply");
|
|
190
|
+
await runImport({
|
|
191
|
+
projectDir: roundtripDir,
|
|
192
|
+
outDir: path.join(roundtripDir, "rebuild"),
|
|
193
|
+
includeLocal: false,
|
|
194
|
+
includeUserSettings: false,
|
|
195
|
+
dryRun: false,
|
|
196
|
+
strict: false,
|
|
197
|
+
emitProfile: true,
|
|
198
|
+
emitOverlay: true,
|
|
199
|
+
emitZip: true,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const failed = reports.filter((r) => !r.ok);
|
|
203
|
+
const passed = reports.filter((r) => r.ok);
|
|
204
|
+
let ok = true;
|
|
205
|
+
if (suite === "pos") {
|
|
206
|
+
ok = failed.length === 0;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
ok = passed.length === 0;
|
|
210
|
+
}
|
|
211
|
+
console.log([
|
|
212
|
+
`quality_v0 suite=${suite}`,
|
|
213
|
+
`cases=${reports.length}`,
|
|
214
|
+
`passed=${passed.length}`,
|
|
215
|
+
`failed=${failed.length}`,
|
|
216
|
+
`ok=${ok}`,
|
|
217
|
+
].join(" "));
|
|
218
|
+
process.exit(ok ? 0 : 1);
|
|
219
|
+
}
|
|
220
|
+
main().catch((err) => {
|
|
221
|
+
console.error(err);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type RedactionHit = {
|
|
2
|
+
rule_id: string;
|
|
3
|
+
key?: string;
|
|
4
|
+
len?: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function redactText(input: string): {
|
|
7
|
+
redacted: string;
|
|
8
|
+
hits: RedactionHit[];
|
|
9
|
+
};
|
|
10
|
+
export declare function redactJson(obj: unknown): {
|
|
11
|
+
redacted: unknown;
|
|
12
|
+
hits: RedactionHit[];
|
|
13
|
+
};
|
|
14
|
+
export declare function stableSha256(text: string): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const KEY_RE = /(api[_-]?key|token|secret|password|authorization|bearer)/i;
|
|
3
|
+
const INLINE_SECRET_RE = /(sk-[a-zA-Z0-9]{8,})/g; // best‑effort
|
|
4
|
+
export function redactText(input) {
|
|
5
|
+
const hits = [];
|
|
6
|
+
let out = input;
|
|
7
|
+
// redact obvious inline tokens
|
|
8
|
+
out = out.replace(INLINE_SECRET_RE, (m) => {
|
|
9
|
+
hits.push({ rule_id: "inline_token_like", len: m.length });
|
|
10
|
+
return "[REDACTED_TOKEN]";
|
|
11
|
+
});
|
|
12
|
+
// redact KEY=VALUE lines (best‑effort)
|
|
13
|
+
out = out.replace(/^([A-Z0-9_]{3,80})\s*=\s*(.+)$/gmi, (line, k, v) => {
|
|
14
|
+
if (!KEY_RE.test(k))
|
|
15
|
+
return line;
|
|
16
|
+
hits.push({ rule_id: "key_value_line", key: k, len: String(v).length });
|
|
17
|
+
return `${k}=[REDACTED_VALUE_LEN_${String(v).length}]`;
|
|
18
|
+
});
|
|
19
|
+
return { redacted: out, hits };
|
|
20
|
+
}
|
|
21
|
+
export function redactJson(obj) {
|
|
22
|
+
const hits = [];
|
|
23
|
+
function walk(x) {
|
|
24
|
+
if (x === null || x === undefined)
|
|
25
|
+
return x;
|
|
26
|
+
if (typeof x === "string") {
|
|
27
|
+
const r = redactText(x);
|
|
28
|
+
hits.push(...r.hits);
|
|
29
|
+
return r.redacted;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(x))
|
|
32
|
+
return x.map(walk);
|
|
33
|
+
if (typeof x === "object") {
|
|
34
|
+
const out = {};
|
|
35
|
+
for (const [k, v] of Object.entries(x)) {
|
|
36
|
+
if (KEY_RE.test(k) && typeof v === "string") {
|
|
37
|
+
hits.push({ rule_id: "json_secret_field", key: k, len: v.length });
|
|
38
|
+
out[k] = `[REDACTED_VALUE_LEN_${v.length}]`;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
out[k] = walk(v);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
return x;
|
|
47
|
+
}
|
|
48
|
+
return { redacted: walk(obj), hits };
|
|
49
|
+
}
|
|
50
|
+
export function stableSha256(text) {
|
|
51
|
+
return crypto.createHash("sha256").update(text, "utf8").digest("hex");
|
|
52
|
+
}
|