@polymorphism-tech/morph-spec 8.47.0 → 9.0.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 +48 -39
- package/README.md +3 -4
- package/bin/morph-spec.js +37 -51
- package/bin/validate.js +3 -111
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +7 -21
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +64 -27
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +2 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- 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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +77 -152
- package/framework/standards/STANDARDS.json +86 -1218
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/architecture/code-comments.md +6 -6
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +4 -0
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +17 -1
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +81 -141
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +85 -84
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +40 -72
- package/src/core/paths/output-schema.js +19 -66
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +5 -5
- package/src/dashboard/app/components/EntityLink.jsx +1 -1
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +5 -15
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +2 -2
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +131 -140
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +22 -45
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/verify-runner.js +118 -235
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +9 -0
- package/src/utils/agents-installer.js +76 -9
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +27 -1
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import { getProjectTags } from '../stack-filter.js';
|
|
4
|
+
import { PACKAGE_LOCATIONS } from '../detectors/index.js';
|
|
5
|
+
import { isTestPath } from '../graph/sanitize.js';
|
|
6
|
+
import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
|
|
7
|
+
|
|
8
|
+
export const SEVERITIES = ['Bloqueante', 'Alto', 'Médio', 'Baixo'];
|
|
9
|
+
export const MAX_FILES = 20000;
|
|
10
|
+
|
|
11
|
+
const SKIP_DIRS = new Set([
|
|
12
|
+
'node_modules', '.git', 'bin', 'obj', 'dist', 'build', 'out', 'coverage', '.next', '.morph', '.claude', '.vs', '.idea',
|
|
13
|
+
...WORKTREE_DIR_NAMES,
|
|
14
|
+
]);
|
|
15
|
+
const SOURCE_EXT = /\.(cs|csproj|sln|props|ts|tsx)$/i;
|
|
16
|
+
const DOTNET_EXPECTED_MAJOR = 10;
|
|
17
|
+
const WHERE_LIMIT = 5;
|
|
18
|
+
|
|
19
|
+
function walk(root) {
|
|
20
|
+
const files = [];
|
|
21
|
+
const dirs = new Set();
|
|
22
|
+
let unreadable = 0;
|
|
23
|
+
let truncated = false;
|
|
24
|
+
const stack = [root];
|
|
25
|
+
while (stack.length > 0) {
|
|
26
|
+
const dir = stack.pop();
|
|
27
|
+
let entries;
|
|
28
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { unreadable++; continue; }
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
const full = join(dir, e.name);
|
|
31
|
+
if (e.isDirectory()) {
|
|
32
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
33
|
+
dirs.add(relative(root, full).replace(/\\/g, '/'));
|
|
34
|
+
stack.push(full);
|
|
35
|
+
} else if (SOURCE_EXT.test(e.name) || e.name === 'package.json') {
|
|
36
|
+
if (files.length >= MAX_FILES) { truncated = true; continue; }
|
|
37
|
+
files.push(relative(root, full).replace(/\\/g, '/'));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { files, dirs, unreadable, truncated };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function reader(root) {
|
|
45
|
+
const failed = new Set();
|
|
46
|
+
const read = (file) => {
|
|
47
|
+
try { return readFileSync(join(root, file), 'utf8'); } catch { failed.add(file); return null; }
|
|
48
|
+
};
|
|
49
|
+
return { read, failed };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const majorOf = (version) => {
|
|
53
|
+
const m = /(\d+)/.exec(String(version ?? ''));
|
|
54
|
+
return m ? Number(m[1]) : null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function dotnetPackages(files, read) {
|
|
58
|
+
const packages = new Map();
|
|
59
|
+
for (const f of files.filter((x) => /\.(csproj|props)$/i.test(x))) {
|
|
60
|
+
const text = read(f);
|
|
61
|
+
if (!text) continue;
|
|
62
|
+
for (const m of text.matchAll(/<Package(?:Reference|Version)\s+Include="([^"]+)"(?:\s+Version="([^"]*)")?/g)) {
|
|
63
|
+
if (!packages.has(m[1]) || m[2]) packages.set(m[1], m[2] ?? packages.get(m[1]) ?? null);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return packages;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function grepFiles(files, read, re) {
|
|
70
|
+
const hits = [];
|
|
71
|
+
for (const f of files) {
|
|
72
|
+
const text = read(f);
|
|
73
|
+
if (text && re.test(text)) hits.push(f);
|
|
74
|
+
}
|
|
75
|
+
return hits;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function auditDotnet(files, read) {
|
|
79
|
+
const rows = [];
|
|
80
|
+
const deviations = [];
|
|
81
|
+
const add = (severity, rule, where, migration) => deviations.push({ severity, stack: 'dotnet', rule, where, migration });
|
|
82
|
+
const projects = files.filter((f) => f.endsWith('.csproj'));
|
|
83
|
+
const source = files.filter((f) => f.endsWith('.cs') && !isTestPath(f));
|
|
84
|
+
|
|
85
|
+
const frameworks = new Map();
|
|
86
|
+
for (const p of projects) {
|
|
87
|
+
const m = /<TargetFrameworks?>([^<]+)</.exec(read(p) ?? '');
|
|
88
|
+
if (m) for (const tf of m[1].split(';').map((s) => s.trim()).filter(Boolean)) frameworks.set(tf, [...(frameworks.get(tf) ?? []), p]);
|
|
89
|
+
}
|
|
90
|
+
const tfs = [...frameworks.keys()];
|
|
91
|
+
const old = tfs.filter((tf) => /^net\d/.test(tf) && majorOf(tf) < DOTNET_EXPECTED_MAJOR);
|
|
92
|
+
rows.push({ layer: 'Runtime', detected: tfs.join(', ') || 'nenhum TargetFramework', expected: `net${DOTNET_EXPECTED_MAJOR}.0`, ok: tfs.length > 0 && old.length === 0 });
|
|
93
|
+
for (const tf of old) add('Bloqueante', `Runtime diferente (${tf})`, frameworks.get(tf), `subir o TargetFramework para net${DOTNET_EXPECTED_MAJOR}.0`);
|
|
94
|
+
|
|
95
|
+
const packages = dotnetPackages(files, read);
|
|
96
|
+
const has = (re) => [...packages.keys()].filter((p) => re.test(p));
|
|
97
|
+
const ef = has(/^Microsoft\.EntityFrameworkCore/);
|
|
98
|
+
const npgsql = has(/Npgsql/);
|
|
99
|
+
const otherDb = has(/^(Microsoft\.EntityFrameworkCore\.SqlServer|Pomelo\.EntityFrameworkCore\.MySql|MySql\.|MySqlConnector|Microsoft\.Data\.SqlClient|System\.Data\.SqlClient)/);
|
|
100
|
+
rows.push({ layer: 'ORM', detected: ef.length > 0 ? 'EF Core' : (packages.has('Dapper') ? 'Dapper' : 'ausente'), expected: 'EF Core', ok: ef.length > 0 });
|
|
101
|
+
rows.push({ layer: 'Banco', detected: npgsql.length > 0 ? 'Postgres (Npgsql)' : (otherDb.join(', ') || 'ausente'), expected: 'Neon (Postgres)', ok: npgsql.length > 0 && otherDb.length === 0 });
|
|
102
|
+
rows.push({ layer: 'IA', detected: has(/^Microsoft\.(Agents\.AI|Extensions\.AI)/).join(', ') || 'ausente', expected: 'MAF quando houver agentes', ok: true });
|
|
103
|
+
rows.push({ layer: 'Jobs', detected: has(/^Hangfire/).length > 0 ? 'Hangfire' : 'ausente', expected: 'Hangfire só com razão real', ok: true });
|
|
104
|
+
if (packages.has('Dapper') && ef.length === 0) add('Bloqueante', 'ORM diferente (Dapper puro sem EF Core)', projects, 'migrar o acesso a dados para EF Core');
|
|
105
|
+
if (otherDb.length > 0) add('Bloqueante', `Banco diferente (${otherDb.join(', ')})`, projects, 'migrar para Neon (Postgres, Npgsql)');
|
|
106
|
+
for (const p of has(/^Microsoft\.(EntityFrameworkCore|AspNetCore|Extensions)(?!\.AI)/)) {
|
|
107
|
+
const major = majorOf(packages.get(p));
|
|
108
|
+
if (major !== null && major < DOTNET_EXPECTED_MAJOR) add('Baixo', `Pacote desatualizado mas compatível (${p} ${packages.get(p)})`, [], `subir para ${DOTNET_EXPECTED_MAJOR}.x`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const newGuid = grepFiles(source, read, /Guid\.NewGuid\(\)/);
|
|
112
|
+
if (newGuid.length > 0) add('Alto', '`Guid.NewGuid()`', newGuid, 'trocar por `Guid.CreateVersion7()`');
|
|
113
|
+
const repository = grepFiles(source, read, /interface\s+(IRepository\s*<|IUnitOfWork\b)/);
|
|
114
|
+
if (repository.length > 0) {
|
|
115
|
+
const di = grepFiles(source, read, /Add(Scoped|Transient|Singleton)\s*(<\s*(IRepository|IUnitOfWork)|\(\s*typeof\s*\(\s*(IRepository|IUnitOfWork))/);
|
|
116
|
+
add('Alto', 'Repository genérico (`IRepository<T>`/`IUnitOfWork`)', [...repository, ...di],
|
|
117
|
+
'código morto: deletar a pasta `Repository/` e as 2 linhas de DI; confirme os consumidores com `morph-spec graph affected IRepository --uses-only`');
|
|
118
|
+
}
|
|
119
|
+
const serviceLayer = source.filter((f) => {
|
|
120
|
+
const text = read(f);
|
|
121
|
+
return text && /\bclass\s+\w+(Service|Manager)\b/.test(text) && /DbContext/.test(text);
|
|
122
|
+
});
|
|
123
|
+
if (serviceLayer.length > 0) add('Alto', 'Camada de Service/Manager entre handler e dados', serviceLayer, 'o handler fala com o `ApplicationDbContext` direto');
|
|
124
|
+
const rawSql = grepFiles(source, read, /\b(FromSqlRaw|ExecuteSqlRaw|SqlQueryRaw|ExecuteQuery)\s*\(/);
|
|
125
|
+
if (rawSql.length > 0) add('Alto', 'SQL cru', rawSql, 'LINQ, ou store dedicado com justificativa em ADR');
|
|
126
|
+
const notSealed = source.filter((f) => {
|
|
127
|
+
const text = read(f) ?? '';
|
|
128
|
+
return [...text.matchAll(/^\s*(?:public|internal)\s+((?:\w+\s+)*)class\s+\w+/gm)]
|
|
129
|
+
.some((m) => !/\b(sealed|abstract|static|partial)\b/.test(m[1]));
|
|
130
|
+
});
|
|
131
|
+
if (notSealed.length > 0) add('Médio', 'Classes não-sealed', notSealed, '`sealed` por padrão');
|
|
132
|
+
const controllers = grepFiles(source, read, /:\s*Controller(Base)?\b/);
|
|
133
|
+
rows.push({ layer: 'Estrutura', detected: controllers.length > 0 ? `${controllers.length} controller(s) MVC` : 'sem controllers', expected: 'VSA (endpoint por slice)', ok: controllers.length === 0 });
|
|
134
|
+
return { rows, deviations };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const UI_LIBRARIES = ['@mui/material', 'antd', '@chakra-ui/react', 'bootstrap', 'react-bootstrap', '@mantine/core'];
|
|
138
|
+
|
|
139
|
+
function auditNextjs(root, read) {
|
|
140
|
+
const rows = [];
|
|
141
|
+
const deviations = [];
|
|
142
|
+
const add = (severity, rule, where, migration) => deviations.push({ severity, stack: 'nextjs', rule, where, migration });
|
|
143
|
+
const packages = PACKAGE_LOCATIONS
|
|
144
|
+
.map((loc) => ({ loc, file: loc === '.' ? 'package.json' : `${loc}/package.json` }))
|
|
145
|
+
.filter(({ file }) => existsSync(join(root, file)))
|
|
146
|
+
.map((p) => {
|
|
147
|
+
const text = read(p.file);
|
|
148
|
+
let deps = null;
|
|
149
|
+
try { const pkg = JSON.parse(text); deps = { ...pkg.dependencies, ...pkg.devDependencies }; } catch { deps = null; }
|
|
150
|
+
return { ...p, deps };
|
|
151
|
+
})
|
|
152
|
+
.filter((p) => p.deps);
|
|
153
|
+
const next = packages.find((p) => p.deps.next);
|
|
154
|
+
if (!next) {
|
|
155
|
+
const other = packages.find((p) => p.deps.react || p.deps.vue);
|
|
156
|
+
rows.push({ layer: 'Frontend', detected: other ? (other.deps.vue ? 'Vue' : 'React sem Next.js') : 'sem frontend', expected: 'Next.js App Router', ok: !other });
|
|
157
|
+
if (other) add('Bloqueante', 'Frontend sem Next.js', [other.file], 'migrar para Next.js App Router');
|
|
158
|
+
return { rows, deviations };
|
|
159
|
+
}
|
|
160
|
+
const base = next.loc === '.' ? '' : `${next.loc}/`;
|
|
161
|
+
const isDir = (p) => { try { return statSync(join(root, p)).isDirectory(); } catch { return false; } };
|
|
162
|
+
const appRouter = isDir(`${base}app`) || isDir(`${base}src/app`);
|
|
163
|
+
const pagesRouter = isDir(`${base}pages`) || isDir(`${base}src/pages`);
|
|
164
|
+
rows.push({ layer: 'Frontend', detected: `Next.js ${next.deps.next}`, expected: 'Next.js App Router', ok: true });
|
|
165
|
+
rows.push({ layer: 'Roteamento', detected: appRouter ? 'App Router' : (pagesRouter ? 'Pages Router' : 'não detectado'), expected: 'App Router', ok: appRouter || !pagesRouter });
|
|
166
|
+
if (pagesRouter && !appRouter) add('Médio', 'Pages Router em vez de App Router', [next.file], 'migrar as rotas para `app/`');
|
|
167
|
+
rows.push({ layer: 'Estilo', detected: next.deps.tailwindcss ? 'Tailwind' : 'ausente', expected: 'Tailwind', ok: Boolean(next.deps.tailwindcss) });
|
|
168
|
+
rows.push({ layer: 'Motion', detected: next.deps['framer-motion'] || next.deps.motion ? 'Framer Motion' : 'ausente', expected: 'Framer Motion', ok: true });
|
|
169
|
+
const shadcn = existsSync(join(root, base, 'components.json'));
|
|
170
|
+
const otherUi = UI_LIBRARIES.filter((lib) => next.deps[lib]);
|
|
171
|
+
rows.push({ layer: 'UI', detected: otherUi.join(', ') || (shadcn ? 'shadcn' : 'ausente'), expected: 'shadcn', ok: otherUi.length === 0 });
|
|
172
|
+
if (otherUi.length > 0) add('Médio', `Biblioteca de UI diferente (${otherUi.join(', ')})`, [next.file], 'shadcn sobre Tailwind');
|
|
173
|
+
return { rows, deviations };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const AUDITS = { dotnet: auditDotnet, nextjs: auditNextjs };
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Deterministic stack audit of the tree at `root`, driven by `config.project.stack`:
|
|
180
|
+
* only the configured stacks are judged; no configured stack means no rule runs.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} root
|
|
183
|
+
* @param {object|null} config the parsed `.morph/config/config.json`
|
|
184
|
+
*/
|
|
185
|
+
export function scanStack(root, config) {
|
|
186
|
+
const stack = typeof config?.project?.stack === 'string' ? config.project.stack : null;
|
|
187
|
+
const tags = getProjectTags(config);
|
|
188
|
+
const { files, unreadable, truncated } = walk(root);
|
|
189
|
+
const { read, failed } = reader(root);
|
|
190
|
+
const hasDotnet = files.some((f) => /\.(csproj|sln|cs)$/i.test(f));
|
|
191
|
+
const hasNode = files.some((f) => f.endsWith('package.json') || /\.(ts|tsx)$/i.test(f));
|
|
192
|
+
const greenfield = !hasDotnet && !hasNode;
|
|
193
|
+
|
|
194
|
+
const rows = [];
|
|
195
|
+
const deviations = [];
|
|
196
|
+
const notes = [];
|
|
197
|
+
if (!greenfield) {
|
|
198
|
+
for (const tag of tags) {
|
|
199
|
+
const audit = AUDITS[tag];
|
|
200
|
+
if (!audit) continue;
|
|
201
|
+
const result = tag === 'nextjs' ? audit(root, read) : audit(files, read);
|
|
202
|
+
rows.push(...result.rows);
|
|
203
|
+
deviations.push(...result.deviations);
|
|
204
|
+
}
|
|
205
|
+
if (hasDotnet && !tags.includes('dotnet')) notes.push('Há código .NET, mas `dotnet` não está em `project.stack` — não auditado.');
|
|
206
|
+
if (hasNode && !tags.includes('nextjs') && files.some((f) => f.endsWith('package.json'))) notes.push('Há `package.json`, mas `nextjs` não está em `project.stack` — não auditado.');
|
|
207
|
+
}
|
|
208
|
+
const unjudged = tags.filter((t) => !AUDITS[t]);
|
|
209
|
+
if (unjudged.length > 0) notes.push(`Sem regras de varredura para: ${unjudged.join(', ')}.`);
|
|
210
|
+
if (truncated) notes.push(`Varredura parou em ${MAX_FILES} arquivos.`);
|
|
211
|
+
if (unreadable + failed.size > 0) notes.push(`${unreadable + failed.size} arquivo(s) ou pasta(s) ilegível(is) ignorado(s).`);
|
|
212
|
+
|
|
213
|
+
deviations.sort((a, b) => SEVERITIES.indexOf(a.severity) - SEVERITIES.indexOf(b.severity));
|
|
214
|
+
const audited = tags.some((t) => AUDITS[t]);
|
|
215
|
+
return { stack, tags, greenfield, audited, rows, deviations, notes, filesScanned: files.length };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const cell = (s) => String(s).replace(/\|/g, '\\|');
|
|
219
|
+
|
|
220
|
+
function whereText(where) {
|
|
221
|
+
if (!where || where.length === 0) return '';
|
|
222
|
+
const unique = [...new Set(where)];
|
|
223
|
+
const shown = unique.slice(0, WHERE_LIMIT).map((w) => `\`${w}\``).join(', ');
|
|
224
|
+
return ` — onde: ${shown}${unique.length > WHERE_LIMIT ? ` (+${unique.length - WHERE_LIMIT})` : ''} (${unique.length} arquivo(s))`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function notAuditedReason(result) {
|
|
228
|
+
return result.stack
|
|
229
|
+
? `não auditado: nenhuma regra de varredura para \`${result.stack}\``
|
|
230
|
+
: 'não auditado: project.stack ausente em .morph/config/config.json';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The scan as the markdown report the morph-plan skill reads. */
|
|
234
|
+
export function renderStackScan(result, title) {
|
|
235
|
+
const out = [`# Stack Scan — ${title}`, ''];
|
|
236
|
+
out.push(`**Stack configurado:** ${result.stack ? `\`${result.stack}\`` : 'ausente em `project.stack` — nenhuma regra aplicada'}`);
|
|
237
|
+
const blocking = result.deviations.filter((d) => d.severity === 'Bloqueante' || d.severity === 'Alto');
|
|
238
|
+
const status = result.greenfield ? 'GREENFIELD (sem código fonte)'
|
|
239
|
+
: !result.audited ? notAuditedReason(result)
|
|
240
|
+
: result.deviations.length === 0 ? 'Em compliance' : `Desvios detectados (${result.deviations.length})`;
|
|
241
|
+
out.push(`**Resultado:** ${status}`, '');
|
|
242
|
+
|
|
243
|
+
if (result.rows.length > 0) {
|
|
244
|
+
out.push('## Stack detectado', '', '| Camada | Detectado | Esperado | Status |', '|---|---|---|---|');
|
|
245
|
+
for (const r of result.rows) out.push(`| ${cell(r.layer)} | ${cell(r.detected)} | ${cell(r.expected)} | ${r.ok ? '✓' : '✗'} |`);
|
|
246
|
+
out.push('');
|
|
247
|
+
}
|
|
248
|
+
if (result.deviations.length > 0) {
|
|
249
|
+
out.push('## Desvios (por severidade)', '');
|
|
250
|
+
for (const severity of SEVERITIES) {
|
|
251
|
+
const list = result.deviations.filter((d) => d.severity === severity);
|
|
252
|
+
if (list.length === 0) continue;
|
|
253
|
+
out.push(`### ${severity}`, '');
|
|
254
|
+
for (const d of list) out.push(`- ${d.rule}${whereText(d.where)} — migração: ${d.migration}`);
|
|
255
|
+
out.push('');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
out.push('## Notas', '');
|
|
259
|
+
out.push('- Contagens por varredura de texto (grep): superestimam consumidores, porque não separam herança de uso. Para um símbolo, `morph-spec graph affected <símbolo> --uses-only`.');
|
|
260
|
+
for (const n of result.notes) out.push(`- ${n}`);
|
|
261
|
+
out.push('', '## Recomendação', '');
|
|
262
|
+
if (result.greenfield) out.push('Compliance trivial: aplicar o stack padrão de `MORPH.md`.');
|
|
263
|
+
else if (!result.audited) out.push('Nenhuma regra rodou, então nada aqui atesta compliance: declare `project.stack` e rode o stack-scan de novo.');
|
|
264
|
+
else if (blocking.length > 0) out.push('Há desvio Bloqueante ou Alto: entra na árvore de decisões como "a migração entra nesta feature ou vira feature separada?".');
|
|
265
|
+
else out.push('Nenhum desvio Bloqueante ou Alto: prossiga com o plano.');
|
|
266
|
+
return `${out.join('\n')}\n`;
|
|
267
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
|
|
4
|
+
export const STANDARDS_FILE = 'STANDARDS.json';
|
|
5
|
+
export const STANDARDS_META_FILE = 'STANDARDS.meta.json';
|
|
6
|
+
|
|
7
|
+
/** Per-standard fields that live in the sidecar, keyed by id: no runtime reader needs them to pick a standard. */
|
|
8
|
+
export const META_FIELDS = ['aliases', 'verifiedAgainst', 'provedBy'];
|
|
9
|
+
|
|
10
|
+
export function metaPathFor(standardsJsonPath) {
|
|
11
|
+
return join(dirname(standardsJsonPath), STANDARDS_META_FILE);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readMeta(standardsJsonPath) {
|
|
15
|
+
const path = metaPathFor(standardsJsonPath);
|
|
16
|
+
if (!existsSync(path)) return { standards: {}, sharedAliases: {} };
|
|
17
|
+
try {
|
|
18
|
+
const meta = JSON.parse(readFileSync(path, 'utf8'));
|
|
19
|
+
return { standards: meta?.standards || {}, sharedAliases: meta?.sharedAliases || {} };
|
|
20
|
+
} catch {
|
|
21
|
+
return { standards: {}, sharedAliases: {} };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The registry as every reader sees it: runtime entries with their sidecar
|
|
27
|
+
* fields merged back by id. A missing or unreadable sidecar degrades to "no
|
|
28
|
+
* aliases"; a missing or unreadable STANDARDS.json throws.
|
|
29
|
+
* @param {string} standardsJsonPath
|
|
30
|
+
* @returns {{ version: string|null, standards: object[], sharedAliases: Record<string, string[]> }}
|
|
31
|
+
*/
|
|
32
|
+
export function loadStandardsRegistry(standardsJsonPath) {
|
|
33
|
+
const runtime = JSON.parse(readFileSync(standardsJsonPath, 'utf8'));
|
|
34
|
+
const meta = readMeta(standardsJsonPath);
|
|
35
|
+
const standards = (Array.isArray(runtime.standards) ? runtime.standards : []).map((s) => {
|
|
36
|
+
const extra = meta.standards[s.id];
|
|
37
|
+
if (!extra) return s;
|
|
38
|
+
const merged = { ...s };
|
|
39
|
+
for (const field of META_FIELDS) if (extra[field] !== undefined) merged[field] = extra[field];
|
|
40
|
+
return merged;
|
|
41
|
+
});
|
|
42
|
+
return { version: runtime.version ?? null, standards, sharedAliases: meta.sharedAliases };
|
|
43
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The literal regex of the `github-slugger` package: anchors are read off the rendered .md.
|
|
2
|
+
const SLUG_STRIP = /[ -⸀-\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~ ]/g;
|
|
3
|
+
|
|
4
|
+
export function ghSlug(heading) {
|
|
5
|
+
return heading.trim().toLowerCase().replace(SLUG_STRIP, '').replace(/ /g, '-');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Every heading outside code fences, with the slug GitHub gives it (`-1`, `-2`
|
|
10
|
+
* for repeats) and the line it sits on.
|
|
11
|
+
* @param {string} text
|
|
12
|
+
* @returns {Array<{ level: number, title: string, slug: string, line: number }>}
|
|
13
|
+
*/
|
|
14
|
+
export function markdownHeadings(text) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const seen = new Map();
|
|
17
|
+
let inFence = false;
|
|
18
|
+
const lines = text.split('\n');
|
|
19
|
+
for (let i = 0; i < lines.length; i++) {
|
|
20
|
+
const line = lines[i];
|
|
21
|
+
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
|
|
22
|
+
if (inFence) continue;
|
|
23
|
+
const m = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line.replace(/\r$/, ''));
|
|
24
|
+
if (!m) continue;
|
|
25
|
+
let slug = ghSlug(m[2]);
|
|
26
|
+
if (seen.has(slug)) {
|
|
27
|
+
const n = seen.get(slug) + 1;
|
|
28
|
+
seen.set(slug, n);
|
|
29
|
+
slug = `${slug}-${n}`;
|
|
30
|
+
} else {
|
|
31
|
+
seen.set(slug, 0);
|
|
32
|
+
}
|
|
33
|
+
out.push({ level: m[1].length, title: m[2], slug, line: i });
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function headingSlugs(text) {
|
|
39
|
+
return new Set(markdownHeadings(text).map((h) => h.slug));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The section a slug opens: its heading through the line before the next
|
|
44
|
+
* heading of the same or a higher level. `null` when no heading has the slug.
|
|
45
|
+
* @param {string} text
|
|
46
|
+
* @param {string} slug - with or without the leading `#`
|
|
47
|
+
* @returns {string|null}
|
|
48
|
+
*/
|
|
49
|
+
export function sectionBySlug(text, slug) {
|
|
50
|
+
const want = String(slug).replace(/^#/, '');
|
|
51
|
+
const all = markdownHeadings(text);
|
|
52
|
+
const idx = all.findIndex((h) => h.slug === want);
|
|
53
|
+
if (idx === -1) return null;
|
|
54
|
+
const start = all[idx];
|
|
55
|
+
const next = all.slice(idx + 1).find((h) => h.level <= start.level);
|
|
56
|
+
const lines = text.split('\n');
|
|
57
|
+
return lines.slice(start.line, next ? next.line : lines.length).join('\n').replace(/\s+$/, '') + '\n';
|
|
58
|
+
}
|
|
@@ -183,26 +183,18 @@ export function readEvalStamp(cacheRoot) {
|
|
|
183
183
|
*
|
|
184
184
|
* **There is no `skip` for a bad cache.** A skip here would recreate the hole
|
|
185
185
|
* this harness closed twice: a green gate over a verification that never
|
|
186
|
-
* happened.
|
|
187
|
-
* per-task loop.
|
|
186
|
+
* happened.
|
|
188
187
|
*
|
|
189
188
|
* @param {string} projectPath
|
|
190
189
|
* @param {object} node - a resolved test node with `runner === 'evals'`
|
|
191
|
-
* @param {{
|
|
192
|
-
* @returns {{status: '
|
|
190
|
+
* @param {{refresh?: boolean}} [options]
|
|
191
|
+
* @returns {{status: 'fail'|null, reason: string|null}} status null means RUN
|
|
193
192
|
*/
|
|
194
193
|
export function evalPreflight(projectPath, node, options = {}) {
|
|
195
|
-
const {
|
|
194
|
+
const { refresh = false } = options;
|
|
196
195
|
|
|
197
196
|
if (node?.runner !== EVAL_RUNNER) return { status: null, reason: null };
|
|
198
197
|
|
|
199
|
-
// Scope first: an eval suite does not pay for itself in a per-task loop, and
|
|
200
|
-
// the result Gate 3 reads is the feature-scoped one. Same treatment `e2e: true`
|
|
201
|
-
// already gets.
|
|
202
|
-
if (taskId) {
|
|
203
|
-
return { status: 'skip', reason: 'eval node runs at feature scope only' };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
198
|
const { cacheRoot, envFile } = resolveEvalPaths(node, projectPath);
|
|
207
199
|
|
|
208
200
|
// In refresh the tree is being (re)written; demanding a populated, stamped
|
|
@@ -51,41 +51,6 @@ import { PACKAGE_LOCATIONS } from '../detectors/index.js';
|
|
|
51
51
|
*/
|
|
52
52
|
export const TEST_RUNNERS = ['dotnet', 'vitest', 'jest', 'node-test', 'playwright', 'none', EVAL_RUNNER];
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
* The task-group vocabulary a node can declare via `groups`, and that
|
|
56
|
-
* `tasks.json` entries declare via `group` (framework/schemas/tasks.schema.json
|
|
57
|
-
* `~:28`) — ONE enum, checked to agree by canary, never two that drift.
|
|
58
|
-
* Single home for B3's task-scope selection (`test-scope.js`) to import from.
|
|
59
|
-
*/
|
|
60
|
-
export const TASK_GROUPS = ['backend', 'frontend', 'tests', 'infra', 'docs'];
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Which group(s) a node serves when it does NOT declare `groups` itself,
|
|
64
|
-
* keyed by runner. `none` and `evals` are deliberately ABSENT — a command
|
|
65
|
-
* nobody could read (`none`) and a declared eval suite (`evals`) have no
|
|
66
|
-
* honest default group, so `effectiveGroups()` answers null for them rather
|
|
67
|
-
* than guessing one.
|
|
68
|
-
*/
|
|
69
|
-
export const RUNNER_DEFAULT_GROUPS = {
|
|
70
|
-
dotnet: ['backend'],
|
|
71
|
-
'node-test': ['backend'],
|
|
72
|
-
vitest: ['frontend'],
|
|
73
|
-
jest: ['frontend'],
|
|
74
|
-
playwright: ['frontend'],
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Which task group(s) this node answers to: what it DECLARED, or else its
|
|
79
|
-
* runner's default. **null is "no mapping"**, never an invented guess — B3's
|
|
80
|
-
* task-scope selection reads null as "this node is unscoped, always runs".
|
|
81
|
-
*
|
|
82
|
-
* @param {import('./test-nodes.js').TestNode} node
|
|
83
|
-
* @returns {string[]|null}
|
|
84
|
-
*/
|
|
85
|
-
export function effectiveGroups(node) {
|
|
86
|
-
return node?.groups ?? RUNNER_DEFAULT_GROUPS[node?.runner] ?? null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
54
|
/**
|
|
90
55
|
* A node id is what `--filter` matches and what keys `runs[]`. Lowercase and
|
|
91
56
|
* dash-only so it survives a shell argument and a JSON key without quoting.
|
|
@@ -209,11 +174,6 @@ function makeNode(partial) {
|
|
|
209
174
|
// eval node"** — distinct from an eval node with an empty block, which
|
|
210
175
|
// `validateTestNodes` already refused.
|
|
211
176
|
evals: partial.evals ?? null,
|
|
212
|
-
// Declared task-group(s) this node serves (§B3 task-scope selection).
|
|
213
|
-
// **null is "not declared"**, distinct from an empty array (which
|
|
214
|
-
// `firstProblem` refuses outright) — effectiveGroups() falls back to the
|
|
215
|
-
// runner's default only when this is null.
|
|
216
|
-
groups: partial.groups ?? null,
|
|
217
177
|
status: partial.status ?? null,
|
|
218
178
|
reason: partial.reason ?? null,
|
|
219
179
|
};
|
|
@@ -296,28 +256,6 @@ function firstProblem(raw, projectPath) {
|
|
|
296
256
|
return `evals applies only to runner "${EVAL_RUNNER}"`;
|
|
297
257
|
}
|
|
298
258
|
|
|
299
|
-
if (raw.groups !== undefined) {
|
|
300
|
-
if (!Array.isArray(raw.groups)) {
|
|
301
|
-
return 'groups must be a non-empty array';
|
|
302
|
-
}
|
|
303
|
-
if (raw.groups.length === 0) {
|
|
304
|
-
return 'groups must be a non-empty array';
|
|
305
|
-
}
|
|
306
|
-
const invalid = raw.groups.filter((g) => !TASK_GROUPS.includes(g));
|
|
307
|
-
if (invalid.length > 0) {
|
|
308
|
-
return `groups must contain only ${TASK_GROUPS.join(' | ')} (invalid: ${invalid.join(', ')})`;
|
|
309
|
-
}
|
|
310
|
-
const seenGroups = new Set();
|
|
311
|
-
const duplicates = new Set();
|
|
312
|
-
for (const g of raw.groups) {
|
|
313
|
-
if (seenGroups.has(g)) duplicates.add(g);
|
|
314
|
-
seenGroups.add(g);
|
|
315
|
-
}
|
|
316
|
-
if (duplicates.size > 0) {
|
|
317
|
-
return `groups must not contain duplicates (repeated: ${[...duplicates].join(', ')})`;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
259
|
// The cwd checks come last because they are the expensive ones, and because
|
|
322
260
|
// a node that is malformed in shape has a better error to report first.
|
|
323
261
|
const cwd = typeof raw.cwd === 'string' ? raw.cwd.trim() : '.';
|
|
@@ -390,7 +328,6 @@ export function validateTestNodes(config, projectPath = null) {
|
|
|
390
328
|
e2e: entry.e2e ?? false,
|
|
391
329
|
linuxImage: entry.linuxImage ?? null,
|
|
392
330
|
evals: entry.evals ?? null,
|
|
393
|
-
groups: entry.groups ?? null,
|
|
394
331
|
projectCount: 1,
|
|
395
332
|
});
|
|
396
333
|
});
|
|
@@ -586,14 +523,9 @@ export function morphConfigPath(projectPath = '.') {
|
|
|
586
523
|
* the declaration the command came to update);
|
|
587
524
|
* - a declared node with no measurement (a baseline recorded for part of a
|
|
588
525
|
* fleet says nothing true about the rest of it);
|
|
589
|
-
* - a measurement carrying a task-scope `selection` (#87): narrowed, scoped
|
|
590
|
-
* out or run whole INSIDE a task-scoped verify, it measured a recorte, and
|
|
591
|
-
* `expectedCount` is the whole suite's number. `verify` already refuses
|
|
592
|
-
* `--record-baseline` with a task id before the build; this is the second
|
|
593
|
-
* barrier, at the writer, so no caller can route around it.
|
|
594
526
|
*
|
|
595
527
|
* @param {string} projectPath - Project root
|
|
596
|
-
* @param {Array<{id: string, count: number|null
|
|
528
|
+
* @param {Array<{id: string, count: number|null}>} measurements - One per node that ran
|
|
597
529
|
* @returns {{ recorded: boolean, reason: string|null, updates: Array<{id: string, from: number|null, to: number}>, path: string }}
|
|
598
530
|
*/
|
|
599
531
|
export function recordTestBaselines(projectPath, measurements) {
|
|
@@ -614,11 +546,6 @@ export function recordTestBaselines(projectPath, measurements) {
|
|
|
614
546
|
return refuse('config.json não declara project.tests[] — não há entrada onde gravar a baseline');
|
|
615
547
|
}
|
|
616
548
|
|
|
617
|
-
const recortes = measurements.filter((m) => m.selection != null).map((m) => m.id);
|
|
618
|
-
if (recortes.length > 0) {
|
|
619
|
-
return refuse(`medição de recorte de task em: ${recortes.join(', ')} — baseline só de execução inteira, sem {task}`);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
549
|
const unreadable = measurements.filter((m) => !Number.isInteger(m.count)).map((m) => m.id);
|
|
623
550
|
if (unreadable.length > 0) {
|
|
624
551
|
return refuse(`contagem desconhecida em: ${unreadable.join(', ')} — desconhecido nunca vira 0`);
|