@polymorphism-tech/morph-spec 8.28.0 → 8.31.0

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 (185) hide show
  1. package/CLAUDE.md +6 -1
  2. package/README.md +13 -2
  3. package/bin/morph-spec.js +48 -0
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -0
  9. package/framework/MORPH.md +2 -1
  10. package/framework/agents.json +20 -9
  11. package/framework/commands/create-umbraco-lp.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +18 -0
  14. package/framework/evals/rubrics/code-quality.md +17 -0
  15. package/framework/evals/rubrics/condensed.md +3 -1
  16. package/framework/hooks/claude-code/statusline.sh +11 -11
  17. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  18. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  19. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  20. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  21. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  22. package/framework/hooks/shared/phase-utils.js +2 -0
  23. package/framework/hooks/shared/stdin-reader.js +26 -26
  24. package/framework/mcp/registry.json +97 -11
  25. package/framework/rubrics/backend.json +2 -2
  26. package/framework/rubrics/default.json +2 -2
  27. package/framework/rubrics/frontend.json +2 -2
  28. package/framework/schemas/tasks.schema.json +25 -0
  29. package/framework/skills/core/create-umbraco-lp/SKILL.md +13 -2
  30. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +598 -0
  31. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +92 -7
  32. package/framework/skills/core/morph-apply/SKILL.md +12 -3
  33. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  34. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  35. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  36. package/framework/skills/core/morph-implement/SKILL.md +12 -2
  37. package/framework/skills/core/morph-plan/SKILL.md +45 -3
  38. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  39. package/framework/skills/core/morph-review/SKILL.md +11 -3
  40. package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
  41. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  42. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  43. package/framework/standards/PROVENANCE.json +41 -0
  44. package/framework/standards/STANDARDS.json +218 -7
  45. package/framework/standards/ai-agents/agent-session.md +160 -160
  46. package/framework/standards/ai-agents/batching.md +193 -193
  47. package/framework/standards/ai-agents/context-providers.md +188 -188
  48. package/framework/standards/ai-agents/mcp-server.md +250 -250
  49. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  50. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  51. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  52. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  53. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  54. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  55. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  56. package/framework/standards/ai-agents/production.md +231 -231
  57. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  58. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  59. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  60. package/framework/standards/ai-agents/structured-output.md +249 -249
  61. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  62. package/framework/standards/frontend/design-system/ai-image-generation.md +43 -0
  63. package/framework/standards/frontend/design-system/ai-video-generation.md +113 -0
  64. package/framework/standards/frontend/design-system/premium-finish.md +160 -0
  65. package/framework/standards/frontend/gsap/gsap-core.md +1 -1
  66. package/framework/standards/frontend/gsap/gsap-plugins.md +1 -1
  67. package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +3 -1
  68. package/framework/standards/frontend/nextjs/motion-patterns.md +3 -1
  69. package/framework/standards/frontend/scroll-driven/frame-scrub.md +287 -0
  70. package/framework/standards/frontend/scroll-driven/scroll-components.md +256 -0
  71. package/framework/standards/frontend/scroll-driven/smooth-scroll.md +148 -0
  72. package/framework/standards/frontend/seo/meta-schema-audit.md +134 -0
  73. package/framework/templates/.idea/morph-templates.xml +92 -0
  74. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  75. package/framework/templates/IDE-SNIPPETS.md +262 -262
  76. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  77. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  78. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  79. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  80. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  81. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  82. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  83. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  84. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  85. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  86. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  87. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  88. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  89. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  90. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  91. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  92. package/framework/templates/code/typescript/contracts.ts +167 -167
  93. package/framework/templates/context/ARCHITECTURE.md +20 -0
  94. package/framework/templates/docs/clarifications.md +253 -253
  95. package/framework/templates/docs/schema-analysis.md +119 -119
  96. package/framework/templates/docs/ui-components.md +52 -52
  97. package/framework/templates/docs/ui-design-system.md +76 -76
  98. package/framework/templates/docs/ui-flows.md +167 -167
  99. package/framework/templates/docs/ui-mockups.md +98 -98
  100. package/framework/templates/feature/decisions.md +122 -122
  101. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  102. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  103. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  104. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  105. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  106. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  107. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  108. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  109. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  110. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  111. package/framework/templates/infrastructure/github/README.md +56 -56
  112. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  113. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  114. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  115. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  116. package/framework/templates/integrations/asaas-client.cs +387 -387
  117. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  118. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  119. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  120. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  121. package/framework/templates/project/validate.js +120 -120
  122. package/framework/templates/state.template.json +17 -17
  123. package/framework/templates/ui/design-system.css +226 -226
  124. package/package.json +1 -1
  125. package/src/commands/dashboard/dashboard.js +53 -53
  126. package/src/commands/graph/affected.js +89 -0
  127. package/src/commands/graph/build.js +94 -0
  128. package/src/commands/graph/explain.js +96 -0
  129. package/src/commands/graph/status.js +93 -0
  130. package/src/commands/index.js +13 -13
  131. package/src/commands/project/checkpoint.js +209 -209
  132. package/src/commands/project/doctor.js +11 -0
  133. package/src/core/paths/output-schema.js +7 -0
  134. package/src/core/state/phase-state-machine.js +197 -197
  135. package/src/core/templates/template-renderer.js +477 -477
  136. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  137. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  138. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  139. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  142. package/src/dashboard/app/pages/HookInspector.jsx +418 -418
  143. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  144. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  145. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  146. package/src/dashboard/app/pages/Standards.jsx +332 -332
  147. package/src/dashboard/app/styles/index.css +47 -47
  148. package/src/dashboard/index.html +12 -12
  149. package/src/dashboard/package.json +24 -24
  150. package/src/dashboard/scanner/phases-scanner.js +15 -15
  151. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  152. package/src/dashboard/scanner/rules-scanner.js +37 -37
  153. package/src/dashboard/scanner/skills-scanner.js +43 -43
  154. package/src/dashboard/scanner/squads-scanner.js +13 -13
  155. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  156. package/src/dashboard/vite.config.js +24 -24
  157. package/src/lib/detectors/design-system-detector.js +188 -188
  158. package/src/lib/detectors/index.js +217 -217
  159. package/src/lib/generators/architecture-scanner.js +45 -4
  160. package/src/lib/generators/design-system-generator.js +508 -508
  161. package/src/lib/generators/settings-generator.js +209 -209
  162. package/src/lib/graph/god-nodes.js +75 -0
  163. package/src/lib/graph/graph-health.js +61 -0
  164. package/src/lib/graph/graph-loader.js +86 -0
  165. package/src/lib/graph/graphify-runtime.js +100 -0
  166. package/src/lib/graph/graphifyignore.js +86 -0
  167. package/src/lib/graph/sanitize.js +280 -0
  168. package/src/lib/hooks/hook-executor.js +167 -167
  169. package/src/lib/installers/mcp-installer.js +719 -719
  170. package/src/lib/tasks/task-classifier.js +30 -30
  171. package/src/lib/tasks/task-parser.js +209 -209
  172. package/src/lib/tasks/tasks-json-parser.js +26 -0
  173. package/src/lib/validators/checkpoint-persistence.js +39 -39
  174. package/src/lib/validators/css/css-validator.js +340 -340
  175. package/src/lib/validators/reuse/index.js +10 -0
  176. package/src/lib/validators/reuse/reuse-validator.js +324 -0
  177. package/src/lib/validators/reuse/symbol-scanner.js +144 -0
  178. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  179. package/src/lib/validators/shared/issue-counter.js +14 -14
  180. package/src/lib/validators/validation-runner.js +40 -1
  181. package/src/scripts/setup-infra.js +60 -1
  182. package/src/utils/agent-id-mapper.js +11 -11
  183. package/src/utils/claude-settings-manager.js +300 -300
  184. package/src/utils/file-copier.js +40 -0
  185. package/src/utils/skills-installer.js +187 -187
@@ -1,209 +1,209 @@
1
- /**
2
- * Settings Generator
3
- *
4
- * Generates .claude/settings.local.json for projects based on detected
5
- * stack, MCPs, and morph-spec configuration. Uses _morph_managed marker
6
- * to track managed entries for safe reset.
7
- */
8
-
9
- import { join } from 'path';
10
- import { existsSync } from 'fs';
11
- import { readFile, writeFile, mkdir } from 'fs/promises';
12
-
13
- const MORPH_MANAGED_KEY = '_morph_managed';
14
-
15
- /**
16
- * Generate and merge settings for a project
17
- * @param {string} targetPath - Project root directory
18
- * @param {Object} detectedConfig - Output from detectClaudeConfig()
19
- * @param {Object} morphConfig - morph-spec config (from .morph/config/config.json)
20
- * @returns {Promise<Object>} Generated settings summary
21
- */
22
- export async function generateSettings(targetPath, detectedConfig, morphConfig = {}) {
23
- const claudeDir = join(targetPath, '.claude');
24
- const settingsPath = join(claudeDir, 'settings.local.json');
25
-
26
- // Read existing settings
27
- const existing = await readSettingsSafe(settingsPath);
28
-
29
- // Build stack-based permissions
30
- const stack = morphConfig?.project?.architecture || 'unknown';
31
- const permissions = buildPermissions(stack, detectedConfig);
32
-
33
- // Build generated settings
34
- const generated = {};
35
-
36
- if (permissions.length > 0) {
37
- generated.permissions = permissions;
38
- }
39
-
40
- // Track what we manage
41
- const managed = {
42
- permissions_added: permissions,
43
- mcps_detected: detectedConfig.mcpServers.map(s => s.name),
44
- plugins_detected: detectedConfig.plugins.map(p => p.name),
45
- generated_at: new Date().toISOString()
46
- };
47
-
48
- // Merge without overwriting user config
49
- const merged = mergeSettings(existing, generated);
50
- merged[MORPH_MANAGED_KEY] = managed;
51
-
52
- // Ensure directory exists
53
- if (!existsSync(claudeDir)) {
54
- await mkdir(claudeDir, { recursive: true });
55
- }
56
-
57
- await writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
58
-
59
- return {
60
- settingsPath,
61
- permissionsAdded: permissions.length,
62
- managed
63
- };
64
- }
65
-
66
- /**
67
- * Build permission rules based on detected stack
68
- * @param {string} stack - Detected project stack
69
- * @param {Object} detectedConfig - Detected Claude config
70
- * @returns {Array<string>} Permission patterns
71
- */
72
- export function buildPermissions(stack, detectedConfig = {}) {
73
- const permissions = [];
74
-
75
- // Stack-based permissions
76
- const stackPermissions = {
77
- nodejs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
78
- typescript: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)', 'Bash(tsc:*)'],
79
- nextjs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
80
- react: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
81
- vue: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
82
- dotnet: ['Bash(dotnet:*)', 'Bash(nuget:*)'],
83
- python: ['Bash(python:*)', 'Bash(pip:*)', 'Bash(pytest:*)'],
84
- go: ['Bash(go:*)']
85
- };
86
-
87
- if (stackPermissions[stack]) {
88
- permissions.push(...stackPermissions[stack]);
89
- }
90
-
91
- // morph-spec CLI always needs node/npx
92
- if (!permissions.includes('Bash(npx:*)')) {
93
- permissions.push('Bash(npx:*)');
94
- }
95
-
96
- return permissions;
97
- }
98
-
99
- /**
100
- * Deep merge settings without overwriting user entries
101
- * @param {Object} existing - Current settings.local.json
102
- * @param {Object} generated - Generated settings to merge
103
- * @returns {Object} Merged settings
104
- */
105
- export function mergeSettings(existing, generated) {
106
- const result = { ...existing };
107
-
108
- for (const [key, value] of Object.entries(generated)) {
109
- if (key === MORPH_MANAGED_KEY) continue;
110
-
111
- if (Array.isArray(value) && Array.isArray(result[key])) {
112
- // Merge arrays without duplicates
113
- const set = new Set(result[key]);
114
- for (const item of value) {
115
- set.add(item);
116
- }
117
- result[key] = [...set];
118
- } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
119
- // Recursively merge objects
120
- result[key] = mergeSettings(result[key] || {}, value);
121
- } else if (!(key in result)) {
122
- // Only add new keys, never overwrite existing
123
- result[key] = value;
124
- }
125
- }
126
-
127
- return result;
128
- }
129
-
130
- /**
131
- * Remove only morph-spec managed entries from settings
132
- * @param {string} settingsPath - Path to settings.local.json
133
- * @returns {Promise<Object>} Summary of what was removed
134
- */
135
- export async function resetMorphSettings(settingsPath) {
136
- if (!existsSync(settingsPath)) {
137
- return { removed: false, reason: 'file not found' };
138
- }
139
-
140
- const settings = await readSettingsSafe(settingsPath);
141
- const managed = settings[MORPH_MANAGED_KEY];
142
-
143
- if (!managed) {
144
- return { removed: false, reason: 'no _morph_managed section found' };
145
- }
146
-
147
- // Remove managed permissions
148
- if (managed.permissions_added && Array.isArray(settings.permissions)) {
149
- settings.permissions = settings.permissions.filter(
150
- p => !managed.permissions_added.includes(p)
151
- );
152
- if (settings.permissions.length === 0) {
153
- delete settings.permissions;
154
- }
155
- }
156
-
157
- // Remove the managed marker itself
158
- delete settings[MORPH_MANAGED_KEY];
159
-
160
- // Write back
161
- await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
162
-
163
- return {
164
- removed: true,
165
- permissionsRemoved: managed.permissions_added || [],
166
- previouslyGenerated: managed.generated_at
167
- };
168
- }
169
-
170
- /**
171
- * Save integrations.json with detected config
172
- * @param {string} targetPath - Project root directory
173
- * @param {Object} detectedConfig - Output from detectClaudeConfig()
174
- */
175
- export async function saveIntegrations(targetPath, detectedConfig) {
176
- const integrationsPath = join(targetPath, '.morph', 'config', 'integrations.json');
177
- const integrationsDir = join(targetPath, '.morph', 'config');
178
-
179
- if (!existsSync(integrationsDir)) {
180
- await mkdir(integrationsDir, { recursive: true });
181
- }
182
-
183
- const integrations = {
184
- version: '1.0.0',
185
- detectedAt: new Date().toISOString(),
186
- plugins: detectedConfig.plugins,
187
- mcpServers: detectedConfig.mcpServers,
188
- superpowers: detectedConfig.superpowers,
189
- customSkills: detectedConfig.customSkills.length,
190
- customCommands: detectedConfig.customCommands.length,
191
- hooks: detectedConfig.hooks.length
192
- };
193
-
194
- await writeFile(integrationsPath, JSON.stringify(integrations, null, 2) + '\n', 'utf-8');
195
- return integrationsPath;
196
- }
197
-
198
- // ============================================================================
199
- // Helpers
200
- // ============================================================================
201
-
202
- async function readSettingsSafe(settingsPath) {
203
- try {
204
- if (!existsSync(settingsPath)) return {};
205
- return JSON.parse(await readFile(settingsPath, 'utf-8'));
206
- } catch {
207
- return {};
208
- }
209
- }
1
+ /**
2
+ * Settings Generator
3
+ *
4
+ * Generates .claude/settings.local.json for projects based on detected
5
+ * stack, MCPs, and morph-spec configuration. Uses _morph_managed marker
6
+ * to track managed entries for safe reset.
7
+ */
8
+
9
+ import { join } from 'path';
10
+ import { existsSync } from 'fs';
11
+ import { readFile, writeFile, mkdir } from 'fs/promises';
12
+
13
+ const MORPH_MANAGED_KEY = '_morph_managed';
14
+
15
+ /**
16
+ * Generate and merge settings for a project
17
+ * @param {string} targetPath - Project root directory
18
+ * @param {Object} detectedConfig - Output from detectClaudeConfig()
19
+ * @param {Object} morphConfig - morph-spec config (from .morph/config/config.json)
20
+ * @returns {Promise<Object>} Generated settings summary
21
+ */
22
+ export async function generateSettings(targetPath, detectedConfig, morphConfig = {}) {
23
+ const claudeDir = join(targetPath, '.claude');
24
+ const settingsPath = join(claudeDir, 'settings.local.json');
25
+
26
+ // Read existing settings
27
+ const existing = await readSettingsSafe(settingsPath);
28
+
29
+ // Build stack-based permissions
30
+ const stack = morphConfig?.project?.architecture || 'unknown';
31
+ const permissions = buildPermissions(stack, detectedConfig);
32
+
33
+ // Build generated settings
34
+ const generated = {};
35
+
36
+ if (permissions.length > 0) {
37
+ generated.permissions = permissions;
38
+ }
39
+
40
+ // Track what we manage
41
+ const managed = {
42
+ permissions_added: permissions,
43
+ mcps_detected: detectedConfig.mcpServers.map(s => s.name),
44
+ plugins_detected: detectedConfig.plugins.map(p => p.name),
45
+ generated_at: new Date().toISOString()
46
+ };
47
+
48
+ // Merge without overwriting user config
49
+ const merged = mergeSettings(existing, generated);
50
+ merged[MORPH_MANAGED_KEY] = managed;
51
+
52
+ // Ensure directory exists
53
+ if (!existsSync(claudeDir)) {
54
+ await mkdir(claudeDir, { recursive: true });
55
+ }
56
+
57
+ await writeFile(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
58
+
59
+ return {
60
+ settingsPath,
61
+ permissionsAdded: permissions.length,
62
+ managed
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Build permission rules based on detected stack
68
+ * @param {string} stack - Detected project stack
69
+ * @param {Object} detectedConfig - Detected Claude config
70
+ * @returns {Array<string>} Permission patterns
71
+ */
72
+ export function buildPermissions(stack, detectedConfig = {}) {
73
+ const permissions = [];
74
+
75
+ // Stack-based permissions
76
+ const stackPermissions = {
77
+ nodejs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
78
+ typescript: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)', 'Bash(tsc:*)'],
79
+ nextjs: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
80
+ react: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
81
+ vue: ['Bash(node:*)', 'Bash(npm:*)', 'Bash(npx:*)'],
82
+ dotnet: ['Bash(dotnet:*)', 'Bash(nuget:*)'],
83
+ python: ['Bash(python:*)', 'Bash(pip:*)', 'Bash(pytest:*)'],
84
+ go: ['Bash(go:*)']
85
+ };
86
+
87
+ if (stackPermissions[stack]) {
88
+ permissions.push(...stackPermissions[stack]);
89
+ }
90
+
91
+ // morph-spec CLI always needs node/npx
92
+ if (!permissions.includes('Bash(npx:*)')) {
93
+ permissions.push('Bash(npx:*)');
94
+ }
95
+
96
+ return permissions;
97
+ }
98
+
99
+ /**
100
+ * Deep merge settings without overwriting user entries
101
+ * @param {Object} existing - Current settings.local.json
102
+ * @param {Object} generated - Generated settings to merge
103
+ * @returns {Object} Merged settings
104
+ */
105
+ export function mergeSettings(existing, generated) {
106
+ const result = { ...existing };
107
+
108
+ for (const [key, value] of Object.entries(generated)) {
109
+ if (key === MORPH_MANAGED_KEY) continue;
110
+
111
+ if (Array.isArray(value) && Array.isArray(result[key])) {
112
+ // Merge arrays without duplicates
113
+ const set = new Set(result[key]);
114
+ for (const item of value) {
115
+ set.add(item);
116
+ }
117
+ result[key] = [...set];
118
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
119
+ // Recursively merge objects
120
+ result[key] = mergeSettings(result[key] || {}, value);
121
+ } else if (!(key in result)) {
122
+ // Only add new keys, never overwrite existing
123
+ result[key] = value;
124
+ }
125
+ }
126
+
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * Remove only morph-spec managed entries from settings
132
+ * @param {string} settingsPath - Path to settings.local.json
133
+ * @returns {Promise<Object>} Summary of what was removed
134
+ */
135
+ export async function resetMorphSettings(settingsPath) {
136
+ if (!existsSync(settingsPath)) {
137
+ return { removed: false, reason: 'file not found' };
138
+ }
139
+
140
+ const settings = await readSettingsSafe(settingsPath);
141
+ const managed = settings[MORPH_MANAGED_KEY];
142
+
143
+ if (!managed) {
144
+ return { removed: false, reason: 'no _morph_managed section found' };
145
+ }
146
+
147
+ // Remove managed permissions
148
+ if (managed.permissions_added && Array.isArray(settings.permissions)) {
149
+ settings.permissions = settings.permissions.filter(
150
+ p => !managed.permissions_added.includes(p)
151
+ );
152
+ if (settings.permissions.length === 0) {
153
+ delete settings.permissions;
154
+ }
155
+ }
156
+
157
+ // Remove the managed marker itself
158
+ delete settings[MORPH_MANAGED_KEY];
159
+
160
+ // Write back
161
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
162
+
163
+ return {
164
+ removed: true,
165
+ permissionsRemoved: managed.permissions_added || [],
166
+ previouslyGenerated: managed.generated_at
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Save integrations.json with detected config
172
+ * @param {string} targetPath - Project root directory
173
+ * @param {Object} detectedConfig - Output from detectClaudeConfig()
174
+ */
175
+ export async function saveIntegrations(targetPath, detectedConfig) {
176
+ const integrationsPath = join(targetPath, '.morph', 'config', 'integrations.json');
177
+ const integrationsDir = join(targetPath, '.morph', 'config');
178
+
179
+ if (!existsSync(integrationsDir)) {
180
+ await mkdir(integrationsDir, { recursive: true });
181
+ }
182
+
183
+ const integrations = {
184
+ version: '1.0.0',
185
+ detectedAt: new Date().toISOString(),
186
+ plugins: detectedConfig.plugins,
187
+ mcpServers: detectedConfig.mcpServers,
188
+ superpowers: detectedConfig.superpowers,
189
+ customSkills: detectedConfig.customSkills.length,
190
+ customCommands: detectedConfig.customCommands.length,
191
+ hooks: detectedConfig.hooks.length
192
+ };
193
+
194
+ await writeFile(integrationsPath, JSON.stringify(integrations, null, 2) + '\n', 'utf-8');
195
+ return integrationsPath;
196
+ }
197
+
198
+ // ============================================================================
199
+ // Helpers
200
+ // ============================================================================
201
+
202
+ async function readSettingsSafe(settingsPath) {
203
+ try {
204
+ if (!existsSync(settingsPath)) return {};
205
+ return JSON.parse(await readFile(settingsPath, 'utf-8'));
206
+ } catch {
207
+ return {};
208
+ }
209
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * God nodes e comunidades para o contexto de arquitetura.
3
+ *
4
+ * O inventário do architecture-scanner diz o que EXISTE; os god nodes dizem o
5
+ * que é CENTRAL. No ProspectPRO, seis linhas (`Result` 223, `Abstractions` 217,
6
+ * `Repository` 156, `cn()` 141) orientam melhor que 665 de inventário — sem
7
+ * substituí-lo, porque exaustividade é um valor que o grafo não tem.
8
+ *
9
+ * Só entram nós COM source_file: um god node sem arquivo não orienta ninguém.
10
+ *
11
+ * @module god-nodes
12
+ */
13
+ import { loadGraph } from './graph-loader.js';
14
+ import { buildIndex, resolveEdgeTarget } from './sanitize.js';
15
+
16
+ /** Sem `graphify label`, as comunidades se chamam Community_N — ruído puro. */
17
+ const PLACEHOLDER_LABEL = /^Community[_ ]?\d+$/i;
18
+
19
+ /**
20
+ * @param {string} projectPath
21
+ * @param {{top?: number, topCommunities?: number}} [opts]
22
+ * @returns {Promise<{available: boolean, godNodes: Array<{label: string, source_file: string, degree: number}>, communities: Array<{id: number, label: string, size: number, labeled: boolean}>}>}
23
+ */
24
+ export async function scanGraph(projectPath, opts = {}) {
25
+ const top = opts.top ?? 10;
26
+ const topCommunities = opts.topCommunities ?? 10;
27
+
28
+ const loaded = loadGraph(projectPath);
29
+ if (!loaded.ok) return { available: false, godNodes: [], communities: [] };
30
+
31
+ const { graph } = loaded;
32
+ const index = buildIndex(graph);
33
+
34
+ // Grau depois do saneamento: a coalescência muda quem é central, porque as
35
+ // arestas que apontavam para stub passam a contar no nó real.
36
+ const degree = new Map();
37
+ const bump = (id) => {
38
+ if (id) degree.set(id, (degree.get(id) ?? 0) + 1);
39
+ };
40
+ for (const link of graph.links ?? []) {
41
+ bump(link.source);
42
+ bump(resolveEdgeTarget(index, link).id);
43
+ }
44
+
45
+ const godNodes = [...degree.entries()]
46
+ .map(([id, d]) => ({ node: index.byId.get(id), degree: d }))
47
+ .filter(({ node }) => node?.source_file)
48
+ .sort((a, b) => b.degree - a.degree || a.node.label.localeCompare(b.node.label))
49
+ .slice(0, top)
50
+ .map(({ node, degree: d }) => ({
51
+ label: node.label,
52
+ source_file: node.source_file,
53
+ degree: d,
54
+ }));
55
+
56
+ const sizes = new Map();
57
+ const labels = new Map();
58
+ for (const node of graph.nodes ?? []) {
59
+ if (node.community === undefined || node.community === null) continue;
60
+ sizes.set(node.community, (sizes.get(node.community) ?? 0) + 1);
61
+ if (node.community_label && !labels.has(node.community)) {
62
+ labels.set(node.community, node.community_label);
63
+ }
64
+ }
65
+
66
+ const communities = [...sizes.entries()]
67
+ .sort((a, b) => b[1] - a[1] || a[0] - b[0])
68
+ .slice(0, topCommunities)
69
+ .map(([id, size]) => {
70
+ const label = labels.get(id) ?? `Community_${id}`;
71
+ return { id, label, size, labeled: !PLACEHOLDER_LABEL.test(label) };
72
+ });
73
+
74
+ return { available: true, godNodes, communities };
75
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Auditoria do grafo para o `morph-spec doctor`.
3
+ *
4
+ * Nunca devolve estado de erro: o grafo é opcional (ADR-002) e não pode pintar
5
+ * de vermelho o doctor de um projeto que não o usa. O pior veredito é `warn`.
6
+ *
7
+ * @module graph-health
8
+ */
9
+ import { loadGraph, getFreshness } from './graph-loader.js';
10
+ import { GRAPHIFY_VERSION, detectGraphifyVersion } from './graphify-runtime.js';
11
+
12
+ /**
13
+ * @param {string} projectPath
14
+ * @param {{_version?: () => string|null}} [deps]
15
+ * @returns {{status: 'ok'|'warn'|'absent', msg: string, details: string[]}}
16
+ */
17
+ export function auditGraph(projectPath, deps = {}) {
18
+ const installed = (deps._version ?? detectGraphifyVersion)();
19
+ const loaded = loadGraph(projectPath);
20
+
21
+ if (!loaded.ok && loaded.reason === 'missing') {
22
+ return {
23
+ status: 'absent',
24
+ msg: 'opcional, não gerado — morph-spec graph build',
25
+ details: [],
26
+ };
27
+ }
28
+ if (!loaded.ok) {
29
+ return {
30
+ status: 'warn',
31
+ msg: `graph.json ${loaded.reason} — regenere com: morph-spec graph build`,
32
+ details: [loaded.path],
33
+ };
34
+ }
35
+
36
+ const { graph } = loaded;
37
+ const details = [`${graph.nodes.length} nós, ${graph.links.length} arestas`];
38
+ let status = 'ok';
39
+
40
+ const freshness = getFreshness(projectPath, graph);
41
+ if (freshness.state === 'stale') {
42
+ status = 'warn';
43
+ details.push(
44
+ `stale: construído em ${freshness.builtAtCommit.slice(0, 8)}, HEAD é ${freshness.headCommit.slice(0, 8)} — morph-spec graph refresh`,
45
+ );
46
+ }
47
+
48
+ if (!installed) {
49
+ status = 'warn';
50
+ details.push(`graphify não está no PATH (pinado: ${GRAPHIFY_VERSION})`);
51
+ } else if (installed !== GRAPHIFY_VERSION) {
52
+ status = 'warn';
53
+ details.push(`graphify ${installed} instalado; ${GRAPHIFY_VERSION} é a versão pinada e medida`);
54
+ }
55
+
56
+ return {
57
+ status,
58
+ msg: status === 'ok' ? details[0] : details[details.length - 1],
59
+ details,
60
+ };
61
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Leitura fail-open do graph.json produzido pelo graphify.
3
+ *
4
+ * Medido em 2026-08-27 sobre o MORPH_GHLBrain (11,46 MB, 6.829 nós, 15.668
5
+ * arestas): JSON.parse leva 52 ms e 28 MB de heap. Streaming e cache são
6
+ * desnecessários nesse porte — se um dia deixarem de ser, a troca acontece só
7
+ * aqui, porque este módulo é a única porta de leitura do grafo.
8
+ *
9
+ * Nada aqui lança: ausência de grafo é um estado válido do projeto (ADR-002).
10
+ *
11
+ * @module graph-loader
12
+ */
13
+ import { readFileSync, existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { execFileSync } from 'node:child_process';
16
+
17
+ /**
18
+ * @param {string} projectPath
19
+ * @returns {string} caminho canônico do graph.json do projeto
20
+ */
21
+ export function resolveGraphPath(projectPath) {
22
+ return join(projectPath, 'graphify-out', 'graph.json');
23
+ }
24
+
25
+ /**
26
+ * @param {string} projectPath
27
+ * @returns {{ok: boolean, reason?: 'missing'|'unreadable'|'malformed', graph?: object, path: string}}
28
+ */
29
+ export function loadGraph(projectPath) {
30
+ const path = resolveGraphPath(projectPath);
31
+ if (!existsSync(path)) return { ok: false, reason: 'missing', path };
32
+
33
+ let raw;
34
+ try {
35
+ raw = readFileSync(path, 'utf-8');
36
+ } catch {
37
+ return { ok: false, reason: 'unreadable', path };
38
+ }
39
+
40
+ try {
41
+ const graph = JSON.parse(raw.replace(/^/, ''));
42
+ // A chave das arestas é `links`, não `edges`. Um JSON válido sem uma das
43
+ // duas listas não é um grafo utilizável.
44
+ if (!graph || !Array.isArray(graph.nodes) || !Array.isArray(graph.links)) {
45
+ return { ok: false, reason: 'malformed', path };
46
+ }
47
+ return { ok: true, graph, path };
48
+ } catch {
49
+ return { ok: false, reason: 'malformed', path };
50
+ }
51
+ }
52
+
53
+ function readHead(projectPath) {
54
+ try {
55
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
56
+ cwd: projectPath,
57
+ encoding: 'utf-8',
58
+ stdio: ['ignore', 'pipe', 'ignore'],
59
+ }).trim() || null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * O hook post-commit do graphify rebuilda em BACKGROUND: logo depois de um
67
+ * commit o grafo ainda é o do commit anterior. Quem consome precisa saber, ou
68
+ * conclui "ninguém usa isso" a partir de um grafo velho.
69
+ *
70
+ * @param {string} projectPath
71
+ * @param {object} graph
72
+ * @param {{_head?: (projectPath: string) => string|null}} [deps]
73
+ * @returns {{builtAtCommit: string|null, headCommit: string|null, state: 'fresh'|'stale'|'unknown'}}
74
+ */
75
+ export function getFreshness(projectPath, graph, deps = {}) {
76
+ const headCommit = (deps._head ?? readHead)(projectPath);
77
+ const builtAtCommit = graph?.built_at_commit ?? null;
78
+ if (!headCommit || !builtAtCommit) {
79
+ return { builtAtCommit, headCommit, state: 'unknown' };
80
+ }
81
+ return {
82
+ builtAtCommit,
83
+ headCommit,
84
+ state: builtAtCommit === headCommit ? 'fresh' : 'stale',
85
+ };
86
+ }