@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
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
* Fail-open like the promoter: no `usecases/` folder means nothing to check.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import { readFileSync } from 'fs';
|
|
37
37
|
import { join } from 'path';
|
|
38
|
-
import {
|
|
38
|
+
import { isCanonicalUseCaseName, useCaseFilesById } from './acervo-format.js';
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* A post-condition line: `- **P2** — Cada veterinário traz suas especialidades`.
|
|
@@ -180,23 +180,18 @@ export function parsePostConditions(content) {
|
|
|
180
180
|
* continuation); `postConditions[].text` stays the first line, which is what
|
|
181
181
|
* the literal-carry check compares against.
|
|
182
182
|
*
|
|
183
|
+
* Any `UC-{nn}-….md` counts, canonical or not — installed plans rely on it.
|
|
184
|
+
* `nonCanonical` lists the files of that id the promotion will not carry, so
|
|
185
|
+
* `checkUseCases` can say so instead of the finish dropping them in silence.
|
|
186
|
+
*
|
|
183
187
|
* @param {string} usecasesDir - absolute path to `2-plan/usecases`
|
|
184
|
-
* @returns {Map<string, { id: string, file: string, postConditions: Array<{label: string, text: string}>, artifactJustified: Map<string, string>, fullText: Map<string, string> }>}
|
|
188
|
+
* @returns {Map<string, { id: string, file: string, nonCanonical: string[], postConditions: Array<{label: string, text: string}>, artifactJustified: Map<string, string>, fullText: Map<string, string> }>}
|
|
185
189
|
*/
|
|
186
190
|
export function loadUseCases(usecasesDir) {
|
|
187
191
|
const byId = new Map();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
names = readdirSync(usecasesDir).sort();
|
|
193
|
-
} catch {
|
|
194
|
-
return byId; // unreadable folder is "no use cases", never a crash
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
for (const file of names) {
|
|
198
|
-
const id = useCaseId(file);
|
|
199
|
-
if (!id || !file.endsWith('.md')) continue;
|
|
192
|
+
for (const [id, files] of useCaseFilesById(usecasesDir, { canonicalOnly: false })) {
|
|
193
|
+
const file = files[files.length - 1];
|
|
194
|
+
const nonCanonical = files.filter((f) => !isCanonicalUseCaseName(f));
|
|
200
195
|
try {
|
|
201
196
|
const content = readFileSync(join(usecasesDir, file), 'utf8');
|
|
202
197
|
const blocks = parsePostConditionBlocks(content);
|
|
@@ -206,7 +201,7 @@ export function loadUseCases(usecasesDir) {
|
|
|
206
201
|
fullText.set(label, block.text);
|
|
207
202
|
if (block.justified) artifactJustified.set(label, block.text);
|
|
208
203
|
}
|
|
209
|
-
byId.set(id, { id, file, postConditions: parsePostConditions(content), artifactJustified, fullText });
|
|
204
|
+
byId.set(id, { id, file, nonCanonical, postConditions: parsePostConditions(content), artifactJustified, fullText });
|
|
210
205
|
} catch { /* one unreadable use case must not sink the rest */ }
|
|
211
206
|
}
|
|
212
207
|
return byId;
|
|
@@ -241,6 +236,15 @@ export function checkUseCases(tasksData, usecasesDir) {
|
|
|
241
236
|
|
|
242
237
|
if (byId.size === 0) return { errors, warnings, checked: false };
|
|
243
238
|
|
|
239
|
+
for (const uc of byId.values()) {
|
|
240
|
+
for (const file of uc.nonCanonical) {
|
|
241
|
+
warnings.push(
|
|
242
|
+
`${uc.id} está em 2-plan/usecases/${file}, nome fora do padrão UC-{nn}-{slug}.md: ` +
|
|
243
|
+
'o finish não o promove ao acervo — renomeie'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
244
248
|
// Pre-normalize every doneCriteria once: this is O(tasks × postConditions)
|
|
245
249
|
// and both are small, but the normalization itself is not free.
|
|
246
250
|
const criteria = tasks.map((t) => ({ id: t?.id, text: normalize(t?.doneCriteria || ''), usecase: t?.usecase }));
|
|
@@ -30,22 +30,26 @@
|
|
|
30
30
|
* rather than silently destroy the other feature's use case.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import { mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
34
34
|
import { basename, join, relative } from 'path';
|
|
35
35
|
import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
|
|
36
36
|
import { resolveGitCommonDir } from '../../../framework/hooks/shared/git-paths.js';
|
|
37
37
|
import { reservationOwner } from './usecase-registry.js';
|
|
38
|
+
import { SUPERSEDED, useCaseFilesById, useCaseId } from './acervo-format.js';
|
|
39
|
+
|
|
40
|
+
export { useCaseId };
|
|
38
41
|
|
|
39
42
|
const UC = OUTPUT_SCHEMA.usecases;
|
|
40
|
-
const UC_FILENAME_RE = new RegExp(UC.filenamePattern);
|
|
41
43
|
|
|
42
44
|
/** Project-relative path of the permanent use-case archive (git pathspec-ready). */
|
|
43
45
|
export const SPECS_USECASES_PATHSPEC = join('.morph', 'specs', 'usecases');
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const FRONTMATTER_RE = /^(?---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/;
|
|
48
|
+
|
|
49
|
+
/** `true` when the document opens with a frontmatter block. */
|
|
50
|
+
export function hasFrontmatter(content) {
|
|
51
|
+
const m = FRONTMATTER_RE.exec(content);
|
|
52
|
+
return !!m && m.index === 0;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
/**
|
|
@@ -58,33 +62,70 @@ export function useCaseId(filename) {
|
|
|
58
62
|
*
|
|
59
63
|
* @param {string} content
|
|
60
64
|
* @param {string} key
|
|
61
|
-
* @param {string} value
|
|
65
|
+
* @param {string} value - written as-is; quote it first when it is free text
|
|
62
66
|
* @returns {string}
|
|
63
67
|
*/
|
|
64
|
-
function setFrontmatterField(content, key, value) {
|
|
65
|
-
const m =
|
|
68
|
+
export function setFrontmatterField(content, key, value) {
|
|
69
|
+
const m = FRONTMATTER_RE.exec(content);
|
|
66
70
|
if (!m || m.index !== 0) return content;
|
|
67
71
|
|
|
68
72
|
const [full, open, body, close] = m;
|
|
69
73
|
const eol = full.includes('\r\n') ? '\r\n' : '\n';
|
|
70
74
|
const fieldLine = new RegExp(`^${key}:[ \\t]*.*$`, 'm');
|
|
75
|
+
// Function replacer: a value carrying `$&` or `$1` is text, not a pattern.
|
|
71
76
|
const next = fieldLine.test(body)
|
|
72
|
-
? body.replace(fieldLine, `${key}: ${value}`)
|
|
77
|
+
? body.replace(fieldLine, () => `${key}: ${value}`)
|
|
73
78
|
: `${body}${eol}${key}: ${value}`;
|
|
74
79
|
|
|
75
80
|
return open + next + close + content.slice(full.length);
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Drop a single top-level frontmatter field (its whole line, EOL included).
|
|
85
|
+
* Absent field or no frontmatter → content unchanged.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} content
|
|
88
|
+
* @param {string} key
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
export function removeFrontmatterField(content, key) {
|
|
92
|
+
const m = FRONTMATTER_RE.exec(content);
|
|
93
|
+
if (!m || m.index !== 0) return content;
|
|
94
|
+
|
|
95
|
+
const [full, open, body, close] = m;
|
|
96
|
+
const lines = body.split(/(?<=\n)/);
|
|
97
|
+
const kept = lines.filter((line) => !new RegExp(`^${key}:`).test(line));
|
|
98
|
+
if (kept.length === lines.length) return content;
|
|
99
|
+
// The last kept line lost its EOL when the removed one was the final line;
|
|
100
|
+
// the closing delimiter carries its own leading EOL, so trim the dangling one.
|
|
101
|
+
const next = kept.join('').replace(/\r?\n$/, '');
|
|
102
|
+
return open + next + close + content.slice(full.length);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Read a single top-level frontmatter field. `null` when absent or there is no
|
|
107
|
+
* frontmatter block. A double-quoted value is read as the JSON string it is
|
|
108
|
+
* written as (that is how free text such as `scope:` is stored); a
|
|
109
|
+
* single-quoted one as YAML reads it (`''` is a quote).
|
|
110
|
+
*/
|
|
111
|
+
export function readFrontmatterField(content, key) {
|
|
112
|
+
const m = FRONTMATTER_RE.exec(content);
|
|
81
113
|
if (!m || m.index !== 0) return null;
|
|
82
114
|
const line = new RegExp(`^${key}:[ \\t]*(.+?)[ \\t]*$`, 'm').exec(m[2]);
|
|
83
|
-
|
|
115
|
+
if (!line) return null;
|
|
116
|
+
const raw = line[1].trim();
|
|
117
|
+
if (raw.length >= 2 && raw.startsWith('"') && raw.endsWith('"')) {
|
|
118
|
+
try { return JSON.parse(raw); } catch { return raw; }
|
|
119
|
+
}
|
|
120
|
+
if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1).replace(/''/g, "'");
|
|
121
|
+
return raw;
|
|
84
122
|
}
|
|
85
123
|
|
|
86
124
|
/**
|
|
87
|
-
* Rewrite the frontmatter `status:` to `implemented
|
|
125
|
+
* Rewrite the frontmatter `status:` to `implemented` — unless the use case is
|
|
126
|
+
* superseded (its status says so, or it carries a `superseded_by`): a
|
|
127
|
+
* promotion, such as a `bug` fix on the part that still holds, must never
|
|
128
|
+
* hand the rest of it back its authority.
|
|
88
129
|
*
|
|
89
130
|
* A file with no frontmatter is copied verbatim: the use case belongs to the
|
|
90
131
|
* author, and inventing a header would be a silent edit of their document.
|
|
@@ -93,7 +134,32 @@ function readFrontmatterField(content, key) {
|
|
|
93
134
|
* @returns {string}
|
|
94
135
|
*/
|
|
95
136
|
export function stampImplemented(content) {
|
|
96
|
-
|
|
137
|
+
const superseded = readFrontmatterField(content, 'status') === SUPERSEDED
|
|
138
|
+
|| readFrontmatterField(content, 'superseded_by') !== null;
|
|
139
|
+
return setFrontmatterField(content, 'status', superseded ? SUPERSEDED : 'implemented');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Carry the acervo's supersession onto an incoming copy that does not declare
|
|
144
|
+
* one — a copy taken before the revocation landed, or stripped of the fields
|
|
145
|
+
* on the way. Without this the promotion would publish the old use case as
|
|
146
|
+
* `implemented` again.
|
|
147
|
+
*
|
|
148
|
+
* @param {string} content - the incoming copy
|
|
149
|
+
* @param {string|null} previous - the acervo entry it replaces
|
|
150
|
+
* @returns {{ content: string, supersededBy: string|null, scope: string|null }|null} null = nothing to carry
|
|
151
|
+
*/
|
|
152
|
+
function inheritSupersession(content, previous) {
|
|
153
|
+
if (!previous || readFrontmatterField(previous, 'status') !== SUPERSEDED) return null;
|
|
154
|
+
if (readFrontmatterField(content, 'status') === SUPERSEDED || readFrontmatterField(content, 'superseded_by') !== null) return null;
|
|
155
|
+
if (!hasFrontmatter(content)) return null;
|
|
156
|
+
|
|
157
|
+
const supersededBy = readFrontmatterField(previous, 'superseded_by');
|
|
158
|
+
const scope = readFrontmatterField(previous, 'scope');
|
|
159
|
+
let next = setFrontmatterField(content, 'status', SUPERSEDED);
|
|
160
|
+
if (supersededBy) next = setFrontmatterField(next, 'superseded_by', supersededBy);
|
|
161
|
+
if (scope !== null) next = setFrontmatterField(next, 'scope', JSON.stringify(scope));
|
|
162
|
+
return { content: next, supersededBy, scope };
|
|
97
163
|
}
|
|
98
164
|
|
|
99
165
|
/**
|
|
@@ -138,27 +204,24 @@ function collisionMessage({ id, incoming, existing, existingFeature, feature, re
|
|
|
138
204
|
* @param {string} cwd - project root that owns `.morph/specs/`
|
|
139
205
|
* @param {string} featureDir - the feature folder holding `2-plan/usecases/`
|
|
140
206
|
* @returns {{ promoted: string[], replaced: string[], refused: Array<{id: string,
|
|
141
|
-
* incoming: string, existing: string, existingFeature: string, message: string}
|
|
142
|
-
*
|
|
207
|
+
* incoming: string, existing: string, existingFeature: string, message: string}>,
|
|
208
|
+
* keptSuperseded: Array<{id: string, file: string, supersededBy: string|null, scope?: string}> }}
|
|
209
|
+
* always arrays; `keptSuperseded` = promoted copies that stayed superseded
|
|
210
|
+
* because the acervo entry they replaced was
|
|
143
211
|
*/
|
|
144
212
|
export function promoteUseCases(cwd, featureDir) {
|
|
145
|
-
const empty = { promoted: [], replaced: [], refused: [] };
|
|
213
|
+
const empty = { promoted: [], replaced: [], refused: [], keptSuperseded: [] };
|
|
146
214
|
const srcDir = join(featureDir, UC.phaseDir, UC.dirname);
|
|
147
215
|
const feature = basename(featureDir);
|
|
148
216
|
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
if (!existsSync(srcDir)) return empty;
|
|
152
|
-
names = readdirSync(srcDir).filter((n) => UC_FILENAME_RE.test(n)).sort();
|
|
153
|
-
} catch {
|
|
154
|
-
return empty;
|
|
155
|
-
}
|
|
217
|
+
const names = [...useCaseFilesById(srcDir).values()].flat().sort();
|
|
156
218
|
if (!names.length) return empty;
|
|
157
219
|
|
|
158
220
|
const destDir = join(cwd, '.morph', 'specs', 'usecases');
|
|
159
221
|
const promoted = [];
|
|
160
222
|
const replaced = [];
|
|
161
223
|
const refused = [];
|
|
224
|
+
const keptSuperseded = [];
|
|
162
225
|
|
|
163
226
|
try {
|
|
164
227
|
mkdirSync(destDir, { recursive: true });
|
|
@@ -173,18 +236,16 @@ export function promoteUseCases(cwd, featureDir) {
|
|
|
173
236
|
|
|
174
237
|
// Existing archive entries, indexed by use-case ID — the key the overwrite
|
|
175
238
|
// rule is written against.
|
|
176
|
-
const existingById = new Map();
|
|
177
|
-
try {
|
|
178
|
-
for (const name of readdirSync(destDir)) {
|
|
179
|
-
const id = UC_FILENAME_RE.test(name) ? useCaseId(name) : null;
|
|
180
|
-
if (id) existingById.set(id, name);
|
|
181
|
-
}
|
|
182
|
-
} catch { /* unreadable archive — treat as empty, never fatal */ }
|
|
239
|
+
const existingById = new Map([...useCaseFilesById(destDir)].map(([id, files]) => [id, files[files.length - 1]]));
|
|
183
240
|
|
|
184
241
|
for (const name of names) {
|
|
185
242
|
try {
|
|
186
243
|
const id = useCaseId(name);
|
|
187
244
|
const stale = id ? existingById.get(id) : null;
|
|
245
|
+
let previous = null;
|
|
246
|
+
if (stale) {
|
|
247
|
+
try { previous = readFileSync(join(destDir, stale), 'utf8'); } catch { /* unreadable — no signal */ }
|
|
248
|
+
}
|
|
188
249
|
|
|
189
250
|
// Same ID under a different filename: normally the old file is dropped
|
|
190
251
|
// (a slug reword, or the `bug` flow correcting a published UC), or the
|
|
@@ -202,8 +263,7 @@ export function promoteUseCases(cwd, featureDir) {
|
|
|
202
263
|
// (no git common dir, or a legacy archive entry predating this stamp) is
|
|
203
264
|
// fail-open: nothing to compare against, so nothing is refused.
|
|
204
265
|
if (stale && stale !== name) {
|
|
205
|
-
|
|
206
|
-
try { staleFeature = readFrontmatterField(readFileSync(join(destDir, stale), 'utf8'), 'sourceFeature'); } catch { /* unreadable — no signal */ }
|
|
266
|
+
const staleFeature = previous === null ? null : readFrontmatterField(previous, 'sourceFeature');
|
|
207
267
|
const incomingOwner = id ? reservationOwner(id, { cwd, gitCommonDir }) : null;
|
|
208
268
|
|
|
209
269
|
if (staleFeature && staleFeature !== feature && incomingOwner === feature) {
|
|
@@ -224,13 +284,18 @@ export function promoteUseCases(cwd, featureDir) {
|
|
|
224
284
|
replaced.push(stale);
|
|
225
285
|
}
|
|
226
286
|
|
|
227
|
-
let content =
|
|
228
|
-
|
|
287
|
+
let content = readFileSync(join(srcDir, name), 'utf8');
|
|
288
|
+
const kept = inheritSupersession(content, previous);
|
|
289
|
+
if (kept) {
|
|
290
|
+
content = kept.content;
|
|
291
|
+
keptSuperseded.push({ id, file: name, supersededBy: kept.supersededBy, ...(kept.scope !== null ? { scope: kept.scope } : {}) });
|
|
292
|
+
}
|
|
293
|
+
content = stampSourceFeature(stampImplemented(content), feature);
|
|
229
294
|
writeFileSync(join(destDir, name), content, 'utf8');
|
|
230
295
|
promoted.push(name);
|
|
231
296
|
if (id) existingById.set(id, name);
|
|
232
297
|
} catch { /* one unreadable use case must not sink the rest */ }
|
|
233
298
|
}
|
|
234
299
|
|
|
235
|
-
return { promoted, replaced, refused };
|
|
300
|
+
return { promoted, replaced, refused, keptSuperseded };
|
|
236
301
|
}
|
|
@@ -40,10 +40,9 @@ import {
|
|
|
40
40
|
} from 'fs';
|
|
41
41
|
import { join } from 'path';
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const UC_ID_RE = /^(UC-(\d+))-/;
|
|
43
|
+
// From acervo-format.js, not usecase-promoter.js: the promoter depends on THIS
|
|
44
|
+
// module (to look up who reserved a number), and a two-way import is circular.
|
|
45
|
+
import { useCaseId } from './acervo-format.js';
|
|
47
46
|
|
|
48
47
|
/** `UC-7` (or any width) -> `UC-07` — the archive's zero-padded convention. */
|
|
49
48
|
function formatUcId(n) {
|
|
@@ -138,9 +137,10 @@ function maxUcNumberInDir(dir) {
|
|
|
138
137
|
try {
|
|
139
138
|
if (!existsSync(dir)) return 0;
|
|
140
139
|
for (const name of readdirSync(dir)) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
// Any `UC-{nn}-` prefix counts, canonical or not: a number already on disk is never handed out again.
|
|
141
|
+
const id = useCaseId(name);
|
|
142
|
+
if (!id) continue;
|
|
143
|
+
const n = parseInt(id.slice('UC-'.length), 10);
|
|
144
144
|
if (Number.isFinite(n) && n > max) max = n;
|
|
145
145
|
}
|
|
146
146
|
} catch { /* unreadable dir — treat as empty, never fatal */ }
|
|
@@ -12,6 +12,7 @@ import fs from 'fs-extra';
|
|
|
12
12
|
import { readFileSync, existsSync } from 'fs';
|
|
13
13
|
import { join, dirname } from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
|
+
import { UC_ID_RE } from '../specs/acervo-format.js';
|
|
15
16
|
|
|
16
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const SCHEMA_PATH = join(__dirname, '..', '..', '..', 'framework', 'schemas', 'tasks.schema.json');
|
|
@@ -26,8 +27,6 @@ export function getSchema() {
|
|
|
26
27
|
const VALID_STATUS = ['pending', 'in_progress', 'done', 'skipped', 'blocked'];
|
|
27
28
|
const VALID_EFFORT = ['S', 'M', 'L', 'XL'];
|
|
28
29
|
const VALID_GROUP = ['backend', 'frontend', 'tests', 'infra', 'docs'];
|
|
29
|
-
/** Use-case reference: the ID only (`UC-07`), never the filename. */
|
|
30
|
-
const USECASE_RE = /^UC-\d+$/;
|
|
31
30
|
/** Veredito de reuso por conceito — ver framework/schemas/tasks.schema.json. */
|
|
32
31
|
const VALID_REUSE_VERDICT = ['REUSAR', 'ESTENDER', 'CRIAR'];
|
|
33
32
|
|
|
@@ -86,7 +85,7 @@ export function validateTasksJson(data) {
|
|
|
86
85
|
// Optional: a task with no observable behaviour references no use case.
|
|
87
86
|
// When present it must be the bare ID — `morph-spec dag` is the gate that
|
|
88
87
|
// catches "UC-07-listar.md" or "UC7" before the plan reaches Gate 2.
|
|
89
|
-
if (task.usecase !== undefined && (typeof task.usecase !== 'string' || !
|
|
88
|
+
if (task.usecase !== undefined && (typeof task.usecase !== 'string' || !UC_ID_RE.test(task.usecase))) {
|
|
90
89
|
errors.push(`${ctx}.usecase must match /^UC-\\d+$/ (the ID alone, e.g. "UC-07")`);
|
|
91
90
|
}
|
|
92
91
|
|