@polymorphism-tech/morph-spec 8.34.2 → 8.35.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 +20 -3
- package/README.md +2 -2
- package/bin/morph-spec.js +18 -4
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/MORPH.md +16 -17
- package/framework/agents.json +197 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +234 -9
- package/framework/evals/proof-rubrics/README.md +76 -0
- package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
- package/framework/evals/proof-rubrics/fiacao.md +54 -0
- package/framework/evals/proof-rubrics/guard-cego.md +52 -0
- package/framework/evals/proof-rubrics/media-claims.md +109 -0
- package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
- package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
- package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
- package/framework/evals/rubrics/architecture.md +30 -0
- package/framework/evals/rubrics/code-quality.md +26 -0
- package/framework/evals/rubrics/test-coverage.md +20 -0
- package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/e2e-check.js +135 -22
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/rules/ai-agents-standards.md +14 -2
- package/framework/schemas/ai-pin.schema.json +97 -0
- package/framework/schemas/config.schema.json +107 -0
- package/framework/schemas/feature.schema.json +23 -4
- 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 +26 -5
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +18 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +33 -2
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +166 -9
- 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 +1 -1
- package/framework/standards/PROVENANCE.json +81 -3
- package/framework/standards/STANDARDS.json +1483 -839
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +164 -68
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +61 -37
- package/framework/standards/ai-agents/context-providers.md +97 -24
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
- package/framework/standards/ai-agents/cost-and-budget.md +256 -0
- package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
- package/framework/standards/ai-agents/evals-with-cache.md +289 -0
- package/framework/standards/ai-agents/guardrails.md +222 -0
- package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
- package/framework/standards/ai-agents/mcp-server.md +119 -18
- package/framework/standards/ai-agents/mcp-tools.md +85 -32
- package/framework/standards/ai-agents/media-pipeline.md +352 -0
- package/framework/standards/ai-agents/media-video.md +267 -0
- package/framework/standards/ai-agents/middleware-patterns.md +39 -5
- package/framework/standards/ai-agents/modalities-image-gen.md +354 -105
- package/framework/standards/ai-agents/modalities-speech-to-text.md +8 -2
- package/framework/standards/ai-agents/modalities-text-to-speech.md +10 -2
- package/framework/standards/ai-agents/modalities-vision.md +6 -2
- package/framework/standards/ai-agents/multi-agent-patterns.md +61 -5
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +38 -9
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +174 -39
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +27 -19
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +34 -3
- package/framework/standards/ai-agents/sweet-spot.md +23 -3
- package/framework/standards/ai-agents/testing-ai.md +236 -0
- package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
- package/framework/standards/ai-agents/workflows.md +27 -7
- package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +3 -1
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +13 -0
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +1 -1
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +1 -1
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
- package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
- package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
- package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
- package/framework/templates/dotnet/ai-kit/README.md +128 -0
- package/framework/templates/dotnet/ai-kit/global.json +9 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +3 -3
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/e2e/e2e.js +15 -2
- package/src/commands/index.js +13 -13
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +140 -5
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +8 -1
- package/src/commands/state/advance.js +24 -2
- package/src/commands/state/approve.js +33 -1
- package/src/commands/state/create.js +72 -11
- package/src/commands/state/finish.js +333 -21
- package/src/commands/state/gate-check.js +190 -6
- package/src/commands/state/score.js +36 -1
- package/src/commands/validation/verify.js +134 -25
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +9 -1
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/ai/ai-checks.js +182 -0
- package/src/lib/ai/ai-project-scan.js +347 -0
- package/src/lib/ai/code-audit.js +181 -0
- package/src/lib/ai/comment-stripper.js +184 -0
- package/src/lib/ai/pin-drift.js +138 -0
- package/src/lib/ai/registry-audit.js +285 -0
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/e2e/compose-env.js +50 -4
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/effects.js +193 -0
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/installers/templates-reconcile.js +3 -3
- package/src/lib/risk-detector.js +284 -0
- package/src/lib/standards/verified-against.js +276 -0
- package/src/lib/tasks/eval-node.js +485 -0
- package/src/lib/tasks/linux-runner.js +297 -0
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +410 -33
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/verify-runner.js +583 -28
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +24 -0
- package/src/lib/worktree/worktree-audit.js +30 -1
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +11 -4
- package/src/utils/hooks-installer.js +29 -26
- package/src/utils/skills-installer.js +36 -29
- package/src/utils/version-checker.js +68 -3
- package/framework/standards/ai-agents/prompts-in-db.md +0 -214
package/framework/docs/CLI.md
CHANGED
|
@@ -8,27 +8,27 @@
|
|
|
8
8
|
|
|
9
9
|
- **`morph-spec approve <feature> <gate> --mode auto`** — aprova um gate (`proposal`/`uiux`/`plan`/`review`) em `trust: auto`. **`uiux`** é o único gate cujo `AskUserQuestion` o hook `state-sync.js` não reconhece por texto (só casa "Gate 1/2/3") — aprove-o sempre explicitamente por este comando ou por `advance`, nunca contando com o hook. **O Gate 3 exige `4-review/evaluator-report.md`**: sem o relatório do avaliador independente o comando recusa (exit 1). Se o dispatch do avaliador for impossível nesta sessão, aprove com `--self-assessed` — a degradação é gravada em `approvalGates.review.selfAssessed` e aparece marcada no `morph-spec list`. `--force` **não** dispensa esta regra (é só para ordem de gate). **Exceção `hotfix`:** esse tipo nunca despacha o avaliador (`morph-hotfix` §7); o Gate 3 exige `4-review/review-report.md` no lugar do `evaluator-report.md`, e a assinatura humana continua obrigatória via `--approver` (o backstop de `--mode auto` acima permanece recusando).
|
|
10
10
|
- **`morph-spec advance <feature>`** — aprova o gate da fase corrente **e** cria a pasta da próxima fase num passo só (`--design` toma o ramo opcional de uiux a partir da proposta).
|
|
11
|
-
- **`morph-spec score <feature> <task>
|
|
12
|
-
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp).
|
|
11
|
+
- **`morph-spec score <feature> <task> [score] [--dims a,c,q,t]`** — grava `taskScores` no feature.json com timestamp server-side; **nunca edite taskScores à mão**. **Duas recusas duras (exit 1), nunca um registro silencioso:** `<score>` em branco ou ausente é **recusado, jamais gravado como `0`** (o `Number('')` que valia `0` gravava um zero fabricado — e era esse zero que o Gate 3 lia depois, como `evalScore: 0 / evalScoreSource: taskScores-proxy`); e `<task>` fora de `^T\d+$` é **recusado** em vez de aninhado (`T3.2` virava `taskScores.T3["2"]` — inválido contra `feature.schema.json` e invisível ao proxy). O `[score]` aparece opcional só para que o valor ausente chegue ao comando e receba uma mensagem que nomeia o argumento e a ordem posicional — ele continua obrigatório. Em PowerShell, cite o valor de `--dims` entre aspas (`--dims "9.5,9,9,9"`) — sem aspas, a lista pode chegar como tokens separados; o parser aceita ambas as formas, mas citar evita qualquer ambiguidade.
|
|
12
|
+
- **`morph-spec gate-check <feature> <gate> [--signals <json>] [--trust <mode>] [--json]`** — o caminho **default**: coleta do disco os sinais que `shouldPauseGate` (risk-detector.js) já sabe avaliar (`workType`, `taskCount`, verificação, sha/diffstat), decide a pausa e persiste `gateDecisions.{gate}`. `--signals` aceita só as chaves que o disco genuinamente não expõe sozinho — `designSystemDivergence`, `standardsViolations`, `evalScore` — qualquer outra chave é ignorada com aviso. No **review**, o `evalScore` real vem do token `**Composite-Score:** {n.n}` do `evaluator-report.md` (o avaliador o grava por contrato). **Score que não venha desse token PAUSA o Gate 3**, qualquer que seja o valor: o proxy (média das `taskScores`) e o override de `--signals` medem auto-avaliação de quem implementou, não qualidade da feature — em campo, 7 tasks com auto-score 9.0-9.3 auto-passaram um produto que o avaliador independente pontuou 7.2 e reprovou com 3 P1. `evalScoreSource` registra a procedência (`evaluator-report` / `taskScores-proxy` / `signals-override`). Para forçar a passagem mesmo assim, use `gate-decision --pause false` — explícito e registrado. Fora do review a regra não vale: antes de haver implementação não existe avaliador independente. Um stamp de `verification` com status `fail` (último `morph-spec verify` vermelho) **força pausa** em qualquer gate. Um stamp `pass` mas **stale** (sha diferente do HEAD atual — código mudou depois do último verify) força pausa **só no ship** (`review`); em `plan`/`uiux`/`proposal` a staleness é computada e reportada em `signals`, mas não pausa nada — é regra de ship, não de gate genérico. Stamp **ausente** nunca pausa por isso (features legadas sem stamp). **Dispensa de e2e:** o stamp carrega `nodes.e2e` e a razão literal `e2eReason`, porque um nó e2e que **pula** não reprova o rollup — sem eles o gate lê `pass` tanto quando o e2e rodou e passou quanto quando ele nunca rodou. No **ship** (`review`), um `skip` é classificado pela razão (`classifyE2eSkip`, em `risk-detector.js` — fonte única) e o veredito fica registrado em `gateDecisions.review.signals` (`e2eStatus`, `e2eReason`, `e2eSkipClass`): `project` ("project does not containerize") e `scope` ("task group … does not touch runtime") são **dispensa legítima** — passam, com o motivo gravado, sem precisar de `gate-decision` manual; `environment` ("docker is not available…"), `requested` (`--skip-e2e`) e `unknown` (qualquer outra razão, **inclusive razão nenhuma**) **pausam** — a máquina ou o operador não são evidência sobre o código, e uma dispensa sem motivo é o mesmo no-op silencioso que a regra existe para eliminar. Fora do review a regra não vale (pausar o Gate 1/2 numa máquina sem docker travaria planejamento legítimo). **A dispensa também depende de QUEM carimbou.** O stamp `verification` é singular e qualquer `verify` o sobrescreve, de qualquer escopo — então um stamp de escopo `task:{id}` no momento do ship significa que o último `verify` **não foi o da feature**. A dispensa (`project`/`scope`) só conta vinda de `scope: 'feature'`; de um stamp de task ela pausa, nomeando o escopo que carimbou e mandando rodar `morph-spec verify {feature}` sem task. Sem isso, um `morph-spec verify {f} {task}` de uma task cujo grupo não toca runtime (a última task de uma feature é tipicamente `group: docs`) compra o Gate 3 da feature inteira numa máquina onde a app nunca subiu. Escopo ausente cai do mesmo lado seguro que razão nenhuma. A regra governa a **dispensa**, não a prova: um e2e que rodou e passou vale de qualquer escopo. `e2eStatus`/`e2eReason`/`e2eSkipClass`/`verificationScope` **não** são sobrescrevíveis por `--signals`: descrevem o que aconteceu no disco, como `gate`/`workType`. **Prova de mutação:** no ship, o `gate-check` lê do MESMO `evaluator-report.md` se existe a seção `## Mutações` (`evaluatorProofState`, em `src/lib/evaluator-report.js` — leitor único) e grava `signals.mutationProof = {section, min, productionCode}`. Relatório **sem** a seção numa feature com código de produção (ao menos uma task com `group` diferente de `docs`) **pausa o Gate 3**, qualquer que seja o score: um teste verde prova que o código roda, não que o teste morde. Feature 100% `docs` é isenta, e `tasks.json` ausente também (sem plano não há como afirmar que existe código de produção). `min` é o menor `**Proof-Scores:**` do relatório e impõe teto em `test-coverage` (rubricas em `framework/evals/proof-rubrics/`); `null` é **sem teto**, jamais teto 0. `mutationProof` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos sinais de e2e. **O token vigente é o ÚLTIMO:** o `evaluator-report.md` é append-only, então numa reavaliação o `**Composite-Score:**` que decide o gate é o do FIM do arquivo — ler o primeiro auto-passaria o Gate 3 com a nota superada quando uma correção BAIXA a nota. **Prova de mídia:** do MESMO relatório e pelo MESMO leitor, o `gate-check` grava `signals.mediaClaims = {section, score, featureGeneratesMedia}`. `featureGeneratesMedia` é determinístico do disco — existe ao menos uma task de `tasks.json` cujo array `standards` cita `standards/ai-agents/modalities-image-gen.md`, `standards/ai-agents/media-video.md` ou `standards/ai-agents/media-pipeline.md` — e a grafia do disco (`framework/standards/…`) conta igual, porque um plano escrito com o prefixo do disco desarmaria o guardrail sem uma linha de erro (`featureGeneratesMedia`, função exportada de `gate-check.js`, é onde essa normalização mora e é testada). Nessa condição, relatório **sem** a seção `## Mídia gerada` **pausa o Gate 3**: as quatro afirmações de mídia (custo, revisão humana, marcação, hot path não bloqueado) não foram provadas (rubrica `framework/evals/proof-rubrics/media-claims.md`). Feature que não pina standard de mídia é **isenta automaticamente** — a regra não pede cerimônia de quem não gera mídia. `score` é a chave `media=` dos `**Proof-Scores:**` e impõe teto em `architecture`, **não** em `test-coverage`: ela fica FORA do `min` do `mutationProof`, senão um `media` baixo derrubaria o teto de teste por um motivo que não é de teste. `mediaClaims` **não** é sobrescrevível por `--signals`, pela mesma razão anti-spoof dos demais sinais de disco.
|
|
13
13
|
- **`morph-spec gate-decision <feature> <gate> --pause <bool> [--reason <txt>] [--signals <json>]`** — o **escape manual**: registra uma decisão já tomada pela sessão (ex.: `discoveryMode: "interview"`, sinal que `shouldPauseGate` nem representa). Mesmo write path do `gate-check` — grava `gateDecisions` no feature.json com timestamp server-side (shape em `framework/schemas/feature.schema.json`); **nunca edite gateDecisions à mão** — o Edit manual colide com o hook state-sync.
|
|
14
14
|
- **Guarda de árvore dona:** `approve`/`unapprove`/`advance`/`score`/`gate-decision`/`gate-check` recusam (exit 1) quando um worktree vivo é dono da feature e a sessão está em outra árvore — sem isso o comando carimbaria o gate/score/decisão na cópia congelada da raiz, criando o drift em vez de só herdá-lo. A recusa nomeia o worktree e o comando exato para rodar lá; `--force` **não** dispensa (é sobre gate order, não sobre árvore). Rodando de dentro do worktree dono, ou numa feature sem worktree algum, nada muda.
|
|
15
15
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
|
-
- **`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos.
|
|
18
|
+
- **`morph-spec create <feature> --description "<o quê/porquê>" --type <feature|bug|chore|hotfix> [--request "<txt>"] [--name "<nome>"] [--worktree] [--no-checkout]`** — cria a estrutura da feature + `feature.json` canônico explicitamente (nascimento explícito no início da proposal; o hook state-sync continua cobrindo o caminho implícito como fallback). **Identidade no nascimento:** `name` (default = slug) e `description` são carimbados; a `--description` é **obrigatória** numa feature nova (ou `--request`, que a semeia) — sem nenhuma das duas o `create` recusa (fim dos scaffolds anônimos). `--type` classifica direto; `--request "<pedido>"` roda o detector determinístico (empate/sem sinal sai com exit 2 e candidatos — a LLM decide e re-roda com `--type` + `--request`/`--description`); `--worktree` já nasce a feature no seu worktree próprio. **Toda feature nasce numa branch `morph/{f}`** (ramificada da default) mesmo sem `--worktree` — por isso o fechamento por `morph-spec finish` é idêntico nos dois fluxos. **E sem `--worktree` o checkout PRINCIPAL se move para essa branch**: o comando avisa em amarelo, nomeando a branch anterior e a nova (sob `--json` o aviso vai para **stderr**, para sobreviver ao redirecionamento do stdout), porque trabalho não commitado vai junto e todo commit seguinte naquela árvore cai na branch da feature. `--no-checkout` cria a branch **sem** mover a árvore (a branch existe de todo jeito, para o `finish --pr|--merge` continuar simétrico); a saída estruturada distingue os dois casos em `branch.checkedOut` (+ `branch.movedFrom` / `branch.stayedOn`). Para trabalho em paralelo o caminho recomendado continua sendo `--worktree`, que não toca no checkout da raiz.
|
|
19
19
|
- **`morph-spec list [--all] [--json] [--no-branch]`** — o quadro de auditoria: uma linha por feature × workType × status × fase × gates G1/G2/G3 × descrição, com `⚠` para inconsistências (sem descrição, ordem de gate). Uma feature com worktree/branch ativos tem a cópia da RAIZ congelada no instante do nascimento (`create --worktree` grava lá um esqueleto completo e nunca mais atualiza) — por padrão o `list` lê a verdade commitada em `morph/{f}` quando ela diverge do disco desta árvore (mesma leitura do `fleet`), reportando `source: 'branch'|'disk'` e uma coluna **WHERE** (`raiz` / `branch morph/{f}` / `worktree {dir}` / `⚠ worktree ausente`). `--no-branch` volta ao disco puro, sem custo de git — **e também pula a consulta de PR** (nenhuma coluna, nenhuma linha sintetizada, ver abaixo): não é só o modo disk-only de antes, é o modo offline completo. Um ponteiro `worktree.path` que não existe mais aparece com o flag `worktree-missing` (repare com `doctor --worktrees --prune`). Uma feature já arquivada em outra árvore/branch, cujo esqueleto sobrevive aqui, ganha o flag `archived-elsewhere` em vez de sumir da lista (read-only: nunca ressuscita nem poda). Coluna **PR** mostra `#{n} {estado}` (`-` sem PR para a branch da feature). Uma feature cujo `finish --pr` rodou mas cujo PR segue aberto — sem cópia local nesta árvore — aparece como linha **sintetizada**: fase `aguardando-merge`, flag `pr-pending`, WHERE `branch morph/{f}`, descrição "aguardando merge há Nd — sem cópia local" (mesma síntese que `fleet` e `doctor` usam, de `src/lib/git/pending-prs.js` — uma feature visível numa dessas telas e invisível na outra é o bug que esse módulo compartilhado existe para impedir). `--all` inclui as arquivadas; `--json` para scripting. É o complemento do `delete` (enxergar o que está morto antes de remover).
|
|
20
20
|
- **`morph-spec delete <feature> [--archive] [--force] [--with-worktree]`** — remove uma feature. Hard por padrão (pasta apagada, git-aware, + entrada podada do índice raiz para não ressuscitar); `--archive` move para `.morph/archive` com status `abandoned` em vez de apagar. Recusa se algum gate estiver aprovado, a menos que `--force`. Recusa TAMBÉM se um worktree vivo estiver em outra árvore (`feature-has-worktree` — apagar o registro da raiz órfão o worktree+branch em silêncio) — `--force` **não** dispensa isto (é consentimento sobre gate, não sobre filesystem); feche primeiro com `finish`/`worktree remove`, ou passe `--with-worktree` para desmontar o worktree (PROVE-or-DECLINE) e apagar num passo só — a branch sobrevive por padrão (`--delete-branch` para descartá-la também). Use para faxina de scaffolds mortos e drafts abandonados (`archive` é para trabalho concluído; `delete` é para trabalho morto).
|
|
21
21
|
- **`morph-spec retype <feature> <newType>`** — muda o tipo de trabalho mid-flight; desfaz só os gates carimbados por política, preserva aprovações humanas; bloqueado após o Gate 3.
|
|
22
22
|
|
|
23
23
|
## Worktree / Fleet
|
|
24
24
|
|
|
25
|
-
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. Flags: `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas.
|
|
25
|
+
- **`morph-spec finish <feature> --pr|--merge`** — o **encerramento único** da feature (worktree ou comum): **arquiva** commitando `features/{f} → archive/{f}` dentro de `morph/{f}` — promovendo junto, no mesmo commit, os use cases da feature para o acervo permanente `.morph/specs/usecases/` com `status: implemented` (fail-open: sem `2-plan/usecases/`, no-op silencioso) — depois integra: `--merge` na hora; `--pr` só abre o PR, sem integrar. **O payload distingue os dois** — `--merge` sai com `integrated: true`; `--pr` sai com `integrated: false`, `awaiting: "merge"`, `pr: {number, url}` e um `nextStep` apontando para o merge. **`finished: true` sai nos dois casos e marca só o comando ter terminado, nunca a integração** — `--pr` não encerra a feature, quem encerra é o merge do PR; até lá ela aparece em `morph-spec list`/`fleet` como `aguardando-merge`, sem cópia local (ver `list` acima e `doctor` abaixo). Um `UC-{nn}` cuja promoção colide com outra feature já publicada sob o mesmo número (`ucCollisions` no output; ver `usecase reserve` abaixo) **não é promovido**, mas isso nunca recusa o `finish` — só aquele use case fica de fora, com a mensagem apontando onde o arquivo ficou e como recuperar. `--merge` faz merge `--no-ff` **local** na default (sem push); `--pr` faz `push` + `gh pr create` (exige `origin` + `gh`, checados **antes** de arquivar — um `--pr` recusado não deixa meio-estado). Desmonta o worktree (PROVE-or-DECLINE) e libera o bloco de portas; no `--pr` **preserva** a branch (o PR precisa dela) e fica em `morph/{f}`. **O `--merge` diz o que falta publicar:** depois do merge local a saída traz `publish: {remote, branch, ahead, pushed}` e um `nextStep` com a CONTAGEM de commits que a default local tem à frente de `origin/{default}` mais o comando concreto (`git push origin {default}`) — em **todas** as saídas do `--merge`, inclusive sob `--keep`. Sem contraparte remota para comparar (`refs/remotes/origin/{default}` ausente) `ahead` fica `null` e a frase estática de integração local permanece — nenhuma contagem é inventada. Flags: `--push` publica a default logo depois do merge (sai com `publish.pushed: true` e `publish.published: N`; sem remoto `origin` o merge NÃO é desfeito — a saída reporta `pushError: "no-origin-remote"` e mantém o aviso local) — **`--push` só existe com `--merge`: combinado com `--pr` é RECUSA, exit 1, `push-with-pr`, antes de qualquer mutação** (o `--pr` já publica `morph/{f}` e não tem merge local para publicar depois; a recusa nomeia o comando de cada intenção); `--keep` mantém o worktree **e** o registro dele na raiz (válido enquanto o diretório existir) — o output reporta `worktree: {path, branch}` e um `hint` lembrando `worktree remove <f>` para fechar depois; `--force` dispensa o Gate 3; `--discard-ignored` consente descartar gitignored no teardown. Substitui o antigo `worktree finish` + `archive` separados. **Retomável:** conflito de merge, checkout ou push falham *depois* do archive (é o commit do archive que viaja com a branch) — re-executar o `finish` retoma na integração e sai com `resumed: true`, em vez de recusar por feature ausente. Se a integração já tiver acontecido, recusa com `already-finished` — e o que conta como "já integrada" depende do modo: no `--merge`, branch contida na default; no `--pr`, PR aberto para `morph/{f}` (a branch de um `--pr` concluído fica não-mergeada de propósito, então só o teste de ancestralidade deixaria passar um segundo `gh pr create`). Sem conseguir provar nem uma coisa nem outra, recusa com `resume-undecidable` em vez de retomar às cegas. **`--dry-run` é o PRÉ-VOO:** não muta nada (nenhum commit, archive, remoção de worktree, chamada a `gh` que crie algo, nem `git fetch`) e emite `{preflight: true, finished: false}` com o que ESTE finish faria — `ignored.shieldable` (gitignored exclusivo, copiado para o backup) × `ignored.replicable` × `ignored.discardable`, os `reparsePoints` com o aviso de que apagá-los fora do morph-spec apaga o conteúdo REAL na raiz, `cwdInsideWorktree` e `baseAdvanced: {ref, commits}` — a pergunta INVERSA da do `--merge`: quantos commits `origin/{default}` tem que a base da feature não tem. `commits: null` é DESCONHECIDO, nunca 0. Rodado de DENTRO do worktree ele **avisa e completa o relatório** (sai 0); o caminho real continua recusando antes da primeira mutação.
|
|
26
26
|
- **`morph-spec usecase reserve [--count <n>] [--feature <name>] [--json]`** — reserva atomicamente os próximos N números `UC-{nn}`, compartilhado por **todo worktree** deste repo (`<git-common-dir>/morph-spec/uc-registry.json`, mesmo mecanismo do `claim`/env-registry do fleet). Substitui o scan manual "leia o acervo + as features da árvore e some 1": um scan só enxerga a ÁRVORE CORRENTE, então dois worktrees planejando ao mesmo tempo mintavam o MESMO número — colisão que a promoção do `finish` resolvia sobrescrevendo por ID, apagando o use case do primeiro a chegar (já custou perda de dados em produção). O contador nunca regride (semeado do maior número já visto no acervo + nas features da árvore + no próprio registro). `--feature` é mais que auditoria: é o sinal que o guarda de colisão do `finish`/`archive` usa para distinguir uma correção legítima (`bug`, nunca reserva — reaproveita um número publicado) de uma colisão real (a feature reservou o número e o acervo já tem outra dona nele); sem `--feature`, infere a única feature ativa se for inequívoco. Fora de um repositório git: cai para o scan local (avisa no output, `shared: false`) — ainda evita autocolisão dentro da mesma árvore, mas sem a garantia entre worktrees.
|
|
27
27
|
- **`morph-spec archive <feature>`** — arquiva-**sem-integrar** (atômico, git-aware, commita o move escopado por pathspec): reservado a features legadas (sem branch `morph/{f}`) e faxina. O fechamento normal (arquiva **e** integra num passo) é `morph-spec finish`. É o que `/morph-archive` chama.
|
|
28
28
|
- **`morph-spec worktree setup|provision|link|remove|list`** — gerencia o ciclo do worktree (`setup <f>` cria `worktrees/{f}/` na branch `morph/{f}`, aloca o bloco de portas e escreve `.morph/worktree.env`; `provision <n> [--base <ref>]` cria `worktrees/{n}/` na branch `morph-task/{n}` — um worktree de **isolamento de task**, sem feature, com as mesmas junctions e um bloco próprio sob a chave `task/{n}`; `link --all` religa a infra compartilhada **e regenera o `worktree.env`** com o layout atual; `list` mostra fase/gate de cada worktree ativo). **Fechar a feature** continua sendo `morph-spec finish` (acima).
|
|
29
29
|
**Worktrees nativos do Claude Code.** `EnterWorktree`, `claude -w <n>` e `Agent` com `isolation: "worktree"` passam pelo hook `WorktreeCreate` instalado pelo morph: nome conhecido como feature → `worktree setup`; senão → `worktree provision`. O resultado cai sempre em `worktrees/`, nunca em `.claude/worktrees/`. O hook `WorktreeRemove` chama `worktree remove --target <path> --if-clean` (só remove árvore limpa; a branch sobrevive) — mas ele **não disparou** em nenhum run headless medido, então a limpeza primária continua sendo o passo 4 de `morph-apply` §3b-w e o `doctor --worktrees`. `git worktree add` cru é bloqueado pelo hook `block-raw-worktree-add` (sem junctions, sem portas, sem identidade → invisível ao `fleet`).
|
|
30
30
|
**Layout do bloco (10 portas):** `PORT` (+0), `API_PORT` (+1, `ASPNETCORE_URLS` em dotnet), `POSTGRES_PORT` (+2), `REDIS_PORT` (+3), +4..+9 livres; mais `COMPOSE_PROJECT_NAME=morph-{f}`, `MORPH_PORT_BASE/RANGE`. O mesmo layout alimenta o `worktree.env`, o `docker compose` do `verify`/`e2e` e o hook `worktree-env-inject` (que prefixa `npm run dev`/`dotnet run`/`docker compose`/`playwright test` com `export …` quando o cwd tem manifesto e o comando não escolheu porta própria).
|
|
31
|
-
- **`morph-spec env [feature] [--json|--powershell]`** — imprime o bloco do worktree em forma shell-loadable (`eval "$(morph-spec env)"`; PowerShell: `morph-spec env --powershell | Invoke-Expression`). Feature vem do argumento ou da branch `morph/{f}`/`morph-task/{n}` do cwd. Deriva de `buildComposeEnv`, a mesma função do nó e2e — um shell e o stack nunca discordam. É o caminho para quem roda **fora** do Claude Code (terminal humano, CI, sub-agent sem hooks).
|
|
31
|
+
- **`morph-spec env [feature] [--json|--powershell]`** — imprime o bloco do worktree em forma shell-loadable (`eval "$(morph-spec env)"`; PowerShell: `morph-spec env --powershell | Invoke-Expression`). Feature vem do argumento ou da branch `morph/{f}`/`morph-task/{n}` do cwd. Deriva de `buildComposeEnv`, a mesma função do nó e2e — um shell e o stack nunca discordam. É o caminho para quem roda **fora** do Claude Code (terminal humano, CI, sub-agent sem hooks). As credenciais do arquivo apontado por `e2e.auth.envFile` entram no ambiente, mas **chave de layout de portas vinda de lá é ignorada** — o bloco alocado a esta worktree prevalece (um `.env.e2e` copiado de outra árvore carrega o bloco DELA). A chave ignorada é nomeada em **stderr**, nunca em stdout, para o `eval` do shell seguir limpo.
|
|
32
32
|
- **`morph-spec worktree remove [feature] [--target <path>] [--if-clean] [--discard-ignored] [--delete-branch]`** — a remoção **segura** de um worktree, sem fechar a feature. `--if-clean` recusa (`worktree-dirty`, exit 1, junctions intactas) se a árvore tiver qualquer mudança não commitada — é o que o hook `WorktreeRemove` usa, porque um sub-agent que não commitou ainda é o único exemplar do trabalho. Derruba o stack compose da árvore **sem** apagar volumes (a feature segue aberta) e libera o bloco de portas pela chave da branch (`<f>` ou `task/<n>`). Desmonta as junctions de infra ANTES de deixar o git chegar no diretório e recusa se não conseguir **provar** que é seguro (PROVE-or-DECLINE) — `git worktree remove` cru, com ou sem `--force`, atravessa a junction e apaga o `.claude/`/`.morph/framework/`/`node_modules/` **reais da raiz**, saindo com sucesso. Use `--target` para worktree sem feature registrada (os que o `isolation: "worktree"` cria). A branch **sobrevive** por padrão: remover o worktree não é descartar o trabalho. **Os gitignored são triados um a um**, nunca com um veredito só para o conjunto: o que existe na raiz no mesmo path com o **mesmo SHA-256** é replicável e some sem cerimônia (`replicatedInRoot`); artefato de build regenerável (`bin/`, `obj/`, `node_modules/`, `__pycache__/`, `test-results/`, `.auth/`, `*.tsbuildinfo`) é descartado (`discardedIgnored`); o que existe **só ali** é copiado para `.git/morph-spec/finish-backup/` e a remoção **prossegue** (`ignoredBackup` + `backedUp`). A única recusa por gitignored que sobrou é o **backup ter falhado** — e aí o `hint` diz o que é exclusivo, o que era replicável e para onde o backup ia, em vez de mandar rodar o comando que acabou de falhar. `--discard-ignored` deixou de significar "apague o exclusivo junto": agora significa **pular o backup**. As junctions desmontadas saem em `unlinked` — a prova de que o `.claude/` da raiz foi desligado, nunca seguido.
|
|
33
33
|
- **`morph-spec branch prune [--remote] [--dry-run] [--json]`** — apaga as branches locais cujo trabalho **já está** na default, **incluindo as squash-merged que o git não enxerga**. O problema que resolve: num repo que faz squash-merge, o commit de merge entra em `origin/master` mas os SHAs da branch não — `git branch --merged` não a lista, `git branch -d` recusa, e sobra o `-D`, indistinguível de descartar trabalho de verdade. Cada branch é classificada com evidência impressa: `merged-ancestral` (o git prova), `merged-squash` (PR `MERGED` pelo `gh`, **ou** árvore idêntica à da default), `open` (PR OPEN — nunca apagada), `in-worktree` (em uso — nunca apagada) e `unmerged` (**não foi possível provar** integração: sem `gh`, sem default resolvível ou trabalho de verdade — todas tratadas igual, de propósito). Só as duas primeiras são apagadas. Branch remota só com `--remote` explícito.
|
|
34
34
|
- **`morph-spec handoff <feature> [--here]`** — regenera o documento de retomada (`.morph/features/{f}/handoff.md`): comprimido, derivado, aponta para os artefatos em vez de copiá-los. **Resolve a árvore dona** antes de gerar (mesma lógica do `list`/`fleet`) e escreve LÁ, não necessariamente no cwd — rodar da raiz com o worktree vivo é seguro: o handoff sai correto e no lugar certo, em vez do esqueleto de nascimento da raiz. Sem árvore nenhuma materializada (só existe commitado em `morph/{f}`) recusa com `feature-not-in-tree`, nomeando a branch, em vez de gerar um documento apontando para arquivos inexistentes. `--here` força construir/escrever no cwd mesmo assim. Gerado automaticamente em `setup`/`create --worktree`/`advance`/`approve`.
|
|
@@ -38,15 +38,240 @@
|
|
|
38
38
|
## Validação
|
|
39
39
|
|
|
40
40
|
- **`morph-spec dag <feature>`** — análise do DAG de tasks.json com decisão sequencial vs paralelo **ponderada por effort** (raízes S vão inline, nunca ganham agente próprio). Também cruza `tasks.json` com os use cases: `usecase` apontando para um `UC-{nn}` inexistente é **erro** (exit 1, não despacha); pós-condição órfã e `doneCriteria` que parafraseou em vez de copiar são avisos. `--mermaid` nunca é bloqueado.
|
|
41
|
-
- **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--skip-e2e] [--fresh-e2e]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza. **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado.
|
|
42
|
-
- **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
|
|
41
|
+
- **`morph-spec verify <feature> [task] [--json] [--timeout <ms>] [--filter <ids>] [--record-baseline] [--refresh-cache] [--linux] [--linux-image <img>] [--skip-e2e] [--fresh-e2e]`** — cadeia determinística build+testes+validadores+**e2e** (código antes do juiz LLM); escopo de task quando informado, senão a feature inteira. O nó `e2e` é o único que **executa** a aplicação: sobe o `docker compose` do projeto, espera todo serviço ficar saudável e roda as specs Playwright quando existem (sem specs, ainda vale como smoke — "a app sobe" já pega o serviço não registrado que derruba o startup e passa batido no build). Aplicabilidade: escopo de feature (Gate 3) **sempre** roda; escopo de task roda para `group` `frontend`/`backend`/`infra` e dispensa `tests`/`docs`. **Dispensa legítima × setup faltando:** projeto sem compose e sem a integração `docker` é `skip` com razão; projeto que **declara** `docker` e não tem compose é `fail` — é verificação que nunca foi montada, não projeto que não containeriza. **Descoberta de testes e relógio:** a varredura por `*Tests.csproj` **não enxerga worktrees** — nem os convencionais (`worktrees/`, `.worktrees/`) nem os registrados em `git worktree list` fora da convenção. Um worktree é o checkout de OUTRA feature do mesmo repo: incluí-lo dobrava o relógio e reportava sobre código que não está sob verificação (em campo isso deu `tests: fail` no timeout exato, com o sumário da 1ª execução dizendo "Com falha: 0"). O timeout do nó `tests` deixou de ser fixo: `--timeout` (que agora cobre build **e** tests) > `project.testTimeoutMs` no `config.json` > **120 s por projeto de teste encadeado** — um encadeamento nunca nasce condenado pelo tamanho. Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum teste falhou`) em vez de devolver log truncado. **`--fresh-e2e` é destrutivo por definição:** derruba a stack anterior **com os volumes nomeados** (`down -v`), apaga o carimbo de frescor, sobe com `--build` e força o seed de auth a rodar de novo. Sem o `down -v`, "fresco" reconstruiria a imagem e preservaria o banco velho — e um `storageState` capturado contra aquele banco continuaria autenticando um usuário que não existe mais (403 que parece app quebrada). **`--filter <ids>` escolhe quais suítes rodam** (lista separada por vírgula, casando `id` exato ou substring): o loop por task usa `verify {f} {task} --filter {stack-da-task}` porque o relógio de uma task não paga a suíte inteira, e o Gate 3 usa `verify {f} --json` **sem filtro** porque a prova que o avaliador lê tem de cobrir todos os nós (o CI idem). O nó não casado **aparece** em `runs[]` como `skip` nomeando o filtro, em vez de sumir — suíte que some do relatório é verde sobre código não testado. E `--filter` que não casa nenhum nó é **erro de uso (exit 2)**, checado antes do build: filtrar tudo e reportar verde seria a forma mais barata de recriar o defeito que a lista de nós existe para matar. **`--record-baseline` é a ÚNICA via de escrita da baseline:** mede cada nó numa execução real e grava `project.tests[].expectedCount` de volta no `config.json` (escrita atômica `tmp.{pid}`+rename, preservando todo o resto do arquivo). A regra do `morph-plan` — *baseline é um COMANDO, nunca um número* — sobrevive porque ninguém digita o número: um valor escrito à mão descreve o que alguém acreditou, e a divergência entre a crença e a suíte é o defeito que a regra existe para matar. Recusa alto e claro em vez de improvisar: execução **vermelha** não grava (congelaria a regressão), execução **parcial** não grava (com `--filter` alguns nós ficam sem medida, e baseline de meia frota é afirmação sobre a outra metade que ninguém fez), contagem **desconhecida** não grava (`null` nunca vira `0`), e **sem `config.json` é erro de uso (exit 2)** — o arquivo nunca é criado implicitamente. Também **não** persiste o stamp `verification`: é manutenção de config, não prova de gate. **`--linux` roda as suítes dentro de um container Linux** (`mcr.microsoft.com/dotnet/sdk:10.0` por default; sobrescrevível por `--linux-image` ou por `project.tests[].linuxImage`), e a árvore sob teste é **copiada** para o filesystem do container, não montada: o bind mount do Docker Desktop no Windows atravessa 9p/gRPC-FUSE, que é **case-insensitive** — rodar em cima do mount devolveria o mesmo verde do Windows e não provaria nada sobre defeitos de caixa (`Link=`, `Uri.TryCreate`), que são exatamente os que só aparecem no Linux. A raiz é montada **read-only** em `/src` só como origem do `tar` (com exclusão de `node_modules`, `bin`, `obj`, `.git` e os diretórios-contêiner de worktree), a cópia vive em `/work`, e cada nó roda de `/work/{cwd}`. Sem Docker, **erro de uso (exit 2)**, jamais um `skip`: "não deu para checar no Linux" lido como verde é o próprio defeito. `--linux` com `--skip-tests` se anulam (erro de uso), e um nó `e2e: true` é pulado no container com motivo (docker-em-docker fora de escopo). O resultado marca `nodes.tests.environment = { kind: 'linux-container', image }` — sem isso, um verde de container e um verde de host viram indistinguíveis no `feature.json` e a próxima pessoa não sabe o que foi provado. Forma de `project.tests[]`, prefixo `[tests/{id}]` do `feedback[]`, regra de contagem/baseline e a divisão loop × suíte: seção **Suítes de teste** abaixo.
|
|
42
|
+
- **`morph-spec e2e up|status|down <feature>`** — ciclo de vida do stack de verificação, compartilhando toda a lógica com o nó `e2e` do verify. `up [--fresh]` reusa um stack quente **só** quando ele provadamente serve o código atual: o carimbo compara `HEAD` + `git status --porcelain` + `git diff HEAD`, então qualquer edição, commitada ou não, invalida o reuso (a exceção é o serviço com bind mount do código, que serve o disco por construção). Na dúvida, rebuild — o layer cache do Docker faz disso barato. O motivo impresso diz **o que** decidiu o reuso, e as duas procedências nunca se confundem: `source unchanged since build` é o carimbo, `source is bind-mounted` é o disco. E a segunda só sai quando **todo** serviço com seção `build:` monta a raiz do projeto — um sidecar Playwright montando `./e2e` ao lado de uma app compilada em imagem não fala pela stack inteira; nesse caso quem decide é o carimbo. `--fresh` derruba com volumes (`down -v`), apaga o carimbo e re-roda o seed. `status [--json]` responde a pergunta que decide se um verde vale alguma coisa (no ar? saudável? construído do código atual?) e sai 0 só com os três; o `baseUrl` do payload é o endereço real da app — é dele que a `morph-review` tira a URL que vai no prompt do avaliador, em vez de deixá-lo adivinhar `localhost:3000`. `down [--volumes]` é best-effort e nunca bloqueia. Isolamento entre worktrees: `COMPOSE_PROJECT_NAME=morph-{feature}` separa os containers e o bloco de portas do fleet (`env-allocator`) separa as portas — um compose com porta fixa não pode ser isolado, e isso é avisado. O bloco vale nos **dois** caminhos (este comando e o nó do `verify`): `PORT`, `MORPH_PORT_BASE`, `ASPNETCORE_URLS` em dotnet, mais `PLAYWRIGHT_BASE_URL` apontando para a porta alocada. Por isso o `playwright.config.ts` precisa ler a base URL do ambiente em vez de cravá-la — com a URL cravada, a suíte bate numa porta vazia e o `ECONNREFUSED` se disfarça de app quebrada.
|
|
43
43
|
- **`morph-spec doctor [--full] [--mcp] [--reset]`** — checagem de saúde padrão (sem `--worktrees`): instalação (versões, `.claude/`, hooks, agents, grafo de codebase, templates, `state.json`) mais duas auditorias.
|
|
44
44
|
**Ciclo de vida das features:** ordem de gates, feature sem identidade, pasta órfã, entrada de índice morta, `branch-drift` (a cópia da raiz atrasada em relação ao `morph/{f}` commitado) e `abandoned-scaffold`. Este último é o esqueleto que o `create --worktree` deixa para trás: `feature.json` sozinho, `in_progress`, **0 artefatos, 0 gates aprovados** e `morph/{f}` **sem commit próprio** (a branch ainda apontando para o commit de merge de outra PR). As quatro condições valem juntas e só depois de **24h** — recém-nascida tem exatamente a mesma forma, e alarme em todo `create` é alarme que ninguém lê. Fora de repo git, ou com a default branch indeterminável, o check cala (não dá para provar). Reparo: `morph-spec delete <f> --archive`, ou retome a feature.
|
|
45
45
|
**Pull requests:** `pr-awaiting-merge` (PR aberto há mais de 7 dias sem cópia local nesta árvore) e `pr-merged-not-landed` (PR já `MERGED` mas `.morph/archive/{f}` ainda não chegou aqui — sinal de `git pull` faltando). Offline-safe: sem `gh`, sem remoto no GitHub, ou qualquer falha, a checagem sai **não verificado** (nunca `ok`) — `[]` (perguntei, nada encontrado) e `null` (não pude perguntar) nunca se confundem, e o "não verificado" nunca deixa o `doctor` mais barulhento nem muda o exit code.
|
|
46
|
-
Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado).
|
|
47
|
-
- **`morph-spec doctor --
|
|
46
|
+
Ver `--worktrees` abaixo para a reconciliação de worktrees (modo separado), e `--ai` para a auditoria de projeto .NET com agentes.
|
|
47
|
+
- **`morph-spec doctor --ai`** — auditoria de um projeto **.NET com agentes**, em superfície própria: **não entra no `--full` nem no `doctor` default**. O renderer do `--full` manda tudo que não é `ok`/`warn` para `✗` (não tem o ramo `skip → ○`), então uma checagem de IA lá seria vermelha mesmo dizendo "não verificado"; e cinco linhas `○` permanentes no comando mais rodado do dia é ruído com custo e sem informação. Combinar `--ai` com `--full`/`--mcp`/`--worktrees` é **erro de uso**: exit 1, sem rodar nada — cada superfície tem o seu relatório e o seu código de saída.
|
|
48
|
+
|
|
49
|
+
**Cinco checagens.** `pin-drift` (o que o `.csproj` declara × o que `framework/ai-pin.json` fixa), `removed-api` (símbolos renomeados antes do 1.0 GA do `Microsoft.Agents.AI`), `pricing-missing` (todo alias do registry de modelos tem tarifa?), `api-per-alias` (todo alias declara `api`?) e `prompt-source` (as `Instructions` do agente têm fonte declarada?).
|
|
50
|
+
|
|
51
|
+
**Só `pin-drift` produz `error`, e só na faixa flutuante** — é a única classe com prejuízo medido: uma faixa `2.*` sobe sozinha para uma versão com assinatura mudada e o `MissingMethodException` chega **depois** de a chamada ao modelo ser paga. Versão fixa ≠ pin é `warn` nomeando as duas versões; pacote de IA presente no projeto e ausente do pin é `warn` descrito como **lacuna do pin**, nunca como defeito do projeto. As outras quatro checagens informam e nunca travam. **Exit code:** 1 com ao menos um `error`; 0 caso contrário — `warn` e `skip` nunca mudam o código de saída.
|
|
52
|
+
|
|
53
|
+
**Quatro condições de silêncio**, todas com razão obrigatória (um "não verificado" sem motivo é ruído): (1) nenhum `.csproj` alcançável — e aí **o pin nem é lido**; (2) nenhum `ai-pin.json` resolvível — falta de régua é ausência de dado, nunca defeito do projeto; (3) `.csproj` presentes e nenhum pacote de IA entre eles — um projeto .NET sem MAF não é um projeto MAF quebrado; (4) `.csproj` ilegível devolve **não verificado** para aquele arquivo, nunca "sem drift". Quando NENHUMA checagem teve insumo, o relatório sai em **uma linha** em vez de cinco `○` vazios. O gate é o **arquivo em disco**, nunca `project.stack` do `config.json`: o campo declara intenção, o arquivo é fato. Nada alcançável por `--ai` escreve — `readAiPin()` sim (leitura pura); os geradores que escrevem por padrão, jamais.
|
|
54
|
+
- **`morph-spec doctor --worktrees [--prune] [--json]`** — reconcilia o registro do git × as pastas em disco × as features conhecidas. Worktrees órfãos acumulavam **invisíveis**: o `fleet` só reporta worktrees em branch `morph/*`, então um worktree em outra branch (resíduo de finish anterior) não aparecia em lugar nenhum. `--prune` roda os reparos seguros: `git worktree prune` (registros de pastas que já sumiram), **remove os diretórios VAZIOS** em `worktrees/`/`.worktrees/` que o git não registra (`unregistered-dir` com `empty: true` — resíduo de finish; pasta com zero itens não tem o que perder, então some sem perguntar) **e** limpa o registro `worktree` de qualquer feature cujo `.path` aponte para um diretório já sumido (`dangling-registration` — a direção simétrica do `no-feature`: não é o worktree sem feature, é a feature com um ponteiro morto na raiz; a ordem é carregada: a pasta vazia sai primeiro, e o ponteiro que a nomeava é limpo no MESMO run). Apagar pasta que ainda **guarda** alguma coisa continua só reportado com o comando exato, nunca executado. **Nunca sugere remover worktree com trabalho não integrado:** antes de emitir `no-feature` **ou** `non-morph-branch` (os dois diagnósticos que sugeriam remoção), checa commits fora da default e PR aberto — havendo qualquer um, o diagnóstico vira `unintegrated-work` (severidade **info**, não conta como inconsistência e não faz o comando sair 1) e o `fix` diz para integrar, não para apagar. "Sem feature conhecida" é fato de bookkeeping (o `finish` arquivou a feature), não veredito sobre o trabalho — a versão anterior recomendou `worktree remove --target` no worktree que carregava o único exemplar de um PR aberto. **A quarta fonte não é um arquivo: as stacks docker.** Um container sobrevive à remoção do worktree, da branch e da feature — nenhuma das três reconciliações acima consegue vê-lo, e ele segue segurando o bloco de portas. Dois achados novos, ambos `severity: warn` (reportados, nunca falha do comando): `docker-stack-orphan` — projeto compose `morph-*` **deste repositório** (o arquivo compose fica sob a raiz; o prefixo `morph-` é global no host, e reportar a stack de outro repo seria sugerir `down -v`, com volumes, na stack viva do vizinho) de pé para uma feature que esta árvore não conhece, com o teardown exato (`docker compose -p morph-{slug} down -v`); e `docker-port-held` — porta do bloco de uma feature (derivada de `portLayout`, nunca recalculada) publicada por um container de **outro** projeto compose, nomeando o container que a segura. A auditoria devolve **nada** sem tentar coisa alguma em três situações: nenhuma feature alocou bloco de porta (sem registro em `<git-common-dir>/morph-spec/`), docker indisponível, ou qualquer chamada ao docker saindo != 0 / respondendo algo que não é JSON. Por isso ela é estruturalmente silenciosa num repo que não containeriza — e por isso ela **não** entra no `doctor --full`, onde toda checagem tem de ser satisfazível.
|
|
48
55
|
- **`morph-spec report-check <feature> [task] [--json]`** — audita os relatórios de sub-agent contra o trabalho que realmente entrou. **Relatório ausente NÃO quer dizer que o agente falhou** — ele pode ter feito tudo e perdido o meio de gravar. E relatório PRESENTE não basta: `report-stale` é o relatório completo porém gravado ANTES do fim do trabalho (o agente relatou cedo e continuou escrevendo) — completo e mentindo. O comando responde a pergunta certa (`work-landed-report-missing` → aceite degradado, NÃO re-despache; `no-work-no-report` → re-despache uma vez; `report-stale` → atualize a partir do campo `observed`, NÃO re-despache). A evidência de `observed` vem do `events.jsonl` (todo `Write`/`Edit` observado na janela da task, cruzada com `task-times.json`), não de `outputs`, que é auto-declarado pelo próprio agente — writes do orquestrador sob `.morph/` e o do próprio relatório são excluídos. Sem telemetria no projeto, o comando volta ao comportamento anterior (fail-open). Protocolo completo em MORPH.md §6.
|
|
49
56
|
|
|
57
|
+
## Suítes de teste (`project.tests[]`) e a divisão loop × suíte
|
|
58
|
+
|
|
59
|
+
O nó `tests` do `verify` é **plural**: um projeto pode declarar quantas suítes tiver em
|
|
60
|
+
`.morph/config/config.json → project.tests[]`, e cada uma vira um nó com comando, diretório, relógio e
|
|
61
|
+
baseline próprios. Contrato declarativo em `.morph/framework/schemas/config.schema.json`; a autoridade
|
|
62
|
+
em runtime é `validateTestNodes()` — schema que ninguém chama é decoração.
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"project": {
|
|
67
|
+
"tests": [
|
|
68
|
+
{ "id": "dotnet", "runner": "dotnet", "cwd": ".",
|
|
69
|
+
"command": "dotnet test tests/App.Tests/App.Tests.csproj --nologo -v q --no-build",
|
|
70
|
+
"build": ["tests/App.Tests/App.Tests.csproj"], "expectedCount": 3057, "timeoutMs": 600000 },
|
|
71
|
+
{ "id": "vitest", "runner": "vitest", "cwd": "catalog-admin",
|
|
72
|
+
"command": "npx vitest run --reporter=default", "expectedCount": 3082 },
|
|
73
|
+
{ "id": "playwright", "runner": "playwright", "cwd": "catalog-admin",
|
|
74
|
+
"command": "npx playwright test", "e2e": true }
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Campo | Obrigatório | Semântica |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `id` | **sim** | `^[a-z0-9][a-z0-9-]*$`, único no array. É o que `--filter` casa e a chave de `runs[]`. `id` duplicado é erro de config, não empate silencioso — **os dois** lados falham |
|
|
83
|
+
| `command` | **sim** | Comando literal, executado por shell a partir de `cwd`. Nunca é reescrito pelo verify |
|
|
84
|
+
| `cwd` | não (`"."`) | Relativo à raiz. Path que resolve **fora** da raiz é recusado (endereça outra árvore); `cwd` inexistente é `fail`, não `skip` — um skip esconderia erro de plano atrás de gate verde |
|
|
85
|
+
| `runner` | não (inferido) | `dotnet` \| `vitest` \| `jest` \| `node-test` \| `playwright` \| `none` \| `evals`. Escolhe o extrator de contagem e se há prebuild. `none` = contagem DESCONHECIDA, nunca zero. `evals` é o único que **nunca é inferido** e o único que exige um bloco próprio — ver a subseção abaixo |
|
|
86
|
+
| `expectedCount` | não | Baseline. **Gravada só por `verify --record-baseline`**, nunca digitada. Ausente = `null` (sem baseline); `0` **declarado** é uma baseline real |
|
|
87
|
+
| `timeoutMs` | não | Teto deste nó. Existe porque o default derivado (120 s × projetos encadeados) é desenhado para `dotnet test` e é hostil a uma suíte Node de doze minutos |
|
|
88
|
+
| `build` | não | Projetos a compilar antes (só `runner: "dotnet"`). Declarado em nó não-dotnet é erro de config, não campo silenciosamente ignorado |
|
|
89
|
+
| `e2e` | não (`false`) | Exige a stack de pé. Roda em escopo de feature; é pulado **com motivo** no loop por task e dentro do `--linux` |
|
|
90
|
+
| `linuxImage` | não | Imagem do container deste nó sob `--linux` |
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
### O runner `evals`: regressão de prompt e de modelo com cache commitada
|
|
94
|
+
|
|
95
|
+
Um eval **é** `dotnet test` com um filtro de categoria — mesmo comando, mesmo `cwd`, mesmo relógio,
|
|
96
|
+
mesmo código de saída. Por isso ele entra como um **sétimo valor de `runner`** dentro de
|
|
97
|
+
`project.tests[]`, e não como um sexto nó do `verify`: assim herda `--filter`, `--linux`,
|
|
98
|
+
`expectedCount`, o prefixo `[tests/{id}]` e o rollup sem uma segunda cópia de nada.
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"project": {
|
|
103
|
+
"tests": [
|
|
104
|
+
{ "id": "unit", "command": "dotnet test tests/App.Tests/App.Tests.csproj" },
|
|
105
|
+
{ "id": "evals", "runner": "evals",
|
|
106
|
+
"command": "dotnet test tests/App.Tests/App.Tests.csproj --filter-trait Category=Eval",
|
|
107
|
+
"evals": { "cachePath": "tests/App.Tests/eval-cache",
|
|
108
|
+
"datasetPath": "tests/App.Tests/eval-datasets",
|
|
109
|
+
"envFile": ".env.evals" },
|
|
110
|
+
"timeoutMs": 900000 }
|
|
111
|
+
],
|
|
112
|
+
"evals": { "watch": ["tenants/**", "product/**", "config/model-registry.json"] }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Campo | Obrigatório | Semântica |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| `runner: "evals"` | **sim** | O único valor de `runner` que **nunca é inferido**: um comando de eval é literalmente `dotnet test`, e farejá-lo classificaria toda suíte dotnet como eval — pior, o erro inverso (eval declarado e lido como dotnet) derrubaria o pré-voo da cache em silêncio |
|
|
120
|
+
| `evals.cachePath` | **sim** | Raiz da árvore de cache de respostas, relativa à raiz do projeto, e **commitada**. Fora da raiz é recusado, mesma regra do `cwd`. **Sem default**: um default silencioso criaria árvore de cache num lugar que ninguém declarou. **Prefira um caminho raso**: as entradas ficam em `cache/<cenário>/<iteração>/<chave-curta>/`, e no Windows um caminho absoluto acima de 260 chars faz o `git` avisar `Filename too long` **no stderr**, commitar parte dos arquivos e sair com código 0 — medido: 1 de 9 arquivos no commit, com todo canário sobre a árvore reportando limpo por nunca tê-la enxergado. A árvore também precisa de `<cachePath>/** -text` no `.gitattributes`, senão o `text=auto` normaliza os payloads opacos e os bytes não sobrevivem ao clone |
|
|
121
|
+
| `evals.datasetPath` | não | Dataset versionado, em `{agente}/{tenant}/{cenário}.json`. Default: `eval-datasets` ao lado de `cachePath` |
|
|
122
|
+
| `evals.envFile` | não (`.env.evals`) | Arquivo **gitignored** com a credencial, lido **só** por `--refresh-cache`. Mesmo padrão de `e2e.auth.envFile` — credencial nunca viaja no config commitado |
|
|
123
|
+
| `project.evals.watch[]` | não | Globs que significam "isto é prompt ou modelo". Casados contra o que a feature mudou (diff contra a default **mais** arquivos novos não rastreados) e lidos pela regra de Gate 3 |
|
|
124
|
+
|
|
125
|
+
**O que o CLI dita, e por quê.** O nó recebe `MORPH_EVAL_CACHE`, `MORPH_EVAL_DATASET`,
|
|
126
|
+
`MORPH_EVAL_RESULTS`, `MORPH_EVAL_MODE`, `MORPH_EVAL_SUMMARY`, `MORPH_EVAL_ENVFILE` e
|
|
127
|
+
`MORPH_EVAL_PIN`. O caminho do sumário é escolhido **pelo CLI** (arquivo temporário) para que dois nós
|
|
128
|
+
de eval no mesmo `verify` não colidam, e `MORPH_EVAL_PIN` leva só os pacotes que decidem o **formato**
|
|
129
|
+
da cache — a família `Microsoft.Extensions.AI` — para que subir um pacote de provider não invalide uma
|
|
130
|
+
árvore por nada.
|
|
131
|
+
|
|
132
|
+
**Os estados da cache são julgados ANTES de o comando rodar**, e todos os quatro são `fail` nomeado,
|
|
133
|
+
**nunca** `skip`:
|
|
134
|
+
|
|
135
|
+
| Estado | Motivo |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| árvore ausente | `eval-cache-missing: {caminho}` |
|
|
138
|
+
| árvore sem nenhuma entrada | `eval-cache-empty: {caminho}` — distinto de ausente: diretório commitado vazio é mais provavelmente engano de `.gitignore` |
|
|
139
|
+
| sem carimbo `.morph-eval-cache.json` | `eval-cache-unstamped: …` — a árvore não diz sob quais versões foi gravada |
|
|
140
|
+
| carimbo divergente do `ai-pin.json` | `eval-cache-stale-pin: {gravada} vs {pin}` — nomeia **as duas** versões |
|
|
141
|
+
| `--refresh-cache` sem o `envFile` | `eval-refresh-no-credential: …`, e a árvore fica **intacta** |
|
|
142
|
+
|
|
143
|
+
Pagar o relógio de uma suíte inteira para descobrir no fim que a cache não servia é a pior ordem
|
|
144
|
+
possível. E um `skip` aqui seria gate verde sobre verificação que não aconteceu — o defeito que a
|
|
145
|
+
lista plural de nós existe para matar.
|
|
146
|
+
|
|
147
|
+
**Escopo.** O nó de eval **não roda no `verify` por task**: é pulado com
|
|
148
|
+
`eval node runs at feature scope only`, o mesmo tratamento de `e2e: true`. O relógio de uma task não
|
|
149
|
+
paga uma suíte de eval, e o resultado que o Gate 3 lê é o de escopo de feature.
|
|
150
|
+
|
|
151
|
+
**Contagem.** O `scenarios` do sumário vence quando legível; sem sumário legível, o extrator `dotnet`
|
|
152
|
+
decide (um eval **é** `dotnet test`). Nos dois caminhos, **`null` é DESCONHECIDO, jamais zero** — um
|
|
153
|
+
sumário ilegível não pode fabricar uma queda contra `expectedCount`. E um sumário dizendo
|
|
154
|
+
`scenarios: 0` com saída 0 é `fail` (`eval-empty-run`): nada falhou porque nada rodou.
|
|
155
|
+
|
|
156
|
+
**Cache fria avisa, não reprova.** Um run que foi ao provider marca `cache: "cold"` e emite warning de
|
|
157
|
+
que a árvore mudou e precisa ser commitada. Gastar dinheiro sem pedir é ruim; mentir sobre o resultado
|
|
158
|
+
é pior — e sem o aviso a cache nova ficaria só no disco de quem rodou.
|
|
159
|
+
|
|
160
|
+
**`--refresh-cache` regrava a árvore contra o provider real, e nunca é implícito** — regravação
|
|
161
|
+
automática é chamada paga que ninguém pediu. Três recusas, todas **erro de uso (exit 2)** julgadas
|
|
162
|
+
**antes** de qualquer mutação (a mesma disciplina do `finish --merge`, que valida a raiz antes de
|
|
163
|
+
arquivar): nenhum nó `runner: "evals"` declarado (sucesso silencioso ensinaria a rodar uma flag que não
|
|
164
|
+
faz nada), junto com `--skip-tests` (as duas se anulam), e um `--filter` que exclui todos os nós de
|
|
165
|
+
eval. A ausência de credencial é julgada **por nó** no pré-voo, ainda antes do comando.
|
|
166
|
+
|
|
167
|
+
**`--linux` com nó de eval declarado é erro de uso (exit 2).** O canal `MORPH_EVAL_*` carrega caminhos
|
|
168
|
+
**absolutos do host**, e o container roda uma **cópia** da árvore em `/work`: rodar assim mesmo falharia
|
|
169
|
+
por um motivo que não tem nada a ver com o eval, e pular seria verde sobre nada. A saída é nomeada —
|
|
170
|
+
rode o eval fora do container, ou exclua-o com `--filter`.
|
|
171
|
+
|
|
172
|
+
**O stamp ganha `nodes.evals`** (`pass` | `fail` | `skip` | `null`), derivado das entradas de `runs[]`
|
|
173
|
+
com `runner: "evals"`. `null` é "este projeto não declara nó de eval" e é **distinto** de `skip`
|
|
174
|
+
("declara, e esta corrida não o executou") — o `gate-check` pausa o Gate 3 por razões diferentes nos
|
|
175
|
+
dois casos, porque as ações são diferentes. Quem lê `verification.nodes.tests` continua lendo a mesma
|
|
176
|
+
string.
|
|
177
|
+
|
|
178
|
+
**A regra de Gate 3 PAUSA, nunca bloqueia.** Feature que tocou caminho de `project.evals.watch[]` sem
|
|
179
|
+
eval verde carimbado no mesmo commit faz o gate de `review` pausar, com a razão nomeando o caminho
|
|
180
|
+
tocado. `evalsStatus` e `promptModel` são **fatos de disco** e **não** entram em
|
|
181
|
+
`OVERRIDABLE_SIGNAL_KEYS`: um `--signals` capaz de afirmar "o eval rodou" derrotaria a política que a
|
|
182
|
+
regra existe para tornar determinística. E nada disso produz número — a procedência do score composto
|
|
183
|
+
continua sendo, exclusivamente, o token gravado pelo avaliador independente.
|
|
184
|
+
|
|
185
|
+
Conteúdo (o que é um bom dataset, o que entra na chave de cache, TTL, o que se commita):
|
|
186
|
+
`ai-agents/evals-with-cache.md`.
|
|
187
|
+
|
|
188
|
+
**Fail-closed, de propósito.** Um nó malformado não é ignorado: vira `status: 'fail'` com motivo
|
|
189
|
+
`invalid-test-node: {id}: {problema}`, e reprova o gate **mesmo filtrado** por `--filter` (filtrar é
|
|
190
|
+
"não gaste relógio com esta suíte", não "não me conte que meu config está quebrado"). O regime antigo —
|
|
191
|
+
`JSON.parse` + optional chaining fail-open — fazia um typo em `tests` sumir sem aviso e devolvia o
|
|
192
|
+
projeto ao comportamento anterior, que é exatamente o defeito.
|
|
193
|
+
|
|
194
|
+
**`tests: []` ≠ `tests` ausente.** O array vazio é declaração explícita de "não há suítes" (um nó
|
|
195
|
+
`skip`); a chave **ausente** cai no fallback de quatro passos, que continua íntegro e é o caminho de
|
|
196
|
+
todo projeto criado antes desta chave: `project.testCommand` → `.sln`/`.csproj` na raiz → varredura de
|
|
197
|
+
`*Tests.csproj` (que **não enxerga worktrees**) → `package.json` com `scripts.test`. Sem `config.json`
|
|
198
|
+
nenhum, o `verify` resolve exatamente **um** nó `default` — o repositório-fonte do morph-spec roda
|
|
199
|
+
assim, e nada aqui depende do arquivo existir.
|
|
200
|
+
|
|
201
|
+
**Forma do resultado.** `nodes.tests` continua um objeto com `status` (o rollup) e ganha `runs[]` — uma
|
|
202
|
+
entrada por nó, com `id`, `command`, `cwd`, `status`, `durationMs`, `count`, `expectedCount` e
|
|
203
|
+
`warnings[]` — mais `source` e `environment`. **Um verde nunca esconde um vermelho:** o rollup reprova
|
|
204
|
+
se qualquer run reprovar, e um nó que estoura o relógio não interrompe os seguintes. As linhas de
|
|
205
|
+
`feedback[]` são prefixadas por **`[tests/{id}]`** (era `[tests]`): com N suítes, uma linha que não diz
|
|
206
|
+
**qual** falhou manda o leitor para a metade errada do repositório.
|
|
207
|
+
|
|
208
|
+
**Contagem e a regra assimétrica.** O extrator lê o sumário do runner (somando sumários encadeados no
|
|
209
|
+
`dotnet`, e exigindo a linha `Tests` — não `Test Files` — no vitest). Contra `expectedCount`: **queda
|
|
210
|
+
reprova** (mesmo com exit 0 — é regressão silenciosa), **alta apenas avisa** e pede regravação
|
|
211
|
+
(reprovar a alta ensinaria o agente a apagar testes), e contagem **desconhecida** é `null`, jamais `0`
|
|
212
|
+
— um zero fabricaria queda a partir de "não deu para ler o output". O aviso de alta sai **mesmo com o
|
|
213
|
+
nó verde**, que é o único lugar onde ele pode existir.
|
|
214
|
+
|
|
215
|
+
**Prebuild e `--no-build` (por que um `dotnet test` verde podia mentir).** Num nó `runner: "dotnet"`,
|
|
216
|
+
os alvos de `build[]` — ou, na falta, os `.csproj` extraídos do próprio `command` — são compilados
|
|
217
|
+
antes da execução, com `dotnet build "<alvo>" --nologo -v q`. Compilação que falha reprova o nó com
|
|
218
|
+
`test-build-failed` e a suíte **não roda**. E um `command` com `--no-build` que **não** foi precedido de
|
|
219
|
+
build nesta invocação reprova com `stale-binary`, também sem executar: o predicado é "houve build
|
|
220
|
+
aqui", nunca "a flag está ausente". Sem isso, um projeto de teste que deixou de compilar era executado
|
|
221
|
+
a partir do binário do último build bom e reportava verde. `--no-build` continua legítimo — com
|
|
222
|
+
`build[]` declarado ele roda normalmente, e o comando do usuário nunca é reescrito. O prebuild é
|
|
223
|
+
**condicional** (só com `build[]` declarado ou com `--no-build` no comando): um `dotnet test X.csproj`
|
|
224
|
+
sem `--no-build` já compila sozinho, e pré-compilar ali só dobraria o relógio do caminho quente.
|
|
225
|
+
|
|
226
|
+
**O ciclo da baseline, e o número que só sai à mão.** `verify --record-baseline` é a única via de
|
|
227
|
+
escrita: mede cada nó numa execução real e grava `expectedCount` de volta no `config.json` (escrita
|
|
228
|
+
atômica `tmp.{pid}` + rename, preservando todo o resto do arquivo). Recusa **quatro** vezes — execução
|
|
229
|
+
vermelha, execução **parcial** (sob `--filter`, mesmo com o verify verde: baseline de meia frota é
|
|
230
|
+
afirmação sobre a outra metade que ninguém fez), contagem desconhecida, e **sem `config.json` ou sem
|
|
231
|
+
`project.tests[]`** (erro de uso, exit 2; o arquivo nunca é criado implicitamente — gravar os nós que o
|
|
232
|
+
fallback sintetiza exigiria inventar a declaração que o comando veio manter). Verde com gravação
|
|
233
|
+
recusada sai **exit 1**, com a razão em **stderr** (sobrevive ao `--json`). A gravação é governada
|
|
234
|
+
pelos **nós de teste**, não pelos quatro nós do verify: um `validators` vermelho não muda quantos
|
|
235
|
+
testes rodaram. E o comando **não** persiste o stamp `verification` — é manutenção de config, não prova
|
|
236
|
+
de gate; não o use como atalho de Gate 3. **Consequência a conhecer:** um `expectedCount` digitado à
|
|
237
|
+
mão **alto demais** (9 onde a suíte roda 7) deixa o verify vermelho por `count-drop`, e então
|
|
238
|
+
`--record-baseline` recusa — preservando o 9. **Não há flag de regravação forçada:** rebaixe ou apague
|
|
239
|
+
o número à mão e então re-meça pelo comando. O caso oposto se conserta sozinho — número baixo demais
|
|
240
|
+
passa com aviso de alta, e o comando grava.
|
|
241
|
+
|
|
242
|
+
**A divisão loop × suíte.** É a resposta ao "a suíte inteira não cabe numa chamada":
|
|
243
|
+
|
|
244
|
+
| Momento | Comando | Por quê |
|
|
245
|
+
| --- | --- | --- |
|
|
246
|
+
| Loop por task | `verify {f} {task} --filter {stack-da-task}` | O relógio de uma task não paga a suíte inteira; a task tocou **um** stack |
|
|
247
|
+
| Gate 3 | `verify {f} --json` (sem filtro) | A prova que o avaliador lê tem de cobrir **todos** os nós |
|
|
248
|
+
| CI | a suíte inteira, todos os nós | idem — e é onde as duas plataformas rodam |
|
|
249
|
+
|
|
250
|
+
> **Não existe constante de 600 s neste CLI** (`grep -rn "600000" src/ framework/` → zero). O teto que
|
|
251
|
+
> corta uma suíte longa é o do **Bash do harness**, externo ao morph-spec, e não há o que "subir". Os
|
|
252
|
+
> relógios que o CLI de fato tem: build **120 000 ms**, tests **120 000 ms por projeto de teste
|
|
253
|
+
> encadeado**, health do e2e **180 000 ms**, specs Playwright **300 000 ms**, mais `timeoutMs` por nó,
|
|
254
|
+
> que substitui o default do `tests` quando ele não serve. `--timeout <ms>` cobre build **e** tests.
|
|
255
|
+
> **A suíte inteira de um projeto grande não cabe numa invocação de Bash do harness:** rode-a em
|
|
256
|
+
> background, ou por partes com `--filter`, e deixe a execução completa para o Gate 3 e para o CI.
|
|
257
|
+
> Quando o relógio estoura, o nó diz isso com nome (`timeout após N ms na execução X de Y — nenhum
|
|
258
|
+
> teste falhou`) em vez de devolver log truncado.
|
|
259
|
+
|
|
260
|
+
## Instalação e atualização
|
|
261
|
+
|
|
262
|
+
- **`morph-spec update [--templates] [--standards] [--dry-run] [--force]`** — reinstala os payloads do framework no projeto (standards, hooks, schemas, rubrics, evals, `.claude/{commands,rules,skills,agents}`, `.claude/CLAUDE.md`, `.gitignore`/`.gitattributes`, statusline global).
|
|
263
|
+
|
|
264
|
+
**`--dry-run` prevê sem tocar em nada.** Previsão e execução são o **mesmo percurso**, com os **mesmos filtros** e o **mesmo coletor de efeitos**: em modo normal cada verbo executa *e* registra, em dry apenas registra. Não há travessia paralela — o resumo de uma execução real também sai do coletor, de modo que um ponto de emissão esquecido some das duas saídas ao mesmo tempo, em vez de mentir só no `--dry-run`.
|
|
265
|
+
|
|
266
|
+
O relatório agrupa por natureza do efeito (apagar, criar diretório, copiar, escrever, mesclar, executar, índice do git) e **nomeia separadamente três grupos que ninguém adivinha da palavra "update"**:
|
|
267
|
+
- **apagados por inteiro antes da reinstalação** — `.claude/{commands,rules,skills,agents}` e `.morph/{framework/standards,framework/hooks,framework/schemas,framework/rubrics,framework/evals,config}`, com a contagem de arquivos que hoje moram em cada um;
|
|
268
|
+
- **fora do projeto** — `~/.claude/statusline.sh`, `~/.claude/statusline.py` e a mesclagem de `~/.claude/settings.json`;
|
|
269
|
+
- **dentro do pacote da CLI** — `generate-refs.js` escrevendo `framework/hooks/shared/phase-utils.js` do pacote, que num `npm link` de desenvolvimento é o próprio repositório do framework. Em dry roda a variante `--check`, que verifica e não escreve.
|
|
270
|
+
|
|
271
|
+
O `--dry-run` **passa por cima** da saída antecipada de "CLI desatualizada": bloquear o relatório justamente quando o usuário está decidindo se atualiza seria inútil — o aviso vira a primeira linha do relatório. `init` delega a `updateCommand` e **nunca** passa `dryRun`.
|
|
272
|
+
|
|
273
|
+
**Freio de major.** Um salto de **dois majors ou mais** é recusado sem `--force`, **antes de qualquer mutação** — recusar depois de mutar é meio-estado. A versão instalada vem de `.morph/.morphversion → morphVersion`, o mesmo registro que o `update` já lê; **nunca** de `config.json → frameworkVersion`, que o `update` escreve e jamais lê para decidir (medido: os dois divergem por um major inteiro num projeto real). Quando os dois discordam, a mensagem imprime **os dois** e diz qual decidiu — escolher em silêncio é acertar metade das vezes e nunca saber qual metade. `.morphversion` ausente ou ilegível é tratado como salto grande, com motivo próprio (`versão instalada indeterminável`) distinto do salto medido: "não sei quão velho isto é" é precisamente quando sobrescrever às cegas é mais perigoso. `--force` dispensa o freio e o relatório final **registra a dispensa**. `--dry-run` combinado com uma recusa **imprime o relatório**, com a recusa como primeiro item.
|
|
274
|
+
|
|
50
275
|
## Grafo de codebase
|
|
51
276
|
|
|
52
277
|
Grafo de símbolos extraído localmente pelo [graphify](https://pypi.org/project/graphifyy/) (versão pinada **0.9.50**, AST via tree-sitter, **zero token**), servido atrás de uma camada de saneamento própria. **Tudo é fail-open:** sem Python, sem grafo, com grafo velho ou corrompido, o framework segue por grep — é assim que ele sempre funcionou, e nenhuma skill pode exigir o grafo (ADR-002).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Rubricas de prova (`proof-rubrics/`)
|
|
2
|
+
|
|
3
|
+
Sete rubricas pontuáveis **0-10** que o avaliador independente do **Gate 3** aplica sobre a
|
|
4
|
+
EVIDÊNCIA que ele mesmo produziu — não sobre o produto.
|
|
5
|
+
|
|
6
|
+
**Elas não são dimensões do score composto.** O composto continua sendo
|
|
7
|
+
`arquitetura + contratos + qualidade + testes`, com os pesos declarados em `../rubrics/`.
|
|
8
|
+
|
|
9
|
+
São de dois tipos, e a diferença está em **onde** cada uma impõe teto:
|
|
10
|
+
|
|
11
|
+
| Tipo | Quando se aplica | Onde impõe teto |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| **Universais** — `mutation-proof`, `verde-nao-e-coberto`, `fiacao`, `guard-cego`, `eval-de-modelo` | sempre | *Cobertura de Testes* (`../rubrics/test-coverage.md`, seção "Tetos de prova (Gate 3)"). O **menor das cinco** é o que vale — todas perguntam a mesma coisa: *a evidência é real?* |
|
|
14
|
+
| **Condicional** — `media-claims` | só em feature que **gera mídia** | *Arquitetura* (`../rubrics/architecture.md`, seção "Tetos de prova de mídia"). Entra **sozinha**, fora do menor-das-cinco |
|
|
15
|
+
| **Condicional** — `observabilidade-genai` | só em feature que **chama LLM**. Duas etapas, e ela **não** se declara determinística: filtro mecânico de alta recall (o mandate cita um standard `ai-agents`) **+** determinação escrita pelo avaliador, com o motivo. `not_evaluated` sem motivo não é dispensa | *Qualidade de Código* (`../rubrics/code-quality.md`, seção "Teto de prova de observabilidade GenAI"). Entra **sozinha**, fora do menor-das-cinco |
|
|
16
|
+
|
|
17
|
+
As duas condicionais ficam **fora** do mínimo de propósito: as afirmações que elas cobram são
|
|
18
|
+
**propriedades estruturais** — onde a chamada mora, que estados existem, o que é persistido, o que é
|
|
19
|
+
instrumentado —, não perguntas sobre a evidência ser real. Somá-las faria um `media=4` ou um `obs=4`
|
|
20
|
+
derrubar o teto de TESTE por um motivo que não tem nada a ver com teste.
|
|
21
|
+
|
|
22
|
+
## Por que num diretório próprio
|
|
23
|
+
|
|
24
|
+
`scripts/check-rubric-parity.js` varre **todo** `.md` de `../rubrics/` (exceto `README.md` e
|
|
25
|
+
`condensed.md`) e exige o H1 `# Rubrica: {Nome} (Peso {N}%)`, com a soma dos pesos em 100. Um
|
|
26
|
+
arquivo destes largado lá reprova o `npm run validate` com um erro de extração — e "consertar"
|
|
27
|
+
dando-lhe um peso obrigaria a rebalancear os quatro pesos em oito lugares, mudando o significado de
|
|
28
|
+
todo score histórico da base.
|
|
29
|
+
|
|
30
|
+
Por isso o H1 daqui é deliberadamente **`# Prova: {Nome}`**: se alguém mover um destes arquivos
|
|
31
|
+
para `../rubrics/`, o erro é imediato e nomeado, nunca um desvio silencioso de peso.
|
|
32
|
+
|
|
33
|
+
## Consumidores (cada rubrica nasce com dono)
|
|
34
|
+
|
|
35
|
+
| Rubrica | Quem lê | Quem cobra |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `mutation-proof.md` | prompt de dispatch do avaliador — `skills/core/morph-review/SKILL.md` §4 | `src/lib/risk-detector.js` (Gate 3 pausa sem a seção `## Mutações`) + teto em `../rubrics/test-coverage.md` |
|
|
38
|
+
| `verde-nao-e-coberto.md` | prompt de dispatch do avaliador — `morph-review` §4 | teto em `../rubrics/test-coverage.md` |
|
|
39
|
+
| `fiacao.md` | prompt de dispatch do avaliador — `morph-review` §4 | teto em `../rubrics/test-coverage.md`; cita `verify` `nodes.tests.runs[].count` por `id` |
|
|
40
|
+
| `guard-cego.md` | prompt de dispatch do avaliador — `morph-review` §4 | teto em `../rubrics/test-coverage.md` |
|
|
41
|
+
| `eval-de-modelo.md` | prompt de dispatch do avaliador — `morph-review` §4 | teto em `../rubrics/test-coverage.md`; a superfície que build e teste unitário não leem |
|
|
42
|
+
| `media-claims.md` | prompt de dispatch do avaliador — `morph-review` §4, **só quando a feature gera mídia** | `src/lib/risk-detector.js` (Gate 3 pausa sem a seção `## Mídia gerada`) + teto em `../rubrics/architecture.md` |
|
|
43
|
+
| `observabilidade-genai.md` | prompt de dispatch do avaliador — `morph-review` §4, sob a condição de aplicabilidade | teto em `../rubrics/code-quality.md`; a régua da propriedade está em `standards/ai-agents/observability-patterns.md` |
|
|
44
|
+
|
|
45
|
+
`test/framework/proof-rubrics.test.js` é o canário: nenhuma rubrica de prova pode ficar **órfã**
|
|
46
|
+
(citada por ninguém), nem migrar para o diretório de dimensões.
|
|
47
|
+
|
|
48
|
+
## Como o avaliador reporta
|
|
49
|
+
|
|
50
|
+
Duas coisas, no `4-review/evaluator-report.md`:
|
|
51
|
+
|
|
52
|
+
1. A seção `## Mutações` — obrigatória quando a feature tem código de produção (ao menos uma task
|
|
53
|
+
com `group` diferente de `docs`). Sem ela, o `morph-spec gate-check {feature} review` **pausa**.
|
|
54
|
+
2. A seção `## Mídia gerada` — obrigatória **só** quando a feature gera mídia (ao menos uma task
|
|
55
|
+
cujo `standards[]` cite um dos três standards de mídia). Sem ela, o `gate-check` também **pausa**;
|
|
56
|
+
feature que não gera mídia é isenta automaticamente, sem ninguém precisar lembrar.
|
|
57
|
+
3. A linha machine-readable, no mesmo estilo do `**Composite-Score:**`:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
**Proof-Scores:** mutation=9 verde=8 fiacao=9 guard=7 eval=8 media=9
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
E, quando a feature chama LLM, a mesma linha carrega também a chave `obs`:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
**Proof-Scores:** mutation=9 verde=8 fiacao=9 guard=7 obs=8
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
O **menor** de `mutation`/`verde`/`fiacao`/`guard`/`eval` é o que impõe o teto em Cobertura —
|
|
70
|
+
as cinco perguntam a mesma coisa: *a evidência é real?*. Chave ausente não vira zero: o mínimo é
|
|
71
|
+
tirado sobre as chaves PRESENTES, para que um relatório antigo de quatro chaves continue legível.
|
|
72
|
+
|
|
73
|
+
As chaves `media` e `obs` são lidas **à parte** e não entram naquele mínimo: `media` limita
|
|
74
|
+
**Arquitetura** e só aparece quando a feature gera mídia; `obs` limita **Qualidade de Código** e só
|
|
75
|
+
aparece quando a feature chama LLM. Ausente a chave (ou com `obs=not_evaluated`), não há teto
|
|
76
|
+
daquela família a aplicar.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Prova: Eval de Modelo
|
|
2
|
+
|
|
3
|
+
> Escopo: Gate 3 (avaliador independente). Não é dimensão do score composto — impõe **teto** em
|
|
4
|
+
> Cobertura de Testes (`evals/rubrics/test-coverage.md`, seção "Tetos de prova").
|
|
5
|
+
|
|
6
|
+
## O que esta prova responde
|
|
7
|
+
|
|
8
|
+
Prompt e modelo são a única parte do produto cujo comportamento **nem o build nem o teste unitário
|
|
9
|
+
leem**: os dois continuam verdes com o prompt inteiro trocado. O eval existe para cobrir
|
|
10
|
+
exatamente essa superfície — e é por isso que ele é o artefato que mais facilmente vira teatro.
|
|
11
|
+
|
|
12
|
+
A pergunta NÃO é "o eval rodou?", nem "quantos cenários passaram?". É:
|
|
13
|
+
|
|
14
|
+
**o que este eval reprovaria, que hoje ele aprova?**
|
|
15
|
+
|
|
16
|
+
Um eval que não sabe responder isso mede **execução**, não **detecção** — e a diferença entre as
|
|
17
|
+
duas é o defeito inteiro que esta classe de rubrica persegue.
|
|
18
|
+
|
|
19
|
+
## Bandas 0-10
|
|
20
|
+
|
|
21
|
+
| 0-3 | 4-6 | 7-8 | 9-10 |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| Superfície de prompt/modelo mexida sem eval nenhum. Ou eval cujo verde significa "executou": conta cenários e não julga resposta. Miss de cache lido como `skip` | O eval julga a resposta, mas toda asserção passa com qualquer resposta plausível (`Contains` de palavra que o próprio prompt já carrega, "não vazio", "é JSON"). Nenhuma resposta degradada foi injetada — o eval nunca foi visto vermelho | Resposta degradada injetada e o eval ficou vermelho, mas num cenário só; ou quem mordeu foi a asserção de FORMA (o JSON tem a chave) e não a de CONTEÚDO; ou a árvore de cache não tem canário provando que o git a enxerga | Degradação injetada **por cláusula de asserção**, cada uma derrubando o cenário correspondente e **nomeando-o**; contagem de cenários pinada contra encolhimento silencioso; a cache provada legível pelo git; miss é `fail` nomeado, jamais `skip` |
|
|
24
|
+
|
|
25
|
+
## Anti-padrões literais
|
|
26
|
+
|
|
27
|
+
- **`Invocations = 0` como prova de qualidade.** Ele prova que **nenhuma chamada paga aconteceu** —
|
|
28
|
+
não que o eval reprovaria um modelo pior. Contador de chamadas mede execução; detecção só se mede
|
|
29
|
+
**degradando a resposta** e exigindo o vermelho. Medido nesta epic: uma suíte com
|
|
30
|
+
`Invocations = 0`, cache quente e 4 de 4 cenários verdes não dizia uma palavra sobre morder.
|
|
31
|
+
- **Miss de cache lido como `skip`.** `skip` é lido como "não se aplica" e soma verde no agregado.
|
|
32
|
+
Cenário não avaliado é **`fail` nomeado**, com o nome do cenário e o comando que regrava.
|
|
33
|
+
- **Asserção que qualquer resposta plausível satisfaz** — `Contains("orçamento")` num cenário cujo
|
|
34
|
+
prompt já contém a palavra. A régua tem de separar a resposta boa da ruim, não a resposta da
|
|
35
|
+
ausência de resposta. Teste a régua contra a resposta ERRADA, não só contra a certa.
|
|
36
|
+
- **O guarda que não enxerga o objeto** (cross-ref `guard-cego.md`). Caso real desta base: o
|
|
37
|
+
`git diff --exit-code` sobre a árvore de cache reportava **limpo** — não porque a árvore
|
|
38
|
+
estivesse limpa, mas porque os caminhos estouravam o `MAX_PATH` e o git não conseguia **abrir**
|
|
39
|
+
os diretórios. Ele avisou no stderr, commitou **1 de 9** arquivos e saiu com código 0. Antes de
|
|
40
|
+
acreditar em qualquer canário sobre a cache, prove que ele **enxerga** os arquivos.
|
|
41
|
+
- **Dataset que encolhe em silêncio.** Uma suíte que passa a avaliar menos cenários é a mesma classe
|
|
42
|
+
de defeito que uma baseline de teste caindo — e ninguém percebe, porque o número que aparece é o
|
|
43
|
+
de aprovados.
|
|
44
|
+
- **Juiz é o modelo avaliado.** Auto-avaliação do mesmo modelo mede concordância consigo, não
|
|
45
|
+
qualidade. Vale para o avaliador LLM da suíte tanto quanto vale para o Gate 3.
|
|
46
|
+
- **Regravar a cache para o eval passar.** Regravar é a resposta certa quando **o pin mudou**, e a
|
|
47
|
+
errada quando **a asserção começou a reprovar**. As duas situações produzem o mesmo comando e
|
|
48
|
+
vereditos opostos: diga qual das duas era, e como sabe.
|
|
49
|
+
|
|
50
|
+
## Como registrar no relatório
|
|
51
|
+
|
|
52
|
+
Uma linha por cláusula degradada, dizendo o que foi degradado e qual cenário caiu **pelo nome**:
|
|
53
|
+
|
|
54
|
+
```markdown
|
|
55
|
+
- `triagem.acme.a-saudacao` — degradei a resposta EM CACHE trocando a saudação por texto vazio:
|
|
56
|
+
`Contains` derrubou o cenário nomeando-o (`eval-fail: triagem.acme.a-saudacao`). Restaurado por
|
|
57
|
+
cópia de bytes feita antes; md5 confere.
|
|
58
|
+
- `orcamento.acme.a-resposta-estruturada` — removi a chave `total` do payload: `JsonHasKey` mordeu;
|
|
59
|
+
`NoPii` continuou verde, como esperado (cláusulas independentes).
|
|
60
|
+
- Cache: 9 de 9 arquivos endereçáveis por `git ls-files` (canário `eval-cache-portability`), pior
|
|
61
|
+
caminho relativo 145 contra teto 160.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
E no `**Proof-Scores:**`, a chave `eval`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Prova: Fiação
|
|
2
|
+
|
|
3
|
+
> Escopo: Gate 3 (avaliador independente). Não é dimensão do score composto — impõe **teto** em
|
|
4
|
+
> Cobertura de Testes (`evals/rubrics/test-coverage.md`, seção "Tetos de prova").
|
|
5
|
+
|
|
6
|
+
## O que esta prova responde
|
|
7
|
+
|
|
8
|
+
A camada pura pode estar perfeita e o produto continuar quebrado, porque **ninguém ligou o fio**.
|
|
9
|
+
Mutar a função pura prova a função; não prova que o comando a chama, que o handler a expõe, nem que
|
|
10
|
+
o efeito é observável na saída.
|
|
11
|
+
|
|
12
|
+
A pergunta é: **existe um teste que vai da ENTRADA REAL ao EFEITO OBSERVÁVEL, e o inventário de
|
|
13
|
+
consumidores do eixo novo está completo?**
|
|
14
|
+
|
|
15
|
+
## Bandas 0-10
|
|
16
|
+
|
|
17
|
+
| 0-3 | 4-6 | 7-8 | 9-10 |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| Só testes de unidade da camada pura. Nenhum consumidor inventariado. Contagem de testes citada como total agregado, escondendo a perna que não rodou | Um teste de ponta a ponta existe, mas só de RECUSA — o caminho de sucesso segue intestável. Inventário por literal, que não vê argumento de helper | Um teste por comando/handler da entrada ao efeito. Inventário por `grep` presente mas incompleto (falta ação em massa, ou estado vazio, ou "limpar", ou chave de cache) | Entrada real → efeito observável em cada comando/handler; inventário de consumidores fechado, um a um; contagem de testes citada por `id` de nó, nunca agregada |
|
|
20
|
+
|
|
21
|
+
## Anti-padrões literais
|
|
22
|
+
|
|
23
|
+
- **Mutar a camada pura e concluir que o fix está ligado** — a mutação prova o módulo, nunca a
|
|
24
|
+
fiação. Prove o fio com um teste que entra pela superfície real.
|
|
25
|
+
- **Só testes de recusa** — eles passam com a escrita quebrada. Medido: um caminho de escrita cujo
|
|
26
|
+
corpo virou `return 0;` passou **23 de 23** testes, todos de recusa. O caminho de SUCESSO é o que
|
|
27
|
+
precisa de teste.
|
|
28
|
+
- **Eixo novo no estado sem inventário de consumidores** — um campo novo tem N leitores. Levante-os
|
|
29
|
+
por `grep` e percorra os quatro suspeitos de sempre: **ação em massa**, **estado vazio**,
|
|
30
|
+
**"limpar"/reset** e **chave de cache**. Um deles fica de fora por descuido, sempre o mesmo.
|
|
31
|
+
- **Varredura por literal quando o valor viaja como argumento de helper** — procurar `"Gardim"` não
|
|
32
|
+
encontra `Compose("Gardim")`. Medido: **11 call sites** sobraram numa varredura por literal.
|
|
33
|
+
Procure pelo helper, não só pelo valor.
|
|
34
|
+
- **Campo novo em spec sem os pontos de wiring, um a um** — schema, parser, escritor, leitor,
|
|
35
|
+
validador e superfície de exibição. Liste-os e marque cada um.
|
|
36
|
+
- **Citar um total agregado de testes** — o `verify` reporta `nodes.tests.runs[]` com um `count`
|
|
37
|
+
**por `id`** (contrato de #90/8.35). Cite **por `id`**: `runs[dotnet].count`, `runs[node].count`.
|
|
38
|
+
Um total esconde exatamente a perna que não rodou. E `count: null` é **contagem desconhecida**,
|
|
39
|
+
nunca zero — um nó com `null` não autoriza nenhuma afirmação numérica sobre ele.
|
|
40
|
+
|
|
41
|
+
## Como registrar no relatório
|
|
42
|
+
|
|
43
|
+
Duas coisas, sempre:
|
|
44
|
+
|
|
45
|
+
```markdown
|
|
46
|
+
**Fiação verificada:** `POST /leads` → handler → `LeadService.Create` → linha em `leads` (teste
|
|
47
|
+
`LeadsEndpointTests:88`, caminho de SUCESSO, não só de recusa).
|
|
48
|
+
|
|
49
|
+
**Contagem por nó:** runs[node].count = 2712 · runs[dotnet].count = 41 · runs[e2e].count = null
|
|
50
|
+
(desconhecida — o nó pulou, razão `docker is not available`).
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
E, quando a feature acrescenta um eixo ao estado, o inventário de consumidores como lista fechada,
|
|
54
|
+
com o comando de `grep` que a produziu.
|