@polymorphism-tech/morph-spec 8.46.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +53 -44
- package/README.md +3 -4
- package/bin/morph-spec.js +38 -55
- package/bin/validate.js +5 -134
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +2 -2
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +75 -18
- package/framework/commands/create-umbraco-lp.md +2 -0
- package/framework/commands/migrate-lp-to-umbraco.md +2 -0
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +2 -2
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +17 -203
- package/framework/commands/morph-media.md +3 -0
- package/framework/commands/morph-preflight.md +7 -31
- package/framework/commands/morph-proposal.md +2 -50
- package/framework/commands/morph-status.md +3 -3
- package/framework/commands/morph-troubleshoot.md +2 -14
- package/framework/docs/CLI.md +585 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/comment-guard.js +1 -1
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
- package/framework/hooks/claude-code/core/gate-guard.js +2 -11
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
- package/framework/hooks/claude-code/core/state-sync.js +17 -19
- package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
- package/framework/hooks/claude-code/core/validate-completion.js +33 -97
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
- package/framework/hooks/claude-code/core/worktree-context.js +5 -4
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +1 -1
- package/framework/hooks/shared/build-check.js +10 -6
- package/framework/hooks/shared/command-tokens.js +169 -0
- package/framework/hooks/shared/comment-rules.js +1 -1
- package/framework/hooks/shared/comment-scanner.js +10 -23
- package/framework/hooks/shared/comment-stripper.js +1 -1
- package/framework/hooks/shared/e2e-check.js +67 -31
- package/framework/hooks/shared/errata.js +72 -0
- package/framework/hooks/shared/feature-locator.js +1 -1
- package/framework/hooks/shared/frontend-locations.js +6 -0
- package/framework/hooks/shared/gate-question.js +18 -0
- package/framework/hooks/shared/phase-utils.js +15 -32
- package/framework/hooks/shared/slice-progress.js +118 -0
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +61 -87
- package/framework/hooks/shared/state-writer.js +3 -3
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/ai-agents-standards.md +18 -16
- package/framework/rules/asaas-standards.md +3 -1
- package/framework/rules/csharp-standards.md +10 -8
- package/framework/rules/data-standards.md +5 -5
- package/framework/rules/dotnet-templates.md +1 -1
- package/framework/rules/frontend-standards.md +4 -2
- package/framework/rules/gsap-standards.md +10 -5
- package/framework/rules/hangfire-standards.md +3 -1
- package/framework/rules/infrastructure-standards.md +4 -2
- package/framework/rules/neon-auth-standards.md +3 -1
- package/framework/rules/neon-standards.md +6 -4
- package/framework/rules/nextjs-standards.md +13 -11
- package/framework/rules/nextjs-templates.md +1 -1
- package/framework/rules/resend-standards.md +3 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -52
- package/framework/skills/README.md +4 -5
- package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
- package/framework/skills/core/morph-apply/SKILL.md +150 -223
- package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
- package/framework/skills/core/morph-machine/SKILL.md +4 -4
- package/framework/skills/core/morph-media/SKILL.md +3 -3
- package/framework/skills/core/morph-plan/SKILL.md +138 -300
- package/framework/skills/core/morph-review/SKILL.md +112 -137
- package/framework/skills/core/morph-standards/SKILL.md +29 -41
- package/framework/skills/core/morph-uiux/SKILL.md +78 -153
- package/framework/standards/STANDARDS.json +90 -1219
- package/framework/standards/STANDARDS.meta.json +1305 -0
- package/framework/standards/ai-agents/evals-with-cache.md +2 -3
- package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
- package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
- package/framework/standards/ai-agents/prompt-sources.md +6 -4
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/structured-output.md +1 -1
- package/framework/standards/ai-agents/sweet-spot.md +1 -1
- package/framework/standards/architecture/code-comments.md +8 -8
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
- package/framework/standards/backend/api/validation.md +135 -47
- package/framework/standards/backend/database/postgresql/database.md +11 -7
- package/framework/standards/backend/database/vector-search-rag.md +207 -324
- package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
- package/framework/standards/backend/dotnet/result-pattern.md +40 -6
- package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
- package/framework/standards/backend/dotnet/scrutor.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
- package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
- package/framework/standards/data/nosql/cache/redis.md +120 -44
- package/framework/standards/data/nosql/cosmos-db.md +114 -71
- package/framework/standards/frontend/design-system/animations.md +1 -1
- package/framework/standards/frontend/design-system/naming.md +1 -1
- package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
- package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
- package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
- package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +6 -170
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
- package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
- package/framework/templates/infrastructure/github/README.md +2 -1
- package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +2 -2
- package/src/commands/observability/cost.js +18 -60
- package/src/commands/observability/telemetry.js +3 -5
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +9 -9
- package/src/commands/project/fleet.js +4 -4
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +6 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +79 -25
- package/src/commands/project/status.js +29 -69
- package/src/commands/project/update.js +33 -19
- package/src/commands/project/worktree.js +1 -1
- package/src/commands/state/advance.js +62 -46
- package/src/commands/state/approve.js +95 -138
- package/src/commands/state/archive.js +20 -84
- package/src/commands/state/auto-approval.js +68 -0
- package/src/commands/state/create.js +28 -1
- package/src/commands/state/delete.js +1 -1
- package/src/commands/state/finish.js +11 -23
- package/src/commands/state/gate-check.js +86 -95
- package/src/commands/state/gate-decision.js +6 -4
- package/src/commands/state/handoff.js +1 -1
- package/src/commands/state/list.js +9 -9
- package/src/commands/state/report-check.js +157 -311
- package/src/commands/state/retype.js +9 -3
- package/src/commands/validation/mutate.js +77 -14
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +60 -135
- package/src/commands/validation/verify.js +41 -82
- package/src/core/paths/output-schema.js +20 -67
- package/src/core/state/gate-order.js +5 -6
- package/src/core/state/lifecycle-audit.js +2 -2
- package/src/core/state/state-manager.js +17 -98
- package/src/core/templates/template-registry.js +2 -2
- package/src/core/templates/template-seed.js +22 -0
- package/src/core/work-types.js +10 -10
- package/src/dashboard/app/App.jsx +0 -2
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
- package/src/dashboard/app/components/CommandPalette.jsx +7 -8
- package/src/dashboard/app/components/EntityLink.jsx +1 -2
- package/src/dashboard/app/components/Markdown.jsx +1 -1
- package/src/dashboard/app/components/Sidebar.jsx +1 -2
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
- package/src/dashboard/app/lib/phases.js +2 -4
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
- package/src/dashboard/app/pages/Features.jsx +9 -9
- package/src/dashboard/app/pages/Guide.jsx +2 -2
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
- package/src/dashboard/app/pages/RulesMap.jsx +3 -3
- package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
- package/src/dashboard/app/pages/Standards.jsx +1 -1
- package/src/dashboard/scanner/activity-scanner.js +6 -16
- package/src/dashboard/scanner/agents-scanner.js +1 -1
- package/src/dashboard/scanner/docs-scanner.js +5 -23
- package/src/dashboard/scanner/features-scanner.js +95 -60
- package/src/dashboard/scanner/index.js +0 -2
- package/src/dashboard/scanner/project-api.js +1 -1
- package/src/dashboard/scanner/rules-scanner.js +15 -10
- package/src/dashboard/scanner/skills-scanner.js +2 -3
- package/src/dashboard/scanner/standards-scanner.js +2 -1
- package/src/lib/ci/ci-verify.js +17 -1
- package/src/lib/ci/github-ci.js +1 -1
- package/src/lib/detectors/index.js +3 -8
- package/src/lib/generators/handoff-generator.js +34 -65
- package/src/lib/generators/settings-generator.js +11 -89
- package/src/lib/git/evidence-staleness.js +1 -1
- package/src/lib/git/tree-fingerprint.js +19 -7
- package/src/lib/git/tree-state.js +2 -2
- package/src/lib/heavy/cleanup.js +1 -5
- package/src/lib/installers/memory-installer.js +6 -8
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +46 -9
- package/src/lib/plan/artifact-vocabulary.js +60 -0
- package/src/lib/plan/checks.js +234 -0
- package/src/lib/plan/plan-parser.js +209 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/review-reports.js +222 -0
- package/src/lib/risk-detector.js +134 -135
- package/src/lib/specs/adr-index.js +50 -69
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +44 -0
- package/src/lib/specs/gfm-table.js +17 -0
- package/src/lib/specs/old-format.js +68 -0
- package/src/lib/specs/revocations.js +37 -95
- package/src/lib/specs/screen-checklist.js +18 -17
- package/src/lib/stack/dotnet-build-ignores.js +47 -0
- package/src/lib/stack/stack-profile.js +1 -1
- package/src/lib/stack/stack-scan.js +267 -0
- package/src/lib/standards/registry.js +43 -0
- package/src/lib/standards/sections.js +58 -0
- package/src/lib/tasks/eval-node.js +4 -12
- package/src/lib/tasks/test-nodes.js +1 -74
- package/src/lib/tasks/test-runner.js +75 -117
- package/src/lib/telemetry/metrics.js +8 -69
- package/src/lib/telemetry/telemetry-reader.js +5 -118
- package/src/lib/telemetry/verify-event.js +5 -7
- package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
- package/src/lib/validators/reuse/reuse-validator.js +24 -48
- package/src/lib/validators/reuse/symbol-scanner.js +2 -4
- package/src/lib/validators/traceability-check.js +85 -129
- package/src/lib/validators/validation-runner.js +8 -88
- package/src/lib/validators/verify-runner.js +134 -301
- package/src/lib/worktree/docker-audit.js +1 -2
- package/src/lib/worktree/fleet-scanner.js +25 -38
- package/src/scripts/setup-infra.js +42 -22
- package/src/utils/agents-installer.js +92 -13
- package/src/utils/claude-settings-manager.js +0 -35
- package/src/utils/file-copier.js +34 -6
- package/src/utils/hooks-installer.js +16 -9
- package/src/utils/skills-installer.js +1 -2
- package/framework/commands/commit.md +0 -166
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rubrics/backend.json +0 -41
- package/framework/rubrics/default.json +0 -36
- package/framework/rubrics/frontend.json +0 -36
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/project/validate.js +0 -97
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/dashboard/app/pages/Rubrics.jsx +0 -97
- package/src/dashboard/scanner/rubrics-scanner.js +0 -12
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/evaluator-report.js +0 -91
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/lib/validators/architecture/architecture-validator.js +0 -270
- package/src/lib/validators/architecture/index.js +0 -7
- package/src/lib/validators/css/css-validator.js +0 -340
- package/src/lib/validators/css/index.js +0 -7
- package/src/lib/validators/design-system/design-system-validator.js +0 -237
- package/src/lib/validators/design-system/index.js +0 -7
- package/src/lib/validators/score-aggregator.js +0 -244
- package/src/lib/validators/ui/index.js +0 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { mkdirSync } from 'fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { getFeature, setApprovalGate, updateFeature } from '../../core/state/state-manager.js';
|
|
5
5
|
import { WORK_TYPES, isValidWorkType, POLICY_APPROVER_PREFIX } from '../../core/work-types.js';
|
|
6
|
+
import { renderPlanSkeleton } from '../../lib/plan/plan-template.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* `morph-spec retype <feature> <newType>` — mid-flight work-type change with
|
|
@@ -12,7 +13,7 @@ import { WORK_TYPES, isValidWorkType, POLICY_APPROVER_PREFIX } from '../../core/
|
|
|
12
13
|
* - Blocked outright once Gate 3 (review) is approved — the feature already
|
|
13
14
|
* shipped its pipeline, retyping it retroactively would be revisionist.
|
|
14
15
|
* - Downgrade (fewer skipped gates → more skipped gates, e.g. feature/bug
|
|
15
|
-
* → chore/hotfix) is blocked once
|
|
16
|
+
* → chore/hotfix) is blocked once the plan gate carries a genuine
|
|
16
17
|
* (non-policy) approval — the plan was already accepted under the old
|
|
17
18
|
* type's scrutiny; retyping around it would erase that judgment call.
|
|
18
19
|
* - Upgrade (more skipped gates → fewer, e.g. chore/hotfix → feature/bug)
|
|
@@ -61,7 +62,7 @@ export async function retypeCommand(featureName, newType, options = {}) {
|
|
|
61
62
|
const genuinelyApproved = planGate?.approved === true
|
|
62
63
|
&& !String(planGate.approvedBy || '').startsWith(POLICY_APPROVER_PREFIX);
|
|
63
64
|
if (genuinelyApproved) {
|
|
64
|
-
console.error(chalk.red(`\n❌ Gate
|
|
65
|
+
console.error(chalk.red(`\n❌ Gate do plano já aprovado por "${planGate.approvedBy}"; downgrade bloqueado — o plano já foi aceito\n`));
|
|
65
66
|
process.exit(1);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -93,6 +94,11 @@ export async function retypeCommand(featureName, newType, options = {}) {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
const featureDir = join(process.cwd(), '.morph', 'features', featureName);
|
|
97
|
+
const planPath = join(featureDir, '2-plan', 'plan.md');
|
|
98
|
+
if (stampedGates.includes('plan') && existsSync(planPath)
|
|
99
|
+
&& readFileSync(planPath, 'utf8') === renderPlanSkeleton(featureName)) {
|
|
100
|
+
unlinkSync(planPath);
|
|
101
|
+
}
|
|
96
102
|
for (const folder of newWorkType.scaffold) {
|
|
97
103
|
mkdirSync(join(featureDir, folder), { recursive: true });
|
|
98
104
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
import { dotnetTestMode } from '../../lib/
|
|
5
|
+
import { discoverDotnetBuildTargets } from '../../lib/tasks/test-runner.js';
|
|
6
|
+
import { dotnetTestMode } from '../../lib/mutation/dotnet-introspection.js';
|
|
7
7
|
import { dotnetBuildCommands } from '../../lib/mutation/commands.js';
|
|
8
8
|
import { resolveTestNodes } from '../../lib/tasks/test-nodes.js';
|
|
9
9
|
import { changedPathsAgainst, detectDefaultBranch, getHeadSha } from '../../lib/git/default-branch.js';
|
|
@@ -12,7 +12,8 @@ import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js
|
|
|
12
12
|
import { resolveGitCommonDir } from '../../../framework/hooks/shared/git-paths.js';
|
|
13
13
|
import { BACKSLASH_TOKEN, ManifestError } from '../../lib/mutation/manifest.js';
|
|
14
14
|
import { groupByNode, loadPlan } from '../../lib/mutation/plan.js';
|
|
15
|
-
import { dotnetSubset, jsSubset, testProjectDirsOf } from '../../lib/mutation/subset.js';
|
|
15
|
+
import { dotnetSubset, jsSubset, mutatedTypesOf, testFilesCiting, testProjectDirsOf } from '../../lib/mutation/subset.js';
|
|
16
|
+
import { isFinished } from '../../dashboard/scanner/feature-status.js';
|
|
16
17
|
import { dotnetAdapter, genericAdapter, spawnExec } from '../../lib/mutation/adapters.js';
|
|
17
18
|
import { runMutation } from '../../lib/mutation/runner.js';
|
|
18
19
|
import { renderSection, summarize } from '../../lib/mutation/report.js';
|
|
@@ -97,23 +98,72 @@ export function restoreLatest(root, out = console) {
|
|
|
97
98
|
return restored;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
function testDirsOf(node, root, { projectsOnly = false } = {}) {
|
|
102
|
+
const cwd = resolve(root, node.cwd || '.');
|
|
103
|
+
const targets = discoverDotnetBuildTargets(node, cwd);
|
|
104
|
+
return testProjectDirsOf(root, projectsOnly ? targets.filter((t) => !/\.slnx?$/i.test(t)) : targets, cwd);
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
function subsetFor({ node, group, root, options }) {
|
|
101
108
|
if (options.subset === false) return { usable: false, reason: '--no-subset' };
|
|
102
109
|
const mutatedFiles = group.regions.map((r) => r.file);
|
|
103
110
|
if (node.runner === 'dotnet') {
|
|
104
111
|
const base = detectDefaultBranch(root);
|
|
105
112
|
const changed = base ? changedPathsAgainst(base, root) : [];
|
|
106
|
-
return dotnetSubset({ root, testProjectDirs:
|
|
113
|
+
return dotnetSubset({ root, testProjectDirs: testDirsOf(node, root), mutatedFiles, changed });
|
|
107
114
|
}
|
|
108
115
|
return jsSubset({ runner: node.runner, command: node.command, root, mutatedFiles });
|
|
109
116
|
}
|
|
110
117
|
|
|
111
|
-
|
|
118
|
+
function e2eManualProtocol(feature, node) {
|
|
119
|
+
const f = feature ?? '<feature>';
|
|
120
|
+
const round = `eval "$(morph-spec env ${f})" && morph-spec e2e up ${f} && morph-spec heavy run -- ${node.command}`;
|
|
121
|
+
return 'mutate só recompila localmente, e a suíte que alcança esse código testa o container do compose: o mutante sairia sobrevivente em falso. ' +
|
|
122
|
+
`Prove à mão pelo nó "${node.id}", cada passo numa linha só e encadeado por \`&&\` (nunca \`;\`: um passo que falha não pode deixar a suíte rodar sobre o código original; ` +
|
|
123
|
+
'e o env da feature se perde entre chamadas de Bash): ' +
|
|
124
|
+
`(1) base verde antes do 1º mutante: \`${round}\`; ` +
|
|
125
|
+
`(2) por mutante: edite a linha && \`${round}\`; ` +
|
|
126
|
+
`(3) restaure, confirme com \`git diff --quiet -- <arquivo>\` e suba a stack de novo (\`eval "$(morph-spec env ${f})" && morph-spec e2e up ${f}\`). ` +
|
|
127
|
+
'Registre cada mutante em 4-review (expect, resultado, teste que o matou).';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// A .cs file belongs to every dotnet suite, by runner, not by project reference: a unit suite
|
|
131
|
+
// "owns" code only an e2e suite executes. With an e2e node declared, a unit suite with no test
|
|
132
|
+
// naming a type of the mutated file is not evidence, so the run is refused.
|
|
133
|
+
function uncitedUnderE2e({ root, groups, nodes }) {
|
|
134
|
+
const e2e = nodes.filter((n) => n.e2e);
|
|
135
|
+
if (e2e.length === 0) return [];
|
|
136
|
+
const e2eDirs = new Set(e2e.filter((n) => n.runner === 'dotnet').flatMap((n) => testDirsOf(n, root)));
|
|
137
|
+
const uncited = [];
|
|
138
|
+
for (const { node, regions } of groups.filter((g) => g.node.runner === 'dotnet')) {
|
|
139
|
+
const direct = new Set(testDirsOf(node, root, { projectsOnly: true }));
|
|
140
|
+
const dirs = testDirsOf(node, root).filter((d) => direct.has(d) || !e2eDirs.has(d));
|
|
141
|
+
for (const region of regions) {
|
|
142
|
+
const types = mutatedTypesOf(root, [region.file]);
|
|
143
|
+
if (testFilesCiting({ root, testProjectDirs: dirs, types, exclude: [region.file] }).length === 0) uncited.push({ region, node, types, e2e });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return uncited;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function soleActiveFeature(root) {
|
|
150
|
+
const dir = join(root, '.morph', 'features');
|
|
151
|
+
let names;
|
|
152
|
+
try { names = readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return null; }
|
|
153
|
+
const active = names.filter((name) => {
|
|
154
|
+
try { return !isFinished(JSON.parse(readFileSync(join(dir, name, 'feature.json'), 'utf8'))); } catch { return false; }
|
|
155
|
+
});
|
|
156
|
+
return active.length === 1 ? active[0] : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function mutateCommand(featureArg, options = {}) {
|
|
112
160
|
const root = resolve(options.cwd ?? process.cwd());
|
|
113
161
|
if (options.restore) { restoreLatest(root); return null; }
|
|
114
162
|
|
|
115
|
-
const
|
|
116
|
-
if (!
|
|
163
|
+
const feature = featureArg ?? soleActiveFeature(root);
|
|
164
|
+
if (!feature && !options.plan) return fail('sem feature: passe <feature> (nenhuma, ou mais de uma, em andamento) ou o manifesto por --plan.', EXIT.usage);
|
|
165
|
+
const planPath = resolve(root, options.plan ?? join('.morph', 'features', feature, '4-review', 'mutants.json'));
|
|
166
|
+
if (!existsSync(planPath)) return fail(`manifesto não encontrado: ${planPath}\nEscreva-o com a ferramenta Write, se você a tem; só com Bash, heredoc de delimitador entre aspas simples, com a barra invertida do código como ${BACKSLASH_TOKEN} — formato em .morph/framework/CLI.md, seção "### mutate".`, EXIT.usage);
|
|
117
167
|
|
|
118
168
|
let regions;
|
|
119
169
|
try {
|
|
@@ -125,16 +175,26 @@ export async function mutateCommand(feature, options = {}) {
|
|
|
125
175
|
}
|
|
126
176
|
|
|
127
177
|
const { nodes } = resolveTestNodes(root);
|
|
128
|
-
const { groups, orphans } = groupByNode(regions, nodes, root);
|
|
178
|
+
const { groups, orphans, stackOnly } = groupByNode(regions, nodes, root);
|
|
129
179
|
if (orphans.length > 0) {
|
|
130
180
|
return fail(`nenhuma suíte declarada roda: ${orphans.map((o) => `${o.id} (${o.file})`).join(', ')}\nDeclare o nó em project.tests[] (.morph/config/config.json).`, EXIT.usage);
|
|
131
181
|
}
|
|
182
|
+
if (stackOnly.length > 0) {
|
|
183
|
+
const named = stackOnly.map(({ region, node }) => `${region.id} (${region.file}) → nó "${node.id}"`).join(', ');
|
|
184
|
+
return fail(`só uma suíte e2e: true cobre: ${named}\nProve a guarda por um teste de unidade, ou: ${e2eManualProtocol(feature, stackOnly[0].node)}`, EXIT.usage);
|
|
185
|
+
}
|
|
132
186
|
const mtp = groups.find((g) => g.node.runner === 'dotnet' && dotnetTestMode(resolve(root, g.node.cwd || '.')) === 'mtp');
|
|
133
187
|
if (mtp) return fail(`a suíte "${mtp.node.id}" roda no Microsoft.Testing.Platform (global.json): o TRX do VSTest, de onde o veredito e o motivo saem, não existe nela. Suporte a MTP ainda não há — rode a mutação com a suíte inteira à mão.`, EXIT.usage);
|
|
134
188
|
const uncompilable = groups.find((g) => g.node.runner === 'dotnet' && dotnetBuildCommands(g.node, root).length === 0);
|
|
135
|
-
if (uncompilable) return fail(`a suíte "${uncompilable.node.id}" não
|
|
189
|
+
if (uncompilable) return fail(`a suíte "${uncompilable.node.id}" não deixa descobrir o que compilar: o comando não nomeia .sln/.slnx/.csproj e o diretório alvo não tem uma única solution (ou um único projeto). Sem isso, todo mutante rodaria sobre o binário antigo e sairia "sobrevivente". Nomeie a solution no comando ou declare o campo build no nó (config.json, project.tests[]).`, EXIT.usage);
|
|
136
190
|
const preDecided = groups.find((g) => g.node.status);
|
|
137
191
|
if (preDecided) return fail(`a suíte "${preDecided.node.id}" já vem decidida como ${preDecided.node.status}: ${preDecided.node.reason ?? ''}`, EXIT.usage);
|
|
192
|
+
const uncited = uncitedUnderE2e({ root, groups, nodes });
|
|
193
|
+
if (uncited.length > 0) {
|
|
194
|
+
const named = uncited.map(({ region, node, types }) => `${region.id} (${region.file}; tipos: ${types.join(', ') || 'nenhum legível'}) → nenhum teste da suíte "${node.id}" os cita`).join('\n');
|
|
195
|
+
const e2eIds = uncited[0].e2e.map((n) => `"${n.id}"`).join(', ');
|
|
196
|
+
return fail(`${named}\nNó e2e: true declarado: ${e2eIds}. ${e2eManualProtocol(feature, uncited[0].e2e[0])}`, EXIT.usage);
|
|
197
|
+
}
|
|
138
198
|
|
|
139
199
|
if (!options.allowDirty) {
|
|
140
200
|
const dirty = gitOut(root, ['status', '--porcelain', '--', ...new Set(regions.map((r) => r.file))]).trim();
|
|
@@ -184,15 +244,18 @@ export async function mutateCommand(feature, options = {}) {
|
|
|
184
244
|
const order = new Map(regions.map((r, i) => [r.id, i]));
|
|
185
245
|
merged.mutants.sort((a, b) => order.get(a.id) - order.get(b.id));
|
|
186
246
|
const section = renderSection(merged, { head: git.head(), date: new Date().toISOString().slice(0, 10), subset: subsets[0] ?? null });
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
247
|
+
const outRel = options.out ?? (feature ? join('.morph', 'features', feature, '4-review', 'mutation-section.md') : null);
|
|
248
|
+
const outPath = outRel ? resolve(root, outRel) : null;
|
|
249
|
+
if (outPath) {
|
|
250
|
+
mkdirSync(join(outPath, '..'), { recursive: true });
|
|
251
|
+
writeFileSync(outPath, section);
|
|
252
|
+
}
|
|
190
253
|
|
|
191
254
|
const tally = summarize(merged);
|
|
192
255
|
if (options.json) console.log(JSON.stringify({ ...merged, tally, section: outPath }, null, 2));
|
|
193
256
|
else {
|
|
194
257
|
console.log(section);
|
|
195
|
-
console.log(chalk.gray(`seção gravada em ${outPath}`));
|
|
258
|
+
console.log(chalk.gray(outPath ? `seção gravada em ${outPath}` : 'seção não gravada: sem feature nem --out'));
|
|
196
259
|
console.log(`${chalk.green(`${tally.killed} mortos`)} · ${tally.survived ? chalk.yellow(`${tally.survived} sobreviveram`) : '0 sobreviveram'} · ${tally['build-error']} não compilaram · ${tally.inconclusive} inconclusivos`);
|
|
197
260
|
}
|
|
198
261
|
if (!merged.restore.ok || ['mismatch', 'unlocated'].includes(merged.binary.proof) || !merged.restore.treeClean) process.exitCode = EXIT.unsafe;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
5
|
+
import { isGateApproved } from '../../../framework/hooks/shared/state-reader.js';
|
|
6
|
+
import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
|
|
7
|
+
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
8
|
+
import { buildChecks, knownPersonas, parallelGroups, planHash, validatePlan } from '../../lib/plan/checks.js';
|
|
9
|
+
import { knownSurfaces } from '../../lib/specs/screen-checklist.js';
|
|
10
|
+
import { revocationWarnings } from '../../lib/specs/adr-index.js';
|
|
11
|
+
import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
|
|
12
|
+
import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
|
|
13
|
+
import { reportDotnetIgnores } from '../state/advance.js';
|
|
14
|
+
|
|
15
|
+
function readJson(path) {
|
|
16
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Parse and judge `2-plan/plan.md` of the project at `cwd`; with no error, write `2-plan/checks.json`. */
|
|
20
|
+
export function runPlanCheck(featureName, cwd = process.cwd()) {
|
|
21
|
+
const planPath = getAbsoluteOutputPath(cwd, featureName, 'plan');
|
|
22
|
+
const checksPath = getAbsoluteOutputPath(cwd, featureName, 'checks');
|
|
23
|
+
const base = { feature: featureName, plan: planPath, checksPath };
|
|
24
|
+
if (!existsSync(planPath)) return { ...base, ok: false, exit: 2, error: 'plan-not-found', errors: [], warnings: [], groups: [] };
|
|
25
|
+
|
|
26
|
+
const text = readFileSync(planPath, 'utf8');
|
|
27
|
+
const plan = parsePlan(text);
|
|
28
|
+
const { errors, warnings } = validatePlan(plan, { personas: knownPersonas(cwd), surfaces: knownSurfaces(cwd) });
|
|
29
|
+
warnings.push(...revocationWarnings(cwd, plan));
|
|
30
|
+
const groups = errors.length === 0 ? parallelGroups(plan.slices) : [];
|
|
31
|
+
const hash = planHash(text);
|
|
32
|
+
const result = { ...base, planHash: hash, errors, warnings, groups, written: false };
|
|
33
|
+
if (errors.length > 0) return { ...result, ok: false, exit: 1 };
|
|
34
|
+
|
|
35
|
+
const approved = isGateApproved(featureName, 'plan', cwd);
|
|
36
|
+
const previous = existsSync(checksPath) ? readJson(checksPath) : null;
|
|
37
|
+
if (approved && previous && previous.planHash !== hash) {
|
|
38
|
+
return { ...result, ok: false, exit: 1, error: 'frozen', previousHash: previous.planHash ?? null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
atomicWriteJson(checksPath, buildChecks(featureName, text, plan));
|
|
42
|
+
return { ...result, ok: true, exit: 0, written: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function planCheckCommand(featureName, options = {}) {
|
|
46
|
+
const owner = assertOwnerTree(featureName, process.cwd());
|
|
47
|
+
if (owner.blocked) {
|
|
48
|
+
for (const { tone, text } of ownerTreeRefusalLines(featureName, owner.tree, owner.branch, `morph-spec plan-check ${featureName}`)) {
|
|
49
|
+
console.error(chalk[tone](text));
|
|
50
|
+
}
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = runPlanCheck(featureName);
|
|
55
|
+
const gitignoreAdded = result.written ? seedDotnetBuildIgnores(process.cwd(), { featureName }) : [];
|
|
56
|
+
if (options.json) {
|
|
57
|
+
if (gitignoreAdded.length) result.gitignoreAdded = gitignoreAdded;
|
|
58
|
+
console.log(JSON.stringify(result, null, 2));
|
|
59
|
+
process.exit(result.exit);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (result.error === 'plan-not-found') {
|
|
63
|
+
console.error(chalk.red(`plan-check: ${result.plan} não existe — o create escreve o esqueleto em 2-plan/plan.md (chore/hotfix: você o cria); gramática completa: morph-spec plan-check --help`));
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
for (const e of result.errors) console.log(chalk.red(` ✗ ${e}`));
|
|
67
|
+
for (const w of result.warnings) console.log(chalk.yellow(` ⚠ ${w}`));
|
|
68
|
+
|
|
69
|
+
if (result.error === 'frozen') {
|
|
70
|
+
console.log(chalk.red(`✗ plan-check: o gate plan de ${featureName} está aprovado e o plan.md mudou fora da errata — checks.json congelado.`));
|
|
71
|
+
console.log(chalk.gray(' Fato errado sobre o código: acrescente em ## Errata (não muda o planHash).'));
|
|
72
|
+
console.log(chalk.gray(` Mudança real de escopo: morph-spec unapprove ${featureName} plan, rode plan-check e re-apresente o gate.`));
|
|
73
|
+
} else if (!result.ok) {
|
|
74
|
+
console.log(chalk.red(`✗ plan-check: ${result.errors.length} erro(s) em ${result.plan} — checks.json não foi gravado`));
|
|
75
|
+
} else {
|
|
76
|
+
console.log(chalk.green(`✓ plan-check: ${join('2-plan', 'checks.json')} gravado (${result.planHash.slice(0, 12)})`));
|
|
77
|
+
result.groups.forEach((g, i) => console.log(chalk.gray(` onda ${i + 1}: ${g.join(', ')}${g.length > 1 ? ' (paralelo)' : ''}`)));
|
|
78
|
+
reportDotnetIgnores(gitignoreAdded);
|
|
79
|
+
}
|
|
80
|
+
process.exit(result.exit);
|
|
81
|
+
}
|
|
@@ -15,95 +15,43 @@
|
|
|
15
15
|
import fs from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import chalk from 'chalk';
|
|
18
|
-
import {
|
|
18
|
+
import { parsePlan } from '../../lib/plan/plan-parser.js';
|
|
19
|
+
import { knownPersonas, planHash, validatePlan } from '../../lib/plan/checks.js';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
|
-
* Expected sections per output
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Expected sections per markdown output — matched against *section markers*
|
|
23
|
+
* (headings and line-start bold labels), not whole-file substrings; see
|
|
24
|
+
* `extractSectionMarkers`. plan.md is judged by the plan parser instead.
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* recommended → WARNING if absent. Skills emit these, but a lean doc may skip.
|
|
26
|
+
* required → ERROR if absent.
|
|
27
|
+
* recommended → WARNING if absent.
|
|
28
28
|
* optional → silent. Tracked when present, never penalized.
|
|
29
29
|
*
|
|
30
|
-
* Each entry is a string or an array of acceptable
|
|
31
|
-
* form carries bilingual aliases (pt-BR section names the skills emit AND their
|
|
32
|
-
* English equivalents in the canonical templates), e.g. ['Escopo', 'Scope'].
|
|
33
|
-
*
|
|
34
|
-
* Matching is done against *section markers* (headings and line-start bold
|
|
35
|
-
* labels), not whole-file substrings — see `extractSectionMarkers`.
|
|
30
|
+
* Each entry is a string or an array of acceptable aliases (any-of).
|
|
36
31
|
*/
|
|
37
32
|
const OUTPUT_SECTIONS = {
|
|
38
|
-
|
|
39
|
-
// Stack, Riscos e dependências, Compliance de stack, Conflitos com o acervo.
|
|
40
|
-
//
|
|
41
|
-
// "Critérios de aceite" is now OPTIONAL, not required: the behaviour layer
|
|
42
|
-
// moved to the use cases (2-plan/usecases/), whose post-conditions must be
|
|
43
|
-
// observable in a test, and morph-brainstorming no longer emits the section.
|
|
44
|
-
// `optional` — not `recommended` — precisely because the skill never emits it
|
|
45
|
-
// anymore: a warning on every new proposal would be systematic noise. Legacy
|
|
46
|
-
// proposals that DO carry the section keep validating exactly as before.
|
|
47
|
-
//
|
|
48
|
-
// "Conflitos com o acervo" is recommended: morph-brainstorming always emits
|
|
49
|
-
// it ("nenhum" included), so its absence means the acervo check never ran.
|
|
50
|
-
// Not required — proposals written before the section existed stay valid.
|
|
51
|
-
'proposal.md': {
|
|
52
|
-
required: [
|
|
53
|
-
['Problema', 'Problem'],
|
|
54
|
-
['Escopo', 'Scope'],
|
|
55
|
-
],
|
|
56
|
-
recommended: [
|
|
57
|
-
['Riscos', 'Risks'],
|
|
58
|
-
['Conflitos', 'Conflicts'],
|
|
59
|
-
],
|
|
60
|
-
optional: [
|
|
61
|
-
['Critérios de aceite', 'Critério', 'Aceite', 'Acceptance Criteria', 'Acceptance', 'Success Criteria', 'Success Metrics'],
|
|
62
|
-
'Stack',
|
|
63
|
-
['Usuários', 'Usuarios', 'Users', 'User Journey', 'User Stories', 'Fluxos'],
|
|
64
|
-
['Dependências', 'Dependencias', 'Dependencies'],
|
|
65
|
-
['Compliance'],
|
|
66
|
-
['Overview', 'Visão geral', 'Visao geral', 'Resumo'],
|
|
67
|
-
],
|
|
68
|
-
},
|
|
69
|
-
// morph-plan (spec.md) emits: Arquitetura da solução, Edge cases,
|
|
70
|
-
// Integrações externas e contratos esperados.
|
|
71
|
-
'spec.md': {
|
|
33
|
+
'verifier.md': {
|
|
72
34
|
required: [
|
|
73
|
-
['
|
|
74
|
-
['Integrações', 'Integracoes', 'Integrations', 'Contratos', 'Contracts', 'Data Model', 'Modelo de Dados'],
|
|
35
|
+
['Verdict', 'Veredito'],
|
|
75
36
|
],
|
|
76
37
|
recommended: [
|
|
77
|
-
['
|
|
78
|
-
|
|
79
|
-
optional: [
|
|
80
|
-
['Overview', 'Visão geral', 'Resumo'],
|
|
81
|
-
['Requirements', 'Requisitos'],
|
|
82
|
-
['Security', 'Segurança', 'Seguranca'],
|
|
83
|
-
['Testing', 'Testes', 'Estratégia de Testes'],
|
|
84
|
-
['Performance', 'Desempenho'],
|
|
38
|
+
['Bloqueadores', 'Bloqueantes', 'Blockers'],
|
|
39
|
+
'Checks',
|
|
85
40
|
],
|
|
41
|
+
optional: [],
|
|
86
42
|
},
|
|
87
|
-
|
|
88
|
-
// `**Status:** / **Context:** / **Decision:**` line-start bold labels.
|
|
89
|
-
'decisions.md': {
|
|
43
|
+
'reviewer.md': {
|
|
90
44
|
required: [
|
|
91
|
-
'
|
|
92
|
-
'Status',
|
|
93
|
-
['Contexto', 'Context'],
|
|
94
|
-
['Decisão', 'Decisao', 'Decision'],
|
|
45
|
+
['Verdict', 'Veredito'],
|
|
95
46
|
],
|
|
96
47
|
recommended: [
|
|
97
|
-
['
|
|
98
|
-
],
|
|
99
|
-
optional: [
|
|
100
|
-
['Alternativas', 'Alternatives'],
|
|
101
|
-
['Custos', 'Costs'],
|
|
48
|
+
['Bloqueadores', 'Bloqueantes', 'Blockers'],
|
|
102
49
|
],
|
|
50
|
+
optional: [],
|
|
103
51
|
},
|
|
104
|
-
'
|
|
52
|
+
'security.md': {
|
|
105
53
|
required: [
|
|
106
|
-
['
|
|
54
|
+
['Verdict', 'Veredito'],
|
|
107
55
|
],
|
|
108
56
|
recommended: [
|
|
109
57
|
['Bloqueadores', 'Bloqueantes', 'Blockers'],
|
|
@@ -116,28 +64,18 @@ const OUTPUT_SECTIONS = {
|
|
|
116
64
|
* Phase-to-output mapping (v5 NLH)
|
|
117
65
|
*/
|
|
118
66
|
const PHASE_OUTPUTS = {
|
|
119
|
-
|
|
67
|
+
plan: ['plan.md', 'checks.json'],
|
|
120
68
|
uiux: ['design-system.md', 'mockups.md', 'components.md', 'flows.md'],
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
review: ['review-report.md'],
|
|
69
|
+
implement: [],
|
|
70
|
+
review: ['verifier.md', 'reviewer.md'],
|
|
124
71
|
};
|
|
125
72
|
|
|
126
|
-
// Work types whose plan phase (see src/core/work-types.js) skips proposal.md/
|
|
127
|
-
// spec.md — chore and hotfix stamp proposal+plan gates at birth
|
|
128
|
-
// (policy-approved, no human deliberation over those docs) and go straight to
|
|
129
|
-
// tasks.json. Validating the full plan doc set against a feature that was
|
|
130
|
-
// never asked to write those docs would be a false failure, not a real gap.
|
|
131
|
-
const LEAN_PLAN_WORK_TYPES = ['chore', 'hotfix'];
|
|
132
|
-
const LEAN_PLAN_OUTPUTS = ['tasks.json'];
|
|
133
|
-
|
|
134
73
|
/**
|
|
135
74
|
* Phase-to-folder mapping (v5 filesystem convention)
|
|
136
75
|
*/
|
|
137
76
|
const PHASE_FOLDERS = {
|
|
138
|
-
proposal: '0-proposal',
|
|
139
|
-
uiux: '1-design',
|
|
140
77
|
plan: '2-plan',
|
|
78
|
+
uiux: '1-design',
|
|
141
79
|
implement: '3-implement',
|
|
142
80
|
review: '4-review',
|
|
143
81
|
};
|
|
@@ -149,11 +87,10 @@ const PHASE_FOLDERS = {
|
|
|
149
87
|
* - a markdown heading → `## Arquitetura da solução`
|
|
150
88
|
* - a line-start bold label → `**Status:** Aceito`
|
|
151
89
|
*
|
|
152
|
-
* Bold labels are included on purpose (deviation from heading-only):
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* accidental-prose-match problem that whole-file substring matching had.
|
|
90
|
+
* Bold labels are included on purpose (deviation from heading-only): reports
|
|
91
|
+
* write `**Verdict:**` as a line-start bold label, not a heading. They are
|
|
92
|
+
* structural, not prose, so this avoids the accidental-prose-match problem that
|
|
93
|
+
* whole-file substring matching had.
|
|
157
94
|
*
|
|
158
95
|
* Returns lowercased marker texts.
|
|
159
96
|
*/
|
|
@@ -197,9 +134,12 @@ function validateOutput(featurePath, outputFile) {
|
|
|
197
134
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
198
135
|
result.lineCount = content.split('\n').length;
|
|
199
136
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
137
|
+
if (outputFile === 'plan.md') {
|
|
138
|
+
validatePlanOutput(content, featurePath, result);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
if (outputFile === 'checks.json') {
|
|
142
|
+
validateChecksOutput(content, featurePath, result);
|
|
203
143
|
return result;
|
|
204
144
|
}
|
|
205
145
|
|
|
@@ -209,7 +149,6 @@ function validateOutput(featurePath, outputFile) {
|
|
|
209
149
|
return result;
|
|
210
150
|
}
|
|
211
151
|
|
|
212
|
-
// v5: only markdown outputs — contracts moved into spec.md
|
|
213
152
|
validateMarkdownSections(content, sections, result);
|
|
214
153
|
|
|
215
154
|
// Size warnings
|
|
@@ -221,27 +160,33 @@ function validateOutput(featurePath, outputFile) {
|
|
|
221
160
|
}
|
|
222
161
|
|
|
223
162
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
163
|
+
* plan.md is judged by the same parser and rules as `morph-spec plan-check`.
|
|
164
|
+
* `planDir` is `.morph/features/{f}/2-plan`, so the project is four levels up.
|
|
226
165
|
*/
|
|
227
|
-
function
|
|
228
|
-
|
|
166
|
+
function validatePlanOutput(content, planDir, result) {
|
|
167
|
+
const plan = parsePlan(content);
|
|
168
|
+
const { errors, warnings } = validatePlan(plan, { personas: knownPersonas(path.resolve(planDir, '..', '..', '..', '..')) });
|
|
169
|
+
result.errors.push(...errors);
|
|
170
|
+
result.warnings.push(...warnings);
|
|
171
|
+
result.sections.found.push(...plan.sections.filter((x) => x.key).map((x) => x.name));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** checks.json must parse and carry the hash of the plan.md beside it. */
|
|
175
|
+
function validateChecksOutput(content, planDir, result) {
|
|
176
|
+
let doc;
|
|
229
177
|
try {
|
|
230
|
-
|
|
178
|
+
doc = JSON.parse(content);
|
|
231
179
|
} catch (err) {
|
|
232
|
-
result.errors.push(`
|
|
180
|
+
result.errors.push(`checks.json is not valid JSON: ${err.message}`);
|
|
233
181
|
return;
|
|
234
182
|
}
|
|
235
|
-
const
|
|
236
|
-
if (!
|
|
237
|
-
|
|
183
|
+
const planPath = path.join(planDir, 'plan.md');
|
|
184
|
+
if (!fs.existsSync(planPath)) return;
|
|
185
|
+
if (doc?.planHash !== planHash(fs.readFileSync(planPath, 'utf8'))) {
|
|
186
|
+
result.errors.push('checks.json is stale — plan.md changed after it was generated; run `morph-spec plan-check`');
|
|
238
187
|
return;
|
|
239
188
|
}
|
|
240
|
-
|
|
241
|
-
result.errors.push('tasks.json has no task entries');
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
result.sections.found.push(`${data.tasks.length} task(s)`);
|
|
189
|
+
result.sections.found.push(`${doc.checks?.length ?? 0} check(s), ${doc.slices?.length ?? 0} slice(s)`);
|
|
245
190
|
}
|
|
246
191
|
|
|
247
192
|
/**
|
|
@@ -294,16 +239,11 @@ function validateMarkdownSections(content, sections, result) {
|
|
|
294
239
|
*
|
|
295
240
|
* @param {string} featurePath Absolute path to the feature directory.
|
|
296
241
|
* @param {string|null} phase Phase key, or null to validate everything.
|
|
297
|
-
* @param {string|undefined} workType Feature's workType (see src/core/work-types.js).
|
|
298
|
-
* When `phase === 'plan'` and workType is 'chore'/'hotfix', only tasks.json
|
|
299
|
-
* is required (see LEAN_PLAN_WORK_TYPES above).
|
|
300
242
|
* @returns {{ outputFiles?: Array<{file:string, dir:string}>, error?: string }}
|
|
301
243
|
*/
|
|
302
|
-
function resolveOutputFiles(featurePath, phase
|
|
244
|
+
function resolveOutputFiles(featurePath, phase) {
|
|
303
245
|
if (phase) {
|
|
304
|
-
const files =
|
|
305
|
-
? LEAN_PLAN_OUTPUTS
|
|
306
|
-
: PHASE_OUTPUTS[phase];
|
|
246
|
+
const files = PHASE_OUTPUTS[phase];
|
|
307
247
|
if (!files) return { error: `Unknown phase: ${phase}` };
|
|
308
248
|
const phaseFolder = PHASE_FOLDERS[phase];
|
|
309
249
|
const searchPath = phaseFolder ? path.join(featurePath, phaseFolder) : featurePath;
|
|
@@ -312,7 +252,7 @@ function resolveOutputFiles(featurePath, phase, workType) {
|
|
|
312
252
|
|
|
313
253
|
// Validate every existing output across the feature root + phase folders.
|
|
314
254
|
const outputFiles = [];
|
|
315
|
-
const isOutput = name => name.endsWith('.md') || name.endsWith('.cs') || name === '
|
|
255
|
+
const isOutput = name => name.endsWith('.md') || name.endsWith('.cs') || name === 'checks.json';
|
|
316
256
|
const entries = fs.readdirSync(featurePath, { withFileTypes: true });
|
|
317
257
|
for (const entry of entries) {
|
|
318
258
|
if (entry.isDirectory()) {
|
|
@@ -331,7 +271,7 @@ function resolveOutputFiles(featurePath, phase, workType) {
|
|
|
331
271
|
* Pure validation core — no console output, no process.exit.
|
|
332
272
|
*
|
|
333
273
|
* @param {string} featurePath Absolute path to the feature directory.
|
|
334
|
-
* @param {{ phase?: string
|
|
274
|
+
* @param {{ phase?: string }} [options]
|
|
335
275
|
* @returns {{
|
|
336
276
|
* feature: string, phase: string, valid: boolean, error?: string,
|
|
337
277
|
* totalErrors: number, totalWarnings: number, totalFound: number,
|
|
@@ -340,10 +280,9 @@ function resolveOutputFiles(featurePath, phase, workType) {
|
|
|
340
280
|
*/
|
|
341
281
|
export function runFeatureValidation(featurePath, options = {}) {
|
|
342
282
|
const phase = options.phase || null;
|
|
343
|
-
const workType = options.workType || null;
|
|
344
283
|
const feature = path.basename(featurePath);
|
|
345
284
|
|
|
346
|
-
const { outputFiles, error } = resolveOutputFiles(featurePath, phase
|
|
285
|
+
const { outputFiles, error } = resolveOutputFiles(featurePath, phase);
|
|
347
286
|
if (error) {
|
|
348
287
|
return {
|
|
349
288
|
feature,
|
|
@@ -397,23 +336,9 @@ export async function validateFeatureCommand(featureName, options = {}) {
|
|
|
397
336
|
process.exit(1);
|
|
398
337
|
}
|
|
399
338
|
|
|
400
|
-
|
|
401
|
-
// state-manager's getFeature/loadState) so a broken/missing root state.json
|
|
402
|
-
// elsewhere in the project can never make `validate` itself throw — it just
|
|
403
|
-
// falls back to the full (non-lean) output set, same as today.
|
|
404
|
-
let workType;
|
|
405
|
-
try {
|
|
406
|
-
const fjPath = path.join(featurePath, 'feature.json');
|
|
407
|
-
if (fs.existsSync(fjPath)) {
|
|
408
|
-
workType = JSON.parse(fs.readFileSync(fjPath, 'utf8')).workType;
|
|
409
|
-
}
|
|
410
|
-
} catch {
|
|
411
|
-
workType = undefined;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const validation = runFeatureValidation(featurePath, { phase, workType });
|
|
339
|
+
const validation = runFeatureValidation(featurePath, { phase });
|
|
415
340
|
const { results, totalErrors, totalWarnings, totalFound } = validation;
|
|
416
|
-
const schemaWarnings = [];
|
|
341
|
+
const schemaWarnings = [];
|
|
417
342
|
|
|
418
343
|
// JSON output mode
|
|
419
344
|
if (options.json) {
|