@polymorphism-tech/morph-spec 8.29.0 → 8.32.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 +11 -1
- package/README.md +13 -2
- package/bin/morph-spec.js +67 -1
- 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 +4 -1
- package/framework/MORPH.md +4 -3
- package/framework/agents.json +6 -6
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +68 -45
- 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 +39 -4
- 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 +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +14 -4
- 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 +2 -2
- 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 +12 -4
- 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/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/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/branch.js +170 -0
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +148 -9
- package/src/commands/project/fleet.js +10 -3
- package/src/commands/project/worktree.js +73 -50
- package/src/commands/state/finish.js +41 -26
- package/src/commands/state/list.js +79 -6
- package/src/core/paths/output-schema.js +7 -0
- package/src/core/state/lifecycle-audit.js +127 -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/git/branch-classifier.js +177 -0
- package/src/lib/git/gh.js +30 -0
- package/src/lib/git/pending-prs.js +89 -0
- package/src/lib/git/pr-index.js +70 -0
- 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/lib/worktree/fleet-scanner.js +51 -21
- package/src/lib/worktree/ignored-triage.js +197 -0
- package/src/lib/worktree/worktree-audit.js +200 -35
- 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,217 +1,217 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Project Detector
|
|
3
|
-
*
|
|
4
|
-
* Scans a project directory for stack, architecture, UI library, and integrations.
|
|
5
|
-
* Used by mcp-setup, morph-init, and session-start hooks.
|
|
6
|
-
*
|
|
7
|
-
* @module detectors/index
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Detect project structure from filesystem evidence.
|
|
15
|
-
*
|
|
16
|
-
* @param {string} targetPath - Root path of the project
|
|
17
|
-
* @param {Object} [opts] - Options
|
|
18
|
-
* @param {boolean} [opts.generateStandards=false] - Whether to generate inferred markdown
|
|
19
|
-
* @returns {{ structure: { stack: string, architecture: string|null, uiLibrary: string|null, integrations: string[], hasVercel: boolean }, inferred: { markdown: string } }}
|
|
20
|
-
*/
|
|
21
|
-
export function detectProject(targetPath, opts = {}) {
|
|
22
|
-
const structure = {
|
|
23
|
-
stack: 'unknown',
|
|
24
|
-
architecture: null,
|
|
25
|
-
uiLibrary: null,
|
|
26
|
-
integrations: [],
|
|
27
|
-
hasVercel: false,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
// ── Package.json analysis ──────────────────────────────────────────────
|
|
32
|
-
// Collect deps from root and common monorepo subfolders
|
|
33
|
-
const pkgLocations = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
|
|
34
|
-
const allDeps = {};
|
|
35
|
-
let rootPkg = null;
|
|
36
|
-
let scripts = [];
|
|
37
|
-
for (const loc of pkgLocations) {
|
|
38
|
-
const pkgPath = join(targetPath, loc, 'package.json');
|
|
39
|
-
if (!existsSync(pkgPath)) continue;
|
|
40
|
-
try {
|
|
41
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
42
|
-
if (loc === '.') rootPkg = pkg;
|
|
43
|
-
Object.assign(allDeps, pkg.dependencies, pkg.devDependencies);
|
|
44
|
-
scripts.push(...Object.keys(pkg.scripts || {}));
|
|
45
|
-
// Track frontend path for config
|
|
46
|
-
if (loc !== '.' && (pkg.dependencies?.next || pkg.dependencies?.react || pkg.dependencies?.vue)) {
|
|
47
|
-
structure.frontendPath = loc;
|
|
48
|
-
}
|
|
49
|
-
} catch { /* invalid JSON */ }
|
|
50
|
-
}
|
|
51
|
-
const depNames = Object.keys(allDeps);
|
|
52
|
-
|
|
53
|
-
if (depNames.length > 0) {
|
|
54
|
-
|
|
55
|
-
// Stack detection from deps
|
|
56
|
-
if (depNames.includes('next')) {
|
|
57
|
-
structure.stack = 'nextjs';
|
|
58
|
-
} else if (depNames.includes('react')) {
|
|
59
|
-
structure.stack = 'react';
|
|
60
|
-
} else if (depNames.includes('vue')) {
|
|
61
|
-
structure.stack = 'vue';
|
|
62
|
-
} else if (depNames.includes('express') || depNames.includes('fastify') || depNames.includes('hono')) {
|
|
63
|
-
structure.stack = 'nodejs';
|
|
64
|
-
} else if (rootPkg?.name) {
|
|
65
|
-
structure.stack = 'nodejs';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// UI library detection
|
|
69
|
-
if (existsSync(join(targetPath, 'components.json')) || existsSync(join(targetPath, structure.frontendPath || '.', 'components.json')) || depNames.includes('@shadcn/ui')) {
|
|
70
|
-
structure.uiLibrary = 'shadcn';
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Integrations
|
|
74
|
-
if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
|
|
75
|
-
structure.integrations.push('neon');
|
|
76
|
-
}
|
|
77
|
-
if (depNames.includes('@neondatabase/auth')) {
|
|
78
|
-
structure.integrations.push('neon-auth');
|
|
79
|
-
}
|
|
80
|
-
if (depNames.includes('stripe')) {
|
|
81
|
-
structure.integrations.push('stripe');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Vercel detection
|
|
85
|
-
if (depNames.includes('vercel') || scripts.includes('vercel')) {
|
|
86
|
-
structure.hasVercel = true;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── .csproj detection ──────────────────────────────────────────────────
|
|
91
|
-
let hasDotnet = false;
|
|
92
|
-
const csprojLocations = ['.', 'src', 'src/Api', 'src/Web', 'src/backend'];
|
|
93
|
-
|
|
94
|
-
function checkCsproj(filePath) {
|
|
95
|
-
hasDotnet = true;
|
|
96
|
-
try {
|
|
97
|
-
const content = readFileSync(filePath, 'utf8');
|
|
98
|
-
if (content.includes('Hangfire')) {
|
|
99
|
-
if (!structure.integrations.includes('hangfire')) structure.integrations.push('hangfire');
|
|
100
|
-
}
|
|
101
|
-
} catch { /* ignore */ }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
for (const loc of csprojLocations) {
|
|
105
|
-
try {
|
|
106
|
-
const dir = join(targetPath, loc);
|
|
107
|
-
if (!existsSync(dir)) continue;
|
|
108
|
-
const entries = readdirSyncSafe(dir);
|
|
109
|
-
// Direct .csproj files
|
|
110
|
-
for (const e of entries.filter(f => f.endsWith('.csproj'))) {
|
|
111
|
-
checkCsproj(join(dir, e));
|
|
112
|
-
}
|
|
113
|
-
// One level deeper (e.g., src/backend/MyProject/*.csproj)
|
|
114
|
-
for (const sub of entries) {
|
|
115
|
-
const subDir = join(dir, sub);
|
|
116
|
-
for (const e of readdirSyncSafe(subDir).filter(f => f.endsWith('.csproj'))) {
|
|
117
|
-
checkCsproj(join(subDir, e));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
} catch { /* ignore */ }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Determine stack: monorepo (nextjs+dotnet), dotnet, or keep frontend stack
|
|
124
|
-
if (hasDotnet) {
|
|
125
|
-
if (structure.stack === 'nextjs' || structure.stack === 'react' || structure.stack === 'vue') {
|
|
126
|
-
// Monorepo: frontend + .NET backend
|
|
127
|
-
structure.stack = `${structure.stack}+dotnet`;
|
|
128
|
-
} else if (structure.stack === 'unknown') {
|
|
129
|
-
structure.stack = 'dotnet';
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ── vercel.json detection ──────────────────────────────────────────────
|
|
134
|
-
if (existsSync(join(targetPath, 'vercel.json'))) {
|
|
135
|
-
structure.hasVercel = true;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ── tsconfig.json detection (fallback for stack) ────────────────────────
|
|
139
|
-
if (structure.stack === 'unknown' && existsSync(join(targetPath, 'tsconfig.json'))) {
|
|
140
|
-
structure.stack = 'nodejs';
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ── Architecture detection ─────────────────────────────────────────────
|
|
144
|
-
try {
|
|
145
|
-
const configPath = join(targetPath, '.morph/config/config.json');
|
|
146
|
-
if (existsSync(configPath)) {
|
|
147
|
-
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
148
|
-
if (config.architecture?.style) {
|
|
149
|
-
structure.architecture = config.architecture.style;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
} catch { /* ignore */ }
|
|
153
|
-
|
|
154
|
-
// ── Docker detection ───────────────────────────────────────────────────
|
|
155
|
-
if (existsSync(join(targetPath, 'docker-compose.yml')) ||
|
|
156
|
-
existsSync(join(targetPath, 'docker-compose.yaml')) ||
|
|
157
|
-
existsSync(join(targetPath, 'Dockerfile'))) {
|
|
158
|
-
if (!structure.integrations.includes('docker')) structure.integrations.push('docker');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ── Azure detection ────────────────────────────────────────────────────
|
|
162
|
-
const azureSignals = [
|
|
163
|
-
'.azure',
|
|
164
|
-
'azure-pipelines.yml',
|
|
165
|
-
'infra/main.bicep',
|
|
166
|
-
'infra',
|
|
167
|
-
];
|
|
168
|
-
const hasAzure = azureSignals.some(signal => {
|
|
169
|
-
const p = join(targetPath, signal);
|
|
170
|
-
if (!existsSync(p)) return false;
|
|
171
|
-
if (signal === 'infra') {
|
|
172
|
-
// Check for .bicep files inside infra/
|
|
173
|
-
return readdirSyncSafe(p).some(f => f.endsWith('.bicep'));
|
|
174
|
-
}
|
|
175
|
-
return true;
|
|
176
|
-
});
|
|
177
|
-
if (hasAzure && !structure.integrations.includes('azure')) {
|
|
178
|
-
structure.integrations.push('azure');
|
|
179
|
-
}
|
|
180
|
-
} catch {
|
|
181
|
-
// Fail-safe — return defaults
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
structure,
|
|
186
|
-
inferred: {
|
|
187
|
-
markdown: opts.generateStandards ? generateInferredMarkdown(structure) : '',
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Safe readdir that returns [] on error.
|
|
194
|
-
* @param {string} dir
|
|
195
|
-
* @returns {string[]}
|
|
196
|
-
*/
|
|
197
|
-
function readdirSyncSafe(dir) {
|
|
198
|
-
try {
|
|
199
|
-
return readdirSync(dir);
|
|
200
|
-
} catch {
|
|
201
|
-
return [];
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Generate a markdown summary from detected structure.
|
|
207
|
-
* @param {Object} structure
|
|
208
|
-
* @returns {string}
|
|
209
|
-
*/
|
|
210
|
-
function generateInferredMarkdown(structure) {
|
|
211
|
-
const lines = [`## Detected Stack: ${structure.stack}`];
|
|
212
|
-
if (structure.architecture) lines.push(`Architecture: ${structure.architecture}`);
|
|
213
|
-
if (structure.uiLibrary) lines.push(`UI Library: ${structure.uiLibrary}`);
|
|
214
|
-
if (structure.integrations.length > 0) lines.push(`Integrations: ${structure.integrations.join(', ')}`);
|
|
215
|
-
if (structure.hasVercel) lines.push(`Vercel: detected`);
|
|
216
|
-
return lines.join('\n');
|
|
217
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Project Detector
|
|
3
|
+
*
|
|
4
|
+
* Scans a project directory for stack, architecture, UI library, and integrations.
|
|
5
|
+
* Used by mcp-setup, morph-init, and session-start hooks.
|
|
6
|
+
*
|
|
7
|
+
* @module detectors/index
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Detect project structure from filesystem evidence.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} targetPath - Root path of the project
|
|
17
|
+
* @param {Object} [opts] - Options
|
|
18
|
+
* @param {boolean} [opts.generateStandards=false] - Whether to generate inferred markdown
|
|
19
|
+
* @returns {{ structure: { stack: string, architecture: string|null, uiLibrary: string|null, integrations: string[], hasVercel: boolean }, inferred: { markdown: string } }}
|
|
20
|
+
*/
|
|
21
|
+
export function detectProject(targetPath, opts = {}) {
|
|
22
|
+
const structure = {
|
|
23
|
+
stack: 'unknown',
|
|
24
|
+
architecture: null,
|
|
25
|
+
uiLibrary: null,
|
|
26
|
+
integrations: [],
|
|
27
|
+
hasVercel: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// ── Package.json analysis ──────────────────────────────────────────────
|
|
32
|
+
// Collect deps from root and common monorepo subfolders
|
|
33
|
+
const pkgLocations = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
|
|
34
|
+
const allDeps = {};
|
|
35
|
+
let rootPkg = null;
|
|
36
|
+
let scripts = [];
|
|
37
|
+
for (const loc of pkgLocations) {
|
|
38
|
+
const pkgPath = join(targetPath, loc, 'package.json');
|
|
39
|
+
if (!existsSync(pkgPath)) continue;
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
42
|
+
if (loc === '.') rootPkg = pkg;
|
|
43
|
+
Object.assign(allDeps, pkg.dependencies, pkg.devDependencies);
|
|
44
|
+
scripts.push(...Object.keys(pkg.scripts || {}));
|
|
45
|
+
// Track frontend path for config
|
|
46
|
+
if (loc !== '.' && (pkg.dependencies?.next || pkg.dependencies?.react || pkg.dependencies?.vue)) {
|
|
47
|
+
structure.frontendPath = loc;
|
|
48
|
+
}
|
|
49
|
+
} catch { /* invalid JSON */ }
|
|
50
|
+
}
|
|
51
|
+
const depNames = Object.keys(allDeps);
|
|
52
|
+
|
|
53
|
+
if (depNames.length > 0) {
|
|
54
|
+
|
|
55
|
+
// Stack detection from deps
|
|
56
|
+
if (depNames.includes('next')) {
|
|
57
|
+
structure.stack = 'nextjs';
|
|
58
|
+
} else if (depNames.includes('react')) {
|
|
59
|
+
structure.stack = 'react';
|
|
60
|
+
} else if (depNames.includes('vue')) {
|
|
61
|
+
structure.stack = 'vue';
|
|
62
|
+
} else if (depNames.includes('express') || depNames.includes('fastify') || depNames.includes('hono')) {
|
|
63
|
+
structure.stack = 'nodejs';
|
|
64
|
+
} else if (rootPkg?.name) {
|
|
65
|
+
structure.stack = 'nodejs';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// UI library detection
|
|
69
|
+
if (existsSync(join(targetPath, 'components.json')) || existsSync(join(targetPath, structure.frontendPath || '.', 'components.json')) || depNames.includes('@shadcn/ui')) {
|
|
70
|
+
structure.uiLibrary = 'shadcn';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Integrations
|
|
74
|
+
if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
|
|
75
|
+
structure.integrations.push('neon');
|
|
76
|
+
}
|
|
77
|
+
if (depNames.includes('@neondatabase/auth')) {
|
|
78
|
+
structure.integrations.push('neon-auth');
|
|
79
|
+
}
|
|
80
|
+
if (depNames.includes('stripe')) {
|
|
81
|
+
structure.integrations.push('stripe');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Vercel detection
|
|
85
|
+
if (depNames.includes('vercel') || scripts.includes('vercel')) {
|
|
86
|
+
structure.hasVercel = true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── .csproj detection ──────────────────────────────────────────────────
|
|
91
|
+
let hasDotnet = false;
|
|
92
|
+
const csprojLocations = ['.', 'src', 'src/Api', 'src/Web', 'src/backend'];
|
|
93
|
+
|
|
94
|
+
function checkCsproj(filePath) {
|
|
95
|
+
hasDotnet = true;
|
|
96
|
+
try {
|
|
97
|
+
const content = readFileSync(filePath, 'utf8');
|
|
98
|
+
if (content.includes('Hangfire')) {
|
|
99
|
+
if (!structure.integrations.includes('hangfire')) structure.integrations.push('hangfire');
|
|
100
|
+
}
|
|
101
|
+
} catch { /* ignore */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const loc of csprojLocations) {
|
|
105
|
+
try {
|
|
106
|
+
const dir = join(targetPath, loc);
|
|
107
|
+
if (!existsSync(dir)) continue;
|
|
108
|
+
const entries = readdirSyncSafe(dir);
|
|
109
|
+
// Direct .csproj files
|
|
110
|
+
for (const e of entries.filter(f => f.endsWith('.csproj'))) {
|
|
111
|
+
checkCsproj(join(dir, e));
|
|
112
|
+
}
|
|
113
|
+
// One level deeper (e.g., src/backend/MyProject/*.csproj)
|
|
114
|
+
for (const sub of entries) {
|
|
115
|
+
const subDir = join(dir, sub);
|
|
116
|
+
for (const e of readdirSyncSafe(subDir).filter(f => f.endsWith('.csproj'))) {
|
|
117
|
+
checkCsproj(join(subDir, e));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch { /* ignore */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Determine stack: monorepo (nextjs+dotnet), dotnet, or keep frontend stack
|
|
124
|
+
if (hasDotnet) {
|
|
125
|
+
if (structure.stack === 'nextjs' || structure.stack === 'react' || structure.stack === 'vue') {
|
|
126
|
+
// Monorepo: frontend + .NET backend
|
|
127
|
+
structure.stack = `${structure.stack}+dotnet`;
|
|
128
|
+
} else if (structure.stack === 'unknown') {
|
|
129
|
+
structure.stack = 'dotnet';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── vercel.json detection ──────────────────────────────────────────────
|
|
134
|
+
if (existsSync(join(targetPath, 'vercel.json'))) {
|
|
135
|
+
structure.hasVercel = true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── tsconfig.json detection (fallback for stack) ────────────────────────
|
|
139
|
+
if (structure.stack === 'unknown' && existsSync(join(targetPath, 'tsconfig.json'))) {
|
|
140
|
+
structure.stack = 'nodejs';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Architecture detection ─────────────────────────────────────────────
|
|
144
|
+
try {
|
|
145
|
+
const configPath = join(targetPath, '.morph/config/config.json');
|
|
146
|
+
if (existsSync(configPath)) {
|
|
147
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
148
|
+
if (config.architecture?.style) {
|
|
149
|
+
structure.architecture = config.architecture.style;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch { /* ignore */ }
|
|
153
|
+
|
|
154
|
+
// ── Docker detection ───────────────────────────────────────────────────
|
|
155
|
+
if (existsSync(join(targetPath, 'docker-compose.yml')) ||
|
|
156
|
+
existsSync(join(targetPath, 'docker-compose.yaml')) ||
|
|
157
|
+
existsSync(join(targetPath, 'Dockerfile'))) {
|
|
158
|
+
if (!structure.integrations.includes('docker')) structure.integrations.push('docker');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Azure detection ────────────────────────────────────────────────────
|
|
162
|
+
const azureSignals = [
|
|
163
|
+
'.azure',
|
|
164
|
+
'azure-pipelines.yml',
|
|
165
|
+
'infra/main.bicep',
|
|
166
|
+
'infra',
|
|
167
|
+
];
|
|
168
|
+
const hasAzure = azureSignals.some(signal => {
|
|
169
|
+
const p = join(targetPath, signal);
|
|
170
|
+
if (!existsSync(p)) return false;
|
|
171
|
+
if (signal === 'infra') {
|
|
172
|
+
// Check for .bicep files inside infra/
|
|
173
|
+
return readdirSyncSafe(p).some(f => f.endsWith('.bicep'));
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
});
|
|
177
|
+
if (hasAzure && !structure.integrations.includes('azure')) {
|
|
178
|
+
structure.integrations.push('azure');
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// Fail-safe — return defaults
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
structure,
|
|
186
|
+
inferred: {
|
|
187
|
+
markdown: opts.generateStandards ? generateInferredMarkdown(structure) : '',
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Safe readdir that returns [] on error.
|
|
194
|
+
* @param {string} dir
|
|
195
|
+
* @returns {string[]}
|
|
196
|
+
*/
|
|
197
|
+
function readdirSyncSafe(dir) {
|
|
198
|
+
try {
|
|
199
|
+
return readdirSync(dir);
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generate a markdown summary from detected structure.
|
|
207
|
+
* @param {Object} structure
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
function generateInferredMarkdown(structure) {
|
|
211
|
+
const lines = [`## Detected Stack: ${structure.stack}`];
|
|
212
|
+
if (structure.architecture) lines.push(`Architecture: ${structure.architecture}`);
|
|
213
|
+
if (structure.uiLibrary) lines.push(`UI Library: ${structure.uiLibrary}`);
|
|
214
|
+
if (structure.integrations.length > 0) lines.push(`Integrations: ${structure.integrations.join(', ')}`);
|
|
215
|
+
if (structure.hasVercel) lines.push(`Vercel: detected`);
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
@@ -18,6 +18,7 @@ import fs from 'fs/promises';
|
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import { existsSync, readFileSync } from 'fs';
|
|
21
|
+
import { scanGraph } from '../graph/god-nodes.js';
|
|
21
22
|
import { renderTemplate } from '../../core/templates/template-renderer.js';
|
|
22
23
|
|
|
23
24
|
const SKIP_DIRS = new Set(['node_modules', 'bin', 'obj', '.git', '.morph', '.next', 'dist', 'out', 'worktrees', '.worktrees']);
|
|
@@ -435,13 +436,17 @@ export async function scanStackDependencies(projectPath) {
|
|
|
435
436
|
* @returns {Promise<Object>} ArchitectureScanResult
|
|
436
437
|
*/
|
|
437
438
|
export async function scanArchitecture(projectPath) {
|
|
438
|
-
const [entities, endpoints, nextjs, integrations, database, stack] = await Promise.all([
|
|
439
|
+
const [entities, endpoints, nextjs, integrations, database, stack, graph] = await Promise.all([
|
|
439
440
|
scanEntities(projectPath),
|
|
440
441
|
scanEndpoints(projectPath),
|
|
441
442
|
scanNextjsComponents(projectPath),
|
|
442
443
|
scanIntegrations(projectPath),
|
|
443
444
|
scanDatabaseSchema(projectPath),
|
|
444
|
-
scanStackDependencies(projectPath)
|
|
445
|
+
scanStackDependencies(projectPath),
|
|
446
|
+
// Aditivo: sem grafo devolve listas vazias e nada muda no README. O
|
|
447
|
+
// inventário acima continua sendo a espinha do arquivo — ele tem
|
|
448
|
+
// exaustividade que o grafo não tem.
|
|
449
|
+
scanGraph(projectPath)
|
|
445
450
|
]);
|
|
446
451
|
|
|
447
452
|
return {
|
|
@@ -451,7 +456,8 @@ export async function scanArchitecture(projectPath) {
|
|
|
451
456
|
nextjs,
|
|
452
457
|
integrations,
|
|
453
458
|
database,
|
|
454
|
-
stack
|
|
459
|
+
stack,
|
|
460
|
+
graph
|
|
455
461
|
};
|
|
456
462
|
}
|
|
457
463
|
|
|
@@ -496,6 +502,17 @@ function cliRootDir() {
|
|
|
496
502
|
/**
|
|
497
503
|
* Render architecture context to .morph/context/README.md
|
|
498
504
|
*
|
|
505
|
+
* QUEM LÊ ESTE ARQUIVO, E QUANDO: o passo 1c da skill `morph-plan` (mapa de
|
|
506
|
+
* reuso), antes de decidir REUSAR/ESTENDER/CRIAR cada conceito da feature. É a
|
|
507
|
+
* granularidade de CONCEITO — entidades, endpoints com rota, componentes com
|
|
508
|
+
* path — que o grafo de codebase, que casa por símbolo, não dá; e é o único
|
|
509
|
+
* inventário legível num projeto sem grafo (sem Python), antes do grep.
|
|
510
|
+
*
|
|
511
|
+
* Até 2026-08-27 este arquivo era gerado e NUNCA lido por skill nenhuma
|
|
512
|
+
* (docs/specs/code-reuse-gap/analysis.md §2.5). Se um dia o consumidor acima
|
|
513
|
+
* desaparecer do `morph-plan`, este gerador volta a queimar tempo por nada —
|
|
514
|
+
* a decisão então é remover os dois, não deixar o arquivo órfão de novo.
|
|
515
|
+
*
|
|
499
516
|
* @param {string} projectPath
|
|
500
517
|
* @param {Object} scanResult - Result from scanArchitecture()
|
|
501
518
|
* @param {Object} [options]
|
|
@@ -559,7 +576,14 @@ export async function renderArchitectureContext(projectPath, scanResult, options
|
|
|
559
576
|
hasDotnetPackages: scanResult.stack.dotnet.length > 0,
|
|
560
577
|
dotnetPackages: scanResult.stack.dotnet,
|
|
561
578
|
hasNpmPackages: scanResult.stack.npm.length > 0,
|
|
562
|
-
npmPackages: scanResult.stack.npm
|
|
579
|
+
npmPackages: scanResult.stack.npm,
|
|
580
|
+
// Centralidade (grafo de codebase) — ACRESCENTA, nunca substitui.
|
|
581
|
+
// Comunidade ainda com rótulo Community_N fica de fora: sem
|
|
582
|
+
// `graphify label` ela é ruído puro (441 páginas assim no eval).
|
|
583
|
+
hasGraph: scanResult.graph?.available === true && (scanResult.graph.godNodes?.length ?? 0) > 0,
|
|
584
|
+
godNodes: scanResult.graph?.godNodes ?? [],
|
|
585
|
+
communities: (scanResult.graph?.communities ?? []).filter((c) => c.labeled),
|
|
586
|
+
hasLabeledCommunities: (scanResult.graph?.communities ?? []).some((c) => c.labeled)
|
|
563
587
|
};
|
|
564
588
|
|
|
565
589
|
let content;
|
|
@@ -647,6 +671,23 @@ function generateFallbackMarkdown(vars) {
|
|
|
647
671
|
lines.push('');
|
|
648
672
|
}
|
|
649
673
|
|
|
674
|
+
if (vars.hasGraph) {
|
|
675
|
+
lines.push('## Centralidade (grafo de codebase)\n');
|
|
676
|
+
lines.push('| Símbolo | Arquivo | Grau |');
|
|
677
|
+
lines.push('|---------|---------|------|');
|
|
678
|
+
for (const n of vars.godNodes) {
|
|
679
|
+
lines.push(`| \`${n.label}\` | \`${n.source_file}\` | ${n.degree} |`);
|
|
680
|
+
}
|
|
681
|
+
lines.push('');
|
|
682
|
+
if (vars.hasLabeledCommunities) {
|
|
683
|
+
lines.push('### Comunidades\n');
|
|
684
|
+
for (const c of vars.communities) {
|
|
685
|
+
lines.push(`- **${c.label}** (${c.size} nós)`);
|
|
686
|
+
}
|
|
687
|
+
lines.push('');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
650
691
|
lines.push(`---\n*Generated by morph-spec v${vars.morphVersion}*`);
|
|
651
692
|
return lines.join('\n');
|
|
652
693
|
}
|