@polymorphism-tech/morph-spec 8.35.1 → 8.36.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 +9 -7
- package/README.md +2 -2
- package/bin/morph-spec.js +2 -1
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +24 -23
- package/framework/docs/CLI.md +184 -10
- package/framework/evals/proof-rubrics/eval-de-modelo.md +16 -0
- package/framework/evals/rubrics/condensed.md +3 -1
- package/framework/evals/rubrics/test-coverage.md +8 -0
- package/framework/hooks/shared/phase-utils.js +1 -0
- package/framework/phases.json +1 -1
- package/framework/rules/morph-workflow.md +1 -1
- package/framework/schemas/config.schema.json +7 -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 +21 -10
- 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 +20 -3
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +41 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +11 -2
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +2 -2
- package/framework/standards/ai-agents/prompt-sources.md +4 -3
- package/framework/standards/ai-agents/testing-ai.md +14 -13
- package/framework/templates/dotnet/ai-kit/README.md +14 -8
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +11 -3
- package/package.json +1 -1
- package/src/commands/project/doctor.js +51 -2
- package/src/commands/tasks/dag.js +13 -1
- package/src/commands/validation/verify.js +47 -18
- package/src/core/paths/output-schema.js +7 -0
- package/src/lib/detectors/index.js +225 -217
- package/src/lib/risk-detector.js +38 -18
- package/src/lib/specs/done-criteria-check.js +200 -0
- package/src/lib/specs/usecase-check.js +129 -5
- package/src/lib/tasks/dotnet-test-introspection.js +517 -0
- package/src/lib/tasks/test-nodes.js +190 -20
- package/src/lib/tasks/test-runner.js +122 -2
- package/src/lib/tasks/test-scope.js +567 -0
- package/src/lib/validators/traceability-check.js +9 -2
- package/src/lib/validators/verify-runner.js +150 -18
- package/src/lib/worktree/docker-audit.js +5 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
3
|
import { resolve, join } from 'path';
|
|
4
4
|
import { getFeature, updateFeature } from '../../core/state/state-manager.js';
|
|
5
|
-
import {
|
|
6
|
-
import { runVerify, VerifyUsageError } from '../../lib/validators/verify-runner.js';
|
|
5
|
+
import { runVerify, readTask, VerifyUsageError } from '../../lib/validators/verify-runner.js';
|
|
7
6
|
import { resolvePrimaryRoot } from '../project/worktree.js';
|
|
8
7
|
import { getHeadSha } from '../../lib/git/default-branch.js';
|
|
9
8
|
import { EVAL_RUNNER } from '../../lib/tasks/eval-node.js';
|
|
@@ -15,6 +14,28 @@ function statusIcon(status) {
|
|
|
15
14
|
return chalk.gray('–');
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
/**
|
|
18
|
+
* One line per test run naming what the task-scope recorte did to it (#87):
|
|
19
|
+
* narrowed (how many selectors, from how many test files), whole, out of scope
|
|
20
|
+
* or full, each with its reason. A green `tests` line alone cannot say that the
|
|
21
|
+
* task ran 12 tests instead of 3.082. Feature-scope runs carry no selection and
|
|
22
|
+
* print nothing extra; `feedback[]` is untouched either way.
|
|
23
|
+
*
|
|
24
|
+
* @param {Array<{id: string, selection: object|null}>} [runs]
|
|
25
|
+
* @returns {string[]}
|
|
26
|
+
*/
|
|
27
|
+
function selectionLines(runs = []) {
|
|
28
|
+
return runs
|
|
29
|
+
.filter((run) => run.selection)
|
|
30
|
+
.map((run) => {
|
|
31
|
+
const { mode, reason, selectors = [], files = [] } = run.selection;
|
|
32
|
+
const detail = mode === 'narrowed'
|
|
33
|
+
? `${selectors.length} selector(s) from ${files.length} test file(s)`
|
|
34
|
+
: (reason ?? '');
|
|
35
|
+
return chalk.gray(` ↳ ${run.id.padEnd(10)} ${mode.padEnd(12)} ${detail}`);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
18
39
|
/**
|
|
19
40
|
* Human-readable rendering of a runVerify() result — mirrors the
|
|
20
41
|
* ✓/✗-per-node + feedback-list style of formatScoreBreakdown() in
|
|
@@ -51,6 +72,7 @@ function formatVerifyResult(result) {
|
|
|
51
72
|
detail = `${node.durationMs}ms`;
|
|
52
73
|
}
|
|
53
74
|
lines.push(` ${icon} ${name.padEnd(11)} ${node.status.padEnd(5)} ${chalk.gray(detail)}`);
|
|
75
|
+
if (name === 'tests') lines.push(...selectionLines(node.runs));
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
if (result.feedback.length > 0) {
|
|
@@ -166,17 +188,6 @@ function evalsStatusOf(result) {
|
|
|
166
188
|
return 'skip';
|
|
167
189
|
}
|
|
168
190
|
|
|
169
|
-
/** Read tasks.json's task list for a feature; [] if missing/corrupt. */
|
|
170
|
-
function readTasks(featureName) {
|
|
171
|
-
try {
|
|
172
|
-
const tasksPath = getAbsoluteOutputPath(process.cwd(), featureName, 'tasks');
|
|
173
|
-
if (!existsSync(tasksPath)) return [];
|
|
174
|
-
return JSON.parse(readFileSync(tasksPath, 'utf8')).tasks || [];
|
|
175
|
-
} catch {
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
191
|
/**
|
|
181
192
|
* `morph-spec verify <feature> [task]` — the deterministic code-before-judge
|
|
182
193
|
* gate: build + tests + validators, BEFORE the LLM eval. See verify-runner.js
|
|
@@ -194,7 +205,8 @@ function readTasks(featureName) {
|
|
|
194
205
|
* `morph-spec score` gives for the same situation);
|
|
195
206
|
* - `--filter` that matches no declared node (a filtered-to-empty run is
|
|
196
207
|
* green and proves nothing);
|
|
197
|
-
* - `--record-baseline` with nowhere to write (no `config.json`)
|
|
208
|
+
* - `--record-baseline` with nowhere to write (no `config.json`), or with a
|
|
209
|
+
* task id (a recorte de task is never a fleet baseline, #87);
|
|
198
210
|
* - `--linux` without Docker.
|
|
199
211
|
* The last three arrive as `VerifyUsageError` from the runner.
|
|
200
212
|
*/
|
|
@@ -210,9 +222,10 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
210
222
|
process.exit(2);
|
|
211
223
|
}
|
|
212
224
|
|
|
225
|
+
// The runner's own reader (one reader of tasks.json for verify, #87): null
|
|
226
|
+
// means tasks.json or the task is missing, and both are this usage error.
|
|
213
227
|
if (taskId) {
|
|
214
|
-
|
|
215
|
-
if (!tasks.some((t) => t.id === taskId)) {
|
|
228
|
+
if (!readTask(process.cwd(), featureName, taskId)) {
|
|
216
229
|
console.error(chalk.red(`\n❌ Task "${taskId}" not found in tasks.json for feature "${featureName}"\n`));
|
|
217
230
|
process.exit(2);
|
|
218
231
|
}
|
|
@@ -232,6 +245,9 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
232
245
|
refreshCache: options.refreshCache,
|
|
233
246
|
linux: options.linux,
|
|
234
247
|
linuxImage: options.linuxImage,
|
|
248
|
+
// Task scope: switch the recorte off (#87). At feature scope nothing is
|
|
249
|
+
// ever narrowed, so the flag is a documented no-op there, not an error.
|
|
250
|
+
full: options.full,
|
|
235
251
|
});
|
|
236
252
|
} catch (err) {
|
|
237
253
|
// A wrong invocation is exit 2, like an unknown feature or task — never a
|
|
@@ -249,9 +265,22 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
249
265
|
// the suite in order to write `expectedCount`. Persisting a `verification`
|
|
250
266
|
// stamp from it would let a maintenance run stand in for the proof a gate
|
|
251
267
|
// asks for — the same class of confusion as a hand-typed baseline.
|
|
268
|
+
//
|
|
269
|
+
// `--filter` at FEATURE scope is the other side of the same hole (#87): the
|
|
270
|
+
// run is narrowed to a subset of nodes, but the stamp it would write carries
|
|
271
|
+
// `scope: 'feature'` — the one scope risk-detector.js trusts unconditionally.
|
|
272
|
+
// A partial run must not stand in for the proof a gate asks for, same as
|
|
273
|
+
// `--record-baseline` above. Task-scope `--filter` is unaffected: that stamp
|
|
274
|
+
// already says `scope: 'task:{id}'`, and `taskVerifications` never claimed
|
|
275
|
+
// to cover more than the task in the first place. Checked AFTER `--no-persist`
|
|
276
|
+
// (via the `!options.noPersist` guard) so an explicit `--no-persist` keeps its
|
|
277
|
+
// own reason instead of being reported as 'filtered' — the flags are both
|
|
278
|
+
// "don't write", but decidePersistence() already owns the former's reason.
|
|
252
279
|
const { persist, reason } = options.recordBaseline
|
|
253
280
|
? { persist: false, reason: 'record-baseline' }
|
|
254
|
-
:
|
|
281
|
+
: (!options.noPersist && !taskId && options.filter)
|
|
282
|
+
? { persist: false, reason: 'filtered' }
|
|
283
|
+
: decidePersistence({ cwd: process.cwd(), featureName, noPersist: options.noPersist });
|
|
255
284
|
|
|
256
285
|
let compact = null;
|
|
257
286
|
if (persist) {
|
|
@@ -72,6 +72,13 @@ export const OUTPUT_SCHEMA = {
|
|
|
72
72
|
|
|
73
73
|
// 3-implement/
|
|
74
74
|
recap: { filename: 'recap.md', phaseDir: '3-implement', phase: 'implement', protected: false },
|
|
75
|
+
// N files, one per task whose doneCriteria judges what a MODEL answers
|
|
76
|
+
// (morph-plan §5). Runtime evidence: input, model, output, verdict — pointing
|
|
77
|
+
// at the `runner: "evals"` node when the stack declares one. CONDITIONAL by
|
|
78
|
+
// construction: never in phases.json requiredOutputs. Readers: morph-review
|
|
79
|
+
// dispatch prompt, proof rubric eval-de-modelo, test-coverage Q7.
|
|
80
|
+
evidencias: { dirname: 'evidencias', filenamePattern: '^T\\d+\\.md$', displayFilename: '{taskId}.md',
|
|
81
|
+
phaseDir: '3-implement', phase: 'implement', protected: false, multi: true },
|
|
75
82
|
|
|
76
83
|
// 4-review/
|
|
77
84
|
reviewReport: { filename: 'review-report.md', phaseDir: '4-review', phase: 'review', protected: false },
|
|
@@ -1,217 +1,225 @@
|
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
structure.stack = '
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (depNames.includes('@
|
|
78
|
-
structure.
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (depNames.includes('
|
|
86
|
-
structure.
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
'
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
structure
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
* Common monorepo locations for a frontend `package.json`, root included.
|
|
15
|
+
*
|
|
16
|
+
* Exported so `discoverFrontendPackages()` (src/lib/tasks/test-nodes.js, #87
|
|
17
|
+
* PR B) scans the SAME candidate list this detector already uses — one
|
|
18
|
+
* spelling of "where a frontend package might live", not two that drift.
|
|
19
|
+
*/
|
|
20
|
+
export const PACKAGE_LOCATIONS = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect project structure from filesystem evidence.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} targetPath - Root path of the project
|
|
26
|
+
* @param {Object} [opts] - Options
|
|
27
|
+
* @param {boolean} [opts.generateStandards=false] - Whether to generate inferred markdown
|
|
28
|
+
* @returns {{ structure: { stack: string, architecture: string|null, uiLibrary: string|null, integrations: string[], hasVercel: boolean }, inferred: { markdown: string } }}
|
|
29
|
+
*/
|
|
30
|
+
export function detectProject(targetPath, opts = {}) {
|
|
31
|
+
const structure = {
|
|
32
|
+
stack: 'unknown',
|
|
33
|
+
architecture: null,
|
|
34
|
+
uiLibrary: null,
|
|
35
|
+
integrations: [],
|
|
36
|
+
hasVercel: false,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// ── Package.json analysis ──────────────────────────────────────────────
|
|
41
|
+
// Collect deps from root and common monorepo subfolders
|
|
42
|
+
const allDeps = {};
|
|
43
|
+
let rootPkg = null;
|
|
44
|
+
let scripts = [];
|
|
45
|
+
for (const loc of PACKAGE_LOCATIONS) {
|
|
46
|
+
const pkgPath = join(targetPath, loc, 'package.json');
|
|
47
|
+
if (!existsSync(pkgPath)) continue;
|
|
48
|
+
try {
|
|
49
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
50
|
+
if (loc === '.') rootPkg = pkg;
|
|
51
|
+
Object.assign(allDeps, pkg.dependencies, pkg.devDependencies);
|
|
52
|
+
scripts.push(...Object.keys(pkg.scripts || {}));
|
|
53
|
+
// Track frontend path for config
|
|
54
|
+
if (loc !== '.' && (pkg.dependencies?.next || pkg.dependencies?.react || pkg.dependencies?.vue)) {
|
|
55
|
+
structure.frontendPath = loc;
|
|
56
|
+
}
|
|
57
|
+
} catch { /* invalid JSON */ }
|
|
58
|
+
}
|
|
59
|
+
const depNames = Object.keys(allDeps);
|
|
60
|
+
|
|
61
|
+
if (depNames.length > 0) {
|
|
62
|
+
|
|
63
|
+
// Stack detection from deps
|
|
64
|
+
if (depNames.includes('next')) {
|
|
65
|
+
structure.stack = 'nextjs';
|
|
66
|
+
} else if (depNames.includes('react')) {
|
|
67
|
+
structure.stack = 'react';
|
|
68
|
+
} else if (depNames.includes('vue')) {
|
|
69
|
+
structure.stack = 'vue';
|
|
70
|
+
} else if (depNames.includes('express') || depNames.includes('fastify') || depNames.includes('hono')) {
|
|
71
|
+
structure.stack = 'nodejs';
|
|
72
|
+
} else if (rootPkg?.name) {
|
|
73
|
+
structure.stack = 'nodejs';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// UI library detection
|
|
77
|
+
if (existsSync(join(targetPath, 'components.json')) || existsSync(join(targetPath, structure.frontendPath || '.', 'components.json')) || depNames.includes('@shadcn/ui')) {
|
|
78
|
+
structure.uiLibrary = 'shadcn';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Integrations
|
|
82
|
+
if (depNames.includes('@neondatabase/serverless') || depNames.includes('postgres') || depNames.includes('pg')) {
|
|
83
|
+
structure.integrations.push('neon');
|
|
84
|
+
}
|
|
85
|
+
if (depNames.includes('@neondatabase/auth')) {
|
|
86
|
+
structure.integrations.push('neon-auth');
|
|
87
|
+
}
|
|
88
|
+
if (depNames.includes('stripe')) {
|
|
89
|
+
structure.integrations.push('stripe');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Vercel detection
|
|
93
|
+
if (depNames.includes('vercel') || scripts.includes('vercel')) {
|
|
94
|
+
structure.hasVercel = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── .csproj detection ──────────────────────────────────────────────────
|
|
99
|
+
let hasDotnet = false;
|
|
100
|
+
const csprojLocations = ['.', 'src', 'src/Api', 'src/Web', 'src/backend'];
|
|
101
|
+
|
|
102
|
+
function checkCsproj(filePath) {
|
|
103
|
+
hasDotnet = true;
|
|
104
|
+
try {
|
|
105
|
+
const content = readFileSync(filePath, 'utf8');
|
|
106
|
+
if (content.includes('Hangfire')) {
|
|
107
|
+
if (!structure.integrations.includes('hangfire')) structure.integrations.push('hangfire');
|
|
108
|
+
}
|
|
109
|
+
} catch { /* ignore */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const loc of csprojLocations) {
|
|
113
|
+
try {
|
|
114
|
+
const dir = join(targetPath, loc);
|
|
115
|
+
if (!existsSync(dir)) continue;
|
|
116
|
+
const entries = readdirSyncSafe(dir);
|
|
117
|
+
// Direct .csproj files
|
|
118
|
+
for (const e of entries.filter(f => f.endsWith('.csproj'))) {
|
|
119
|
+
checkCsproj(join(dir, e));
|
|
120
|
+
}
|
|
121
|
+
// One level deeper (e.g., src/backend/MyProject/*.csproj)
|
|
122
|
+
for (const sub of entries) {
|
|
123
|
+
const subDir = join(dir, sub);
|
|
124
|
+
for (const e of readdirSyncSafe(subDir).filter(f => f.endsWith('.csproj'))) {
|
|
125
|
+
checkCsproj(join(subDir, e));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch { /* ignore */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Determine stack: monorepo (nextjs+dotnet), dotnet, or keep frontend stack
|
|
132
|
+
if (hasDotnet) {
|
|
133
|
+
if (structure.stack === 'nextjs' || structure.stack === 'react' || structure.stack === 'vue') {
|
|
134
|
+
// Monorepo: frontend + .NET backend
|
|
135
|
+
structure.stack = `${structure.stack}+dotnet`;
|
|
136
|
+
} else if (structure.stack === 'unknown') {
|
|
137
|
+
structure.stack = 'dotnet';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── vercel.json detection ──────────────────────────────────────────────
|
|
142
|
+
if (existsSync(join(targetPath, 'vercel.json'))) {
|
|
143
|
+
structure.hasVercel = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── tsconfig.json detection (fallback for stack) ────────────────────────
|
|
147
|
+
if (structure.stack === 'unknown' && existsSync(join(targetPath, 'tsconfig.json'))) {
|
|
148
|
+
structure.stack = 'nodejs';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Architecture detection ─────────────────────────────────────────────
|
|
152
|
+
try {
|
|
153
|
+
const configPath = join(targetPath, '.morph/config/config.json');
|
|
154
|
+
if (existsSync(configPath)) {
|
|
155
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
156
|
+
if (config.architecture?.style) {
|
|
157
|
+
structure.architecture = config.architecture.style;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch { /* ignore */ }
|
|
161
|
+
|
|
162
|
+
// ── Docker detection ───────────────────────────────────────────────────
|
|
163
|
+
if (existsSync(join(targetPath, 'docker-compose.yml')) ||
|
|
164
|
+
existsSync(join(targetPath, 'docker-compose.yaml')) ||
|
|
165
|
+
existsSync(join(targetPath, 'Dockerfile'))) {
|
|
166
|
+
if (!structure.integrations.includes('docker')) structure.integrations.push('docker');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Azure detection ────────────────────────────────────────────────────
|
|
170
|
+
const azureSignals = [
|
|
171
|
+
'.azure',
|
|
172
|
+
'azure-pipelines.yml',
|
|
173
|
+
'infra/main.bicep',
|
|
174
|
+
'infra',
|
|
175
|
+
];
|
|
176
|
+
const hasAzure = azureSignals.some(signal => {
|
|
177
|
+
const p = join(targetPath, signal);
|
|
178
|
+
if (!existsSync(p)) return false;
|
|
179
|
+
if (signal === 'infra') {
|
|
180
|
+
// Check for .bicep files inside infra/
|
|
181
|
+
return readdirSyncSafe(p).some(f => f.endsWith('.bicep'));
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
});
|
|
185
|
+
if (hasAzure && !structure.integrations.includes('azure')) {
|
|
186
|
+
structure.integrations.push('azure');
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
// Fail-safe — return defaults
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
structure,
|
|
194
|
+
inferred: {
|
|
195
|
+
markdown: opts.generateStandards ? generateInferredMarkdown(structure) : '',
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Safe readdir that returns [] on error.
|
|
202
|
+
* @param {string} dir
|
|
203
|
+
* @returns {string[]}
|
|
204
|
+
*/
|
|
205
|
+
function readdirSyncSafe(dir) {
|
|
206
|
+
try {
|
|
207
|
+
return readdirSync(dir);
|
|
208
|
+
} catch {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Generate a markdown summary from detected structure.
|
|
215
|
+
* @param {Object} structure
|
|
216
|
+
* @returns {string}
|
|
217
|
+
*/
|
|
218
|
+
function generateInferredMarkdown(structure) {
|
|
219
|
+
const lines = [`## Detected Stack: ${structure.stack}`];
|
|
220
|
+
if (structure.architecture) lines.push(`Architecture: ${structure.architecture}`);
|
|
221
|
+
if (structure.uiLibrary) lines.push(`UI Library: ${structure.uiLibrary}`);
|
|
222
|
+
if (structure.integrations.length > 0) lines.push(`Integrations: ${structure.integrations.join(', ')}`);
|
|
223
|
+
if (structure.hasVercel) lines.push(`Vercel: detected`);
|
|
224
|
+
return lines.join('\n');
|
|
225
|
+
}
|