@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/rules/nextjs-standards.md +1 -1
- 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 +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +10 -4
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- 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/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- 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/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- 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/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,67 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* templates-reconcile.js — reconcile a pre-existing `.morph/framework/templates/`
|
|
3
|
-
* copy against the project's prior recorded morph-spec version.
|
|
4
|
-
*
|
|
5
|
-
* Shared by BOTH install paths that can encounter this directory:
|
|
6
|
-
* - `morph-spec update` (src/commands/project/update.js) — always knows the
|
|
7
|
-
* project's prior version (`checkProjectOutdated().current`, read from
|
|
8
|
-
* `.morph/.morphversion` BEFORE this run overwrites it).
|
|
9
|
-
* - `morph-spec init`/`setup-infra` (src/scripts/setup-infra.js) — the SAME
|
|
10
|
-
* source (`getProjectMorphVersion()`, the function `checkProjectOutdated`
|
|
11
|
-
* itself calls), read at the same point in the run (before its own write
|
|
12
|
-
* of `.morph/.morphversion`), when it's available — e.g. a repair run
|
|
13
|
-
* over an already-initialized project. Falls back to a plain warning when
|
|
14
|
-
* it isn't (a project with no recorded version has nothing here for this
|
|
15
|
-
* function to reconcile against safely).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
1
|
import { existsSync } from 'fs';
|
|
19
2
|
import { compareVersions } from '../../utils/version-checker.js';
|
|
20
3
|
import { realEffects } from './effects.js';
|
|
21
4
|
|
|
22
|
-
|
|
23
|
-
* Last CLI version whose `update`/`setup-infra` still wrote
|
|
24
|
-
* `.morph/framework/templates/` unconditionally, before Task 2 of the R3
|
|
25
|
-
* perf plan removed that copy. Used as the decision boundary in
|
|
26
|
-
* `reconcileLegacyTemplatesCopy` below — see its doc comment for why a
|
|
27
|
-
* version check is used instead of diffing file content.
|
|
28
|
-
*/
|
|
5
|
+
// Last CLI version whose install still copied `.morph/framework/templates/` unconditionally.
|
|
29
6
|
export const LAST_VERSION_THAT_COPIED_TEMPLATES = '8.17.0';
|
|
30
7
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* plan — resolution falls back to the npm package's own framework/templates/,
|
|
35
|
-
* see template-registry.js / architecture-scanner.js's layered resolver).
|
|
36
|
-
*
|
|
37
|
-
* Decided from the project's PRIOR recorded CLI version (before THIS run
|
|
38
|
-
* writes a new one), not by diffing content against the package. A content
|
|
39
|
-
* diff can't tell "stale copy from an older CLI version" apart from
|
|
40
|
-
* "deliberate user override" — and staleness is the dominant case for
|
|
41
|
-
* anyone who actually runs `update`/`setup-infra` again, since template
|
|
42
|
-
* content does change between releases. The version boundary is unambiguous
|
|
43
|
-
* instead:
|
|
44
|
-
*
|
|
45
|
-
* - No local copy → nothing to do.
|
|
46
|
-
* - Prior version is unrecorded, OR <= LAST_VERSION_THAT_COPIED_TEMPLATES
|
|
47
|
-
* → the copy could ONLY have been put there by the OLD installer:
|
|
48
|
-
* `.morph/framework/**` is Edit-denied to the agent (MORPH_DENY in
|
|
49
|
-
* hooks-installer.js — "installer-managed, immutable at runtime"),
|
|
50
|
-
* the directory is gitignored, and every install/update before this fix
|
|
51
|
-
* unconditionally wiped + recopied it. There is no path by which a
|
|
52
|
-
* project on an old version could have a genuine override sitting
|
|
53
|
-
* here. Safe to delete unconditionally — it is dead weight, not data.
|
|
54
|
-
* - Prior version > LAST_VERSION_THAT_COPIED_TEMPLATES → this install path
|
|
55
|
-
* has never written this directory for this project. Anything found
|
|
56
|
-
* here was placed deliberately (by the user or other tooling) — this is
|
|
57
|
-
* the forward-looking override use case the layered resolver exists
|
|
58
|
-
* for. Preserve untouched, unconditionally, with no diffing.
|
|
59
|
-
*
|
|
60
|
-
* @param {string} templatesDest - .morph/framework/templates (may not exist)
|
|
61
|
-
* @param {string|null} priorProjectVersion - project's recorded morph-spec
|
|
62
|
-
* version BEFORE this run (e.g. `checkProjectOutdated().current`)
|
|
63
|
-
* @returns {'absent'|'removed-legacy'|'preserved-override'}
|
|
64
|
-
*/
|
|
8
|
+
// Decided by the PRIOR recorded version, not by diffing: a diff can't tell a stale copy from an override.
|
|
9
|
+
// Unrecorded or <= the boundary → only the old installer could have written it, delete.
|
|
10
|
+
// Newer → placed deliberately, preserve.
|
|
65
11
|
export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion, fx = realEffects()) {
|
|
66
12
|
if (!existsSync(templatesDest)) return 'absent';
|
|
67
13
|
|
|
@@ -74,8 +20,7 @@ export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion,
|
|
|
74
20
|
fx.remove(templatesDest);
|
|
75
21
|
return 'removed-legacy';
|
|
76
22
|
} catch {
|
|
77
|
-
// Couldn't remove
|
|
78
|
-
// the layered resolver's override-first check pick it up as before.
|
|
23
|
+
// Couldn't remove: leave it; the resolver's override-first check picks it up.
|
|
79
24
|
return 'preserved-override';
|
|
80
25
|
}
|
|
81
26
|
}
|
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { dotnetBuildCommands, withFilter, withTrx, withoutBuild } from './commands.js';
|
|
5
|
-
import {
|
|
5
|
+
import { parseTrx } from './trx.js';
|
|
6
6
|
import { stripAnsi } from '../tasks/test-runner.js';
|
|
7
7
|
|
|
8
8
|
const MAX_CAPTURE = 16 * 1024 * 1024;
|
|
@@ -39,14 +39,10 @@ export function dotnetAdapter({ node, root, workDir, exec, subset, timeoutMs })
|
|
|
39
39
|
const trxDir = join(workDir, 'trx');
|
|
40
40
|
mkdirSync(trxDir, { recursive: true });
|
|
41
41
|
const base = withoutBuild(node.command);
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
scope === 'full' ? base : withFilter(base, filterOf[scope](filter)),
|
|
45
|
-
{ dir: trxDir, name: `${label}.trx` },
|
|
46
|
-
);
|
|
47
|
-
const runTests = async ({ scope, label, env, filter }) => {
|
|
42
|
+
const commandFor = (label) => withTrx(withFilter(base, subset.filter), { dir: trxDir, name: `${label}.trx` });
|
|
43
|
+
const runTests = async ({ label, env }) => {
|
|
48
44
|
const trxPath = join(trxDir, `${label}.trx`);
|
|
49
|
-
const r = await exec(commandFor(
|
|
45
|
+
const r = await exec(commandFor(label), { cwd, env, timeoutMs, label });
|
|
50
46
|
if (!existsSync(trxPath)) {
|
|
51
47
|
return { outcome: 'crash', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs, note: r.timedOut ? `timeout de ${timeoutMs} ms` : `sem TRX (exit ${r.status})` };
|
|
52
48
|
}
|
|
@@ -62,12 +58,6 @@ export function dotnetAdapter({ node, root, workDir, exec, subset, timeoutMs })
|
|
|
62
58
|
buildCommands: dotnetBuildCommands(node, root),
|
|
63
59
|
cwd,
|
|
64
60
|
runTests,
|
|
65
|
-
reproduce: ({ names, label, env }) => runTests({
|
|
66
|
-
scope: 'names',
|
|
67
|
-
label,
|
|
68
|
-
env,
|
|
69
|
-
filter: [...new Set(names.map(methodOf))].map((n) => `FullyQualifiedName~${n}`).join('|'),
|
|
70
|
-
}),
|
|
71
61
|
};
|
|
72
62
|
}
|
|
73
63
|
|
|
@@ -78,9 +68,8 @@ export function genericAdapter({ node, root, exec, subset, timeoutMs }) {
|
|
|
78
68
|
hasSubset: Boolean(subset?.usable),
|
|
79
69
|
buildCommands: [],
|
|
80
70
|
cwd,
|
|
81
|
-
async runTests({
|
|
82
|
-
const
|
|
83
|
-
const r = await exec(command, { cwd, env, timeoutMs, label });
|
|
71
|
+
async runTests({ label, env }) {
|
|
72
|
+
const r = await exec(subset.command, { cwd, env, timeoutMs, label });
|
|
84
73
|
if (r.timedOut) return { outcome: 'crash', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs, note: `timeout de ${timeoutMs} ms` };
|
|
85
74
|
if (r.status === 0) return { outcome: 'green', executed: null, total: null, failed: 0, failedTests: [], durationMs: r.durationMs };
|
|
86
75
|
const failedTests = genericFailures(`${r.stdout}\n${r.stderr}`);
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
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).
|
|
5
|
-
*
|
|
6
|
-
* Pure: reads files, never spawns a process.
|
|
7
|
-
*/
|
|
8
1
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
9
2
|
import { dirname, join, resolve } from 'path';
|
|
10
3
|
|
|
@@ -13,54 +6,21 @@ import { jsoncToJson } from '../../../framework/hooks/shared/comment-stripper.js
|
|
|
13
6
|
|
|
14
7
|
const MTP_RUNNER = 'Microsoft.Testing.Platform';
|
|
15
8
|
const TYPE_HEADER = /\b(?:class|struct|record|interface)\b/;
|
|
16
|
-
|
|
9
|
+
// `@` = verbatim identifier, `\` = unicode escape.
|
|
17
10
|
const NAME_START = '[\\p{L}\\p{Nl}_@\\\\]';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* round 5 showed that any exclusion decided by what precedes the keyword (the
|
|
22
|
-
* `:` of a generic constraint, a `where T :` shape) can be forged by a comment in
|
|
23
|
-
* the raw text or by string content a cut exposes in the code. `record class`/
|
|
24
|
-
* `record struct` count once. What follows the keyword, after optional whitespace,
|
|
25
|
-
* is a name start (NAME_START), a comment or preprocessor line standing between
|
|
26
|
-
* the keyword and its name (`class/**\/BTests` is legal), OR a quote — a quote is
|
|
27
|
-
* what the RAW text shows where the blanked code shows the spaces of a string the
|
|
28
|
-
* keyword is followed by (`"; class " : " Fake"` exposed as code): every keyword
|
|
29
|
-
* the code counts is a keyword the raw text counts, by offset.
|
|
30
|
-
* A constraint keyword is therefore counted too whenever a name-shaped token
|
|
31
|
-
* follows it (`where T : class where U : …`, `: class // note`): it is not named,
|
|
32
|
-
* so the file is unscannable. `class,` / `class\n{` / `class?` are not counted at all.
|
|
33
|
-
*/
|
|
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.
|
|
34
14
|
const TYPE_KEYWORD = new RegExp(`\\b(?:record\\s+(?:class|struct)|class|record|struct|interface)\\b(?=\\s*(?:${NAME_START}|["']|\\/[/*]|#))`, 'gu');
|
|
35
|
-
/** A type declaration the scanner can NAME: after `; { } ]`, a newline or the file start; attributes; modifiers; a NAME_START name. */
|
|
36
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');
|
|
37
16
|
const IDENT = '[\\p{L}_][\\p{L}\\p{N}_]*';
|
|
38
17
|
const QUALIFIER = `(?:${IDENT}\\s*(?:\\.|::)\\s*)*`;
|
|
39
|
-
/** An attribute's optional arguments: one nested level of parentheses, no `]`. */
|
|
40
18
|
const ATTRIBUTE_ARGS = '(?:\\((?:[^()\\]]|\\([^()\\]]*\\))*\\))?';
|
|
41
|
-
/** Any other attribute of the same section: a name, optionally with arguments. */
|
|
42
19
|
const OTHER_ATTRIBUTE = `${QUALIFIER}${IDENT}\\s*${ATTRIBUTE_ARGS}`;
|
|
43
20
|
const TEST_ATTRIBUTE_NAME = '(?:Fact|Theory|Test|TestMethod|DataTestMethod|TestCase|TestCaseSource)(?:Attribute)?';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
* `TestCaseSource`, optionally `…Attribute` and qualified (`Xunit.Fact`) — inside an
|
|
48
|
-
* attribute section that CLOSES as one: `[` where a declaration may start (line start,
|
|
49
|
-
* after `;` `{` `}` `]` or a `*\/`), an optional target, other attributes before it
|
|
50
|
-
* separated by `,`, the test name with its optional arguments, any further attributes,
|
|
51
|
-
* then the `]` — followed by a declaration start (`[`, a name, a comment, a directive)
|
|
52
|
-
* or the end of the input.
|
|
53
|
-
*
|
|
54
|
-
* A false POSITIVE is the unsafe direction here: a helper taken for a test file
|
|
55
|
-
* narrows the subset to its own name, and the tests that USE the helper never run. The final review of branch B found
|
|
56
|
-
* exactly that with the old prefix match (`, TestContext` / `, Factory` / `, TheoryData`
|
|
57
|
-
* after any `,`), and its re-review (N1) with a test name merely FOLLOWED by `(` or `,`:
|
|
58
|
-
* a C# 12 collection laid out with `[` alone on its line (`[\n Env.Dev,\n Env.Test,\n];`,
|
|
59
|
-
* a row `[Env.Test, Env.Dev],`, `[\n Test("a"),\n];`). Requiring the section to close
|
|
60
|
-
* rejects all three; what still matches is stated in csharpClasses. A false NEGATIVE
|
|
61
|
-
* only leaves a test file out of a selection (a custom attribute such as `[SkippableFact]`, a test
|
|
62
|
-
* attribute after an argument holding `]`).
|
|
63
|
-
*/
|
|
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.
|
|
64
24
|
export const CS_TEST_ATTRIBUTE = new RegExp(
|
|
65
25
|
'(?:^|[;{}\\]]|\\*\\/)[ \\t]*\\['
|
|
66
26
|
+ `\\s*(?:${IDENT}\\s*:(?!:)\\s*)?`
|
|
@@ -71,22 +31,12 @@ export const CS_TEST_ATTRIBUTE = new RegExp(
|
|
|
71
31
|
'mu',
|
|
72
32
|
);
|
|
73
33
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
* one) and every C# new-line character mapped 1:1 to `\n` — CR, NEL (U+0085),
|
|
77
|
-
* LS (U+2028), PS (U+2029): JS `\s` excludes NEL, so `class<NEL>Name` was
|
|
78
|
-
* invisible to both regexes (compiled and discovered by xunit); a CR-only file had
|
|
79
|
-
* no line ends at all. U+001A (Ctrl-Z) is mapped 1:1 to a space: Roslyn reads it
|
|
80
|
-
* as whitespace and JS `\s` does not, so `class<SUB>Name` hid a declaration the
|
|
81
|
-
* same way (B3 carry-over: compiled, green while skipping the class). Length is kept (CRLF becomes `\n\n`). null when unreadable.
|
|
82
|
-
*/
|
|
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.
|
|
83
36
|
const readText = (path) => {
|
|
84
37
|
try { return readFileSync(path, 'utf8').replace(/\x1A/g, ' ').replace(/^\uFEFF/, '').replace(/[\r\u0085\u2028\u2029]/g, '\n'); } catch { return null; }
|
|
85
38
|
};
|
|
86
39
|
|
|
87
|
-
/** Which test framework a csproj references, by package (or MSBuild SDK) name. */
|
|
88
|
-
|
|
89
|
-
/** Nearest file matching `projectName` walking up from a file's directory, inside the root; null when none. */
|
|
90
40
|
function nearestProjectFile(fileAbs, rootAbs, projectName) {
|
|
91
41
|
for (let dir = dirname(fileAbs); isInside(dir, rootAbs); dir = dirname(dir)) {
|
|
92
42
|
let names = [];
|
|
@@ -97,32 +47,20 @@ function nearestProjectFile(fileAbs, rootAbs, projectName) {
|
|
|
97
47
|
return null;
|
|
98
48
|
}
|
|
99
49
|
|
|
100
|
-
/**
|
|
101
|
-
* The project — `.csproj`, `.fsproj` or `.vbproj` — nearest to ANY file walking
|
|
102
|
-
* up, or null inside the root. Asked about the .NET sources the C# scan cannot
|
|
103
|
-
* read (a bUnit `.razor`, an F# `.fs`).
|
|
104
|
-
*/
|
|
105
50
|
export function nearestProjectOf(fileAbs, rootAbs) {
|
|
106
51
|
return nearestProjectFile(fileAbs, rootAbs, /\.(?:cs|fs|vb)proj$/i);
|
|
107
52
|
}
|
|
108
53
|
|
|
109
|
-
/** .NET sources the C# scan cannot read: Razor (`.razor`, `.cshtml`), F# (`.fs`, `.fsi`, `.fsx`), VB (`.vb`). */
|
|
110
54
|
const NON_CSHARP_SOURCE = /\.(?:razor|cshtml|fs|fsi|fsx|vb)$/i;
|
|
111
55
|
|
|
112
|
-
/**
|
|
56
|
+
/** No type scan, hence no class selector, for these. */
|
|
113
57
|
export const isNonCSharpSource = (path) => NON_CSHARP_SOURCE.test(String(path));
|
|
114
58
|
|
|
115
|
-
/** Any .NET source a project compiles — C# or one of the others. */
|
|
116
59
|
export const isDotnetSource = (path) => /\.cs$/i.test(String(path)) || isNonCSharpSource(path);
|
|
117
60
|
|
|
118
61
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* @param {string} absCwd
|
|
123
|
-
* @returns {'mtp'|'vstest'|null} `mtp` when `test.runner` is Microsoft.Testing.Platform;
|
|
124
|
-
* `vstest` when readable without it (or absent: the SDK default); null when a
|
|
125
|
-
* global.json exists but cannot be read — never a guess.
|
|
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.
|
|
126
64
|
*/
|
|
127
65
|
export function dotnetTestMode(absCwd) {
|
|
128
66
|
for (let dir = resolve(absCwd); ; dir = dirname(dir)) {
|
|
@@ -141,7 +79,6 @@ export function dotnetTestMode(absCwd) {
|
|
|
141
79
|
|
|
142
80
|
const lineEnd = (text, i) => { const e = text.indexOf('\n', i); return e < 0 ? text.length : e; };
|
|
143
81
|
|
|
144
|
-
/** End (exclusive) of a regular string or char literal opened at `start`; -1 if the line ends first. */
|
|
145
82
|
function closeOnLine(text, start, quote) {
|
|
146
83
|
for (let j = start + 1; j < text.length && text[j] !== '\n'; j++) {
|
|
147
84
|
if (text[j] === '\\') j++;
|
|
@@ -150,11 +87,8 @@ function closeOnLine(text, start, quote) {
|
|
|
150
87
|
return -1;
|
|
151
88
|
}
|
|
152
89
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
* (`$"{d["k"]}"`) ends the literal as the scanner reads it, so what it took for
|
|
156
|
-
* the literal stops with the hole still open. `{{`/`}}` outside a hole are escapes.
|
|
157
|
-
*/
|
|
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.
|
|
158
92
|
function holeLeftOpen(content) {
|
|
159
93
|
let depth = 0;
|
|
160
94
|
for (let k = 0; k < content.length; k++) {
|
|
@@ -167,26 +101,9 @@ function holeLeftOpen(content) {
|
|
|
167
101
|
}
|
|
168
102
|
|
|
169
103
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* - an interpolated `$"…"`/`$@"…"`/`@$"…"` literal is cut by a quote inside a hole
|
|
174
|
-
* (its content ends with the hole open). Blanking the part before the cut would
|
|
175
|
-
* leave the rest of the hole as "code", with braces that can cancel out.
|
|
176
|
-
*
|
|
177
|
-
* Raw strings (`"""…"""`, 3+ quotes, with or without `$`/`$$`) end at the same run
|
|
178
|
-
* of quotes; a quote inside a raw interpolation hole is NOT checked. Verbatim `@"…"`
|
|
179
|
-
* ends at a lone `"`. Regular and char literals cannot cross a line.
|
|
180
|
-
*
|
|
181
|
-
* This is a heuristic lexer and it CAN misread (known: `holeLeftOpen` counts the
|
|
182
|
-
* braces of a char literal such as `'}'`, so `$"{(c == '}' ? "a" : "/*")}"` is cut
|
|
183
|
-
* at the inner quote and `"/*"` becomes code). Nothing relies on the
|
|
184
|
-
* blanking being right: csharpClasses compares the declarations it names against
|
|
185
|
-
* the type keywords of the RAW text, and any declaration a misread hides makes the
|
|
186
|
-
* file unscannable.
|
|
187
|
-
*
|
|
188
|
-
* @param {string} text
|
|
189
|
-
* @returns {string|null}
|
|
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).
|
|
190
107
|
*/
|
|
191
108
|
export function blankCSharpLiterals(text) {
|
|
192
109
|
const out = text.split('');
|
|
@@ -232,7 +149,6 @@ export function blankCSharpLiterals(text) {
|
|
|
232
149
|
return out.join('');
|
|
233
150
|
}
|
|
234
151
|
|
|
235
|
-
/** Type-keyword tokens in a text (raw or blanked) — every one; no context rule (see TYPE_KEYWORD). */
|
|
236
152
|
const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
|
|
237
153
|
|
|
238
154
|
/**
|
|
@@ -285,7 +201,7 @@ const typeKeywordCount = (text) => [...text.matchAll(TYPE_KEYWORD)].length;
|
|
|
285
201
|
* or a custom one (`[SkippableFact]`) is not seen: the file counts as a helper and
|
|
286
202
|
* it is left out of a selection;
|
|
287
203
|
* - `hasTestAttribute` is still a heuristic, and ITS false positive is the unsafe
|
|
288
|
-
* side.
|
|
204
|
+
* side. The section must close; what still matches in a
|
|
289
205
|
* helper — probed — is a LIST PATTERN opening a line (or after `;` `{` `}` `]`)
|
|
290
206
|
* whose element is named exactly `Test`/`Fact`/… (qualified or not) and whose `]`
|
|
291
207
|
* is followed by a combinator: `[Env.Test] or [Env.Dev] => true`. Such a helper is
|
|
@@ -317,10 +233,8 @@ export function csharpClasses(csFileAbs) {
|
|
|
317
233
|
// length, so the raw text at the same offsets shows the directive.
|
|
318
234
|
const split = decls.find((d) => /(^|\n)[ \t]*#/.test(text.slice(d.at - d.gap, d.at)));
|
|
319
235
|
if (split) return unscannable(`a preprocessor directive stands between a type keyword and the name "${split.name}"`);
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
// blanked code — against what was actually named (see the JSDoc for why
|
|
323
|
-
// equality of counts is equality of sets).
|
|
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).
|
|
324
238
|
const rawKeywords = typeKeywordCount(text);
|
|
325
239
|
if (rawKeywords !== decls.length) {
|
|
326
240
|
return unscannable(`${rawKeywords} type keyword(s) in the file but ${decls.length} declaration(s) recognised`);
|
|
@@ -4,12 +4,13 @@ import { applyRegions, diffOf } from './manifest.js';
|
|
|
4
4
|
import { blame, parseCompilerErrors, patchFile, switchClassName, switchSource, SWITCH_ENV } from './schemata.js';
|
|
5
5
|
import { md5Of, md5OfFile, openSnapshot, restoreSnapshot, trackGenerated } from './snapshot.js';
|
|
6
6
|
import { parseBuiltDlls } from './commands.js';
|
|
7
|
-
import { firstLines
|
|
7
|
+
import { firstLines } from './trx.js';
|
|
8
8
|
import { nearestProjectOf } from './dotnet-introspection.js';
|
|
9
9
|
|
|
10
10
|
const posix = (p) => p.replace(/\\/g, '/');
|
|
11
11
|
const MAX_SCHEMATA_ATTEMPTS = 3;
|
|
12
|
-
|
|
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`;
|
|
13
14
|
|
|
14
15
|
const envWithout = (env) => { const { [SWITCH_ENV]: _drop, ...rest } = env; return rest; };
|
|
15
16
|
const failedNames = (result) => new Set(result.failedTests.map((t) => t.name));
|
|
@@ -123,29 +124,15 @@ export async function runMutation(input) {
|
|
|
123
124
|
.map((name) => [name, md5OfFile(dlls.get(name))]),
|
|
124
125
|
);
|
|
125
126
|
|
|
126
|
-
const run = (
|
|
127
|
-
|
|
128
|
-
const confirmKill = async (id, verdict, env) => {
|
|
129
|
-
if (!adapter.reproduce) return verdict;
|
|
130
|
-
const names = [...new Set(verdict.failures.map((f) => f.name))].slice(0, MAX_REPRODUCE);
|
|
131
|
-
const again = await timed(`${id}-reproduce`, () => adapter.reproduce({ names, label: `${id}-reproduce`, env }));
|
|
132
|
-
if (again.outcome === 'crash' || !again.executed) {
|
|
133
|
-
return { state: 'inconclusive', reason: `a falha de ${names[0]} não pôde ser confirmada na repetição (${again.note ?? 'nenhum teste executado'})` };
|
|
134
|
-
}
|
|
135
|
-
const wanted = new Set(names.map(methodOf));
|
|
136
|
-
const held = again.failedTests.filter((t) => wanted.has(methodOf(t.name)));
|
|
137
|
-
if (held.length > 0) return { ...verdict, reason: reasonOf(held) };
|
|
138
|
-
const whole = await timed(`${id}-reproduce-full`, () => adapter.runTests({ scope: 'full', label: `${id}-reproduce-full`, env }));
|
|
139
|
-
if (whole.outcome === 'crash' || !whole.executed) {
|
|
140
|
-
return { state: 'inconclusive', reason: `a falha de ${names[0]} não pôde ser confirmada na repetição (${whole.note ?? 'nenhum teste executado'})` };
|
|
141
|
-
}
|
|
142
|
-
const heldWhole = whole.failedTests.filter((t) => wanted.has(methodOf(t.name)));
|
|
143
|
-
if (heldWhole.length > 0) return { ...verdict, reason: reasonOf(heldWhole) };
|
|
144
|
-
const which = names.length === 1 ? names[0] : `${names.length} testes`;
|
|
145
|
-
return { state: 'survived', reason: `nenhum teste falhou de forma reproduzível: ${which} falhou uma vez e passou isolado e na suíte inteira repetida (instável, não prova o mutante)` };
|
|
146
|
-
};
|
|
127
|
+
const run = (label, env) => timed(label, () => adapter.runTests({ label, env }));
|
|
147
128
|
|
|
148
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
|
+
};
|
|
149
136
|
const applyOnDisk = (id) => {
|
|
150
137
|
const region = byId.get(id);
|
|
151
138
|
writeFileSync(join(root, region.file), applyRegions(originals.get(region.file), [region], (x) => x.replaceText).text);
|
|
@@ -154,10 +141,8 @@ export async function runMutation(input) {
|
|
|
154
141
|
let pristine = new Map();
|
|
155
142
|
let binary = { proof: 'unavailable', projects: [] };
|
|
156
143
|
let baselineSubset = null;
|
|
157
|
-
let baselineFull = null;
|
|
158
144
|
let subsetLive = adapter.hasSubset;
|
|
159
145
|
let subsetNote = null;
|
|
160
|
-
const needFullBaseline = [];
|
|
161
146
|
let restoreReport = [];
|
|
162
147
|
|
|
163
148
|
try {
|
|
@@ -238,40 +223,26 @@ export async function runMutation(input) {
|
|
|
238
223
|
if (subsetLive && (schemataIds.length > 0 || hasRebuild)) {
|
|
239
224
|
const copies = schemataIds.length > 0 ? jobs : 1;
|
|
240
225
|
baselineSubset = mergeBaselines(await Promise.all(Array.from({ length: copies }, (_, i) => (
|
|
241
|
-
run(
|
|
226
|
+
run(i === 0 ? 'baseline-subset' : `baseline-subset-${i + 1}`, envWithout(process.env))))));
|
|
242
227
|
if (baselineSubset.outcome === 'crash' || baselineSubset.executed === 0) {
|
|
243
228
|
subsetLive = false;
|
|
244
229
|
subsetNote = baselineSubset.executed === 0 ? 'o filtro do subconjunto não selecionou teste algum' : (baselineSubset.note ?? 'a rodada-base do subconjunto não terminou');
|
|
245
230
|
}
|
|
246
231
|
}
|
|
247
|
-
if (!subsetLive && schemataIds.length > 0) baselineFull = await run('full', 'baseline-full', envWithout(process.env));
|
|
248
|
-
|
|
249
|
-
const scopeOf = subsetLive ? 'subset' : 'full';
|
|
250
|
-
const scopeBaseline = () => (subsetLive ? baselineSubset : baselineFull);
|
|
251
232
|
const routed = [];
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
const base = { scope: scopeOf, executed: result.executed, durationMs: result.durationMs };
|
|
263
|
-
if (verdict.state === 'killed') {
|
|
264
|
-
record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason });
|
|
265
|
-
} else if (scopeOf === 'subset' && verdict.state === 'survived') {
|
|
266
|
-
record(id, { ...base, verdict: 'survived', reason: '' });
|
|
267
|
-
} else {
|
|
268
|
-
record(id, { ...base, verdict: null, reason: verdict.reason ?? '' });
|
|
269
|
-
routed.push({ id, needs: 'full' });
|
|
270
|
-
}
|
|
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) });
|
|
271
243
|
}
|
|
272
|
-
for (const r of results.values()) if (r.mode === 'rebuild') routed.push({ id: r.id, needs: subsetLive ? 'subset' : 'full' });
|
|
273
244
|
|
|
274
|
-
for (const
|
|
245
|
+
for (const id of routed) {
|
|
275
246
|
applyOnDisk(id);
|
|
276
247
|
try {
|
|
277
248
|
if (adapter.buildCommands.length > 0) {
|
|
@@ -282,21 +253,7 @@ export async function runMutation(input) {
|
|
|
282
253
|
continue;
|
|
283
254
|
}
|
|
284
255
|
}
|
|
285
|
-
|
|
286
|
-
const sub = await run('subset', `${id}-subset`, envWithout(process.env));
|
|
287
|
-
const verdict = judge(sub, baselineSubset);
|
|
288
|
-
const base = { scope: 'subset', executed: sub.executed, durationMs: sub.durationMs };
|
|
289
|
-
if (verdict.state === 'killed') record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason });
|
|
290
|
-
else record(id, { ...base, verdict: verdict.state, reason: verdict.reason ?? '' });
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
const full = await run('full', `${id}-full`, envWithout(process.env));
|
|
294
|
-
const first = judge(full, baselineFull);
|
|
295
|
-
const verdict = first.state === 'killed' ? await confirmKill(id, first, envWithout(process.env)) : first;
|
|
296
|
-
const base = { scope: 'full', executed: full.executed, durationMs: full.durationMs };
|
|
297
|
-
if (verdict.state === 'inconclusive') record(id, { ...base, verdict: 'inconclusive', reason: verdict.reason });
|
|
298
|
-
else if (verdict.state === 'survived') record(id, { ...base, verdict: 'survived', reason: verdict.reason ?? '' });
|
|
299
|
-
else { record(id, { ...base, verdict: 'killed', killers: verdict.failures.length, reason: verdict.reason }); if (!baselineFull) needFullBaseline.push({ id, full }); }
|
|
256
|
+
recordSubset(id, await run(`${id}-subset`, envWithout(process.env)));
|
|
300
257
|
} finally {
|
|
301
258
|
restoreSnapshot(snapshot);
|
|
302
259
|
}
|
|
@@ -310,14 +267,6 @@ export async function runMutation(input) {
|
|
|
310
267
|
binary = proveBinary({ pristine, dlls: last.dlls, restoredAt, hashes: dllHashes(last.dlls) });
|
|
311
268
|
}
|
|
312
269
|
|
|
313
|
-
if (needFullBaseline.length > 0) {
|
|
314
|
-
baselineFull = await run('full', 'baseline-full', envWithout(process.env));
|
|
315
|
-
for (const { id, full } of needFullBaseline) {
|
|
316
|
-
const verdict = judge(full, baselineFull);
|
|
317
|
-
if (verdict.state === 'killed') record(id, { killers: verdict.failures.length, reason: verdict.reason });
|
|
318
|
-
else record(id, { verdict: 'survived', killers: 0, reason: '' });
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
270
|
} finally {
|
|
322
271
|
restoreReport = restoreSnapshot(snapshot, { rewrite: false });
|
|
323
272
|
gate?.release();
|
|
@@ -338,7 +287,6 @@ export async function runMutation(input) {
|
|
|
338
287
|
mutants,
|
|
339
288
|
baseline: {
|
|
340
289
|
subset: baselineSubset ? { executed: baselineSubset.executed, failed: baselineSubset.failed } : null,
|
|
341
|
-
full: baselineFull ? { executed: baselineFull.executed, failed: baselineFull.failed } : null,
|
|
342
290
|
},
|
|
343
291
|
binary,
|
|
344
292
|
restore: {
|
|
@@ -51,10 +51,7 @@ export function testFilesCiting({ root, testProjectDirs, types, exclude = [] })
|
|
|
51
51
|
return found.sort();
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
* The tests to run FIRST, chosen by the tool: files the feature changed, and files naming a
|
|
56
|
-
* type the mutated files declare. A mutant the subset does not kill goes to the whole suite.
|
|
57
|
-
*/
|
|
54
|
+
/** Chosen by the tool, never the verifier: test files the feature changed or that name a mutated type. */
|
|
58
55
|
export function dotnetSubset({ root, testProjectDirs, mutatedFiles, changed = [] }) {
|
|
59
56
|
const changedSet = new Set(changed.map(posix));
|
|
60
57
|
const mention = mentionOf(mutatedTypesOf(root, mutatedFiles));
|
package/src/lib/mutation/trx.js
CHANGED
|
@@ -43,7 +43,6 @@ export function parseTrx(xml) {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export const methodOf = (testName) => String(testName).replace(/\s*\(.*$/s, '').trim();
|
|
47
46
|
|
|
48
47
|
export function firstLines(message, max = 2, width = 240) {
|
|
49
48
|
const lines = String(message).split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(0, max).join(' ⏎ ');
|
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
/** The inline justification that lets a criterion talk about an artifact on purpose. */
|
|
2
2
|
export const ARTIFACT_MARKER_RE = /por\s+que\s+artefato\s*:/i;
|
|
3
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
|
-
*/
|
|
4
|
+
// Node 18 has no inline `(?i:…)` modifier, and the rest of the regex must stay case-sensitive.
|
|
8
5
|
const anyCase = (word) => [...word].map((c) => {
|
|
9
6
|
const lo = c.toLowerCase();
|
|
10
7
|
const up = c.toUpperCase();
|
|
11
8
|
return lo === up ? c : `[${lo}${up}]`;
|
|
12
9
|
}).join('');
|
|
13
10
|
|
|
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
|
-
*/
|
|
11
|
+
// Case-sensitive on purpose: `src`, `docs` or `lib` after "arquivo em" are places, and places are artifacts.
|
|
20
12
|
const FORMAT_TOKEN = String.raw`\s+(?:[A-Z]{2,5}(?![\p{L}\d_])|${anyCase('formato')}(?![\p{L}]))`;
|
|
21
13
|
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
*/
|
|
14
|
+
// `term` is cited verbatim by the `morph-plan` skill (a parity test imports it): keep it stable.
|
|
15
|
+
// Unicode lookarounds, never `\b`: JS `\b` false-boundaries inside "não" or "binário".
|
|
31
16
|
export const ARTIFACT_VOCABULARY = [
|
|
32
17
|
{ term: 'grep', re: /(?<![\p{L}\d_])grep(?![\p{L}\d_])/iu },
|
|
33
18
|
{ term: 'contém a string', re: /cont[ée]m\s+as?\s+strings?/iu },
|
|
@@ -44,12 +29,7 @@ export const ARTIFACT_VOCABULARY = [
|
|
|
44
29
|
{ term: 'glob **/', re: /\*\*\/|\/\*\*/ },
|
|
45
30
|
];
|
|
46
31
|
|
|
47
|
-
/**
|
|
48
|
-
* Vocabulary terms matched in `text`, in vocabulary order, each at most once.
|
|
49
|
-
*
|
|
50
|
-
* @param {string} text
|
|
51
|
-
* @returns {string[]}
|
|
52
|
-
*/
|
|
32
|
+
/** Each term at most once, in vocabulary order. */
|
|
53
33
|
export function findArtifactTerms(text) {
|
|
54
34
|
const s = String(text ?? '');
|
|
55
35
|
const found = [];
|