@polymorphism-tech/morph-spec 8.45.0 → 8.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +6 -5
- package/README.md +2 -2
- package/bin/morph-spec.js +2 -4
- package/bin/validate.js +4 -25
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -2
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +9 -9
- package/framework/commands/create-umbraco-lp.md +2 -0
- package/framework/commands/migrate-lp-to-umbraco.md +2 -0
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +1 -1
- package/framework/commands/morph-infra.md +17 -203
- package/framework/commands/morph-media.md +3 -0
- package/framework/commands/morph-preflight.md +5 -28
- package/framework/commands/morph-proposal.md +2 -2
- package/framework/commands/morph-troubleshoot.md +2 -14
- package/framework/docs/CLI.md +8 -7
- package/framework/hooks/claude-code/core/state-sync.js +13 -1
- package/framework/hooks/shared/e2e-check.js +3 -4
- package/framework/hooks/shared/state-writer.js +1 -0
- package/framework/rules/ai-agents-standards.md +18 -16
- package/framework/rules/asaas-standards.md +3 -1
- package/framework/rules/csharp-standards.md +10 -8
- package/framework/rules/data-standards.md +5 -5
- package/framework/rules/dotnet-templates.md +1 -1
- package/framework/rules/frontend-standards.md +4 -2
- package/framework/rules/gsap-standards.md +10 -5
- package/framework/rules/hangfire-standards.md +3 -1
- package/framework/rules/infrastructure-standards.md +4 -2
- package/framework/rules/morph-workflow.md +2 -2
- package/framework/rules/neon-auth-standards.md +3 -1
- package/framework/rules/neon-standards.md +6 -4
- package/framework/rules/nextjs-standards.md +13 -11
- package/framework/rules/nextjs-templates.md +1 -1
- package/framework/rules/resend-standards.md +3 -1
- package/framework/schemas/config.schema.json +4 -0
- package/framework/schemas/feature.schema.json +4 -4
- package/framework/skills/README.md +1 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
- package/framework/skills/core/morph-apply/SKILL.md +3 -3
- package/framework/skills/core/morph-brainstorming/SKILL.md +3 -3
- package/framework/skills/core/morph-hotfix/SKILL.md +4 -4
- package/framework/skills/core/morph-implement/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +3 -3
- package/framework/skills/core/morph-media/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/SKILL.md +3 -3
- package/framework/skills/core/morph-proposal/SKILL.md +3 -3
- package/framework/skills/core/morph-review/SKILL.md +30 -17
- package/framework/skills/core/morph-stack-scan/SKILL.md +3 -3
- package/framework/skills/core/morph-standards/SKILL.md +6 -6
- package/framework/skills/core/morph-uiux/SKILL.md +3 -3
- package/framework/standards/STANDARDS.json +59 -56
- package/framework/standards/ai-agents/evals-with-cache.md +1 -1
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
- package/framework/standards/ai-agents/prompt-sources.md +6 -4
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/architecture/code-comments.md +2 -2
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
- package/framework/standards/backend/api/validation.md +135 -47
- package/framework/standards/backend/database/postgresql/database.md +11 -7
- package/framework/standards/backend/database/vector-search-rag.md +207 -324
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
- package/framework/standards/backend/dotnet/result-pattern.md +40 -6
- package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
- package/framework/standards/backend/dotnet/scrutor.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
- package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
- package/framework/standards/data/nosql/cache/redis.md +120 -44
- package/framework/standards/data/nosql/cosmos-db.md +114 -71
- package/framework/standards/frontend/design-system/animations.md +1 -1
- package/framework/standards/frontend/design-system/naming.md +1 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
- package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
- package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
- package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
- package/framework/templates/infrastructure/github/README.md +2 -1
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
- package/package.json +1 -1
- package/src/commands/observability/telemetry.js +3 -5
- package/src/commands/project/init.js +2 -1
- package/src/commands/project/update.js +16 -18
- package/src/commands/state/approve.js +17 -0
- package/src/commands/state/gate-check.js +4 -14
- package/src/commands/validation/verify.js +99 -57
- package/src/core/paths/output-schema.js +1 -1
- package/src/dashboard/app/App.jsx +0 -2
- package/src/dashboard/app/components/CommandPalette.jsx +2 -3
- package/src/dashboard/app/components/EntityLink.jsx +0 -1
- package/src/dashboard/app/components/Markdown.jsx +1 -1
- package/src/dashboard/app/components/Sidebar.jsx +1 -2
- package/src/dashboard/app/pages/RulesMap.jsx +3 -3
- package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
- package/src/dashboard/scanner/activity-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +5 -23
- package/src/dashboard/scanner/features-scanner.js +1 -1
- package/src/dashboard/scanner/index.js +0 -2
- package/src/dashboard/scanner/rules-scanner.js +15 -10
- package/src/dashboard/scanner/skills-scanner.js +2 -3
- package/src/lib/ci/ci-verify.js +118 -0
- package/src/lib/ci/github-ci.js +192 -0
- package/src/lib/ci/workflow-scaffold.js +122 -0
- package/src/lib/evaluator-report.js +13 -0
- package/src/lib/risk-detector.js +8 -0
- package/src/lib/stack/stack-profile.js +1 -1
- package/src/lib/telemetry/verify-event.js +3 -2
- package/src/lib/validators/reuse/reuse-validator.js +2 -3
- package/src/lib/validators/reuse/symbol-scanner.js +2 -4
- package/src/lib/validators/validation-runner.js +8 -88
- package/src/lib/validators/verify-runner.js +22 -72
- package/src/scripts/setup-infra.js +33 -22
- package/src/utils/agents-installer.js +20 -8
- package/src/utils/file-copier.js +7 -5
- package/framework/commands/commit.md +0 -166
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/rubrics/backend.json +0 -41
- package/framework/rubrics/default.json +0 -36
- package/framework/rubrics/frontend.json +0 -36
- package/framework/templates/project/validate.js +0 -97
- package/src/dashboard/app/pages/Rubrics.jsx +0 -97
- package/src/dashboard/scanner/rubrics-scanner.js +0 -12
- package/src/lib/validators/architecture/architecture-validator.js +0 -270
- package/src/lib/validators/architecture/index.js +0 -7
- package/src/lib/validators/css/css-validator.js +0 -340
- package/src/lib/validators/css/index.js +0 -7
- package/src/lib/validators/design-system/design-system-validator.js +0 -237
- package/src/lib/validators/design-system/index.js +0 -7
- package/src/lib/validators/score-aggregator.js +0 -244
- package/src/lib/validators/ui/index.js +0 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
|
@@ -7,24 +7,14 @@
|
|
|
7
7
|
* MORPH-SPEC 3.0 - Phase A: Active Validation Pipeline
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { existsSync
|
|
11
|
-
import { join } from 'path';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
12
11
|
import chalk from 'chalk';
|
|
13
12
|
import { getAbsoluteOutputPath, resolveContractsOutputType } from '../../core/paths/output-schema.js';
|
|
14
13
|
import { getStackProfile } from '../stack/stack-profile.js';
|
|
15
|
-
import { aggregateScores, loadRubric } from './score-aggregator.js';
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
* Heavy validators skipped in 'light' mode (checkpoint-only).
|
|
19
|
-
*
|
|
20
|
-
* What light mode actually leaves behind is whatever the stack contributes that
|
|
21
|
-
* is NOT in this set — in practice `contrast` and `nextjs-component`. There are
|
|
22
|
-
* no `spec` or `content` validators; the switch in runSingleValidator has no
|
|
23
|
-
* case for either, and the old comment here promised both.
|
|
24
|
-
*/
|
|
15
|
+
// Skipped in 'light' mode (checkpoint-only); light keeps `nextjs-component`.
|
|
25
16
|
const HEAVY_VALIDATORS = new Set([
|
|
26
|
-
'
|
|
27
|
-
'css', 'design-system', 'reuse', 'code-budget'
|
|
17
|
+
'packages', 'contract-compliance', 'reuse', 'code-budget'
|
|
28
18
|
]);
|
|
29
19
|
|
|
30
20
|
/**
|
|
@@ -167,17 +157,12 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
167
157
|
result.issues.push(...issues);
|
|
168
158
|
|
|
169
159
|
// A validator that skipped itself checked nothing: in `results` it would
|
|
170
|
-
//
|
|
160
|
+
// read as a validator that ran clean.
|
|
171
161
|
if (!validatorResult._skipReason) result.results[validatorId] = { passed: errors === 0, issues };
|
|
172
162
|
}
|
|
173
163
|
} catch (err) {
|
|
174
|
-
// A validator that throws is a FAILED validation, not a silent skip
|
|
175
|
-
//
|
|
176
|
-
// `result.errors` never moved, so `result.passed = errors === 0` stayed
|
|
177
|
-
// true and the run reported "✅ All validations passed". The only trace
|
|
178
|
-
// was a console line, and `verify` silences stdout. That is how a broken
|
|
179
|
-
// import (`css`) and a path bug (`packages` under a different cwd) went
|
|
180
|
-
// unnoticed: both threw, both read as clean.
|
|
164
|
+
// A validator that throws is a FAILED validation, not a silent skip:
|
|
165
|
+
// `verify` silences stdout, so a console line alone would read as clean.
|
|
181
166
|
if (options.verbose) {
|
|
182
167
|
console.log(chalk.yellow(` ⚠ Validator '${validatorId}' failed: ${err.message}`));
|
|
183
168
|
}
|
|
@@ -208,17 +193,6 @@ export async function runValidation(projectPath, featureName, options = {}) {
|
|
|
208
193
|
? `✅ All validations passed (${result.warnings} warnings)`
|
|
209
194
|
: `❌ ${result.errors} error(s), ${result.warnings} warning(s)`;
|
|
210
195
|
|
|
211
|
-
// Score aggregation — layered on top of pass/fail
|
|
212
|
-
try {
|
|
213
|
-
const rubricId = readRubricConfig(projectPath);
|
|
214
|
-
const rubric = loadRubric(projectPath, rubricId);
|
|
215
|
-
const thresholdOverrides = readThresholdConfig(projectPath);
|
|
216
|
-
result.scores = aggregateScores(result, rubric, thresholdOverrides);
|
|
217
|
-
} catch {
|
|
218
|
-
// Score aggregation is non-blocking — if it fails, pass/fail still works
|
|
219
|
-
result.scores = null;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
196
|
return result;
|
|
223
197
|
}
|
|
224
198
|
|
|
@@ -243,7 +217,7 @@ function detectValidators(projectPath = '.') {
|
|
|
243
217
|
* seven validators only two returned all three keys. `packages` — one of the
|
|
244
218
|
* two defaults for the `dotnet` stack — returned
|
|
245
219
|
* `{status, totalFiles, filesWithIssues, results}` and had every NuGet
|
|
246
|
-
* incompatibility it found thrown away, while `verify` reported
|
|
220
|
+
* incompatibility it found thrown away, while `verify` reported green.
|
|
247
221
|
*
|
|
248
222
|
* Reading the nested `results` shape here keeps that contract in ONE place
|
|
249
223
|
* instead of asking every validator to remember it.
|
|
@@ -257,7 +231,7 @@ export function normalizeValidatorResult(validatorId, raw) {
|
|
|
257
231
|
|
|
258
232
|
// Issues can be top-level, or nested one level under `results` — either as
|
|
259
233
|
// an array of `{issues}` (packages: one entry per .csproj) or an object of
|
|
260
|
-
// them
|
|
234
|
+
// them keyed by check name. The nested read is a FALLBACK,
|
|
261
235
|
// never additive: validators that expose both would otherwise report every
|
|
262
236
|
// finding twice.
|
|
263
237
|
const topLevel = Array.isArray(raw.issues) ? raw.issues : [];
|
|
@@ -299,38 +273,16 @@ export function normalizeValidatorResult(validatorId, raw) {
|
|
|
299
273
|
*/
|
|
300
274
|
async function runSingleValidator(validatorId, projectPath, featureName, options) {
|
|
301
275
|
switch (validatorId) {
|
|
302
|
-
case 'architecture': {
|
|
303
|
-
const { validateArchitecture } = await import('./architecture/architecture-validator.js');
|
|
304
|
-
return await validateArchitecture(projectPath, options);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
276
|
case 'packages': {
|
|
308
277
|
const { validatePackages } = await import('./packages/package-validator.js');
|
|
309
278
|
return await validatePackages(projectPath, options);
|
|
310
279
|
}
|
|
311
280
|
|
|
312
|
-
case 'contrast': {
|
|
313
|
-
const { validateContrast } = await import('./ui/ui-contrast-validator.js');
|
|
314
|
-
return await validateContrast(projectPath, options);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
case 'css': {
|
|
318
|
-
// The module exports `validateCssClasses`; this asked for `validateCss`,
|
|
319
|
-
// which is undefined — the call threw on every invocation.
|
|
320
|
-
const { validateCssClasses } = await import('./css/css-validator.js');
|
|
321
|
-
return await validateCssClasses(projectPath, options);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
281
|
case 'contract-compliance': {
|
|
325
282
|
const { validateContracts } = await import('./contracts/contract-compliance-validator.js');
|
|
326
283
|
return await validateContracts(projectPath, featureName, options);
|
|
327
284
|
}
|
|
328
285
|
|
|
329
|
-
case 'design-system': {
|
|
330
|
-
const { validateDesignSystem } = await import('./design-system/design-system-validator.js');
|
|
331
|
-
return await validateDesignSystem(projectPath, featureName, options);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
286
|
case 'reuse': {
|
|
335
287
|
const { validateReuse } = await import('./reuse/reuse-validator.js');
|
|
336
288
|
return await validateReuse(projectPath, featureName, options);
|
|
@@ -414,35 +366,3 @@ export function formatValidationResults(result) {
|
|
|
414
366
|
|
|
415
367
|
console.log('');
|
|
416
368
|
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Read rubric ID from project config.
|
|
420
|
-
* @param {string} projectPath
|
|
421
|
-
* @returns {string}
|
|
422
|
-
*/
|
|
423
|
-
function readRubricConfig(projectPath) {
|
|
424
|
-
try {
|
|
425
|
-
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
426
|
-
if (!existsSync(configPath)) return 'default';
|
|
427
|
-
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
428
|
-
return config.validation?.rubric ?? 'default';
|
|
429
|
-
} catch {
|
|
430
|
-
return 'default';
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Read threshold overrides from project config.
|
|
436
|
-
* @param {string} projectPath
|
|
437
|
-
* @returns {Object} { approve?: number, retry?: number }
|
|
438
|
-
*/
|
|
439
|
-
function readThresholdConfig(projectPath) {
|
|
440
|
-
try {
|
|
441
|
-
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
442
|
-
if (!existsSync(configPath)) return {};
|
|
443
|
-
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
444
|
-
return config.validation?.thresholds ?? {};
|
|
445
|
-
} catch {
|
|
446
|
-
return {};
|
|
447
|
-
}
|
|
448
|
-
}
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
* - applyTaskScope() from src/lib/tasks/test-scope.js (the task-scoped cut, #87)
|
|
19
19
|
* - runValidation() from ./validation-runner.js
|
|
20
20
|
* - runE2eCheck() from framework/hooks/shared/e2e-check.js
|
|
21
|
-
* - loadRubric() + aggregateScores() from ./score-aggregator.js
|
|
22
21
|
*
|
|
23
22
|
* @module verify-runner
|
|
24
23
|
*/
|
|
@@ -44,7 +43,6 @@ import {
|
|
|
44
43
|
evalEmptyRunProblem, evalColdCacheWarning,
|
|
45
44
|
} from '../tasks/eval-node.js';
|
|
46
45
|
import { runValidation } from './validation-runner.js';
|
|
47
|
-
import { loadRubric, aggregateScores } from './score-aggregator.js';
|
|
48
46
|
import { checkOutputsTraceability } from './traceability-check.js';
|
|
49
47
|
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
50
48
|
import { createHeavyGate } from '../heavy/gate.js';
|
|
@@ -52,6 +50,9 @@ import { createHeavyGate } from '../heavy/gate.js';
|
|
|
52
50
|
/** Default build timeout — dotnet cold build is slow; 30s (the Stop-hook default) is too short. */
|
|
53
51
|
const DEFAULT_BUILD_TIMEOUT = 120_000;
|
|
54
52
|
|
|
53
|
+
// Validator issues surfaced in `feedback`, errors first; the full list stays on the node.
|
|
54
|
+
const VALIDATOR_FEEDBACK_LIMIT = 10;
|
|
55
|
+
|
|
55
56
|
/** Number of trailing output lines kept for feedback/reporting. */
|
|
56
57
|
const OUTPUT_TAIL_LINES = 20;
|
|
57
58
|
|
|
@@ -87,7 +88,6 @@ export class VerifyUsageError extends Error {
|
|
|
87
88
|
* @param {string} featureName - Feature name
|
|
88
89
|
* @param {string|null} [taskId] - Task ID for task-scoped verify, or null/undefined for feature scope
|
|
89
90
|
* @param {Object} [options]
|
|
90
|
-
* @param {string} [options.rubric] - Force a rubric id (highest precedence)
|
|
91
91
|
* @param {number} [options.timeout] - Timeout in ms for the build AND tests nodes
|
|
92
92
|
* (default: 120000 for build; for tests, see resolveTestTimeout())
|
|
93
93
|
* @param {boolean} [options.skipTests] - Skip the tests node
|
|
@@ -121,7 +121,6 @@ export async function runVerify(projectPath, featureName, taskId, options = {})
|
|
|
121
121
|
|
|
122
122
|
async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
123
123
|
const {
|
|
124
|
-
rubric: rubricOverride,
|
|
125
124
|
timeout = DEFAULT_BUILD_TIMEOUT,
|
|
126
125
|
skipTests = false,
|
|
127
126
|
skipBuild = false,
|
|
@@ -157,9 +156,9 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
157
156
|
);
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
// The task is read ONCE: its group picks the
|
|
161
|
-
//
|
|
162
|
-
//
|
|
159
|
+
// The task is read ONCE: its group picks the e2e applicability and, with its
|
|
160
|
+
// outputs, the test scope. One reader of tasks.json for verify, so the two
|
|
161
|
+
// can never disagree about the task.
|
|
163
162
|
const task = taskId ? readTask(projectPath, featureName, taskId) : null;
|
|
164
163
|
|
|
165
164
|
// `--record-baseline` is judged in the same breath, and for the same reason:
|
|
@@ -253,8 +252,7 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
253
252
|
appendTestsFeedback(feedback, nodes.tests);
|
|
254
253
|
|
|
255
254
|
// 3. validators — ALWAYS runs, even on a red build (free static feedback).
|
|
256
|
-
|
|
257
|
-
nodes.validators = await runValidatorsNode({ projectPath, featureName, rubricId, _runValidation });
|
|
255
|
+
nodes.validators = await runValidatorsNode({ projectPath, featureName, _runValidation });
|
|
258
256
|
appendValidatorsFeedback(feedback, nodes.validators);
|
|
259
257
|
|
|
260
258
|
// 4. e2e — the only node that runs the application. Like tests, it is not
|
|
@@ -298,7 +296,6 @@ async function runVerifyGated(projectPath, featureName, taskId, options, gate) {
|
|
|
298
296
|
task: taskId || null,
|
|
299
297
|
scope: taskId ? 'task' : 'feature',
|
|
300
298
|
stack,
|
|
301
|
-
rubric: rubricId,
|
|
302
299
|
status,
|
|
303
300
|
nodes,
|
|
304
301
|
feedback,
|
|
@@ -972,7 +969,7 @@ function recordBaselineFrom(projectPath, testsNode) {
|
|
|
972
969
|
// Validators node
|
|
973
970
|
// ============================================================================
|
|
974
971
|
|
|
975
|
-
async function runValidatorsNode({ projectPath, featureName,
|
|
972
|
+
async function runValidatorsNode({ projectPath, featureName, _runValidation }) {
|
|
976
973
|
const start = Date.now();
|
|
977
974
|
|
|
978
975
|
// runValidation() prints chalk-formatted progress to stdout. verify emits
|
|
@@ -997,46 +994,40 @@ async function runValidatorsNode({ projectPath, featureName, rubricId, _runValid
|
|
|
997
994
|
if (validationResult && validationResult._skipReason) {
|
|
998
995
|
return {
|
|
999
996
|
status: 'skip',
|
|
1000
|
-
score: null,
|
|
1001
|
-
action: null,
|
|
1002
997
|
ran: [],
|
|
1003
998
|
skipped,
|
|
1004
999
|
reason: validationResult._skipReason,
|
|
1005
|
-
|
|
1000
|
+
issues: [],
|
|
1006
1001
|
durationMs,
|
|
1007
1002
|
};
|
|
1008
1003
|
}
|
|
1009
1004
|
|
|
1010
|
-
const rubric = loadRubric(projectPath, rubricId);
|
|
1011
|
-
const scoreResult = aggregateScores(validationResult, rubric);
|
|
1012
|
-
|
|
1013
1005
|
return {
|
|
1014
1006
|
status: validationResult.passed === false ? 'fail' : 'pass',
|
|
1015
|
-
score: scoreResult.score,
|
|
1016
|
-
action: scoreResult.action,
|
|
1017
1007
|
ran: Object.keys(validationResult.results || {}),
|
|
1018
1008
|
skipped,
|
|
1019
1009
|
reason: null,
|
|
1020
|
-
|
|
1010
|
+
issues: validationResult.issues || [],
|
|
1021
1011
|
durationMs,
|
|
1022
1012
|
};
|
|
1023
1013
|
}
|
|
1024
1014
|
|
|
1025
1015
|
function appendValidatorsFeedback(feedback, validatorsNode) {
|
|
1026
1016
|
// A validator that could not check (reuse without a graph) is invisible in a
|
|
1027
|
-
// green
|
|
1017
|
+
// green node; when the whole node skipped, its own line already says why.
|
|
1028
1018
|
if (validatorsNode.status !== 'skip') {
|
|
1029
1019
|
for (const s of validatorsNode.skipped || []) {
|
|
1030
1020
|
if (s.unavailable) feedback.push(`[validators/${s.validator}] não rodou: ${s.reason}`);
|
|
1031
1021
|
}
|
|
1032
1022
|
}
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1023
|
+
const issues = validatorsNode.issues || [];
|
|
1024
|
+
const ordered = [
|
|
1025
|
+
...issues.filter((i) => i.level === 'error'),
|
|
1026
|
+
...issues.filter((i) => i.level !== 'error'),
|
|
1027
|
+
].slice(0, VALIDATOR_FEEDBACK_LIMIT);
|
|
1028
|
+
for (const issue of ordered) {
|
|
1029
|
+
const location = issue.file ? ` (${issue.file}${issue.line ? ':' + issue.line : ''})` : '';
|
|
1030
|
+
feedback.push(`[validators/${issue.validator ?? 'unknown'}] ${issue.message}${location}`);
|
|
1040
1031
|
}
|
|
1041
1032
|
}
|
|
1042
1033
|
|
|
@@ -1105,42 +1096,12 @@ function appendE2eFeedback(feedback, e2eNode) {
|
|
|
1105
1096
|
}
|
|
1106
1097
|
}
|
|
1107
1098
|
|
|
1108
|
-
// ============================================================================
|
|
1109
|
-
// Rubric selection — flag > task group > config > default
|
|
1110
|
-
// ============================================================================
|
|
1111
|
-
|
|
1112
|
-
function resolveRubricId({ projectPath, task, rubricOverride }) {
|
|
1113
|
-
if (rubricOverride) return rubricOverride;
|
|
1114
|
-
|
|
1115
|
-
const mapped = mapGroupToRubric(task?.group);
|
|
1116
|
-
if (mapped) return mapped;
|
|
1117
|
-
|
|
1118
|
-
const configRubric = readConfigRubric(projectPath);
|
|
1119
|
-
if (configRubric) return configRubric;
|
|
1120
|
-
|
|
1121
|
-
return 'default';
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
/**
|
|
1125
|
-
* Map a task's `group` field to a rubric id.
|
|
1126
|
-
* backend|infra → 'backend', frontend → 'frontend', tests|docs|absent → null
|
|
1127
|
-
* (falls through to config/default).
|
|
1128
|
-
*/
|
|
1129
|
-
function mapGroupToRubric(group) {
|
|
1130
|
-
if (!group) return null;
|
|
1131
|
-
const g = String(group).toLowerCase();
|
|
1132
|
-
if (g === 'backend' || g === 'infra') return 'backend';
|
|
1133
|
-
if (g === 'frontend') return 'frontend';
|
|
1134
|
-
return null;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
1099
|
/**
|
|
1138
1100
|
* The task as verify needs it, or null when tasks.json or the task is missing.
|
|
1139
1101
|
*
|
|
1140
|
-
* The ONE reader of tasks.json for verify
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
* a scope start disagreeing about the same task.
|
|
1102
|
+
* The ONE reader of tasks.json for verify: the group picks the e2e
|
|
1103
|
+
* applicability, group + outputs pick the test scope, and the command uses the
|
|
1104
|
+
* null to refuse an unknown task id.
|
|
1144
1105
|
*
|
|
1145
1106
|
* @param {string} projectPath
|
|
1146
1107
|
* @param {string} featureName
|
|
@@ -1163,17 +1124,6 @@ export function readTask(projectPath, featureName, taskId) {
|
|
|
1163
1124
|
}
|
|
1164
1125
|
}
|
|
1165
1126
|
|
|
1166
|
-
function readConfigRubric(projectPath) {
|
|
1167
|
-
try {
|
|
1168
|
-
const configPath = join(projectPath, '.morph', 'config', 'config.json');
|
|
1169
|
-
if (!existsSync(configPath)) return null;
|
|
1170
|
-
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1171
|
-
return config?.validation?.rubric ?? null;
|
|
1172
|
-
} catch {
|
|
1173
|
-
return null;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
1127
|
// ============================================================================
|
|
1178
1128
|
// Shared helpers
|
|
1179
1129
|
// ============================================================================
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { join, dirname, resolve } from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
|
-
import { readFileSync, readdirSync } from 'fs';
|
|
16
|
+
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
17
17
|
import {
|
|
18
18
|
copyDirectory,
|
|
19
19
|
copyFile,
|
|
@@ -89,7 +89,27 @@ export function installRulesForStack(rulesSrc, rulesDest, projectTags, fx = real
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
92
|
+
* Slash commands filtered by `stacks:` frontmatter. Clean-slate like the
|
|
93
|
+
* skills: removes only a framework command that the stack filtered out, never
|
|
94
|
+
* a command the user wrote into `.claude/commands/`.
|
|
95
|
+
*/
|
|
96
|
+
export function installCommandsForStack(commandsSrc, commandsDest, projectTags, fx = realEffects()) {
|
|
97
|
+
const sourceFiles = readdirSync(commandsSrc).filter(f => f.endsWith('.md'));
|
|
98
|
+
let installed = 0;
|
|
99
|
+
for (const file of sourceFiles) {
|
|
100
|
+
const dest = join(commandsDest, file);
|
|
101
|
+
if (shouldInstall(parseStacks(readFileSync(join(commandsSrc, file), 'utf-8')), projectTags)) {
|
|
102
|
+
fx.copyFile(join(commandsSrc, file), dest);
|
|
103
|
+
installed++;
|
|
104
|
+
} else if (!fx.dryRun && existsSync(dest)) {
|
|
105
|
+
fx.remove(dest);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { installed };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Re-install only stack-aware assets (commands, rules, skills, agents) using current config.json.
|
|
93
113
|
* Skips infrastructure steps (dirs, framework copy, hooks).
|
|
94
114
|
*
|
|
95
115
|
* @param {string} targetPath - Absolute path to the target project directory
|
|
@@ -109,8 +129,16 @@ async function installStackAwareAssets(targetPath, log) {
|
|
|
109
129
|
} catch { /* ignore malformed config */ }
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
log('Installing stack-filtered rules...');
|
|
113
132
|
const claudeDest = join(targetPath, '.claude');
|
|
133
|
+
log('Installing stack-filtered commands...');
|
|
134
|
+
const commandsSrc = join(FRAMEWORK_DIR, 'commands');
|
|
135
|
+
const commandsDest = join(claudeDest, 'commands');
|
|
136
|
+
if (await pathExists(commandsSrc)) {
|
|
137
|
+
await ensureDir(commandsDest);
|
|
138
|
+
installCommandsForStack(commandsSrc, commandsDest, projectTags);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
log('Installing stack-filtered rules...');
|
|
114
142
|
const rulesSrc = join(FRAMEWORK_DIR, 'rules');
|
|
115
143
|
const rulesDest = join(claudeDest, 'rules');
|
|
116
144
|
if (await pathExists(rulesSrc)) {
|
|
@@ -336,18 +364,8 @@ export async function setupInfra(targetPath, {
|
|
|
336
364
|
await copyDirectory(hooksSrc, hooksDest);
|
|
337
365
|
}
|
|
338
366
|
|
|
339
|
-
// --- 7b. Copy framework/rubrics → .morph/framework/rubrics ---
|
|
340
|
-
log('Step 7b: Copying rubrics...');
|
|
341
|
-
const rubricsSrc = join(FRAMEWORK_DIR, 'rubrics');
|
|
342
|
-
const rubricsDest = join(frameworkDestDir, 'rubrics');
|
|
343
|
-
if (await pathExists(rubricsSrc)) {
|
|
344
|
-
await copyDirectory(rubricsSrc, rubricsDest);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
367
|
// --- 7c. Copy framework/evals → .morph/framework/evals ---
|
|
348
|
-
|
|
349
|
-
// morph-review/agents.json via .morph/framework/evals/.
|
|
350
|
-
log('Step 7c: Copying evals (rubrics .md + trace schema)...');
|
|
368
|
+
log('Step 7c: Copying evals (review checklist + trace schema)...');
|
|
351
369
|
const evalsSrc = join(FRAMEWORK_DIR, 'evals');
|
|
352
370
|
const evalsDest = join(frameworkDestDir, 'evals');
|
|
353
371
|
if (await pathExists(evalsSrc)) {
|
|
@@ -396,16 +414,9 @@ export async function setupInfra(targetPath, {
|
|
|
396
414
|
await copyDirectory(schemasSrc, schemasDest);
|
|
397
415
|
}
|
|
398
416
|
|
|
399
|
-
// --- 9. Copy framework/commands → .claude/commands ---
|
|
400
|
-
log('Step 9: Setting up Claude Code integration...');
|
|
401
417
|
const claudeDest = join(targetPath, '.claude');
|
|
402
|
-
const commandsSrc = join(FRAMEWORK_DIR, 'commands');
|
|
403
|
-
const commandsDest = join(claudeDest, 'commands');
|
|
404
|
-
if (await pathExists(commandsSrc)) {
|
|
405
|
-
await copyDirectory(commandsSrc, commandsDest);
|
|
406
|
-
}
|
|
407
418
|
|
|
408
|
-
// ---
|
|
419
|
+
// --- 9-13. Install stack-aware assets (commands, rules, skills, agents) ---
|
|
409
420
|
log('Steps 10-13: Installing stack-aware assets...');
|
|
410
421
|
const { agentCounts, domainCounts, projectTags } = await installStackAwareAssets(targetPath, log);
|
|
411
422
|
|
|
@@ -431,10 +431,8 @@ function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
|
|
|
431
431
|
// Canonical 1:1 naming: frontmatter name === filename === subagent_type.
|
|
432
432
|
`name: morph-${agent.id}`,
|
|
433
433
|
`description: ${description}`,
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
// scout → sonnet (measured: same cost as haiku, fewer rejected maps). Falls back to `inherit` only for
|
|
437
|
-
// personas that omit the field.
|
|
434
|
+
// The evaluator is sonnet here; the dispatch routes it to opus when the risk
|
|
435
|
+
// detector flags the diff. A persona without the field inherits the session model.
|
|
438
436
|
`model: ${agent.model ?? 'inherit'}`,
|
|
439
437
|
`tools: ${buildToolsLine(agent, knownMcpIds)}`,
|
|
440
438
|
// Per persona in agents.json (ADR-004); absent → DEFAULT_MAX_TURNS. The
|
|
@@ -487,11 +485,25 @@ function buildBody(agent, maxTurns) {
|
|
|
487
485
|
}
|
|
488
486
|
}
|
|
489
487
|
|
|
488
|
+
if (isImplementer(agent)) {
|
|
489
|
+
lines.push(
|
|
490
|
+
'## How you operate',
|
|
491
|
+
'',
|
|
492
|
+
'You are a domain persona dispatched per task. Read the spec/tasks/standards passed to you, do the work, and report each doneCriteria with its evidence. Refer to MORPH.md section 6 for the dispatch contract.',
|
|
493
|
+
'',
|
|
494
|
+
);
|
|
495
|
+
} else {
|
|
496
|
+
// A read-only persona has no task to do nor doneCriteria to report.
|
|
497
|
+
lines.push(
|
|
498
|
+
'## Como você opera',
|
|
499
|
+
'',
|
|
500
|
+
'Você é read-only: nunca escreva, edite nem reverta código, testes, `tasks.json` ou `feature.json` (nada de `git checkout --`). ' +
|
|
501
|
+
'O entregável é o arquivo de relatório que o dispatch nomeia; sem arquivo nomeado, é a sua mensagem final (no scout, o mapa).',
|
|
502
|
+
'',
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
490
506
|
lines.push(
|
|
491
|
-
'## How you operate',
|
|
492
|
-
'',
|
|
493
|
-
'You are a domain persona dispatched per task. Read the spec/tasks/standards passed to you, do the work, and report each doneCriteria with its evidence. Refer to MORPH.md section 6 for the dispatch contract.',
|
|
494
|
-
'',
|
|
495
507
|
// `PARCIAL` on line 1 is what report-check reads; "no verdict" keeps an
|
|
496
508
|
// evaluator partial from carrying a Verdict the gate would read as final.
|
|
497
509
|
'## Turn budget',
|
package/src/utils/file-copier.js
CHANGED
|
@@ -199,10 +199,12 @@ export async function updateGitignore(projectPath, fx = realEffects()) {
|
|
|
199
199
|
content = await readFile(gitignorePath);
|
|
200
200
|
hasMorphSection = content.includes('# MORPH-SPEC');
|
|
201
201
|
}
|
|
202
|
+
const original = content;
|
|
203
|
+
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
202
204
|
|
|
203
205
|
// If MORPH section already exists, check if rules are up to date
|
|
204
206
|
if (hasMorphSection) {
|
|
205
|
-
const lines = content.split(
|
|
207
|
+
const lines = content.split(/\r?\n/);
|
|
206
208
|
const morphStartIndex = lines.findIndex(line => line.trim() === '# MORPH-SPEC');
|
|
207
209
|
|
|
208
210
|
// Find the end of MORPH section (next empty line or section header)
|
|
@@ -225,17 +227,17 @@ export async function updateGitignore(projectPath, fx = realEffects()) {
|
|
|
225
227
|
if (missingRules.length > 0) {
|
|
226
228
|
// Update the section with all rules
|
|
227
229
|
lines.splice(morphStartIndex, morphEndIndex - morphStartIndex, ...morphRules.slice(1, -1));
|
|
228
|
-
content = lines.join(
|
|
230
|
+
content = lines.join(eol);
|
|
229
231
|
}
|
|
230
232
|
} else {
|
|
231
233
|
// Add MORPH section
|
|
232
234
|
if (content && !content.endsWith('\n')) {
|
|
233
|
-
content +=
|
|
235
|
+
content += eol;
|
|
234
236
|
}
|
|
235
|
-
content += morphRules.join(
|
|
237
|
+
content += morphRules.join(eol);
|
|
236
238
|
}
|
|
237
239
|
|
|
238
|
-
fx.writeFile(gitignorePath, content);
|
|
240
|
+
if (content !== original) fx.writeFile(gitignorePath, content);
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
/**
|