@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.
- package/CLAUDE.md +6 -1
- package/README.md +13 -2
- package/bin/morph-spec.js +48 -0
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -0
- package/framework/MORPH.md +2 -1
- package/framework/agents.json +20 -9
- package/framework/commands/create-umbraco-lp.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +18 -0
- package/framework/evals/rubrics/code-quality.md +17 -0
- package/framework/evals/rubrics/condensed.md +3 -1
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/phase-utils.js +2 -0
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/mcp/registry.json +97 -11
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/schemas/tasks.schema.json +25 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +13 -2
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +598 -0
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +92 -7
- package/framework/skills/core/morph-apply/SKILL.md +12 -3
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +12 -2
- package/framework/skills/core/morph-plan/SKILL.md +45 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +11 -3
- package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/PROVENANCE.json +41 -0
- package/framework/standards/STANDARDS.json +218 -7
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +231 -231
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/standards/frontend/design-system/ai-image-generation.md +43 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +113 -0
- package/framework/standards/frontend/design-system/premium-finish.md +160 -0
- package/framework/standards/frontend/gsap/gsap-core.md +1 -1
- package/framework/standards/frontend/gsap/gsap-plugins.md +1 -1
- package/framework/standards/frontend/gsap/gsap-scrolltrigger.md +3 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +3 -1
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +287 -0
- package/framework/standards/frontend/scroll-driven/scroll-components.md +256 -0
- package/framework/standards/frontend/scroll-driven/smooth-scroll.md +148 -0
- package/framework/standards/frontend/seo/meta-schema-audit.md +134 -0
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/context/ARCHITECTURE.md +20 -0
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/graph/affected.js +89 -0
- package/src/commands/graph/build.js +94 -0
- package/src/commands/graph/explain.js +96 -0
- package/src/commands/graph/status.js +93 -0
- package/src/commands/index.js +13 -13
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +11 -0
- package/src/core/paths/output-schema.js +7 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +45 -4
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/graph/god-nodes.js +75 -0
- package/src/lib/graph/graph-health.js +61 -0
- package/src/lib/graph/graph-loader.js +86 -0
- package/src/lib/graph/graphify-runtime.js +100 -0
- package/src/lib/graph/graphifyignore.js +86 -0
- package/src/lib/graph/sanitize.js +280 -0
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +26 -0
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/index.js +10 -0
- package/src/lib/validators/reuse/reuse-validator.js +324 -0
- package/src/lib/validators/reuse/symbol-scanner.js +144 -0
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +40 -1
- package/src/scripts/setup-infra.js +60 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +40 -0
- 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
|
+
}
|