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.
Files changed (204) hide show
  1. package/.github/workflows/ci.yml +42 -0
  2. package/.github/workflows/release-dry-run.yml +40 -0
  3. package/.github/workflows/release-publish.yml +43 -0
  4. package/.tmp_test_control_apply/proj/.claude/agents/example_agent.md +3 -0
  5. package/.tmp_test_control_apply/proj/.claude/commands/example_command.md +3 -0
  6. package/.tmp_test_control_apply/proj/.claude/hooks/example_hook.sh +2 -0
  7. package/.tmp_test_control_apply/proj/.claude/output-styles/example_style.md +3 -0
  8. package/.tmp_test_control_apply/proj/.claude/settings.json +30 -0
  9. package/.tmp_test_control_apply/proj/.claude/settings.local.example.json +3 -0
  10. package/.tmp_test_control_apply/proj/.mcp.json +3 -0
  11. package/.tmp_test_control_apply/proj/CLAUDE.md +13 -0
  12. package/.tmp_test_control_apply/proj/MOVA.md +3 -0
  13. package/.tmp_test_control_check/proj/.mcp.json +1 -0
  14. package/.tmp_test_control_check/proj/CLAUDE.md +1 -0
  15. package/.tmp_test_control_prefill/out1/claude_control_profile_v0.json +114 -0
  16. package/.tmp_test_control_prefill/out1/prefill_report_v0.json +13 -0
  17. package/.tmp_test_control_prefill/out2/claude_control_profile_v0.json +114 -0
  18. package/.tmp_test_control_prefill/out2/prefill_report_v0.json +13 -0
  19. package/.tmp_test_overlay/proj/.claude/skills/a.md +1 -0
  20. package/.tmp_test_overlay/proj/.mcp.json +1 -0
  21. package/.tmp_test_overlay/proj/CLAUDE.md +1 -0
  22. package/.tmp_test_profile/proj/.claude/skills/a.md +1 -0
  23. package/.tmp_test_profile/proj/.mcp.json +1 -0
  24. package/.tmp_test_profile/proj/CLAUDE.md +1 -0
  25. package/.tmp_test_scaffold_apply/proj/.claude/agents/example_agent.md +3 -0
  26. package/.tmp_test_scaffold_apply/proj/.claude/commands/example_command.md +3 -0
  27. package/.tmp_test_scaffold_apply/proj/.claude/hooks/example_hook.sh +2 -0
  28. package/.tmp_test_scaffold_apply/proj/.claude/output-styles/example_style.md +3 -0
  29. package/.tmp_test_scaffold_apply/proj/.claude/settings.json +30 -0
  30. package/.tmp_test_scaffold_apply/proj/.claude/settings.local.example.json +3 -0
  31. package/.tmp_test_scaffold_apply/proj/.mcp.json +3 -0
  32. package/.tmp_test_scaffold_apply/proj/CLAUDE.md +13 -0
  33. package/.tmp_test_scaffold_apply/proj/MOVA.md +3 -0
  34. package/.tmp_test_strict/mova/claude_import/v0/VERSION.json +10 -0
  35. package/.tmp_test_strict/mova/claude_import/v0/episode_import_run.json +20 -0
  36. package/.tmp_test_strict/mova/claude_import/v0/import_manifest.json +20 -0
  37. package/.tmp_test_strict/mova/claude_import/v0/input_policy_report_v0.json +32 -0
  38. package/.tmp_test_zip/out1/.claude/agents/example_agent.md +3 -0
  39. package/.tmp_test_zip/out1/.claude/commands/example_command.md +3 -0
  40. package/.tmp_test_zip/out1/.claude/commands/mova_context.md +4 -0
  41. package/.tmp_test_zip/out1/.claude/commands/mova_lint.md +4 -0
  42. package/.tmp_test_zip/out1/.claude/commands/mova_proof.md +6 -0
  43. package/.tmp_test_zip/out1/.claude/hooks/example_hook.sh +2 -0
  44. package/.tmp_test_zip/out1/.claude/output-styles/example_style.md +3 -0
  45. package/.tmp_test_zip/out1/.claude/settings.json +30 -0
  46. package/.tmp_test_zip/out1/.claude/settings.local.example.json +3 -0
  47. package/.tmp_test_zip/out1/.claude/skills/a/SKILL.md +1 -0
  48. package/.tmp_test_zip/out1/.claude/skills/mova-control-v0/SKILL.md +11 -0
  49. package/.tmp_test_zip/out1/.claude/skills/mova-layer-v0/SKILL.md +8 -0
  50. package/.tmp_test_zip/out1/.mcp.json +3 -0
  51. package/.tmp_test_zip/out1/CLAUDE.md +4 -0
  52. package/.tmp_test_zip/out1/MOVA.md +10 -0
  53. package/.tmp_test_zip/out1/export.zip +0 -0
  54. package/.tmp_test_zip/out1/mova/claude_import/v0/VERSION.json +10 -0
  55. package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/instruction_profile_v0.json +8 -0
  56. package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/mcp_servers_v0.json +4 -0
  57. package/.tmp_test_zip/out1/mova/claude_import/v0/contracts/skills_catalog_v0.json +11 -0
  58. package/.tmp_test_zip/out1/mova/claude_import/v0/episode_import_run.json +80 -0
  59. package/.tmp_test_zip/out1/mova/claude_import/v0/export_manifest_v0.json +32 -0
  60. package/.tmp_test_zip/out1/mova/claude_import/v0/import_manifest.json +33 -0
  61. package/.tmp_test_zip/out1/mova/claude_import/v0/input_policy_report_v0.json +38 -0
  62. package/.tmp_test_zip/out1/mova/claude_import/v0/lint_report_v0.json +6 -0
  63. package/.tmp_test_zip/out1/mova/claude_import/v0/redaction_report.json +4 -0
  64. package/.tmp_test_zip/out2/.claude/agents/example_agent.md +3 -0
  65. package/.tmp_test_zip/out2/.claude/commands/example_command.md +3 -0
  66. package/.tmp_test_zip/out2/.claude/commands/mova_context.md +4 -0
  67. package/.tmp_test_zip/out2/.claude/commands/mova_lint.md +4 -0
  68. package/.tmp_test_zip/out2/.claude/commands/mova_proof.md +6 -0
  69. package/.tmp_test_zip/out2/.claude/hooks/example_hook.sh +2 -0
  70. package/.tmp_test_zip/out2/.claude/output-styles/example_style.md +3 -0
  71. package/.tmp_test_zip/out2/.claude/settings.json +30 -0
  72. package/.tmp_test_zip/out2/.claude/settings.local.example.json +3 -0
  73. package/.tmp_test_zip/out2/.claude/skills/a/SKILL.md +1 -0
  74. package/.tmp_test_zip/out2/.claude/skills/mova-control-v0/SKILL.md +11 -0
  75. package/.tmp_test_zip/out2/.claude/skills/mova-layer-v0/SKILL.md +8 -0
  76. package/.tmp_test_zip/out2/.mcp.json +3 -0
  77. package/.tmp_test_zip/out2/CLAUDE.md +4 -0
  78. package/.tmp_test_zip/out2/MOVA.md +10 -0
  79. package/.tmp_test_zip/out2/export.zip +0 -0
  80. package/.tmp_test_zip/out2/mova/claude_import/v0/VERSION.json +10 -0
  81. package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/instruction_profile_v0.json +8 -0
  82. package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/mcp_servers_v0.json +4 -0
  83. package/.tmp_test_zip/out2/mova/claude_import/v0/contracts/skills_catalog_v0.json +11 -0
  84. package/.tmp_test_zip/out2/mova/claude_import/v0/episode_import_run.json +80 -0
  85. package/.tmp_test_zip/out2/mova/claude_import/v0/export_manifest_v0.json +32 -0
  86. package/.tmp_test_zip/out2/mova/claude_import/v0/import_manifest.json +33 -0
  87. package/.tmp_test_zip/out2/mova/claude_import/v0/input_policy_report_v0.json +38 -0
  88. package/.tmp_test_zip/out2/mova/claude_import/v0/lint_report_v0.json +6 -0
  89. package/.tmp_test_zip/out2/mova/claude_import/v0/redaction_report.json +4 -0
  90. package/.tmp_test_zip/proj/.claude/skills/a.md +1 -0
  91. package/.tmp_test_zip/proj/.mcp.json +1 -0
  92. package/.tmp_test_zip/proj/CLAUDE.md +1 -0
  93. package/README.md +86 -0
  94. package/create_files.js +52 -0
  95. package/dist/anthropic_profile_v0.d.ts +2 -0
  96. package/dist/anthropic_profile_v0.js +66 -0
  97. package/dist/claude_profile_scaffold_v0.d.ts +2 -0
  98. package/dist/claude_profile_scaffold_v0.js +110 -0
  99. package/dist/cli.d.ts +1 -0
  100. package/dist/cli.js +163 -0
  101. package/dist/cli_entry.d.ts +1 -0
  102. package/dist/cli_entry.js +1 -0
  103. package/dist/control_apply_v0.d.ts +6 -0
  104. package/dist/control_apply_v0.js +86 -0
  105. package/dist/control_check_v0.d.ts +7 -0
  106. package/dist/control_check_v0.js +80 -0
  107. package/dist/control_contracts_v0.d.ts +8 -0
  108. package/dist/control_contracts_v0.js +17 -0
  109. package/dist/control_prefill_v0.d.ts +6 -0
  110. package/dist/control_prefill_v0.js +61 -0
  111. package/dist/export_zip_v0.d.ts +8 -0
  112. package/dist/export_zip_v0.js +79 -0
  113. package/dist/index.d.ts +30 -0
  114. package/dist/index.js +2 -0
  115. package/dist/init_v0.d.ts +7 -0
  116. package/dist/init_v0.js +47 -0
  117. package/dist/input_policy_v0.d.ts +26 -0
  118. package/dist/input_policy_v0.js +76 -0
  119. package/dist/lint_v0.d.ts +18 -0
  120. package/dist/lint_v0.js +131 -0
  121. package/dist/mova_overlay_v0.d.ts +14 -0
  122. package/dist/mova_overlay_v0.js +65 -0
  123. package/dist/mova_spec_bindings_v0.d.ts +5 -0
  124. package/dist/mova_spec_bindings_v0.js +5 -0
  125. package/dist/quality_v0.d.ts +1 -0
  126. package/dist/quality_v0.js +223 -0
  127. package/dist/redaction.d.ts +14 -0
  128. package/dist/redaction.js +52 -0
  129. package/dist/run_import.d.ts +2 -0
  130. package/dist/run_import.js +479 -0
  131. package/dist/stable_json.d.ts +1 -0
  132. package/dist/stable_json.js +15 -0
  133. package/docs/ANTHROPIC_PROFILE_v0.md +38 -0
  134. package/docs/COMPATIBILITY_MATRIX.md +25 -0
  135. package/docs/CONTROL_PROFILE_GUIDE_v0.md +40 -0
  136. package/docs/IMPORT_SPEC_v0.md +30 -0
  137. package/docs/MOVA_SPEC_BINDINGS.json +21 -0
  138. package/docs/MOVA_SPEC_BINDINGS.md +11 -0
  139. package/docs/OPERATOR_GUIDE_v0.md +43 -0
  140. package/docs/SECURITY_MODEL_v0.md +20 -0
  141. package/examples/control_profile_min.json +37 -0
  142. package/examples/control_profile_standard.json +81 -0
  143. package/examples/control_profile_strict.json +68 -0
  144. package/fixtures/neg/bad_skill_structure/.claude/skills/bad/README.md +3 -0
  145. package/fixtures/neg/bad_skill_structure/CLAUDE.md +3 -0
  146. package/fixtures/neg/local_settings_present/.claude/settings.local.json +3 -0
  147. package/fixtures/neg/local_settings_present/.claude/skills/alpha/SKILL.md +6 -0
  148. package/fixtures/neg/local_settings_present/CLAUDE.md +3 -0
  149. package/fixtures/neg/secret_leak/.claude/skills/alpha/SKILL.md +6 -0
  150. package/fixtures/neg/secret_leak/.mcp.json +3 -0
  151. package/fixtures/neg/secret_leak/CLAUDE.md +3 -0
  152. package/fixtures/neg/strict_denied_local/.claude/settings.local.json +3 -0
  153. package/fixtures/neg/strict_denied_local/CLAUDE.md +3 -0
  154. package/fixtures/pos/basic/.claude/skills/alpha/SKILL.md +8 -0
  155. package/fixtures/pos/basic/.mcp.json +3 -0
  156. package/fixtures/pos/basic/CLAUDE.md +3 -0
  157. package/fixtures/pos/control_basic_project/.mcp.json +3 -0
  158. package/fixtures/pos/control_basic_project/CLAUDE.md +3 -0
  159. package/fixtures/pos/control_profile_filled/claude_control_profile_v0.json +18 -0
  160. package/fixtures/pos/full_scaffold_roundtrip/README.md +1 -0
  161. package/package.json +39 -0
  162. package/schemas/claude_control/v0/ds/ds.claude_control_mapping_v0.json +227 -0
  163. package/schemas/claude_control/v0/ds/ds.claude_control_profile_v0.json +114 -0
  164. package/schemas/claude_control/v0/env/env.claude_control_apply_v0.json +170 -0
  165. package/schemas/claude_control/v0/env/env.claude_control_import_prefill_v0.json +171 -0
  166. package/schemas/claude_control/v0/global/global.claude_control_precedence_v0.json +58 -0
  167. package/schemas/claude_control/v0/global/global.claude_control_vocab_v0.json +98 -0
  168. package/schemas/ds.claude_import.instruction_profile_v0.schema.json +31 -0
  169. package/schemas/ds.claude_import.mcp_servers_v0.schema.json +47 -0
  170. package/schemas/ds.claude_import.skills_catalog_v0.schema.json +48 -0
  171. package/src/anthropic_profile_v0.ts +68 -0
  172. package/src/claude_profile_scaffold_v0.ts +117 -0
  173. package/src/cli.ts +160 -0
  174. package/src/cli_entry.ts +1 -0
  175. package/src/control_apply_v0.ts +108 -0
  176. package/src/control_check_v0.ts +98 -0
  177. package/src/control_contracts_v0.ts +26 -0
  178. package/src/control_prefill_v0.ts +74 -0
  179. package/src/export_zip_v0.ts +90 -0
  180. package/src/index.ts +29 -0
  181. package/src/init_v0.ts +59 -0
  182. package/src/input_policy_v0.ts +103 -0
  183. package/src/lint_v0.ts +151 -0
  184. package/src/mova_overlay_v0.ts +79 -0
  185. package/src/mova_spec_bindings_v0.ts +5 -0
  186. package/src/quality_v0.ts +264 -0
  187. package/src/redaction.ts +63 -0
  188. package/src/run_import.ts +526 -0
  189. package/src/stable_json.ts +15 -0
  190. package/test/control_apply_apply.test.js +40 -0
  191. package/test/control_check_preview.test.js +38 -0
  192. package/test/control_prefill.test.js +30 -0
  193. package/test/demo_v0_smoke.test.js +37 -0
  194. package/test/export_zip_determinism.test.js +41 -0
  195. package/test/import_determinism.test.js +53 -0
  196. package/test/init_v0.test.js +37 -0
  197. package/test/overlay_v0_output.test.js +38 -0
  198. package/test/profile_v0_output.test.js +44 -0
  199. package/test/scaffold_v0_output.test.js +64 -0
  200. package/test/strict_input_policy.test.js +45 -0
  201. package/tools/demo_v0.mjs +98 -0
  202. package/tools/deps_audit_v0.mjs +123 -0
  203. package/tools/write_mova_spec_bindings_v0.mjs +122 -0
  204. package/tsconfig.json +13 -0
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { stableStringify } from "./stable_json.js";
4
+ import { loadControlContractsV0 } from "./control_contracts_v0.js";
5
+
6
+ type PrefillResult = {
7
+ profile_path: string;
8
+ report_path: string;
9
+ };
10
+
11
+ async function exists(p: string): Promise<boolean> {
12
+ try {
13
+ await fs.stat(p);
14
+ return true;
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+
20
+ async function readJson(p: string) {
21
+ const raw = await fs.readFile(p, "utf8");
22
+ return JSON.parse(raw);
23
+ }
24
+
25
+ async function writeJson(p: string, obj: any) {
26
+ await fs.mkdir(path.dirname(p), { recursive: true });
27
+ await fs.writeFile(p, stableStringify(obj) + "\n", "utf8");
28
+ }
29
+
30
+ export async function controlPrefillV0(projectDir: string, outDir: string): Promise<PrefillResult> {
31
+ const contracts = await loadControlContractsV0();
32
+ const template = JSON.parse(JSON.stringify(contracts.claude_control_profile));
33
+ const mcpPath = path.join(projectDir, ".mcp.json");
34
+ const settingsPath = path.join(projectDir, ".claude", "settings.json");
35
+ const settingsLocalPath = path.join(projectDir, ".claude", "settings.local.json");
36
+
37
+ let mcpServers: any = {};
38
+ let mcpFound = false;
39
+ if (await exists(mcpPath)) {
40
+ const parsed = await readJson(mcpPath);
41
+ if (Array.isArray(parsed?.servers)) {
42
+ mcpServers = { servers: parsed.servers };
43
+ } else if (parsed?.servers && typeof parsed.servers === "object") {
44
+ mcpServers = { servers: parsed.servers };
45
+ }
46
+ mcpFound = true;
47
+ }
48
+
49
+ if (template?.anthropic?.mcp) {
50
+ template.anthropic.mcp.servers = mcpServers.servers ?? {};
51
+ }
52
+
53
+ const profilePath = path.join(outDir, "claude_control_profile_v0.json");
54
+ const reportPath = path.join(outDir, "prefill_report_v0.json");
55
+
56
+ const report = {
57
+ profile_version: "v0",
58
+ project_dir: projectDir,
59
+ profile_path: profilePath,
60
+ found: {
61
+ mcp_json: mcpFound,
62
+ settings_json: await exists(settingsPath),
63
+ settings_local_json: await exists(settingsLocalPath),
64
+ },
65
+ applied: {
66
+ mcp_servers: mcpFound,
67
+ },
68
+ };
69
+
70
+ await writeJson(profilePath, template);
71
+ await writeJson(reportPath, report);
72
+
73
+ return { profile_path: profilePath, report_path: reportPath };
74
+ }
@@ -0,0 +1,90 @@
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import crypto from "node:crypto";
5
+ import yazl from "yazl";
6
+
7
+ type ExportZipResult = {
8
+ zipAbsPath: string;
9
+ zipRelPath: string;
10
+ zipSha256: string;
11
+ files: string[];
12
+ };
13
+
14
+ const FIXED_MTIME = new Date(Date.UTC(2000, 0, 1, 0, 0, 0));
15
+
16
+ function shouldExclude(rel: string): boolean {
17
+ const parts = rel.split("/");
18
+ for (const part of parts) {
19
+ if (part === "node_modules" || part === "dist" || part === "artifacts") return true;
20
+ if (part.startsWith(".tmp")) return true;
21
+ }
22
+ return false;
23
+ }
24
+
25
+ async function listFilesRec(dir: string): Promise<string[]> {
26
+ const out: string[] = [];
27
+ const stack = [dir];
28
+ while (stack.length) {
29
+ const current = stack.pop()!;
30
+ const entries = await fsp.readdir(current, { withFileTypes: true });
31
+ for (const e of entries) {
32
+ const abs = path.join(current, e.name);
33
+ if (e.isDirectory()) stack.push(abs);
34
+ else if (e.isFile()) out.push(abs);
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+
40
+ function normalizeRelPath(root: string, abs: string): string {
41
+ return path.relative(root, abs).replace(/\\/g, "/");
42
+ }
43
+
44
+ async function sha256File(p: string): Promise<string> {
45
+ return new Promise((resolve, reject) => {
46
+ const hash = crypto.createHash("sha256");
47
+ const stream = fs.createReadStream(p);
48
+ stream.on("error", reject);
49
+ stream.on("data", (d) => hash.update(d));
50
+ stream.on("end", () => resolve(hash.digest("hex")));
51
+ });
52
+ }
53
+
54
+ async function writeZip(absPath: string, files: Array<{ abs: string; rel: string }>) {
55
+ await fsp.mkdir(path.dirname(absPath), { recursive: true });
56
+ const zipfile = new yazl.ZipFile();
57
+ for (const f of files) {
58
+ zipfile.addFile(f.abs, f.rel, { mtime: FIXED_MTIME, mode: 0o100644 });
59
+ }
60
+ const outStream = fs.createWriteStream(absPath);
61
+ const done = new Promise<void>((resolve, reject) => {
62
+ outStream.on("close", resolve);
63
+ outStream.on("error", reject);
64
+ });
65
+ zipfile.outputStream.pipe(outStream);
66
+ zipfile.end();
67
+ await done;
68
+ }
69
+
70
+ export async function createExportZipV0(outRoot: string, zipName?: string): Promise<ExportZipResult> {
71
+ const name = zipName && zipName.trim().length ? zipName.trim() : "export.zip";
72
+ const zipAbsPath = path.isAbsolute(name) ? name : path.join(outRoot, name);
73
+ const zipRelPath = normalizeRelPath(outRoot, zipAbsPath);
74
+ const absFiles = await listFilesRec(outRoot);
75
+ const files = absFiles
76
+ .map((abs) => ({ abs, rel: normalizeRelPath(outRoot, abs) }))
77
+ .filter((f) => !shouldExclude(f.rel))
78
+ .filter((f) => f.rel !== zipRelPath)
79
+ .filter((f) => f.rel !== "mova/claude_import/v0/export_manifest_v0.json")
80
+ .sort((a, b) => a.rel.localeCompare(b.rel));
81
+
82
+ await writeZip(zipAbsPath, files);
83
+ const zipSha256 = await sha256File(zipAbsPath);
84
+ return {
85
+ zipAbsPath,
86
+ zipRelPath,
87
+ zipSha256,
88
+ files: files.map((f) => f.rel),
89
+ };
90
+ }
package/src/index.ts ADDED
@@ -0,0 +1,29 @@
1
+ export type ImportOptions = {
2
+ projectDir: string;
3
+ outDir: string;
4
+ includeLocal: boolean;
5
+ includeUserSettings: boolean;
6
+ dryRun: boolean;
7
+ strict: boolean;
8
+ emitProfile: boolean;
9
+ emitZip: boolean;
10
+ zipName?: string;
11
+ emitOverlay: boolean;
12
+ };
13
+
14
+ export type ImportResult = {
15
+ ok: boolean;
16
+ exit_code?: number;
17
+ run_id: string;
18
+ out_dir: string;
19
+ imported: {
20
+ claude_md: boolean;
21
+ mcp_json: boolean;
22
+ skills_count: number;
23
+ };
24
+ skipped: Array<{ path: string; reason: string }>;
25
+ lint_summary: string;
26
+ };
27
+
28
+ export { runImport } from "./run_import.js";
29
+ export { scanInputPolicyV0 } from "./input_policy_v0.js";
package/src/init_v0.ts ADDED
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getAnthropicProfileV0Files } from "./anthropic_profile_v0.js";
4
+ import { stableStringify } from "./stable_json.js";
5
+ import { createExportZipV0 } from "./export_zip_v0.js";
6
+ import { writeCleanClaudeProfileScaffoldV0 } from "./claude_profile_scaffold_v0.js";
7
+
8
+ type InitResult = {
9
+ createdFiles: string[];
10
+ zipRelPath?: string;
11
+ zipSha256?: string;
12
+ };
13
+
14
+ async function writeTextFile(absPath: string, content: string) {
15
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
16
+ await fs.writeFile(absPath, content, "utf8");
17
+ }
18
+
19
+ async function writeJsonFile(absPath: string, obj: any) {
20
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
21
+ await fs.writeFile(absPath, stableStringify(obj) + "\n", "utf8");
22
+ }
23
+
24
+ export async function initProfileV0(outRoot: string, emitZip: boolean): Promise<InitResult> {
25
+ const createdFiles: string[] = [];
26
+ await writeCleanClaudeProfileScaffoldV0(outRoot);
27
+ const profileFiles = getAnthropicProfileV0Files();
28
+ for (const [rel, content] of Object.entries(profileFiles)) {
29
+ if (rel === "CLAUDE.md" || rel === ".claude/settings.json") continue;
30
+ await writeTextFile(path.join(outRoot, rel), content);
31
+ createdFiles.push(rel);
32
+ }
33
+
34
+ const movaBase = path.join(outRoot, "mova", "claude_import", "v0");
35
+ const initManifestRel = path.join("mova", "claude_import", "v0", "init_manifest_v0.json").replace(/\\/g, "/");
36
+
37
+ let zipRelPath: string | undefined;
38
+ let zipSha256: string | undefined;
39
+ if (emitZip) {
40
+ const exportZip = await createExportZipV0(outRoot);
41
+ zipRelPath = exportZip.zipRelPath;
42
+ zipSha256 = exportZip.zipSha256;
43
+ }
44
+
45
+ const initManifest = {
46
+ profile_version: "v0",
47
+ created_files: createdFiles.slice().sort(),
48
+ zip_rel_path: zipRelPath ?? null,
49
+ zip_sha256: zipSha256 ?? null,
50
+ };
51
+ await writeJsonFile(path.join(outRoot, initManifestRel), initManifest);
52
+ createdFiles.push(initManifestRel);
53
+
54
+ return {
55
+ createdFiles,
56
+ zipRelPath,
57
+ zipSha256,
58
+ };
59
+ }
@@ -0,0 +1,103 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export type InputPolicyKind =
5
+ | "claude_settings_local"
6
+ | "local_file"
7
+ | "env_file"
8
+ | "private_key_like"
9
+ | "other";
10
+
11
+ export type InputPolicyEntry = {
12
+ path: string;
13
+ kind: InputPolicyKind;
14
+ };
15
+
16
+ export type InputPolicyDenied = InputPolicyEntry & {
17
+ reason: string;
18
+ };
19
+
20
+ export type InputPolicyReportV0 = {
21
+ policy_version: "v0";
22
+ found: InputPolicyEntry[];
23
+ denied: InputPolicyDenied[];
24
+ allowed: InputPolicyEntry[];
25
+ opts: { strict: boolean; include_local: boolean };
26
+ ok: boolean;
27
+ exit_code_recommended: 0 | 2;
28
+ };
29
+
30
+ type ScanOpts = { strict: boolean; include_local: boolean };
31
+
32
+ async function listFilesRec(dir: string): Promise<string[]> {
33
+ const out: string[] = [];
34
+ const stack = [dir];
35
+ while (stack.length) {
36
+ const current = stack.pop()!;
37
+ const entries = await fs.readdir(current, { withFileTypes: true });
38
+ for (const e of entries) {
39
+ const abs = path.join(current, e.name);
40
+ if (e.isDirectory()) stack.push(abs);
41
+ else if (e.isFile()) out.push(abs);
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+
47
+ function classify(relPath: string): InputPolicyKind {
48
+ const rel = relPath.replace(/\\/g, "/");
49
+ const base = path.posix.basename(rel);
50
+ if (rel.endsWith("/.claude/settings.local.json") || base === "settings.local.json") {
51
+ return "claude_settings_local";
52
+ }
53
+ if (base === "CLAUDE.local.md" || base.includes(".local.")) {
54
+ return "local_file";
55
+ }
56
+ if (base === ".env" || base.startsWith(".env.")) {
57
+ return "env_file";
58
+ }
59
+ if (base.startsWith("id_rsa") || base.endsWith(".pem") || base.endsWith(".key")) {
60
+ return "private_key_like";
61
+ }
62
+ return "other";
63
+ }
64
+
65
+ export async function scanInputPolicyV0(projectRoot: string, opts: ScanOpts): Promise<InputPolicyReportV0> {
66
+ const absFiles = await listFilesRec(projectRoot);
67
+ const found: InputPolicyEntry[] = [];
68
+ const denied: InputPolicyDenied[] = [];
69
+ const allowed: InputPolicyEntry[] = [];
70
+
71
+ for (const abs of absFiles) {
72
+ const rel = path.relative(projectRoot, abs).replace(/\\/g, "/");
73
+ const kind = classify(rel);
74
+ const entry = { path: rel, kind };
75
+ found.push(entry);
76
+
77
+ if (kind === "env_file") {
78
+ denied.push({ ...entry, reason: "env_files_not_allowed" });
79
+ } else if (kind === "private_key_like") {
80
+ denied.push({ ...entry, reason: "private_keys_not_allowed" });
81
+ } else if (kind === "claude_settings_local") {
82
+ denied.push({ ...entry, reason: "settings_local_not_allowed" });
83
+ } else if (kind === "local_file") {
84
+ if (opts.include_local) allowed.push(entry);
85
+ else denied.push({ ...entry, reason: "local_files_not_allowed" });
86
+ } else {
87
+ allowed.push(entry);
88
+ }
89
+ }
90
+
91
+ const ok = denied.length === 0;
92
+ const exit_code_recommended: 0 | 2 = !ok && opts.strict ? 2 : 0;
93
+
94
+ return {
95
+ policy_version: "v0",
96
+ found,
97
+ denied,
98
+ allowed,
99
+ opts,
100
+ ok,
101
+ exit_code_recommended,
102
+ };
103
+ }
package/src/lint_v0.ts ADDED
@@ -0,0 +1,151 @@
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
+
6
+ export type LintIssue = {
7
+ code: string;
8
+ message: string;
9
+ path?: string;
10
+ };
11
+
12
+ export type LintReportV0 = {
13
+ profile_version: "v0";
14
+ ok: boolean;
15
+ issues: LintIssue[];
16
+ summary: string;
17
+ };
18
+
19
+ type LintOptions = {
20
+ outRoot: string;
21
+ emitProfile: boolean;
22
+ mcpExpected: boolean;
23
+ };
24
+
25
+ async function exists(p: string): Promise<boolean> {
26
+ try {
27
+ await fs.stat(p);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ async function listFilesRec(dir: string): Promise<string[]> {
35
+ const out: string[] = [];
36
+ const stack = [dir];
37
+ while (stack.length) {
38
+ const current = stack.pop()!;
39
+ const entries = await fs.readdir(current, { withFileTypes: true });
40
+ for (const e of entries) {
41
+ const abs = path.join(current, e.name);
42
+ if (e.isDirectory()) stack.push(abs);
43
+ else if (e.isFile()) out.push(abs);
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+
49
+ function extractFrontmatterName(body: string): string | null {
50
+ const match = body.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
51
+ if (!match) return null;
52
+ const lines = match[1].split(/\r?\n/);
53
+ for (const line of lines) {
54
+ const m = line.match(/^name:\s*(.+)\s*$/);
55
+ if (m) return m[1].trim();
56
+ }
57
+ return null;
58
+ }
59
+
60
+ export async function lintV0(opts: LintOptions): Promise<LintReportV0> {
61
+ const issues: LintIssue[] = [];
62
+ const required = [
63
+ ...(opts.emitProfile ? anthropicProfileV0RequiredFiles : []),
64
+ "mova/claude_import/v0/import_manifest.json",
65
+ "mova/claude_import/v0/redaction_report.json",
66
+ "mova/claude_import/v0/contracts/instruction_profile_v0.json",
67
+ "mova/claude_import/v0/contracts/skills_catalog_v0.json",
68
+ "mova/claude_import/v0/contracts/mcp_servers_v0.json",
69
+ "mova/claude_import/v0/episode_import_run.json",
70
+ ];
71
+
72
+ if (opts.mcpExpected && opts.emitProfile) {
73
+ required.push(".mcp.json");
74
+ }
75
+
76
+ for (const rel of required) {
77
+ const abs = path.join(opts.outRoot, rel);
78
+ if (!(await exists(abs))) {
79
+ issues.push({ code: "missing_file", message: "Required file is missing.", path: rel });
80
+ }
81
+ }
82
+
83
+ const settingsLocal = path.join(opts.outRoot, ".claude", "settings.local.json");
84
+ if (await exists(settingsLocal)) {
85
+ issues.push({ code: "settings_local_present", message: "settings.local.json must not be written.", path: ".claude/settings.local.json" });
86
+ }
87
+
88
+ const skillsRoot = path.join(opts.outRoot, ".claude", "skills");
89
+ if (await exists(skillsRoot)) {
90
+ const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
91
+ for (const e of entries) {
92
+ const abs = path.join(skillsRoot, e.name);
93
+ if (e.isFile()) {
94
+ issues.push({ code: "skill_root_file", message: "Files are not allowed directly under .claude/skills.", path: `.claude/skills/${e.name}` });
95
+ continue;
96
+ }
97
+ if (e.isDirectory()) {
98
+ const skillMd = path.join(abs, "SKILL.md");
99
+ if (!(await exists(skillMd))) {
100
+ issues.push({ code: "missing_skill_md", message: "Skill directory is missing SKILL.md.", path: `.claude/skills/${e.name}/SKILL.md` });
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ const skillNameCounts = new Map<string, number>();
107
+ if (await exists(skillsRoot)) {
108
+ const entries = await fs.readdir(skillsRoot, { withFileTypes: true });
109
+ for (const e of entries) {
110
+ if (!e.isDirectory()) continue;
111
+ const skillMd = path.join(skillsRoot, e.name, "SKILL.md");
112
+ if (!(await exists(skillMd))) continue;
113
+ const body = await fs.readFile(skillMd, "utf8");
114
+ const name = extractFrontmatterName(body);
115
+ if (!name) continue;
116
+ skillNameCounts.set(name, (skillNameCounts.get(name) ?? 0) + 1);
117
+ }
118
+ }
119
+ for (const [name, count] of skillNameCounts.entries()) {
120
+ if (count > 1) {
121
+ issues.push({ code: "duplicate_skill_name", message: `Duplicate skill name in frontmatter: ${name}`, path: ".claude/skills" });
122
+ }
123
+ }
124
+
125
+ const jsonFiles = (await exists(opts.outRoot)) ? (await listFilesRec(opts.outRoot)).filter((p) => p.toLowerCase().endsWith(".json")) : [];
126
+ for (const abs of jsonFiles) {
127
+ if (abs.endsWith(`${path.sep}lint_report_v0.json`)) continue;
128
+ const rel = path.relative(opts.outRoot, abs).replace(/\\/g, "/");
129
+ const raw = await fs.readFile(abs, "utf8");
130
+ let parsed: any;
131
+ try {
132
+ parsed = JSON.parse(raw);
133
+ } catch {
134
+ issues.push({ code: "invalid_json", message: "JSON file failed to parse.", path: rel });
135
+ continue;
136
+ }
137
+ const normalized = stableStringify(parsed) + "\n";
138
+ if (raw !== normalized) {
139
+ issues.push({ code: "json_not_normalized", message: "JSON file is not normalized (sorted keys, 2 spaces, newline).", path: rel });
140
+ }
141
+ }
142
+
143
+ const ok = issues.length === 0;
144
+ const summary = ok ? "lint_v0: ok" : `lint_v0: ${issues.length} issue(s)`;
145
+ return {
146
+ profile_version: "v0",
147
+ ok,
148
+ issues,
149
+ summary,
150
+ };
151
+ }
@@ -0,0 +1,79 @@
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
+
12
+ export const MOVA_CONTROL_ENTRY_MARKER = "<!-- MOVA_CONTROL_ENTRY_V0 -->";
13
+
14
+ export function buildMovaOverlayV0(params: OverlayParams): Record<string, string> {
15
+ const contractsDir = params.contractsDir;
16
+ const artifactsDir = params.artifactsDir;
17
+ const instruction = `${contractsDir}${params.instructionProfileFile}`;
18
+ const skills = `${contractsDir}${params.skillsCatalogFile}`;
19
+ const mcp = `${contractsDir}${params.mcpServersFile}`;
20
+ const lint = `${artifactsDir}${params.lintReportFile}`;
21
+ const quality = `${artifactsDir}${params.qualityReportFile}`;
22
+ const exportManifest = `${artifactsDir}${params.exportManifestFile}`;
23
+
24
+ return {
25
+ ".claude/commands/mova_context.md": [
26
+ "# mova_context",
27
+ "",
28
+ "Use MOVA contracts as source of truth:",
29
+ `- ${instruction}`,
30
+ `- ${skills}`,
31
+ `- ${mcp}`,
32
+ "",
33
+ "Then use CLAUDE.md and MOVA.md as narrative guides.",
34
+ "",
35
+ ].join("\n"),
36
+ ".claude/commands/mova_proof.md": [
37
+ "# mova_proof",
38
+ "",
39
+ "Proof/evidence files:",
40
+ `- ${lint}`,
41
+ `- ${quality}`,
42
+ `- ${exportManifest}`,
43
+ "",
44
+ ].join("\n"),
45
+ ".claude/skills/mova-control-v0/SKILL.md": [
46
+ "---",
47
+ "name: mova-control-v0",
48
+ "version: v0",
49
+ "---",
50
+ "",
51
+ "# mova-control-v0",
52
+ "",
53
+ "Rules:",
54
+ "- Use MOVA contracts first.",
55
+ "- Use evidence files for verification.",
56
+ "- Do not invent missing data.",
57
+ "",
58
+ ].join("\n"),
59
+ };
60
+ }
61
+
62
+ export function buildMovaControlEntryV0(params: OverlayParams): string {
63
+ const contractsDir = params.contractsDir;
64
+ const artifactsDir = params.artifactsDir;
65
+ return [
66
+ MOVA_CONTROL_ENTRY_MARKER,
67
+ "## MOVA Control Entry (v0)",
68
+ "",
69
+ "Source of truth (do this first):",
70
+ `- ${contractsDir}${params.instructionProfileFile}`,
71
+ `- ${contractsDir}${params.skillsCatalogFile}`,
72
+ `- ${contractsDir}${params.mcpServersFile}`,
73
+ "",
74
+ "Proof / evidence:",
75
+ `- ${artifactsDir}${params.qualityReportFile}`,
76
+ `- ${artifactsDir}${params.exportManifestFile}`,
77
+ "",
78
+ ].join("\n");
79
+ }
@@ -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
+ } as const;