@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
|
@@ -1,40 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* .NET
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* file declares. Split out of test-scope.js, which decides; this only reads.
|
|
6
|
-
*
|
|
7
|
-
* Every answer errs toward "run the node whole". A wrong answer that NARROWS runs
|
|
8
|
-
* fewer tests and still comes out green — the one failure issue #87 must never buy.
|
|
9
|
-
*
|
|
10
|
-
* The C# scan is NOT a parser. What narrowing relies on is narrower than "the
|
|
11
|
-
* scan is right" and is stated exactly in csharpClasses: every type-keyword
|
|
12
|
-
* token of the RAW text (nothing blanked, no context rule) must be a declaration
|
|
13
|
-
* the scanner NAMED. Type names are BARE and the selectors namespace-free
|
|
14
|
-
* (`*Type*` / `FullyQualifiedName~Type`), so no scope reading can misname one.
|
|
15
|
-
* What that does NOT cover is listed there too.
|
|
2
|
+
* .NET introspection the targeted mutation (`morph-spec mutate`) needs: which
|
|
3
|
+
* project owns a file, which test mode the SDK picks, and which types a C# file
|
|
4
|
+
* declares — read FAIL-SAFE (see csharpClasses).
|
|
16
5
|
*
|
|
17
6
|
* Pure: reads files, never spawns a process.
|
|
18
|
-
*
|
|
19
|
-
* @module dotnet-test-introspection
|
|
20
7
|
*/
|
|
21
|
-
|
|
22
8
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
23
9
|
import { dirname, join, resolve } from 'path';
|
|
24
10
|
|
|
25
|
-
import { isInside } from '
|
|
11
|
+
import { isInside } from '../tasks/test-runner.js';
|
|
26
12
|
import { jsoncToJson } from '../../../framework/hooks/shared/comment-stripper.js';
|
|
27
13
|
|
|
28
14
|
const MTP_RUNNER = 'Microsoft.Testing.Platform';
|
|
29
|
-
const FLOOR_PROPERTY = /<TestingPlatformCommandLineArguments\b[^>]*>[^<]*--minimum-expected-tests/i;
|
|
30
|
-
/**
|
|
31
|
-
* `--ignore-exit-code`, or the env var MTP reads for the same thing, named ANYWHERE in an
|
|
32
|
-
* MSBuild file that binds the project — not only inside `<TestingPlatformCommandLineArguments>`:
|
|
33
|
-
* a property indirection (`<MtpExtraArgs>--ignore-exit-code 8</MtpExtraArgs>` referenced as
|
|
34
|
-
* `$(MtpExtraArgs)`) carries it just as well (re-review N4, probed). Errs wide.
|
|
35
|
-
*/
|
|
36
|
-
const IGNORE_EXIT_MARKER = /--ignore-exit-code|TESTINGPLATFORM_EXITCODE_IGNORE/i;
|
|
37
|
-
const CASE_FILTER_PROPERTY = /<VSTestTestCaseFilter\b[^>]*>/i;
|
|
38
15
|
const TYPE_HEADER = /\b(?:class|struct|record|interface)\b/;
|
|
39
16
|
/** A C# identifier's first character: a letter, an Nl letter (`Ⅱ`), `_`, `@` (verbatim) or `\` (a unicode escape). */
|
|
40
17
|
const NAME_START = '[\\p{L}\\p{Nl}_@\\\\]';
|
|
@@ -52,7 +29,7 @@ const NAME_START = '[\\p{L}\\p{Nl}_@\\\\]';
|
|
|
52
29
|
* the code counts is a keyword the raw text counts, by offset.
|
|
53
30
|
* A constraint keyword is therefore counted too whenever a name-shaped token
|
|
54
31
|
* follows it (`where T : class where U : …`, `: class // note`): it is not named,
|
|
55
|
-
* so the file
|
|
32
|
+
* so the file is unscannable. `class,` / `class\n{` / `class?` are not counted at all.
|
|
56
33
|
*/
|
|
57
34
|
const TYPE_KEYWORD = new RegExp(`\\b(?:record\\s+(?:class|struct)|class|record|struct|interface)\\b(?=\\s*(?:${NAME_START}|["']|\\/[/*]|#))`, 'gu');
|
|
58
35
|
/** A type declaration the scanner can NAME: after `; { } ]`, a newline or the file start; attributes; modifiers; a NAME_START name. */
|
|
@@ -75,14 +52,13 @@ const TEST_ATTRIBUTE_NAME = '(?:Fact|Theory|Test|TestMethod|DataTestMethod|TestC
|
|
|
75
52
|
* or the end of the input.
|
|
76
53
|
*
|
|
77
54
|
* A false POSITIVE is the unsafe direction here: a helper taken for a test file
|
|
78
|
-
* narrows to its own name, and
|
|
79
|
-
* while the tests that USE the helper never run. The final review of branch B found
|
|
55
|
+
* narrows the subset to its own name, and the tests that USE the helper never run. The final review of branch B found
|
|
80
56
|
* exactly that with the old prefix match (`, TestContext` / `, Factory` / `, TheoryData`
|
|
81
57
|
* after any `,`), and its re-review (N1) with a test name merely FOLLOWED by `(` or `,`:
|
|
82
58
|
* a C# 12 collection laid out with `[` alone on its line (`[\n Env.Dev,\n Env.Test,\n];`,
|
|
83
59
|
* a row `[Env.Test, Env.Dev],`, `[\n Test("a"),\n];`). Requiring the section to close
|
|
84
60
|
* rejects all three; what still matches is stated in csharpClasses. A false NEGATIVE
|
|
85
|
-
* only
|
|
61
|
+
* only leaves a test file out of a selection (a custom attribute such as `[SkippableFact]`, a test
|
|
86
62
|
* attribute after an argument holding `]`).
|
|
87
63
|
*/
|
|
88
64
|
export const CS_TEST_ATTRIBUTE = new RegExp(
|
|
@@ -109,35 +85,6 @@ const readText = (path) => {
|
|
|
109
85
|
};
|
|
110
86
|
|
|
111
87
|
/** Which test framework a csproj references, by package (or MSBuild SDK) name. */
|
|
112
|
-
function testFrameworkOf(csprojText) {
|
|
113
|
-
const refs = [...csprojText.matchAll(/(?:Include|Sdk)\s*=\s*"([^"/]+)/gi)].map((m) => m[1].toLowerCase());
|
|
114
|
-
if (refs.some((r) => r.startsWith('xunit.v3'))) return 'xunit.v3';
|
|
115
|
-
if (refs.some((r) => r.startsWith('mstest'))) return 'mstest';
|
|
116
|
-
if (refs.some((r) => r === 'nunit')) return 'nunit';
|
|
117
|
-
if (refs.some((r) => r === 'xunit')) return 'xunit';
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* A test project: named `*Tests.csproj`/`.fsproj`/`.vbproj`, OR referencing a
|
|
123
|
-
* test framework / `Microsoft.NET.Test.Sdk` in the project file itself.
|
|
124
|
-
*/
|
|
125
|
-
export function isDotnetTestProject(projectAbs) {
|
|
126
|
-
const text = readText(projectAbs) ?? '';
|
|
127
|
-
return /Tests\.(?:cs|fs|vb)proj$/i.test(projectAbs) || testFrameworkOf(text) !== null || /Microsoft\.NET\.Test\.Sdk/i.test(text);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The test project owning a `.cs` file — nearest `.csproj` walking up, as MSBuild
|
|
132
|
-
* globs it — or null when the file is not a test file (see isDotnetTestProject).
|
|
133
|
-
*
|
|
134
|
-
* @returns {{ csproj: string, framework: 'xunit.v3'|'mstest'|'nunit'|'xunit'|null }|null}
|
|
135
|
-
*/
|
|
136
|
-
export function dotnetTestProjectOf(fileAbs, rootAbs) {
|
|
137
|
-
const csproj = nearestCsprojOf(fileAbs, rootAbs);
|
|
138
|
-
if (csproj === null || !isDotnetTestProject(csproj)) return null;
|
|
139
|
-
return { csproj, framework: testFrameworkOf(readText(csproj) ?? '') };
|
|
140
|
-
}
|
|
141
88
|
|
|
142
89
|
/** Nearest file matching `projectName` walking up from a file's directory, inside the root; null when none. */
|
|
143
90
|
function nearestProjectFile(fileAbs, rootAbs, projectName) {
|
|
@@ -150,21 +97,10 @@ function nearestProjectFile(fileAbs, rootAbs, projectName) {
|
|
|
150
97
|
return null;
|
|
151
98
|
}
|
|
152
99
|
|
|
153
|
-
/**
|
|
154
|
-
* The `.csproj` that compiles a `.cs` file — nearest one walking up, as MSBuild
|
|
155
|
-
* globs it — test project or not; null when there is none inside the root.
|
|
156
|
-
* The one door for "which project owns this file": link ownership in test-scope
|
|
157
|
-
* asks it about every touched `.cs`, recognised as a test file or not.
|
|
158
|
-
*/
|
|
159
|
-
export function nearestCsprojOf(fileAbs, rootAbs) {
|
|
160
|
-
if (!/\.cs$/i.test(fileAbs)) return null;
|
|
161
|
-
return nearestProjectFile(fileAbs, rootAbs, /\.csproj$/i);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
100
|
/**
|
|
165
101
|
* The project — `.csproj`, `.fsproj` or `.vbproj` — nearest to ANY file walking
|
|
166
102
|
* up, or null inside the root. Asked about the .NET sources the C# scan cannot
|
|
167
|
-
* read (a bUnit `.razor`, an F# `.fs`)
|
|
103
|
+
* read (a bUnit `.razor`, an F# `.fs`).
|
|
168
104
|
*/
|
|
169
105
|
export function nearestProjectOf(fileAbs, rootAbs) {
|
|
170
106
|
return nearestProjectFile(fileAbs, rootAbs, /\.(?:cs|fs|vb)proj$/i);
|
|
@@ -173,33 +109,12 @@ export function nearestProjectOf(fileAbs, rootAbs) {
|
|
|
173
109
|
/** .NET sources the C# scan cannot read: Razor (`.razor`, `.cshtml`), F# (`.fs`, `.fsi`, `.fsx`), VB (`.vb`). */
|
|
174
110
|
const NON_CSHARP_SOURCE = /\.(?:razor|cshtml|fs|fsi|fsx|vb)$/i;
|
|
175
111
|
|
|
176
|
-
/** A .NET source other than C#:
|
|
112
|
+
/** A .NET source other than C#: no type scan, hence no class selector, for it. */
|
|
177
113
|
export const isNonCSharpSource = (path) => NON_CSHARP_SOURCE.test(String(path));
|
|
178
114
|
|
|
179
115
|
/** Any .NET source a project compiles — C# or one of the others. */
|
|
180
116
|
export const isDotnetSource = (path) => /\.cs$/i.test(String(path)) || isNonCSharpSource(path);
|
|
181
117
|
|
|
182
|
-
const MARKER_TAIL = '\\s*(?:[\\p{L}_][\\p{L}\\p{N}_]*\\s*(?:\\.|::)\\s*)*(?:Fact|Theory|Test|DataTestMethod)';
|
|
183
|
-
/** Per language, the LOOSE test marker readsLikeTestSource looks for in the raw text. */
|
|
184
|
-
const LOOSE_TEST_MARKERS = [
|
|
185
|
-
[/\.(?:cs|razor|cshtml)$/i, new RegExp(`[[,]${MARKER_TAIL}`, 'u')], // C#, Razor: `[Fact`, `, Theory`, `[Xunit.Fact(`
|
|
186
|
-
[/\.(?:fs|fsi|fsx)$/i, new RegExp(`(?:\\[<|;)${MARKER_TAIL}`, 'u')], // F#: `[<Fact>]`, `[<Trait(…); Fact>]`
|
|
187
|
-
[/\.vb$/i, new RegExp(`[<,]${MARKER_TAIL}`, 'u')], // VB: `<Fact>`, `<Trait(…), Fact>`
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Does a file the recogniser did NOT classify READ like a test, in its raw text
|
|
192
|
-
* (comments and strings included)? The question test-scope asks before narrowing
|
|
193
|
-
* PAST a touched file, so its errors cost the OPPOSITE of CS_TEST_ATTRIBUTE's: a
|
|
194
|
-
* false positive runs a node whole (time), a false negative skips a touched test.
|
|
195
|
-
* So it errs wide on purpose — a name PREFIX is enough (`[TestContext` counts).
|
|
196
|
-
* false for any extension it has no marker for.
|
|
197
|
-
*/
|
|
198
|
-
export function readsLikeTestSource(fileAbs) {
|
|
199
|
-
const marker = LOOSE_TEST_MARKERS.find(([ext]) => ext.test(fileAbs))?.[1];
|
|
200
|
-
return marker ? marker.test(readText(fileAbs) ?? '') : false;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
118
|
/**
|
|
204
119
|
* `.NET` test mode for a cwd: nearest `global.json` walking UP (as the SDK
|
|
205
120
|
* resolves it — from the CWD, never from the `.sln` argument), read as JSONC.
|
|
@@ -224,172 +139,6 @@ export function dotnetTestMode(absCwd) {
|
|
|
224
139
|
}
|
|
225
140
|
}
|
|
226
141
|
|
|
227
|
-
/** Read on EVERY level from the project's folder up (bindingMsbuildFiles). */
|
|
228
|
-
const DIRECTORY_FILES = ['Directory.Build.props', 'Directory.Build.targets', 'Directory.Build.rsp', 'Directory.Packages.props'];
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* The MSBuild files that bind a project, as far as this module reads them: the csproj,
|
|
232
|
-
* the `<csproj>.user` Microsoft.Common.targets imports beside it, and every
|
|
233
|
-
* DIRECTORY_FILES walking up to the FILESYSTEM root — MSBuild's lookup does not stop at
|
|
234
|
-
* the repository, and a Directory.Build.targets above it binds the project just the same
|
|
235
|
-
* (#114 review F3). Every level is read, not only the first file MSBuild would import:
|
|
236
|
-
* that errs wide. Not read: an `<Import Project=…>` (floorOverride refuses a csproj that
|
|
237
|
-
* has one), a NuGet package's `build/*.targets`, the SDK's own imports.
|
|
238
|
-
*/
|
|
239
|
-
function bindingMsbuildFiles(csprojAbs) {
|
|
240
|
-
const files = [csprojAbs, `${csprojAbs}.user`];
|
|
241
|
-
for (let dir = dirname(csprojAbs); ; dir = dirname(dir)) {
|
|
242
|
-
files.push(...DIRECTORY_FILES.map((name) => join(dir, name)));
|
|
243
|
-
if (dirname(dir) === dir) return files;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Does an MSBuild file that binds this project (bindingMsbuildFiles) match `pattern`?
|
|
249
|
-
* It errs wide: a false positive costs a whole run, a false negative a red (or a
|
|
250
|
-
* green) that no test earned.
|
|
251
|
-
*/
|
|
252
|
-
function msbuildDeclares(csprojAbs, pattern) {
|
|
253
|
-
return bindingMsbuildFiles(csprojAbs).some((p) => pattern.test(readText(p) ?? ''));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* The project imposes `--minimum-expected-tests` on every run: in the MSBuild
|
|
258
|
-
* property (attributes such as `Condition=` allowed), or in a `*testconfig.json`
|
|
259
|
-
* beside the csproj. Measured: the CLI flag cannot override it, so a green recorte
|
|
260
|
-
* exits 9 — unless floorOverride can lower it (#114).
|
|
261
|
-
*/
|
|
262
|
-
export function imposesMinimumExpected(csprojAbs) {
|
|
263
|
-
return msbuildDeclares(csprojAbs, FLOOR_PROPERTY) || testconfigDeclares(csprojAbs, /minimum-?expected-?tests/i);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const ARGS_NAME = 'TestingPlatformCommandLineArguments';
|
|
267
|
-
const ARGS_ELEMENT = /<TestingPlatformCommandLineArguments\b([^>]*)>([^<]*)<\/TestingPlatformCommandLineArguments\s*>/gi;
|
|
268
|
-
const ARGS_OPENING = /<TestingPlatformCommandLineArguments\b/gi;
|
|
269
|
-
/** Named anywhere: an element, a `$(…)` read, a `-p:` in a Directory.Build.rsp. */
|
|
270
|
-
const ARGS_MENTION = /TestingPlatformCommandLineArguments/i;
|
|
271
|
-
const IMPORT_ELEMENT = /<Import\b/i;
|
|
272
|
-
/**
|
|
273
|
-
* A tag — or a comment, CDATA section, declaration or processing instruction, which
|
|
274
|
-
* enclosingElements skips. An attribute value may hold `>` (`Condition="'$(A)' > '1'"`),
|
|
275
|
-
* so a quoted value is consumed whole.
|
|
276
|
-
*/
|
|
277
|
-
const XML_TOKEN = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<[!?][^>]*>|<(\/?)([A-Za-z_][\w.:-]*)((?:[^>"']|"[^"]*"|'[^']*')*?)(\/?)>/g;
|
|
278
|
-
/**
|
|
279
|
-
* What a value cannot carry and still be re-passed as ONE double-quoted `-p:`
|
|
280
|
-
* argument meaning the same thing: MSBuild expansion and escapes (`$ @ %`), its
|
|
281
|
-
* property separators (`; ,`), an XML entity (`&`), and shell quoting (`" ' \` \ !`).
|
|
282
|
-
*/
|
|
283
|
-
const UNSAFE_IN_PROPERTY = /[$@%;,&"'`\\!]/;
|
|
284
|
-
/** Read after ASCII whitespace is collapsed: a no-break space between flag and number matches nothing (review F7). */
|
|
285
|
-
const FLOOR_ARGUMENT = /(^| )--minimum-expected-tests \d+(?= |$)/;
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* The elements enclosing the opening tag of `name`, outermost first — or null when the
|
|
289
|
-
* tags do not balance. Fails SAFE: a document it cannot walk is null, and the node runs
|
|
290
|
-
* whole.
|
|
291
|
-
*/
|
|
292
|
-
function enclosingElements(text, name) {
|
|
293
|
-
const stack = [];
|
|
294
|
-
let enclosing = null;
|
|
295
|
-
for (const [, closing, tag, attributes, selfClosing] of text.matchAll(XML_TOKEN)) {
|
|
296
|
-
if (tag === undefined) continue;
|
|
297
|
-
if (closing) {
|
|
298
|
-
if (stack.pop()?.tag.toLowerCase() !== tag.toLowerCase()) return null;
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (tag.toLowerCase() === name.toLowerCase()) enclosing = [...stack];
|
|
302
|
-
if (!selfClosing) stack.push({ tag, attributes });
|
|
303
|
-
}
|
|
304
|
-
return stack.length === 0 ? enclosing : null;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* The element is set on EVERY evaluation, by nothing but itself: its parent is a
|
|
309
|
-
* `<PropertyGroup>` with no attribute, directly under `<Project>` — never a `<Target>`, a
|
|
310
|
-
* `<Choose>/<When>`, an `<ItemDefinitionGroup>` or a conditional group (#114 review F1:
|
|
311
|
-
* the element's OWN attributes were the only ones read, and a false `Condition` on its
|
|
312
|
-
* group got the value injected into a run MSBuild would never give it). `<Project>` does
|
|
313
|
-
* not mark it `TreatAsLocalProperty` (the csproj would beat the global `-p:` and a green
|
|
314
|
-
* recorte exit 9, review F5), and the csproj imports nothing that could append to it.
|
|
315
|
-
*/
|
|
316
|
-
function setUnconditionally(text) {
|
|
317
|
-
if (IMPORT_ELEMENT.test(text)) return false;
|
|
318
|
-
const enclosing = enclosingElements(text, ARGS_NAME);
|
|
319
|
-
if (enclosing?.length !== 2) return false;
|
|
320
|
-
const [project, group] = enclosing;
|
|
321
|
-
return /^Project$/i.test(project.tag) && !/TreatAsLocalProperty/i.test(project.attributes)
|
|
322
|
-
&& /^PropertyGroup$/i.test(group.tag) && group.attributes.trim() === '';
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* The value for `-p:TestingPlatformCommandLineArguments=` that lowers the project's
|
|
327
|
-
* floor to 1 and keeps every other argument (#114) — or null, and the node runs whole.
|
|
328
|
-
*
|
|
329
|
-
* Measured (test/fixtures/test-output/): the CLI flag does NOT override the project's
|
|
330
|
-
* floor — a green recorte still exits 9 —, the global MSBuild property does — green
|
|
331
|
-
* exits 0, zero tests still exit 8 — and it replaces the value WHOLE, so the rest is
|
|
332
|
-
* re-passed verbatim (mtp-minimum-expected-override-multiarg.txt). No rebuild: six
|
|
333
|
-
* runs alternating with and without it kept the test DLL's mtime, ~3 s each.
|
|
334
|
-
*
|
|
335
|
-
* So the value has to be exactly the one MSBuild would use, read from ONE place:
|
|
336
|
-
* - ONE `<TestingPlatformCommandLineArguments>` in the csproj itself, set on every
|
|
337
|
-
* evaluation (setUnconditionally) — no attribute of its own, no second opening
|
|
338
|
-
* (another element, or one in an XML comment: the count is over the raw text);
|
|
339
|
-
* - the property named in no other file that binds the project (bindingMsbuildFiles:
|
|
340
|
-
* the `.user` file, Directory.Build.* up to the filesystem root) — one could append
|
|
341
|
-
* to it or replace it;
|
|
342
|
-
* - no floor in a `*testconfig.json` beside it (`-p:` does not reach that file);
|
|
343
|
-
* - exactly one literal `--minimum-expected-tests N`, and nothing UNSAFE_IN_PROPERTY.
|
|
344
|
-
* Out of reach: a NuGet package's build targets appending to the property. The override
|
|
345
|
-
* drops that argument during the recorte, and one like xunit.v3's `--explicit` changes
|
|
346
|
-
* WHICH tests run — a limit framework/docs/CLI.md states.
|
|
347
|
-
*
|
|
348
|
-
* @returns {string|null}
|
|
349
|
-
*/
|
|
350
|
-
export function floorOverride(csprojAbs) {
|
|
351
|
-
const text = readText(csprojAbs);
|
|
352
|
-
if (text === null) return null;
|
|
353
|
-
const elements = [...text.matchAll(ARGS_ELEMENT)];
|
|
354
|
-
if (elements.length !== 1 || (text.match(ARGS_OPENING) ?? []).length !== 1) return null;
|
|
355
|
-
const [, attributes, raw] = elements[0];
|
|
356
|
-
if (attributes.trim() !== '' || !setUnconditionally(text)) return null;
|
|
357
|
-
if (bindingMsbuildFiles(csprojAbs).slice(1).some((p) => ARGS_MENTION.test(readText(p) ?? ''))) return null;
|
|
358
|
-
if (testconfigDeclares(csprojAbs, /minimum-?expected-?tests/i)) return null;
|
|
359
|
-
const value = raw.replace(/[ \t\r\n]+/g, ' ').trim();
|
|
360
|
-
if (UNSAFE_IN_PROPERTY.test(value)) return null;
|
|
361
|
-
if ((value.match(/--minimum-expected-tests/g) ?? []).length !== 1 || !FLOOR_ARGUMENT.test(value)) return null;
|
|
362
|
-
return value.replace(FLOOR_ARGUMENT, '$1--minimum-expected-tests 1');
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* The project can make MTP IGNORE an exit code on every run: `--ignore-exit-code` or
|
|
367
|
-
* the `TESTINGPLATFORM_EXITCODE_IGNORE` variable named anywhere in an MSBuild file that
|
|
368
|
-
* binds the project — inside `<TestingPlatformCommandLineArguments>` or in a property
|
|
369
|
-
* it references through `$(…)` — or either one in a `*testconfig.json` beside the csproj.
|
|
370
|
-
* Under MTP a narrowed run's proof that tests ran is the summary's count and — when
|
|
371
|
-
* the summary is not readable (#115: pt-BR and en-US are) — exit 0 (zero tests exit
|
|
372
|
-
* 8); with 8 ignored, zero tests exit 0 and that proof is gone — the final review of
|
|
373
|
-
* branch B probed it end to end. Errs wide, like the floor.
|
|
374
|
-
*/
|
|
375
|
-
export function ignoresExitCode(csprojAbs) {
|
|
376
|
-
return msbuildDeclares(csprojAbs, IGNORE_EXIT_MARKER)
|
|
377
|
-
|| testconfigDeclares(csprojAbs, /ignore-?exit-?code|exit-?code-?ignore|TESTINGPLATFORM_EXITCODE_IGNORE/i);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Does a `*testconfig.json` beside the csproj (MTP reads it from the project) match `pattern`? */
|
|
381
|
-
function testconfigDeclares(csprojAbs, pattern) {
|
|
382
|
-
const projectDir = dirname(csprojAbs);
|
|
383
|
-
let names = [];
|
|
384
|
-
try { names = readdirSync(projectDir).filter((n) => /testconfig\.json$/i.test(n)); } catch { /* none */ }
|
|
385
|
-
return names.some((n) => pattern.test(readText(join(projectDir, n)) ?? ''));
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/** The project declares `<VSTestTestCaseFilter>` — an appended `--filter` would silently REPLACE it. */
|
|
389
|
-
export function declaresCaseFilter(csprojAbs) {
|
|
390
|
-
return msbuildDeclares(csprojAbs, CASE_FILTER_PROPERTY);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
142
|
const lineEnd = (text, i) => { const e = text.indexOf('\n', i); return e < 0 ? text.length : e; };
|
|
394
143
|
|
|
395
144
|
/** End (exclusive) of a regular string or char literal opened at `start`; -1 if the line ends first. */
|
|
@@ -431,7 +180,7 @@ function holeLeftOpen(content) {
|
|
|
431
180
|
*
|
|
432
181
|
* This is a heuristic lexer and it CAN misread (known: `holeLeftOpen` counts the
|
|
433
182
|
* braces of a char literal such as `'}'`, so `$"{(c == '}' ? "a" : "/*")}"` is cut
|
|
434
|
-
* at the inner quote and `"/*"` becomes code). Nothing
|
|
183
|
+
* at the inner quote and `"/*"` becomes code). Nothing relies on the
|
|
435
184
|
* blanking being right: csharpClasses compares the declarations it names against
|
|
436
185
|
* the type keywords of the RAW text, and any declaration a misread hides makes the
|
|
437
186
|
* file unscannable.
|
|
@@ -490,8 +239,8 @@ const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
|
|
|
490
239
|
* The types of a C# file, read FAIL-SAFE.
|
|
491
240
|
*
|
|
492
241
|
* `types` lists the BARE name of EVERY type declaration (class, record, struct,
|
|
493
|
-
* interface) at ANY depth, in source order, with no namespace.
|
|
494
|
-
*
|
|
242
|
+
* interface) at ANY depth, in source order, with no namespace. A selection built
|
|
243
|
+
* from these names is namespace-free (`--filter-class "*T*"`, measured, or the
|
|
495
244
|
* substring `FullyQualifiedName~T`): any namespace, `Outer+Inner`, `Name`1` — a
|
|
496
245
|
* selection that can only run MORE tests than the file's, never fewer.
|
|
497
246
|
*
|
|
@@ -517,25 +266,24 @@ const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
|
|
|
517
266
|
* opener, a quote inside an interpolation hole, a char literal — is still in R
|
|
518
267
|
* and cannot be compensated by anything (a fake the code exposes is in R too).
|
|
519
268
|
* - `abstract` lists the abstract types: their tests run under the names of DERIVED
|
|
520
|
-
* classes, possibly in files
|
|
269
|
+
* classes, possibly in other files.
|
|
521
270
|
* - `hasTestAttribute`: a test attribute (CS_TEST_ATTRIBUTE: a whole name inside an
|
|
522
271
|
* attribute section) is present in the blanked code (a file without one is a
|
|
523
272
|
* helper/fake/fixture: the tests using it are elsewhere).
|
|
524
273
|
* - kept fail-safes: braces balance and blankCSharpLiterals returning null → unscannable.
|
|
525
274
|
*
|
|
526
275
|
* NOT guaranteed (stated, not hidden):
|
|
527
|
-
* -
|
|
276
|
+
* - completeness: keyword-shaped text in a real comment or string (`// the
|
|
528
277
|
* class under test`), a constraint keyword followed by a name-shaped token
|
|
529
278
|
* (`where T : class where …`, `: class // note`), `record with`, a directive
|
|
530
|
-
* splitting a name — all
|
|
531
|
-
* - which scope a type was read in
|
|
532
|
-
* informational only — nothing in narrowing depends on it);
|
|
279
|
+
* splitting a name — all make the file unscannable;
|
|
280
|
+
* - which scope a type was read in;
|
|
533
281
|
* - a NON-abstract class whose tests are inherited by derived classes declared in
|
|
534
282
|
* OTHER files: the touched class runs; the derived copies only if their names
|
|
535
283
|
* contain it;
|
|
536
284
|
* - a test attribute reached through an alias (`using F = Xunit.FactAttribute; [F]`)
|
|
537
285
|
* or a custom one (`[SkippableFact]`) is not seen: the file counts as a helper and
|
|
538
|
-
*
|
|
286
|
+
* it is left out of a selection;
|
|
539
287
|
* - `hasTestAttribute` is still a heuristic, and ITS false positive is the unsafe
|
|
540
288
|
* side. Since the re-review (N1) the section must close; what still matches in a
|
|
541
289
|
* helper — probed — is a LIST PATTERN opening a line (or after `;` `{` `}` `]`)
|
|
@@ -617,16 +365,3 @@ export function csharpClasses(csFileAbs) {
|
|
|
617
365
|
};
|
|
618
366
|
}
|
|
619
367
|
|
|
620
|
-
/**
|
|
621
|
-
* BARE names of the TOP-LEVEL `class` declarations in a C# file — no namespace
|
|
622
|
-
* (the selectors are namespace-free by construction, see csharpClasses). Records
|
|
623
|
-
* are not test classes. Nested classes are left out. `[]` also when the file
|
|
624
|
-
* cannot be scanned safely.
|
|
625
|
-
*
|
|
626
|
-
* @param {string} csFileAbs
|
|
627
|
-
* @returns {string[]} unvalidated — narrowNode refuses names outside `[A-Za-z0-9_.]`
|
|
628
|
-
*/
|
|
629
|
-
export function classNamesOf(csFileAbs) {
|
|
630
|
-
const { names, scannable } = csharpClasses(csFileAbs);
|
|
631
|
-
return scannable ? names : [];
|
|
632
|
-
}
|
package/src/lib/mutation/plan.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { isInside } from '../tasks/test-runner.js';
|
|
4
|
-
import { isDotnetSource } from '
|
|
4
|
+
import { isDotnetSource } from './dotnet-introspection.js';
|
|
5
5
|
import { resolveRegions, validateManifest } from './manifest.js';
|
|
6
6
|
|
|
7
7
|
const JS_RUNNERS = new Set(['vitest', 'jest', 'node-test']);
|
|
@@ -14,23 +14,31 @@ export function loadPlan({ root, planPath }) {
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
function ownersOf(file, nodes, root) {
|
|
18
|
+
if (isDotnetSource(file)) return nodes.filter((n) => n.runner === 'dotnet');
|
|
18
19
|
const abs = resolve(root, file);
|
|
19
|
-
|
|
20
|
-
const owners = nodes
|
|
20
|
+
return nodes
|
|
21
21
|
.filter((n) => JS_RUNNERS.has(n.runner) && isInside(abs, resolve(root, n.cwd || '.')))
|
|
22
22
|
.sort((a, b) => resolve(root, b.cwd || '.').length - resolve(root, a.cwd || '.').length);
|
|
23
|
-
|
|
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;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export function groupByNode(regions, nodes, root) {
|
|
27
33
|
const groups = new Map();
|
|
28
34
|
const orphans = [];
|
|
35
|
+
const stackOnly = [];
|
|
29
36
|
for (const region of regions) {
|
|
30
37
|
const node = pickNode(region.file, nodes, root);
|
|
31
38
|
if (!node) { orphans.push(region); continue; }
|
|
39
|
+
if (node.e2e) { stackOnly.push({ region, node }); continue; }
|
|
32
40
|
if (!groups.has(node.id)) groups.set(node.id, { node, regions: [] });
|
|
33
41
|
groups.get(node.id).regions.push(region);
|
|
34
42
|
}
|
|
35
|
-
return { groups: [...groups.values()], orphans };
|
|
43
|
+
return { groups: [...groups.values()], orphans, stackOnly };
|
|
36
44
|
}
|
|
@@ -5,7 +5,7 @@ import { blame, parseCompilerErrors, patchFile, switchClassName, switchSource, S
|
|
|
5
5
|
import { md5Of, md5OfFile, openSnapshot, restoreSnapshot, trackGenerated } from './snapshot.js';
|
|
6
6
|
import { parseBuiltDlls } from './commands.js';
|
|
7
7
|
import { firstLines, methodOf } from './trx.js';
|
|
8
|
-
import { nearestProjectOf } from '
|
|
8
|
+
import { nearestProjectOf } from './dotnet-introspection.js';
|
|
9
9
|
|
|
10
10
|
const posix = (p) => p.replace(/\\/g, '/');
|
|
11
11
|
const MAX_SCHEMATA_ATTEMPTS = 3;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
3
|
-
import { csharpClasses } from '
|
|
3
|
+
import { csharpClasses } from './dotnet-introspection.js';
|
|
4
4
|
import { relatedCommand } from './commands.js';
|
|
5
5
|
|
|
6
6
|
const SKIP_DIRS = new Set(['bin', 'obj', 'node_modules', '.git', 'worktrees', 'TestResults']);
|
|
@@ -24,18 +24,40 @@ function walkCs(dir, out = []) {
|
|
|
24
24
|
|
|
25
25
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
26
26
|
|
|
27
|
+
export function mutatedTypesOf(root, files) {
|
|
28
|
+
const types = new Set();
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
for (const t of csharpClasses(resolve(root, file)).types) if (t.length >= MIN_TYPE_LENGTH) types.add(t);
|
|
31
|
+
}
|
|
32
|
+
return [...types];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const mentionOf = (types) => (types.length > 0 ? new RegExp(`\\b(?:${types.map(escapeRe).join('|')})\\b`) : null);
|
|
36
|
+
|
|
37
|
+
/** Test files (a test attribute in the code) under `testProjectDirs` naming one of `types` as a whole word. */
|
|
38
|
+
export function testFilesCiting({ root, testProjectDirs, types, exclude = [] }) {
|
|
39
|
+
const mention = mentionOf(types);
|
|
40
|
+
if (mention === null) return [];
|
|
41
|
+
const skip = new Set(exclude.map(posix));
|
|
42
|
+
const found = [];
|
|
43
|
+
for (const dir of testProjectDirs) {
|
|
44
|
+
for (const abs of walkCs(resolve(root, dir))) {
|
|
45
|
+
const rel = posix(relative(root, abs));
|
|
46
|
+
if (skip.has(rel)) continue;
|
|
47
|
+
const info = csharpClasses(abs);
|
|
48
|
+
if (info.scannable && info.hasTestAttribute && mention.test(readFileSync(abs, 'utf8'))) found.push(rel);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return found.sort();
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
/**
|
|
28
55
|
* The tests to run FIRST, chosen by the tool: files the feature changed, and files naming a
|
|
29
56
|
* type the mutated files declare. A mutant the subset does not kill goes to the whole suite.
|
|
30
57
|
*/
|
|
31
58
|
export function dotnetSubset({ root, testProjectDirs, mutatedFiles, changed = [] }) {
|
|
32
59
|
const changedSet = new Set(changed.map(posix));
|
|
33
|
-
const
|
|
34
|
-
for (const file of mutatedFiles) {
|
|
35
|
-
for (const t of csharpClasses(resolve(root, file)).types) if (t.length >= MIN_TYPE_LENGTH) mutatedTypes.add(t);
|
|
36
|
-
}
|
|
37
|
-
const mention = mutatedTypes.size > 0
|
|
38
|
-
? new RegExp(`\\b(?:${[...mutatedTypes].map(escapeRe).join('|')})\\b`) : null;
|
|
60
|
+
const mention = mentionOf(mutatedTypesOf(root, mutatedFiles));
|
|
39
61
|
|
|
40
62
|
const byChange = new Set();
|
|
41
63
|
const byMention = new Set();
|
|
@@ -70,6 +92,21 @@ export function jsSubset({ runner, command, root, mutatedFiles }) {
|
|
|
70
92
|
return { usable: true, command: cmd, files: { related: files } };
|
|
71
93
|
}
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
const SLN_PROJECT = /^Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/gim;
|
|
96
|
+
const SLNX_PROJECT = /<Project\b[^>]*\bPath="([^"]+\.csproj)"/gi;
|
|
97
|
+
|
|
98
|
+
function solutionProjects(solutionAbs) {
|
|
99
|
+
let text;
|
|
100
|
+
try { text = readFileSync(solutionAbs, 'utf8'); } catch { return []; }
|
|
101
|
+
const pattern = /\.slnx$/i.test(solutionAbs) ? SLNX_PROJECT : SLN_PROJECT;
|
|
102
|
+
return [...text.matchAll(pattern)].map((m) => resolve(dirname(solutionAbs), posix(m[1])));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A solution stands for every project it lists; one that lists none yields no directory. */
|
|
106
|
+
export function testProjectDirsOf(root, targets, cwd = root) {
|
|
107
|
+
const projects = targets.flatMap((t) => {
|
|
108
|
+
const abs = resolve(cwd, t);
|
|
109
|
+
return /\.slnx?$/i.test(t) ? solutionProjects(abs) : [abs];
|
|
110
|
+
});
|
|
111
|
+
return [...new Set(projects.map((p) => posix(relative(root, dirname(p)))))];
|
|
75
112
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** The inline justification that lets a criterion talk about an artifact on purpose. */
|
|
2
|
+
export const ARTIFACT_MARKER_RE = /por\s+que\s+artefato\s*:/i;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Case-insensitive spelling of `word` inside a regex that must stay
|
|
6
|
+
* case-SENSITIVE elsewhere — Node 18 has no inline `(?i:…)` modifier.
|
|
7
|
+
*/
|
|
8
|
+
const anyCase = (word) => [...word].map((c) => {
|
|
9
|
+
const lo = c.toLowerCase();
|
|
10
|
+
const up = c.toUpperCase();
|
|
11
|
+
return lo === up ? c : `[${lo}${up}]`;
|
|
12
|
+
}).join('');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What follows "arquivo em" in a behavioural export criterion ("exporta o
|
|
16
|
+
* arquivo em CSV", "salva o arquivo em formato aberto"): an uppercase file
|
|
17
|
+
* format acronym or the word `formato`. Case-sensitive on purpose — `src`,
|
|
18
|
+
* `docs` or `lib` after "arquivo em" are places, and places are artifacts.
|
|
19
|
+
*/
|
|
20
|
+
const FORMAT_TOKEN = String.raw`\s+(?:[A-Z]{2,5}(?![\p{L}\d_])|${anyCase('formato')}(?![\p{L}]))`;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Vocabulary of artifact-shaped phrasing. `term` is the label surfaced in
|
|
24
|
+
* warnings and cited by the `morph-plan` skill prose — keep it stable, a
|
|
25
|
+
* parity test imports it verbatim.
|
|
26
|
+
*
|
|
27
|
+
* Unicode lookarounds, never `\b`: in JS, `\b` does not treat accented
|
|
28
|
+
* letters (é, á, ó…) as word characters, so it would false-boundary inside
|
|
29
|
+
* "não" or "binário".
|
|
30
|
+
*/
|
|
31
|
+
export const ARTIFACT_VOCABULARY = [
|
|
32
|
+
{ term: 'grep', re: /(?<![\p{L}\d_])grep(?![\p{L}\d_])/iu },
|
|
33
|
+
{ term: 'contém a string', re: /cont[ée]m\s+as?\s+strings?/iu },
|
|
34
|
+
{ term: 'o binário', re: /(?<![\p{L}])o\s+bin[áa]rio(?![\p{L}])/iu },
|
|
35
|
+
{ term: 'nenhum arquivo', re: /(?<![\p{L}])nenhum\s+arquivo(?![\p{L}])/iu },
|
|
36
|
+
{
|
|
37
|
+
term: 'arquivo/pasta/diretório em',
|
|
38
|
+
re: new RegExp(
|
|
39
|
+
String.raw`(?<![\p{L}])(?:${anyCase('arquivo')}|${anyCase('pasta')}|${anyCase('diret')}[óoÓO]${anyCase('rio')})` +
|
|
40
|
+
String.raw`[sS]?\s+${anyCase('em')}(?![\p{L}])(?!${FORMAT_TOKEN})`,
|
|
41
|
+
'u',
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
{ term: 'glob **/', re: /\*\*\/|\/\*\*/ },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Vocabulary terms matched in `text`, in vocabulary order, each at most once.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @returns {string[]}
|
|
52
|
+
*/
|
|
53
|
+
export function findArtifactTerms(text) {
|
|
54
|
+
const s = String(text ?? '');
|
|
55
|
+
const found = [];
|
|
56
|
+
for (const { term, re } of ARTIFACT_VOCABULARY) {
|
|
57
|
+
if (re.test(s)) found.push(term);
|
|
58
|
+
}
|
|
59
|
+
return found;
|
|
60
|
+
}
|