@bonesofspring/ai-rules 0.2.22 → 0.2.23
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/CHANGELOG.md +8 -0
- package/README.md +6 -2
- package/bin/cli.js +32 -8
- package/bin/mavis-install.js +441 -0
- package/package.json +9 -2
- package/presets/_shared/core/meta/preset-pr-checklist.md +1 -0
- package/presets/_shared/core/meta/preset-twin-sync.md +16 -0
- package/presets/claude/android-kotlin/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/claude/go/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/claude/ios-swift/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/claude/java/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/claude/mcp-ts/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/claude/next/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/claude/nuxt/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/claude/php-laravel/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/claude/svelte/rules/tooling-and-review/preset-pr-checklist.md +1 -0
- package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +16 -0
- package/presets/cursor/android-kotlin/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +16 -0
- package/presets/cursor/go/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/go/rules/preset-twin-sync.mdc +16 -0
- package/presets/cursor/ios-swift/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +16 -0
- package/presets/cursor/java/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/java/rules/preset-twin-sync.mdc +16 -0
- package/presets/cursor/mcp-ts/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +16 -0
- package/presets/cursor/next/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/next/rules/preset-twin-sync.mdc +16 -0
- package/presets/cursor/nuxt/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +16 -0
- package/presets/cursor/php-hexagonal/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +16 -0
- package/presets/cursor/php-laravel/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +16 -0
- package/presets/cursor/svelte/rules/preset-pr-checklist.mdc +1 -0
- package/presets/cursor/svelte/rules/preset-twin-sync.mdc +16 -0
- package/presets/mavis/README.md +60 -0
- package/presets/mavis/next/MAVIS.md +67 -0
- package/presets/mavis/next/MCP.md +36 -0
- package/presets/mavis/next/README.md +15 -0
- package/presets/mavis/next/REPO_AGENTS.md +94 -0
- package/presets/mavis/next/adapter-map.json +959 -0
- package/presets/mavis/next/agents/README.md +206 -0
- package/presets/mavis/next/agents/accessibility-reviewer.md +61 -0
- package/presets/mavis/next/agents/api-contract-reviewer.md +57 -0
- package/presets/mavis/next/agents/build-verifier.md +110 -0
- package/presets/mavis/next/agents/ci-investigator.md +63 -0
- package/presets/mavis/next/agents/code-reviewer.md +61 -0
- package/presets/mavis/next/agents/codebase-analyzer.md +21 -0
- package/presets/mavis/next/agents/debugger.md +80 -0
- package/presets/mavis/next/agents/feature-developer.md +101 -0
- package/presets/mavis/next/agents/migration-specialist.md +67 -0
- package/presets/mavis/next/agents/mockup-reviewer.md +53 -0
- package/presets/mavis/next/agents/openspec-author.md +157 -0
- package/presets/mavis/next/agents/performance-auditor.md +68 -0
- package/presets/mavis/next/agents/playwright-test-generator.md +28 -0
- package/presets/mavis/next/agents/playwright-test-healer.md +28 -0
- package/presets/mavis/next/agents/playwright-test-planner.md +21 -0
- package/presets/mavis/next/agents/qa-tester.md +52 -0
- package/presets/mavis/next/agents/security-reviewer.md +54 -0
- package/presets/mavis/next/agents/solution-architect.md +68 -0
- package/presets/mavis/next/agents/task-analyst.md +166 -0
- package/presets/mavis/next/agents/task-router.md +178 -0
- package/presets/mavis/next/agents/tech-writer.md +57 -0
- package/presets/mavis/next/agents/unit-test-generator.md +31 -0
- package/presets/mavis/next/agents/unit-test-healer.md +32 -0
- package/presets/mavis/next/agents/unit-test-planner.md +54 -0
- package/presets/mavis/next/commands/README.md +49 -0
- package/presets/mavis/next/commands/spec-start.md +24 -0
- package/presets/mavis/next/commands/task-continue.md +50 -0
- package/presets/mavis/next/commands/task.md +50 -0
- package/presets/mavis/next/commands/technical-retro.md +80 -0
- package/presets/mavis/next/hooks/README.md +22 -0
- package/presets/mavis/next/hooks/chain-team-phases.sh +845 -0
- package/presets/mavis/next/mcp.json +19 -0
- package/presets/mavis/next/rules/README.md +16 -0
- package/presets/mavis/next/rules/agent-team-intake.md +11 -0
- package/presets/mavis/next/rules/agent-team-orchestrator.md +210 -0
- package/presets/mavis/next/rules/ai-local-overlay.md +37 -0
- package/presets/mavis/next/rules/anti-sycophancy-discipline.md +23 -0
- package/presets/mavis/next/rules/api-services.md +72 -0
- package/presets/mavis/next/rules/architecture-boundaries-ui.md +9 -0
- package/presets/mavis/next/rules/architecture-boundaries.md +86 -0
- package/presets/mavis/next/rules/arrow-functions.md +25 -0
- package/presets/mavis/next/rules/code-quality-and-refactoring.md +40 -0
- package/presets/mavis/next/rules/code-review-mr.md +40 -0
- package/presets/mavis/next/rules/css-property-order-stylelint.md +5 -0
- package/presets/mavis/next/rules/design-guidance.md +99 -0
- package/presets/mavis/next/rules/feature-delivery-workflow.md +49 -0
- package/presets/mavis/next/rules/http-client.md +55 -0
- package/presets/mavis/next/rules/layer-barrel-exports.md +68 -0
- package/presets/mavis/next/rules/mcp-usage.md +86 -0
- package/presets/mavis/next/rules/mockup-review.md +107 -0
- package/presets/mavis/next/rules/navigation-router-stack.md +56 -0
- package/presets/mavis/next/rules/navigation-router-ui.md +10 -0
- package/presets/mavis/next/rules/next-app-core.md +29 -0
- package/presets/mavis/next/rules/next-app-router.md +44 -0
- package/presets/mavis/next/rules/no-props-spread.md +56 -0
- package/presets/mavis/next/rules/no-type-assertion-as-import-export.md +51 -0
- package/presets/mavis/next/rules/package-manager.md +11 -0
- package/presets/mavis/next/rules/playwright-agents.md +67 -0
- package/presets/mavis/next/rules/post-change-lint.md +35 -0
- package/presets/mavis/next/rules/preset-layering.md +27 -0
- package/presets/mavis/next/rules/preset-no-cross-stack-leakage.md +90 -0
- package/presets/mavis/next/rules/preset-pr-checklist.md +35 -0
- package/presets/mavis/next/rules/preset-token-budget.md +36 -0
- package/presets/mavis/next/rules/preset-twin-sync.md +65 -0
- package/presets/mavis/next/rules/product-specs-authoring.md +112 -0
- package/presets/mavis/next/rules/product-specs.md +56 -0
- package/presets/mavis/next/rules/public-imports.md +50 -0
- package/presets/mavis/next/rules/react-a11y-coding.md +31 -0
- package/presets/mavis/next/rules/react-next-anti-hallucination.md +60 -0
- package/presets/mavis/next/rules/react-ui.md +105 -0
- package/presets/mavis/next/rules/reference-features.md +45 -0
- package/presets/mavis/next/rules/security-next.md +45 -0
- package/presets/mavis/next/rules/store-rtk.md +78 -0
- package/presets/mavis/next/rules/technical-retro.md +11 -0
- package/presets/mavis/next/rules/tests-e2e-structure.md +72 -0
- package/presets/mavis/next/rules/tests-unit.md +72 -0
- package/presets/mavis/next/rules/types-jsdoc.md +43 -0
- package/presets/mavis/next/rules/ux-design.md +99 -0
- package/presets/mavis/next/skills/README.md +21 -0
- package/presets/mavis/next/skills/ci-investigation/SKILL.md +39 -0
- package/presets/mavis/next/skills/code-review/SKILL.md +39 -0
- package/presets/mavis/next/skills/debug-investigation/SKILL.md +26 -0
- package/presets/mavis/next/skills/feature-delivery/SKILL.md +34 -0
- package/presets/mavis/next/skills/mockup-review/SKILL.md +24 -0
- package/presets/mavis/next/skills/playwright-e2e/SKILL.md +28 -0
- package/presets/mavis/next/skills/react-performance/SKILL.md +39 -0
- package/presets/mavis/next/skills/spec-authoring/SKILL.md +21 -0
- package/presets/mavis/next/skills/spec-discovery/SKILL.md +53 -0
- package/presets/mavis/next/skills/technical-retro/SKILL.md +49 -0
- package/presets/mavis/next/skills/unit-testing/SKILL.md +29 -0
- package/presets/mavis/next/skills/ux-design/SKILL.md +31 -0
- package/presets/mavis/next/skills/write-adr/SKILL.md +36 -0
- package/presets/mavis/next/team/README.md +148 -0
- package/presets/mavis/next/team/conventions.md +21 -0
- package/presets/mavis/next/team/fixtures/artifact-manifest.example.json +79 -0
- package/presets/mavis/next/team/fixtures/bugfix-standard.json +25 -0
- package/presets/mavis/next/team/fixtures/feature-full.json +29 -0
- package/presets/mavis/next/team/fixtures/feature-light.json +27 -0
- package/presets/mavis/next/team/fixtures/feature-with-figma-url.json +17 -0
- package/presets/mavis/next/team/fixtures/feature-with-ux-no-figma.json +17 -0
- package/presets/mavis/next/team/fixtures/memlog.example.md +9 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/human-gate/metrics.json +69 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/missing-ledger/status.json +5 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/parallel-receipts/metrics.json +79 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/serial-retry/metrics.json +124 -0
- package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/validation-failed/metrics.json +124 -0
- package/presets/mavis/next/team/fixtures/metrics.example.json +54 -0
- package/presets/mavis/next/team/fixtures/mockup-review-fail-embedded-status.json +8 -0
- package/presets/mavis/next/team/fixtures/mockup-review-standalone.json +14 -0
- package/presets/mavis/next/team/fixtures/ux-design-standalone.json +15 -0
- package/presets/mavis/next/team/tasks/.gitkeep +0 -0
- package/presets/mavis/platform-contract.json +131 -0
- package/scripts/capture-cli-golden.mjs +44 -0
- package/scripts/check-chain-hook-coverage.sh +1 -0
- package/scripts/check-mavis-adapter-map.mjs +294 -0
- package/scripts/check-mavis-local-overlay.mjs +56 -0
- package/scripts/check-mavis-platform-contract.mjs +388 -0
- package/scripts/check-mavis-token-budget.mjs +48 -0
- package/scripts/check-preset-leakage.sh +35 -3
- package/scripts/check-preset-structure.sh +63 -0
- package/scripts/check-shared-core-drift.sh +18 -0
- package/scripts/check-task-router-intents.sh +15 -2
- package/scripts/fixtures/cli-golden/claude/next.json +130 -0
- package/scripts/fixtures/cli-golden/cursor/next.json +132 -0
- package/scripts/fixtures/cli-golden/mavis/next.json +125 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/AGENTS.md +1 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/extra.mdc +1 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/sample.mdc +2 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/MAVIS.md +1 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/adapter-map.json +26 -0
- package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/rules/sample.md +2 -0
- package/scripts/fixtures/mavis-adapter-map/valid/cursor/AGENTS.md +1 -0
- package/scripts/fixtures/mavis-adapter-map/valid/cursor/rules/sample.mdc +2 -0
- package/scripts/fixtures/mavis-adapter-map/valid/mavis/MAVIS.md +1 -0
- package/scripts/fixtures/mavis-adapter-map/valid/mavis/adapter-map.json +26 -0
- package/scripts/fixtures/mavis-adapter-map/valid/mavis/rules/sample.md +2 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-minimax-destination.json +131 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-missing-capability.json +126 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-released-unknown-stack.json +133 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-unknown-native-path.json +131 -0
- package/scripts/fixtures/mavis-platform-contract/invalid-version-pin.json +132 -0
- package/scripts/fixtures/mavis-platform-contract/valid.json +131 -0
- package/scripts/fixtures/mavis-probe/AGENTS.md +6 -0
- package/scripts/fixtures/mavis-probe/agents/probe-agent.md +6 -0
- package/scripts/fixtures/mavis-probe/fake-mcode.mjs +98 -0
- package/scripts/fixtures/mavis-probe/output.schema.json +28 -0
- package/scripts/fixtures/mavis-probe/rules/probe.md +5 -0
- package/scripts/fixtures/mavis-probe/skills/probe/SKILL.md +11 -0
- package/scripts/generate-mavis-next.mjs +704 -0
- package/scripts/lib/cli-golden.mjs +50 -0
- package/scripts/lib/discover-hooks.mjs +72 -0
- package/scripts/probe-mavis-capabilities.mjs +480 -0
- package/scripts/test-agent-task-metrics-hooks.mjs +22 -16
- package/scripts/test-chain-team-phases-coverage.mjs +10 -7
- package/scripts/test-cli-init-clean.mjs +256 -0
- package/scripts/test-cli-mcp-merge.mjs +23 -0
- package/scripts/test-mavis-context-budget.mjs +55 -0
- package/scripts/test-mavis-orchestration.mjs +649 -0
- package/scripts/test-probe-mavis-capabilities.mjs +205 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate presets/mavis/platform-contract.json against architecture §3.4.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/check-mavis-platform-contract.mjs
|
|
7
|
+
* node scripts/check-mavis-platform-contract.mjs --contract <path>
|
|
8
|
+
* node scripts/check-mavis-platform-contract.mjs --self-test
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const packageRoot = path.join(scriptDirectory, '..');
|
|
16
|
+
const defaultContractPath = path.join(
|
|
17
|
+
packageRoot,
|
|
18
|
+
'presets',
|
|
19
|
+
'mavis',
|
|
20
|
+
'platform-contract.json',
|
|
21
|
+
);
|
|
22
|
+
const fixtureDirectory = path.join(
|
|
23
|
+
scriptDirectory,
|
|
24
|
+
'fixtures',
|
|
25
|
+
'mavis-platform-contract',
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const ALLOWED_STATUS = new Set([
|
|
29
|
+
'confirmed',
|
|
30
|
+
'partial',
|
|
31
|
+
'unknown',
|
|
32
|
+
'no-equivalent',
|
|
33
|
+
]);
|
|
34
|
+
const KNOWN_STACKS = new Set([
|
|
35
|
+
'next',
|
|
36
|
+
'nuxt',
|
|
37
|
+
'svelte',
|
|
38
|
+
'ios-swift',
|
|
39
|
+
'android-kotlin',
|
|
40
|
+
'go',
|
|
41
|
+
'java',
|
|
42
|
+
'mcp-ts',
|
|
43
|
+
'php-hexagonal',
|
|
44
|
+
'php-laravel',
|
|
45
|
+
]);
|
|
46
|
+
const REQUIRED_CAPABILITIES = [
|
|
47
|
+
'cliVersion',
|
|
48
|
+
'rootAgents',
|
|
49
|
+
'explicitAgentFile',
|
|
50
|
+
'structuredOutput',
|
|
51
|
+
'referencedRule',
|
|
52
|
+
'referencedSkill',
|
|
53
|
+
'freshInit',
|
|
54
|
+
'nativeProjectRules',
|
|
55
|
+
'nativeWorkspaceSkills',
|
|
56
|
+
'nativeWorkspaceAgents',
|
|
57
|
+
'projectMcp',
|
|
58
|
+
'projectHooks',
|
|
59
|
+
'slashCommands',
|
|
60
|
+
'pluginSkillMcp',
|
|
61
|
+
'nativeTeam',
|
|
62
|
+
];
|
|
63
|
+
const REQUIRED_ENTRY_PAIRS = [
|
|
64
|
+
{ source: 'REPO_AGENTS.md', destination: 'AGENTS.md' },
|
|
65
|
+
{ source: 'MAVIS.md', destination: '.mavis/AGENTS.md' },
|
|
66
|
+
];
|
|
67
|
+
const REQUIRED_DIRECTORY_SOURCES = [
|
|
68
|
+
'rules',
|
|
69
|
+
'agents',
|
|
70
|
+
'skills',
|
|
71
|
+
'commands',
|
|
72
|
+
'hooks',
|
|
73
|
+
'team',
|
|
74
|
+
];
|
|
75
|
+
const FORBIDDEN_PATH_RE =
|
|
76
|
+
/(?:^|[\\/])\.minimax(?:[\\/]|$)|(?:^|[\\/])\.minimax-code(?:[\\/]|$)|~\/\.minimax|\$HOME\/\.minimax/i;
|
|
77
|
+
const VERSION_PIN_KEYS = new Set([
|
|
78
|
+
'requiredCliVersion',
|
|
79
|
+
'requiredDesktopVersion',
|
|
80
|
+
'minCliVersion',
|
|
81
|
+
'pinnedVersion',
|
|
82
|
+
'requiredVersion',
|
|
83
|
+
]);
|
|
84
|
+
const EVIDENCE_MARKER = 'MAVIS-COMPATIBILITY.md';
|
|
85
|
+
|
|
86
|
+
export function validateContract(contract, options = {}) {
|
|
87
|
+
const failures = [];
|
|
88
|
+
const fail = (message) => {
|
|
89
|
+
failures.push(message);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (contract === null || typeof contract !== 'object' || Array.isArray(contract)) {
|
|
93
|
+
return ['contract must be a JSON object'];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (contract.schemaVersion !== 1) {
|
|
97
|
+
fail('schemaVersion must be 1');
|
|
98
|
+
}
|
|
99
|
+
if (contract.id !== 'mavis') {
|
|
100
|
+
fail('id must be "mavis"');
|
|
101
|
+
}
|
|
102
|
+
if (contract.runtimeExecutable !== 'mcode') {
|
|
103
|
+
fail('runtimeExecutable must be "mcode"');
|
|
104
|
+
}
|
|
105
|
+
if (contract.sourceRoot !== 'presets/mavis') {
|
|
106
|
+
fail('sourceRoot must be "presets/mavis"');
|
|
107
|
+
}
|
|
108
|
+
if (contract.managedRoot !== '.mavis') {
|
|
109
|
+
fail('managedRoot must be ".mavis"');
|
|
110
|
+
}
|
|
111
|
+
if (contract.cleanStrategy !== 'manifest-owned') {
|
|
112
|
+
fail('cleanStrategy must be "manifest-owned"');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const key of Object.keys(contract)) {
|
|
116
|
+
if (VERSION_PIN_KEYS.has(key)) {
|
|
117
|
+
fail(`forbidden version-pin key "${key}"`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!Array.isArray(contract.releasedStacks)) {
|
|
122
|
+
fail('releasedStacks must be an array');
|
|
123
|
+
} else {
|
|
124
|
+
for (const stack of contract.releasedStacks) {
|
|
125
|
+
if (typeof stack !== 'string' || !KNOWN_STACKS.has(stack)) {
|
|
126
|
+
fail(`releasedStacks contains unknown stack "${stack}"`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (options.requireReleasedStackDir) {
|
|
130
|
+
const stackDir = path.join(packageRoot, 'presets', 'mavis', stack);
|
|
131
|
+
if (!fs.existsSync(stackDir)) {
|
|
132
|
+
fail(`released stack "${stack}" has no source directory ${stackDir}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!Array.isArray(contract.entryFiles)) {
|
|
139
|
+
fail('entryFiles must be an array');
|
|
140
|
+
} else {
|
|
141
|
+
const pairs = new Set(
|
|
142
|
+
contract.entryFiles.map((entry) => `${entry?.source}→${entry?.destination}`),
|
|
143
|
+
);
|
|
144
|
+
for (const required of REQUIRED_ENTRY_PAIRS) {
|
|
145
|
+
if (!pairs.has(`${required.source}→${required.destination}`)) {
|
|
146
|
+
fail(
|
|
147
|
+
`entryFiles must include ${required.source} → ${required.destination}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const [index, entry] of contract.entryFiles.entries()) {
|
|
152
|
+
if (!isSourceDestination(entry)) {
|
|
153
|
+
fail(`entryFiles[${index}] must have string source and destination`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
failIfForbiddenPath(fail, `entryFiles[${index}].destination`, entry.destination);
|
|
157
|
+
if (
|
|
158
|
+
entry.destination !== 'AGENTS.md' &&
|
|
159
|
+
!entry.destination.startsWith('.mavis/')
|
|
160
|
+
) {
|
|
161
|
+
fail(
|
|
162
|
+
`entryFiles[${index}].destination must be AGENTS.md or under .mavis/`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!Array.isArray(contract.directoryMappings)) {
|
|
169
|
+
fail('directoryMappings must be an array');
|
|
170
|
+
} else {
|
|
171
|
+
const sources = new Set(
|
|
172
|
+
contract.directoryMappings
|
|
173
|
+
.filter((entry) => entry && typeof entry.source === 'string')
|
|
174
|
+
.map((entry) => entry.source),
|
|
175
|
+
);
|
|
176
|
+
for (const source of REQUIRED_DIRECTORY_SOURCES) {
|
|
177
|
+
if (!sources.has(source)) {
|
|
178
|
+
fail(`directoryMappings must include source "${source}"`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const [index, entry] of contract.directoryMappings.entries()) {
|
|
182
|
+
if (!isSourceDestination(entry)) {
|
|
183
|
+
fail(
|
|
184
|
+
`directoryMappings[${index}] must have string source and destination`,
|
|
185
|
+
);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
failIfForbiddenPath(
|
|
189
|
+
fail,
|
|
190
|
+
`directoryMappings[${index}].destination`,
|
|
191
|
+
entry.destination,
|
|
192
|
+
);
|
|
193
|
+
if (!entry.destination.startsWith('.mavis/')) {
|
|
194
|
+
fail(
|
|
195
|
+
`directoryMappings[${index}].destination must start with .mavis/`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const mcp = contract.mcp;
|
|
202
|
+
if (mcp === null || typeof mcp !== 'object' || Array.isArray(mcp)) {
|
|
203
|
+
fail('mcp must be an object');
|
|
204
|
+
} else {
|
|
205
|
+
if (mcp.source !== 'mcp.json') {
|
|
206
|
+
fail('mcp.source must be "mcp.json"');
|
|
207
|
+
}
|
|
208
|
+
if (mcp.fallbackDestination !== '.mavis/mcp.json') {
|
|
209
|
+
fail('mcp.fallbackDestination must be ".mavis/mcp.json"');
|
|
210
|
+
}
|
|
211
|
+
if (mcp.mode !== 'fallback-manual-import') {
|
|
212
|
+
fail('mcp.mode must be "fallback-manual-import"');
|
|
213
|
+
}
|
|
214
|
+
failIfForbiddenPath(fail, 'mcp.fallbackDestination', mcp.fallbackDestination);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const docsSeed = contract.docsSeed;
|
|
218
|
+
if (docsSeed === null || typeof docsSeed !== 'object' || Array.isArray(docsSeed)) {
|
|
219
|
+
fail('docsSeed must be an object');
|
|
220
|
+
} else {
|
|
221
|
+
if (docsSeed.source !== 'presets/_shared/assets/docs-specs') {
|
|
222
|
+
fail('docsSeed.source must be "presets/_shared/assets/docs-specs"');
|
|
223
|
+
}
|
|
224
|
+
if (docsSeed.behavior !== 'absent-only') {
|
|
225
|
+
fail('docsSeed.behavior must be "absent-only"');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const capabilities = contract.nativeCapabilities;
|
|
230
|
+
if (
|
|
231
|
+
capabilities === null ||
|
|
232
|
+
typeof capabilities !== 'object' ||
|
|
233
|
+
Array.isArray(capabilities)
|
|
234
|
+
) {
|
|
235
|
+
fail('nativeCapabilities must be an object');
|
|
236
|
+
} else {
|
|
237
|
+
for (const key of REQUIRED_CAPABILITIES) {
|
|
238
|
+
if (!Object.hasOwn(capabilities, key)) {
|
|
239
|
+
fail(`nativeCapabilities missing required key "${key}"`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const [key, capability] of Object.entries(capabilities)) {
|
|
243
|
+
if (capability === null || typeof capability !== 'object' || Array.isArray(capability)) {
|
|
244
|
+
fail(`nativeCapabilities.${key} must be an object`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (!ALLOWED_STATUS.has(capability.status)) {
|
|
248
|
+
fail(
|
|
249
|
+
`nativeCapabilities.${key}.status must be confirmed|partial|unknown|no-equivalent`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
if (
|
|
253
|
+
typeof capability.evidence !== 'string' ||
|
|
254
|
+
!capability.evidence.includes(EVIDENCE_MARKER)
|
|
255
|
+
) {
|
|
256
|
+
fail(
|
|
257
|
+
`nativeCapabilities.${key}.evidence must reference ${EVIDENCE_MARKER}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const nativePath = capability.nativePath ?? null;
|
|
261
|
+
if (nativePath !== null && typeof nativePath !== 'string') {
|
|
262
|
+
fail(`nativeCapabilities.${key}.nativePath must be a string or null`);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
(capability.status === 'unknown' || capability.status === 'no-equivalent') &&
|
|
267
|
+
nativePath
|
|
268
|
+
) {
|
|
269
|
+
fail(
|
|
270
|
+
`nativeCapabilities.${key} is ${capability.status} and cannot declare nativePath`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (nativePath) {
|
|
274
|
+
failIfForbiddenPath(fail, `nativeCapabilities.${key}.nativePath`, nativePath);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return failures;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function isSourceDestination(entry) {
|
|
283
|
+
return (
|
|
284
|
+
entry &&
|
|
285
|
+
typeof entry === 'object' &&
|
|
286
|
+
typeof entry.source === 'string' &&
|
|
287
|
+
entry.source.length > 0 &&
|
|
288
|
+
typeof entry.destination === 'string' &&
|
|
289
|
+
entry.destination.length > 0
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function failIfForbiddenPath(fail, label, value) {
|
|
294
|
+
if (typeof value === 'string' && FORBIDDEN_PATH_RE.test(value)) {
|
|
295
|
+
fail(`${label} claims a MiniMax-native or home path: ${value}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function readJson(filePath) {
|
|
300
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function printFailures(label, failures) {
|
|
304
|
+
for (const failure of failures) {
|
|
305
|
+
console.error(`FAIL ${label}: ${failure}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function runSelfTest() {
|
|
310
|
+
const cases = [
|
|
311
|
+
{ name: 'valid.json', expectFail: false },
|
|
312
|
+
{ name: 'invalid-unknown-native-path.json', expectFail: true },
|
|
313
|
+
{ name: 'invalid-minimax-destination.json', expectFail: true },
|
|
314
|
+
{ name: 'invalid-missing-capability.json', expectFail: true },
|
|
315
|
+
{ name: 'invalid-released-unknown-stack.json', expectFail: true },
|
|
316
|
+
{ name: 'invalid-version-pin.json', expectFail: true },
|
|
317
|
+
];
|
|
318
|
+
let failed = 0;
|
|
319
|
+
for (const testCase of cases) {
|
|
320
|
+
const filePath = path.join(fixtureDirectory, testCase.name);
|
|
321
|
+
const failures = validateContract(readJson(filePath));
|
|
322
|
+
const didFail = failures.length > 0;
|
|
323
|
+
if (didFail !== testCase.expectFail) {
|
|
324
|
+
failed += 1;
|
|
325
|
+
console.error(
|
|
326
|
+
`FAIL self-test ${testCase.name}: expected ${
|
|
327
|
+
testCase.expectFail ? 'invalid' : 'valid'
|
|
328
|
+
}, got ${didFail ? 'invalid' : 'valid'}`,
|
|
329
|
+
);
|
|
330
|
+
printFailures(testCase.name, failures);
|
|
331
|
+
} else {
|
|
332
|
+
console.log(
|
|
333
|
+
`ok self-test ${testCase.name} (${didFail ? 'invalid' : 'valid'})`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return failed;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function parseArgs(argv) {
|
|
341
|
+
const options = { contract: defaultContractPath, selfTest: false };
|
|
342
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
343
|
+
const arg = argv[index];
|
|
344
|
+
if (arg === '--self-test') {
|
|
345
|
+
options.selfTest = true;
|
|
346
|
+
} else if (arg === '--contract') {
|
|
347
|
+
options.contract = argv[index + 1];
|
|
348
|
+
index += 1;
|
|
349
|
+
} else {
|
|
350
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return options;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function main() {
|
|
357
|
+
const options = parseArgs(process.argv.slice(2));
|
|
358
|
+
let exitCode = 0;
|
|
359
|
+
|
|
360
|
+
if (options.selfTest) {
|
|
361
|
+
exitCode = runSelfTest() === 0 ? 0 : 1;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!options.selfTest || exitCode === 0) {
|
|
365
|
+
const liveFailures = validateContract(readJson(options.contract), {
|
|
366
|
+
requireReleasedStackDir: options.contract === defaultContractPath,
|
|
367
|
+
});
|
|
368
|
+
if (liveFailures.length > 0) {
|
|
369
|
+
printFailures(path.relative(packageRoot, options.contract), liveFailures);
|
|
370
|
+
exitCode = 1;
|
|
371
|
+
} else if (!options.selfTest) {
|
|
372
|
+
console.log(`ok ${path.relative(packageRoot, options.contract)}`);
|
|
373
|
+
} else {
|
|
374
|
+
console.log('ok live platform-contract.json');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
process.exit(exitCode);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
382
|
+
try {
|
|
383
|
+
main();
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error(`FAIL: ${error.message}`);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Isolated Mavis token-budget analog. Does not change Cursor/Claude 3/4 caps.
|
|
4
|
+
* Root-entry rules in adapter-map.json must stay ≤ 4.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { loadReleasedMavisStacks } from './lib/discover-hooks.mjs';
|
|
10
|
+
|
|
11
|
+
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const presetsRoot = path.join(scriptDirectory, '..', 'presets');
|
|
13
|
+
const ROOT_ENTRY_CAP = 4;
|
|
14
|
+
|
|
15
|
+
const stacks = loadReleasedMavisStacks(presetsRoot);
|
|
16
|
+
if (stacks.length === 0) {
|
|
17
|
+
console.log('ok mavis token-budget: no released stacks');
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let failed = 0;
|
|
22
|
+
for (const stack of stacks) {
|
|
23
|
+
const mapPath = path.join(presetsRoot, 'mavis', stack, 'adapter-map.json');
|
|
24
|
+
if (!fs.existsSync(mapPath)) {
|
|
25
|
+
console.error(`FAIL mavis/${stack}: missing adapter-map.json`);
|
|
26
|
+
failed += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const map = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
|
|
30
|
+
const rootRules = (map.entries || []).filter(
|
|
31
|
+
(entry) =>
|
|
32
|
+
entry.category === 'rules' &&
|
|
33
|
+
entry.loadMode === 'root-entry' &&
|
|
34
|
+
entry.target,
|
|
35
|
+
);
|
|
36
|
+
if (rootRules.length > ROOT_ENTRY_CAP) {
|
|
37
|
+
console.error(
|
|
38
|
+
`FAIL mavis/${stack}: ${rootRules.length} root-entry rules (cap ${ROOT_ENTRY_CAP})`,
|
|
39
|
+
);
|
|
40
|
+
failed += 1;
|
|
41
|
+
} else {
|
|
42
|
+
console.log(
|
|
43
|
+
`ok mavis/${stack}: ${rootRules.length} root-entry rules (cap ${ROOT_ENTRY_CAP})`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -18,9 +18,21 @@ import sys
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
20
|
presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
|
|
21
|
-
PLATFORMS = ("cursor", "claude")
|
|
21
|
+
PLATFORMS = ("cursor", "claude", "mavis")
|
|
22
22
|
STACKS = ("next", "nuxt", "svelte", "ios-swift", "android-kotlin", "go", "java", "mcp-ts", "php-hexagonal", "php-laravel")
|
|
23
23
|
|
|
24
|
+
|
|
25
|
+
def released_mavis_stacks() -> list[str]:
|
|
26
|
+
contract = presets / "mavis" / "platform-contract.json"
|
|
27
|
+
if not contract.is_file():
|
|
28
|
+
return []
|
|
29
|
+
try:
|
|
30
|
+
data = json.loads(contract.read_text(encoding="utf-8"))
|
|
31
|
+
return list(data.get("releasedStacks") or [])
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
|
|
24
36
|
NEGATION_RE = re.compile(
|
|
25
37
|
r"(?i)\b("
|
|
26
38
|
r"never|do\s+not|don't|must\s+not|forbidden|ban(?:ned)?|avoid|"
|
|
@@ -325,7 +337,11 @@ for stack in STACKS:
|
|
|
325
337
|
stack_negation = 0
|
|
326
338
|
stack_fixture_skipped = 0
|
|
327
339
|
|
|
328
|
-
|
|
340
|
+
platforms_for_stack = ["cursor", "claude"]
|
|
341
|
+
if stack in released_mavis_stacks():
|
|
342
|
+
platforms_for_stack.append("mavis")
|
|
343
|
+
|
|
344
|
+
for platform in platforms_for_stack:
|
|
329
345
|
for script in iter_hook_scripts(stack, platform):
|
|
330
346
|
text = script.read_text(encoding="utf-8")
|
|
331
347
|
for pat in hard_patterns:
|
|
@@ -412,7 +428,7 @@ for stack in STACKS:
|
|
|
412
428
|
|
|
413
429
|
# --- Chain fork: pairwise among stacks that have chain scripts ---
|
|
414
430
|
print("=== chain fork ===")
|
|
415
|
-
for platform in
|
|
431
|
+
for platform in ("cursor", "claude"):
|
|
416
432
|
chains: dict[str, Path] = {}
|
|
417
433
|
for stack in STACKS:
|
|
418
434
|
p = presets / platform / stack / "hooks" / "chain-team-phases.sh"
|
|
@@ -433,6 +449,22 @@ for platform in PLATFORMS:
|
|
|
433
449
|
else:
|
|
434
450
|
print(f"OK: [{platform}] {a} chain ≠ {b} chain")
|
|
435
451
|
|
|
452
|
+
for stack in released_mavis_stacks():
|
|
453
|
+
mavis_chain = presets / "mavis" / stack / "hooks" / "chain-team-phases.sh"
|
|
454
|
+
if not mavis_chain.is_file():
|
|
455
|
+
print(f"FAIL: [mavis/{stack}] missing chain-team-phases.sh")
|
|
456
|
+
failed += 1
|
|
457
|
+
continue
|
|
458
|
+
for platform in ("cursor", "claude"):
|
|
459
|
+
other = presets / platform / stack / "hooks" / "chain-team-phases.sh"
|
|
460
|
+
if other.is_file() and mavis_chain.read_bytes() == other.read_bytes():
|
|
461
|
+
print(
|
|
462
|
+
f"FAIL: [mavis/{stack}] chain is byte-identical to [{platform}/{stack}]"
|
|
463
|
+
)
|
|
464
|
+
failed += 1
|
|
465
|
+
elif other.is_file():
|
|
466
|
+
print(f"OK: [mavis/{stack}] chain ≠ [{platform}/{stack}] chain")
|
|
467
|
+
|
|
436
468
|
print()
|
|
437
469
|
print(f"WARN count: {warned} (build-verifier packaging refs / chain skips)")
|
|
438
470
|
print(
|
|
@@ -29,6 +29,60 @@ run_gate() {
|
|
|
29
29
|
echo
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
echo ">>> mavis-platform-contract"
|
|
33
|
+
if node "$SCRIPT_DIR/check-mavis-platform-contract.mjs" --self-test; then
|
|
34
|
+
echo "<<< mavis-platform-contract: OK"
|
|
35
|
+
else
|
|
36
|
+
echo "<<< mavis-platform-contract: FAIL"
|
|
37
|
+
status=1
|
|
38
|
+
fi
|
|
39
|
+
echo
|
|
40
|
+
|
|
41
|
+
echo ">>> mavis-adapter-map"
|
|
42
|
+
if node "$SCRIPT_DIR/check-mavis-adapter-map.mjs" --self-test; then
|
|
43
|
+
echo "<<< mavis-adapter-map: OK"
|
|
44
|
+
else
|
|
45
|
+
echo "<<< mavis-adapter-map: FAIL"
|
|
46
|
+
status=1
|
|
47
|
+
fi
|
|
48
|
+
echo
|
|
49
|
+
|
|
50
|
+
echo ">>> mavis-token-budget"
|
|
51
|
+
if node "$SCRIPT_DIR/check-mavis-token-budget.mjs"; then
|
|
52
|
+
echo "<<< mavis-token-budget: OK"
|
|
53
|
+
else
|
|
54
|
+
echo "<<< mavis-token-budget: FAIL"
|
|
55
|
+
status=1
|
|
56
|
+
fi
|
|
57
|
+
echo
|
|
58
|
+
|
|
59
|
+
echo ">>> mavis-local-overlay"
|
|
60
|
+
if node "$SCRIPT_DIR/check-mavis-local-overlay.mjs"; then
|
|
61
|
+
echo "<<< mavis-local-overlay: OK"
|
|
62
|
+
else
|
|
63
|
+
echo "<<< mavis-local-overlay: FAIL"
|
|
64
|
+
status=1
|
|
65
|
+
fi
|
|
66
|
+
echo
|
|
67
|
+
|
|
68
|
+
echo ">>> mavis-context-budget"
|
|
69
|
+
if node "$SCRIPT_DIR/test-mavis-context-budget.mjs"; then
|
|
70
|
+
echo "<<< mavis-context-budget: OK"
|
|
71
|
+
else
|
|
72
|
+
echo "<<< mavis-context-budget: FAIL"
|
|
73
|
+
status=1
|
|
74
|
+
fi
|
|
75
|
+
echo
|
|
76
|
+
|
|
77
|
+
echo ">>> mavis-orchestration"
|
|
78
|
+
if node "$SCRIPT_DIR/test-mavis-orchestration.mjs"; then
|
|
79
|
+
echo "<<< mavis-orchestration: OK"
|
|
80
|
+
else
|
|
81
|
+
echo "<<< mavis-orchestration: FAIL"
|
|
82
|
+
status=1
|
|
83
|
+
fi
|
|
84
|
+
echo
|
|
85
|
+
|
|
32
86
|
run_gate "twin-map" "$SCRIPT_DIR/check-preset-twin-map.sh"
|
|
33
87
|
run_gate "token-budget" "$SCRIPT_DIR/check-preset-token-budget.sh"
|
|
34
88
|
run_gate "leakage" "$SCRIPT_DIR/check-preset-leakage.sh"
|
|
@@ -48,6 +102,15 @@ else
|
|
|
48
102
|
fi
|
|
49
103
|
echo
|
|
50
104
|
|
|
105
|
+
echo ">>> cli-init-clean"
|
|
106
|
+
if node "$SCRIPT_DIR/test-cli-init-clean.mjs"; then
|
|
107
|
+
echo "<<< cli-init-clean: OK"
|
|
108
|
+
else
|
|
109
|
+
echo "<<< cli-init-clean: FAIL"
|
|
110
|
+
status=1
|
|
111
|
+
fi
|
|
112
|
+
echo
|
|
113
|
+
|
|
51
114
|
echo ">>> chain-team-phases coverage"
|
|
52
115
|
if node "$SCRIPT_DIR/test-chain-team-phases-coverage.mjs"; then
|
|
53
116
|
echo "<<< chain-team-phases coverage: OK"
|
|
@@ -80,6 +80,24 @@ def consumers(stem: str) -> list[Path]:
|
|
|
80
80
|
out.append(path)
|
|
81
81
|
except OSError:
|
|
82
82
|
continue
|
|
83
|
+
contract_path = presets / "mavis" / "platform-contract.json"
|
|
84
|
+
if contract_path.is_file():
|
|
85
|
+
try:
|
|
86
|
+
contract = json.loads(contract_path.read_text(encoding="utf-8"))
|
|
87
|
+
for stack in contract.get("releasedStacks") or []:
|
|
88
|
+
mavis_base = presets / "mavis" / stack
|
|
89
|
+
if not mavis_base.is_dir():
|
|
90
|
+
continue
|
|
91
|
+
for path in mavis_base.rglob("*"):
|
|
92
|
+
if path.suffix not in {".md", ".mdc"}:
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
if needle in path.read_text(encoding="utf-8"):
|
|
96
|
+
out.append(path)
|
|
97
|
+
except OSError:
|
|
98
|
+
continue
|
|
99
|
+
except (json.JSONDecodeError, OSError):
|
|
100
|
+
pass
|
|
83
101
|
return out
|
|
84
102
|
|
|
85
103
|
|
|
@@ -30,15 +30,27 @@ export AI_RULES_PRESETS_ROOT="${AI_RULES_PRESETS_ROOT:-$ROOT/packages/ai-rules/p
|
|
|
30
30
|
python3 <<'PY'
|
|
31
31
|
from __future__ import annotations
|
|
32
32
|
|
|
33
|
+
import json
|
|
33
34
|
import os
|
|
34
35
|
import re
|
|
35
36
|
import sys
|
|
36
37
|
from pathlib import Path
|
|
37
38
|
|
|
38
39
|
presets = Path(os.environ["AI_RULES_PRESETS_ROOT"])
|
|
39
|
-
PLATFORMS = ("cursor", "claude")
|
|
40
|
+
PLATFORMS = ("cursor", "claude", "mavis")
|
|
40
41
|
STACKS = ("next", "nuxt", "svelte", "ios-swift", "android-kotlin", "go", "java", "mcp-ts", "php-hexagonal", "php-laravel")
|
|
41
42
|
|
|
43
|
+
|
|
44
|
+
def released_mavis_stacks() -> list[str]:
|
|
45
|
+
contract = presets / "mavis" / "platform-contract.json"
|
|
46
|
+
if not contract.is_file():
|
|
47
|
+
return []
|
|
48
|
+
try:
|
|
49
|
+
data = json.loads(contract.read_text(encoding="utf-8"))
|
|
50
|
+
return list(data.get("releasedStacks") or [])
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
return []
|
|
53
|
+
|
|
42
54
|
# Agents that are shipped for orchestration purposes but do not need to appear
|
|
43
55
|
# in task-router.md intent rows (router-only / analyzer-only / orchestrator-only).
|
|
44
56
|
ROUTER_EXEMPT_FROM_INTENT = frozenset({
|
|
@@ -59,7 +71,8 @@ failures: list[str] = []
|
|
|
59
71
|
checks = 0
|
|
60
72
|
|
|
61
73
|
for platform in PLATFORMS:
|
|
62
|
-
|
|
74
|
+
stacks = STACKS if platform != "mavis" else released_mavis_stacks()
|
|
75
|
+
for stack in stacks:
|
|
63
76
|
agents_dir = presets / platform / stack / "agents"
|
|
64
77
|
if not agents_dir.is_dir():
|
|
65
78
|
continue
|