@polymorphism-tech/morph-spec 8.33.0 → 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 +22 -2
- package/README.md +2 -2
- package/bin/morph-spec.js +43 -5
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +5 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -2
- package/framework/MORPH.md +20 -22
- package/framework/agents.json +198 -44
- package/framework/ai-pin.json +22 -0
- package/framework/commands/morph-media.md +45 -0
- package/framework/docs/CLI.md +238 -10
- 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/README.md +11 -1
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
- package/framework/hooks/claude-code/core/chain.js +27 -7
- package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
- package/framework/hooks/claude-code/core/worktree-context.js +83 -14
- package/framework/hooks/claude-code/core/worktree-create.js +206 -0
- package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
- package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
- package/framework/hooks/shared/command-tokens.js +23 -0
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/git-paths.js +25 -0
- package/framework/hooks/shared/hook-response.js +17 -0
- package/framework/hooks/shared/morph-cli.js +134 -0
- package/framework/rules/ai-agents-standards.md +14 -2
- package/framework/rules/infrastructure-standards.md +3 -1
- 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 +28 -7
- 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 +20 -2
- package/framework/skills/core/morph-media/SKILL.md +215 -0
- package/framework/skills/core/morph-plan/SKILL.md +38 -4
- 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 +1504 -830
- package/framework/standards/ai-agents/agent-archetypes.md +160 -0
- package/framework/standards/ai-agents/agent-session.md +256 -160
- package/framework/standards/ai-agents/agent-spec.md +196 -0
- package/framework/standards/ai-agents/batching.md +217 -193
- package/framework/standards/ai-agents/context-providers.md +261 -188
- 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 +351 -250
- package/framework/standards/ai-agents/mcp-tools.md +250 -197
- 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 +241 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
- package/framework/standards/ai-agents/modalities-vision.md +205 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
- package/framework/standards/ai-agents/observability-patterns.md +227 -17
- package/framework/standards/ai-agents/production.md +260 -231
- package/framework/standards/ai-agents/prompt-sources.md +389 -0
- package/framework/standards/ai-agents/providers/model-registry.md +293 -158
- package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
- package/framework/standards/ai-agents/service-tier-flex.md +143 -135
- package/framework/standards/ai-agents/setup.md +166 -50
- package/framework/standards/ai-agents/structured-output.md +280 -249
- package/framework/standards/ai-agents/sweet-spot.md +129 -109
- 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 +90 -0
- package/framework/templates/REGISTRY.json +14 -1
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- 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/infrastructure/docker/docker-compose.template.yml +24 -14
- package/package.json +3 -3
- package/src/commands/e2e/e2e.js +91 -40
- package/src/commands/project/doctor.js +143 -6
- package/src/commands/project/env.js +95 -0
- package/src/commands/project/update.js +469 -153
- package/src/commands/project/worktree.js +274 -53
- 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/delete.js +8 -0
- package/src/commands/state/finish.js +360 -53
- 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/state-manager.js +9 -1
- package/src/dashboard/app/pages/HookInspector.jsx +430 -418
- package/src/dashboard/scanner/hooks-scanner.js +5 -1
- 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/e2e/compose-env.js +55 -14
- package/src/lib/e2e/stack-teardown.js +115 -0
- package/src/lib/evaluator-report.js +269 -15
- package/src/lib/generators/architecture-scanner.js +2 -1
- package/src/lib/git/default-branch.js +92 -4
- package/src/lib/graph/graphifyignore.js +2 -1
- package/src/lib/installers/effects.js +193 -0
- 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/test-nodes.js +486 -0
- package/src/lib/tasks/test-runner.js +431 -37
- package/src/lib/validators/verify-runner.js +590 -28
- package/src/lib/worktree/dirs.js +32 -0
- package/src/lib/worktree/docker-audit.js +287 -0
- package/src/lib/worktree/env-allocator.js +69 -11
- package/src/lib/worktree/worktree-audit.js +70 -3
- package/src/scripts/setup-infra.js +24 -11
- package/src/utils/agents-installer.js +18 -8
- package/src/utils/claude-md-injector.js +10 -5
- package/src/utils/file-copier.js +28 -16
- package/src/utils/hooks-installer.js +75 -29
- package/src/utils/skills-installer.js +194 -187
- package/src/utils/version-checker.js +68 -3
- package/framework/standards/ai-agents/prompts-in-db.md +0 -214
|
@@ -15,6 +15,41 @@
|
|
|
15
15
|
* checagem por `existsSync` puro do `approve` aceitava um relatório SEM o token
|
|
16
16
|
* que o `gate-check` então descartava — o Gate 3 passava carimbado `auto` sobre
|
|
17
17
|
* uma média de auto-avaliação, exatamente o que MORPH.md §5 proíbe.
|
|
18
|
+
*
|
|
19
|
+
* ── O TOKEN VIGENTE É O ÚLTIMO ──────────────────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* O `evaluator-report.md` é APPEND-ONLY por contrato: uma reavaliação acrescenta
|
|
22
|
+
* ao fim, preservando a análise anterior. Este módulo lia o COMEÇO (`String.match`
|
|
23
|
+
* com regex sem flag `g`), então num relatório reavaliado o gate era decidido
|
|
24
|
+
* pelo score SUPERADO. Medido em campo três vezes na mesma epic — #90 (8.7 →
|
|
25
|
+
* 9.4), #92 (8.2 → 9.0) e #93 (8.9 → 8.3 → 9.1) — e nas três o avaliador teve de
|
|
26
|
+
* desarmar o token antigo à mão para o gate ler a nota vigente.
|
|
27
|
+
*
|
|
28
|
+
* O caso perigoso é o INVERSO e é silencioso: numa reavaliação que BAIXA a nota
|
|
29
|
+
* (o #93 caiu de 8.9 para 8.3 quando uma correção abriu um bloqueante), o gate
|
|
30
|
+
* leria a nota antiga, mais alta, e AUTO-PASSARIA o Gate 3 sobre um produto com
|
|
31
|
+
* bloqueante aberto — a mesma classe de defeito que a v8.26.0 existiu para
|
|
32
|
+
* fechar, voltando por outra porta: lendo um veredito real, porém vencido.
|
|
33
|
+
*
|
|
34
|
+
* DECISÃO EXPLÍCITA — "o último vence", e não "múltiplos tokens viram
|
|
35
|
+
* `report-ambiguous` e recusam":
|
|
36
|
+
*
|
|
37
|
+
* 1. É a semântica do próprio artefato. Append-only significa que o arquivo é
|
|
38
|
+
* um LOG, e o valor corrente de um log é a sua última entrada. Ler o
|
|
39
|
+
* primeiro não é uma política conservadora — é ler a entrada errada.
|
|
40
|
+
* 2. Recusar transformaria um veredito REAL e CORRENTE numa pausa que só sai
|
|
41
|
+
* editando à mão um arquivo declarado append-only. Isto é exatamente o
|
|
42
|
+
* contorno manual que este conserto existe para eliminar, promovido a
|
|
43
|
+
* requisito.
|
|
44
|
+
* 3. Recusar não é mais seguro em nenhum dos dois casos observados: com a nota
|
|
45
|
+
* SUBINDO, o humano é obrigado a mexer no relatório para liberar um gate
|
|
46
|
+
* que já foi aprovado; com a nota CAINDO, "o último vence" já pausa sozinho
|
|
47
|
+
* pela regra de score, que é a razão certa a reportar.
|
|
48
|
+
*
|
|
49
|
+
* Ambiguidade não fica silenciosa, ainda assim: `evaluatorReportState` devolve
|
|
50
|
+
* `scoreTokens` (quantos tokens vivos o arquivo tem) e `superseded` (os valores
|
|
51
|
+
* anteriores, na ordem do arquivo). Quem consome pode dizer "9.1, terceira
|
|
52
|
+
* rodada, superando 8.9 e 8.3" em vez de fingir que só havia um veredito.
|
|
18
53
|
*/
|
|
19
54
|
|
|
20
55
|
import { existsSync, readFileSync } from 'fs';
|
|
@@ -27,29 +62,133 @@ import { getAbsoluteOutputPath } from '../core/paths/output-schema.js';
|
|
|
27
62
|
*/
|
|
28
63
|
export const COMPOSITE_SCORE_TOKEN = '**Composite-Score:**';
|
|
29
64
|
|
|
30
|
-
|
|
65
|
+
/**
|
|
66
|
+
* O token das rubricas de PROVA (`.morph/framework/evals/proof-rubrics/`):
|
|
67
|
+
* `**Proof-Scores:** mutation=9 verde=8 fiacao=9 guard=7 eval=8`. Mesma convenção
|
|
68
|
+
* literal do Composite-Score de propósito — mesmo arquivo, mesmo módulo leitor,
|
|
69
|
+
* mesmo estilo de token. Ele pontua a EVIDÊNCIA que o avaliador produziu, nunca
|
|
70
|
+
* o produto: não entra no composto, impõe teto em `test-coverage`.
|
|
71
|
+
*/
|
|
72
|
+
export const PROOF_SCORES_TOKEN = '**Proof-Scores:**';
|
|
73
|
+
|
|
74
|
+
/** `g` é obrigatório: o arquivo é append-only e o token vigente é o ÚLTIMO. */
|
|
75
|
+
const COMPOSITE_SCORE_RE = /\*\*Composite-Score:\*\*\s*([0-9]+(?:\.[0-9]+)?)/g;
|
|
76
|
+
|
|
77
|
+
/** Idem — uma reavaliação reescreve os Proof-Scores acrescentando ao fim. */
|
|
78
|
+
const PROOF_SCORES_RE = /\*\*Proof-Scores:\*\*[ \t]*([^\r\n]*)/g;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A seção de prova de mutação. Exige `##`: um `# Mutações` é outra estrutura
|
|
82
|
+
* (H1 é título de documento, não seção), e o template que a `morph-review`
|
|
83
|
+
* entrega ao avaliador gera exatamente `## Mutações`. Aceita a grafia sem
|
|
84
|
+
* acento porque um relatório escrito em terminal ASCII continua sendo o mesmo
|
|
85
|
+
* fato — o que não se aceita é o nível errado de cabeçalho.
|
|
86
|
+
*/
|
|
87
|
+
const MUTATION_SECTION_RE = /^##\s+Muta[çc][õo]es\s*$/m;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A seção de prova de MÍDIA. Mesma disciplina do `## Mutações`: exige `##`, e
|
|
91
|
+
* aceita a grafia sem acento porque um relatório escrito em terminal ASCII
|
|
92
|
+
* continua sendo o mesmo fato. `Mídia gerada` / `Midia gerada`.
|
|
93
|
+
*/
|
|
94
|
+
const MEDIA_SECTION_RE = /^##\s+M[íi]dia\s+gerada\s*$/m;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* As chaves reconhecidas de `**Proof-Scores:**`, uma por rubrica de prova.
|
|
98
|
+
* Chave fora desta lista é IGNORADA (não vira score, não vira erro): o relatório
|
|
99
|
+
* é texto de LLM e uma chave inventada não deve nem contaminar nenhum mínimo nem
|
|
100
|
+
* derrubar a leitura das outras.
|
|
101
|
+
*
|
|
102
|
+
* `media` PRECISA estar aqui. Sem isso a rubrica `media-claims.md` seria
|
|
103
|
+
* decorativa: o avaliador escreveria `media=7` e o número seria descartado em
|
|
104
|
+
* silêncio, que é o caso "campo publicado sem nenhum leitor".
|
|
105
|
+
*/
|
|
106
|
+
export const PROOF_SCORE_KEYS = ['mutation', 'verde', 'fiacao', 'guard', 'eval', 'media', 'obs'];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* As chaves que NÃO entram no `min` de `test-coverage` porque impõem teto em
|
|
110
|
+
* OUTRA dimensão: `media` (teto em `architecture`) e `obs` (teto em
|
|
111
|
+
* `code-quality`).
|
|
112
|
+
*
|
|
113
|
+
* **`obs` chegou exatamente pelo caminho que o parágrafo abaixo antecipou**, e
|
|
114
|
+
* vale registrar porque o aviso não bastou. A feature irmã (#94) criou a rubrica
|
|
115
|
+
* `observabilidade-genai.md`, escreveu `obs=` onze vezes no seu próprio
|
|
116
|
+
* `evaluator-report.md` — e a chave não estava em `PROOF_SCORE_KEYS`, nem lá nem
|
|
117
|
+
* na base dela. O parser a descartava em silêncio (ver o `continue` de
|
|
118
|
+
* `parseProofScores`), então a rubrica nasceu DECORATIVA e atravessou **onze
|
|
119
|
+
* rodadas de Gate 3** sem que ninguém percebesse: o número era escrito, lido por
|
|
120
|
+
* um humano no relatório, e nunca chegava ao gate. Só o rebase, que põe as duas
|
|
121
|
+
* features na mesma árvore, tornou o buraco visível.
|
|
122
|
+
*
|
|
123
|
+
* **Isto é uma exclusão, e a forma importa mais que a lista.** A primeira versão
|
|
124
|
+
* era o inverso — uma lista fechada das quatro chaves que entram. Ela responde
|
|
125
|
+
* certo hoje e erra sozinha amanhã: a feature irmã acrescenta uma quinta chave
|
|
126
|
+
* (`eval`) com rubrica própria, e depois do rebase esse `eval=N` cairia do teto
|
|
127
|
+
* **em silêncio** — o "campo publicado sem nenhum leitor" que este mesmo arquivo
|
|
128
|
+
* existe para impedir, produzido pela correção.
|
|
129
|
+
*
|
|
130
|
+
* Por exclusão, uma chave nova nasce DENTRO do mínimo por padrão, e tirá-la de lá
|
|
131
|
+
* passa a ser um ato deliberado e visível: uma linha aqui, com a dimensão que ela
|
|
132
|
+
* limita escrita ao lado. Errar para o lado de "conta no mínimo" custa um teto a
|
|
133
|
+
* mais; errar para o outro lado devolve o gate cego.
|
|
134
|
+
*
|
|
135
|
+
* O motivo de `media` estar fora continua o mesmo: as quatro do mínimo perguntam
|
|
136
|
+
* "a evidência é real?"; `media` pergunta "a chamada mora no lugar certo? que
|
|
137
|
+
* estados existem? o que é publicado?". Um `media=4` derrubando o teto de TESTE
|
|
138
|
+
* seriam dois fatos diferentes lidos como um só.
|
|
139
|
+
*/
|
|
140
|
+
export const NON_MUTATION_PROOF_KEYS = ['media', 'obs'];
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* As chaves que entram no `min` que impõe teto em `test-coverage`: DERIVADA de
|
|
144
|
+
* {@link PROOF_SCORE_KEYS} menos {@link NON_MUTATION_PROOF_KEYS} — nunca digitada.
|
|
145
|
+
* Ver o racional da exclusão acima.
|
|
146
|
+
*/
|
|
147
|
+
export const MUTATION_PROOF_KEYS =
|
|
148
|
+
PROOF_SCORE_KEYS.filter((k) => !NON_MUTATION_PROOF_KEYS.includes(k));
|
|
31
149
|
|
|
32
150
|
/** Path canônico do evaluator-report da feature. Fonte única para os dois comandos. */
|
|
33
151
|
export function evaluatorReportPath(cwd, featureName) {
|
|
34
152
|
return getAbsoluteOutputPath(cwd, featureName, 'evaluatorReport');
|
|
35
153
|
}
|
|
36
154
|
|
|
155
|
+
/** Conteúdo do relatório, ou null quando ausente/ilegível. Nunca lança. */
|
|
156
|
+
function readReport(cwd, featureName) {
|
|
157
|
+
try {
|
|
158
|
+
const p = evaluatorReportPath(cwd, featureName);
|
|
159
|
+
if (!existsSync(p)) return null;
|
|
160
|
+
return readFileSync(p, 'utf8');
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Todos os valores de `**Composite-Score:**` do arquivo, NA ORDEM DO ARQUIVO.
|
|
168
|
+
* O último é o vigente; os anteriores são rodadas superadas.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} text
|
|
171
|
+
* @returns {number[]}
|
|
172
|
+
*/
|
|
173
|
+
function compositeScoresIn(text) {
|
|
174
|
+
return [...text.matchAll(COMPOSITE_SCORE_RE)].map((m) => Number(m[1]));
|
|
175
|
+
}
|
|
176
|
+
|
|
37
177
|
/**
|
|
38
178
|
* Score composto REAL da feature, do token gravado pelo avaliador independente.
|
|
39
179
|
*
|
|
180
|
+
* Num relatório reavaliado (append-only), é o token do FIM — o veredito
|
|
181
|
+
* vigente. Ver o bloco "O TOKEN VIGENTE É O ÚLTIMO" no topo do arquivo.
|
|
182
|
+
*
|
|
40
183
|
* @returns {number|undefined} undefined quando o relatório não existe, não é
|
|
41
184
|
* legível, ou existe sem o token (relatório legado ou oco). `undefined` é
|
|
42
185
|
* deliberado e não 0: quem chama distingue "sem veredito" de "veredito ruim".
|
|
43
186
|
*/
|
|
44
187
|
export function evaluatorCompositeScore(cwd, featureName) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return m ? Number(m[1]) : undefined;
|
|
50
|
-
} catch {
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
188
|
+
const text = readReport(cwd, featureName);
|
|
189
|
+
if (text === null) return undefined;
|
|
190
|
+
const scores = compositeScoresIn(text);
|
|
191
|
+
return scores.length === 0 ? undefined : scores[scores.length - 1];
|
|
53
192
|
}
|
|
54
193
|
|
|
55
194
|
/**
|
|
@@ -60,13 +199,128 @@ export function evaluatorCompositeScore(cwd, featureName) {
|
|
|
60
199
|
* "o avaliador rodou mas o relatório está oco — ele não seguiu o contrato";
|
|
61
200
|
* e só o terceiro caso é aprovável.
|
|
62
201
|
*
|
|
63
|
-
*
|
|
202
|
+
* `scoreTokens`/`superseded` só aparecem no caso `ok` e existem para que uma
|
|
203
|
+
* reavaliação nunca passe silenciosa: o score é o último, mas quem consome sabe
|
|
204
|
+
* quantas rodadas o arquivo carrega e quais notas foram superadas.
|
|
205
|
+
*
|
|
206
|
+
* @returns {{ status: 'ok'|'missing'|'untokenized', score?: number,
|
|
207
|
+
* scoreTokens?: number, superseded?: number[], path: string }}
|
|
64
208
|
*/
|
|
65
209
|
export function evaluatorReportState(cwd, featureName) {
|
|
66
210
|
const path = evaluatorReportPath(cwd, featureName);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
211
|
+
const text = readReport(cwd, featureName);
|
|
212
|
+
if (text === null) return { status: 'missing', path };
|
|
213
|
+
const scores = compositeScoresIn(text);
|
|
214
|
+
if (scores.length === 0) return { status: 'untokenized', path };
|
|
215
|
+
return {
|
|
216
|
+
status: 'ok',
|
|
217
|
+
score: scores[scores.length - 1],
|
|
218
|
+
scoreTokens: scores.length,
|
|
219
|
+
superseded: scores.slice(0, -1),
|
|
220
|
+
path,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Parseia o corpo de UMA linha `**Proof-Scores:**` — pares `chave=N` separados
|
|
226
|
+
* por espaço.
|
|
227
|
+
*
|
|
228
|
+
* @param {string} body
|
|
229
|
+
* @returns {Record<string, number>|undefined} undefined quando nenhum par
|
|
230
|
+
* VÁLIDO foi lido (linha vazia, só chaves desconhecidas, ou só valores
|
|
231
|
+
* não-numéricos). `undefined` e não `{}` pela mesma disciplina do
|
|
232
|
+
* `evaluatorCompositeScore`: "sem veredito" tem de ser distinguível de
|
|
233
|
+
* "veredito vazio".
|
|
234
|
+
*/
|
|
235
|
+
function parseProofScores(body) {
|
|
236
|
+
const out = {};
|
|
237
|
+
for (const m of body.matchAll(/([A-Za-zÀ-ÿ][\w-]*)\s*=\s*([^\s]+)/g)) {
|
|
238
|
+
const key = m[1].toLowerCase();
|
|
239
|
+
if (!PROOF_SCORE_KEYS.includes(key)) continue;
|
|
240
|
+
if (!/^[0-9]+(?:\.[0-9]+)?$/.test(m[2])) continue;
|
|
241
|
+
out[key] = Number(m[2]);
|
|
242
|
+
}
|
|
243
|
+
return Object.keys(out).length === 0 ? undefined : out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Estado de PROVA do relatório: a seção `## Mutações` está lá, e quais
|
|
248
|
+
* Proof-Scores o avaliador registrou.
|
|
249
|
+
*
|
|
250
|
+
* Deriva do disco e não julga: quem decide se a ausência pausa o Gate 3 é
|
|
251
|
+
* `risk-detector.js` (a divisão "gate-check coleta, risk-detector decide"), e
|
|
252
|
+
* quem decide o teto é a rubrica `test-coverage.md`. O `min` NÃO é gravado
|
|
253
|
+
* aqui — é derivado por quem consome, para que exista um só lugar onde
|
|
254
|
+
* "mínimo de quantas chaves" é definido.
|
|
255
|
+
*
|
|
256
|
+
* Arquivo ausente ou ilegível: `{ mutationSection: false, mediaSection: false,
|
|
257
|
+
* proofScores: undefined }`. Nunca lança, e nunca devolve `0` no lugar de
|
|
258
|
+
* `undefined`.
|
|
259
|
+
*
|
|
260
|
+
* `mediaSection` é lido do MESMO arquivo pelo MESMO módulo, de propósito: uma
|
|
261
|
+
* segunda leitura do relatório noutro lugar é como duas superfícies passam a
|
|
262
|
+
* discordar sobre o que ele diz.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} cwd
|
|
265
|
+
* @param {string} featureName
|
|
266
|
+
* @returns {{ mutationSection: boolean, mediaSection: boolean, proofScores: Record<string, number>|undefined, path: string }}
|
|
267
|
+
*/
|
|
268
|
+
export function evaluatorProofState(cwd, featureName) {
|
|
269
|
+
const path = evaluatorReportPath(cwd, featureName);
|
|
270
|
+
const text = readReport(cwd, featureName);
|
|
271
|
+
if (text === null) {
|
|
272
|
+
return { mutationSection: false, mediaSection: false, proofScores: undefined, path };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Append-only outra vez: numa reavaliação a linha nova vai para o fim, e é
|
|
276
|
+
// ela que vale. A primeira linha descreveria a prova de uma rodada superada.
|
|
277
|
+
const lines = [...text.matchAll(PROOF_SCORES_RE)];
|
|
278
|
+
const proofScores = lines.length === 0
|
|
279
|
+
? undefined
|
|
280
|
+
: parseProofScores(lines[lines.length - 1][1]);
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
mutationSection: MUTATION_SECTION_RE.test(text),
|
|
284
|
+
mediaSection: MEDIA_SECTION_RE.test(text),
|
|
285
|
+
proofScores,
|
|
286
|
+
path,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* O menor Proof-Score das provas de EVIDÊNCIA — o que impõe o teto em
|
|
292
|
+
* `test-coverage`.
|
|
293
|
+
*
|
|
294
|
+
* Calculado sobre {@link MUTATION_PROOF_KEYS} — que é
|
|
295
|
+
* {@link PROOF_SCORE_KEYS} MENOS {@link NON_MUTATION_PROOF_KEYS}, e não uma lista
|
|
296
|
+
* digitada. `media` responde a outra pergunta e limita outra dimensão
|
|
297
|
+
* (`architecture`); ler as duas famílias como um mínimo só faria um `media=4`
|
|
298
|
+
* derrubar o teto de teste sem que nada de teste tivesse acontecido. Uma chave
|
|
299
|
+
* NOVA de prova entra aqui sozinha: excluí-la exige uma linha deliberada em
|
|
300
|
+
* `NON_MUTATION_PROOF_KEYS`, e nunca acontece por esquecimento.
|
|
301
|
+
*
|
|
302
|
+
* @param {Record<string, number>|undefined} proofScores
|
|
303
|
+
* @returns {number|null} null quando não há nenhum score do mínimo: SEM TETO A
|
|
304
|
+
* APLICAR, jamais "teto 0". A mesma razão pela qual `count: null` do verify
|
|
305
|
+
* não é 0.
|
|
306
|
+
*/
|
|
307
|
+
export function minProofScore(proofScores) {
|
|
308
|
+
if (!proofScores) return null;
|
|
309
|
+
const values = MUTATION_PROOF_KEYS
|
|
310
|
+
.map((key) => proofScores[key])
|
|
311
|
+
.filter((n) => typeof n === 'number' && Number.isFinite(n));
|
|
312
|
+
return values.length === 0 ? null : Math.min(...values);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* O Proof-Score de MÍDIA, lido à parte. Impõe teto em `architecture` (tabela em
|
|
317
|
+
* `framework/evals/rubrics/architecture.md`, "Tetos de prova de mídia").
|
|
318
|
+
*
|
|
319
|
+
* @param {Record<string, number>|undefined} proofScores
|
|
320
|
+
* @returns {number|null} null quando a chave não foi registrada — o caso normal
|
|
321
|
+
* de uma feature que não gera mídia, e nunca "teto 0".
|
|
322
|
+
*/
|
|
323
|
+
export function mediaProofScore(proofScores) {
|
|
324
|
+
const value = proofScores?.media;
|
|
325
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
72
326
|
}
|
|
@@ -20,8 +20,9 @@ import { fileURLToPath } from 'url';
|
|
|
20
20
|
import { existsSync, readFileSync } from 'fs';
|
|
21
21
|
import { scanGraph } from '../graph/god-nodes.js';
|
|
22
22
|
import { renderTemplate } from '../../core/templates/template-renderer.js';
|
|
23
|
+
import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
|
|
23
24
|
|
|
24
|
-
const SKIP_DIRS = new Set(['node_modules', 'bin', 'obj', '.git', '.morph', '.next', 'dist', 'out',
|
|
25
|
+
export const SKIP_DIRS = new Set(['node_modules', 'bin', 'obj', '.git', '.morph', '.next', 'dist', 'out', ...WORKTREE_DIR_NAMES]);
|
|
25
26
|
const MAX_FILES_PER_SCANNER = 200;
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -126,12 +126,52 @@ function parseShortstat(text) {
|
|
|
126
126
|
* @returns {number}
|
|
127
127
|
*/
|
|
128
128
|
function countUntrackedFiles(cwd) {
|
|
129
|
+
return listUntrackedFiles(cwd).length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Split git's stdout into clean, forward-slashed, non-empty path lines.
|
|
134
|
+
*
|
|
135
|
+
* One helper for both probes because the two disagreeing about what a "line" is
|
|
136
|
+
* — a stray `\r` on Windows, a trailing empty line — would make the count and
|
|
137
|
+
* the listing describe different sets from the same output.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} text
|
|
140
|
+
* @returns {string[]}
|
|
141
|
+
*/
|
|
142
|
+
function splitLines(text) {
|
|
143
|
+
return String(text)
|
|
144
|
+
.split('\n')
|
|
145
|
+
.map((line) => line.trim().replace(/\\/g, '/'))
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The brand-new UNTRACKED files, by NAME — the listing behind
|
|
151
|
+
* {@link countUntrackedFiles}, and the second half of {@link changedPathsAgainst}.
|
|
152
|
+
*
|
|
153
|
+
* The two callers need the same fact at different resolutions, and this is the
|
|
154
|
+
* ONLY place the probe is spelled: `git diff` never shows a file that was never
|
|
155
|
+
* `git add`-ed, regardless of ref or working-tree mode, so anything that answers
|
|
156
|
+
* "what changed" has to ask twice. A second copy of this command is exactly how
|
|
157
|
+
* the invisible-new-file bug would come back on one caller and not the other.
|
|
158
|
+
*
|
|
159
|
+
* `--exclude-standard` honours gitignore the way `git status` does, so build
|
|
160
|
+
* output never inflates either answer.
|
|
161
|
+
*
|
|
162
|
+
* Fail-open to `[]`: this probe is secondary to the diff and must never take the
|
|
163
|
+
* whole result down.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} cwd
|
|
166
|
+
* @returns {string[]}
|
|
167
|
+
*/
|
|
168
|
+
function listUntrackedFiles(cwd) {
|
|
129
169
|
try {
|
|
130
|
-
|
|
131
|
-
.toString()
|
|
132
|
-
|
|
170
|
+
return splitLines(
|
|
171
|
+
execFileSync('git', ['ls-files', '--others', '--exclude-standard'], { cwd, stdio: 'pipe' }).toString()
|
|
172
|
+
);
|
|
133
173
|
} catch {
|
|
134
|
-
return
|
|
174
|
+
return [];
|
|
135
175
|
}
|
|
136
176
|
}
|
|
137
177
|
|
|
@@ -172,3 +212,51 @@ export function getDiffStat(baseRef, cwd = process.cwd()) {
|
|
|
172
212
|
return null;
|
|
173
213
|
}
|
|
174
214
|
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Every path this working tree CHANGED against `baseRef` — tracked edits plus
|
|
218
|
+
* brand-new untracked files.
|
|
219
|
+
*
|
|
220
|
+
* WHY IT LIVES BESIDE getDiffStat, AND REUSES ITS UNTRACKED PROBE
|
|
221
|
+
*
|
|
222
|
+
* The two answer the same question at different resolutions ("how much changed"
|
|
223
|
+
* versus "what changed"), and both depend on the SAME fragile fact: `git diff`
|
|
224
|
+
* is tracked-edits-only, so a brand-new file is invisible to it no matter which
|
|
225
|
+
* ref is named. `getDiffStat`'s docblock has documented that for a while; a
|
|
226
|
+
* second copy of the probe here is precisely how the "new file is invisible"
|
|
227
|
+
* bug comes back on one of the two call sites and not the other. So
|
|
228
|
+
* `countUntrackedFiles`'s command is not retyped — the listing is.
|
|
229
|
+
*
|
|
230
|
+
* `git diff --name-only <baseRef>` against the WORKING TREE, deliberately not
|
|
231
|
+
* `baseRef...HEAD`: a gate-check in the middle of implement must see work that
|
|
232
|
+
* is not committed yet. Someone who has only staged the prompt change has still
|
|
233
|
+
* changed the prompt.
|
|
234
|
+
*
|
|
235
|
+
* FAIL-OPEN, and the caller must know what that means. `[]` comes back both for
|
|
236
|
+
* "nothing changed" and for "the ref did not resolve", exactly like
|
|
237
|
+
* `getDiffStat` returning null. The distinction that MATTERS to the caller is a
|
|
238
|
+
* different one — "the project declares no watched path" versus "it declares
|
|
239
|
+
* some and none were touched" — and that one is answered by the caller's own
|
|
240
|
+
* `declared` flag, never by the emptiness of this array.
|
|
241
|
+
*
|
|
242
|
+
* @param {string|null} baseRef - typically `detectDefaultBranch(cwd)`
|
|
243
|
+
* @param {string} [cwd]
|
|
244
|
+
* @returns {string[]} Repo-relative paths with forward slashes, deduplicated,
|
|
245
|
+
* sorted. Empty when `baseRef` is falsy, cwd is not a repo, or the diff fails.
|
|
246
|
+
*/
|
|
247
|
+
export function changedPathsAgainst(baseRef, cwd = process.cwd()) {
|
|
248
|
+
if (!baseRef) return [];
|
|
249
|
+
|
|
250
|
+
let tracked;
|
|
251
|
+
try {
|
|
252
|
+
tracked = splitLines(
|
|
253
|
+
execFileSync('git', ['diff', '--name-only', baseRef], { cwd, stdio: 'pipe' }).toString()
|
|
254
|
+
);
|
|
255
|
+
} catch {
|
|
256
|
+
// A baseRef that does not resolve is not a reason to claim "nothing
|
|
257
|
+
// changed" loudly — the caller's policy is written to be inert on `[]`.
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return [...new Set([...tracked, ...listUntrackedFiles(cwd)])].sort();
|
|
262
|
+
}
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { writeFileSync, existsSync } from 'node:fs';
|
|
19
19
|
import { join } from 'node:path';
|
|
20
|
+
import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
|
|
20
21
|
|
|
21
22
|
export const ALWAYS_IGNORED = [
|
|
22
|
-
'.git/', '.morph/', 'artifacts/',
|
|
23
|
+
'.git/', '.morph/', 'artifacts/', ...WORKTREE_DIR_NAMES.map((d) => `${d}/`),
|
|
23
24
|
'coverage/', 'graphify-out/', 'dist/', 'out/',
|
|
24
25
|
];
|
|
25
26
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* effects.js — o coletor de efeitos ÚNICO do caminho de instalação/atualização.
|
|
3
|
+
*
|
|
4
|
+
* A propriedade que faz o `--dry-run` ser honesto está numa frase:
|
|
5
|
+
*
|
|
6
|
+
* > **o caminho real também emite.**
|
|
7
|
+
*
|
|
8
|
+
* Em modo normal cada verbo EXECUTA e registra; em `dryRun`, só registra. O
|
|
9
|
+
* resumo impresso ao fim de uma execução real sai de `list()`, não de
|
|
10
|
+
* contadores paralelos. A consequência é o ponto: um ponto de emissão
|
|
11
|
+
* esquecido some das DUAS saídas ao mesmo tempo, e o resumo de uma execução
|
|
12
|
+
* real fica visivelmente incompleto — o defeito aparece sem sequer usar o
|
|
13
|
+
* `--dry-run`.
|
|
14
|
+
*
|
|
15
|
+
* **Não há segunda travessia.** Nenhum filtro, walker ou lista de payload é
|
|
16
|
+
* reimplementado aqui. Os filtros que decidem o que instalar
|
|
17
|
+
* (`shouldInstallSkill`, `agentEligible`, `shouldInstall`) já são puros e
|
|
18
|
+
* continuam sendo os únicos. O anti-exemplar tem nome e mora neste
|
|
19
|
+
* repositório: `checkAgentsDrift` funciona como preditor porque re-deriva o
|
|
20
|
+
* conjunto elegível por conta própria e porque os dois lados chamam o mesmo
|
|
21
|
+
* renderer puro. Estender essa forma para 9 payloads + hooks + merge de
|
|
22
|
+
* settings + gitignore + índice do git é reimplementar o instalador inteiro e
|
|
23
|
+
* depender de disciplina humana para mantê-los sincronizados.
|
|
24
|
+
*
|
|
25
|
+
* **Os verbos são SÍNCRONOS de propósito.** Metade dos instaladores que eles
|
|
26
|
+
* atravessam (`installSkills`, `installAgents`, `installRulesForStack`) é
|
|
27
|
+
* síncrona; um coletor assíncrono obrigaria a tornar `installRulesForStack`
|
|
28
|
+
* async e a mudar a assinatura que `setup-infra`, `init` e os testes já usam.
|
|
29
|
+
*
|
|
30
|
+
* **O registro é a IDENTIDADE do efeito, nunca o conteúdo.** `writeFile` grava
|
|
31
|
+
* `{ verb, target }`, não os bytes: `installClaudeHooks` carimba um
|
|
32
|
+
* `generated_at` a cada execução, e comparar conteúdo faria o canário de
|
|
33
|
+
* igualdade dry × real reprovar por causa do relógio.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import fs from 'fs-extra';
|
|
37
|
+
import { spawnSync } from 'node:child_process';
|
|
38
|
+
import { dirname } from 'node:path';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {Object} Effect
|
|
42
|
+
* @property {string} verb - ensureDir | remove | copyDir | copyFile | writeFile | writeJson | mergeJson | spawn | gitIndex
|
|
43
|
+
* @property {string} target - o caminho (ou comando) sobre o qual o efeito age
|
|
44
|
+
* @property {string} [from] - origem, para os verbos de cópia
|
|
45
|
+
* @property {string} [detail] - qualificador estável (argumentos do spawn, operação do git)
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Cria um coletor de efeitos.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ dryRun?: boolean }} [options]
|
|
52
|
+
* @returns {{
|
|
53
|
+
* dryRun: boolean,
|
|
54
|
+
* ensureDir: (path: string) => void,
|
|
55
|
+
* remove: (path: string) => void,
|
|
56
|
+
* copyDir: (from: string, to: string) => void,
|
|
57
|
+
* copyFile: (from: string, to: string) => void,
|
|
58
|
+
* writeFile: (path: string, content: string) => void,
|
|
59
|
+
* writeJson: (path: string, data: object) => void,
|
|
60
|
+
* mergeJson: (path: string, patch: object) => void,
|
|
61
|
+
* spawn: (cmd: string, args: string[], options?: object) => object|null,
|
|
62
|
+
* gitIndex: (op: string, path: string, options?: object) => boolean,
|
|
63
|
+
* list: () => Effect[],
|
|
64
|
+
* }}
|
|
65
|
+
*/
|
|
66
|
+
export function createEffects({ dryRun = false } = {}) {
|
|
67
|
+
/** @type {Effect[]} */
|
|
68
|
+
const effects = [];
|
|
69
|
+
|
|
70
|
+
const record = (verb, target, extra = {}) => {
|
|
71
|
+
const effect = { verb, target };
|
|
72
|
+
if (extra.from !== undefined) effect.from = extra.from;
|
|
73
|
+
if (extra.detail !== undefined) effect.detail = extra.detail;
|
|
74
|
+
if (extra.args !== undefined) effect.args = extra.args;
|
|
75
|
+
effects.push(effect);
|
|
76
|
+
return effect;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
dryRun,
|
|
81
|
+
|
|
82
|
+
ensureDir(path) {
|
|
83
|
+
record('ensureDir', path);
|
|
84
|
+
if (!dryRun) fs.ensureDirSync(path);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
remove(path) {
|
|
88
|
+
record('remove', path);
|
|
89
|
+
if (!dryRun) fs.removeSync(path);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
copyDir(from, to, options = {}) {
|
|
93
|
+
record('copyDir', to, { from });
|
|
94
|
+
if (!dryRun) fs.copySync(from, to, { overwrite: true, errorOnExist: false, ...options });
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
copyFile(from, to) {
|
|
98
|
+
record('copyFile', to, { from });
|
|
99
|
+
if (!dryRun) {
|
|
100
|
+
fs.ensureDirSync(dirname(to));
|
|
101
|
+
fs.copySync(from, to, { overwrite: true });
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
writeFile(path, content) {
|
|
106
|
+
record('writeFile', path);
|
|
107
|
+
if (!dryRun) {
|
|
108
|
+
fs.ensureDirSync(dirname(path));
|
|
109
|
+
fs.writeFileSync(path, content, 'utf8');
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
writeJson(path, data) {
|
|
114
|
+
record('writeJson', path);
|
|
115
|
+
if (!dryRun) {
|
|
116
|
+
fs.ensureDirSync(dirname(path));
|
|
117
|
+
fs.writeJsonSync(path, data, { spaces: 2 });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Leitura + fusão rasa + escrita, como UM efeito.
|
|
123
|
+
*
|
|
124
|
+
* A leitura acontece nos dois modos (ler não muda nada); só a escrita é
|
|
125
|
+
* suprimida em dry. É assim que `~/.claude/settings.json` — um arquivo do
|
|
126
|
+
* usuário, FORA do projeto — é atualizado sem que o dry o toque.
|
|
127
|
+
*/
|
|
128
|
+
mergeJson(path, patch) {
|
|
129
|
+
record('mergeJson', path);
|
|
130
|
+
if (dryRun) return;
|
|
131
|
+
let current = {};
|
|
132
|
+
try {
|
|
133
|
+
if (fs.existsSync(path)) current = fs.readJsonSync(path);
|
|
134
|
+
} catch {
|
|
135
|
+
current = {};
|
|
136
|
+
}
|
|
137
|
+
fs.ensureDirSync(dirname(path));
|
|
138
|
+
fs.writeJsonSync(path, { ...current, ...patch }, { spaces: 2 });
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {string} cmd
|
|
143
|
+
* @param {string[]} args
|
|
144
|
+
* @param {{ cwd?: string, dryRunArgs?: string[] }} [options]
|
|
145
|
+
* `dryRunArgs` é a variante SÓ-LEITURA do mesmo comando (por exemplo
|
|
146
|
+
* `--check`). Em dry ela é executada no lugar; sem ela, nada roda.
|
|
147
|
+
* O REGISTRO é o mesmo nos dois modos: o efeito previsto é o comando
|
|
148
|
+
* real, não a sua sonda.
|
|
149
|
+
*/
|
|
150
|
+
spawn(cmd, args = [], options = {}) {
|
|
151
|
+
// `args` viaja no registro ALÉM do `detail` legível. O `detail` é para
|
|
152
|
+
// humano; quebrá-lo por espaço para descobrir onde o spawn escreve é o
|
|
153
|
+
// bug que um caminho como `R:\Polymorphism Tech\...` produz em silêncio,
|
|
154
|
+
// e que o relatório mostrou pondo um efeito do PACOTE da CLI no grupo
|
|
155
|
+
// "no projeto".
|
|
156
|
+
record('spawn', cmd, { detail: args.join(' '), args: [...args] });
|
|
157
|
+
const { cwd, dryRunArgs } = options;
|
|
158
|
+
if (dryRun) {
|
|
159
|
+
if (!dryRunArgs) return null;
|
|
160
|
+
return spawnSync(cmd, dryRunArgs, { cwd, encoding: 'utf8' });
|
|
161
|
+
}
|
|
162
|
+
return spawnSync(cmd, args, { cwd, encoding: 'utf8' });
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Mutação do índice do git (`git rm --cached`, etc.). A SONDA que decide
|
|
167
|
+
* se há o que fazer fica fora daqui de propósito: sondar é ler, e ler
|
|
168
|
+
* acontece nos dois modos.
|
|
169
|
+
*/
|
|
170
|
+
gitIndex(op, path, options = {}) {
|
|
171
|
+
record('gitIndex', path, { detail: op });
|
|
172
|
+
if (dryRun) return true;
|
|
173
|
+
const result = spawnSync('git', [...op.split(' '), path], {
|
|
174
|
+
cwd: options.cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
175
|
+
});
|
|
176
|
+
return result.status === 0;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
list() {
|
|
180
|
+
return effects.map((e) => ({ ...e }));
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Coletor no-op-de-registro para chamadores que não passam um: executa
|
|
187
|
+
* normalmente e registra numa lista descartada. Existe para que assinaturas
|
|
188
|
+
* novas de `installSkills`/`installAgents`/`installRulesForStack` continuem
|
|
189
|
+
* funcionando para `setup-infra` e `init` sem mudança de comportamento.
|
|
190
|
+
*/
|
|
191
|
+
export function realEffects() {
|
|
192
|
+
return createEffects({ dryRun: false });
|
|
193
|
+
}
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { existsSync } from 'fs';
|
|
19
|
-
import fs from 'fs-extra';
|
|
20
19
|
import { compareVersions } from '../../utils/version-checker.js';
|
|
20
|
+
import { realEffects } from './effects.js';
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Last CLI version whose `update`/`setup-infra` still wrote
|
|
@@ -62,7 +62,7 @@ export const LAST_VERSION_THAT_COPIED_TEMPLATES = '8.17.0';
|
|
|
62
62
|
* version BEFORE this run (e.g. `checkProjectOutdated().current`)
|
|
63
63
|
* @returns {'absent'|'removed-legacy'|'preserved-override'}
|
|
64
64
|
*/
|
|
65
|
-
export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion) {
|
|
65
|
+
export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion, fx = realEffects()) {
|
|
66
66
|
if (!existsSync(templatesDest)) return 'absent';
|
|
67
67
|
|
|
68
68
|
const isLegacy = !priorProjectVersion
|
|
@@ -71,7 +71,7 @@ export function reconcileLegacyTemplatesCopy(templatesDest, priorProjectVersion)
|
|
|
71
71
|
if (!isLegacy) return 'preserved-override';
|
|
72
72
|
|
|
73
73
|
try {
|
|
74
|
-
|
|
74
|
+
fx.remove(templatesDest);
|
|
75
75
|
return 'removed-legacy';
|
|
76
76
|
} catch {
|
|
77
77
|
// Couldn't remove — don't risk a half-broken state; leave it and let
|