@polymorphism-tech/morph-spec 8.37.1 → 8.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +37 -13
- package/README.md +2 -2
- package/bin/morph-spec.js +75 -49
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +6 -5
- package/framework/MORPH.md +25 -22
- package/framework/agents.json +96 -36
- package/framework/docs/CLI.md +54 -17
- package/framework/evals/rubrics/code-quality.md +5 -0
- package/framework/evals/rubrics/condensed.md +6 -2
- package/framework/evals/rubrics/contracts.md +20 -0
- package/framework/evals/rubrics/test-coverage.md +17 -6
- package/framework/hooks/README.md +9 -3
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
- package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
- package/framework/hooks/claude-code/core/validate-completion.js +16 -90
- package/framework/hooks/claude-code/core/worktree-context.js +10 -0
- package/framework/hooks/shared/command-tokens.js +96 -0
- package/framework/hooks/shared/git-exclude.js +44 -0
- package/framework/hooks/shared/git-paths.js +52 -0
- package/framework/hooks/shared/graph-freshness.js +380 -0
- package/framework/hooks/shared/graph-refresh.js +416 -0
- package/framework/hooks/shared/morph-cli.js +17 -5
- package/framework/hooks/shared/redact-secrets.js +55 -0
- package/framework/hooks/shared/telemetry-logger.js +25 -2
- package/framework/hooks/shared/transcript-usage.js +80 -0
- package/framework/mcp/registry.json +33 -2
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/rules/csharp-standards.md +1 -0
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +10 -4
- package/framework/rules/nextjs-standards.md +1 -0
- package/framework/schemas/config.schema.json +41 -1
- package/framework/schemas/feature.schema.json +5 -1
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +40 -11
- package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +44 -10
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +73 -15
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +51 -13
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +39 -13
- package/framework/standards/STANDARDS.json +189 -14
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/ai-agents/testing-ai.md +2 -1
- package/framework/standards/architecture/code-comments.md +100 -0
- package/framework/standards/backend/dotnet/testing.md +22 -0
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/admin-app/data-table.md +562 -0
- package/framework/standards/frontend/admin-app/feedback.md +236 -0
- package/framework/standards/frontend/admin-app/forms.md +232 -0
- package/framework/standards/frontend/admin-app/page-structure.md +203 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
- package/framework/standards/frontend/nextjs/components.md +37 -58
- package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
- package/framework/standards/frontend/nextjs/forms.md +31 -30
- package/framework/standards/frontend/nextjs/state-management.md +2 -1
- package/framework/standards/frontend/nextjs/testing.md +18 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -117
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/context/design-system.md +3 -3
- package/framework/templates/docs/proposal.md +11 -0
- package/framework/templates/docs/spec.md +15 -0
- package/framework/templates/docs/ui-mockups.md +115 -98
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/feature/decisions.md +124 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/graph/affected.js +4 -3
- package/src/commands/graph/build.js +109 -18
- package/src/commands/graph/explain.js +8 -5
- package/src/commands/graph/status.js +14 -8
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/observability/cost.js +148 -18
- package/src/commands/project/doctor.js +85 -15
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +30 -0
- package/src/commands/project/worktree.js +73 -90
- package/src/commands/state/advance.js +18 -0
- package/src/commands/state/approve.js +15 -0
- package/src/commands/state/archive.js +62 -9
- package/src/commands/state/finish.js +62 -14
- package/src/commands/state/gate-check.js +8 -1
- package/src/commands/state/gate-decision.js +8 -4
- package/src/commands/state/report-check.js +90 -9
- package/src/commands/state/usecase.js +66 -0
- package/src/commands/tasks/dag.js +17 -2
- package/src/commands/validation/validate-feature.js +6 -1
- package/src/commands/validation/verify.js +20 -1
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +63 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/hooks-scanner.js +8 -1
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +4 -1
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/ai/comment-stripper.js +101 -4
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +47 -0
- package/src/lib/generators/handoff-generator.js +4 -18
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/git/default-branch.js +52 -0
- package/src/lib/git/tree-fingerprint.js +58 -0
- package/src/lib/git/tree-state.js +167 -0
- package/src/lib/graph/god-nodes.js +12 -3
- package/src/lib/graph/graph-health.js +48 -2
- package/src/lib/graph/graph-loader.js +99 -25
- package/src/lib/graph/graphifyignore.js +145 -28
- package/src/lib/graph/sanitize.js +117 -11
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +90 -0
- package/src/lib/specs/acervo-format.js +70 -0
- package/src/lib/specs/acervo-lifecycle.js +264 -0
- package/src/lib/specs/adr-index.js +169 -0
- package/src/lib/specs/revocations.js +156 -0
- package/src/lib/specs/screen-checklist.js +357 -0
- package/src/lib/specs/usecase-check.js +20 -16
- package/src/lib/specs/usecase-promoter.js +103 -38
- package/src/lib/specs/usecase-registry.js +7 -7
- package/src/lib/tasks/tasks-json-parser.js +2 -3
- package/src/lib/telemetry/metrics.js +386 -0
- package/src/lib/telemetry/telemetry-reader.js +161 -51
- package/src/lib/telemetry/verify-event.js +79 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +200 -0
- package/src/lib/validators/code-budget/comment-scanner.js +75 -0
- package/src/lib/validators/code-budget/index.js +9 -0
- package/src/lib/validators/reuse/reuse-validator.js +4 -2
- package/src/lib/validators/reuse/symbol-scanner.js +27 -22
- package/src/lib/validators/validation-runner.js +21 -5
- package/src/lib/validators/verify-runner.js +11 -0
- package/src/lib/worktree/finish-backup.js +739 -0
- package/src/lib/worktree/ignored-triage.js +15 -8
- package/src/lib/worktree/worktree-audit.js +5 -0
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +31 -22
- package/src/utils/agents-installer.js +338 -24
- package/src/utils/banner.js +1 -1
- package/src/utils/hooks-installer.js +75 -15
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- package/src/lib/validators/shared/result-formatter.js +0 -124
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The working tree as the orchestrator needs it when it takes control back
|
|
3
|
+
* from a dispatch, in one call instead of `git status` + `git diff --stat` +
|
|
4
|
+
* `git log` + a marker grep. Everything is read outside `.morph/`, the same
|
|
5
|
+
* cut the tree fingerprint makes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { OUTSIDE_MORPH, treeFingerprint, git as gitBuffer } from './tree-fingerprint.js';
|
|
11
|
+
import { detectDefaultBranch } from './default-branch.js';
|
|
12
|
+
import { resolveBaseRef } from './branch-classifier.js';
|
|
13
|
+
|
|
14
|
+
/** What an evaluator writes next to a mutation it has not reverted yet. */
|
|
15
|
+
export const MUTATION_MARKER = 'MUTACAO';
|
|
16
|
+
const MAX_LISTED = 50;
|
|
17
|
+
const MAX_COMMITS = 10;
|
|
18
|
+
|
|
19
|
+
function git(cwd, args, input) {
|
|
20
|
+
return gitBuffer(cwd, args, input).toString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function headSha(cwd) {
|
|
24
|
+
try { return git(cwd, ['rev-parse', 'HEAD']).trim(); } catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Every file that differs from HEAD outside `.morph/`, untracked ones one by one. */
|
|
28
|
+
function changedFiles(cwd) {
|
|
29
|
+
return parsePorcelain(git(cwd, ['status', '--porcelain=v1', '-z', '--untracked-files=all', ...OUTSIDE_MORPH]))
|
|
30
|
+
.map((e) => e.path);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Blob id of each file as it is on disk; null for a file that is gone. */
|
|
34
|
+
function diskBlobs(cwd, paths) {
|
|
35
|
+
const present = paths.filter((p) => existsSync(join(cwd, p)));
|
|
36
|
+
const ids = present.length ? git(cwd, ['hash-object', '--stdin-paths'], present.join('\n')).split('\n') : [];
|
|
37
|
+
const out = Object.fromEntries(paths.map((p) => [p, null]));
|
|
38
|
+
present.forEach((p, i) => { out[p] = ids[i].trim(); });
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Blob id of each path in a commit; null where the commit does not have it. */
|
|
43
|
+
function commitBlobs(cwd, commit, paths) {
|
|
44
|
+
const out = Object.fromEntries(paths.map((p) => [p, null]));
|
|
45
|
+
if (!commit || !paths.length) return out;
|
|
46
|
+
const lines = git(cwd, ['cat-file', '--batch-check=%(objectname)'], paths.map((p) => `${commit}:${p}`).join('\n')).split('\n');
|
|
47
|
+
paths.forEach((p, i) => { out[p] = /^[0-9a-f]{40,64}$/.test(lines[i]?.trim()) ? lines[i].trim() : null; });
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What the tree is before a dispatch whose work must leave it unchanged (the
|
|
53
|
+
* evaluator mutates and reverts): HEAD plus the blob of every file that
|
|
54
|
+
* differs from it. Kept on disk, so a session that resumes after the agent
|
|
55
|
+
* died compares against the tree before the dispatch, not its own start.
|
|
56
|
+
* @returns {{ at: string, head: string|null, files: Object<string, string|null> }|null}
|
|
57
|
+
*/
|
|
58
|
+
export function snapshotTree(cwd) {
|
|
59
|
+
try {
|
|
60
|
+
return { at: new Date().toISOString(), head: headSha(cwd), files: diskBlobs(cwd, changedFiles(cwd)) };
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Files whose content on disk is not what it was in the snapshot. Content,
|
|
68
|
+
* not status: a mutation committed since shows up, and a file committed
|
|
69
|
+
* unchanged does not.
|
|
70
|
+
* @returns {{ at: string, headChanged: boolean, changed: string[], changedCount: number }}
|
|
71
|
+
*/
|
|
72
|
+
export function compareToSnapshot(cwd, snapshot) {
|
|
73
|
+
const head = headSha(cwd);
|
|
74
|
+
const candidates = new Set([...Object.keys(snapshot.files || {}), ...changedFiles(cwd)]);
|
|
75
|
+
if (snapshot.head && head && snapshot.head !== head) {
|
|
76
|
+
for (const p of git(cwd, ['diff', '--name-only', '-z', snapshot.head, head, ...OUTSIDE_MORPH]).split('\0')) {
|
|
77
|
+
if (p) candidates.add(p);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const paths = [...candidates];
|
|
81
|
+
const now = diskBlobs(cwd, paths);
|
|
82
|
+
const unlisted = paths.filter((p) => !(p in (snapshot.files || {})));
|
|
83
|
+
const before = { ...commitBlobs(cwd, snapshot.head, unlisted), ...snapshot.files };
|
|
84
|
+
const changed = paths.filter((p) => before[p] !== now[p]).sort();
|
|
85
|
+
return { at: snapshot.at, headChanged: snapshot.head !== head, changed: changed.slice(0, MAX_LISTED), changedCount: changed.length };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `git status --porcelain -z`: a rename carries its old path as an extra entry. */
|
|
89
|
+
function parsePorcelain(out) {
|
|
90
|
+
const entries = [];
|
|
91
|
+
const parts = out.split('\0');
|
|
92
|
+
for (let i = 0; i < parts.length; i++) {
|
|
93
|
+
const item = parts[i];
|
|
94
|
+
if (!item) continue;
|
|
95
|
+
const status = item.slice(0, 2);
|
|
96
|
+
entries.push({ status: status.trim(), path: item.slice(3) });
|
|
97
|
+
if (status[0] === 'R' || status[0] === 'C') i++;
|
|
98
|
+
}
|
|
99
|
+
return entries;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function diffstat(cwd) {
|
|
103
|
+
const stat = { files: 0, insertions: 0, deletions: 0 };
|
|
104
|
+
for (const line of git(cwd, ['diff', 'HEAD', '--numstat', ...OUTSIDE_MORPH]).split('\n')) {
|
|
105
|
+
const [ins, del] = line.split('\t');
|
|
106
|
+
if (del === undefined) continue;
|
|
107
|
+
stat.files += 1;
|
|
108
|
+
stat.insertions += Number(ins) || 0;
|
|
109
|
+
stat.deletions += Number(del) || 0;
|
|
110
|
+
}
|
|
111
|
+
return stat;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function commitsOnTop(cwd) {
|
|
115
|
+
const base = resolveBaseRef(detectDefaultBranch(cwd), cwd);
|
|
116
|
+
if (!base) return null;
|
|
117
|
+
return git(cwd, ['log', `--max-count=${MAX_COMMITS}`, '--format=%h %s', `${base}..HEAD`])
|
|
118
|
+
.split('\n').filter(Boolean);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Untracked files count too; gitignored ones (build output) do not. */
|
|
122
|
+
function markers(cwd) {
|
|
123
|
+
try {
|
|
124
|
+
const out = git(cwd, ['grep', '-n', '-I', '--untracked', '-F', '-e', MUTATION_MARKER, ...OUTSIDE_MORPH]);
|
|
125
|
+
return out.split('\n').filter(Boolean).slice(0, MAX_LISTED).map((line) => {
|
|
126
|
+
const [file, lineNo] = line.split(':');
|
|
127
|
+
return { file, line: Number(lineNo) };
|
|
128
|
+
});
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (err.status === 1) return [];
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} cwd
|
|
137
|
+
* @param {{ snapshot?: object|null }} [opts] - a `snapshotTree()` to compare with
|
|
138
|
+
* @returns {{ head: string|null, branch: string|null, fingerprint: string|null,
|
|
139
|
+
* dirty: {status: string, path: string}[], dirtyCount: number,
|
|
140
|
+
* diffstat: {files: number, insertions: number, deletions: number}|null,
|
|
141
|
+
* commits: string[]|null, markers: {file: string, line: number}[],
|
|
142
|
+
* sinceBeforeEval: ReturnType<typeof compareToSnapshot>|null }|null}
|
|
143
|
+
* null when git cannot answer (outside a repo, or an error): unknown, never
|
|
144
|
+
* a clean tree. `commits` is null when there is no default branch to count
|
|
145
|
+
* from; `diffstat` is null before the first commit; `sinceBeforeEval` is
|
|
146
|
+
* null without a snapshot.
|
|
147
|
+
*/
|
|
148
|
+
export function treeState(cwd, { snapshot = null } = {}) {
|
|
149
|
+
try {
|
|
150
|
+
const dirty = parsePorcelain(git(cwd, ['status', '--porcelain=v1', '-z', ...OUTSIDE_MORPH]));
|
|
151
|
+
let head = null;
|
|
152
|
+
try { head = git(cwd, ['rev-parse', '--short', 'HEAD']).trim(); } catch { /* no commit yet */ }
|
|
153
|
+
return {
|
|
154
|
+
head,
|
|
155
|
+
branch: head ? git(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']).trim() : null,
|
|
156
|
+
fingerprint: treeFingerprint(cwd),
|
|
157
|
+
dirty: dirty.slice(0, MAX_LISTED),
|
|
158
|
+
dirtyCount: dirty.length,
|
|
159
|
+
diffstat: head ? diffstat(cwd) : null,
|
|
160
|
+
commits: head ? commitsOnTop(cwd) : null,
|
|
161
|
+
markers: markers(cwd),
|
|
162
|
+
sinceBeforeEval: snapshot ? compareToSnapshot(cwd, snapshot) : null,
|
|
163
|
+
};
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
* substituí-lo, porque exaustividade é um valor que o grafo não tem.
|
|
8
8
|
*
|
|
9
9
|
* Só entram nós COM source_file: um god node sem arquivo não orienta ninguém.
|
|
10
|
+
* E nada de teste: dublês e fixtures estão no grafo para serem achados, não
|
|
11
|
+
* para disputar centralidade — sem o filtro o `FakeGhlToolbelt` entra no top
|
|
12
|
+
* 10 do MORPH_GHLBrain e as arestas vindas de teste inflam o grau de `Result`
|
|
13
|
+
* de 380 para 443. Aresta com uma ponta em teste não conta para ninguém, e
|
|
14
|
+
* comunidade só de teste some da lista.
|
|
10
15
|
*
|
|
11
16
|
* @module god-nodes
|
|
12
17
|
*/
|
|
13
18
|
import { loadGraph } from './graph-loader.js';
|
|
14
|
-
import { buildIndex, resolveEdgeTarget } from './sanitize.js';
|
|
19
|
+
import { buildIndex, resolveEdgeTarget, linksOf, isTestPath } from './sanitize.js';
|
|
15
20
|
|
|
16
21
|
/** Sem `graphify label`, as comunidades se chamam Community_N — ruído puro. */
|
|
17
22
|
const PLACEHOLDER_LABEL = /^Community[_ ]?\d+$/i;
|
|
@@ -37,9 +42,12 @@ export async function scanGraph(projectPath, opts = {}) {
|
|
|
37
42
|
const bump = (id) => {
|
|
38
43
|
if (id) degree.set(id, (degree.get(id) ?? 0) + 1);
|
|
39
44
|
};
|
|
40
|
-
|
|
45
|
+
const isTest = (id) => isTestPath(index.byId.get(id)?.source_file);
|
|
46
|
+
for (const link of linksOf(index, graph)) {
|
|
47
|
+
const target = resolveEdgeTarget(index, link).id;
|
|
48
|
+
if (isTest(link.source) || (target && isTest(target))) continue;
|
|
41
49
|
bump(link.source);
|
|
42
|
-
bump(
|
|
50
|
+
bump(target);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
const godNodes = [...degree.entries()]
|
|
@@ -57,6 +65,7 @@ export async function scanGraph(projectPath, opts = {}) {
|
|
|
57
65
|
const labels = new Map();
|
|
58
66
|
for (const node of graph.nodes ?? []) {
|
|
59
67
|
if (node.community === undefined || node.community === null) continue;
|
|
68
|
+
if (isTestPath(node.source_file)) continue;
|
|
60
69
|
sizes.set(node.community, (sizes.get(node.community) ?? 0) + 1);
|
|
61
70
|
if (node.community_label && !labels.has(node.community)) {
|
|
62
71
|
labels.set(node.community, node.community_label);
|
|
@@ -6,9 +6,46 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module graph-health
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import {
|
|
13
|
+
loadGraph, getFreshness, describeStale, staleCause,
|
|
14
|
+
} from './graph-loader.js';
|
|
10
15
|
import { GRAPHIFY_VERSION, detectGraphifyVersion } from './graphify-runtime.js';
|
|
11
16
|
|
|
17
|
+
const GRAPHIFY_HOOK_MARKER = /# graphify-(?:checkout-)?hook-start/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* O hook git do próprio graphify (`graphify hook install`, que o setup-infra
|
|
21
|
+
* instalava até a 8.37) está no repositório? Ele é danoso aqui: num commit
|
|
22
|
+
* depois de um fast-forward carimba o HEAD num grafo SEM os arquivos que o
|
|
23
|
+
* merge trouxe — "fresh" falso — e disputa o graph.json com o refresh do
|
|
24
|
+
* morph-spec. Lê o diretório de hooks que o git usa (`core.hooksPath`
|
|
25
|
+
* incluído).
|
|
26
|
+
*
|
|
27
|
+
* @param {string} projectPath
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
export function graphifyGitHookInstalled(projectPath) {
|
|
31
|
+
let dir;
|
|
32
|
+
try {
|
|
33
|
+
const rel = execFileSync('git', ['rev-parse', '--git-path', 'hooks'], {
|
|
34
|
+
cwd: projectPath, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true,
|
|
35
|
+
}).trim();
|
|
36
|
+
dir = resolve(projectPath, rel);
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return ['post-commit', 'post-checkout'].some((hook) => {
|
|
41
|
+
try {
|
|
42
|
+
return GRAPHIFY_HOOK_MARKER.test(readFileSync(join(dir, hook), 'utf-8'));
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
12
49
|
/**
|
|
13
50
|
* @param {string} projectPath
|
|
14
51
|
* @param {{_version?: () => string|null}} [deps]
|
|
@@ -41,7 +78,16 @@ export function auditGraph(projectPath, deps = {}) {
|
|
|
41
78
|
if (freshness.state === 'stale') {
|
|
42
79
|
status = 'warn';
|
|
43
80
|
details.push(
|
|
44
|
-
`stale:
|
|
81
|
+
`stale: ${staleCause(freshness)} — ${describeStale(projectPath, freshness)}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (graphifyGitHookInstalled(projectPath)) {
|
|
86
|
+
status = 'warn';
|
|
87
|
+
details.push(
|
|
88
|
+
'hook git do graphify instalado: depois de um fast-forward ele carimba o HEAD num grafo sem os arquivos do merge ' +
|
|
89
|
+
'e disputa o graph.json com o refresh do morph-spec — remova com `graphify hook uninstall` ' +
|
|
90
|
+
'(tira também o merge driver do .gitattributes)',
|
|
45
91
|
);
|
|
46
92
|
}
|
|
47
93
|
|
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { readFileSync, existsSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
graphFreshness, readRefreshRecord,
|
|
17
|
+
} from '../../../framework/hooks/shared/graph-freshness.js';
|
|
18
|
+
import {
|
|
19
|
+
isPrimaryTree, readRefreshLock, resolveGraphify,
|
|
20
|
+
} from '../../../framework/hooks/shared/graph-refresh.js';
|
|
21
|
+
import { isSecondaryWorktree, workTreeTop } from '../../../framework/hooks/shared/git-paths.js';
|
|
22
|
+
import { GRAPHIFY_PACKAGE, GRAPHIFY_VERSION } from './graphify-runtime.js';
|
|
16
23
|
|
|
17
24
|
/**
|
|
18
25
|
* @param {string} projectPath
|
|
@@ -50,37 +57,104 @@ export function loadGraph(projectPath) {
|
|
|
50
57
|
}
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
function readHead(projectPath) {
|
|
54
|
-
try {
|
|
55
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
56
|
-
cwd: projectPath,
|
|
57
|
-
encoding: 'utf-8',
|
|
58
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
59
|
-
}).trim() || null;
|
|
60
|
-
} catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
60
|
/**
|
|
66
|
-
* O
|
|
67
|
-
*
|
|
68
|
-
*
|
|
61
|
+
* O frescor do grafo, pela regra única (framework/hooks/shared/graph-freshness.js):
|
|
62
|
+
* o carimbo e o registro da última rodada contra o HEAD, e as regras do
|
|
63
|
+
* `.graphifyignore` por conteúdo. Um grafo velho diz "não existe" sobre o que
|
|
64
|
+
* o último commit trouxe — quem consome precisa saber, ou conclui "ninguém usa
|
|
65
|
+
* isso" a partir dele.
|
|
69
66
|
*
|
|
70
67
|
* @param {string} projectPath
|
|
71
|
-
* @param {object} graph
|
|
68
|
+
* @param {object} graph - o graph.json já parseado (o carimbo sai dele)
|
|
72
69
|
* @param {{_head?: (projectPath: string) => string|null}} [deps]
|
|
73
|
-
* @returns {{builtAtCommit: string|null, headCommit: string|null, state: 'fresh'|'stale'|'unknown'}}
|
|
70
|
+
* @returns {{builtAtCommit: string|null, headCommit: string|null, state: 'fresh'|'stale'|'unknown', reason?: 'ignore-changed'|'rules-unknown'}}
|
|
74
71
|
*/
|
|
75
72
|
export function getFreshness(projectPath, graph, deps = {}) {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const f = graphFreshness(projectPath, {
|
|
74
|
+
graph,
|
|
75
|
+
...(deps._head ? { head: deps._head(projectPath) } : {}),
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
builtAtCommit: f.built,
|
|
79
|
+
headCommit: f.head,
|
|
80
|
+
state: f.state === 'missing' ? 'unknown' : f.state,
|
|
81
|
+
...(f.reason ? { reason: f.reason } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const short = (sha) => (sha ? String(sha).slice(0, 8) : '?');
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* O porquê de um grafo stale, em uma linha — carimbo antigo ou regras.
|
|
89
|
+
*
|
|
90
|
+
* @param {{builtAtCommit: string|null, headCommit: string|null, reason?: string}} freshness
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function staleCause(freshness) {
|
|
94
|
+
if (freshness.reason === 'ignore-changed') {
|
|
95
|
+
return 'as regras do .graphifyignore mudaram depois da extração';
|
|
96
|
+
}
|
|
97
|
+
if (freshness.reason === 'rules-unknown') {
|
|
98
|
+
return 'sem registro das regras do .graphifyignore com que o grafo foi extraído';
|
|
80
99
|
}
|
|
100
|
+
return `construído em ${short(freshness.builtAtCommit)}, HEAD é ${short(freshness.headCommit)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* O que o refresh automático faz por esta árvore. Ele cobre só a árvore
|
|
105
|
+
* principal (`auto`); numa worktree, `running` é sempre false — um lock vivo
|
|
106
|
+
* ali seria o refresh da raiz. `last` é a última rodada registrada para esta
|
|
107
|
+
* árvore: em background, manual ou a semeadura.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} projectPath
|
|
110
|
+
* @returns {{auto: boolean, running: boolean, since: string|null, pid: number|null, last: {ok: boolean, exit: number|null, head: string|null, ts: string, unavailable?: boolean, seeded?: boolean}|null}}
|
|
111
|
+
*/
|
|
112
|
+
export function refreshState(projectPath) {
|
|
113
|
+
const auto = isPrimaryTree(projectPath);
|
|
114
|
+
const lock = auto ? readRefreshLock(projectPath) : null;
|
|
115
|
+
const record = readRefreshRecord(projectPath);
|
|
81
116
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
117
|
+
auto,
|
|
118
|
+
running: Boolean(lock?.live),
|
|
119
|
+
since: lock?.live ? lock.since : null,
|
|
120
|
+
pid: lock?.live ? lock.pid : null,
|
|
121
|
+
last: record ? {
|
|
122
|
+
ok: record.exit === 0,
|
|
123
|
+
exit: record.exit ?? null,
|
|
124
|
+
head: record.head ?? null,
|
|
125
|
+
ts: record.at,
|
|
126
|
+
...(record.unavailable ? { unavailable: true } : {}),
|
|
127
|
+
...(record.seeded ? { seeded: true } : {}),
|
|
128
|
+
} : null,
|
|
85
129
|
};
|
|
86
130
|
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A frase de grafo stale — a mesma no status, no explain, no affected e no
|
|
134
|
+
* doctor, e verdadeira sobre o mecanismo: o refresh em background só cobre a
|
|
135
|
+
* raiz, disparado pelos hooks do morph-spec; a worktree só anda à mão.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} projectPath
|
|
138
|
+
* @param {{builtAtCommit?: string|null, headCommit?: string|null}} [freshness]
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
export function describeStale(projectPath, freshness = {}) {
|
|
142
|
+
const tree = workTreeTop(projectPath) ?? projectPath;
|
|
143
|
+
const state = refreshState(projectPath);
|
|
144
|
+
if (!state.auto && isSecondaryWorktree(tree)) {
|
|
145
|
+
return state.last?.seeded
|
|
146
|
+
? `grafo semeado da raiz em ${short(state.last.head ?? freshness.builtAtCommit)}; \`morph-spec graph refresh\` atualiza esta worktree`
|
|
147
|
+
: 'o refresh automático só cobre a raiz; `morph-spec graph refresh` atualiza esta worktree';
|
|
148
|
+
}
|
|
149
|
+
if (state.running) {
|
|
150
|
+
return `refresh em background em andamento desde ${new Date(state.since).toLocaleTimeString()} — confirme no código antes de agir`;
|
|
151
|
+
}
|
|
152
|
+
// Registrado como ausente mas já de volta ao PATH: o próximo disparo roda.
|
|
153
|
+
if (state.last?.unavailable && !resolveGraphify()) {
|
|
154
|
+
return `graphify fora do PATH: o refresh automático está parado até ele voltar (uv tool install ${GRAPHIFY_PACKAGE}==${GRAPHIFY_VERSION}); depois, morph-spec graph refresh`;
|
|
155
|
+
}
|
|
156
|
+
if (state.last && !state.last.ok && state.last.head === (freshness.headCommit ?? null)) {
|
|
157
|
+
return `o último refresh falhou neste HEAD (exit ${state.last.exit ?? '?'}) e só se repete quando o HEAD andar — rode morph-spec graph refresh e leia a saída`;
|
|
158
|
+
}
|
|
159
|
+
return 'o refresh roda em background depois dos comandos git/gh/morph-spec do agente e no início da sessão; para já: morph-spec graph refresh';
|
|
160
|
+
}
|
|
@@ -2,30 +2,53 @@
|
|
|
2
2
|
* Geração stack-aware do .graphifyignore (ADR-004).
|
|
3
3
|
*
|
|
4
4
|
* Filtrar teste não é cosmético: sem isso o god node #1 do MORPH_GHLBrain é
|
|
5
|
-
* `FakeGhlToolbelt`, um dublê.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* `FakeGhlToolbelt`, um dublê. E o padrão genérico `tests/` NÃO serve em .NET:
|
|
6
|
+
* no ProspectPRO os testes vivem em `src/backend/ProspectPRO.Tests/`. Com
|
|
7
|
+
* `**\/*.Tests/` o grafo caiu de 10.450 para 7.384 nós — 30% era ruído de teste.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Mas dublê, fixture e helper de teste precisam ser ACHÁVEIS: são o que um
|
|
10
|
+
* teste novo deveria reusar, e fora do grafo `graph explain FakeX` dá
|
|
11
|
+
* not-found. Por isso o conteúdo dos projetos de teste sai e esses arquivos
|
|
12
|
+
* voltam. A forma é ditada pela regra do gitignore que o graphify 0.9.50 segue
|
|
13
|
+
* (detect.py `_is_ignored`): um `!` não reinclui nada sob um diretório já
|
|
14
|
+
* excluído. Medido num fixture: `**\/*.Tests/` + `!**\/Fakes/` não traz nada de
|
|
15
|
+
* volta; excluir o CONTEÚDO (`**\/*.Tests/**`), reincluir os diretórios
|
|
16
|
+
* (`!**\/*.Tests/**\/`) e então os helpers traz. `bin/`/`obj/` vêm DEPOIS das
|
|
17
|
+
* reinclusões — antes, a reinclusão de diretório os devolveria (`obj/` tem
|
|
18
|
+
* `.cs` gerado). No MORPH_GHLBrain: +9,6% de nós (11.591 → 12.706), 94
|
|
19
|
+
* arquivos de teste, nenhum de bin/obj.
|
|
20
|
+
*
|
|
21
|
+
* Os dublês quebram o guard de "implementador único" do `dispatches_to` do
|
|
22
|
+
* graphify (de 122 para 5 arestas no GHLBrain); o saneamento as recupera
|
|
23
|
+
* (sanitize.js `recoverDispatch`) e tira o teste dos god nodes.
|
|
12
24
|
*
|
|
13
25
|
* Em Next.js o problema é outro: metade dos nós vinha de JSON de configuração,
|
|
14
26
|
* e os god nodes viravam `dependencies` e `compilerOptions`.
|
|
15
27
|
*
|
|
16
28
|
* @module graphifyignore
|
|
17
29
|
*/
|
|
18
|
-
import { writeFileSync, existsSync } from 'node:fs';
|
|
30
|
+
import { writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
31
|
+
import { execFileSync } from 'node:child_process';
|
|
19
32
|
import { join } from 'node:path';
|
|
20
33
|
import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
|
|
34
|
+
import { ignoreRules } from '../../../framework/hooks/shared/graph-freshness.js';
|
|
21
35
|
|
|
22
36
|
export const ALWAYS_IGNORED = [
|
|
23
37
|
'.git/', '.morph/', 'artifacts/', ...WORKTREE_DIR_NAMES.map((d) => `${d}/`),
|
|
24
38
|
'coverage/', 'graphify-out/', 'dist/', 'out/',
|
|
25
39
|
];
|
|
26
40
|
|
|
41
|
+
const TEST_PROJECTS = ['**/*.Tests', '**/*Tests', '**/*.Test'];
|
|
42
|
+
const TEST_HELPER_DIRS = ['TestDoubles', 'Fakes', 'Stubs', 'Mocks', 'Fixtures', 'Helpers', 'Builders'];
|
|
43
|
+
const TEST_HELPER_FILES = ['*Fixture.cs', '*Fixtures.cs', 'Fake*.cs', 'Stub*.cs', '*Stub.cs'];
|
|
44
|
+
const DOTNET_BUILD_OUTPUT = ['bin/', 'obj/', 'TestResults/'];
|
|
45
|
+
|
|
27
46
|
const DOTNET_IGNORED = [
|
|
28
|
-
|
|
47
|
+
...TEST_PROJECTS.map((p) => `${p}/**`),
|
|
48
|
+
...TEST_PROJECTS.map((p) => `!${p}/**/`),
|
|
49
|
+
...TEST_HELPER_DIRS.map((d) => `!**/${d}/**`),
|
|
50
|
+
...TEST_HELPER_FILES.map((f) => `!**/${f}`),
|
|
51
|
+
...DOTNET_BUILD_OUTPUT,
|
|
29
52
|
];
|
|
30
53
|
|
|
31
54
|
const WEB_IGNORED = [
|
|
@@ -35,31 +58,35 @@ const WEB_IGNORED = [
|
|
|
35
58
|
'/next.config.js', '/next.config.mjs', '/tailwind.config.js', '/tailwind.config.ts',
|
|
36
59
|
];
|
|
37
60
|
|
|
61
|
+
const HEADER = '# Gerado por morph-spec (ADR-004). Editável: um arquivo editado nunca é sobrescrito.';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* O que a versão anterior gerava — os testes .NET inteiros fora. Só existe
|
|
65
|
+
* para reconhecer um arquivo que ninguém editou e trocá-lo pelo atual.
|
|
66
|
+
*/
|
|
67
|
+
const PREVIOUS = {
|
|
68
|
+
header: '# Gerado por morph-spec (ADR-004). Editável: o CLI não sobrescreve.',
|
|
69
|
+
dotnetTitle: '# .NET — testes em qualquer nível (tests/ genérico não serve)',
|
|
70
|
+
dotnet: ['**/*.Tests/', '**/*Tests/', '**/*.Test/', ...DOTNET_BUILD_OUTPUT],
|
|
71
|
+
};
|
|
72
|
+
|
|
38
73
|
const DOTNET_TAGS = new Set(['dotnet', 'aspnet']);
|
|
39
74
|
const WEB_TAGS = new Set([
|
|
40
75
|
'nextjs', 'next', 'next.js', 'react', 'vue', 'nodejs', 'nodejs-cli', 'node-cli',
|
|
41
76
|
]);
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
* @returns {string} conteúdo completo do .graphifyignore
|
|
47
|
-
*/
|
|
48
|
-
export function buildGraphifyignore(projectTags = []) {
|
|
78
|
+
const hasDotnetTag = (tags) => [...new Set(tags)].some((t) => DOTNET_TAGS.has(t));
|
|
79
|
+
|
|
80
|
+
function render(projectTags, { header, dotnetTitle, dotnet }) {
|
|
49
81
|
const tags = new Set(projectTags);
|
|
50
|
-
const hasDotnet = [...tags].some((t) => DOTNET_TAGS.has(t));
|
|
51
82
|
const hasWeb = [...tags].some((t) => WEB_TAGS.has(t));
|
|
52
83
|
|
|
53
84
|
const sections = [
|
|
54
|
-
[
|
|
85
|
+
[header, ''],
|
|
55
86
|
['# Sempre', ...ALWAYS_IGNORED],
|
|
56
87
|
];
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
if (hasWeb) {
|
|
61
|
-
sections.push(['', '# Web — dependências e JSON de configuração de raiz', ...WEB_IGNORED]);
|
|
62
|
-
}
|
|
88
|
+
if (hasDotnetTag(tags)) sections.push(['', dotnetTitle, ...dotnet]);
|
|
89
|
+
if (hasWeb) sections.push(['', '# Web — dependências e JSON de configuração de raiz', ...WEB_IGNORED]);
|
|
63
90
|
|
|
64
91
|
const seen = new Set();
|
|
65
92
|
const lines = [];
|
|
@@ -73,15 +100,105 @@ export function buildGraphifyignore(projectTags = []) {
|
|
|
73
100
|
}
|
|
74
101
|
|
|
75
102
|
/**
|
|
76
|
-
*
|
|
103
|
+
* @param {string[]} [projectTags] saída de `getProjectTags(config)` — stack
|
|
104
|
+
* splitado por `-`/`+` mais as integrations
|
|
105
|
+
* @returns {string} conteúdo completo do .graphifyignore
|
|
106
|
+
*/
|
|
107
|
+
export function buildGraphifyignore(projectTags = []) {
|
|
108
|
+
return render(projectTags, {
|
|
109
|
+
header: HEADER,
|
|
110
|
+
dotnetTitle: '# .NET — conteúdo dos projetos de teste fora; dublês, fixtures e helpers dentro',
|
|
111
|
+
dotnet: DOTNET_IGNORED,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const normalize = (text) => String(text).replace(/^/, '').replace(/\r\n/g, '\n');
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Escreve ou migra o .graphifyignore.
|
|
119
|
+
*
|
|
120
|
+
* ausente → escreve o atual
|
|
121
|
+
* idêntico ao que a versão anterior
|
|
122
|
+
* gerava para as mesmas tags → troca pelo atual (`migrated`) —
|
|
123
|
+
* só se as REGRAS mudam (em
|
|
124
|
+
* projeto sem .NET a diferença é
|
|
125
|
+
* só o cabeçalho, e reescrever
|
|
126
|
+
* seria um diff à toa num arquivo
|
|
127
|
+
* versionado)
|
|
128
|
+
* qualquer outra coisa → é do usuário: não toca, e em
|
|
129
|
+
* projeto .NET que ainda exclui os
|
|
130
|
+
* testes inteiros devolve em
|
|
131
|
+
* `outdated` a troca a fazer
|
|
77
132
|
*
|
|
78
133
|
* @param {string} projectPath
|
|
79
134
|
* @param {string[]} [projectTags]
|
|
80
|
-
* @
|
|
135
|
+
* @param {{fx?: {writeFile: (path: string, content: string) => void}}} [opts]
|
|
136
|
+
* coletor de efeitos do `update` — com ele, o `--dry-run` prevê a escrita
|
|
137
|
+
* @returns {{written: boolean, migrated?: boolean, outdated?: {remove: string[], add: string[]}, path: string}}
|
|
81
138
|
*/
|
|
82
|
-
export function writeGraphifyignore(projectPath, projectTags = []) {
|
|
139
|
+
export function writeGraphifyignore(projectPath, projectTags = [], { fx } = {}) {
|
|
83
140
|
const path = join(projectPath, '.graphifyignore');
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
141
|
+
const current = buildGraphifyignore(projectTags);
|
|
142
|
+
const write = () => (fx ? fx.writeFile(path, current) : writeFileSync(path, current, 'utf-8'));
|
|
143
|
+
|
|
144
|
+
if (!existsSync(path)) {
|
|
145
|
+
write();
|
|
146
|
+
return { written: true, path };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let existing;
|
|
150
|
+
try {
|
|
151
|
+
existing = normalize(readFileSync(path, 'utf-8'));
|
|
152
|
+
} catch {
|
|
153
|
+
return { written: false, path };
|
|
154
|
+
}
|
|
155
|
+
if (existing === current) return { written: false, path };
|
|
156
|
+
if (existing === render(projectTags, PREVIOUS)) {
|
|
157
|
+
// A régua é a mesma do frescor do grafo (graph-freshness.js): o que o
|
|
158
|
+
// graphify lê, sem cabeçalho nem comentário.
|
|
159
|
+
if (ignoreRules(existing) === ignoreRules(current)) return { written: false, path };
|
|
160
|
+
write();
|
|
161
|
+
return { written: true, migrated: true, path };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Acrescentar não basta: enquanto `**/*.Tests/` excluir o DIRETÓRIO, nenhum
|
|
165
|
+
// `!` reinclui o que está dentro dele.
|
|
166
|
+
const lines = new Set(existing.split('\n').map((l) => l.trim()));
|
|
167
|
+
const remove = PREVIOUS.dotnet.slice(0, 3).filter((l) => lines.has(l));
|
|
168
|
+
if (hasDotnetTag(projectTags) && remove.length) {
|
|
169
|
+
const add = DOTNET_IGNORED.filter((l) => !DOTNET_BUILD_OUTPUT.includes(l) && !lines.has(l));
|
|
170
|
+
return { written: false, outdated: { remove, add }, path };
|
|
171
|
+
}
|
|
172
|
+
return { written: false, path };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* A frase que todo chamador imprime para um `outdated` — uma só.
|
|
177
|
+
*
|
|
178
|
+
* @param {{remove: string[], add: string[]}} outdated
|
|
179
|
+
* @returns {string}
|
|
180
|
+
*/
|
|
181
|
+
export function describeOutdatedIgnore(outdated) {
|
|
182
|
+
return `.graphifyignore editado mantém os testes .NET inteiros fora do grafo ` +
|
|
183
|
+
`(dublês e fixtures dão not-found). Troque ${outdated.remove.join(' ')} por, nesta ordem e ` +
|
|
184
|
+
`antes de bin/ e obj/: ${outdated.add.join(' ')} — depois, morph-spec graph refresh`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* O `.graphifyignore` é versionado? Então uma migração só chega às worktrees
|
|
189
|
+
* e ao resto do time depois de commitada — cada checkout lê o seu. Fail-open:
|
|
190
|
+
* sem git, false.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} projectPath
|
|
193
|
+
* @returns {boolean}
|
|
194
|
+
*/
|
|
195
|
+
export function graphifyignoreTracked(projectPath) {
|
|
196
|
+
try {
|
|
197
|
+
execFileSync('git', ['ls-files', '--error-unmatch', '--', '.graphifyignore'], {
|
|
198
|
+
cwd: projectPath, stdio: 'ignore', windowsHide: true, timeout: 10000,
|
|
199
|
+
});
|
|
200
|
+
return true;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
87
204
|
}
|