@polymorphism-tech/morph-spec 9.7.0 → 10.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 +11 -11
- package/README.md +4 -4
- package/bin/morph-spec.js +41 -44
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +24 -30
- package/framework/MORPH.md +47 -123
- package/framework/agents.json +6 -374
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-archive.md +4 -3
- package/framework/commands/morph-hotfix.md +8 -30
- package/framework/commands/morph-infra.md +2 -2
- package/framework/commands/morph-media.md +10 -9
- package/framework/commands/morph-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +100 -102
- package/framework/evals/review-checklist.md +46 -54
- package/framework/hooks/README.md +4 -4
- package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
- package/framework/hooks/claude-code/core/README.md +1 -1
- package/framework/hooks/claude-code/core/denylist.json +4 -4
- package/framework/hooks/claude-code/core/gate-guard.js +9 -0
- package/framework/hooks/claude-code/core/masked-exit.js +49 -0
- package/framework/hooks/claude-code/core/state-sync.js +4 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
- package/framework/hooks/claude-code/core/test-budget.js +10 -70
- package/framework/hooks/claude-code/core/validate-completion.js +11 -24
- package/framework/hooks/claude-code/core/worktree-context.js +5 -22
- package/framework/hooks/claude-code/core/worktree-create.js +6 -45
- package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/gate-event.js +2 -2
- package/framework/hooks/shared/graph-refresh.js +20 -3
- package/framework/hooks/shared/heavy-command.js +32 -1
- package/framework/hooks/shared/heavy-config.js +1 -1
- package/framework/hooks/shared/heavy-slots.js +41 -4
- package/framework/hooks/shared/next-step.js +63 -24
- package/framework/hooks/shared/phase-utils.js +6 -9
- package/framework/hooks/shared/report-progress.js +86 -0
- package/framework/hooks/shared/state-reader.js +3 -2
- package/framework/hooks/statusline.js +332 -0
- package/framework/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +9 -1
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- package/framework/rules/nextjs-standards.md +7 -0
- package/framework/rules/plan-grammar.md +53 -0
- package/framework/rules/ui-design.md +27 -0
- package/framework/schemas/config.schema.json +12 -19
- package/framework/schemas/feature.schema.json +7 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
- package/framework/skills/core/morph-apply/SKILL.md +38 -166
- package/framework/skills/core/morph-apply/references/debugging.md +8 -0
- package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
- package/framework/skills/core/morph-apply/references/machine.md +29 -0
- package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
- package/framework/skills/core/morph-apply/references/review.md +65 -0
- package/framework/skills/core/morph-plan/SKILL.md +42 -108
- package/framework/skills/core/morph-plan/references/critique.md +40 -0
- package/framework/skills/core/morph-plan/references/interview.md +25 -0
- package/framework/skills/core/morph-uiux/SKILL.md +47 -64
- package/framework/skills/core/morph-uiux/surfaces.json +32 -0
- package/framework/standards/STANDARDS.json +32 -5
- package/framework/standards/STANDARDS.meta.json +5 -1
- package/framework/standards/ai-agents/agent-session.md +2 -3
- package/framework/standards/ai-agents/context-providers.md +2 -2
- package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +2 -2
- package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
- package/framework/standards/ai-agents/setup.md +1 -1
- package/framework/standards/ai-agents/tool-adapters.md +2 -2
- package/framework/standards/architecture/stack-baseline.md +47 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
- package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
- package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
- package/framework/standards/frontend/design-system/art-direction.md +2 -2
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
- package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +12 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +88 -23
- package/src/commands/graph/diff.js +74 -0
- package/src/commands/graph/status.js +1 -1
- package/src/commands/observability/cost.js +2 -4
- package/src/commands/observability/telemetry.js +1 -1
- package/src/commands/project/doctor.js +23 -39
- package/src/commands/project/fleet.js +5 -6
- package/src/commands/project/init.js +63 -36
- package/src/commands/project/stack-scan.js +3 -42
- package/src/commands/project/stack-set.js +26 -0
- package/src/commands/project/status.js +12 -13
- package/src/commands/project/update.js +39 -3
- package/src/commands/project/worktree.js +27 -16
- package/src/commands/state/advance.js +17 -17
- package/src/commands/state/approve.js +0 -6
- package/src/commands/state/archive.js +27 -1
- package/src/commands/state/auto-approval.js +1 -1
- package/src/commands/state/create.js +0 -5
- package/src/commands/state/finish.js +43 -1
- package/src/commands/state/gate-check.js +37 -30
- package/src/commands/state/gate-decision.js +3 -2
- package/src/commands/state/next.js +1 -0
- package/src/commands/state/recap.js +67 -0
- package/src/commands/state/report-check.js +39 -146
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +6 -6
- package/src/core/paths/output-schema.js +3 -3
- package/src/core/state/state-manager.js +2 -4
- package/src/dashboard/app/components/CommandPalette.jsx +2 -5
- package/src/dashboard/app/components/GatePauses.jsx +116 -0
- package/src/dashboard/app/components/SessionMap.jsx +77 -0
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/Activity.jsx +4 -0
- package/src/dashboard/app/pages/AgentMap.jsx +1 -2
- package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
- package/src/dashboard/app/pages/Features.jsx +7 -7
- package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
- package/src/dashboard/app/pages/Portfolio.jsx +40 -5
- package/src/dashboard/scanner/activity-scanner.js +29 -0
- package/src/dashboard/scanner/agents-scanner.js +11 -14
- package/src/dashboard/scanner/features-scanner.js +45 -24
- package/src/dashboard/scanner/project-api.js +11 -2
- package/src/dashboard/scanner/skills-scanner.js +11 -1
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/e2e/applicability.js +3 -1
- package/src/lib/git/tree-state.js +1 -16
- package/src/lib/graph/diff-tree.js +127 -0
- package/src/lib/graph/graphify-runtime.js +12 -5
- package/src/lib/heavy/gate.js +22 -1
- package/src/lib/installers/memory-installer.js +2 -2
- package/src/lib/installers/rules-installer.js +57 -0
- package/src/lib/installers/stack-assets.js +17 -0
- package/src/lib/installers/stack-sync.js +219 -0
- package/src/lib/plan/checks.js +55 -110
- package/src/lib/plan/critique.js +28 -0
- package/src/lib/plan/media.js +42 -0
- package/src/lib/plan/plan-html.js +125 -0
- package/src/lib/plan/plan-parser.js +34 -45
- package/src/lib/recap/piper.js +36 -0
- package/src/lib/recap/recap.js +232 -0
- package/src/lib/review-reports.js +33 -7
- package/src/lib/risk-detector.js +90 -11
- package/src/lib/specs/screen-checklist.js +33 -48
- package/src/lib/stack/dotnet-build-ignores.js +8 -9
- package/src/lib/tasks/test-nodes.js +24 -6
- package/src/lib/tasks/test-runner.js +40 -1
- package/src/lib/telemetry/metrics.js +27 -29
- package/src/lib/validators/reuse/reuse-validator.js +1 -9
- package/src/lib/validators/verify-runner.js +50 -19
- package/src/lib/worktree/fleet-scanner.js +7 -20
- package/src/lib/worktree/worktree-audit.js +0 -37
- package/src/scripts/setup-infra.js +15 -50
- package/src/utils/agents-installer.js +27 -201
- package/src/utils/hooks-installer.js +11 -95
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
- package/framework/hooks/claude-code/statusline.py +0 -699
- package/framework/hooks/claude-code/statusline.sh +0 -11
- package/framework/hooks/shared/agent-reap.js +0 -74
- package/framework/hooks/shared/slice-progress.js +0 -110
- package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
- package/framework/skills/core/morph-machine/SKILL.md +0 -46
- package/framework/skills/core/morph-review/SKILL.md +0 -129
- package/framework/skills/core/morph-standards/SKILL.md +0 -52
- package/framework/standards/PROVENANCE.json +0 -119
- package/src/commands/project/worktree-reap.js +0 -34
- package/src/commands/state/handoff.js +0 -65
- package/src/commands/validation/mutate.js +0 -263
- package/src/commands/validation/scout-check.js +0 -28
- package/src/lib/generators/handoff-generator.js +0 -172
- package/src/lib/mutation/adapters.js +0 -79
- package/src/lib/mutation/commands.js +0 -61
- package/src/lib/mutation/dotnet-introspection.js +0 -281
- package/src/lib/mutation/manifest.js +0 -144
- package/src/lib/mutation/plan.js +0 -44
- package/src/lib/mutation/report.js +0 -68
- package/src/lib/mutation/runner.js +0 -301
- package/src/lib/mutation/schemata.js +0 -60
- package/src/lib/mutation/snapshot.js +0 -65
- package/src/lib/mutation/subset.js +0 -109
- package/src/lib/mutation/trx.js +0 -50
- package/src/lib/scout/scout-check.js +0 -293
- package/src/lib/validators/traceability-check.js +0 -103
- package/src/lib/worktree/agent-reaper.js +0 -247
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { posix, win32 } from 'node:path';
|
|
2
|
-
|
|
3
|
-
export const MUTANT_ID = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
4
|
-
export const MODES = ['schemata', 'rebuild'];
|
|
5
|
-
export const BACKSLASH_TOKEN = '{{BS}}';
|
|
6
|
-
export const MAX_MUTANTS = 3;
|
|
7
|
-
|
|
8
|
-
const expandBackslash = (s) => (typeof s === 'string' ? s.split(BACKSLASH_TOKEN).join(String.fromCharCode(92)) : s);
|
|
9
|
-
|
|
10
|
-
export class ManifestError extends Error {
|
|
11
|
-
constructor(problems) {
|
|
12
|
-
super(`manifesto de mutantes inválido:\n${problems.map((p) => ` - ${p}`).join('\n')}`);
|
|
13
|
-
this.name = 'ManifestError';
|
|
14
|
-
this.problems = problems;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function eolOf(text) {
|
|
19
|
-
return text.includes('\r\n') ? '\r\n' : '\n';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const toEol = (s, eol) => s.replace(/\r?\n/g, eol);
|
|
23
|
-
|
|
24
|
-
function lineBounds(text, line) {
|
|
25
|
-
let start = 0;
|
|
26
|
-
for (let n = 1; n < line; n++) {
|
|
27
|
-
const nl = text.indexOf('\n', start);
|
|
28
|
-
if (nl === -1) return null;
|
|
29
|
-
start = nl + 1;
|
|
30
|
-
}
|
|
31
|
-
const nl = text.indexOf('\n', start);
|
|
32
|
-
return { start, end: nl === -1 ? text.length : nl };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Where `find` begins: it must start on `line` and occur there exactly once,
|
|
37
|
-
* so the `arquivo:linha` of the report is a fact the tool derived, not typed.
|
|
38
|
-
*/
|
|
39
|
-
export function locate(text, { line, find }) {
|
|
40
|
-
const bounds = lineBounds(text, line);
|
|
41
|
-
if (!bounds) return { error: `o arquivo tem menos de ${line} linhas` };
|
|
42
|
-
const needle = toEol(find, eolOf(text));
|
|
43
|
-
const at = text.indexOf(needle, bounds.start);
|
|
44
|
-
if (at === -1) return { error: `\`find\` não aparece a partir da linha ${line}` };
|
|
45
|
-
if (at > bounds.end) {
|
|
46
|
-
const first = text.slice(0, at).split('\n').length;
|
|
47
|
-
return { error: `\`find\` começa na linha ${first}, não na ${line}` };
|
|
48
|
-
}
|
|
49
|
-
const again = text.indexOf(needle, at + 1);
|
|
50
|
-
if (again !== -1 && again <= bounds.end) {
|
|
51
|
-
return { error: `\`find\` aparece mais de uma vez na linha ${line}: estenda-o até ficar único` };
|
|
52
|
-
}
|
|
53
|
-
return { start: at, end: at + needle.length, needle };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function validateManifest(manifest, { fileExists } = {}) {
|
|
57
|
-
const problems = [];
|
|
58
|
-
const list = manifest?.mutants;
|
|
59
|
-
if (!Array.isArray(list) || list.length === 0) {
|
|
60
|
-
throw new ManifestError(['`mutants` precisa ser um array com ao menos um mutante']);
|
|
61
|
-
}
|
|
62
|
-
if (list.length > MAX_MUTANTS) {
|
|
63
|
-
throw new ManifestError([`no máximo ${MAX_MUTANTS} mutantes por rodada: escolha as linhas de guarda ou regra que um UC exige`]);
|
|
64
|
-
}
|
|
65
|
-
const seen = new Set();
|
|
66
|
-
const mutants = list.map((raw, index) => {
|
|
67
|
-
const at = `mutants[${index}]${typeof raw?.id === 'string' ? ` (${raw.id})` : ''}`;
|
|
68
|
-
const m = raw ?? {};
|
|
69
|
-
if (typeof m.id !== 'string' || !MUTANT_ID.test(m.id)) problems.push(`${at}: \`id\` inválido (letra + letras/dígitos/_/-, até 32)`);
|
|
70
|
-
else if (seen.has(m.id)) problems.push(`${at}: \`id\` repetido`);
|
|
71
|
-
else seen.add(m.id);
|
|
72
|
-
if (typeof m.file !== 'string' || m.file === '') problems.push(`${at}: \`file\` ausente`);
|
|
73
|
-
else if (posix.isAbsolute(m.file) || win32.isAbsolute(m.file) || m.file.split(/[\\/]/).includes('..')) problems.push(`${at}: \`file\` tem de ser relativo à raiz do projeto, sem \`..\``);
|
|
74
|
-
else if (fileExists && !fileExists(m.file)) problems.push(`${at}: \`file\` não existe: ${m.file}`);
|
|
75
|
-
if (!Number.isInteger(m.line) || m.line < 1) problems.push(`${at}: \`line\` tem de ser inteiro >= 1`);
|
|
76
|
-
if (typeof m.find !== 'string' || m.find === '') problems.push(`${at}: \`find\` ausente`);
|
|
77
|
-
if (typeof m.replace !== 'string') problems.push(`${at}: \`replace\` ausente (use string vazia para apagar)`);
|
|
78
|
-
else if (m.replace === m.find) problems.push(`${at}: \`replace\` igual a \`find\` não é mutação`);
|
|
79
|
-
if (m.mode !== undefined && !MODES.includes(m.mode)) problems.push(`${at}: \`mode\` tem de ser ${MODES.join(' | ')}`);
|
|
80
|
-
if (typeof m.expect !== 'string' || m.expect.trim() === '') problems.push(`${at}: \`expect\` ausente — diga qual teste deve falhar e por quê; sem isso o mutante pode ser equivalente`);
|
|
81
|
-
return {
|
|
82
|
-
id: m.id, file: typeof m.file === 'string' ? m.file.replace(/\\/g, '/') : m.file,
|
|
83
|
-
line: m.line, find: expandBackslash(m.find), replace: expandBackslash(m.replace), mode: m.mode ?? null,
|
|
84
|
-
note: typeof m.note === 'string' ? m.note : null,
|
|
85
|
-
expect: typeof m.expect === 'string' ? m.expect : null,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
if (problems.length > 0) throw new ManifestError(problems);
|
|
89
|
-
return mutants;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Resolves every mutant against the file text and refuses overlaps: two
|
|
94
|
-
* regions of one file inside each other cannot both be a switch in one build.
|
|
95
|
-
*/
|
|
96
|
-
export function resolveRegions(mutants, readText) {
|
|
97
|
-
const problems = [];
|
|
98
|
-
const byFile = new Map();
|
|
99
|
-
for (const m of mutants) {
|
|
100
|
-
const text = readText(m.file);
|
|
101
|
-
if (text === null) { problems.push(`${m.id}: não consegui ler ${m.file}`); continue; }
|
|
102
|
-
const at = locate(text, m);
|
|
103
|
-
if (at.error) { problems.push(`${m.id}: ${at.error}`); continue; }
|
|
104
|
-
const eol = eolOf(text);
|
|
105
|
-
const region = {
|
|
106
|
-
...m, start: at.start, end: at.end, eol,
|
|
107
|
-
findText: at.needle, replaceText: toEol(m.replace, eol),
|
|
108
|
-
};
|
|
109
|
-
if (!byFile.has(m.file)) byFile.set(m.file, []);
|
|
110
|
-
byFile.get(m.file).push(region);
|
|
111
|
-
}
|
|
112
|
-
for (const regions of byFile.values()) {
|
|
113
|
-
regions.sort((a, b) => a.start - b.start);
|
|
114
|
-
for (let i = 1; i < regions.length; i++) {
|
|
115
|
-
if (regions[i].start < regions[i - 1].end) {
|
|
116
|
-
problems.push(`${regions[i].id} e ${regions[i - 1].id} se sobrepõem em ${regions[i].file}: rode-os em manifestos separados`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (problems.length > 0) throw new ManifestError(problems);
|
|
121
|
-
return mutants.map((m) => [...byFile.values()].flat().find((r) => r.id === m.id));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function applyRegions(text, regions, produce) {
|
|
125
|
-
let out = '';
|
|
126
|
-
let cursor = 0;
|
|
127
|
-
const spans = new Map();
|
|
128
|
-
for (const region of [...regions].sort((a, b) => a.start - b.start)) {
|
|
129
|
-
out += text.slice(cursor, region.start);
|
|
130
|
-
const from = out.split('\n').length;
|
|
131
|
-
out += produce(region);
|
|
132
|
-
spans.set(region.id, { from, to: out.split('\n').length });
|
|
133
|
-
cursor = region.end;
|
|
134
|
-
}
|
|
135
|
-
return { text: out + text.slice(cursor), spans };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function diffOf(m, limit = 160) {
|
|
139
|
-
const one = (s) => {
|
|
140
|
-
const flat = s.replace(/\r?\n\s*/g, ' ').replace(/\|/g, '\\|');
|
|
141
|
-
return flat.length > limit ? `${flat.slice(0, limit)}…` : flat;
|
|
142
|
-
};
|
|
143
|
-
return `\`- ${one(m.find)}\` / \`+ ${one(m.replace)}\``;
|
|
144
|
-
}
|
package/src/lib/mutation/plan.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
3
|
-
import { isInside } from '../tasks/test-runner.js';
|
|
4
|
-
import { isDotnetSource } from './dotnet-introspection.js';
|
|
5
|
-
import { resolveRegions, validateManifest } from './manifest.js';
|
|
6
|
-
|
|
7
|
-
const JS_RUNNERS = new Set(['vitest', 'jest', 'node-test']);
|
|
8
|
-
|
|
9
|
-
export function loadPlan({ root, planPath }) {
|
|
10
|
-
const manifest = JSON.parse(readFileSync(planPath, 'utf8').replace(/^/, ''));
|
|
11
|
-
const mutants = validateManifest(manifest, { fileExists: (f) => existsSync(join(root, f)) });
|
|
12
|
-
return resolveRegions(mutants, (f) => {
|
|
13
|
-
try { return readFileSync(join(root, f), 'utf8'); } catch { return null; }
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function ownersOf(file, nodes, root) {
|
|
18
|
-
if (isDotnetSource(file)) return nodes.filter((n) => n.runner === 'dotnet');
|
|
19
|
-
const abs = resolve(root, file);
|
|
20
|
-
return nodes
|
|
21
|
-
.filter((n) => JS_RUNNERS.has(n.runner) && isInside(abs, resolve(root, n.cwd || '.')))
|
|
22
|
-
.sort((a, b) => resolve(root, b.cwd || '.').length - resolve(root, a.cwd || '.').length);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// An `e2e: true` suite tests the running compose stack, which mutate never
|
|
26
|
-
// rebuilds: it is picked only when no other suite owns the file, and the caller refuses.
|
|
27
|
-
export function pickNode(file, nodes, root) {
|
|
28
|
-
const owners = ownersOf(file, nodes, root);
|
|
29
|
-
return owners.find((n) => !n.e2e) ?? owners[0] ?? null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function groupByNode(regions, nodes, root) {
|
|
33
|
-
const groups = new Map();
|
|
34
|
-
const orphans = [];
|
|
35
|
-
const stackOnly = [];
|
|
36
|
-
for (const region of regions) {
|
|
37
|
-
const node = pickNode(region.file, nodes, root);
|
|
38
|
-
if (!node) { orphans.push(region); continue; }
|
|
39
|
-
if (node.e2e) { stackOnly.push({ region, node }); continue; }
|
|
40
|
-
if (!groups.has(node.id)) groups.set(node.id, { node, regions: [] });
|
|
41
|
-
groups.get(node.id).regions.push(region);
|
|
42
|
-
}
|
|
43
|
-
return { groups: [...groups.values()], orphans, stackOnly };
|
|
44
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
const cell = (s) => String(s ?? '').replace(/\r?\n/g, ' ').replace(/\|/g, '\\|');
|
|
2
|
-
const short = (md5) => (md5 ? md5.slice(0, 12) : '—');
|
|
3
|
-
const seconds = (ms) => `${(ms / 1000).toFixed(1)} s`;
|
|
4
|
-
|
|
5
|
-
function countCell(m) {
|
|
6
|
-
const scope = m.scope === 'subset' ? 'subconjunto' : 'suíte inteira';
|
|
7
|
-
switch (m.verdict) {
|
|
8
|
-
case 'killed': return m.scope === 'subset' ? `≥${m.killers} de ${m.executed} (${scope})` : `${m.killers} de ${m.executed ?? '?'} (${scope})`;
|
|
9
|
-
case 'survived': return `**0 de ${m.executed ?? '?'} (SOBREVIVEU, ${scope})**`;
|
|
10
|
-
case 'build-error': return '— (o mutante não compila: não é morte)';
|
|
11
|
-
default: return '— (inconclusivo: não conta como morte nem como sobrevivência)';
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function reasonCell(m) {
|
|
16
|
-
if (m.verdict === 'survived') return m.reason || `nenhum teste falhou ${m.scope === 'subset' ? 'no subconjunto' : 'na suíte inteira'}${m.expect ? ` — esperado: ${m.expect}` : ''}`;
|
|
17
|
-
const how = m.demoted ? ` [rebuild: ${m.demoted}]` : '';
|
|
18
|
-
return `${m.reason || '—'}${how}`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const BINARY = {
|
|
22
|
-
md5: 'DLL do rebuild final idêntica (md5) à DLL construída da árvore limpa antes de qualquer mutante',
|
|
23
|
-
rebuilt: 'DLL recompilada DEPOIS da restauração e sem a chave de schemata, mas com md5 diferente da baseline — o build não é determinístico neste projeto, então o conteúdo não se compara',
|
|
24
|
-
unavailable: 'sem etapa de build neste runner: nada a provar sobre binário',
|
|
25
|
-
unlocated: 'ATENÇÃO — o build não informou a DLL dos projetos mutados, então o binário final NÃO pôde ser provado',
|
|
26
|
-
mismatch: 'ATENÇÃO — a DLL do rebuild final NÃO bate com a baseline e ainda carrega a chave de schemata',
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export function renderSection(result, { head = null, date = null, subset = null, note = null } = {}) {
|
|
30
|
-
const lines = ['## Mutações', ''];
|
|
31
|
-
const { method } = result;
|
|
32
|
-
const where = subset?.usable
|
|
33
|
-
? (subset.classes
|
|
34
|
-
? `${subset.classes.length} classe(s) de teste (${subset.files?.changed?.length ?? 0} alterada(s) pela feature, ${subset.files?.mentions?.length ?? 0} citando os tipos mutados)`
|
|
35
|
-
: `${subset.files?.related?.length ?? 0} arquivo(s) de teste relacionados`)
|
|
36
|
-
: (subset?.reason ?? 'nenhum');
|
|
37
|
-
lines.push(`**Método:** \`morph-spec mutate\`${head ? ` sobre ${head.slice(0, 8)}` : ''}${date ? ` em ${date}` : ''}.`);
|
|
38
|
-
lines.push(`- ${method.schemata} mutante(s) por chave em UM build (schemata), ${method.rebuild} por rebuild real${(method.jobs ?? 1) > 1 ? `; rodadas de escopo rápido em ${method.jobs} workers` : ''}.`);
|
|
39
|
-
lines.push(`- Escopo rápido: ${method.subset.live ? where : `desligado (${method.subset.note ?? subset?.reason ?? 'sem subconjunto'})`}.`);
|
|
40
|
-
lines.push('- Morto = falha nova (ausente na rodada-base do mesmo escopo). Com subconjunto, a decisão fica nele: sobrevivente não é refeito na suíte inteira.');
|
|
41
|
-
if (result.baseline.subset) lines.push(`- Rodada-base do subconjunto: ${result.baseline.subset.executed} executados, ${result.baseline.subset.failed} falhando.`);
|
|
42
|
-
if (result.baseline.full) lines.push(`- Rodada-base da suíte inteira: ${result.baseline.full.executed} executados, ${result.baseline.full.failed} falhando.`);
|
|
43
|
-
if (note) lines.push(`- ${note}`);
|
|
44
|
-
lines.push('');
|
|
45
|
-
lines.push('| # | Arquivo:linha | Mutação (diff) | md5 antes | md5 depois | Testes derrubados | Motivo da falha |');
|
|
46
|
-
lines.push('|---|---|---|---|---|---|---|');
|
|
47
|
-
for (const m of result.mutants) {
|
|
48
|
-
lines.push(`| ${m.id} | ${cell(`${m.file}:${m.line}`)} | ${m.diff} | ${short(m.md5Before)} | ${short(m.md5After)} | ${cell(countCell(m))} | ${cell(reasonCell(m))} |`);
|
|
49
|
-
}
|
|
50
|
-
lines.push('');
|
|
51
|
-
const r = result.restore;
|
|
52
|
-
const bad = r.files.filter((f) => !f.ok).map((f) => f.file);
|
|
53
|
-
lines.push(`**Reversão:** snapshot/restore de bytes, md5 de cada arquivo conferido${bad.length ? ` — FALHOU em ${bad.join(', ')}` : ' (todos iguais ao original)'}; sem \`git checkout\`. Escrita nova a cada restauração, então o arquivo é mais novo que qualquer binário de mutante e o build recompila.`);
|
|
54
|
-
lines.push(`**Binário:** ${BINARY[result.binary.proof]}.`);
|
|
55
|
-
lines.push(`**Árvore:** \`git status --porcelain\` ${r.treeClean ? 'idêntico ao de antes da rodada (vazio em src/)' : 'DIFERENTE do de antes da rodada — investigar antes de qualquer nota'}.`);
|
|
56
|
-
const sum = result.timings.reduce((acc, t) => acc + t.ms, 0);
|
|
57
|
-
const jobs = method.jobs ?? 1;
|
|
58
|
-
const wall = result.wallMs ?? sum;
|
|
59
|
-
lines.push(`**Tempo:** ${seconds(wall)} de relógio${jobs > 1 ? ` com ${jobs} workers` : ''}; soma das ${result.timings.length} etapas de build+teste: ${seconds(sum)}.`);
|
|
60
|
-
lines.push('');
|
|
61
|
-
return lines.join('\n');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function summarize(result) {
|
|
65
|
-
const tally = { killed: 0, survived: 0, 'build-error': 0, inconclusive: 0 };
|
|
66
|
-
for (const m of result.mutants) tally[m.verdict] = (tally[m.verdict] ?? 0) + 1;
|
|
67
|
-
return tally;
|
|
68
|
-
}
|
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
3
|
-
import { applyRegions, diffOf } from './manifest.js';
|
|
4
|
-
import { blame, parseCompilerErrors, patchFile, switchClassName, switchSource, SWITCH_ENV } from './schemata.js';
|
|
5
|
-
import { md5Of, md5OfFile, openSnapshot, restoreSnapshot, trackGenerated } from './snapshot.js';
|
|
6
|
-
import { parseBuiltDlls } from './commands.js';
|
|
7
|
-
import { firstLines } from './trx.js';
|
|
8
|
-
import { nearestProjectOf } from './dotnet-introspection.js';
|
|
9
|
-
|
|
10
|
-
const posix = (p) => p.replace(/\\/g, '/');
|
|
11
|
-
const MAX_SCHEMATA_ATTEMPTS = 3;
|
|
12
|
-
|
|
13
|
-
export const noSubsetReason = (why) => `sem subconjunto utilizável (${why ?? 'nenhum teste da feature nem que cite os tipos mutados'}): a mutação não roda a suíte inteira — prove o cenário por leitura`;
|
|
14
|
-
|
|
15
|
-
const envWithout = (env) => { const { [SWITCH_ENV]: _drop, ...rest } = env; return rest; };
|
|
16
|
-
const failedNames = (result) => new Set(result.failedTests.map((t) => t.name));
|
|
17
|
-
|
|
18
|
-
function newFailures(result, baseline) {
|
|
19
|
-
const known = baseline ? failedNames(baseline) : new Set();
|
|
20
|
-
return result.failedTests.filter((t) => !known.has(t.name));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function reasonOf(failures) {
|
|
24
|
-
const [first] = failures;
|
|
25
|
-
if (!first) return '';
|
|
26
|
-
const text = firstLines(first.message);
|
|
27
|
-
return text ? `${first.name}: ${text}` : first.name;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Judges one test result against the same scope's baseline: killed needs a
|
|
32
|
-
* failure the pristine run did not have; a run that executed nothing or died
|
|
33
|
-
* proves nothing either way.
|
|
34
|
-
*/
|
|
35
|
-
export function judge(result, baseline) {
|
|
36
|
-
if (result.outcome === 'crash') return { state: 'inconclusive', reason: result.note ?? 'a rodada não terminou' };
|
|
37
|
-
if (result.executed === 0) return { state: 'inconclusive', reason: 'nenhum teste foi executado neste escopo' };
|
|
38
|
-
if (result.outcome === 'red') {
|
|
39
|
-
const fresh = newFailures(result, baseline);
|
|
40
|
-
if (fresh.length > 0) return { state: 'killed', failures: fresh, reason: reasonOf(fresh) };
|
|
41
|
-
}
|
|
42
|
-
return { state: 'survived' };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const carriesSwitch = (dll) => readFileSync(dll).includes(Buffer.from(SWITCH_ENV, 'utf16le'));
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Equal bytes against the DLL built from the clean tree is the proof. A non-deterministic
|
|
49
|
-
* build can only show it was rebuilt AFTER the restore and carries no switch.
|
|
50
|
-
*/
|
|
51
|
-
export function proveBinary({ pristine, dlls, restoredAt, hashes }) {
|
|
52
|
-
const projects = [...pristine].map(([name, md5]) => {
|
|
53
|
-
const dll = dlls.get(name);
|
|
54
|
-
const final = hashes.get(name) ?? null;
|
|
55
|
-
const rebuilt = Boolean(dll) && statSync(dll).mtimeMs >= restoredAt - 50;
|
|
56
|
-
return { name, pristine: md5, final, equal: final === md5, rebuilt, switchLeft: Boolean(dll) && carriesSwitch(dll) };
|
|
57
|
-
});
|
|
58
|
-
if (projects.length === 0) return { proof: 'unlocated', projects };
|
|
59
|
-
if (projects.some((p) => p.switchLeft)) return { proof: 'mismatch', projects };
|
|
60
|
-
if (projects.every((p) => p.equal)) return { proof: 'md5', projects };
|
|
61
|
-
return { proof: projects.every((p) => p.rebuilt) ? 'rebuilt' : 'mismatch', projects };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function pool(items, size, worker) {
|
|
65
|
-
const queue = [...items];
|
|
66
|
-
await Promise.all(Array.from({ length: Math.min(size, queue.length) }, async () => {
|
|
67
|
-
while (queue.length > 0) await worker(queue.shift());
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function mergeBaselines(copies) {
|
|
72
|
-
const [first] = copies;
|
|
73
|
-
const broken = copies.find((c) => c.outcome === 'crash');
|
|
74
|
-
if (broken) return broken;
|
|
75
|
-
const seen = new Map();
|
|
76
|
-
for (const c of copies) for (const t of c.failedTests) if (!seen.has(t.name)) seen.set(t.name, t);
|
|
77
|
-
const failedTests = [...seen.values()];
|
|
78
|
-
return { ...first, outcome: failedTests.length > 0 ? 'red' : 'green', failed: failedTests.length, failedTests };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function runMutation(input) {
|
|
82
|
-
const { root, node, regions, workDir, runId, exec, adapter, gate, git, log = () => {}, options = {} } = input;
|
|
83
|
-
const timeoutMs = options.timeoutMs ?? node.timeoutMs ?? 900_000;
|
|
84
|
-
const startedAt = Date.now();
|
|
85
|
-
let workerCount = 1;
|
|
86
|
-
const timings = [];
|
|
87
|
-
const timed = async (label, fn) => {
|
|
88
|
-
const at = Date.now();
|
|
89
|
-
try { return await fn(); } finally { timings.push({ label, ms: Date.now() - at }); }
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const files = [...new Set(regions.map((r) => r.file))];
|
|
93
|
-
const originals = new Map(files.map((f) => [f, readFileSync(join(root, f), 'utf8')]));
|
|
94
|
-
const porcelainBefore = git.porcelain();
|
|
95
|
-
const snapshot = openSnapshot({ dir: join(workDir, 'snapshot'), root, files, runId });
|
|
96
|
-
const projectOf = (file) => {
|
|
97
|
-
const csproj = nearestProjectOf(join(root, file), root);
|
|
98
|
-
return csproj ? { rel: posix(relative(root, csproj)), dir: dirname(csproj), name: basename(csproj).replace(/\.[a-z]+proj$/i, '') } : null;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const results = new Map(regions.map((r) => [r.id, {
|
|
102
|
-
id: r.id, file: r.file, line: r.line, diff: diffOf(r), note: r.note, expect: r.expect,
|
|
103
|
-
md5Before: md5Of(readFileSync(join(root, r.file))),
|
|
104
|
-
md5After: md5Of(applyRegions(originals.get(r.file), [r], (x) => x.replaceText).text),
|
|
105
|
-
mode: r.mode === 'rebuild' || adapter.kind !== 'dotnet' || !/\.cs$/i.test(r.file) ? 'rebuild' : 'schemata',
|
|
106
|
-
demoted: null, verdict: null, scope: null, killers: 0, executed: null, reason: '', durationMs: 0,
|
|
107
|
-
}]));
|
|
108
|
-
const byId = new Map(regions.map((r) => [r.id, r]));
|
|
109
|
-
|
|
110
|
-
const build = async (label) => {
|
|
111
|
-
const dlls = new Map();
|
|
112
|
-
for (const command of adapter.buildCommands) {
|
|
113
|
-
const r = await timed(label, () => exec(command, { cwd: adapter.cwd, env: process.env, timeoutMs, label }));
|
|
114
|
-
const output = `${r.stdout}\n${r.stderr}`;
|
|
115
|
-
if (r.status !== 0) return { ok: false, output };
|
|
116
|
-
for (const [name, dll] of parseBuiltDlls(output)) dlls.set(name, dll);
|
|
117
|
-
}
|
|
118
|
-
return { ok: true, dlls };
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const dllHashes = (dlls) => new Map(
|
|
122
|
-
[...new Set(files.map((f) => projectOf(f)?.name).filter(Boolean))]
|
|
123
|
-
.filter((name) => dlls.has(name) && existsSync(dlls.get(name)))
|
|
124
|
-
.map((name) => [name, md5OfFile(dlls.get(name))]),
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const run = (label, env) => timed(label, () => adapter.runTests({ label, env }));
|
|
128
|
-
|
|
129
|
-
const record = (id, patch) => Object.assign(results.get(id), patch);
|
|
130
|
-
const recordSubset = (id, result) => {
|
|
131
|
-
const verdict = judge(result, baselineSubset);
|
|
132
|
-
const base = { scope: 'subset', executed: result.executed, durationMs: result.durationMs };
|
|
133
|
-
if (verdict.state === 'killed') record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason });
|
|
134
|
-
else record(id, { ...base, verdict: verdict.state, reason: verdict.reason ?? '' });
|
|
135
|
-
};
|
|
136
|
-
const applyOnDisk = (id) => {
|
|
137
|
-
const region = byId.get(id);
|
|
138
|
-
writeFileSync(join(root, region.file), applyRegions(originals.get(region.file), [region], (x) => x.replaceText).text);
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
let pristine = new Map();
|
|
142
|
-
let binary = { proof: 'unavailable', projects: [] };
|
|
143
|
-
let baselineSubset = null;
|
|
144
|
-
let subsetLive = adapter.hasSubset;
|
|
145
|
-
let subsetNote = null;
|
|
146
|
-
let restoreReport = [];
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
gate?.enter('mutate');
|
|
150
|
-
|
|
151
|
-
if (adapter.buildCommands.length > 0) {
|
|
152
|
-
restoreSnapshot(snapshot);
|
|
153
|
-
const first = await build('build-pristine');
|
|
154
|
-
if (!first.ok) throw Object.assign(new Error('a árvore não compila ANTES da mutação — nada foi mutado'), { code: 'baseline-build-failed', output: first.output });
|
|
155
|
-
pristine = dllHashes(first.dlls);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const compileErrorOf = (output, fallback) => firstLines(parseCompilerErrors(output).map((e) => `${e.code} ${e.message}`)[0] ?? fallback);
|
|
159
|
-
|
|
160
|
-
const screenLiteral = async (ids) => {
|
|
161
|
-
let remaining = ids;
|
|
162
|
-
for (let attempt = 0; attempt < MAX_SCHEMATA_ATTEMPTS && remaining.length > 0; attempt++) {
|
|
163
|
-
const patched = [];
|
|
164
|
-
for (const file of files) {
|
|
165
|
-
const own = remaining.map((id) => byId.get(id)).filter((r) => r.file === file);
|
|
166
|
-
if (own.length === 0) continue;
|
|
167
|
-
const { text, spans } = applyRegions(originals.get(file), own, (r) => r.replaceText);
|
|
168
|
-
writeFileSync(join(root, file), text);
|
|
169
|
-
patched.push({ abs: join(root, file), file, spans });
|
|
170
|
-
}
|
|
171
|
-
const b = await build('build-literal');
|
|
172
|
-
restoreSnapshot(snapshot);
|
|
173
|
-
if (b.ok) return remaining;
|
|
174
|
-
const { blamed, unmapped } = blame(parseCompilerErrors(b.output), patched);
|
|
175
|
-
const demote = unmapped.length > 0 || blamed.length === 0 ? [...remaining] : blamed;
|
|
176
|
-
const note = `a forma literal não compila (${compileErrorOf(b.output, 'erro de compilação')})`;
|
|
177
|
-
for (const id of demote) record(id, { mode: 'rebuild', demoted: note });
|
|
178
|
-
remaining = remaining.filter((id) => !demote.includes(id));
|
|
179
|
-
}
|
|
180
|
-
for (const id of remaining) record(id, { mode: 'rebuild', demoted: 'a triagem da forma literal não convergiu' });
|
|
181
|
-
return [];
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
let schemataIds = [...results.values()].filter((r) => r.mode === 'schemata').map((r) => r.id);
|
|
185
|
-
if (adapter.buildCommands.length > 0) schemataIds = await screenLiteral(schemataIds);
|
|
186
|
-
let schemataBuilt = false;
|
|
187
|
-
for (let attempt = 0; attempt < MAX_SCHEMATA_ATTEMPTS && schemataIds.length > 0; attempt++) {
|
|
188
|
-
const patched = [];
|
|
189
|
-
const classes = new Map();
|
|
190
|
-
for (const file of files) {
|
|
191
|
-
const own = schemataIds.map((id) => byId.get(id)).filter((r) => r.file === file);
|
|
192
|
-
if (own.length === 0) continue;
|
|
193
|
-
const project = projectOf(file);
|
|
194
|
-
const className = switchClassName(project.rel);
|
|
195
|
-
const { text, spans } = patchFile(originals.get(file), own, className);
|
|
196
|
-
writeFileSync(join(root, file), text);
|
|
197
|
-
patched.push({ abs: join(root, file), file, spans });
|
|
198
|
-
if (!classes.has(project.rel)) {
|
|
199
|
-
const generated = posix(relative(root, join(project.dir, `${className}.g.cs`)));
|
|
200
|
-
writeFileSync(join(root, generated), switchSource(className));
|
|
201
|
-
trackGenerated(snapshot, generated);
|
|
202
|
-
classes.set(project.rel, generated);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
const b = await build('build-schemata');
|
|
206
|
-
if (b.ok) { schemataBuilt = true; break; }
|
|
207
|
-
const { blamed, unmapped } = blame(parseCompilerErrors(b.output), patched);
|
|
208
|
-
restoreSnapshot(snapshot);
|
|
209
|
-
const note = `schemata recusou — ${compileErrorOf(b.output, 'o build de schemata falhou')}`;
|
|
210
|
-
const demote = unmapped.length > 0 || blamed.length === 0 ? [...schemataIds] : blamed;
|
|
211
|
-
for (const id of demote) record(id, { mode: 'rebuild', demoted: note });
|
|
212
|
-
schemataIds = schemataIds.filter((id) => !demote.includes(id));
|
|
213
|
-
}
|
|
214
|
-
if (!schemataBuilt) {
|
|
215
|
-
for (const id of schemataIds) record(id, { mode: 'rebuild', demoted: 'schemata recusou — o build não convergiu' });
|
|
216
|
-
schemataIds = [];
|
|
217
|
-
} else {
|
|
218
|
-
restoreSnapshot(snapshot);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const hasRebuild = [...results.values()].some((r) => r.mode === 'rebuild');
|
|
222
|
-
const jobs = Math.max(1, Math.min(options.jobs ?? 1, gate?.jobs ?? options.jobs ?? 1));
|
|
223
|
-
if (subsetLive && (schemataIds.length > 0 || hasRebuild)) {
|
|
224
|
-
const copies = schemataIds.length > 0 ? jobs : 1;
|
|
225
|
-
baselineSubset = mergeBaselines(await Promise.all(Array.from({ length: copies }, (_, i) => (
|
|
226
|
-
run(i === 0 ? 'baseline-subset' : `baseline-subset-${i + 1}`, envWithout(process.env))))));
|
|
227
|
-
if (baselineSubset.outcome === 'crash' || baselineSubset.executed === 0) {
|
|
228
|
-
subsetLive = false;
|
|
229
|
-
subsetNote = baselineSubset.executed === 0 ? 'o filtro do subconjunto não selecionou teste algum' : (baselineSubset.note ?? 'a rodada-base do subconjunto não terminou');
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
const routed = [];
|
|
233
|
-
workerCount = subsetLive && schemataIds.length > 0 ? jobs : 1;
|
|
234
|
-
if (subsetLive) {
|
|
235
|
-
const outcomes = new Map();
|
|
236
|
-
await pool(schemataIds, jobs, async (id) => {
|
|
237
|
-
outcomes.set(id, await run(`${id}-subset`, { ...process.env, [SWITCH_ENV]: id }));
|
|
238
|
-
});
|
|
239
|
-
for (const id of schemataIds) recordSubset(id, outcomes.get(id));
|
|
240
|
-
for (const r of results.values()) if (r.mode === 'rebuild') routed.push(r.id);
|
|
241
|
-
} else {
|
|
242
|
-
for (const r of results.values()) record(r.id, { verdict: 'inconclusive', scope: null, reason: noSubsetReason(subsetNote) });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
for (const id of routed) {
|
|
246
|
-
applyOnDisk(id);
|
|
247
|
-
try {
|
|
248
|
-
if (adapter.buildCommands.length > 0) {
|
|
249
|
-
const b = await build(`build-${id}`);
|
|
250
|
-
if (!b.ok) {
|
|
251
|
-
const err = parseCompilerErrors(b.output)[0];
|
|
252
|
-
record(id, { verdict: 'build-error', reason: err ? `${err.code} ${err.message}` : 'o mutante não compila', scope: null });
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
recordSubset(id, await run(`${id}-subset`, envWithout(process.env)));
|
|
257
|
-
} finally {
|
|
258
|
-
restoreSnapshot(snapshot);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const restoredAt = Date.now();
|
|
263
|
-
restoreReport = restoreSnapshot(snapshot);
|
|
264
|
-
if (adapter.buildCommands.length > 0) {
|
|
265
|
-
const last = await build('build-final');
|
|
266
|
-
if (!last.ok) throw Object.assign(new Error('o rebuild final da árvore restaurada falhou'), { code: 'final-build-failed', output: last.output });
|
|
267
|
-
binary = proveBinary({ pristine, dlls: last.dlls, restoredAt, hashes: dllHashes(last.dlls) });
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
} finally {
|
|
271
|
-
restoreReport = restoreSnapshot(snapshot, { rewrite: false });
|
|
272
|
-
gate?.release();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const porcelainAfter = git.porcelain();
|
|
276
|
-
const mutants = regions.map((r) => results.get(r.id));
|
|
277
|
-
return {
|
|
278
|
-
runId,
|
|
279
|
-
method: {
|
|
280
|
-
adapter: adapter.kind,
|
|
281
|
-
schemata: mutants.filter((m) => m.mode === 'schemata').length,
|
|
282
|
-
rebuild: mutants.filter((m) => m.mode === 'rebuild').length,
|
|
283
|
-
subset: { requested: adapter.hasSubset, live: subsetLive, note: subsetNote },
|
|
284
|
-
jobs: workerCount,
|
|
285
|
-
},
|
|
286
|
-
wallMs: Date.now() - startedAt,
|
|
287
|
-
mutants,
|
|
288
|
-
baseline: {
|
|
289
|
-
subset: baselineSubset ? { executed: baselineSubset.executed, failed: baselineSubset.failed } : null,
|
|
290
|
-
},
|
|
291
|
-
binary,
|
|
292
|
-
restore: {
|
|
293
|
-
files: restoreReport,
|
|
294
|
-
ok: restoreReport.every((f) => f.ok),
|
|
295
|
-
porcelainBefore,
|
|
296
|
-
porcelainAfter,
|
|
297
|
-
treeClean: porcelainBefore === porcelainAfter,
|
|
298
|
-
},
|
|
299
|
-
timings,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { applyRegions } from './manifest.js';
|
|
3
|
-
|
|
4
|
-
export const SWITCH_ENV = 'MORPH_MUTANT';
|
|
5
|
-
|
|
6
|
-
export function switchClassName(csprojRel) {
|
|
7
|
-
return `MorphMutationSwitch_${createHash('md5').update(csprojRel).digest('hex').slice(0, 6)}`;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function switchSource(className) {
|
|
11
|
-
return [
|
|
12
|
-
'// <auto-generated/>',
|
|
13
|
-
'#nullable enable',
|
|
14
|
-
'#pragma warning disable',
|
|
15
|
-
`internal static class ${className}`,
|
|
16
|
-
'{',
|
|
17
|
-
` private static readonly string? Active = System.Environment.GetEnvironmentVariable("${SWITCH_ENV}");`,
|
|
18
|
-
' internal static bool On(string id) => Active == id;',
|
|
19
|
-
'}',
|
|
20
|
-
'',
|
|
21
|
-
].join('\n');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const wrap = (className, region) =>
|
|
25
|
-
`(${className}.On("${region.id}") ? (${region.replaceText}) : (${region.findText}))`;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* A C# file with every region behind its switch; the original text stays as the else branch.
|
|
29
|
-
* Each patched span comes back so a compiler error can be pinned on its mutant.
|
|
30
|
-
*/
|
|
31
|
-
export function patchFile(text, regions, className) {
|
|
32
|
-
return applyRegions(text, regions, (region) => wrap(className, region));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const MSBUILD_ERROR = /^(?<file>.+?)\((?<line>\d+),(?<col>\d+)\): error (?<code>[A-Z]+\d+): (?<msg>.*?)(?: \[.*\])?\s*$/gm;
|
|
36
|
-
|
|
37
|
-
export function parseCompilerErrors(output) {
|
|
38
|
-
return [...String(output).matchAll(MSBUILD_ERROR)].map((m) => ({
|
|
39
|
-
file: m.groups.file.trim(), line: Number(m.groups.line), code: m.groups.code, message: m.groups.msg.trim(),
|
|
40
|
-
}));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* An error inside a patched span names its mutant; one elsewhere in a patched file
|
|
45
|
-
* (an `out` variable the switch left unassigned) names every mutant of that file.
|
|
46
|
-
*/
|
|
47
|
-
export function blame(errors, patched) {
|
|
48
|
-
const blamed = new Set();
|
|
49
|
-
const unmapped = [];
|
|
50
|
-
for (const err of errors) {
|
|
51
|
-
const file = patched.find((p) => samePath(p.abs, err.file));
|
|
52
|
-
if (!file) { unmapped.push(err); continue; }
|
|
53
|
-
const inSpan = [...file.spans].find(([, s]) => err.line >= s.from && err.line <= s.to);
|
|
54
|
-
if (inSpan) blamed.add(inSpan[0]);
|
|
55
|
-
else for (const id of file.spans.keys()) blamed.add(id);
|
|
56
|
-
}
|
|
57
|
-
return { blamed: [...blamed], unmapped };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const samePath = (a, b) => a.replace(/\\/g, '/').toLowerCase() === b.replace(/\\/g, '/').toLowerCase();
|