@polymorphism-tech/morph-spec 8.35.1 → 8.37.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 +10 -8
- package/README.md +2 -2
- package/bin/morph-spec.js +4 -3
- 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 +212 -15
- 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 +2 -2
- 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 +22 -11
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +23 -10
- 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 +59 -17
- 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 +632 -0
- package/src/lib/tasks/eval-node.js +3 -3
- package/src/lib/tasks/test-nodes.js +190 -20
- package/src/lib/tasks/test-runner.js +210 -17
- package/src/lib/tasks/test-scope.js +597 -0
- package/src/lib/validators/traceability-check.js +9 -2
- package/src/lib/validators/verify-runner.js +190 -27
- package/src/lib/worktree/docker-audit.js +5 -1
|
@@ -5,6 +5,7 @@ import { parseTasksJsonLenient } from '../../lib/tasks/tasks-json-parser.js';
|
|
|
5
5
|
import { planDispatch } from '../../lib/tasks/dispatch-planner.js';
|
|
6
6
|
import { renderMermaidFlowchart } from '../../lib/tasks/mermaid-renderer.js';
|
|
7
7
|
import { checkUseCases } from '../../lib/specs/usecase-check.js';
|
|
8
|
+
import { checkArtifactCriteria } from '../../lib/specs/done-criteria-check.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* `morph-spec dag <feature>` — analyze the tasks.json DAG and print the
|
|
@@ -51,10 +52,16 @@ export async function dagCommand(featureName, options = {}) {
|
|
|
51
52
|
// against a plan that points at a use case nobody can read.
|
|
52
53
|
const ucCheck = checkUseCases(data, join(planDir, 'usecases'));
|
|
53
54
|
|
|
55
|
+
// Artifact-postcondition warning (issue #87). Lexical, not wiring — it
|
|
56
|
+
// runs over ALL tasks even when the feature has no use cases at all
|
|
57
|
+
// (checkUseCases short-circuits above in that case), because a task's
|
|
58
|
+
// own doneCriteria can describe an artifact with no use case in sight.
|
|
59
|
+
const artifactCriteria = checkArtifactCriteria(data, join(planDir, 'usecases'));
|
|
60
|
+
|
|
54
61
|
const plan = planDispatch(data);
|
|
55
62
|
|
|
56
63
|
if (options.json) {
|
|
57
|
-
console.log(JSON.stringify({ ...plan, useCases: ucCheck }, null, 2));
|
|
64
|
+
console.log(JSON.stringify({ ...plan, useCases: ucCheck, artifactCriteria }, null, 2));
|
|
58
65
|
if (ucCheck.errors.length) process.exit(1);
|
|
59
66
|
return;
|
|
60
67
|
}
|
|
@@ -71,6 +78,11 @@ export async function dagCommand(featureName, options = {}) {
|
|
|
71
78
|
console.error(chalk.yellow(`\n⚠ Use cases — ${plural(ucCheck.warnings.length, 'aviso', 'avisos')}`));
|
|
72
79
|
for (const w of ucCheck.warnings) console.error(chalk.yellow(` ${w}`));
|
|
73
80
|
}
|
|
81
|
+
if (artifactCriteria.warnings.length) {
|
|
82
|
+
console.error(chalk.yellow(`\n⚠ Critérios de artefato — ${plural(artifactCriteria.warnings.length, 'aviso', 'avisos')}`));
|
|
83
|
+
for (const w of artifactCriteria.warnings) console.error(chalk.yellow(` ${w}`));
|
|
84
|
+
console.error(chalk.gray(' Teste que lê texto de arquivo não prova comportamento (morph-plan §2, "Por que artefato").'));
|
|
85
|
+
}
|
|
74
86
|
|
|
75
87
|
console.log(chalk.bold(`\n📊 DAG — ${featureName}`));
|
|
76
88
|
console.log('━'.repeat(60));
|
|
@@ -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,16 +188,11 @@ function evalsStatusOf(result) {
|
|
|
166
188
|
return 'skip';
|
|
167
189
|
}
|
|
168
190
|
|
|
169
|
-
/**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return JSON.parse(readFileSync(tasksPath, 'utf8')).tasks || [];
|
|
175
|
-
} catch {
|
|
176
|
-
return [];
|
|
177
|
-
}
|
|
178
|
-
}
|
|
191
|
+
/**
|
|
192
|
+
* The options that make a FEATURE-scope run partial, each with the `reason` it
|
|
193
|
+
* reports when it keeps the stamp from being written — first match wins.
|
|
194
|
+
*/
|
|
195
|
+
const PARTIAL_RUN_FLAGS = [['filter', 'filtered'], ['skipTests', 'skip-tests'], ['skipBuild', 'skip-build']];
|
|
179
196
|
|
|
180
197
|
/**
|
|
181
198
|
* `morph-spec verify <feature> [task]` — the deterministic code-before-judge
|
|
@@ -194,7 +211,8 @@ function readTasks(featureName) {
|
|
|
194
211
|
* `morph-spec score` gives for the same situation);
|
|
195
212
|
* - `--filter` that matches no declared node (a filtered-to-empty run is
|
|
196
213
|
* green and proves nothing);
|
|
197
|
-
* - `--record-baseline` with nowhere to write (no `config.json`)
|
|
214
|
+
* - `--record-baseline` with nowhere to write (no `config.json`), or with a
|
|
215
|
+
* task id (a recorte de task is never a fleet baseline, #87);
|
|
198
216
|
* - `--linux` without Docker.
|
|
199
217
|
* The last three arrive as `VerifyUsageError` from the runner.
|
|
200
218
|
*/
|
|
@@ -210,9 +228,10 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
210
228
|
process.exit(2);
|
|
211
229
|
}
|
|
212
230
|
|
|
231
|
+
// The runner's own reader (one reader of tasks.json for verify, #87): null
|
|
232
|
+
// means tasks.json or the task is missing, and both are this usage error.
|
|
213
233
|
if (taskId) {
|
|
214
|
-
|
|
215
|
-
if (!tasks.some((t) => t.id === taskId)) {
|
|
234
|
+
if (!readTask(process.cwd(), featureName, taskId)) {
|
|
216
235
|
console.error(chalk.red(`\n❌ Task "${taskId}" not found in tasks.json for feature "${featureName}"\n`));
|
|
217
236
|
process.exit(2);
|
|
218
237
|
}
|
|
@@ -232,6 +251,9 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
232
251
|
refreshCache: options.refreshCache,
|
|
233
252
|
linux: options.linux,
|
|
234
253
|
linuxImage: options.linuxImage,
|
|
254
|
+
// Task scope: switch the recorte off (#87). At feature scope nothing is
|
|
255
|
+
// ever narrowed, so the flag is a documented no-op there, not an error.
|
|
256
|
+
full: options.full,
|
|
235
257
|
});
|
|
236
258
|
} catch (err) {
|
|
237
259
|
// A wrong invocation is exit 2, like an unknown feature or task — never a
|
|
@@ -249,9 +271,29 @@ export async function verifyCommand(featureName, taskId, options = {}) {
|
|
|
249
271
|
// the suite in order to write `expectedCount`. Persisting a `verification`
|
|
250
272
|
// stamp from it would let a maintenance run stand in for the proof a gate
|
|
251
273
|
// asks for — the same class of confusion as a hand-typed baseline.
|
|
274
|
+
//
|
|
275
|
+
// `--filter` at FEATURE scope is the other side of the same hole (#87): the
|
|
276
|
+
// run is narrowed to a subset of nodes, but the stamp it would write carries
|
|
277
|
+
// `scope: 'feature'` — the one scope risk-detector.js trusts unconditionally.
|
|
278
|
+
// A partial run must not stand in for the proof a gate asks for, same as
|
|
279
|
+
// `--record-baseline` above. `--skip-tests` and `--skip-build` are the same
|
|
280
|
+
// partial run through another door (#113): a skipped node never reddens the
|
|
281
|
+
// rollup, so the stamp said `pass` over a suite (or a build) that never ran,
|
|
282
|
+
// and no gate rule reads a skipped `tests`/`build`. Task scope is unaffected:
|
|
283
|
+
// that stamp already says `scope: 'task:{id}'` (which pauses the ship gate on
|
|
284
|
+
// its own), and `taskVerifications` never claimed to cover more than the
|
|
285
|
+
// task. Checked AFTER `--no-persist` (via the `!options.noPersist` guard) so
|
|
286
|
+
// an explicit `--no-persist` keeps its own reason instead of being reported
|
|
287
|
+
// as one of these — the flags are all "don't write", but decidePersistence()
|
|
288
|
+
// already owns the former's reason.
|
|
289
|
+
const partialFeatureRun = (!options.noPersist && !taskId)
|
|
290
|
+
? PARTIAL_RUN_FLAGS.find(([flag]) => options[flag])?.[1] ?? null
|
|
291
|
+
: null;
|
|
252
292
|
const { persist, reason } = options.recordBaseline
|
|
253
293
|
? { persist: false, reason: 'record-baseline' }
|
|
254
|
-
:
|
|
294
|
+
: partialFeatureRun
|
|
295
|
+
? { persist: false, reason: partialFeatureRun }
|
|
296
|
+
: decidePersistence({ cwd: process.cwd(), featureName, noPersist: options.noPersist });
|
|
255
297
|
|
|
256
298
|
let compact = null;
|
|
257
299
|
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
|
+
}
|