@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
|
@@ -11,15 +11,14 @@ import {
|
|
|
11
11
|
buildBranchName,
|
|
12
12
|
branchExists,
|
|
13
13
|
cwdIsInsideWorktree,
|
|
14
|
+
classifyIgnoredLeftovers,
|
|
14
15
|
} from '../project/worktree.js';
|
|
15
16
|
import { detectDefaultBranch, hasOriginRemote } from '../../lib/git/default-branch.js';
|
|
17
|
+
import { commitsAhead, resolveBaseRef } from '../../lib/git/branch-classifier.js';
|
|
16
18
|
import { resolveGh } from '../../lib/git/gh.js';
|
|
17
19
|
import { releaseClaim } from '../../../framework/hooks/shared/claim-registry.js';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
resolveComposeFile,
|
|
21
|
-
tearDownStack,
|
|
22
|
-
} from '../../../framework/hooks/shared/e2e-check.js';
|
|
20
|
+
import { tearDownFeatureStack } from '../../lib/e2e/stack-teardown.js';
|
|
21
|
+
import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
|
|
23
22
|
import { runArchive } from './archive.js';
|
|
24
23
|
|
|
25
24
|
/** workType → conventional-commit type. `hotfix` IS a fix; anything else is a chore. */
|
|
@@ -88,8 +87,14 @@ export function buildPrTitle(feature, fj) {
|
|
|
88
87
|
* `resumed: true` in its output — see Validation 0c.
|
|
89
88
|
*
|
|
90
89
|
* @param {string} feature
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* `--dry-run` desvia para {@link finishPreflight} ANTES de qualquer validação
|
|
91
|
+
* que saia com exit 1 e ANTES de qualquer mutação: o pré-voo existe para
|
|
92
|
+
* EXPLICAR, e um pré-voo que recusa pelos mesmos motivos do comando real não
|
|
93
|
+
* explica nada.
|
|
94
|
+
*
|
|
95
|
+
* @param {{ pr?: boolean, merge?: boolean, keep?: boolean, keepVolumes?: boolean,
|
|
96
|
+
* push?: boolean, force?: boolean, discardIgnored?: boolean, dryRun?: boolean,
|
|
97
|
+
* cwd?: string }} [options]
|
|
93
98
|
*/
|
|
94
99
|
export async function finishCommand(feature, options = {}) {
|
|
95
100
|
const cwd = options.cwd || process.cwd();
|
|
@@ -101,12 +106,11 @@ export async function finishCommand(feature, options = {}) {
|
|
|
101
106
|
// is the single point that covers merge/PR/--keep alike. Fail-open: a claim
|
|
102
107
|
// that outlives its feature only expires on its own TTL.
|
|
103
108
|
//
|
|
104
|
-
// The e2e stack
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// already merged.
|
|
109
|
+
// The e2e stack is NOT reclaimed here: by the time a success is emitted the
|
|
110
|
+
// worktree that owns the compose file may already be gone. Each integration
|
|
111
|
+
// path calls `reclaimStack` immediately BEFORE its worktree teardown (or
|
|
112
|
+
// before its final emit in the common flow) — see stack-teardown.js.
|
|
113
|
+
//
|
|
110
114
|
// Set by Validation 0c when this run picks up an already-archived feature.
|
|
111
115
|
// Stamped onto EVERY payload emitted from that point on, success or refusal:
|
|
112
116
|
// a resume merges a branch the user did not archive in this run, and that has
|
|
@@ -121,7 +125,6 @@ export async function finishCommand(feature, options = {}) {
|
|
|
121
125
|
const common = execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd: root, stdio: 'pipe' }).toString().trim();
|
|
122
126
|
if (common) releaseClaim(feature, { gitCommonDir: resolve(root, common) });
|
|
123
127
|
} catch { /* fail-open */ }
|
|
124
|
-
tearDownE2eStack(feature, root);
|
|
125
128
|
}
|
|
126
129
|
console.log(JSON.stringify(obj));
|
|
127
130
|
};
|
|
@@ -142,6 +145,26 @@ export async function finishCommand(feature, options = {}) {
|
|
|
142
145
|
process.exit(1);
|
|
143
146
|
}
|
|
144
147
|
const mode = wantPr ? 'pr' : 'merge';
|
|
148
|
+
|
|
149
|
+
// `--push` publica a DEFAULT depois de um merge local; o fluxo `--pr` não tem
|
|
150
|
+
// merge local nenhum (a default só recebe a feature quando o PR mergear), e
|
|
151
|
+
// já publica `morph/{f}` por conta própria. A combinação é um pedido que o
|
|
152
|
+
// comando não consegue honrar — e aceitar em silêncio o que não se pode
|
|
153
|
+
// honrar é exatamente o defeito que esta série de fixes existe para tirar do
|
|
154
|
+
// CLI (o `score` que gravava um zero fabricado, o `approve` que saía 0 sem
|
|
155
|
+
// aprovar nada). Recusa ALTA, e AQUI: junto do gate de modo, antes do
|
|
156
|
+
// archive — que é a primeira mutação do finish. Uma recusa depois dele
|
|
157
|
+
// deixaria a feature meio fechada, pior que o no-op que ela substitui.
|
|
158
|
+
if (wantPr && options.push) {
|
|
159
|
+
emit({
|
|
160
|
+
finished: false,
|
|
161
|
+
error: 'push-with-pr',
|
|
162
|
+
hint: `--push não se combina com --pr: o --pr já publica ${buildBranchName(feature)} e abre o PR, enquanto o --push publica a branch default depois de um merge LOCAL — e no fluxo --pr não há merge local. Para publicar a branch e abrir o PR: \`morph-spec finish ${feature} --pr\`. Para integrar na default localmente e publicá-la: \`morph-spec finish ${feature} --merge --push\`.`,
|
|
163
|
+
feature,
|
|
164
|
+
});
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
145
168
|
const expectedBranch = buildBranchName(feature);
|
|
146
169
|
|
|
147
170
|
// ── Resolve the flow ──────────────────────────────────────────────────────
|
|
@@ -155,6 +178,29 @@ export async function finishCommand(feature, options = {}) {
|
|
|
155
178
|
let worktreePath = null;
|
|
156
179
|
let featureCwd = root; // where the feature's own tree (and Gate 3) lives
|
|
157
180
|
|
|
181
|
+
// ── PRÉ-VOO ───────────────────────────────────────────────────────────────
|
|
182
|
+
// AQUI, e não mais abaixo: as validações seguintes saem com exit 1 (branch
|
|
183
|
+
// divergente, cwd dentro do worktree, feature ausente), e o pré-voo existe
|
|
184
|
+
// justamente para ser rodado ANTES de o estado estar bom. Ele reporta o que
|
|
185
|
+
// encontrar — inclusive o cwd dentro do worktree, como AVISO — e sai 0 sem
|
|
186
|
+
// ter tocado em nada.
|
|
187
|
+
if (options.dryRun) {
|
|
188
|
+
emit(finishPreflight({
|
|
189
|
+
feature,
|
|
190
|
+
root,
|
|
191
|
+
worktreePath: isWorktreeFlow ? found.path : null,
|
|
192
|
+
cwd,
|
|
193
|
+
mode,
|
|
194
|
+
branch: expectedBranch,
|
|
195
|
+
// `options` INTEIRO, e não um subconjunto escolhido a dedo: `--discard-ignored`
|
|
196
|
+
// INVERTE a classificação (o exclusivo deixa de ser copiado e passa a ser
|
|
197
|
+
// destruído), então um pré-voo que não recebe as flags do comando prevê
|
|
198
|
+
// outro comando. Ver B1 na errata E4.
|
|
199
|
+
options,
|
|
200
|
+
}));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
158
204
|
if (isWorktreeFlow) {
|
|
159
205
|
// Validation 0: the worktree must actually be ON the branch we will merge.
|
|
160
206
|
if (found.branch !== found.expectedBranch) {
|
|
@@ -447,6 +493,166 @@ export async function finishCommand(feature, options = {}) {
|
|
|
447
493
|
return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch });
|
|
448
494
|
}
|
|
449
495
|
|
|
496
|
+
/**
|
|
497
|
+
* O aviso que acompanha CADA reparse point listado pelo pré-voo.
|
|
498
|
+
*
|
|
499
|
+
* Exportado porque é a frase que carrega o fato: uma junction/symlink dentro do
|
|
500
|
+
* worktree aponta para conteúdo que vive na RAIZ, e `rm -rf` / `git worktree
|
|
501
|
+
* remove --force` atravessam o link. Já aconteceu: um `worktree remove --force`
|
|
502
|
+
* atravessou a junction e apagou o `node_modules` da raiz.
|
|
503
|
+
*/
|
|
504
|
+
export const REPARSE_WARNING =
|
|
505
|
+
'estes são junctions/symlinks: apagá-los fora do morph-spec apaga o conteúdo REAL na raiz';
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* `morph-spec finish <feature> --pr|--merge --dry-run` — o PRÉ-VOO.
|
|
509
|
+
*
|
|
510
|
+
* NÃO MUTA NADA. Nenhum commit, nenhum archive, nenhum worktree removido,
|
|
511
|
+
* nenhum `gh` que crie coisa alguma, nenhum `git fetch`. Ele responde, ANTES,
|
|
512
|
+
* as três perguntas que hoje só se respondem depois do estrago:
|
|
513
|
+
*
|
|
514
|
+
* 1. O que este `finish` vai COPIAR para o backup e o que vai DESCARTAR?
|
|
515
|
+
* (`classifyIgnoredLeftovers` — a MESMA função que o teardown consulta;
|
|
516
|
+
* um segundo enumerador aqui faria o pré-voo prometer o que o comando não
|
|
517
|
+
* cumpre, e um dry-run que mente é pior que nenhum.)
|
|
518
|
+
* 2. Que reparse points existem, e o que significa apagá-los por fora?
|
|
519
|
+
* (`findReparsePoints`, exaustivo e fail-closed.)
|
|
520
|
+
* 3. A base andou? — a pergunta INVERSA da de #89. Lá se mede "quantos
|
|
521
|
+
* commits EU tenho que o upstream não tem"; aqui, "quantos commits
|
|
522
|
+
* `origin/{default}` tem que a MINHA base não tem", que é o que decide se
|
|
523
|
+
* este merge vai ser trivial ou uma surpresa. Mesma função
|
|
524
|
+
* (`commitsAhead`), argumentos invertidos.
|
|
525
|
+
*
|
|
526
|
+
* `cwd` dentro do worktree é AVISO, não recusa: o caminho real continua
|
|
527
|
+
* recusando antes da primeira mutação (Validation 0b), e recusar aqui negaria
|
|
528
|
+
* ao usuário justamente a explicação que ele veio buscar — do lugar mais
|
|
529
|
+
* provável de estar quando a pede.
|
|
530
|
+
*
|
|
531
|
+
* SEM `git fetch` implícito: um fetch dentro de um comando que promete não
|
|
532
|
+
* mexer em nada é efeito colateral de rede. O pré-voo AVISA que o dado pode
|
|
533
|
+
* estar velho e sugere o fetch.
|
|
534
|
+
*
|
|
535
|
+
* @param {{ feature: string, root: string, worktreePath: string|null,
|
|
536
|
+
* cwd: string, mode: 'pr'|'merge', branch: string,
|
|
537
|
+
* options?: { discardIgnored?: boolean, keep?: boolean } }} args
|
|
538
|
+
* @returns {object} o payload emitido — nunca lança
|
|
539
|
+
*/
|
|
540
|
+
export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch, options = {} }) {
|
|
541
|
+
const warnings = [];
|
|
542
|
+
|
|
543
|
+
// ── A base andou? (vale COM e SEM worktree — UC-16 A1) ────────────────────
|
|
544
|
+
const defaultBranch = detectDefaultBranch(root);
|
|
545
|
+
const baseRef = defaultBranch ? resolveBaseRef(defaultBranch, root) : null;
|
|
546
|
+
// `commitsAhead(base, branch)` conta `base..branch`. Passando a branch da
|
|
547
|
+
// feature como BASE e o baseRef como BRANCH, contamos o que a default tem e a
|
|
548
|
+
// feature não — exatamente a pergunta inversa.
|
|
549
|
+
const commits = baseRef && branchExists(branch, root)
|
|
550
|
+
? commitsAhead(branch, baseRef, root)
|
|
551
|
+
: null;
|
|
552
|
+
|
|
553
|
+
const out = {
|
|
554
|
+
preflight: true,
|
|
555
|
+
finished: false,
|
|
556
|
+
feature,
|
|
557
|
+
mode,
|
|
558
|
+
branch,
|
|
559
|
+
baseAdvanced: { ref: baseRef, commits },
|
|
560
|
+
// `null` é DESCONHECIDO e nunca é renderizado como 0 (UC-16 P6): "não deu
|
|
561
|
+
// para perguntar" e "a default não andou" são fatos opostos, e confundi-los
|
|
562
|
+
// é como um `--dry-run` passa confiança que não tem.
|
|
563
|
+
baseAdvancedNote: commits === null
|
|
564
|
+
? (baseRef === null
|
|
565
|
+
? 'sem contraparte remota nem branch default resolvível — a contagem é DESCONHECIDA, não zero'
|
|
566
|
+
: 'não foi possível comparar as refs — a contagem é DESCONHECIDA, não zero')
|
|
567
|
+
: `o dado é do último \`git fetch\`; rode \`git fetch ${PUSH_REMOTE}\` para atualizá-lo (o pré-voo não faz fetch: ele promete não mexer em nada)`,
|
|
568
|
+
warnings,
|
|
569
|
+
wouldDo: mode === 'merge'
|
|
570
|
+
? [
|
|
571
|
+
`arquivar .morph/features/${feature} em .morph/archive/${feature} (commit na branch ${branch})`,
|
|
572
|
+
`fazer merge --no-ff de ${branch} em ${defaultBranch || '<default não resolvida>'} LOCALMENTE (sem push)`,
|
|
573
|
+
...(worktreePath ? [`derrubar o stack e remover o worktree ${worktreePath.replace(/\\/g, '/')}${options.discardIgnored === true ? ' — SEM copiar os gitignored exclusivos para o backup (--discard-ignored)' : ''}`] : []),
|
|
574
|
+
]
|
|
575
|
+
: [
|
|
576
|
+
`arquivar .morph/features/${feature} em .morph/archive/${feature} (commit na branch ${branch})`,
|
|
577
|
+
`publicar ${branch} em ${PUSH_REMOTE} e abrir um PR contra ${defaultBranch || '<default não resolvida>'} (NÃO integra: o merge do PR é que encerra)`,
|
|
578
|
+
...(worktreePath ? [`derrubar o stack e remover o worktree ${worktreePath.replace(/\\/g, '/')}${options.discardIgnored === true ? ' — SEM copiar os gitignored exclusivos para o backup (--discard-ignored)' : ''}`] : []),
|
|
579
|
+
],
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
if (defaultBranch) out.defaultBranch = defaultBranch;
|
|
583
|
+
|
|
584
|
+
// ── Seções de worktree — omitidas quando não há worktree (UC-16 A1) ───────
|
|
585
|
+
if (worktreePath) {
|
|
586
|
+
out.worktreePath = worktreePath.replace(/\\/g, '/');
|
|
587
|
+
|
|
588
|
+
const inside = cwdIsInsideWorktree(cwd, worktreePath);
|
|
589
|
+
out.cwdInsideWorktree = inside;
|
|
590
|
+
if (inside) {
|
|
591
|
+
warnings.push(
|
|
592
|
+
`você está DENTRO de ${out.worktreePath}: o finish de verdade recusa aqui, antes de qualquer mutação `
|
|
593
|
+
+ `(o Windows tranca o diretório de trabalho do processo e a remoção falharia no meio). `
|
|
594
|
+
+ `Rode o finish a partir de "${root.replace(/\\/g, '/')}"`,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// AS MESMAS FLAGS do comando real. `--discard-ignored` INVERTE a
|
|
599
|
+
// classificação — o exclusivo deixa de ser copiado e entra no lote
|
|
600
|
+
// destruído sem backup — então prever com `options` vazio é prever OUTRO
|
|
601
|
+
// comando. Medido antes do conserto: o pré-voo prometia salvar caminhos que
|
|
602
|
+
// o `--discard-ignored` destruía. Normalizado com `=== true` para que
|
|
603
|
+
// `undefined` e `false` sejam a mesma coisa, como no `worktree remove`.
|
|
604
|
+
const discardIgnored = options.discardIgnored === true;
|
|
605
|
+
const ignored = classifyIgnoredLeftovers(worktreePath, root, { discardIgnored });
|
|
606
|
+
out.discardIgnored = discardIgnored;
|
|
607
|
+
out.ignored = {
|
|
608
|
+
// COPIADO para .git/morph-spec/finish-backup antes da remoção: existe só
|
|
609
|
+
// aqui, ou difere do que está na raiz. Com `--discard-ignored` esta lista
|
|
610
|
+
// sai VAZIA por construção — a flag significa "pule o backup".
|
|
611
|
+
shieldable: ignored.shieldable,
|
|
612
|
+
// Idêntico byte a byte ao da raiz (SHA-256): apagar não perde nada.
|
|
613
|
+
replicable: ignored.replicable,
|
|
614
|
+
// Artefato regenerável + os replicáveis (+ o exclusivo, sob a flag):
|
|
615
|
+
// descartado sem backup.
|
|
616
|
+
discardable: ignored.discarded,
|
|
617
|
+
};
|
|
618
|
+
if (ignored.blocking.length) {
|
|
619
|
+
out.ignored.blocking = ignored.blocking;
|
|
620
|
+
warnings.push('não foi possível enumerar os gitignored do worktree — o finish real vai recusar a remoção');
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (discardIgnored) {
|
|
624
|
+
// SEGUNDA leitura, deliberada e só aqui: sob a flag, `shieldable` sai
|
|
625
|
+
// vazia e a informação mais valiosa do pré-voo — QUAIS destes existem só
|
|
626
|
+
// aqui — desapareceria justamente no modo destrutivo. Perguntar à MESMA
|
|
627
|
+
// função, sem a flag, é a única forma de nomeá-los sem recriar a regra
|
|
628
|
+
// (uma segunda cópia da triagem é o defeito que o export de
|
|
629
|
+
// `classifyIgnoredLeftovers` existe para evitar).
|
|
630
|
+
const semFlag = classifyIgnoredLeftovers(worktreePath, root);
|
|
631
|
+
out.ignored.discardableExclusive = semFlag.shieldable;
|
|
632
|
+
if (semFlag.shieldable.length) {
|
|
633
|
+
warnings.push(
|
|
634
|
+
`--discard-ignored: ${semFlag.shieldable.length} caminho(s) gitignored existem SÓ neste worktree e serão `
|
|
635
|
+
+ `DESTRUÍDOS sem cópia (${semFlag.shieldable.slice(0, 5).join(', ')}). Sem a flag eles iriam para `
|
|
636
|
+
+ '.git/morph-spec/finish-backup e a remoção aconteceria do mesmo jeito.',
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const scan = findReparsePoints(worktreePath);
|
|
642
|
+
out.reparsePoints = scan.points;
|
|
643
|
+
if (scan.unprovable.length) out.reparseUnprovable = scan.unprovable;
|
|
644
|
+
out.reparseWarning = REPARSE_WARNING;
|
|
645
|
+
if (scan.points.length) {
|
|
646
|
+
warnings.push(
|
|
647
|
+
`${scan.points.length} reparse point(s) no worktree — ${REPARSE_WARNING}. `
|
|
648
|
+
+ 'O `morph-spec worktree remove` derruba os links ANTES de apagar; um `git worktree remove --force` cru, não.',
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return out;
|
|
654
|
+
}
|
|
655
|
+
|
|
450
656
|
// ── merge integration (local, no push) ───────────────────────────────────────
|
|
451
657
|
|
|
452
658
|
async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch }) {
|
|
@@ -482,23 +688,26 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
482
688
|
process.exit(1);
|
|
483
689
|
}
|
|
484
690
|
|
|
691
|
+
// Uma medição só para as DUAS saídas deste merge (--keep e normal): é o
|
|
692
|
+
// mesmo merge, então a contagem e a publicação têm de ser as mesmas.
|
|
693
|
+
const publish = publicationState({ root, defaultBranch, push: options.push });
|
|
694
|
+
|
|
485
695
|
if (options.keep) {
|
|
486
696
|
dropFromRootIndex(root, feature);
|
|
487
|
-
// Worktree preserved on purpose: the root's `worktree` registration stays
|
|
488
|
-
// VALID (it still names a live directory), so nothing to clean up here —
|
|
489
|
-
// only report where it lives and how to close it later.
|
|
490
697
|
emit({
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
hint: `worktree preservado em ${worktreePath} — feche depois com \`morph-spec worktree remove ${feature}\`.`,
|
|
494
|
-
...backupNote, ...ucNote,
|
|
698
|
+
...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, shedBackup, kept: true }),
|
|
699
|
+
...ucNote,
|
|
495
700
|
});
|
|
496
701
|
process.exit(0);
|
|
497
702
|
}
|
|
498
703
|
|
|
704
|
+
// The stack comes down BEFORE the worktree: the compose file, the bind
|
|
705
|
+
// mount and the port block all belong to that tree, and once it is gone
|
|
706
|
+
// there is nothing left to address the containers by.
|
|
707
|
+
const e2e = reclaimStack(feature, { root, worktreePath, options });
|
|
499
708
|
const teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true });
|
|
500
709
|
dropFromRootIndex(root, feature);
|
|
501
|
-
emit({ ...mergeOutput({ feature, branch: expectedBranch, worktreePath, teardown, shedBackup }), ...ucNote });
|
|
710
|
+
emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup }), ...ucNote, e2e });
|
|
502
711
|
process.exit(0);
|
|
503
712
|
}
|
|
504
713
|
|
|
@@ -526,8 +735,10 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
|
|
|
526
735
|
process.exit(1);
|
|
527
736
|
}
|
|
528
737
|
tryGit(['branch', '-d', expectedBranch], root);
|
|
738
|
+
const publish = publicationState({ root, defaultBranch, push: options.push });
|
|
739
|
+
const e2e = reclaimStack(feature, { root, worktreePath: null, options });
|
|
529
740
|
// The root index (gitignored) was already updated by the archive (same cwd).
|
|
530
|
-
emit({
|
|
741
|
+
emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish }), ...ucNote, e2e });
|
|
531
742
|
process.exit(0);
|
|
532
743
|
}
|
|
533
744
|
|
|
@@ -571,6 +782,11 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
571
782
|
// feature yet — honest until the PR merges).
|
|
572
783
|
let teardown = null;
|
|
573
784
|
let shedBackup = null;
|
|
785
|
+
// Reclaimed from the owning tree, before that tree goes away; skipped on
|
|
786
|
+
// --keep, where the worktree (and its stack) stays alive on purpose.
|
|
787
|
+
const e2e = isWorktreeFlow
|
|
788
|
+
? (options.keep ? null : reclaimStack(feature, { root, worktreePath, options }))
|
|
789
|
+
: reclaimStack(feature, { root, worktreePath: null, options });
|
|
574
790
|
if (isWorktreeFlow && !options.keep) {
|
|
575
791
|
// Shed the transient root-side scaffold BEFORE teardown — exactly as --merge
|
|
576
792
|
// does. The real `create --worktree` leaves the root's .morph/features/{f}
|
|
@@ -607,6 +823,7 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
|
|
|
607
823
|
nextStep: `mergeie ${prLabel} — a feature só estará encerrada quando ${defaultBranch} receber o archive pelo merge`,
|
|
608
824
|
...promotedNote(arch),
|
|
609
825
|
...(shedBackup ? { shedBackup } : {}),
|
|
826
|
+
...(e2e ? { e2e } : {}),
|
|
610
827
|
...(teardown
|
|
611
828
|
? { worktreeRemoved: teardown.worktreeRemoved, ...teardownExtras(teardown, worktreePath, expectedBranch, false) }
|
|
612
829
|
: {
|
|
@@ -642,25 +859,125 @@ function promotedNote(arch) {
|
|
|
642
859
|
}
|
|
643
860
|
|
|
644
861
|
/**
|
|
645
|
-
* O
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
|
|
862
|
+
* O único remoto com que o `finish` conversa — o mesmo que `hasOriginRemote` e
|
|
863
|
+
* o `resolveBaseRef` do branch-classifier assumem. Um nome só, aqui, para que o
|
|
864
|
+
* comando que a saída IMPRIME e o push que o `--push` EXECUTA jamais possam
|
|
865
|
+
* nomear remotos diferentes.
|
|
866
|
+
*/
|
|
867
|
+
const PUSH_REMOTE = 'origin';
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* O que sobrou por publicar depois que o merge local aterrissou — e, com
|
|
871
|
+
* `--push`, o resultado da publicação.
|
|
872
|
+
*
|
|
873
|
+
* A contagem REUSA `resolveBaseRef` + `commitsAhead` (branch-classifier), em vez
|
|
874
|
+
* de uma terceira cópia de `rev-list --count`. Detalhe que decide o P4:
|
|
875
|
+
* `resolveBaseRef` degrada para a branch LOCAL quando não existe ref de
|
|
876
|
+
* rastreamento remoto — comparar a branch consigo mesma devolveria um `0` de
|
|
877
|
+
* aparência honesta para um repo que nunca deu push. Por isso a comparação só
|
|
878
|
+
* vale quando o ref resolvido é diferente do nome da branch; caso contrário
|
|
879
|
+
* `ahead` fica `null` ("incomparável"), nunca zero.
|
|
880
|
+
*
|
|
881
|
+
* Fail-open por desenho: o merge JÁ aconteceu quando esta função roda, então
|
|
882
|
+
* nenhum resultado daqui — nem um push que falhou — pode transformar um sucesso
|
|
883
|
+
* em exit 1. A falha vira `pushError` no payload.
|
|
884
|
+
*
|
|
885
|
+
* @param {{ root: string, defaultBranch: string, push?: boolean }} ctx
|
|
886
|
+
* @returns {{ remote: string, branch: string, ahead: number|null, pushed: boolean,
|
|
887
|
+
* published?: number|null, pushError?: string }}
|
|
888
|
+
*/
|
|
889
|
+
function publicationState({ root, defaultBranch, push }) {
|
|
890
|
+
const baseRef = resolveBaseRef(defaultBranch, root);
|
|
891
|
+
const tracked = !!baseRef && baseRef !== defaultBranch;
|
|
892
|
+
const ahead = tracked ? commitsAhead(baseRef, defaultBranch, root) : null;
|
|
893
|
+
const state = { remote: PUSH_REMOTE, branch: defaultBranch, ahead, pushed: false };
|
|
894
|
+
|
|
895
|
+
if (!push) return state;
|
|
896
|
+
|
|
897
|
+
if (!hasOriginRemote(root)) {
|
|
898
|
+
return { ...state, pushError: 'no-origin-remote' };
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
execFileSync('git', ['push', PUSH_REMOTE, defaultBranch], { cwd: root, stdio: 'pipe' });
|
|
902
|
+
} catch (err) {
|
|
903
|
+
return { ...state, pushError: gitErr(err) };
|
|
904
|
+
}
|
|
905
|
+
// Re-medir DEPOIS do push: o que ficou para trás é a única prova de que a
|
|
906
|
+
// publicação foi completa, e um `ahead` congelado do antes seria mentira.
|
|
907
|
+
const after = resolveBaseRef(defaultBranch, root);
|
|
908
|
+
const remaining = after && after !== defaultBranch ? commitsAhead(after, defaultBranch, root) : null;
|
|
909
|
+
return { ...state, published: ahead, ahead: remaining, pushed: true };
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* A frase do próximo passo — PRODUTOR ÚNICO.
|
|
914
|
+
*
|
|
915
|
+
* O comando se chama `finish`, e no modo `--merge` ele NÃO publica nada por
|
|
916
|
+
* conta própria: o merge é local, o remoto continua sem a feature. Quem lê
|
|
917
|
+
* "finished: true" conclui o contrário — foi pegadinha repetida em sessões
|
|
918
|
+
* diferentes. Três saídas de `--merge` montavam esse aviso por conta própria, e
|
|
919
|
+
* uma delas (o `--keep`) simplesmente esquecia dele; um produtor único é o que
|
|
920
|
+
* impede a divergência de voltar.
|
|
921
|
+
*
|
|
922
|
+
* @param {ReturnType<typeof publicationState>} publish
|
|
923
|
+
* @returns {string}
|
|
650
924
|
*/
|
|
651
|
-
|
|
925
|
+
function mergeNextStep(publish) {
|
|
926
|
+
const { remote, branch, ahead, pushed, published, pushError } = publish;
|
|
927
|
+
const pushCmd = `\`git push ${remote} ${branch}\``;
|
|
928
|
+
|
|
929
|
+
if (pushed) {
|
|
930
|
+
const n = typeof published === 'number' ? `${published} commit(s)` : 'a integração';
|
|
931
|
+
return `PUBLICADO — ${n} de ${branch} enviado(s) para ${remote}/${branch} pelo --push.`;
|
|
932
|
+
}
|
|
933
|
+
if (pushError === 'no-origin-remote') {
|
|
934
|
+
return `integração LOCAL — nada foi publicado: --push foi pedido mas não há remoto \`${remote}\` configurado.`;
|
|
935
|
+
}
|
|
936
|
+
if (pushError) {
|
|
937
|
+
return `integração LOCAL — o push pedido por --push FALHOU e nada foi publicado. Rode ${pushCmd} manualmente.`;
|
|
938
|
+
}
|
|
939
|
+
// Sem contraparte remota não se inventa contagem (P4): fica a frase estática.
|
|
940
|
+
if (ahead === null) {
|
|
941
|
+
return 'integração LOCAL — nada foi publicado. Rode `git push` para levar ao remoto.';
|
|
942
|
+
}
|
|
943
|
+
return `integração LOCAL — nada foi publicado: ${branch} está ${ahead} commit(s) à frente de ${remote}/${branch}. Rode ${pushCmd} para levar ao remoto (ou re-execute o finish com --push).`;
|
|
944
|
+
}
|
|
652
945
|
|
|
653
|
-
|
|
946
|
+
/**
|
|
947
|
+
* CONSTRUTOR ÚNICO da saída de sucesso do `--merge`. As três emissões (worktree
|
|
948
|
+
* normal, worktree `--keep` e fluxo comum) passam por aqui — era a terceira
|
|
949
|
+
* cópia, montada à mão no `--keep`, que saía sem `nextStep` nenhum.
|
|
950
|
+
*
|
|
951
|
+
* `teardown === null` cobre os dois casos sem desmonte: o `--keep` (worktree
|
|
952
|
+
* vivo de propósito) e o fluxo comum (não há worktree algum).
|
|
953
|
+
*
|
|
954
|
+
* @param {{ feature: string, branch: string, defaultBranch: string,
|
|
955
|
+
* publish: ReturnType<typeof publicationState>, worktreePath?: string|null,
|
|
956
|
+
* teardown?: object|null, shedBackup?: string|null, kept?: boolean }} args
|
|
957
|
+
*/
|
|
958
|
+
function mergeOutput({ feature, branch, defaultBranch, publish, worktreePath = null, teardown = null, shedBackup = null, kept = false }) {
|
|
654
959
|
return {
|
|
655
960
|
finished: true,
|
|
656
961
|
integrated: true,
|
|
657
962
|
mode: 'merge',
|
|
658
963
|
feature,
|
|
659
964
|
merged: branch,
|
|
660
|
-
|
|
661
|
-
|
|
965
|
+
into: defaultBranch,
|
|
966
|
+
...(kept ? { kept: true } : {}),
|
|
967
|
+
worktreeRemoved: teardown ? teardown.worktreeRemoved : false,
|
|
968
|
+
nextStep: mergeNextStep(publish),
|
|
969
|
+
publish,
|
|
662
970
|
...(shedBackup ? { shedBackup } : {}),
|
|
663
|
-
...
|
|
971
|
+
...(kept
|
|
972
|
+
? {
|
|
973
|
+
// Worktree preserved on purpose: the root's `worktree` registration stays
|
|
974
|
+
// VALID (it still names a live directory), so nothing to clean up here —
|
|
975
|
+
// only report where it lives and how to close it later.
|
|
976
|
+
worktree: { path: worktreePath, branch },
|
|
977
|
+
hint: `worktree preservado em ${worktreePath} — feche depois com \`morph-spec worktree remove ${feature}\`.`,
|
|
978
|
+
}
|
|
979
|
+
: {}),
|
|
980
|
+
...(teardown ? teardownExtras(teardown, worktreePath, branch, true) : {}),
|
|
664
981
|
};
|
|
665
982
|
}
|
|
666
983
|
|
|
@@ -856,28 +1173,18 @@ function openPrFor(branch, cwd) {
|
|
|
856
1173
|
}
|
|
857
1174
|
|
|
858
1175
|
/**
|
|
859
|
-
* Reclaim the feature's verification stack,
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
* or a stubborn container are all non-events here.
|
|
1176
|
+
* Reclaim the feature's verification stack (containers, ports, named volumes,
|
|
1177
|
+
* freshness stamp) from the tree that owns it. `--keep-volumes` keeps the
|
|
1178
|
+
* named volumes (e.g. a seeded database) for the next feature. Best-effort by
|
|
1179
|
+
* contract — a stack that refuses to come down must never fail a finish that
|
|
1180
|
+
* already merged, so this never throws.
|
|
865
1181
|
*/
|
|
866
|
-
function
|
|
1182
|
+
function reclaimStack(feature, { root, worktreePath, options }) {
|
|
867
1183
|
try {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (!composeFile) return;
|
|
873
|
-
|
|
874
|
-
const projectName = composeProjectName(feature);
|
|
875
|
-
tearDownStack(root, {
|
|
876
|
-
composeFile,
|
|
877
|
-
projectName,
|
|
878
|
-
env: { COMPOSE_PROJECT_NAME: projectName },
|
|
879
|
-
});
|
|
880
|
-
} catch { /* fail-open — teardown must never fail a completed finish */ }
|
|
1184
|
+
return tearDownFeatureStack(feature, { root, worktreePath, volumes: !options.keepVolumes });
|
|
1185
|
+
} catch {
|
|
1186
|
+
return { attempted: false, cwd: worktreePath || root, status: 'fail', stampDeleted: false };
|
|
1187
|
+
}
|
|
881
1188
|
}
|
|
882
1189
|
|
|
883
1190
|
/** Drop a feature from the thin, gitignored root index (never via saveState). */
|