@polymorphism-tech/morph-spec 9.8.0 → 10.1.1
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 +12 -10
- package/README.md +4 -4
- package/bin/morph-spec.js +40 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +28 -36
- package/docs/QUICKSTART.md +31 -191
- 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-preflight.md +3 -3
- package/framework/commands/morph-review.md +15 -0
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +84 -95
- package/framework/evals/review-checklist.md +45 -53
- package/framework/hooks/README.md +4 -4
- 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/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/mcp/registry.json +4 -4
- package/framework/phases.json +32 -10
- package/framework/rules/ai-agents-standards.md +10 -2
- package/framework/rules/csharp-standards.md +4 -0
- package/framework/rules/data-standards.md +1 -1
- package/framework/rules/ef-standards.md +20 -0
- package/framework/rules/frontend-standards.md +1 -1
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/implement-report.md +25 -0
- package/framework/rules/infrastructure-standards.md +2 -0
- package/framework/rules/neon-auth-standards.md +1 -1
- package/framework/rules/neon-standards.md +3 -3
- 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 +11 -18
- 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 +39 -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 +49 -109
- package/framework/skills/core/morph-plan/references/critique.md +43 -0
- package/framework/skills/core/morph-plan/references/interview.md +37 -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 +51 -28
- package/framework/standards/STANDARDS.meta.json +16 -19
- 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 +7 -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 +70 -0
- package/framework/standards/backend/database/postgresql/database.md +22 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +33 -1
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +2 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +12 -13
- 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/standards/infrastructure/docker/local-compose-isolation.md +2 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +62 -20
- package/framework/standards/infrastructure/neon/neon-platform.md +61 -0
- package/framework/standards/infrastructure/neon/neon-setup.md +14 -13
- package/framework/standards/integration/mcp/mcp-tools.md +20 -20
- package/framework/templates/REGISTRY.json +1 -2
- package/framework/templates/context/ARCHITECTURE.md +1 -1
- package/framework/templates/docs/plan.md +15 -12
- package/framework/templates/docs/ui-mockups.md +17 -105
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +1 -1
- 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 +3 -14
- 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 +2 -2
- package/src/commands/state/create.js +1 -6
- package/src/commands/state/gate-check.js +38 -31
- 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 +36 -178
- package/src/commands/validation/plan-check.js +16 -11
- package/src/commands/validation/validate-feature.js +3 -3
- package/src/commands/validation/verify.js +3 -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/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/lib/detectors/index.js +1 -1
- 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/installers/memory-installer.js +3 -3
- 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 +61 -109
- 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 +28 -4
- 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/telemetry/metrics.js +27 -29
- package/src/lib/validators/reuse/reuse-validator.js +1 -9
- package/src/lib/validators/verify-runner.js +0 -7
- 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/framework/standards/infrastructure/neon/neon-pgrag.md +0 -243
- 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,281 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
|
-
import { dirname, join, resolve } from 'path';
|
|
3
|
-
|
|
4
|
-
import { isInside } from '../tasks/test-runner.js';
|
|
5
|
-
import { jsoncToJson } from '../../../framework/hooks/shared/comment-stripper.js';
|
|
6
|
-
|
|
7
|
-
const MTP_RUNNER = 'Microsoft.Testing.Platform';
|
|
8
|
-
const TYPE_HEADER = /\b(?:class|struct|record|interface)\b/;
|
|
9
|
-
// `@` = verbatim identifier, `\` = unicode escape.
|
|
10
|
-
const NAME_START = '[\\p{L}\\p{Nl}_@\\\\]';
|
|
11
|
-
// TRIPWIRE with NO context rule: any exclusion based on what precedes the keyword can be
|
|
12
|
-
// forged by a comment or exposed string. The quote in the lookahead makes every keyword
|
|
13
|
-
// the blanked code counts also counted in the raw text, at the same offset.
|
|
14
|
-
const TYPE_KEYWORD = new RegExp(`\\b(?:record\\s+(?:class|struct)|class|record|struct|interface)\\b(?=\\s*(?:${NAME_START}|["']|\\/[/*]|#))`, 'gu');
|
|
15
|
-
const TYPE_DECL = new RegExp(`(?:^|[;{}\\]\\n])[ \\t]*(?:\\[[^\\]\\n]*\\][ \\t]*)*(?:(?:public|internal|private|protected|sealed|static|partial|abstract|unsafe|file|new|readonly|ref)\\s+)*(record\\s+(?:class|struct)|class|record|struct|interface)\\s+(${NAME_START}[^\\s:<({;,=]*)`, 'gu');
|
|
16
|
-
const IDENT = '[\\p{L}_][\\p{L}\\p{N}_]*';
|
|
17
|
-
const QUALIFIER = `(?:${IDENT}\\s*(?:\\.|::)\\s*)*`;
|
|
18
|
-
const ATTRIBUTE_ARGS = '(?:\\((?:[^()\\]]|\\([^()\\]]*\\))*\\))?';
|
|
19
|
-
const OTHER_ATTRIBUTE = `${QUALIFIER}${IDENT}\\s*${ATTRIBUTE_ARGS}`;
|
|
20
|
-
const TEST_ATTRIBUTE_NAME = '(?:Fact|Theory|Test|TestMethod|DataTestMethod|TestCase|TestCaseSource)(?:Attribute)?';
|
|
21
|
-
// A false POSITIVE is the unsafe side (a helper taken for a test file hides the tests that
|
|
22
|
-
// use it), so the whole name must sit in an attribute section that CLOSES: a C# 12
|
|
23
|
-
// collection such as `[\n Env.Test,\n];` must not match. Residual cases: csharpClasses.
|
|
24
|
-
export const CS_TEST_ATTRIBUTE = new RegExp(
|
|
25
|
-
'(?:^|[;{}\\]]|\\*\\/)[ \\t]*\\['
|
|
26
|
-
+ `\\s*(?:${IDENT}\\s*:(?!:)\\s*)?`
|
|
27
|
-
+ `(?:${OTHER_ATTRIBUTE}\\s*,\\s*)*`
|
|
28
|
-
+ `${QUALIFIER}${TEST_ATTRIBUTE_NAME}(?![\\p{L}\\p{N}_])\\s*${ATTRIBUTE_ARGS}`
|
|
29
|
-
+ `(?:\\s*,\\s*${OTHER_ATTRIBUTE})*`
|
|
30
|
-
+ '\\s*\\]\\s*(?:[\\[\\p{L}_@/#]|(?![\\s\\S]))',
|
|
31
|
-
'mu',
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
// Roslyn reads CR/NEL/LS/PS as new-lines and U+001A as whitespace; JS `\s` misses NEL
|
|
35
|
-
// and U+001A, which hid `class<NEL>Name`. Mapped 1:1 so offsets stay aligned with the code.
|
|
36
|
-
const readText = (path) => {
|
|
37
|
-
try { return readFileSync(path, 'utf8').replace(/\x1A/g, ' ').replace(/^\uFEFF/, '').replace(/[\r\u0085\u2028\u2029]/g, '\n'); } catch { return null; }
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
function nearestProjectFile(fileAbs, rootAbs, projectName) {
|
|
41
|
-
for (let dir = dirname(fileAbs); isInside(dir, rootAbs); dir = dirname(dir)) {
|
|
42
|
-
let names = [];
|
|
43
|
-
try { names = readdirSync(dir).filter((n) => projectName.test(n)).sort(); } catch { /* unreadable dir */ }
|
|
44
|
-
if (names.length > 0) return join(dir, names[0]);
|
|
45
|
-
if (dirname(dir) === dir) break;
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function nearestProjectOf(fileAbs, rootAbs) {
|
|
51
|
-
return nearestProjectFile(fileAbs, rootAbs, /\.(?:cs|fs|vb)proj$/i);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const NON_CSHARP_SOURCE = /\.(?:razor|cshtml|fs|fsi|fsx|vb)$/i;
|
|
55
|
-
|
|
56
|
-
/** No type scan, hence no class selector, for these. */
|
|
57
|
-
export const isNonCSharpSource = (path) => NON_CSHARP_SOURCE.test(String(path));
|
|
58
|
-
|
|
59
|
-
export const isDotnetSource = (path) => /\.cs$/i.test(String(path)) || isNonCSharpSource(path);
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Nearest global.json walking up from the CWD, as the SDK resolves it (never from the
|
|
63
|
-
* `.sln` argument). null when one exists but cannot be read — never a guess.
|
|
64
|
-
*/
|
|
65
|
-
export function dotnetTestMode(absCwd) {
|
|
66
|
-
for (let dir = resolve(absCwd); ; dir = dirname(dir)) {
|
|
67
|
-
const file = join(dir, 'global.json');
|
|
68
|
-
if (existsSync(file)) {
|
|
69
|
-
try {
|
|
70
|
-
const parsed = JSON.parse(jsoncToJson(readFileSync(file, 'utf8')));
|
|
71
|
-
return parsed?.test?.runner === MTP_RUNNER ? 'mtp' : 'vstest';
|
|
72
|
-
} catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (dirname(dir) === dir) return 'vstest';
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const lineEnd = (text, i) => { const e = text.indexOf('\n', i); return e < 0 ? text.length : e; };
|
|
81
|
-
|
|
82
|
-
function closeOnLine(text, start, quote) {
|
|
83
|
-
for (let j = start + 1; j < text.length && text[j] !== '\n'; j++) {
|
|
84
|
-
if (text[j] === '\\') j++;
|
|
85
|
-
else if (text[j] === quote) return j + 1;
|
|
86
|
-
}
|
|
87
|
-
return -1;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// A `"` inside a hole (`$"{d["k"]}"`) ends the literal as the scanner reads it,
|
|
91
|
-
// leaving the hole open. `{{`/`}}` outside a hole are escapes.
|
|
92
|
-
function holeLeftOpen(content) {
|
|
93
|
-
let depth = 0;
|
|
94
|
-
for (let k = 0; k < content.length; k++) {
|
|
95
|
-
const c = content[k];
|
|
96
|
-
if (depth === 0 && (c === '{' || c === '}') && content[k + 1] === c) { k++; continue; }
|
|
97
|
-
if (c === '{') depth++;
|
|
98
|
-
else if (c === '}' && depth > 0) depth--;
|
|
99
|
-
}
|
|
100
|
-
return depth > 0;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Comments, preprocessor lines and literals blanked, length and newlines kept; null when
|
|
105
|
-
* a literal never ends or an interpolation hole is cut. A heuristic lexer that CAN misread:
|
|
106
|
-
* csharpClasses never trusts it alone (it checks against the raw text).
|
|
107
|
-
*/
|
|
108
|
-
export function blankCSharpLiterals(text) {
|
|
109
|
-
const out = text.split('');
|
|
110
|
-
const blank = (from, to) => { for (let k = from; k < to; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
111
|
-
for (let i = 0; i < text.length;) {
|
|
112
|
-
const c = text[i];
|
|
113
|
-
let end = -1;
|
|
114
|
-
if (c === '#' && /^[ \t]*$/.test(text.slice(text.lastIndexOf('\n', i - 1) + 1, i))) {
|
|
115
|
-
end = lineEnd(text, i);
|
|
116
|
-
} else if (c === '/' && text[i + 1] === '/') {
|
|
117
|
-
end = lineEnd(text, i);
|
|
118
|
-
} else if (c === '/' && text[i + 1] === '*') {
|
|
119
|
-
const close = text.indexOf('*/', i + 2);
|
|
120
|
-
if (close < 0) return null;
|
|
121
|
-
end = close + 2;
|
|
122
|
-
} else if (c === '"' && (text[i - 1] === '@' || (text[i - 1] === '$' && text[i - 2] === '@'))) {
|
|
123
|
-
let j = i + 1;
|
|
124
|
-
while (j < text.length && !(text[j] === '"' && text[j + 1] !== '"')) j += text[j] === '"' ? 2 : 1;
|
|
125
|
-
if (j >= text.length) return null;
|
|
126
|
-
end = j + 1;
|
|
127
|
-
const interpolated = text[i - 1] === '$' || text[i - 2] === '$';
|
|
128
|
-
if (interpolated && holeLeftOpen(text.slice(i + 1, end - 1))) return null;
|
|
129
|
-
} else if (c === '"') {
|
|
130
|
-
let quotes = 0;
|
|
131
|
-
while (text[i + quotes] === '"') quotes++;
|
|
132
|
-
if (quotes >= 3) {
|
|
133
|
-
const close = text.indexOf('"'.repeat(quotes), i + quotes);
|
|
134
|
-
if (close < 0) return null;
|
|
135
|
-
end = close + quotes;
|
|
136
|
-
} else {
|
|
137
|
-
end = closeOnLine(text, i, '"');
|
|
138
|
-
if (end < 0) return null;
|
|
139
|
-
if (text[i - 1] === '$' && holeLeftOpen(text.slice(i + 1, end - 1))) return null;
|
|
140
|
-
}
|
|
141
|
-
} else if (c === "'") {
|
|
142
|
-
end = closeOnLine(text, i, "'");
|
|
143
|
-
if (end < 0) return null;
|
|
144
|
-
}
|
|
145
|
-
if (end < 0) { i++; continue; }
|
|
146
|
-
blank(i, end);
|
|
147
|
-
i = end;
|
|
148
|
-
}
|
|
149
|
-
return out.join('');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* The types of a C# file, read FAIL-SAFE.
|
|
156
|
-
*
|
|
157
|
-
* `types` lists the BARE name of EVERY type declaration (class, record, struct,
|
|
158
|
-
* interface) at ANY depth, in source order, with no namespace. A selection built
|
|
159
|
-
* from these names is namespace-free (`--filter-class "*T*"`, measured, or the
|
|
160
|
-
* substring `FullyQualifiedName~T`): any namespace, `Outer+Inner`, `Name`1` — a
|
|
161
|
-
* selection that can only run MORE tests than the file's, never fewer.
|
|
162
|
-
*
|
|
163
|
-
* THE GUARANTEE (what `scannable: true` means), and why it holds:
|
|
164
|
-
* every type-keyword token of the RAW text is a declaration in `types`.
|
|
165
|
-
* - R = keyword tokens of the raw text (TYPE_KEYWORD, no context rule); C = the
|
|
166
|
-
* same on the blanked code; D = the declarations named (TYPE_DECL on the code).
|
|
167
|
-
* `scannable` requires |R| = |D| and |C| = |D|. By construction D ⊆ C ⊆ R as
|
|
168
|
-
* sets of offsets: blanking keeps length and only replaces characters by spaces,
|
|
169
|
-
* a blanked region starts at `"` `'` `/` `#` and ends at `"` `'` `*` `/` or a
|
|
170
|
-
* line end (never inside an identifier), and the raw lookahead accepts every
|
|
171
|
-
* character the code lookahead can be looking past (a name, a comment, `#`, a
|
|
172
|
-
* quote). So |R| = |D| forces R = D. A real declaration's keyword is always in R
|
|
173
|
-
* (it is followed by its name, or by a comment/directive before the name) —
|
|
174
|
-
* UNDER ONE ASSUMPTION: every character Roslyn reads as whitespace or a new-line
|
|
175
|
-
* between a keyword and its name is whitespace to JS `\s` once readText has run.
|
|
176
|
-
* Roslyn's whitespace is Unicode Zs, TAB, VT, FF and U+001A; its new-lines are
|
|
177
|
-
* CR, LF, NEL, LS and PS. JS `\s` already covers Zs, TAB, VT, FF and LF, and
|
|
178
|
-
* readText maps CR/NEL/LS/PS to `\n` and U+001A to a space. A separator outside
|
|
179
|
-
* that shared set would hide a declaration from R; none is known, and that is a
|
|
180
|
-
* statement about the two lexers as read, not a proof.
|
|
181
|
-
* Hence: a declaration swallowed by ANY misread — `"/*"` taken for a comment
|
|
182
|
-
* opener, a quote inside an interpolation hole, a char literal — is still in R
|
|
183
|
-
* and cannot be compensated by anything (a fake the code exposes is in R too).
|
|
184
|
-
* - `abstract` lists the abstract types: their tests run under the names of DERIVED
|
|
185
|
-
* classes, possibly in other files.
|
|
186
|
-
* - `hasTestAttribute`: a test attribute (CS_TEST_ATTRIBUTE: a whole name inside an
|
|
187
|
-
* attribute section) is present in the blanked code (a file without one is a
|
|
188
|
-
* helper/fake/fixture: the tests using it are elsewhere).
|
|
189
|
-
* - kept fail-safes: braces balance and blankCSharpLiterals returning null → unscannable.
|
|
190
|
-
*
|
|
191
|
-
* NOT guaranteed (stated, not hidden):
|
|
192
|
-
* - completeness: keyword-shaped text in a real comment or string (`// the
|
|
193
|
-
* class under test`), a constraint keyword followed by a name-shaped token
|
|
194
|
-
* (`where T : class where …`, `: class // note`), `record with`, a directive
|
|
195
|
-
* splitting a name — all make the file unscannable;
|
|
196
|
-
* - which scope a type was read in;
|
|
197
|
-
* - a NON-abstract class whose tests are inherited by derived classes declared in
|
|
198
|
-
* OTHER files: the touched class runs; the derived copies only if their names
|
|
199
|
-
* contain it;
|
|
200
|
-
* - a test attribute reached through an alias (`using F = Xunit.FactAttribute; [F]`)
|
|
201
|
-
* or a custom one (`[SkippableFact]`) is not seen: the file counts as a helper and
|
|
202
|
-
* it is left out of a selection;
|
|
203
|
-
* - `hasTestAttribute` is still a heuristic, and ITS false positive is the unsafe
|
|
204
|
-
* side. The section must close; what still matches in a
|
|
205
|
-
* helper — probed — is a LIST PATTERN opening a line (or after `;` `{` `}` `]`)
|
|
206
|
-
* whose element is named exactly `Test`/`Fact`/… (qualified or not) and whose `]`
|
|
207
|
-
* is followed by a combinator: `[Env.Test] or [Env.Dev] => true`. Such a helper is
|
|
208
|
-
* taken for a test file. (`[Env.Test] => 1` does not match: `=` starts no declaration.)
|
|
209
|
-
*
|
|
210
|
-
* @param {string} csFileAbs
|
|
211
|
-
* @returns {{ names: string[], types: string[], abstract: string[], hasTestClass: boolean, hasTestAttribute: boolean, scannable: boolean, why: string|null }}
|
|
212
|
-
* `names`: top-level `class` names (classNamesOf). `abstract`: the abstract
|
|
213
|
-
* types. `hasTestClass`: some plain `class` is declared (records, structs and
|
|
214
|
-
* interfaces hold no tests). `hasTestAttribute`: see above. `why`: the reason
|
|
215
|
-
* when `scannable` is false.
|
|
216
|
-
*/
|
|
217
|
-
export function csharpClasses(csFileAbs) {
|
|
218
|
-
const unscannable = (why) => ({ names: [], types: [], abstract: [], hasTestClass: false, hasTestAttribute: false, scannable: false, why });
|
|
219
|
-
const text = readText(csFileAbs);
|
|
220
|
-
if (text === null) return { names: [], types: [], abstract: [], hasTestClass: false, hasTestAttribute: false, scannable: true, why: null };
|
|
221
|
-
const code = blankCSharpLiterals(text);
|
|
222
|
-
if (code === null) return unscannable('a literal or comment never ends, or an interpolation hole is cut by an inner quote');
|
|
223
|
-
|
|
224
|
-
const decls = [...code.matchAll(TYPE_DECL)].map((m) => ({
|
|
225
|
-
at: m.index + m[0].length - m[2].length,
|
|
226
|
-
kind: m[1].split(/\s+/)[0],
|
|
227
|
-
name: m[2],
|
|
228
|
-
abstract: /\babstract\b/.test(m[0]),
|
|
229
|
-
gap: m[0].slice(0, m[0].length - m[2].length).match(/\s*$/)[0].length,
|
|
230
|
-
}));
|
|
231
|
-
// A preprocessor line between a keyword and its name (`class\n#if X\nA\n#else\nB…`)
|
|
232
|
-
// makes the compiled name depend on a symbol this scan cannot see. Blanking keeps
|
|
233
|
-
// length, so the raw text at the same offsets shows the directive.
|
|
234
|
-
const split = decls.find((d) => /(^|\n)[ \t]*#/.test(text.slice(d.at - d.gap, d.at)));
|
|
235
|
-
if (split) return unscannable(`a preprocessor directive stands between a type keyword and the name "${split.name}"`);
|
|
236
|
-
// The raw text is the lexer-independent truth about how many type keywords the file
|
|
237
|
-
// carries (the JSDoc explains why equal counts mean equal sets).
|
|
238
|
-
const rawKeywords = typeKeywordCount(text);
|
|
239
|
-
if (rawKeywords !== decls.length) {
|
|
240
|
-
return unscannable(`${rawKeywords} type keyword(s) in the file but ${decls.length} declaration(s) recognised`);
|
|
241
|
-
}
|
|
242
|
-
const codeKeywords = typeKeywordCount(code);
|
|
243
|
-
if (codeKeywords !== decls.length) {
|
|
244
|
-
return unscannable(`${codeKeywords} type keyword(s) in the code but ${decls.length} declaration(s) recognised`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const names = [];
|
|
248
|
-
const types = [];
|
|
249
|
-
const abstractTypes = [];
|
|
250
|
-
const scopes = [];
|
|
251
|
-
let hasTestClass = false;
|
|
252
|
-
let header = 0;
|
|
253
|
-
for (let i = 0, d = 0; i < code.length; i++) {
|
|
254
|
-
for (; d < decls.length && decls[d].at === i; d++) {
|
|
255
|
-
types.push(decls[d].name);
|
|
256
|
-
if (decls[d].abstract) abstractTypes.push(decls[d].name);
|
|
257
|
-
if (decls[d].kind === 'class') {
|
|
258
|
-
hasTestClass = true;
|
|
259
|
-
if (!scopes.some((s) => s.type)) names.push(decls[d].name);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const c = code[i];
|
|
263
|
-
if (c === '{') {
|
|
264
|
-
scopes.push({ type: TYPE_HEADER.test(code.slice(header, i)) });
|
|
265
|
-
} else if (c === '}' && scopes.pop() === undefined) {
|
|
266
|
-
return unscannable('a `}` closes nothing');
|
|
267
|
-
}
|
|
268
|
-
if (c === '{' || c === '}' || c === ';') header = i + 1;
|
|
269
|
-
}
|
|
270
|
-
if (scopes.length > 0) return unscannable('braces left open at end of file');
|
|
271
|
-
return {
|
|
272
|
-
names: [...new Set(names)],
|
|
273
|
-
types: [...new Set(types)],
|
|
274
|
-
abstract: [...new Set(abstractTypes)],
|
|
275
|
-
hasTestClass,
|
|
276
|
-
hasTestAttribute: CS_TEST_ATTRIBUTE.test(code),
|
|
277
|
-
scannable: true,
|
|
278
|
-
why: null,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
@@ -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
|
-
}
|