@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
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eval Node — the `runner: "evals"` kind of `project.tests[]`.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS A RUNNER KIND AND NOT A SIXTH VERIFY NODE
|
|
5
|
+
*
|
|
6
|
+
* An eval IS `dotnet test` with a category filter: one command, one cwd, one
|
|
7
|
+
* clock, one exit code. Modelling it as something other than a test suite would
|
|
8
|
+
* mean a second copy of the executor, a second decision for `--filter`, a second
|
|
9
|
+
* decision for `--linux`, a second place for `expectedCount` — and a new line in
|
|
10
|
+
* a residency-capped charter that has zero slack. As a runner kind it inherits
|
|
11
|
+
* all of that for free, and the ONE thing the rollup cannot express (eval red vs
|
|
12
|
+
* unit-test red) is recovered by the verify stamp, which records `nodes.evals`
|
|
13
|
+
* separately.
|
|
14
|
+
*
|
|
15
|
+
* WHAT THIS MODULE OWNS
|
|
16
|
+
*
|
|
17
|
+
* - the STATES of a cache tree, judged BEFORE `dotnet test` is invoked. Paying
|
|
18
|
+
* for a whole suite to discover at the end that the cache did not serve is
|
|
19
|
+
* the worst possible ordering;
|
|
20
|
+
* - the `MORPH_EVAL_*` variables the CLI DICTATES, so the same xunit suite is
|
|
21
|
+
* runnable by `verify` without a second configuration path;
|
|
22
|
+
* - reading the summary the fixture writes — where **null is UNKNOWN, never
|
|
23
|
+
* zero**.
|
|
24
|
+
*
|
|
25
|
+
* WHAT IT DELIBERATELY DOES NOT OWN
|
|
26
|
+
*
|
|
27
|
+
* The pin. `readAiPin()` (src/lib/standards/verified-against.js) is the single
|
|
28
|
+
* reader of `framework/ai-pin.json`; a second parser here is how two truths
|
|
29
|
+
* about "which version is pinned" begin.
|
|
30
|
+
*
|
|
31
|
+
* @module eval-node
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
35
|
+
import { join, resolve, sep } from 'path';
|
|
36
|
+
import { tmpdir } from 'os';
|
|
37
|
+
|
|
38
|
+
import { isInside } from './test-runner.js';
|
|
39
|
+
import { readAiPin } from '../standards/verified-against.js';
|
|
40
|
+
|
|
41
|
+
/** The runner kind. One spelling, imported by every reader. */
|
|
42
|
+
export const EVAL_RUNNER = 'evals';
|
|
43
|
+
|
|
44
|
+
/** Stamp file at the root of a cache tree — the SAME name the .NET fixture writes. */
|
|
45
|
+
export const EVAL_STAMP_FILE = '.morph-eval-cache.json';
|
|
46
|
+
|
|
47
|
+
/** Default dataset root, relative to the cache root's parent. */
|
|
48
|
+
export const DEFAULT_DATASET_DIR = 'eval-datasets';
|
|
49
|
+
|
|
50
|
+
/** Default credential file. Gitignored, like `e2e.auth.envFile`; never in the committed config. */
|
|
51
|
+
export const DEFAULT_ENV_FILE = '.env.evals';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The pin entries that decide the FORMAT of a response cache.
|
|
55
|
+
*
|
|
56
|
+
* Only the `Microsoft.Extensions.AI` family: the cache key is computed by that
|
|
57
|
+
* library, and its own docs say the hash is not guaranteed stable across
|
|
58
|
+
* releases. Bumping an unrelated provider package (OpenAI, Google.GenAI) cannot
|
|
59
|
+
* invalidate a cache tree, and a stamp that reacted to it would fail for
|
|
60
|
+
* nothing — a guardrail that cries wolf gets switched off.
|
|
61
|
+
*
|
|
62
|
+
* @param {Record<string, {version: string}>} packages
|
|
63
|
+
* @returns {Record<string, string>} name → exact version
|
|
64
|
+
*/
|
|
65
|
+
export function cacheRelevantPackages(packages) {
|
|
66
|
+
const out = {};
|
|
67
|
+
for (const [name, entry] of Object.entries(packages ?? {})) {
|
|
68
|
+
if (name === 'Microsoft.Extensions.AI' || name.startsWith('Microsoft.Extensions.AI.Evaluation')) {
|
|
69
|
+
out[name] = entry?.version;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read the pin from a project, wherever the framework lives.
|
|
77
|
+
*
|
|
78
|
+
* Two roots, ONE reader: this repository carries `framework/ai-pin.json` at the
|
|
79
|
+
* top; an installed project carries it under `.morph/framework/`. `readAiPin`
|
|
80
|
+
* appends `framework/ai-pin.json` to whatever root it is given, so both cases
|
|
81
|
+
* are the same function with a different argument — never a second parser.
|
|
82
|
+
*
|
|
83
|
+
* Returns null when neither exists: a project with no pin simply has no stamp
|
|
84
|
+
* ruler, and refusing to run there would make the eval node unusable outside
|
|
85
|
+
* this framework's own stack.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} projectPath
|
|
88
|
+
* @returns {{pinnedAt: string, packages: object}|null}
|
|
89
|
+
*/
|
|
90
|
+
export function readProjectPin(projectPath) {
|
|
91
|
+
for (const root of [projectPath, join(projectPath, '.morph')]) {
|
|
92
|
+
try {
|
|
93
|
+
return readAiPin(root);
|
|
94
|
+
} catch {
|
|
95
|
+
// Next candidate. The throw is readAiPin's contract for "no pin here",
|
|
96
|
+
// and it is the reason this loop exists instead of an existsSync probe
|
|
97
|
+
// that would duplicate the path knowledge.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve every path an eval node names, against the project root.
|
|
105
|
+
*
|
|
106
|
+
* @param {import('./test-nodes.js').TestNode & {evals?: object}} node
|
|
107
|
+
* @param {string} projectPath
|
|
108
|
+
* @returns {{cacheRoot: string, datasetRoot: string, resultsRoot: string, envFile: string}}
|
|
109
|
+
*/
|
|
110
|
+
export function resolveEvalPaths(node, projectPath) {
|
|
111
|
+
const root = resolve(projectPath);
|
|
112
|
+
const evals = node?.evals ?? {};
|
|
113
|
+
const cacheRoot = resolve(root, evals.cachePath ?? '');
|
|
114
|
+
const beside = resolve(cacheRoot, '..');
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
cacheRoot,
|
|
118
|
+
datasetRoot: evals.datasetPath ? resolve(root, evals.datasetPath) : join(beside, DEFAULT_DATASET_DIR),
|
|
119
|
+
// Results live OUTSIDE the committed cache tree by construction — they carry
|
|
120
|
+
// stopwatch latency, token usage and the cache-hit flag, so committing them
|
|
121
|
+
// is a guaranteed diff on every run with no signal in it.
|
|
122
|
+
resultsRoot: join(beside, 'eval-results'),
|
|
123
|
+
envFile: resolve(root, evals.envFile ?? DEFAULT_ENV_FILE),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* First problem with a node's `evals` block, or null when it is well formed.
|
|
129
|
+
*
|
|
130
|
+
* Fail-CLOSED, like the rest of `validateTestNodes`: a typo here would make the
|
|
131
|
+
* eval silently disappear, which is the exact defect this feature exists to
|
|
132
|
+
* prevent.
|
|
133
|
+
*
|
|
134
|
+
* @param {*} raw - the raw `tests[]` entry
|
|
135
|
+
* @param {string|null} projectPath
|
|
136
|
+
* @returns {string|null}
|
|
137
|
+
*/
|
|
138
|
+
export function evalBlockProblem(raw, projectPath) {
|
|
139
|
+
const evals = raw?.evals;
|
|
140
|
+
|
|
141
|
+
if (evals === undefined) {
|
|
142
|
+
return 'runner "evals" requires an evals.cachePath — a default would create a cache tree somewhere nobody declared';
|
|
143
|
+
}
|
|
144
|
+
if (typeof evals !== 'object' || evals === null || Array.isArray(evals)) {
|
|
145
|
+
return 'evals must be an object';
|
|
146
|
+
}
|
|
147
|
+
if (typeof evals.cachePath !== 'string' || !evals.cachePath.trim()) {
|
|
148
|
+
return 'evals.cachePath must be a non-empty string';
|
|
149
|
+
}
|
|
150
|
+
for (const key of ['datasetPath', 'envFile']) {
|
|
151
|
+
if (evals[key] !== undefined && (typeof evals[key] !== 'string' || !evals[key].trim())) {
|
|
152
|
+
return `evals.${key} must be a non-empty string`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const root = resolve(projectPath ?? '.');
|
|
157
|
+
for (const key of ['cachePath', 'datasetPath']) {
|
|
158
|
+
const value = evals[key];
|
|
159
|
+
if (typeof value !== 'string' || !value.trim()) continue;
|
|
160
|
+
// Same rule as `cwd`: a path that leaves the root addresses ANOTHER tree.
|
|
161
|
+
if (!isInside(resolve(root, value.trim()), root)) {
|
|
162
|
+
return `evals.${key} "${value}" resolves outside the project root`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Does this cache tree hold at least one entry, beyond the stamp? */
|
|
170
|
+
function cacheHasEntries(cacheRoot) {
|
|
171
|
+
const walk = (dir) => {
|
|
172
|
+
let entries;
|
|
173
|
+
try {
|
|
174
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
const full = join(dir, entry.name);
|
|
180
|
+
if (entry.isDirectory()) {
|
|
181
|
+
if (walk(full)) return true;
|
|
182
|
+
} else if (entry.name !== EVAL_STAMP_FILE) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
};
|
|
188
|
+
return walk(cacheRoot);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Read the stamp the .NET fixture wrote. null for absent OR unreadable — Check tells them apart. */
|
|
192
|
+
export function readEvalStamp(cacheRoot) {
|
|
193
|
+
const path = join(cacheRoot, EVAL_STAMP_FILE);
|
|
194
|
+
if (!existsSync(path)) return null;
|
|
195
|
+
try {
|
|
196
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
197
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Decide, BEFORE spending a single second of suite clock, whether this eval node
|
|
205
|
+
* can run.
|
|
206
|
+
*
|
|
207
|
+
* The order is the order of the spec, and it is not arbitrary: the three cache
|
|
208
|
+
* states are judged before the command is invoked, because discovering at the
|
|
209
|
+
* END of a suite that the cache did not serve costs the whole clock and tells
|
|
210
|
+
* the reader nothing they could not have been told first.
|
|
211
|
+
*
|
|
212
|
+
* **There is no `skip` for a bad cache.** A skip here would recreate the hole
|
|
213
|
+
* this harness closed twice: a green gate over a verification that never
|
|
214
|
+
* happened. The only skip is scope — an eval suite does not belong in the
|
|
215
|
+
* per-task loop.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} projectPath
|
|
218
|
+
* @param {object} node - a resolved test node with `runner === 'evals'`
|
|
219
|
+
* @param {{taskId?: string|null, refresh?: boolean}} [options]
|
|
220
|
+
* @returns {{status: 'skip'|'fail'|null, reason: string|null}} status null means RUN
|
|
221
|
+
*/
|
|
222
|
+
export function evalPreflight(projectPath, node, options = {}) {
|
|
223
|
+
const { taskId = null, refresh = false } = options;
|
|
224
|
+
|
|
225
|
+
if (node?.runner !== EVAL_RUNNER) return { status: null, reason: null };
|
|
226
|
+
|
|
227
|
+
// Scope first: an eval suite does not pay for itself in a per-task loop, and
|
|
228
|
+
// the result Gate 3 reads is the feature-scoped one. Same treatment `e2e: true`
|
|
229
|
+
// already gets.
|
|
230
|
+
if (taskId) {
|
|
231
|
+
return { status: 'skip', reason: 'eval node runs at feature scope only' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const { cacheRoot, envFile } = resolveEvalPaths(node, projectPath);
|
|
235
|
+
|
|
236
|
+
// In refresh the tree is being (re)written; demanding a populated, stamped
|
|
237
|
+
// cache would make the FIRST cache impossible to create.
|
|
238
|
+
if (refresh) {
|
|
239
|
+
if (!existsSync(envFile)) {
|
|
240
|
+
return {
|
|
241
|
+
status: 'fail',
|
|
242
|
+
reason: `eval-refresh-no-credential: --refresh-cache exige o arquivo de credencial "${envFile}", que não existe`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return { status: null, reason: null };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!existsSync(cacheRoot)) {
|
|
249
|
+
return { status: 'fail', reason: `eval-cache-missing: ${cacheRoot}` };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!cacheHasEntries(cacheRoot)) {
|
|
253
|
+
// Distinct from `missing` on purpose: an empty COMMITTED directory is far
|
|
254
|
+
// more likely a `.gitignore` mistake than a deliberate absence, and the two
|
|
255
|
+
// ask for different fixes.
|
|
256
|
+
return { status: 'fail', reason: `eval-cache-empty: ${cacheRoot} não contém nenhuma entrada de cache` };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const pin = readProjectPin(projectPath);
|
|
260
|
+
if (!pin) {
|
|
261
|
+
// No pin, no ruler — and a project without one is a legitimate case, not a
|
|
262
|
+
// failure: the stamp check simply does not apply. Saying so is the point;
|
|
263
|
+
// silently treating it as "stamp ok" would be the same thing said badly.
|
|
264
|
+
return { status: null, reason: null };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const stamp = readEvalStamp(cacheRoot);
|
|
268
|
+
if (!stamp) {
|
|
269
|
+
return {
|
|
270
|
+
status: 'fail',
|
|
271
|
+
reason: `eval-cache-unstamped: ${join(cacheRoot, EVAL_STAMP_FILE)} ausente ou ilegível`
|
|
272
|
+
+ ' — a árvore não diz sob quais versões foi gravada; rode verify --refresh-cache',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const expected = cacheRelevantPackages(pin.packages);
|
|
277
|
+
const recorded = stamp.packages ?? {};
|
|
278
|
+
const divergent = Object.entries(expected)
|
|
279
|
+
.filter(([name, version]) => recorded[name] !== version)
|
|
280
|
+
.map(([name, version]) => `${name}: gravada ${recorded[name] ?? '(ausente)'} vs pin ${version}`);
|
|
281
|
+
|
|
282
|
+
if (divergent.length > 0) {
|
|
283
|
+
return {
|
|
284
|
+
status: 'fail',
|
|
285
|
+
reason: `eval-cache-stale-pin: ${divergent.join('; ')} — rode verify --refresh-cache`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { status: null, reason: null };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The `MORPH_EVAL_*` variables the CLI dictates for one eval node.
|
|
294
|
+
*
|
|
295
|
+
* The summary path is chosen HERE, by the CLI, and not by the fixture: two eval
|
|
296
|
+
* nodes in the same `verify` must not collide, and the process that reads the
|
|
297
|
+
* file is the one that should decide where it lives.
|
|
298
|
+
*
|
|
299
|
+
* @param {object} node
|
|
300
|
+
* @param {string} projectPath
|
|
301
|
+
* @param {{refresh?: boolean}} [options]
|
|
302
|
+
* @returns {{env: Record<string,string>, summaryPath: string}}
|
|
303
|
+
*/
|
|
304
|
+
export function evalNodeEnv(node, projectPath, options = {}) {
|
|
305
|
+
const { refresh = false } = options;
|
|
306
|
+
const { cacheRoot, datasetRoot, resultsRoot, envFile } = resolveEvalPaths(node, projectPath);
|
|
307
|
+
const summaryPath = join(tmpdir(), `morph-eval-${node.id}-${process.pid}.json`);
|
|
308
|
+
|
|
309
|
+
const env = {
|
|
310
|
+
MORPH_EVAL_CACHE: cacheRoot,
|
|
311
|
+
MORPH_EVAL_DATASET: datasetRoot,
|
|
312
|
+
MORPH_EVAL_RESULTS: resultsRoot,
|
|
313
|
+
MORPH_EVAL_MODE: refresh ? 'refresh' : 'offline',
|
|
314
|
+
MORPH_EVAL_SUMMARY: summaryPath,
|
|
315
|
+
MORPH_EVAL_ENVFILE: envFile,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const pin = readProjectPin(projectPath);
|
|
319
|
+
if (pin) {
|
|
320
|
+
// Compact, and only the packages that decide the cache format. The fixture
|
|
321
|
+
// stamps from THIS, so it never has to read (or parse) the pin itself.
|
|
322
|
+
env.MORPH_EVAL_PIN = JSON.stringify({
|
|
323
|
+
pinnedAt: pin.pinnedAt,
|
|
324
|
+
packages: cacheRelevantPackages(pin.packages),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// The credential file goes on top, same pattern as `e2e.auth.envFile`. Absent
|
|
329
|
+
// is NOT an error offline: that is the whole point of a warm cache.
|
|
330
|
+
Object.assign(env, readEnvFile(envFile));
|
|
331
|
+
|
|
332
|
+
return { env, summaryPath };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Parse a `KEY=value` file. Blank lines and `#` comments ignored; surrounding
|
|
337
|
+
* quotes stripped; the first `=` splits.
|
|
338
|
+
*
|
|
339
|
+
* Fail-open to `{}`: an unreadable credential file must not take down a run that
|
|
340
|
+
* did not need a credential in the first place.
|
|
341
|
+
*
|
|
342
|
+
* @param {string} path
|
|
343
|
+
* @returns {Record<string,string>}
|
|
344
|
+
*/
|
|
345
|
+
export function readEnvFile(path) {
|
|
346
|
+
if (!path || !existsSync(path)) return {};
|
|
347
|
+
let text;
|
|
348
|
+
try {
|
|
349
|
+
text = readFileSync(path, 'utf8');
|
|
350
|
+
} catch {
|
|
351
|
+
return {};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const out = {};
|
|
355
|
+
for (const line of text.split(/\r?\n/)) {
|
|
356
|
+
const trimmed = line.trim();
|
|
357
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
358
|
+
const eq = trimmed.indexOf('=');
|
|
359
|
+
if (eq <= 0) continue;
|
|
360
|
+
const key = trimmed.slice(0, eq).trim();
|
|
361
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
362
|
+
if (value.length >= 2 && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
363
|
+
value = value.slice(1, -1);
|
|
364
|
+
}
|
|
365
|
+
if (key) out[key] = value;
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Read the summary the fixture wrote.
|
|
372
|
+
*
|
|
373
|
+
* @param {string} path
|
|
374
|
+
* @returns {object|null} **null is UNKNOWN, never zero.** A summary nobody could
|
|
375
|
+
* read is not a suite that evaluated zero scenarios: collapsing the two would
|
|
376
|
+
* manufacture a count drop against `expectedCount` out of a missing file.
|
|
377
|
+
*/
|
|
378
|
+
export function readEvalSummary(path) {
|
|
379
|
+
if (!path || !existsSync(path)) return null;
|
|
380
|
+
try {
|
|
381
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
382
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
383
|
+
return parsed;
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* How many scenarios an eval run evaluated.
|
|
391
|
+
*
|
|
392
|
+
* The summary wins when it is readable; otherwise the caller falls back to the
|
|
393
|
+
* `dotnet` extractor, because an eval IS `dotnet test`. **No new extractor is
|
|
394
|
+
* invented**, and an unreadable summary yields null, never 0.
|
|
395
|
+
*
|
|
396
|
+
* @param {object|null} summary
|
|
397
|
+
* @returns {number|null}
|
|
398
|
+
*/
|
|
399
|
+
export function evalScenarioCount(summary) {
|
|
400
|
+
const n = summary?.scenarios;
|
|
401
|
+
return Number.isInteger(n) ? n : null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The NAMED failures of an eval run, one readable line each.
|
|
406
|
+
*
|
|
407
|
+
* The summary publishes `failures[]` (scenario, assertion, kind, reason) and
|
|
408
|
+
* without this reader the field would be written and never surfaced — a
|
|
409
|
+
* published field with no consumer, the same shape #91 closed on `superseded`.
|
|
410
|
+
* Worse here than usual: the whole point of `failures[]` is that a red eval
|
|
411
|
+
* names what broke, and a name nobody prints is not a name.
|
|
412
|
+
*
|
|
413
|
+
* Defensive by construction, because this parses a file produced by ANOTHER
|
|
414
|
+
* process: a summary that is absent, unreadable, or carries a malformed entry
|
|
415
|
+
* yields fewer lines, never a throw. A crash here would turn "the eval failed"
|
|
416
|
+
* into "the verify crashed", which reports the wrong defect.
|
|
417
|
+
*
|
|
418
|
+
* @param {object|null} summary
|
|
419
|
+
* @returns {string[]} readable lines; empty when there is nothing to name
|
|
420
|
+
*/
|
|
421
|
+
export function evalFailureLines(summary) {
|
|
422
|
+
const failures = summary?.failures;
|
|
423
|
+
if (!Array.isArray(failures)) return [];
|
|
424
|
+
|
|
425
|
+
return failures
|
|
426
|
+
.filter((f) => f && typeof f === 'object')
|
|
427
|
+
.map((f) => {
|
|
428
|
+
const scenario = typeof f.scenario === 'string' && f.scenario ? f.scenario : '(cenário sem nome)';
|
|
429
|
+
const assertion = typeof f.assertion === 'string' && f.assertion ? f.assertion : '(asserção sem nome)';
|
|
430
|
+
const kind = typeof f.kind === 'string' && f.kind ? f.kind : '(régua desconhecida)';
|
|
431
|
+
const reason = typeof f.reason === 'string' && f.reason ? f.reason : '(sem motivo registrado)';
|
|
432
|
+
return `eval-fail: ${scenario} — ${assertion} (${kind}): ${reason}`;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Why an otherwise-green eval run must still be red, or null when it is fine.
|
|
438
|
+
*
|
|
439
|
+
* One case, and it is the guardrail-inert shape this repository has already paid
|
|
440
|
+
* to learn: a suite that evaluated ZERO scenarios and exited 0. The exit code
|
|
441
|
+
* alone cannot see it, because nothing failed — nothing ran.
|
|
442
|
+
*
|
|
443
|
+
* @param {object|null} summary
|
|
444
|
+
* @returns {string|null}
|
|
445
|
+
*/
|
|
446
|
+
export function evalEmptyRunProblem(summary) {
|
|
447
|
+
if (summary && summary.scenarios === 0) {
|
|
448
|
+
return 'eval-empty-run: a suíte de eval avaliou 0 cenários e saiu verde';
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Warning for a run whose cache went cold — the tree changed and must be committed.
|
|
455
|
+
*
|
|
456
|
+
* A warning and not a failure, deliberately: spending money nobody asked for is
|
|
457
|
+
* bad, but lying about the result is worse. Without the warning, a freshly
|
|
458
|
+
* written cache stays on the disk of whoever ran it and CI misses tomorrow.
|
|
459
|
+
*
|
|
460
|
+
* @param {object|null} summary
|
|
461
|
+
* @returns {string|null}
|
|
462
|
+
*/
|
|
463
|
+
export function evalColdCacheWarning(summary) {
|
|
464
|
+
if (summary?.cache === 'cold') {
|
|
465
|
+
const misses = Number.isInteger(summary.misses) ? summary.misses : '?';
|
|
466
|
+
return `a árvore de cache mudou (${misses} miss(es)) — commite ${EVAL_STAMP_FILE} e as entradas novas`;
|
|
467
|
+
}
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Relative form of a path, for messages that should not shout an absolute path. */
|
|
472
|
+
export function relativeToProject(projectPath, target) {
|
|
473
|
+
const root = resolve(projectPath) + sep;
|
|
474
|
+
const full = resolve(target);
|
|
475
|
+
return full.startsWith(root) ? full.slice(root.length).replace(/\\/g, '/') : full;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Is `path` a directory? Fail-open to false. */
|
|
479
|
+
export function isDirectory(path) {
|
|
480
|
+
try {
|
|
481
|
+
return statSync(path).isDirectory();
|
|
482
|
+
} catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|