@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.
Files changed (213) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +6 -2
  3. package/bin/cli.js +32 -8
  4. package/bin/mavis-install.js +441 -0
  5. package/package.json +9 -2
  6. package/presets/_shared/core/meta/preset-pr-checklist.md +1 -0
  7. package/presets/_shared/core/meta/preset-twin-sync.md +16 -0
  8. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  9. package/presets/claude/android-kotlin/rules/tooling-and-review/preset-twin-sync.md +16 -0
  10. package/presets/claude/go/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  11. package/presets/claude/go/rules/tooling-and-review/preset-twin-sync.md +16 -0
  12. package/presets/claude/ios-swift/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  13. package/presets/claude/ios-swift/rules/tooling-and-review/preset-twin-sync.md +16 -0
  14. package/presets/claude/java/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  15. package/presets/claude/java/rules/tooling-and-review/preset-twin-sync.md +16 -0
  16. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  17. package/presets/claude/mcp-ts/rules/tooling-and-review/preset-twin-sync.md +16 -0
  18. package/presets/claude/next/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  19. package/presets/claude/next/rules/tooling-and-review/preset-twin-sync.md +16 -0
  20. package/presets/claude/nuxt/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  21. package/presets/claude/nuxt/rules/tooling-and-review/preset-twin-sync.md +16 -0
  22. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  23. package/presets/claude/php-hexagonal/rules/tooling-and-review/preset-twin-sync.md +16 -0
  24. package/presets/claude/php-laravel/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  25. package/presets/claude/php-laravel/rules/tooling-and-review/preset-twin-sync.md +16 -0
  26. package/presets/claude/svelte/rules/tooling-and-review/preset-pr-checklist.md +1 -0
  27. package/presets/claude/svelte/rules/tooling-and-review/preset-twin-sync.md +16 -0
  28. package/presets/cursor/android-kotlin/rules/preset-pr-checklist.mdc +1 -0
  29. package/presets/cursor/android-kotlin/rules/preset-twin-sync.mdc +16 -0
  30. package/presets/cursor/go/rules/preset-pr-checklist.mdc +1 -0
  31. package/presets/cursor/go/rules/preset-twin-sync.mdc +16 -0
  32. package/presets/cursor/ios-swift/rules/preset-pr-checklist.mdc +1 -0
  33. package/presets/cursor/ios-swift/rules/preset-twin-sync.mdc +16 -0
  34. package/presets/cursor/java/rules/preset-pr-checklist.mdc +1 -0
  35. package/presets/cursor/java/rules/preset-twin-sync.mdc +16 -0
  36. package/presets/cursor/mcp-ts/rules/preset-pr-checklist.mdc +1 -0
  37. package/presets/cursor/mcp-ts/rules/preset-twin-sync.mdc +16 -0
  38. package/presets/cursor/next/rules/preset-pr-checklist.mdc +1 -0
  39. package/presets/cursor/next/rules/preset-twin-sync.mdc +16 -0
  40. package/presets/cursor/nuxt/rules/preset-pr-checklist.mdc +1 -0
  41. package/presets/cursor/nuxt/rules/preset-twin-sync.mdc +16 -0
  42. package/presets/cursor/php-hexagonal/rules/preset-pr-checklist.mdc +1 -0
  43. package/presets/cursor/php-hexagonal/rules/preset-twin-sync.mdc +16 -0
  44. package/presets/cursor/php-laravel/rules/preset-pr-checklist.mdc +1 -0
  45. package/presets/cursor/php-laravel/rules/preset-twin-sync.mdc +16 -0
  46. package/presets/cursor/svelte/rules/preset-pr-checklist.mdc +1 -0
  47. package/presets/cursor/svelte/rules/preset-twin-sync.mdc +16 -0
  48. package/presets/mavis/README.md +60 -0
  49. package/presets/mavis/next/MAVIS.md +67 -0
  50. package/presets/mavis/next/MCP.md +36 -0
  51. package/presets/mavis/next/README.md +15 -0
  52. package/presets/mavis/next/REPO_AGENTS.md +94 -0
  53. package/presets/mavis/next/adapter-map.json +959 -0
  54. package/presets/mavis/next/agents/README.md +206 -0
  55. package/presets/mavis/next/agents/accessibility-reviewer.md +61 -0
  56. package/presets/mavis/next/agents/api-contract-reviewer.md +57 -0
  57. package/presets/mavis/next/agents/build-verifier.md +110 -0
  58. package/presets/mavis/next/agents/ci-investigator.md +63 -0
  59. package/presets/mavis/next/agents/code-reviewer.md +61 -0
  60. package/presets/mavis/next/agents/codebase-analyzer.md +21 -0
  61. package/presets/mavis/next/agents/debugger.md +80 -0
  62. package/presets/mavis/next/agents/feature-developer.md +101 -0
  63. package/presets/mavis/next/agents/migration-specialist.md +67 -0
  64. package/presets/mavis/next/agents/mockup-reviewer.md +53 -0
  65. package/presets/mavis/next/agents/openspec-author.md +157 -0
  66. package/presets/mavis/next/agents/performance-auditor.md +68 -0
  67. package/presets/mavis/next/agents/playwright-test-generator.md +28 -0
  68. package/presets/mavis/next/agents/playwright-test-healer.md +28 -0
  69. package/presets/mavis/next/agents/playwright-test-planner.md +21 -0
  70. package/presets/mavis/next/agents/qa-tester.md +52 -0
  71. package/presets/mavis/next/agents/security-reviewer.md +54 -0
  72. package/presets/mavis/next/agents/solution-architect.md +68 -0
  73. package/presets/mavis/next/agents/task-analyst.md +166 -0
  74. package/presets/mavis/next/agents/task-router.md +178 -0
  75. package/presets/mavis/next/agents/tech-writer.md +57 -0
  76. package/presets/mavis/next/agents/unit-test-generator.md +31 -0
  77. package/presets/mavis/next/agents/unit-test-healer.md +32 -0
  78. package/presets/mavis/next/agents/unit-test-planner.md +54 -0
  79. package/presets/mavis/next/commands/README.md +49 -0
  80. package/presets/mavis/next/commands/spec-start.md +24 -0
  81. package/presets/mavis/next/commands/task-continue.md +50 -0
  82. package/presets/mavis/next/commands/task.md +50 -0
  83. package/presets/mavis/next/commands/technical-retro.md +80 -0
  84. package/presets/mavis/next/hooks/README.md +22 -0
  85. package/presets/mavis/next/hooks/chain-team-phases.sh +845 -0
  86. package/presets/mavis/next/mcp.json +19 -0
  87. package/presets/mavis/next/rules/README.md +16 -0
  88. package/presets/mavis/next/rules/agent-team-intake.md +11 -0
  89. package/presets/mavis/next/rules/agent-team-orchestrator.md +210 -0
  90. package/presets/mavis/next/rules/ai-local-overlay.md +37 -0
  91. package/presets/mavis/next/rules/anti-sycophancy-discipline.md +23 -0
  92. package/presets/mavis/next/rules/api-services.md +72 -0
  93. package/presets/mavis/next/rules/architecture-boundaries-ui.md +9 -0
  94. package/presets/mavis/next/rules/architecture-boundaries.md +86 -0
  95. package/presets/mavis/next/rules/arrow-functions.md +25 -0
  96. package/presets/mavis/next/rules/code-quality-and-refactoring.md +40 -0
  97. package/presets/mavis/next/rules/code-review-mr.md +40 -0
  98. package/presets/mavis/next/rules/css-property-order-stylelint.md +5 -0
  99. package/presets/mavis/next/rules/design-guidance.md +99 -0
  100. package/presets/mavis/next/rules/feature-delivery-workflow.md +49 -0
  101. package/presets/mavis/next/rules/http-client.md +55 -0
  102. package/presets/mavis/next/rules/layer-barrel-exports.md +68 -0
  103. package/presets/mavis/next/rules/mcp-usage.md +86 -0
  104. package/presets/mavis/next/rules/mockup-review.md +107 -0
  105. package/presets/mavis/next/rules/navigation-router-stack.md +56 -0
  106. package/presets/mavis/next/rules/navigation-router-ui.md +10 -0
  107. package/presets/mavis/next/rules/next-app-core.md +29 -0
  108. package/presets/mavis/next/rules/next-app-router.md +44 -0
  109. package/presets/mavis/next/rules/no-props-spread.md +56 -0
  110. package/presets/mavis/next/rules/no-type-assertion-as-import-export.md +51 -0
  111. package/presets/mavis/next/rules/package-manager.md +11 -0
  112. package/presets/mavis/next/rules/playwright-agents.md +67 -0
  113. package/presets/mavis/next/rules/post-change-lint.md +35 -0
  114. package/presets/mavis/next/rules/preset-layering.md +27 -0
  115. package/presets/mavis/next/rules/preset-no-cross-stack-leakage.md +90 -0
  116. package/presets/mavis/next/rules/preset-pr-checklist.md +35 -0
  117. package/presets/mavis/next/rules/preset-token-budget.md +36 -0
  118. package/presets/mavis/next/rules/preset-twin-sync.md +65 -0
  119. package/presets/mavis/next/rules/product-specs-authoring.md +112 -0
  120. package/presets/mavis/next/rules/product-specs.md +56 -0
  121. package/presets/mavis/next/rules/public-imports.md +50 -0
  122. package/presets/mavis/next/rules/react-a11y-coding.md +31 -0
  123. package/presets/mavis/next/rules/react-next-anti-hallucination.md +60 -0
  124. package/presets/mavis/next/rules/react-ui.md +105 -0
  125. package/presets/mavis/next/rules/reference-features.md +45 -0
  126. package/presets/mavis/next/rules/security-next.md +45 -0
  127. package/presets/mavis/next/rules/store-rtk.md +78 -0
  128. package/presets/mavis/next/rules/technical-retro.md +11 -0
  129. package/presets/mavis/next/rules/tests-e2e-structure.md +72 -0
  130. package/presets/mavis/next/rules/tests-unit.md +72 -0
  131. package/presets/mavis/next/rules/types-jsdoc.md +43 -0
  132. package/presets/mavis/next/rules/ux-design.md +99 -0
  133. package/presets/mavis/next/skills/README.md +21 -0
  134. package/presets/mavis/next/skills/ci-investigation/SKILL.md +39 -0
  135. package/presets/mavis/next/skills/code-review/SKILL.md +39 -0
  136. package/presets/mavis/next/skills/debug-investigation/SKILL.md +26 -0
  137. package/presets/mavis/next/skills/feature-delivery/SKILL.md +34 -0
  138. package/presets/mavis/next/skills/mockup-review/SKILL.md +24 -0
  139. package/presets/mavis/next/skills/playwright-e2e/SKILL.md +28 -0
  140. package/presets/mavis/next/skills/react-performance/SKILL.md +39 -0
  141. package/presets/mavis/next/skills/spec-authoring/SKILL.md +21 -0
  142. package/presets/mavis/next/skills/spec-discovery/SKILL.md +53 -0
  143. package/presets/mavis/next/skills/technical-retro/SKILL.md +49 -0
  144. package/presets/mavis/next/skills/unit-testing/SKILL.md +29 -0
  145. package/presets/mavis/next/skills/ux-design/SKILL.md +31 -0
  146. package/presets/mavis/next/skills/write-adr/SKILL.md +36 -0
  147. package/presets/mavis/next/team/README.md +148 -0
  148. package/presets/mavis/next/team/conventions.md +21 -0
  149. package/presets/mavis/next/team/fixtures/artifact-manifest.example.json +79 -0
  150. package/presets/mavis/next/team/fixtures/bugfix-standard.json +25 -0
  151. package/presets/mavis/next/team/fixtures/feature-full.json +29 -0
  152. package/presets/mavis/next/team/fixtures/feature-light.json +27 -0
  153. package/presets/mavis/next/team/fixtures/feature-with-figma-url.json +17 -0
  154. package/presets/mavis/next/team/fixtures/feature-with-ux-no-figma.json +17 -0
  155. package/presets/mavis/next/team/fixtures/memlog.example.md +9 -0
  156. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/human-gate/metrics.json +69 -0
  157. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/missing-ledger/status.json +5 -0
  158. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/parallel-receipts/metrics.json +79 -0
  159. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/serial-retry/metrics.json +124 -0
  160. package/presets/mavis/next/team/fixtures/metrics-scenarios/tasks/validation-failed/metrics.json +124 -0
  161. package/presets/mavis/next/team/fixtures/metrics.example.json +54 -0
  162. package/presets/mavis/next/team/fixtures/mockup-review-fail-embedded-status.json +8 -0
  163. package/presets/mavis/next/team/fixtures/mockup-review-standalone.json +14 -0
  164. package/presets/mavis/next/team/fixtures/ux-design-standalone.json +15 -0
  165. package/presets/mavis/next/team/tasks/.gitkeep +0 -0
  166. package/presets/mavis/platform-contract.json +131 -0
  167. package/scripts/capture-cli-golden.mjs +44 -0
  168. package/scripts/check-chain-hook-coverage.sh +1 -0
  169. package/scripts/check-mavis-adapter-map.mjs +294 -0
  170. package/scripts/check-mavis-local-overlay.mjs +56 -0
  171. package/scripts/check-mavis-platform-contract.mjs +388 -0
  172. package/scripts/check-mavis-token-budget.mjs +48 -0
  173. package/scripts/check-preset-leakage.sh +35 -3
  174. package/scripts/check-preset-structure.sh +63 -0
  175. package/scripts/check-shared-core-drift.sh +18 -0
  176. package/scripts/check-task-router-intents.sh +15 -2
  177. package/scripts/fixtures/cli-golden/claude/next.json +130 -0
  178. package/scripts/fixtures/cli-golden/cursor/next.json +132 -0
  179. package/scripts/fixtures/cli-golden/mavis/next.json +125 -0
  180. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/AGENTS.md +1 -0
  181. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/extra.mdc +1 -0
  182. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/cursor/rules/sample.mdc +2 -0
  183. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/MAVIS.md +1 -0
  184. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/adapter-map.json +26 -0
  185. package/scripts/fixtures/mavis-adapter-map/invalid-unmapped/mavis/rules/sample.md +2 -0
  186. package/scripts/fixtures/mavis-adapter-map/valid/cursor/AGENTS.md +1 -0
  187. package/scripts/fixtures/mavis-adapter-map/valid/cursor/rules/sample.mdc +2 -0
  188. package/scripts/fixtures/mavis-adapter-map/valid/mavis/MAVIS.md +1 -0
  189. package/scripts/fixtures/mavis-adapter-map/valid/mavis/adapter-map.json +26 -0
  190. package/scripts/fixtures/mavis-adapter-map/valid/mavis/rules/sample.md +2 -0
  191. package/scripts/fixtures/mavis-platform-contract/invalid-minimax-destination.json +131 -0
  192. package/scripts/fixtures/mavis-platform-contract/invalid-missing-capability.json +126 -0
  193. package/scripts/fixtures/mavis-platform-contract/invalid-released-unknown-stack.json +133 -0
  194. package/scripts/fixtures/mavis-platform-contract/invalid-unknown-native-path.json +131 -0
  195. package/scripts/fixtures/mavis-platform-contract/invalid-version-pin.json +132 -0
  196. package/scripts/fixtures/mavis-platform-contract/valid.json +131 -0
  197. package/scripts/fixtures/mavis-probe/AGENTS.md +6 -0
  198. package/scripts/fixtures/mavis-probe/agents/probe-agent.md +6 -0
  199. package/scripts/fixtures/mavis-probe/fake-mcode.mjs +98 -0
  200. package/scripts/fixtures/mavis-probe/output.schema.json +28 -0
  201. package/scripts/fixtures/mavis-probe/rules/probe.md +5 -0
  202. package/scripts/fixtures/mavis-probe/skills/probe/SKILL.md +11 -0
  203. package/scripts/generate-mavis-next.mjs +704 -0
  204. package/scripts/lib/cli-golden.mjs +50 -0
  205. package/scripts/lib/discover-hooks.mjs +72 -0
  206. package/scripts/probe-mavis-capabilities.mjs +480 -0
  207. package/scripts/test-agent-task-metrics-hooks.mjs +22 -16
  208. package/scripts/test-chain-team-phases-coverage.mjs +10 -7
  209. package/scripts/test-cli-init-clean.mjs +256 -0
  210. package/scripts/test-cli-mcp-merge.mjs +23 -0
  211. package/scripts/test-mavis-context-budget.mjs +55 -0
  212. package/scripts/test-mavis-orchestration.mjs +649 -0
  213. 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
- for platform in PLATFORMS:
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 PLATFORMS:
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
- for stack in STACKS:
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