@polymorphism-tech/morph-spec 8.35.0 → 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 +11 -9
- 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 +187 -13
- 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/claude-code/core/worktree-env-inject.js +43 -0
- package/framework/hooks/shared/payload-utils.js +15 -2
- 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 +59 -3
- package/src/commands/project/worktree.js +68 -3
- package/src/commands/state/finish.js +48 -5
- 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/git/pr-files.js +56 -0
- 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,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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pr-files.js — o diff de um PR carregou uma feature morph-spec?
|
|
3
|
+
*
|
|
4
|
+
* `morph/{f}` é a convenção de nome das branches que o framework cria, não
|
|
5
|
+
* prova de que a branch carregou uma feature: nada impede um PR só de docs
|
|
6
|
+
* numa `morph/x`. O `doctor` lia o nome como prova e acusava
|
|
7
|
+
* `pr-merged-not-landed` para sempre sobre PRs que nunca tiveram archive para
|
|
8
|
+
* pousar (medido no MORPH_GHLBrain: #5, um único `docs/*.md`, e #23, a onda 2
|
|
9
|
+
* de outra feature — nenhum caminho `.morph/` no diff de nenhum dos dois).
|
|
10
|
+
*
|
|
11
|
+
* A pergunta é feita ao diff, e só para o candidato: o `doctor` chama isto
|
|
12
|
+
* depois de já saber que o PR mergeou e que `archive/{f}` falta aqui, então o
|
|
13
|
+
* custo (~650 ms por `gh pr view`) não recai sobre quem não tem pendência. O
|
|
14
|
+
* índice (`pr-index.js`) segue sem o campo de propósito: pedir `mergeCommit` no
|
|
15
|
+
* `pr list` custou ~400 ms a mais em CADA chamada, paga pelos quatro leitores.
|
|
16
|
+
*
|
|
17
|
+
* Três respostas, nunca duas: `true` (o diff tocou `.morph/{features,archive}/{f}/`),
|
|
18
|
+
* `false` (o diff foi lido inteiro e não tocou) e `null` (DESCONHECIDO — sem
|
|
19
|
+
* `gh`, falha, JSON ilegível, ou o diff bateu no teto de arquivos do `gh` sem
|
|
20
|
+
* achar). Só `false` autoriza calar um aviso.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { execFileSync } from 'child_process';
|
|
24
|
+
import { resolveGh } from './gh.js';
|
|
25
|
+
|
|
26
|
+
/** Quantos arquivos o `gh pr view --json files` devolve, no máximo. */
|
|
27
|
+
export const GH_FILES_CAP = 100;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} root - de onde o `gh` roda (resolve o repositório pelo origin)
|
|
31
|
+
* @param {number} number - número do PR
|
|
32
|
+
* @param {string} feature - o `{f}` de `morph/{f}`
|
|
33
|
+
* @param {{ timeout?: number }} [opts]
|
|
34
|
+
* @returns {boolean|null} null = DESCONHECIDO, nunca "não carregou"
|
|
35
|
+
*/
|
|
36
|
+
export function prCarriesFeature(root, number, feature, opts = {}) {
|
|
37
|
+
const timeout = opts.timeout ?? 8000;
|
|
38
|
+
try {
|
|
39
|
+
const { bin, prefix } = resolveGh();
|
|
40
|
+
const out = execFileSync(
|
|
41
|
+
bin,
|
|
42
|
+
[...prefix, 'pr', 'view', String(number), '--json', 'files'],
|
|
43
|
+
{ cwd: root, stdio: 'pipe', timeout },
|
|
44
|
+
).toString();
|
|
45
|
+
const files = JSON.parse(out)?.files;
|
|
46
|
+
if (!Array.isArray(files)) return null;
|
|
47
|
+
// A barra final é a fronteira do slug: `vitrine` não pode casar `vitrine-lead/`.
|
|
48
|
+
const owned = [`.morph/features/${feature}/`, `.morph/archive/${feature}/`];
|
|
49
|
+
if (files.some((f) => typeof f?.path === 'string' && owned.some((p) => f.path.startsWith(p)))) return true;
|
|
50
|
+
// Diff truncado sem o caminho não prova ausência — o archive pode estar além do teto.
|
|
51
|
+
if (files.length >= GH_FILES_CAP) return null;
|
|
52
|
+
return false;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/lib/risk-detector.js
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* - designSystemDivergence: boolean — conflicting design sources detected
|
|
10
10
|
* - standardsViolations: string[] — violations of mandate.md proibições
|
|
11
11
|
* - verificationStatus: string — último `morph-spec verify` ('fail' pausa, qualquer gate)
|
|
12
|
+
* - verificationScope + gate: string — presente e != 'feature' pausa SÓ no ship: o
|
|
13
|
+
* último `verify` foi um recorte de task, não da feature inteira (`null`/ausente
|
|
14
|
+
* não conta — narrow, nunca widen)
|
|
12
15
|
* - e2eStatus + e2eReason + gate — nó e2e do último verify. Um `skip`
|
|
13
|
-
* cuja razão NÃO seja dispensa legítima pausa SÓ no ship (gate 'review')
|
|
14
|
-
*
|
|
15
|
-
* `skip`. Dispensa legítima só conta vinda de 'feature' (ver abaixo)
|
|
16
|
+
* cuja razão NÃO seja dispensa legítima pausa SÓ no ship (gate 'review').
|
|
17
|
+
* Sem escopo (stamp sem o campo) cai do mesmo lado seguro que `unknown`
|
|
16
18
|
* - verificationStale + gate: bool/str — stamp com sha != HEAD pausa SÓ no
|
|
17
19
|
* ship (gate 'review') — código mudou depois do último verify
|
|
18
20
|
* - evalScoreSource + gate: str — no ship, score que NÃO veio do avaliador
|
|
@@ -172,6 +174,30 @@ export function shouldPauseGate(config, signals) {
|
|
|
172
174
|
};
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
// No gate de SHIP: um `verification` PRESENTE cujo escopo não é o da feature
|
|
178
|
+
// é um recorte parcial — de UMA task, nunca da app inteira. O stamp é
|
|
179
|
+
// SINGULAR, sobrescrito por qualquer verify de qualquer escopo, e vale
|
|
180
|
+
// INDEPENDENTE do que os nós individuais dizem: mesmo com e2e 'pass' dentro
|
|
181
|
+
// daquele recorte, build/tests/validators também rodaram só para a task, não
|
|
182
|
+
// para a feature. Antes desta regra, um `verify {f} {task}` totalmente verde
|
|
183
|
+
// comprava o Gate 3 da feature que ele nem tentou verificar por inteiro — já
|
|
184
|
+
// acontecia manualmente (`--filter`), e o recorte automático de PR B faz
|
|
185
|
+
// acontecer em TODA task. Por isso corre ANTES do bloco de e2e abaixo: ele
|
|
186
|
+
// pausa por si só, sem depender de e2eStatus/e2eReason.
|
|
187
|
+
//
|
|
188
|
+
// `typeof === 'string'` exclui `null`/ausente (features legadas sem stamp) —
|
|
189
|
+
// narrow, nunca widen. O caminho de volta é sempre o mesmo, barato e nomeado:
|
|
190
|
+
// `morph-spec verify {feature}` (suíte inteira) antes do ship.
|
|
191
|
+
if (signals.gate === 'review'
|
|
192
|
+
&& typeof signals.verificationScope === 'string'
|
|
193
|
+
&& signals.verificationScope !== FEATURE_SCOPE) {
|
|
194
|
+
return {
|
|
195
|
+
pause: true,
|
|
196
|
+
reason: `último morph-spec verify foi de escopo "${signals.verificationScope}" (recorte parcial de task)`
|
|
197
|
+
+ ' — rode `morph-spec verify {feature}` (suíte inteira) antes do ship',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
175
201
|
// No gate de SHIP apenas: o nó e2e do último verify PULOU. Um skip não
|
|
176
202
|
// reprova o rollup, então `verificationStatus` sai 'pass' e o gate ficava
|
|
177
203
|
// cego — auto-passando um Gate 3 sobre uma app que ninguém subiu. A razão
|
|
@@ -187,21 +213,15 @@ export function shouldPauseGate(config, signals) {
|
|
|
187
213
|
if (E2E_SKIP_CLASSES_THAT_PAUSE.includes(klass)) {
|
|
188
214
|
return { pause: true, reason: e2ePauseReason(klass, signals.e2eReason) };
|
|
189
215
|
}
|
|
190
|
-
// Segunda pergunta, que a razão sozinha não responde: QUEM carimbou.
|
|
191
|
-
// `
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
// mesmo um "project does not containerize" carimbado por task é a resposta
|
|
200
|
-
// a uma pergunta que o gate não fez. O caminho de volta é barato e nomeado
|
|
201
|
-
// (rodar `verify {feature}`), e é o que morph-review já manda fazer antes
|
|
202
|
-
// do Gate 3. Narrow, nunca widen: nenhuma razão nova passa a valer aqui —
|
|
203
|
-
// uma que já valia deixa de valer fora do escopo que responde pela app.
|
|
204
|
-
if (signals.verificationScope !== FEATURE_SCOPE) {
|
|
216
|
+
// Segunda pergunta, que a razão sozinha não responde: QUEM carimbou. Um
|
|
217
|
+
// escopo de TASK (`task:{id}`) já pausou acima, antes deste bloco — o que
|
|
218
|
+
// sobra a checar aqui é só a ausência de escopo (`null`/indefinido:
|
|
219
|
+
// feature legada, ou stamp anterior à introdução do campo). Cai do mesmo
|
|
220
|
+
// lado seguro que `unknown` pelo mesmo motivo de sempre: recusar é barato,
|
|
221
|
+
// aprovar sem prova de QUEM respondeu pela app não é. O caminho de volta é
|
|
222
|
+
// o mesmo (rodar `verify {feature}`), e é o que morph-review já manda
|
|
223
|
+
// fazer antes do Gate 3.
|
|
224
|
+
if (signals.verificationScope == null) {
|
|
205
225
|
return {
|
|
206
226
|
pause: true,
|
|
207
227
|
reason: e2eScopeRefusal(klass, signals.verificationScope, signals.e2eReason),
|