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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function readExportManifest(outDir) {
|
|
11
|
+
const p = path.join(outDir, "mova", "claude_import", "v0", "export_manifest_v0.json");
|
|
12
|
+
const raw = await fs.readFile(p, "utf8");
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test("zip export is deterministic across runs", async () => {
|
|
17
|
+
const tmp = path.join(process.cwd(), ".tmp_test_zip");
|
|
18
|
+
const proj = path.join(tmp, "proj");
|
|
19
|
+
const out1 = path.join(tmp, "out1");
|
|
20
|
+
const out2 = path.join(tmp, "out2");
|
|
21
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
22
|
+
await fs.mkdir(path.join(proj, ".claude", "skills"), { recursive: true });
|
|
23
|
+
|
|
24
|
+
await fs.writeFile(path.join(proj, "CLAUDE.md"), "Hello\n", "utf8");
|
|
25
|
+
await fs.writeFile(path.join(proj, ".mcp.json"), "{\"servers\":[]}", "utf8");
|
|
26
|
+
await fs.writeFile(path.join(proj, ".claude", "skills", "a.md"), "# skill\n", "utf8");
|
|
27
|
+
|
|
28
|
+
await execFileP("node", ["dist/cli.js", "--project", proj, "--out", out1, "--zip"]);
|
|
29
|
+
await execFileP("node", ["dist/cli.js", "--project", proj, "--out", out2, "--zip"]);
|
|
30
|
+
|
|
31
|
+
const manifest1 = await readExportManifest(out1);
|
|
32
|
+
const manifest2 = await readExportManifest(out2);
|
|
33
|
+
|
|
34
|
+
assert.equal(manifest1.zip_sha256, manifest2.zip_sha256);
|
|
35
|
+
|
|
36
|
+
const zip1 = path.join(out1, manifest1.zip_rel_path);
|
|
37
|
+
await fs.stat(zip1);
|
|
38
|
+
|
|
39
|
+
assert.ok(manifest1.files.includes("CLAUDE.md"));
|
|
40
|
+
assert.ok(manifest1.files.includes(".claude/settings.json"));
|
|
41
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function listFilesRec(dir) {
|
|
11
|
+
const out = [];
|
|
12
|
+
const stack = [dir];
|
|
13
|
+
while (stack.length) {
|
|
14
|
+
const current = stack.pop();
|
|
15
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
16
|
+
for (const e of entries) {
|
|
17
|
+
const abs = path.join(current, e.name);
|
|
18
|
+
if (e.isDirectory()) stack.push(abs);
|
|
19
|
+
else if (e.isFile()) out.push(abs);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function readOutputSnapshot(root) {
|
|
26
|
+
const files = await listFilesRec(root);
|
|
27
|
+
const snapshot = {};
|
|
28
|
+
for (const abs of files) {
|
|
29
|
+
const rel = path.relative(root, abs).replace(/\\/g, "/");
|
|
30
|
+
snapshot[rel] = await fs.readFile(abs, "utf8");
|
|
31
|
+
}
|
|
32
|
+
return JSON.stringify(snapshot, Object.keys(snapshot).sort(), 2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("import is deterministic for same inputs", async () => {
|
|
36
|
+
const tmp = path.join(process.cwd(), ".tmp_test");
|
|
37
|
+
const proj = path.join(tmp, "proj");
|
|
38
|
+
const out = path.join(tmp, "out");
|
|
39
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
40
|
+
await fs.mkdir(path.join(proj, ".claude", "skills"), { recursive: true });
|
|
41
|
+
|
|
42
|
+
await fs.writeFile(path.join(proj, "CLAUDE.md"), "Hello\n", "utf8");
|
|
43
|
+
await fs.writeFile(path.join(proj, ".mcp.json"), "{\"servers\":[]}", "utf8");
|
|
44
|
+
await fs.writeFile(path.join(proj, ".claude", "skills", "a.md"), "# skill\n", "utf8");
|
|
45
|
+
|
|
46
|
+
await execFileP("node", ["dist/cli.js", "--project", proj, "--out", out]);
|
|
47
|
+
const snapshot1 = await readOutputSnapshot(out);
|
|
48
|
+
|
|
49
|
+
await execFileP("node", ["dist/cli.js", "--project", proj, "--out", out]);
|
|
50
|
+
const snapshot2 = await readOutputSnapshot(out);
|
|
51
|
+
|
|
52
|
+
assert.equal(snapshot1, snapshot2);
|
|
53
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function assertExists(p) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.stat(p);
|
|
13
|
+
} catch {
|
|
14
|
+
assert.fail(`Expected file to exist: ${p}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("init v0 creates profile scaffold and optional zip", async () => {
|
|
19
|
+
const tmp = path.join(process.cwd(), ".tmp_test_init");
|
|
20
|
+
const out = path.join(tmp, "out");
|
|
21
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
22
|
+
await fs.mkdir(out, { recursive: true });
|
|
23
|
+
|
|
24
|
+
await execFileP("node", ["dist/cli.js", "init", "--out", out, "--zip"]);
|
|
25
|
+
|
|
26
|
+
await assertExists(path.join(out, "CLAUDE.md"));
|
|
27
|
+
await assertExists(path.join(out, ".claude", "settings.json"));
|
|
28
|
+
await assertExists(path.join(out, ".claude", "skills", "mova-layer-v0", "SKILL.md"));
|
|
29
|
+
|
|
30
|
+
const initManifestPath = path.join(out, "mova", "claude_import", "v0", "init_manifest_v0.json");
|
|
31
|
+
const initManifest = JSON.parse(await fs.readFile(initManifestPath, "utf8"));
|
|
32
|
+
assert.equal(initManifest.profile_version, "v0");
|
|
33
|
+
assert.ok(initManifest.zip_sha256);
|
|
34
|
+
assert.ok(initManifest.zip_rel_path);
|
|
35
|
+
|
|
36
|
+
await assertExists(path.join(out, initManifest.zip_rel_path));
|
|
37
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function assertExists(p) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.stat(p);
|
|
13
|
+
} catch {
|
|
14
|
+
assert.fail(`Expected file to exist: ${p}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("overlay v0 files are emitted and CLAUDE entry exists", async () => {
|
|
19
|
+
const tmp = path.join(process.cwd(), ".tmp_test_overlay");
|
|
20
|
+
const proj = path.join(tmp, "proj");
|
|
21
|
+
const out = path.join(tmp, "out");
|
|
22
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
23
|
+
await fs.mkdir(path.join(proj, ".claude", "skills"), { recursive: true });
|
|
24
|
+
|
|
25
|
+
await fs.writeFile(path.join(proj, "CLAUDE.md"), "Hello\n", "utf8");
|
|
26
|
+
await fs.writeFile(path.join(proj, ".mcp.json"), "{\"servers\":[]}", "utf8");
|
|
27
|
+
await fs.writeFile(path.join(proj, ".claude", "skills", "a.md"), "# skill\n", "utf8");
|
|
28
|
+
|
|
29
|
+
await execFileP("node", ["dist/cli.js", "--project", proj, "--out", out]);
|
|
30
|
+
|
|
31
|
+
await assertExists(path.join(out, ".claude", "commands", "mova_context.md"));
|
|
32
|
+
await assertExists(path.join(out, ".claude", "commands", "mova_proof.md"));
|
|
33
|
+
await assertExists(path.join(out, ".claude", "skills", "mova-control-v0", "SKILL.md"));
|
|
34
|
+
|
|
35
|
+
const claudePath = path.join(out, "CLAUDE.md");
|
|
36
|
+
const claude = await fs.readFile(claudePath, "utf8");
|
|
37
|
+
assert.ok(claude.includes("MOVA_CONTROL_ENTRY_V0"));
|
|
38
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function assertExists(p) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.stat(p);
|
|
13
|
+
} catch {
|
|
14
|
+
assert.fail(`Expected file to exist: ${p}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("profile v0 output includes required files", async () => {
|
|
19
|
+
const tmp = path.join(process.cwd(), ".tmp_test_profile");
|
|
20
|
+
const proj = path.join(tmp, "proj");
|
|
21
|
+
const out = path.join(tmp, "out");
|
|
22
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
23
|
+
await fs.mkdir(path.join(proj, ".claude", "skills"), { recursive: true });
|
|
24
|
+
|
|
25
|
+
await fs.writeFile(path.join(proj, "CLAUDE.md"), "Hello\n", "utf8");
|
|
26
|
+
await fs.writeFile(path.join(proj, ".mcp.json"), "{\"servers\":[]}", "utf8");
|
|
27
|
+
await fs.writeFile(path.join(proj, ".claude", "skills", "a.md"), "# skill\n", "utf8");
|
|
28
|
+
|
|
29
|
+
await execFileP("node", ["dist/cli.js", "--project", proj, "--out", out]);
|
|
30
|
+
|
|
31
|
+
await assertExists(path.join(out, "CLAUDE.md"));
|
|
32
|
+
await assertExists(path.join(out, "MOVA.md"));
|
|
33
|
+
await assertExists(path.join(out, ".claude", "settings.json"));
|
|
34
|
+
await assertExists(path.join(out, ".claude", "commands", "mova_context.md"));
|
|
35
|
+
await assertExists(path.join(out, ".claude", "commands", "mova_lint.md"));
|
|
36
|
+
await assertExists(path.join(out, ".claude", "skills", "mova-layer-v0", "SKILL.md"));
|
|
37
|
+
await assertExists(path.join(out, "mova", "claude_import", "v0", "lint_report_v0.json"));
|
|
38
|
+
const versionPath = path.join(out, "mova", "claude_import", "v0", "VERSION.json");
|
|
39
|
+
await assertExists(versionPath);
|
|
40
|
+
const version = JSON.parse(await fs.readFile(versionPath, "utf8"));
|
|
41
|
+
assert.equal(version.tool_name, "mova-claude-import");
|
|
42
|
+
assert.ok(version.tool_version);
|
|
43
|
+
assert.equal(version.profile_version, "anthropic_profile_v0");
|
|
44
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function assertExists(p) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.stat(p);
|
|
13
|
+
} catch {
|
|
14
|
+
assert.fail(`Expected file to exist: ${p}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("init creates full scaffold surfaces", async () => {
|
|
19
|
+
const tmp = path.join(process.cwd(), ".tmp_test_scaffold");
|
|
20
|
+
const out = path.join(tmp, "out");
|
|
21
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
22
|
+
await fs.mkdir(out, { recursive: true });
|
|
23
|
+
|
|
24
|
+
await execFileP("node", ["dist/cli.js", "init", "--out", out]);
|
|
25
|
+
|
|
26
|
+
await assertExists(path.join(out, "CLAUDE.md"));
|
|
27
|
+
await assertExists(path.join(out, ".claude", "settings.json"));
|
|
28
|
+
await assertExists(path.join(out, ".claude", "settings.local.example.json"));
|
|
29
|
+
await assertExists(path.join(out, ".claude", "commands", "example_command.md"));
|
|
30
|
+
await assertExists(path.join(out, ".claude", "agents", "example_agent.md"));
|
|
31
|
+
await assertExists(path.join(out, ".claude", "output-styles", "example_style.md"));
|
|
32
|
+
await assertExists(path.join(out, ".claude", "hooks", "example_hook.sh"));
|
|
33
|
+
await assertExists(path.join(out, ".mcp.json"));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("control apply creates missing surfaces without erasing content", async () => {
|
|
37
|
+
const tmp = path.join(process.cwd(), ".tmp_test_scaffold_apply");
|
|
38
|
+
const proj = path.join(tmp, "proj");
|
|
39
|
+
const out = path.join(tmp, "out");
|
|
40
|
+
const profile = path.join(process.cwd(), "fixtures", "pos", "control_profile_filled", "claude_control_profile_v0.json");
|
|
41
|
+
await fs.rm(tmp, { recursive: true, force: true });
|
|
42
|
+
await fs.mkdir(proj, { recursive: true });
|
|
43
|
+
await fs.writeFile(path.join(proj, "CLAUDE.md"), "Custom\n", "utf8");
|
|
44
|
+
|
|
45
|
+
await execFileP("node", [
|
|
46
|
+
"dist/cli.js",
|
|
47
|
+
"control",
|
|
48
|
+
"apply",
|
|
49
|
+
"--project",
|
|
50
|
+
proj,
|
|
51
|
+
"--profile",
|
|
52
|
+
profile,
|
|
53
|
+
"--mode",
|
|
54
|
+
"apply",
|
|
55
|
+
"--out",
|
|
56
|
+
out,
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const claude = await fs.readFile(path.join(proj, "CLAUDE.md"), "utf8");
|
|
60
|
+
assert.ok(claude.includes("Custom"));
|
|
61
|
+
assert.ok(claude.includes("MOVA_CONTROL_ENTRY_V0"));
|
|
62
|
+
await assertExists(path.join(proj, ".claude", "settings.json"));
|
|
63
|
+
await assertExists(path.join(proj, ".claude", "commands", "example_command.md"));
|
|
64
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function assertExists(p) {
|
|
11
|
+
try {
|
|
12
|
+
await fs.stat(p);
|
|
13
|
+
} catch {
|
|
14
|
+
assert.fail(`Expected file to exist: ${p}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("strict input policy denies local settings and stops rebuild", async () => {
|
|
19
|
+
const out = path.join(process.cwd(), ".tmp_test_strict");
|
|
20
|
+
await fs.rm(out, { recursive: true, force: true });
|
|
21
|
+
await fs.mkdir(out, { recursive: true });
|
|
22
|
+
|
|
23
|
+
const proj = path.join(process.cwd(), "fixtures", "neg", "strict_denied_local");
|
|
24
|
+
let exitCode = 0;
|
|
25
|
+
try {
|
|
26
|
+
await execFileP("node", ["dist/cli.js", "--project", proj, "--out", out, "--strict"]);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
exitCode = err?.code ?? 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
assert.equal(exitCode, 2);
|
|
32
|
+
|
|
33
|
+
const reportPath = path.join(out, "mova", "claude_import", "v0", "input_policy_report_v0.json");
|
|
34
|
+
await assertExists(reportPath);
|
|
35
|
+
const report = JSON.parse(await fs.readFile(reportPath, "utf8"));
|
|
36
|
+
assert.equal(report.ok, false);
|
|
37
|
+
|
|
38
|
+
let claudeExists = true;
|
|
39
|
+
try {
|
|
40
|
+
await fs.stat(path.join(out, "CLAUDE.md"));
|
|
41
|
+
} catch {
|
|
42
|
+
claudeExists = false;
|
|
43
|
+
}
|
|
44
|
+
assert.equal(claudeExists, false);
|
|
45
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execFileP = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
function sha256(text) {
|
|
10
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function exists(p) {
|
|
14
|
+
try {
|
|
15
|
+
await fs.stat(p);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(cmd, args, cwd) {
|
|
23
|
+
await execFileP(cmd, args, { cwd });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function main() {
|
|
27
|
+
const repoRoot = process.cwd();
|
|
28
|
+
const demoRoot = path.join(repoRoot, ".tmp_demo");
|
|
29
|
+
const proj = path.join(demoRoot, "proj");
|
|
30
|
+
const out = path.join(demoRoot, "out");
|
|
31
|
+
|
|
32
|
+
await fs.rm(demoRoot, { recursive: true, force: true });
|
|
33
|
+
await fs.mkdir(demoRoot, { recursive: true });
|
|
34
|
+
|
|
35
|
+
const nodeCmd = process.platform === "win32" ? "node.exe" : "node";
|
|
36
|
+
const cli = path.join(repoRoot, "dist", "cli.js");
|
|
37
|
+
|
|
38
|
+
await run(nodeCmd, [cli, "init", "--out", proj], repoRoot);
|
|
39
|
+
await run(nodeCmd, [cli, "control", "prefill", "--project", proj, "--out", proj], repoRoot);
|
|
40
|
+
|
|
41
|
+
const exampleProfile = path.join(repoRoot, "examples", "control_profile_min.json");
|
|
42
|
+
const profilePath = path.join(proj, "claude_control_profile_v0.json");
|
|
43
|
+
const exampleRaw = await fs.readFile(exampleProfile, "utf8");
|
|
44
|
+
await fs.writeFile(profilePath, exampleRaw, "utf8");
|
|
45
|
+
|
|
46
|
+
await run(nodeCmd, [cli, "control", "check", "--project", proj, "--profile", profilePath, "--out", proj], repoRoot);
|
|
47
|
+
await run(nodeCmd, [cli, "control", "apply", "--project", proj, "--profile", profilePath, "--out", proj, "--mode", "apply"], repoRoot);
|
|
48
|
+
await run(nodeCmd, [cli, "--project", proj, "--out", out, "--zip", "--strict=false"], repoRoot);
|
|
49
|
+
|
|
50
|
+
const runsDir = path.join(proj, "mova", "claude_control", "v0", "runs");
|
|
51
|
+
const runDirs = (await exists(runsDir)) ? await fs.readdir(runsDir) : [];
|
|
52
|
+
runDirs.sort();
|
|
53
|
+
let latestCheckSource = "";
|
|
54
|
+
let latestApplySource = "";
|
|
55
|
+
for (const dir of runDirs) {
|
|
56
|
+
const check = path.join(runsDir, dir, "control_summary_v0.json");
|
|
57
|
+
const apply = path.join(runsDir, dir, "control_apply_report_v0.json");
|
|
58
|
+
if (await exists(check)) latestCheckSource = check;
|
|
59
|
+
if (await exists(apply)) latestApplySource = apply;
|
|
60
|
+
}
|
|
61
|
+
const latestCheck = path.join(runsDir, "latest_check_summary.json");
|
|
62
|
+
const latestApply = path.join(runsDir, "latest_apply_report.json");
|
|
63
|
+
if (latestCheckSource) await fs.writeFile(latestCheck, await fs.readFile(latestCheckSource));
|
|
64
|
+
if (latestApplySource) await fs.writeFile(latestApply, await fs.readFile(latestApplySource));
|
|
65
|
+
|
|
66
|
+
const runId = sha256([proj, out, exampleRaw].join("|")).slice(0, 16);
|
|
67
|
+
const artifactsDir = path.join(repoRoot, "artifacts", "demo_v0", runId);
|
|
68
|
+
await fs.mkdir(artifactsDir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
const exportManifest = path.join(out, "mova", "claude_import", "v0", "export_manifest_v0.json");
|
|
71
|
+
let zipPath = "";
|
|
72
|
+
if (await exists(exportManifest)) {
|
|
73
|
+
const exportObj = JSON.parse(await fs.readFile(exportManifest, "utf8"));
|
|
74
|
+
if (exportObj?.zip_rel_path) {
|
|
75
|
+
zipPath = path.join(out, exportObj.zip_rel_path);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const controlRunsDir = path.join(proj, "mova", "claude_control", "v0", "runs");
|
|
79
|
+
const report = {
|
|
80
|
+
input_project: proj,
|
|
81
|
+
output_path: out,
|
|
82
|
+
zip_path: zipPath,
|
|
83
|
+
refs: {
|
|
84
|
+
manifest: path.join(out, "mova", "claude_import", "v0", "import_manifest.json"),
|
|
85
|
+
export_manifest: exportManifest,
|
|
86
|
+
control_runs_dir: controlRunsDir,
|
|
87
|
+
control_check_summary: path.join(controlRunsDir, "latest_check_summary.json"),
|
|
88
|
+
control_apply_report: path.join(controlRunsDir, "latest_apply_report.json"),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
await fs.writeFile(path.join(artifactsDir, "demo_report_v0.json"), JSON.stringify(report, null, 2) + "\n", "utf8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
main().catch((err) => {
|
|
96
|
+
console.error(err);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
|
|
5
|
+
const TARGET_DEPS = ["@leryk1981/mova-spec", "@leryk1981/mova-core-engine"];
|
|
6
|
+
|
|
7
|
+
async function listFilesRec(dir) {
|
|
8
|
+
const out = [];
|
|
9
|
+
const stack = [dir];
|
|
10
|
+
while (stack.length) {
|
|
11
|
+
const current = stack.pop();
|
|
12
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
13
|
+
for (const e of entries) {
|
|
14
|
+
const abs = path.join(current, e.name);
|
|
15
|
+
if (e.isDirectory()) stack.push(abs);
|
|
16
|
+
else if (e.isFile()) out.push(abs);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sha256Strings(items) {
|
|
23
|
+
const h = crypto.createHash("sha256");
|
|
24
|
+
for (const item of items) h.update(item);
|
|
25
|
+
return h.digest("hex").slice(0, 16);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function importMatches(content, dep) {
|
|
29
|
+
const variants = [dep, `${dep}/package.json`];
|
|
30
|
+
return variants.some((variant) => {
|
|
31
|
+
const esc = variant.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
32
|
+
const importRe = new RegExp(`from\\s+["']${esc}["']|import\\s+["']${esc}["']`, "g");
|
|
33
|
+
const requireRe = new RegExp(`require\\(\\s*["']${esc}["']\\s*\\)`, "g");
|
|
34
|
+
const resolveRe = new RegExp(`require\\.resolve\\(\\s*["']${esc}["']\\s*\\)`, "g");
|
|
35
|
+
return importRe.test(content) || requireRe.test(content) || resolveRe.test(content);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function usageHintsCoreEngine(content) {
|
|
40
|
+
return /EvidenceWriter|EpisodeWriter|PolicyEngine/.test(content);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function usageHintsSpec(content) {
|
|
44
|
+
const hasSpec = /mova-spec/.test(content);
|
|
45
|
+
const hasResolver = /createRequire|require\.resolve|import\.meta\.resolve/.test(content);
|
|
46
|
+
const hasSchemas = /schemas/.test(content);
|
|
47
|
+
return hasSpec && (hasResolver || hasSchemas);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
const repoRoot = process.cwd();
|
|
52
|
+
const pkg = JSON.parse(await fs.readFile(path.join(repoRoot, "package.json"), "utf8"));
|
|
53
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
54
|
+
|
|
55
|
+
const srcRoot = path.join(repoRoot, "src");
|
|
56
|
+
const files = (await listFilesRec(srcRoot)).filter((p) => p.endsWith(".ts"));
|
|
57
|
+
files.sort();
|
|
58
|
+
|
|
59
|
+
const fileContents = [];
|
|
60
|
+
const contentByFile = new Map();
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const content = await fs.readFile(file, "utf8");
|
|
63
|
+
const rel = path.relative(repoRoot, file).replace(/\\/g, "/");
|
|
64
|
+
contentByFile.set(rel, content);
|
|
65
|
+
fileContents.push(`${rel}\n${content}\n`);
|
|
66
|
+
}
|
|
67
|
+
const runId = sha256Strings(fileContents);
|
|
68
|
+
|
|
69
|
+
const foundImports = {};
|
|
70
|
+
const foundUsageHints = {};
|
|
71
|
+
const recommendations = [];
|
|
72
|
+
|
|
73
|
+
for (const dep of TARGET_DEPS) {
|
|
74
|
+
const importFiles = [];
|
|
75
|
+
const usageFiles = [];
|
|
76
|
+
for (const [rel, content] of contentByFile.entries()) {
|
|
77
|
+
if (importMatches(content, dep)) importFiles.push(rel);
|
|
78
|
+
if (dep.endsWith("mova-core-engine") && usageHintsCoreEngine(content)) usageFiles.push(rel);
|
|
79
|
+
if (dep.endsWith("mova-spec") && usageHintsSpec(content)) usageFiles.push(rel);
|
|
80
|
+
}
|
|
81
|
+
importFiles.sort();
|
|
82
|
+
usageFiles.sort();
|
|
83
|
+
foundImports[dep] = importFiles;
|
|
84
|
+
foundUsageHints[dep] = usageFiles;
|
|
85
|
+
|
|
86
|
+
if (deps[dep] && importFiles.length === 0) {
|
|
87
|
+
recommendations.push(`Add import usage for ${dep}.`);
|
|
88
|
+
}
|
|
89
|
+
if (deps[dep] && usageFiles.length === 0) {
|
|
90
|
+
recommendations.push(`Add real usage hints for ${dep}.`);
|
|
91
|
+
}
|
|
92
|
+
if (!deps[dep]) {
|
|
93
|
+
recommendations.push(`Add ${dep} to dependencies.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const ok =
|
|
98
|
+
TARGET_DEPS.every((dep) => deps[dep]) &&
|
|
99
|
+
TARGET_DEPS.every((dep) => foundImports[dep].length > 0) &&
|
|
100
|
+
TARGET_DEPS.every((dep) => foundUsageHints[dep].length > 0);
|
|
101
|
+
|
|
102
|
+
const report = {
|
|
103
|
+
ok,
|
|
104
|
+
found_imports: foundImports,
|
|
105
|
+
found_usage_hints: foundUsageHints,
|
|
106
|
+
recommendations,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const outDir = path.join(repoRoot, "artifacts", "deps_audit", runId);
|
|
110
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
111
|
+
await fs.writeFile(
|
|
112
|
+
path.join(outDir, "deps_audit_report_v0.json"),
|
|
113
|
+
JSON.stringify(report, null, 2) + "\n",
|
|
114
|
+
"utf8"
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
process.exit(ok ? 0 : 2);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
main().catch((err) => {
|
|
121
|
+
console.error(err);
|
|
122
|
+
process.exit(2);
|
|
123
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
|
|
5
|
+
const CHECK_MODE = process.argv.includes("--check");
|
|
6
|
+
const REPO_ROOT = process.cwd();
|
|
7
|
+
const BINDINGS_PATH = path.join(REPO_ROOT, "src", "mova_spec_bindings_v0.ts");
|
|
8
|
+
const OUT_MD = path.join(REPO_ROOT, "docs", "MOVA_SPEC_BINDINGS.md");
|
|
9
|
+
const OUT_JSON = path.join(REPO_ROOT, "docs", "MOVA_SPEC_BINDINGS.json");
|
|
10
|
+
|
|
11
|
+
function parseBindings(source) {
|
|
12
|
+
const ids = {};
|
|
13
|
+
const re = /(\w+_id)\s*:\s*["']([^"']+)["']/g;
|
|
14
|
+
let match;
|
|
15
|
+
while ((match = re.exec(source))) {
|
|
16
|
+
ids[match[1]] = match[2];
|
|
17
|
+
}
|
|
18
|
+
return ids;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function listSchemas(specRoot) {
|
|
22
|
+
const schemasRoot = path.join(specRoot, "schemas");
|
|
23
|
+
const out = [];
|
|
24
|
+
const stack = [schemasRoot];
|
|
25
|
+
while (stack.length) {
|
|
26
|
+
const current = stack.pop();
|
|
27
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
28
|
+
for (const e of entries) {
|
|
29
|
+
const abs = path.join(current, e.name);
|
|
30
|
+
if (e.isDirectory()) stack.push(abs);
|
|
31
|
+
else if (e.isFile() && e.name.endsWith(".json")) out.push(abs);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const require = createRequire(import.meta.url);
|
|
39
|
+
const specPkgPath = require.resolve("@leryk1981/mova-spec/package.json");
|
|
40
|
+
const specRoot = path.dirname(specPkgPath);
|
|
41
|
+
const specPkg = JSON.parse(await fs.readFile(specPkgPath, "utf8"));
|
|
42
|
+
const specVersion = specPkg.version ?? "unknown";
|
|
43
|
+
|
|
44
|
+
const bindingsSource = await fs.readFile(BINDINGS_PATH, "utf8");
|
|
45
|
+
const bindings = parseBindings(bindingsSource);
|
|
46
|
+
const categories = [
|
|
47
|
+
["instruction_profile", "instruction_profile_id"],
|
|
48
|
+
["mcp_servers", "mcp_servers_id"],
|
|
49
|
+
["core_schema", "core_schema_id"],
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const selected = {};
|
|
53
|
+
for (const [label, key] of categories) {
|
|
54
|
+
const id = bindings[key];
|
|
55
|
+
if (!id) {
|
|
56
|
+
console.error(`Missing binding key: ${key}`);
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
selected[label] = id;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const schemaFiles = await listSchemas(specRoot);
|
|
63
|
+
const idToPath = new Map();
|
|
64
|
+
for (const file of schemaFiles) {
|
|
65
|
+
const raw = await fs.readFile(file, "utf8");
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
if (parsed?.$id) {
|
|
69
|
+
const rel = path.relative(specRoot, file).replace(/\\/g, "/");
|
|
70
|
+
idToPath.set(parsed.$id, rel);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// ignore invalid schema files
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rows = categories.map(([label]) => {
|
|
78
|
+
const id = selected[label];
|
|
79
|
+
const filePath = idToPath.get(id) ?? "UNRESOLVED";
|
|
80
|
+
return { category: label, schema_id: id, file_path: filePath };
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const jsonOut = {
|
|
84
|
+
spec_version: specVersion,
|
|
85
|
+
bindings: rows,
|
|
86
|
+
selection_rule: "Bound to MOVA_SPEC_BINDINGS_V0 used by run_import.ts",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const mdLines = [
|
|
90
|
+
"# MOVA Spec Bindings v0",
|
|
91
|
+
"",
|
|
92
|
+
`Spec version: ${specVersion}`,
|
|
93
|
+
"",
|
|
94
|
+
"| Category | Chosen schema $id | File path |",
|
|
95
|
+
"| --- | --- | --- |",
|
|
96
|
+
...rows.map((r) => `| ${r.category} | ${r.schema_id} | ${r.file_path} |`),
|
|
97
|
+
"",
|
|
98
|
+
"Selection rule: Bound to `MOVA_SPEC_BINDINGS_V0` used by `src/run_import.ts`.",
|
|
99
|
+
"",
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
if (CHECK_MODE) {
|
|
103
|
+
const currentMd = await fs.readFile(OUT_MD, "utf8");
|
|
104
|
+
const currentJson = await fs.readFile(OUT_JSON, "utf8");
|
|
105
|
+
const nextMd = mdLines.join("\n");
|
|
106
|
+
const nextJson = JSON.stringify(jsonOut, null, 2) + "\n";
|
|
107
|
+
if (currentMd !== nextMd || currentJson !== nextJson) {
|
|
108
|
+
console.error("Bindings docs are out of date.");
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await fs.mkdir(path.dirname(OUT_MD), { recursive: true });
|
|
115
|
+
await fs.writeFile(OUT_MD, mdLines.join("\n"), "utf8");
|
|
116
|
+
await fs.writeFile(OUT_JSON, JSON.stringify(jsonOut, null, 2) + "\n", "utf8");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
main().catch((err) => {
|
|
120
|
+
console.error(err);
|
|
121
|
+
process.exit(2);
|
|
122
|
+
});
|