@polymorphism-tech/morph-spec 9.0.0 → 9.1.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 +4 -62
- 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 +1 -1
- package/framework/MORPH.md +1 -1
- package/framework/agents.json +12 -4
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +6 -17
- 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 +11 -67
- 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/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/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 +2 -2
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- 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 +2 -2
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- 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 +34 -12
- 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/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/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/docs/ui-mockups.md +4 -0
- 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 +14 -96
- 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 +2 -5
- package/src/commands/validation/validate-feature.js +5 -94
- package/src/commands/validation/verify.js +15 -98
- 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/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/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 +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- 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/dotnet-introspection.js +20 -106
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +5 -31
- package/src/lib/plan/plan-parser.js +1 -6
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +5 -23
- package/src/lib/risk-detector.js +39 -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/validators/code-budget/code-budget-validator.js +3 -43
- 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 +20 -125
- package/src/lib/validators/verify-runner.js +52 -358
- 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 +28 -275
- 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/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
|
}
|
|
@@ -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`);
|
|
@@ -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));
|
|
@@ -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 = [];
|
package/src/lib/plan/checks.js
CHANGED
|
@@ -11,17 +11,12 @@ export const MAX_SLICE_FILES = 8;
|
|
|
11
11
|
export const MAX_SLICES = 12;
|
|
12
12
|
export const MAX_PARALLEL = 4;
|
|
13
13
|
|
|
14
|
-
/** sha256 of the plan without its errata: appending a correction never invalidates checks.
|
|
14
|
+
/** sha256 of the plan without its errata: appending a correction never invalidates the extracted checks. */
|
|
15
15
|
export function planHash(text) {
|
|
16
16
|
return createHash('sha256').update(frozenBody(text), 'utf8').digest('hex');
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* Persona ids the project can dispatch: the installed agents.json, else the package's.
|
|
21
|
-
*
|
|
22
|
-
* @param {string} cwd
|
|
23
|
-
* @returns {string[]|null} null when no agents.json is readable — personas then go unchecked
|
|
24
|
-
*/
|
|
19
|
+
/** The installed agents.json, else the package's; null when unreadable (personas then go unchecked). */
|
|
25
20
|
export function knownPersonas(cwd) {
|
|
26
21
|
const agents = readAgentsJson(cwd)?.agents;
|
|
27
22
|
return agents && typeof agents === 'object' ? Object.keys(agents) : null;
|
|
@@ -54,14 +49,8 @@ export function dependencyCycle(slices) {
|
|
|
54
49
|
|
|
55
50
|
const fileKey = (f) => f.replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase();
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* after every slice it depends on ran in an earlier one, and never with a
|
|
60
|
-
* slice that touches one of its files. At most MAX_PARALLEL per wave.
|
|
61
|
-
*
|
|
62
|
-
* @param {{ id: string, depends?: string[], files?: string[] }[]} slices
|
|
63
|
-
* @returns {string[][]} empty when the dependencies have a cycle
|
|
64
|
-
*/
|
|
52
|
+
// A slice joins a wave only after its dependencies ran, never with one touching its files, and at most
|
|
53
|
+
// MAX_PARALLEL per wave. Empty on a dependency cycle.
|
|
65
54
|
export function parallelGroups(slices) {
|
|
66
55
|
if (dependencyCycle(slices)) return [];
|
|
67
56
|
const ids = new Set(slices.map((s) => s.id));
|
|
@@ -94,13 +83,6 @@ export function parallelGroups(slices) {
|
|
|
94
83
|
return groups;
|
|
95
84
|
}
|
|
96
85
|
|
|
97
|
-
/**
|
|
98
|
-
* Judge a parsed plan.
|
|
99
|
-
*
|
|
100
|
-
* @param {ReturnType<import('./plan-parser.js').parsePlan>} plan
|
|
101
|
-
* @param {{ personas?: string[]|null, surfaces?: string[]|null }} [known]
|
|
102
|
-
* @returns {{ errors: string[], warnings: string[] }}
|
|
103
|
-
*/
|
|
104
86
|
export function validatePlan(plan, { personas = null, surfaces = null } = {}) {
|
|
105
87
|
const errors = [];
|
|
106
88
|
const warnings = [];
|
|
@@ -184,7 +166,6 @@ export function validatePlan(plan, { personas = null, surfaces = null } = {}) {
|
|
|
184
166
|
return { errors, warnings };
|
|
185
167
|
}
|
|
186
168
|
|
|
187
|
-
/** The checks.json document for a parsed plan. */
|
|
188
169
|
export function buildChecks(feature, text, plan) {
|
|
189
170
|
return {
|
|
190
171
|
version: CHECKS_VERSION,
|
|
@@ -202,14 +183,7 @@ export function buildChecks(feature, text, plan) {
|
|
|
202
183
|
};
|
|
203
184
|
}
|
|
204
185
|
|
|
205
|
-
/**
|
|
206
|
-
* The feature's extracted checks and whether they still describe the plan on
|
|
207
|
-
* disk. plan-check writes them only with zero errors, so `fresh` also means
|
|
208
|
-
* the plan passed every plan-check rule (no slice over MAX_SLICE_FILES, every
|
|
209
|
-
* scenario covered).
|
|
210
|
-
*
|
|
211
|
-
* @returns {{ status: 'fresh'|'no-plan'|'no-checks'|'unreadable'|'stale', checks: object|null }}
|
|
212
|
-
*/
|
|
186
|
+
/** plan-check writes the checks only with zero errors, so `fresh` also means the plan passed every rule. */
|
|
213
187
|
export function readPlanChecks(cwd, featureName) {
|
|
214
188
|
const planPath = getAbsoluteOutputPath(cwd, featureName, 'plan');
|
|
215
189
|
const checksPath = getAbsoluteOutputPath(cwd, featureName, 'checks');
|
|
@@ -175,12 +175,7 @@ function parseSlices(lines) {
|
|
|
175
175
|
return slices;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
/**
|
|
179
|
-
* Parse `2-plan/plan.md` into its structure. Pure: no validation, no I/O —
|
|
180
|
-
* `checks.js` judges what this returns.
|
|
181
|
-
*
|
|
182
|
-
* @param {string} text
|
|
183
|
-
*/
|
|
178
|
+
/** Pure: no validation, no I/O; `checks.js` judges what this returns. */
|
|
184
179
|
export function parsePlan(text) {
|
|
185
180
|
const lines = markdownLines(text);
|
|
186
181
|
const sections = splitSections(lines);
|
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* portfolio reads. Nothing else records it: each project only knows its own
|
|
4
|
-
* `.morph/`.
|
|
5
|
-
*
|
|
6
|
-
* Three writers register a project as a side note of what they already do —
|
|
7
|
-
* `init`, `update` (never under `--dry-run`) and `dashboard` — and the
|
|
8
|
-
* dashboard adds and removes on request. Registering is fail-open: a registry
|
|
9
|
-
* that cannot be read or written never fails the command that tried.
|
|
10
|
-
*
|
|
11
|
-
* The file lives in the user's home (`~/.morph-spec/projects.json`, or
|
|
12
|
-
* `MORPH_PROJECTS_REGISTRY`), read through `os.homedir()` at call time so a
|
|
13
|
-
* suite that isolates the home also isolates this file. Paths under the
|
|
14
|
-
* system temp dir are never registered: the test suites run `init`/`update`
|
|
15
|
-
* there by the hundred, including through spawned CLIs that keep the real home.
|
|
16
|
-
*/
|
|
1
|
+
// Paths under the system temp dir are never registered: test suites run `init`/`update` there by the
|
|
2
|
+
// hundred, through spawned CLIs that keep the real home. The home is read at call time so isolating it works.
|
|
17
3
|
|
|
18
4
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
19
5
|
import { homedir, tmpdir } from 'os';
|
|
@@ -35,7 +21,6 @@ export function projectKey(dir) {
|
|
|
35
21
|
|
|
36
22
|
const sameDir = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
|
|
37
23
|
|
|
38
|
-
/** A folder is a morph-spec project when morph-spec installed its config there. */
|
|
39
24
|
export function isMorphProject(dir) {
|
|
40
25
|
return existsSync(join(dir, '.morph', 'config', 'config.json'));
|
|
41
26
|
}
|
|
@@ -61,18 +46,13 @@ function projectName(dir) {
|
|
|
61
46
|
}
|
|
62
47
|
}
|
|
63
48
|
|
|
64
|
-
/**
|
|
65
|
-
* The registered projects that still exist. A folder that was deleted or lost
|
|
66
|
-
* its `.morph/` drops out on read — the file is rewritten on the next write.
|
|
67
|
-
*
|
|
68
|
-
* @returns {{ version: number, projects: Array<{ dir: string, name: string, addedAt: string, lastSeenAt: string, source: string }> }}
|
|
69
|
-
*/
|
|
49
|
+
/** Folders that were deleted or lost `.morph/` drop out on read. */
|
|
70
50
|
export function readRegistry() {
|
|
71
51
|
let data = null;
|
|
72
52
|
try {
|
|
73
53
|
const path = registryPath();
|
|
74
54
|
if (existsSync(path)) data = JSON.parse(readFileSync(path, 'utf8'));
|
|
75
|
-
} catch {
|
|
55
|
+
} catch {}
|
|
76
56
|
const projects = Array.isArray(data?.projects) ? data.projects : [];
|
|
77
57
|
return {
|
|
78
58
|
version: REGISTRY_VERSION,
|
|
@@ -84,16 +64,7 @@ function write(projects) {
|
|
|
84
64
|
atomicWriteJson(registryPath(), { version: REGISTRY_VERSION, projects });
|
|
85
65
|
}
|
|
86
66
|
|
|
87
|
-
/**
|
|
88
|
-
* Note that `dir` is a morph-spec project. Idempotent; refreshes `lastSeenAt`.
|
|
89
|
-
* Never throws.
|
|
90
|
-
*
|
|
91
|
-
* @param {string} dir
|
|
92
|
-
* @param {'init'|'update'|'dashboard'|'manual'} source
|
|
93
|
-
* @param {{ tempRoot?: string }} [opts] - test seam: the folder whose
|
|
94
|
-
* projects are never registered (the system temp dir)
|
|
95
|
-
* @returns {boolean} true when the project is in the registry afterwards
|
|
96
|
-
*/
|
|
67
|
+
/** Idempotent, never throws; `tempRoot` is the test seam for the never-registered folder. */
|
|
97
68
|
export function registerProject(dir, source, { tempRoot = tmpdir() } = {}) {
|
|
98
69
|
try {
|
|
99
70
|
const key = projectKey(dir);
|
|
@@ -115,7 +86,7 @@ export function registerProject(dir, source, { tempRoot = tmpdir() } = {}) {
|
|
|
115
86
|
}
|
|
116
87
|
}
|
|
117
88
|
|
|
118
|
-
/**
|
|
89
|
+
/** Never throws; the project's files are not touched. */
|
|
119
90
|
export function unregisterProject(dir) {
|
|
120
91
|
try {
|
|
121
92
|
const key = projectKey(dir);
|
|
@@ -128,15 +99,7 @@ export function unregisterProject(dir) {
|
|
|
128
99
|
}
|
|
129
100
|
}
|
|
130
101
|
|
|
131
|
-
/**
|
|
132
|
-
* morph-spec projects directly under each root that are not registered yet —
|
|
133
|
-
* the portfolio offers them, the person decides. One level deep: a repos
|
|
134
|
-
* folder is the expected root, and a deeper walk would enter node_modules and
|
|
135
|
-
* worktrees for nothing.
|
|
136
|
-
*
|
|
137
|
-
* @param {string[]} roots
|
|
138
|
-
* @returns {Array<{ dir: string, name: string }>}
|
|
139
|
-
*/
|
|
102
|
+
/** Unregistered projects one level under each root: deeper would walk node_modules and worktrees. */
|
|
140
103
|
export function discoverProjects(roots) {
|
|
141
104
|
const registered = readRegistry().projects.map((p) => p.dir);
|
|
142
105
|
const out = [];
|
|
@@ -60,7 +60,7 @@ function tableRows(body) {
|
|
|
60
60
|
return rows;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
// `| id | tipo | onde | evidência |`; `untyped` = tipo fora de BLOCKER_TYPES.
|
|
64
64
|
function parseBlockers(body) {
|
|
65
65
|
const blockers = [];
|
|
66
66
|
const untyped = [];
|
|
@@ -74,10 +74,7 @@ function parseBlockers(body) {
|
|
|
74
74
|
|
|
75
75
|
const STATUS_BY_KEY = Object.fromEntries(CHECK_STATUSES.map((s) => [normalize(s), s]));
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
* `| Cn | PROVADO|NÃO PROVADO|FALHA | evidência |` de TODAS as seções: uma confirmação
|
|
79
|
-
* traz só os Cn que mudou, então vale a última linha de cada Cn. Status por igualdade exata.
|
|
80
|
-
*/
|
|
77
|
+
// Vale a última linha de cada Cn em todas as seções: uma confirmação traz só os Cn que mudou.
|
|
81
78
|
function parseChecks(bodies) {
|
|
82
79
|
const byId = new Map();
|
|
83
80
|
for (const cells of bodies.flatMap(tableRows)) {
|
|
@@ -110,10 +107,7 @@ export function parseReviewReport(text) {
|
|
|
110
107
|
};
|
|
111
108
|
}
|
|
112
109
|
|
|
113
|
-
|
|
114
|
-
* `missing`: sem arquivo. `partial`: `PARCIAL` na 1ª linha, com ou sem veredito.
|
|
115
|
-
* `unverdicted`: sem `**Verdict:** SHIP|FIX`. `ok`: legível — quem decide é o risk-detector.
|
|
116
|
-
*/
|
|
110
|
+
// `partial`: `PARCIAL` na 1ª linha, com ou sem veredito. `ok` só diz legível: quem decide é o risk-detector.
|
|
117
111
|
function stateOf(path, text) {
|
|
118
112
|
if (text === null) return { status: 'missing', path };
|
|
119
113
|
const parsed = parseReviewReport(text);
|
|
@@ -148,13 +142,7 @@ export function reviewReports(cwd, featureName) {
|
|
|
148
142
|
return reviewReportsFromDir(join(cwd, '.morph', 'features', featureName));
|
|
149
143
|
}
|
|
150
144
|
|
|
151
|
-
/**
|
|
152
|
-
* Todo Cn precisa de uma linha, e só `PROVADO` cobre.
|
|
153
|
-
*
|
|
154
|
-
* @param {string[]} checkIds
|
|
155
|
-
* @param {{id: string, status: string}[]|null} rows - `parseReviewReport(...).checks`
|
|
156
|
-
* @returns {{ missing: string[], notProven: {id: string, status: string}[] }}
|
|
157
|
-
*/
|
|
145
|
+
/** Todo Cn precisa de uma linha, e só `PROVADO` cobre. */
|
|
158
146
|
export function checksCoverage(checkIds, rows) {
|
|
159
147
|
const byId = new Map((rows ?? []).map((r) => [r.id, r]));
|
|
160
148
|
const missing = [];
|
|
@@ -167,12 +155,7 @@ export function checksCoverage(checkIds, rows) {
|
|
|
167
155
|
return { missing, notProven };
|
|
168
156
|
}
|
|
169
157
|
|
|
170
|
-
/**
|
|
171
|
-
* O sinal que `reviewPause` (risk-detector) lê; `checkIds: null` = cobertura não conferível.
|
|
172
|
-
*
|
|
173
|
-
* @param {ReturnType<typeof reviewReportsFromDir>} reports
|
|
174
|
-
* @param {{ required: string[], checkIds: string[]|null }} opts
|
|
175
|
-
*/
|
|
158
|
+
/** O sinal que `reviewPause` lê; `checkIds: null` = cobertura não conferível. */
|
|
176
159
|
export function reviewSignal(reports, { required, checkIds }) {
|
|
177
160
|
const out = { required, reports: {} };
|
|
178
161
|
for (const role of ROLE_ORDER) {
|
|
@@ -205,7 +188,6 @@ export function combinedVerdict(reports, required) {
|
|
|
205
188
|
return required.every((r) => reports[r].status === 'ok' && reports[r].verdict === 'SHIP') ? 'SHIP' : null;
|
|
206
189
|
}
|
|
207
190
|
|
|
208
|
-
/** Todos os bloqueadores tipados dos relatórios presentes, com o papel que os abriu. */
|
|
209
191
|
export function allBlockers(reports) {
|
|
210
192
|
return ROLE_ORDER.flatMap((role) => (reports[role].blockers ?? []).map((b) => ({ ...b, role })));
|
|
211
193
|
}
|