@polymorphism-tech/morph-spec 8.37.1 → 8.40.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 +37 -13
- package/README.md +2 -2
- package/bin/morph-spec.js +75 -49
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +6 -5
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +96 -36
- package/framework/docs/CLI.md +54 -17
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/mcp/registry.json +33 -2
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +10 -4
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -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 +40 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +44 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +73 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +39 -13
- package/framework/standards/STANDARDS.json +189 -14
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +100 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/feature/decisions.md +124 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +85 -15
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +30 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +63 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +4 -1
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +47 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +200 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +31 -22
- package/src/utils/agents-installer.js +338 -24
- package/src/utils/banner.js +1 -1
- package/src/utils/hooks-installer.js +75 -15
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory Installer — populates Claude Code persistent memory during morph-init
|
|
2
|
+
* Memory Installer — populates Claude Code persistent memory during `morph-spec init`.
|
|
3
3
|
*
|
|
4
4
|
* Writes two memory files (project + reference) plus an entry in MEMORY.md
|
|
5
5
|
* so future Claude Code sessions enter pre-contextualized about the
|
|
@@ -67,7 +67,6 @@ metadata:
|
|
|
67
67
|
- When user mentions a feature, check \`.morph/features/\` first.
|
|
68
68
|
- When user types \`/morph-proposal\`, invoke \`Skill(morph-proposal)\` directly.
|
|
69
69
|
- In \`trust: "auto"\`, gates auto-pass; only AskUserQuestion on risk signals.
|
|
70
|
-
- Prefer morph-* skills over superpowers:* equivalents in this project.
|
|
71
70
|
`;
|
|
72
71
|
}
|
|
73
72
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the morph-spec projects on this machine are — the list the dashboard's
|
|
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
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
19
|
+
import { homedir, tmpdir } from 'os';
|
|
20
|
+
import { join, resolve, basename, dirname } from 'path';
|
|
21
|
+
import { atomicWriteJson } from '../../core/state/state-manager.js';
|
|
22
|
+
import { canonicalPath, isInside } from '../tasks/test-runner.js';
|
|
23
|
+
import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
|
|
24
|
+
|
|
25
|
+
export const REGISTRY_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
export function registryPath() {
|
|
28
|
+
return process.env.MORPH_PROJECTS_REGISTRY || join(homedir(), '.morph-spec', 'projects.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** One spelling per directory: canonical, forward slashes (the registry is read back on any OS). */
|
|
32
|
+
export function projectKey(dir) {
|
|
33
|
+
return canonicalPath(resolve(dir)).replace(/\\/g, '/');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sameDir = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
|
|
37
|
+
|
|
38
|
+
/** A folder is a morph-spec project when morph-spec installed its config there. */
|
|
39
|
+
export function isMorphProject(dir) {
|
|
40
|
+
return existsSync(join(dir, '.morph', 'config', 'config.json'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A worktree checkout is part of its repo's project, never a project of its own. */
|
|
44
|
+
function insideWorktreeContainer(dir) {
|
|
45
|
+
const parent = basename(dirname(dir));
|
|
46
|
+
return WORKTREE_DIR_NAMES.includes(parent);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function projectName(dir) {
|
|
50
|
+
try {
|
|
51
|
+
const config = JSON.parse(readFileSync(join(dir, '.morph', 'config', 'config.json'), 'utf8'));
|
|
52
|
+
const name = config?.project?.name;
|
|
53
|
+
// Some configs carry the project path as its name; the folder reads better.
|
|
54
|
+
return typeof name === 'string' && name.trim() && !/[\\/]/.test(name) ? name.trim() : basename(dir);
|
|
55
|
+
} catch {
|
|
56
|
+
return basename(dir);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The registered projects that still exist. A folder that was deleted or lost
|
|
62
|
+
* its `.morph/` drops out on read — the file is rewritten on the next write.
|
|
63
|
+
*
|
|
64
|
+
* @returns {{ version: number, projects: Array<{ dir: string, name: string, addedAt: string, lastSeenAt: string, source: string }> }}
|
|
65
|
+
*/
|
|
66
|
+
export function readRegistry() {
|
|
67
|
+
let data = null;
|
|
68
|
+
try {
|
|
69
|
+
const path = registryPath();
|
|
70
|
+
if (existsSync(path)) data = JSON.parse(readFileSync(path, 'utf8'));
|
|
71
|
+
} catch { /* unreadable registry reads as empty */ }
|
|
72
|
+
const projects = Array.isArray(data?.projects) ? data.projects : [];
|
|
73
|
+
return {
|
|
74
|
+
version: REGISTRY_VERSION,
|
|
75
|
+
projects: projects.filter((p) => typeof p?.dir === 'string' && isMorphProject(p.dir)),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function write(projects) {
|
|
80
|
+
atomicWriteJson(registryPath(), { version: REGISTRY_VERSION, projects });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Note that `dir` is a morph-spec project. Idempotent; refreshes `lastSeenAt`.
|
|
85
|
+
* Never throws.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} dir
|
|
88
|
+
* @param {'init'|'update'|'dashboard'|'manual'} source
|
|
89
|
+
* @param {{ tempRoot?: string }} [opts] - test seam: the folder whose
|
|
90
|
+
* projects are never registered (the system temp dir)
|
|
91
|
+
* @returns {boolean} true when the project is in the registry afterwards
|
|
92
|
+
*/
|
|
93
|
+
export function registerProject(dir, source, { tempRoot = tmpdir() } = {}) {
|
|
94
|
+
try {
|
|
95
|
+
const key = projectKey(dir);
|
|
96
|
+
if (isInside(canonicalPath(key), canonicalPath(tempRoot))) return false;
|
|
97
|
+
if (!isMorphProject(key) || insideWorktreeContainer(key)) return false;
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const { projects } = readRegistry();
|
|
100
|
+
const found = projects.find((p) => sameDir(p.dir, key));
|
|
101
|
+
if (found) {
|
|
102
|
+
found.lastSeenAt = now;
|
|
103
|
+
found.name = projectName(key);
|
|
104
|
+
} else {
|
|
105
|
+
projects.push({ dir: key, name: projectName(key), addedAt: now, lastSeenAt: now, source });
|
|
106
|
+
}
|
|
107
|
+
write(projects);
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Take a project off the list. Its files are not touched. Never throws. */
|
|
115
|
+
export function unregisterProject(dir) {
|
|
116
|
+
try {
|
|
117
|
+
const key = projectKey(dir);
|
|
118
|
+
const { projects } = readRegistry();
|
|
119
|
+
const kept = projects.filter((p) => !sameDir(p.dir, key));
|
|
120
|
+
if (kept.length !== projects.length) write(kept);
|
|
121
|
+
return kept.length !== projects.length;
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* morph-spec projects directly under each root that are not registered yet —
|
|
129
|
+
* the portfolio offers them, the person decides. One level deep: a repos
|
|
130
|
+
* folder is the expected root, and a deeper walk would enter node_modules and
|
|
131
|
+
* worktrees for nothing.
|
|
132
|
+
*
|
|
133
|
+
* @param {string[]} roots
|
|
134
|
+
* @returns {Array<{ dir: string, name: string }>}
|
|
135
|
+
*/
|
|
136
|
+
export function discoverProjects(roots) {
|
|
137
|
+
const registered = readRegistry().projects.map((p) => p.dir);
|
|
138
|
+
const out = [];
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
for (const root of roots) {
|
|
141
|
+
let entries = [];
|
|
142
|
+
try { entries = readdirSync(root, { withFileTypes: true }); } catch { continue; }
|
|
143
|
+
for (const e of entries) {
|
|
144
|
+
if (!e.isDirectory() || e.name.startsWith('.') || e.name === 'node_modules') continue;
|
|
145
|
+
const dir = join(root, e.name);
|
|
146
|
+
if (!isMorphProject(dir)) continue;
|
|
147
|
+
const key = projectKey(dir);
|
|
148
|
+
if (seen.has(key.toLowerCase()) || registered.some((r) => sameDir(r, key))) continue;
|
|
149
|
+
seen.add(key.toLowerCase());
|
|
150
|
+
out.push({ dir: key, name: projectName(key) });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
154
|
+
}
|
package/src/lib/risk-detector.js
CHANGED
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
* evaluator-report.md. No ship, relatório SEM a seção `## Mutações` numa
|
|
24
24
|
* feature com código de produção pausa: score é opinião, mutação é prova
|
|
25
25
|
* - mediaClaims + gate: object — { section, score, featureGeneratesMedia }
|
|
26
|
+
* - screenChecklist + gate: object — { mockups, legacy, screens, problems, exempt?,
|
|
27
|
+
* section?, uncovered? } derivado do 1-design/mockups.md. No uiux, checklist
|
|
28
|
+
* de tela incompleto pausa; no ship, item decidido sem a conferência do
|
|
29
|
+
* avaliador pausa. Mockup legado é isento nos dois, com a razão listada
|
|
26
30
|
* - evalScore: number — morph-eval score (pause if < 9)
|
|
27
31
|
* - taskCount: number — total tasks (pause if > 20)
|
|
28
32
|
*/
|
|
@@ -133,6 +137,35 @@ function e2eScopeRefusal(klass, scope, reason) {
|
|
|
133
137
|
+ ' rode `morph-spec verify {feature}` (sem task) antes do ship';
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
/** A razão gravada no sinal quando o checklist de tela isenta um mockup legado. */
|
|
141
|
+
export const LEGACY_MOCKUP_EXEMPTION = 'checklist dispensado: mockup no formato antigo';
|
|
142
|
+
|
|
143
|
+
/** Os dois primeiros itens de uma lista, e quantos ficaram de fora. */
|
|
144
|
+
function firstOf(items) {
|
|
145
|
+
const shown = items.slice(0, 2).join('; ');
|
|
146
|
+
return items.length > 2 ? `${shown} (+${items.length - 2})` : shown;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Por que o checklist de tela pausa o Gate uiux, ou null. Exportada porque o
|
|
151
|
+
* `approve`/`advance` com assinatura `auto` recusam pela MESMA regra: uma
|
|
152
|
+
* aprovação automática que passasse onde o gate-check pausa seria o atalho.
|
|
153
|
+
*
|
|
154
|
+
* Mockup ausente (`mockups: false`) não conta — nem toda feature de UI tem
|
|
155
|
+
* tela —, e o legado chega sem problemas: a auditoria o isenta.
|
|
156
|
+
*
|
|
157
|
+
* @param {object|undefined} screenChecklist - o sinal de `collectScreenChecklist`
|
|
158
|
+
* @returns {string|null}
|
|
159
|
+
*/
|
|
160
|
+
export function uiuxChecklistPause(screenChecklist) {
|
|
161
|
+
if (screenChecklist?.mockups !== true) return null;
|
|
162
|
+
const problems = screenChecklist.problems ?? [];
|
|
163
|
+
if (problems.length === 0) return null;
|
|
164
|
+
return `checklist de tela incompleto em 1-design/mockups.md: ${firstOf(problems)}`
|
|
165
|
+
+ ' — volume, paginação, ordenação, filtro, os três estados, mobile e abas por tela'
|
|
166
|
+
+ ' ("não se aplica" vale, se escrito)';
|
|
167
|
+
}
|
|
168
|
+
|
|
136
169
|
/**
|
|
137
170
|
* @param {object} config - .morph/config/config.json contents (uses `trust`)
|
|
138
171
|
* @param {object} signals
|
|
@@ -362,6 +395,36 @@ export function shouldPauseGate(config, signals) {
|
|
|
362
395
|
};
|
|
363
396
|
}
|
|
364
397
|
|
|
398
|
+
// No fecho do DESIGN: o mockup declara telas sem o checklist de tela
|
|
399
|
+
// completo. É ele que faz paginação, ordenação, filtro e os três estados
|
|
400
|
+
// virarem `doneCriteria` — sem ele, uma lista de 2 000 linhas é aprovada
|
|
401
|
+
// listando tudo, e o implementador entrega exatamente o critério.
|
|
402
|
+
if (signals.gate === 'uiux') {
|
|
403
|
+
const reason = uiuxChecklistPause(signals.screenChecklist);
|
|
404
|
+
if (reason) return { pause: true, reason };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// No SHIP, depois da cláusula de mídia e pela mesma disciplina: o design
|
|
408
|
+
// decidiu itens de tela e o avaliador não registrou a conferência deles
|
|
409
|
+
// contra a tela entregue. Cobra-se o item DEVIDO (`uncovered`): tela toda
|
|
410
|
+
// "não se aplica", mockup legado ou ausente não devem nada.
|
|
411
|
+
if (signals.gate === 'review' && (signals.screenChecklist?.uncovered?.length ?? 0) > 0) {
|
|
412
|
+
const { section, uncovered } = signals.screenChecklist;
|
|
413
|
+
if (section !== true) {
|
|
414
|
+
return {
|
|
415
|
+
pause: true,
|
|
416
|
+
reason: `relatório do avaliador sem a seção \`## Checklist de tela\` — o design decidiu ${firstOf(uncovered)}`
|
|
417
|
+
+ ' e a avaliação não registrou a conferência contra a tela entregue'
|
|
418
|
+
+ ' (rubrica evals/rubrics/contracts.md, "Contrato de tela")',
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
pause: true,
|
|
423
|
+
reason: `a seção \`## Checklist de tela\` não confere ${firstOf(uncovered)}`
|
|
424
|
+
+ ' — uma linha por item decidido de cada tela, mesmo quando não verificável',
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
365
428
|
if (typeof signals.evalScore === 'number' && signals.evalScore < MIN_EVAL_SCORE) {
|
|
366
429
|
return {
|
|
367
430
|
pause: true,
|
|
@@ -455,6 +518,33 @@ export function getRiskSignals(signals) {
|
|
|
455
518
|
}
|
|
456
519
|
}
|
|
457
520
|
|
|
521
|
+
// Listado mesmo quando NÃO pausa: a isenção (sem mockup, legado, nada
|
|
522
|
+
// devido) e o checklist conferido precisam ser distinguíveis de uma omissão.
|
|
523
|
+
if ((signals.gate === 'uiux' || signals.gate === 'review') && signals.screenChecklist) {
|
|
524
|
+
const { mockups, legacy, exempt, screens = [], problems = [], section, uncovered = [] } = signals.screenChecklist;
|
|
525
|
+
if (!mockups) {
|
|
526
|
+
out.push('sem 1-design/mockups.md — checklist de tela não se aplica');
|
|
527
|
+
} else if (legacy === true) {
|
|
528
|
+
out.push(exempt ?? LEGACY_MOCKUP_EXEMPTION);
|
|
529
|
+
} else if (signals.gate === 'uiux') {
|
|
530
|
+
out.push(problems.length > 0
|
|
531
|
+
? `checklist de tela incompleto (${problems.length} problema(s))`
|
|
532
|
+
: `checklist de tela completo (${screens.length} tela(s))`);
|
|
533
|
+
} else if (uncovered.length > 0) {
|
|
534
|
+
out.push(section !== true
|
|
535
|
+
? 'relatório sem a seção `## Checklist de tela`'
|
|
536
|
+
: `checklist de tela sem conferência em ${uncovered.length} tela(s)`);
|
|
537
|
+
} else if (screens.length === 0) {
|
|
538
|
+
// O design passou pela uiux sem tela nenhuma: nada a cobrar no ship, mas
|
|
539
|
+
// não pode ler igual a uma tela toda "não se aplica".
|
|
540
|
+
out.push(`checklist de tela: ${firstOf(problems)}`);
|
|
541
|
+
} else if (section === true) {
|
|
542
|
+
out.push(`checklist de tela conferido (${screens.length} tela(s))`);
|
|
543
|
+
} else {
|
|
544
|
+
out.push(`checklist de tela sem item a conferir (${screens.length} tela(s))`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
458
548
|
return out;
|
|
459
549
|
}
|
|
460
550
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes every reader and writer of the acervo agrees on — one copy each.
|
|
3
|
+
* A second copy of any of these is how two readers start disagreeing about the
|
|
4
|
+
* same file: a use case one of them sees and the other does not, a table row
|
|
5
|
+
* one of them splits differently.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync } from 'fs';
|
|
9
|
+
import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
|
|
10
|
+
|
|
11
|
+
/** A use-case id: `UC-07`. */
|
|
12
|
+
export const UC_ID_RE = /^UC-\d+$/;
|
|
13
|
+
|
|
14
|
+
/** A use case no longer holds as the system's behaviour (or holds only in its `scope`). */
|
|
15
|
+
export const SUPERSEDED = 'superseded';
|
|
16
|
+
|
|
17
|
+
const UC_FILENAME_RE = new RegExp(OUTPUT_SCHEMA.usecases.filenamePattern);
|
|
18
|
+
|
|
19
|
+
/** `UC-07-listar-veterinarios.md` → `UC-07`. Null when the name isn't a use case. */
|
|
20
|
+
export function useCaseId(filename) {
|
|
21
|
+
const m = /^(UC-\d+)-/.exec(filename);
|
|
22
|
+
return m ? m[1] : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The name the promotion carries: `UC-{nn}-{slug}.md`, slug in lowercase kebab. */
|
|
26
|
+
export const isCanonicalUseCaseName = (name) => UC_FILENAME_RE.test(name);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Every use case in `dir`, grouped by id. By default only the canonical names
|
|
30
|
+
* — the ones the promotion ever carries. `canonicalOnly: false` also lists any
|
|
31
|
+
* `UC-{nn}-….md`, for the plan-time check that has always accepted them and
|
|
32
|
+
* now warns instead. An id with more than one file is a broken acervo, and
|
|
33
|
+
* readers must be able to see it rather than silently pick one.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} dir
|
|
36
|
+
* @param {{ canonicalOnly?: boolean }} [options]
|
|
37
|
+
* @returns {Map<string, string[]>} id → filenames, sorted; empty when the folder is missing or unreadable
|
|
38
|
+
*/
|
|
39
|
+
export function useCaseFilesById(dir, { canonicalOnly = true } = {}) {
|
|
40
|
+
const byId = new Map();
|
|
41
|
+
try {
|
|
42
|
+
if (!existsSync(dir)) return byId;
|
|
43
|
+
for (const name of readdirSync(dir).sort()) {
|
|
44
|
+
const id = useCaseId(name);
|
|
45
|
+
if (!id || !name.endsWith('.md')) continue;
|
|
46
|
+
if (canonicalOnly && !isCanonicalUseCaseName(name)) continue;
|
|
47
|
+
if (!byId.has(id)) byId.set(id, []);
|
|
48
|
+
byId.get(id).push(name);
|
|
49
|
+
}
|
|
50
|
+
} catch { /* unreadable — nothing to see */ }
|
|
51
|
+
return byId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A markdown table separator row: `|---|:--:|`, with or without the outer pipes. */
|
|
55
|
+
export const SEPARATOR_ROW_RE = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/;
|
|
56
|
+
|
|
57
|
+
/** A line that belongs to a table: it carries an unescaped pipe (GFM does not require the leading one). */
|
|
58
|
+
export const isTableLine = (line) => /(?<!\\)\|/.test(line);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Split a table row on unescaped pipes, outer pipes optional; `\|` inside a
|
|
62
|
+
* cell is a literal pipe.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} row
|
|
65
|
+
* @returns {string[]} trimmed cells
|
|
66
|
+
*/
|
|
67
|
+
export function tableCells(row) {
|
|
68
|
+
const inner = row.trim().replace(/^\|/, '').replace(/(?<!\\)\|$/, '');
|
|
69
|
+
return inner.split(/(?<!\\)\|/).map((c) => c.replace(/\\\|/g, '|').trim());
|
|
70
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle of the project's acervo: a use case stops being the current
|
|
3
|
+
* behaviour when a later feature supersedes it.
|
|
4
|
+
*
|
|
5
|
+
* status: superseded
|
|
6
|
+
* superseded_by: UC-589
|
|
7
|
+
* scope: "vitrine pública, fora de `.morph-admin-shell`" ← only when partial
|
|
8
|
+
*
|
|
9
|
+
* Three roles live here:
|
|
10
|
+
* - the ONE rule for that frontmatter (`supersessionProblems`) — the check
|
|
11
|
+
* reads it, and the writer below only ever produces what it accepts;
|
|
12
|
+
* - the close-time writer (`applySpecRevocations`): what the feature's
|
|
13
|
+
* `spec.md` declared it revokes lands on `.morph/specs/usecases/` and on
|
|
14
|
+
* the ADR index, fail-open like the promotion it follows;
|
|
15
|
+
* - the plan-time check (`checkLifecycle`), shared by `dag` and `usecase
|
|
16
|
+
* check`, run while `spec.md` can still be fixed.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
|
|
22
|
+
import {
|
|
23
|
+
SPECS_USECASES_PATHSPEC,
|
|
24
|
+
hasFrontmatter,
|
|
25
|
+
readFrontmatterField,
|
|
26
|
+
removeFrontmatterField,
|
|
27
|
+
setFrontmatterField,
|
|
28
|
+
} from './usecase-promoter.js';
|
|
29
|
+
import { SUPERSEDED, UC_ID_RE, useCaseFilesById } from './acervo-format.js';
|
|
30
|
+
import { parseRevocations } from './revocations.js';
|
|
31
|
+
import { ADR_INDEX_PATHSPEC, readAdrIndex, updateAdrIndex } from './adr-index.js';
|
|
32
|
+
|
|
33
|
+
export { SUPERSEDED };
|
|
34
|
+
|
|
35
|
+
const UC = OUTPUT_SCHEMA.usecases;
|
|
36
|
+
const ACERVO_LABEL = SPECS_USECASES_PATHSPEC.replace(/\\/g, '/');
|
|
37
|
+
const ADR_INDEX_LABEL = `${ADR_INDEX_PATHSPEC.replace(/\\/g, '/')}/`;
|
|
38
|
+
|
|
39
|
+
/** @returns {{ status: string|null, supersededBy: string|null, scope: string|null }} */
|
|
40
|
+
export function lifecycleOf(content) {
|
|
41
|
+
return {
|
|
42
|
+
status: readFrontmatterField(content, 'status'),
|
|
43
|
+
supersededBy: readFrontmatterField(content, 'superseded_by'),
|
|
44
|
+
scope: readFrontmatterField(content, 'scope'),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The rule for a use case's supersession frontmatter.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} content
|
|
52
|
+
* @param {string} id - the use case's own id
|
|
53
|
+
* @returns {string[]} one line per problem; `[]` = well formed
|
|
54
|
+
*/
|
|
55
|
+
export function supersessionProblems(content, id) {
|
|
56
|
+
const { status, supersededBy, scope } = lifecycleOf(content);
|
|
57
|
+
const problems = [];
|
|
58
|
+
if (status === SUPERSEDED) {
|
|
59
|
+
if (!supersededBy) problems.push('status: superseded sem superseded_by');
|
|
60
|
+
else if (!UC_ID_RE.test(supersededBy)) problems.push(`superseded_by "${supersededBy}" não é um UC-{nn}`);
|
|
61
|
+
else if (supersededBy === id) problems.push('superseded_by aponta para o próprio use case');
|
|
62
|
+
} else if (supersededBy !== null || scope !== null) {
|
|
63
|
+
problems.push(`${supersededBy !== null ? 'superseded_by' : 'scope'} sem status: superseded`);
|
|
64
|
+
}
|
|
65
|
+
if (scope !== null && !String(scope).trim()) problems.push('scope vazio');
|
|
66
|
+
return problems;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readOrNull(path) {
|
|
70
|
+
try { return readFileSync(path, 'utf8'); } catch { return null; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const specPathOf = (featureDir) => join(featureDir, OUTPUT_SCHEMA.spec.phaseDir, OUTPUT_SCHEMA.spec.filename);
|
|
74
|
+
|
|
75
|
+
/** A finished feature cannot be re-closed: the fix is the next feature's revocation row. */
|
|
76
|
+
const FIX_NEXT_TIME = 'corrija a linha na seção de revogações do spec.md — a feature já arquivada não é reprocessada, ' +
|
|
77
|
+
'então a revogação corrigida entra no spec.md da próxima feature';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mark the acervo use cases the entries revoke. Runs after the promotion, so
|
|
81
|
+
* the acervo already holds every substitute this feature brought — except the
|
|
82
|
+
* ones the promotion refused, which only LOOK present (the file under that id
|
|
83
|
+
* belongs to another feature).
|
|
84
|
+
*
|
|
85
|
+
* @returns {{ superseded: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
|
|
86
|
+
* problems: Array<{id: string, reason: string, message: string}> }}
|
|
87
|
+
*/
|
|
88
|
+
function supersedeUseCases(cwd, entries, refusedIds) {
|
|
89
|
+
const superseded = [];
|
|
90
|
+
const problems = [];
|
|
91
|
+
if (!entries.length) return { superseded, problems };
|
|
92
|
+
|
|
93
|
+
const dir = join(cwd, SPECS_USECASES_PATHSPEC);
|
|
94
|
+
const files = useCaseFilesById(dir);
|
|
95
|
+
const problem = (id, reason, message) => problems.push({ id, reason, message });
|
|
96
|
+
|
|
97
|
+
for (const e of entries) {
|
|
98
|
+
const targets = files.get(e.id) ?? [];
|
|
99
|
+
if (targets.length === 0) {
|
|
100
|
+
problem(e.id, 'not-in-acervo', `${e.id} não está no acervo (${ACERVO_LABEL}/) — nada foi marcado; ${FIX_NEXT_TIME}`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (targets.length > 1) {
|
|
104
|
+
problem(e.id, 'duplicate-in-acervo',
|
|
105
|
+
`${e.id} tem ${targets.length} arquivos no acervo (${targets.join(', ')}) — nenhum foi marcado; o acervo precisa de um arquivo por UC`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (refusedIds.has(e.substitute)) {
|
|
109
|
+
problem(e.id, 'substitute-refused',
|
|
110
|
+
`o substituto ${e.substitute} de ${e.id} não foi promovido (colisão de numeração) — ${e.id} não foi marcado`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!files.has(e.substitute)) {
|
|
114
|
+
problem(e.id, 'substitute-not-found',
|
|
115
|
+
`o substituto ${e.substitute} de ${e.id} não está no acervo — ${e.id} não foi marcado; ${FIX_NEXT_TIME}`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const file = targets[0];
|
|
120
|
+
try {
|
|
121
|
+
const path = join(dir, file);
|
|
122
|
+
const before = readFileSync(path, 'utf8');
|
|
123
|
+
if (!hasFrontmatter(before)) {
|
|
124
|
+
problem(e.id, 'no-frontmatter', `${ACERVO_LABEL}/${file} não tem frontmatter — o status não foi aplicado`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
let next = setFrontmatterField(before, 'status', SUPERSEDED);
|
|
128
|
+
next = setFrontmatterField(next, 'superseded_by', e.substitute);
|
|
129
|
+
// JSON-quoted: free text with `: ` or a leading backtick breaks a bare YAML scalar.
|
|
130
|
+
next = e.scope
|
|
131
|
+
? setFrontmatterField(next, 'scope', JSON.stringify(e.scope))
|
|
132
|
+
: removeFrontmatterField(next, 'scope');
|
|
133
|
+
if (next !== before) writeFileSync(path, next, 'utf8');
|
|
134
|
+
superseded.push({ id: e.id, file, supersededBy: e.substitute, ...(e.scope ? { scope: e.scope } : {}) });
|
|
135
|
+
} catch {
|
|
136
|
+
problem(e.id, 'unwritable', `${ACERVO_LABEL}/${file} não pôde ser lido/gravado — o status não foi aplicado`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { superseded, problems };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Apply, at close time, what the feature's `spec.md` declared it revokes.
|
|
144
|
+
* Fail-open: nothing here may refuse the close; what did not apply comes back
|
|
145
|
+
* in `problems`, next to the row's own parse problems.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} cwd - project root that owns `.morph/specs/`
|
|
148
|
+
* @param {string} featureDir - the feature folder (already moved to the archive)
|
|
149
|
+
* @param {{ refused?: Array<{ id: string }> }} [promotion] - the promotion's result, run just before
|
|
150
|
+
* @returns {{ superseded: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
|
|
151
|
+
* adrIndex: ReturnType<typeof updateAdrIndex>|null,
|
|
152
|
+
* problems: Array<{id: string|null, reason: string, message: string}> }}
|
|
153
|
+
*/
|
|
154
|
+
export function applySpecRevocations(cwd, featureDir, promotion = {}) {
|
|
155
|
+
const out = { superseded: [], adrIndex: null, problems: [] };
|
|
156
|
+
|
|
157
|
+
const spec = readOrNull(specPathOf(featureDir));
|
|
158
|
+
const parsed = spec === null ? { entries: [], problems: [] } : parseRevocations(spec);
|
|
159
|
+
out.problems.push(...parsed.problems);
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const refusedIds = new Set((promotion.refused ?? []).map((r) => r.id));
|
|
163
|
+
const uc = supersedeUseCases(cwd, parsed.entries.filter((e) => e.kind === 'uc'), refusedIds);
|
|
164
|
+
out.superseded = uc.superseded;
|
|
165
|
+
out.problems.push(...uc.problems);
|
|
166
|
+
} catch { /* fail-open */ }
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
out.adrIndex = updateAdrIndex(cwd, featureDir, parsed.entries.filter((e) => e.kind === 'adr'));
|
|
170
|
+
out.problems.push(...out.adrIndex.problems);
|
|
171
|
+
} catch { /* fail-open */ }
|
|
172
|
+
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Plan-time check of a feature against the acervo's lifecycle.
|
|
178
|
+
*
|
|
179
|
+
* Warnings only, never errors: a superseded use case is a question for the
|
|
180
|
+
* plan, and the revocation rows are re-reported at close time anyway.
|
|
181
|
+
*
|
|
182
|
+
* @param {{ cwd: string, featureDir: string, tasksData?: object|null }} input
|
|
183
|
+
* @returns {{ warnings: string[] }}
|
|
184
|
+
*/
|
|
185
|
+
export function checkLifecycle({ cwd, featureDir, tasksData = null }) {
|
|
186
|
+
const warnings = [];
|
|
187
|
+
const acervoDir = join(cwd, SPECS_USECASES_PATHSPEC);
|
|
188
|
+
const ownDir = join(featureDir, UC.phaseDir, UC.dirname);
|
|
189
|
+
const acervo = useCaseFilesById(acervoDir);
|
|
190
|
+
const own = useCaseFilesById(ownDir);
|
|
191
|
+
|
|
192
|
+
const ownLife = new Map();
|
|
193
|
+
for (const [id, files] of own) {
|
|
194
|
+
const file = files[files.length - 1];
|
|
195
|
+
const content = readOrNull(join(ownDir, file));
|
|
196
|
+
if (content === null) continue;
|
|
197
|
+
ownLife.set(id, lifecycleOf(content));
|
|
198
|
+
for (const p of supersessionProblems(content, id)) warnings.push(`2-plan/usecases/${file}: ${p}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// The acervo can hold hundreds of use cases: read only the ones this plan touches.
|
|
202
|
+
const acervoLife = new Map();
|
|
203
|
+
const acervoOf = (id) => {
|
|
204
|
+
if (acervoLife.has(id)) return acervoLife.get(id);
|
|
205
|
+
const files = acervo.get(id) ?? [];
|
|
206
|
+
if (files.length > 1) warnings.push(`${ACERVO_LABEL}/: ${id} tem ${files.length} arquivos (${files.join(', ')}) — o acervo precisa de um arquivo por UC`);
|
|
207
|
+
const file = files[files.length - 1];
|
|
208
|
+
const content = file ? readOrNull(join(acervoDir, file)) : null;
|
|
209
|
+
const life = content === null ? null : lifecycleOf(content);
|
|
210
|
+
if (content !== null) {
|
|
211
|
+
for (const p of supersessionProblems(content, id)) warnings.push(`${ACERVO_LABEL}/${file}: ${p}`);
|
|
212
|
+
}
|
|
213
|
+
acervoLife.set(id, life);
|
|
214
|
+
return life;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const spec = readOrNull(specPathOf(featureDir));
|
|
218
|
+
const rev = spec === null ? { entries: [], problems: [] } : parseRevocations(spec);
|
|
219
|
+
for (const p of rev.problems) warnings.push(`spec.md, revogações: ${p.message}`);
|
|
220
|
+
|
|
221
|
+
const revokedHere = new Map();
|
|
222
|
+
let indexedAdrs = null;
|
|
223
|
+
for (const e of rev.entries) {
|
|
224
|
+
if (e.kind === 'adr') {
|
|
225
|
+
indexedAdrs ??= new Set(readAdrIndex(cwd).map((r) => r.ref));
|
|
226
|
+
if (!indexedAdrs.has(e.id)) {
|
|
227
|
+
warnings.push(`spec.md, revogações: ${e.id} não está no índice de ADRs ativos (${ADR_INDEX_LABEL}) — o finish não terá o que retirar`);
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
revokedHere.set(e.id, e);
|
|
232
|
+
if (!acervo.has(e.id)) {
|
|
233
|
+
warnings.push(`spec.md, revogações: ${e.id} não está no acervo (${ACERVO_LABEL}/) — o finish não terá o que marcar`);
|
|
234
|
+
} else {
|
|
235
|
+
acervoOf(e.id);
|
|
236
|
+
}
|
|
237
|
+
if (!acervo.has(e.substitute) && !own.has(e.substitute)) {
|
|
238
|
+
warnings.push(`spec.md, revogações: o substituto ${e.substitute} de ${e.id} não existe no acervo nem em 2-plan/usecases/ — o finish não aplicará a linha`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const tasks = Array.isArray(tasksData?.tasks) ? tasksData.tasks : [];
|
|
243
|
+
for (const task of tasks) {
|
|
244
|
+
const id = task?.usecase;
|
|
245
|
+
if (!id) continue;
|
|
246
|
+
const inAcervo = acervoOf(id);
|
|
247
|
+
const inOwn = ownLife.get(id);
|
|
248
|
+
const here = revokedHere.get(id);
|
|
249
|
+
|
|
250
|
+
let where = null;
|
|
251
|
+
if (inAcervo?.status === SUPERSEDED) where = { src: 'está superseded no acervo', ...inAcervo };
|
|
252
|
+
else if (inOwn?.status === SUPERSEDED) where = { src: 'está superseded na cópia da feature', ...inOwn };
|
|
253
|
+
else if (here) where = { src: 'é revogado por esta feature (spec.md)', supersededBy: here.substitute, scope: here.scope };
|
|
254
|
+
if (!where) continue;
|
|
255
|
+
|
|
256
|
+
const by = where.supersededBy;
|
|
257
|
+
const detail = [by ? `substituto: ${by}` : 'sem substituto declarado', where.scope ? `ainda vale em: ${where.scope}` : null]
|
|
258
|
+
.filter(Boolean).join('; ');
|
|
259
|
+
const advice = where.scope ? 'confira que a task realiza a parte que ainda vale' : by ? `cite ${by}` : 'confira o acervo';
|
|
260
|
+
warnings.push(`${task.id}.usecase → ${id} ${where.src} (${detail}) — ${advice}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { warnings };
|
|
264
|
+
}
|