@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 +8 -9
- package/README.md +38 -36
- package/bin/morph-spec.js +11 -63
- package/bin/validate.js +1 -39
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +2 -1
- package/framework/MORPH.md +3 -3
- package/framework/agents.json +19 -11
- package/framework/commands/morph-apply.md +1 -1
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +22 -31
- package/framework/evals/review-checklist.md +12 -2
- package/framework/hooks/README.md +8 -44
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +2 -16
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
- package/framework/hooks/claude-code/core/gate-guard.js +7 -67
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
- package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
- package/framework/hooks/claude-code/core/state-sync.js +12 -68
- package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
- package/framework/hooks/claude-code/core/validate-completion.js +11 -57
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
- package/framework/hooks/claude-code/core/worktree-context.js +10 -110
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +6 -75
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +6 -51
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +6 -25
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +2 -15
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +1 -3
- package/framework/hooks/shared/gate-question.js +1 -7
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +1 -42
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +4 -23
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/state-reader.js +13 -183
- package/framework/hooks/shared/state-writer.js +8 -52
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/rules/nextjs-standards.md +1 -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 +4 -4
- package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
- package/framework/skills/core/morph-machine/SKILL.md +1 -1
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +9 -7
- package/framework/skills/core/morph-review/SKILL.md +9 -6
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +29 -12
- package/framework/standards/STANDARDS.json +36 -14
- package/framework/standards/STANDARDS.meta.json +16 -2
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +2 -2
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +1 -1
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/testing.md +10 -4
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/frontend/nextjs/testing.md +10 -6
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/REGISTRY.json +9 -9
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/docs/plan.md +3 -1
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +17 -104
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +5 -57
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor.js +48 -302
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +3 -32
- package/src/commands/project/init.js +4 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/standards.js +1 -62
- package/src/commands/project/status.js +0 -53
- package/src/commands/project/update.js +31 -239
- package/src/commands/project/worktree.js +108 -882
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +14 -85
- package/src/commands/state/approve.js +15 -97
- package/src/commands/state/archive.js +12 -64
- package/src/commands/state/auto-approval.js +2 -5
- package/src/commands/state/create.js +22 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +98 -475
- package/src/commands/state/gate-check.js +22 -132
- package/src/commands/state/gate-decision.js +3 -39
- package/src/commands/state/handoff.js +3 -30
- package/src/commands/state/list.js +14 -84
- package/src/commands/state/report-check.js +8 -58
- package/src/commands/state/retype.js +4 -26
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +11 -11
- package/src/commands/validation/plan-check.js +4 -2
- package/src/commands/validation/tests-audit.js +7 -0
- package/src/commands/validation/validate-feature.js +10 -97
- package/src/commands/validation/verify.js +30 -113
- package/src/core/paths/output-schema.js +2 -101
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +0 -27
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +22 -304
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +0 -1
- package/src/core/work-types.js +1 -36
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
- package/src/dashboard/app/components/CommandPalette.jsx +0 -7
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +1 -5
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
- package/src/dashboard/app/pages/Features.jsx +1 -8
- package/src/dashboard/app/pages/Guide.jsx +0 -1
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +10 -69
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +0 -6
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +10 -66
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +2 -6
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +4 -19
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +14 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +4 -50
- package/src/lib/e2e/applicability.js +37 -0
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +9 -53
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +4 -37
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +1 -0
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +3 -15
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +1 -1
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +4 -27
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/adapters.js +6 -17
- package/src/lib/mutation/dotnet-introspection.js +20 -106
- package/src/lib/mutation/runner.js +23 -75
- package/src/lib/mutation/subset.js +1 -4
- package/src/lib/mutation/trx.js +0 -1
- package/src/lib/plan/artifact-vocabulary.js +5 -25
- package/src/lib/plan/checks.js +34 -33
- package/src/lib/plan/plan-parser.js +7 -8
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +11 -26
- package/src/lib/risk-detector.js +41 -173
- package/src/lib/specs/adr-index.js +6 -34
- package/src/lib/specs/frontmatter.js +2 -15
- package/src/lib/specs/gfm-table.js +1 -7
- package/src/lib/specs/revocations.js +1 -14
- package/src/lib/specs/screen-checklist.js +47 -124
- package/src/lib/stack/dotnet-build-ignores.js +2 -6
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +1 -8
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +1 -7
- package/src/lib/standards/sections.js +2 -13
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +21 -169
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -240
- package/src/lib/tasks/test-runner.js +43 -341
- package/src/lib/telemetry/metrics.js +7 -36
- package/src/lib/telemetry/telemetry-reader.js +13 -125
- package/src/lib/telemetry/verify-event.js +2 -30
- package/src/lib/tests-audit/audit.js +96 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +11 -101
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +5 -63
- package/src/lib/validators/validation-runner.js +21 -158
- package/src/lib/validators/verify-runner.js +85 -376
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -136
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +13 -95
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -147
- package/src/utils/agents-installer.js +29 -276
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -52
- package/src/utils/file-copier.js +11 -95
- package/src/utils/hooks-installer.js +21 -298
- package/src/utils/skills-installer.js +4 -101
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -9
- package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
- package/framework/hooks/git/pre-commit/agents.sh +0 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
- package/framework/hooks/git/pre-push/run-tests.sh +0 -44
- package/src/commands/utils/index.js +0 -3
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/old-format.js +0 -68
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,27 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* `morph-spec verify` leaves the stack UP on purpose (cold-starting a
|
|
6
|
-
* container per task is what makes the loop unusable), so something has to be
|
|
7
|
-
* the one place that brings it down when the feature closes. That place is
|
|
8
|
-
* this module, shared by `finish` and `delete --with-worktree`.
|
|
9
|
-
*
|
|
10
|
-
* Three things the old root-side teardown got wrong, each pinned by a test:
|
|
11
|
-
* 1. It ran from the ROOT. A compose that exists only in the worktree was
|
|
12
|
-
* never found, and a bind mount resolved to the wrong checkout.
|
|
13
|
-
* 2. It passed a bare `COMPOSE_PROJECT_NAME`. Without the port block
|
|
14
|
-
* (`PORT`, `POSTGRES_PORT`, …) compose resolves a different config than
|
|
15
|
-
* the one that came up, and `down` misses the containers.
|
|
16
|
-
* 3. It never removed volumes or the stamp. Postgres data and a stamp saying
|
|
17
|
-
* "source unchanged since build" outlived the feature.
|
|
18
|
-
*
|
|
19
|
-
* Entirely fail-open: no compose, no docker, or a stubborn container are all
|
|
20
|
-
* non-events for the caller — a stack that refuses to come down must never
|
|
21
|
-
* fail a finish that already merged.
|
|
22
|
-
*
|
|
23
|
-
* @module lib/e2e/stack-teardown
|
|
24
|
-
*/
|
|
1
|
+
// `verify` leaves the stack up on purpose, so this is the one place that brings it down. Runs from the
|
|
2
|
+
// owning tree with the full port block, or compose resolves a different config and `down` misses it.
|
|
3
|
+
// Fail-open: a stack that refuses to come down must never fail a finish that already merged.
|
|
25
4
|
|
|
26
5
|
import { execFileSync } from 'child_process';
|
|
27
6
|
import { existsSync, readFileSync } from 'fs';
|
|
@@ -36,15 +15,6 @@ import {
|
|
|
36
15
|
} from '../../../framework/hooks/shared/e2e-check.js';
|
|
37
16
|
import { buildComposeEnv } from './compose-env.js';
|
|
38
17
|
|
|
39
|
-
/**
|
|
40
|
-
* @param {string} feature
|
|
41
|
-
* @param {object} params
|
|
42
|
-
* @param {string} params.root - primary root (absolute)
|
|
43
|
-
* @param {string|null} [params.worktreePath] - the feature's live worktree, if any
|
|
44
|
-
* @param {boolean} [params.volumes=true] - also remove named volumes (`down -v`)
|
|
45
|
-
* @param {Function} [params._exec] - injectable executor (testing)
|
|
46
|
-
* @returns {{ attempted: boolean, cwd: string, status: 'pass'|'fail'|'skip', stampDeleted: boolean, output?: string }}
|
|
47
|
-
*/
|
|
48
18
|
export function tearDownFeatureStack(feature, { root, worktreePath = null, volumes = true, _exec = defaultExec } = {}) {
|
|
49
19
|
const cwd = worktreePath && existsSync(worktreePath) ? worktreePath : root;
|
|
50
20
|
const projectName = composeProjectName(feature);
|
|
@@ -53,8 +23,7 @@ export function tearDownFeatureStack(feature, { root, worktreePath = null, volum
|
|
|
53
23
|
try {
|
|
54
24
|
const config = readConfig(cwd) ?? readConfig(root);
|
|
55
25
|
|
|
56
|
-
// The owning tree's compose first; the root's copy is the fallback for a
|
|
57
|
-
// worktree already half torn down (versioned file, so equivalent).
|
|
26
|
+
// The owning tree's compose first; the root's copy is the fallback for a worktree already half torn down.
|
|
58
27
|
let composeCwd = cwd;
|
|
59
28
|
let composeFile = resolveComposeFile(cwd, config);
|
|
60
29
|
if (!composeFile && cwd !== root) {
|
|
@@ -63,8 +32,7 @@ export function tearDownFeatureStack(feature, { root, worktreePath = null, volum
|
|
|
63
32
|
}
|
|
64
33
|
|
|
65
34
|
if (!composeFile) {
|
|
66
|
-
// Nothing to bring down
|
|
67
|
-
// `up` that a stack it cannot see is still fresh.
|
|
35
|
+
// Nothing to bring down, but a stamp left behind would tell the next `up` a stack it cannot see is fresh.
|
|
68
36
|
return { attempted: false, cwd, status: 'skip', stampDeleted: deleteStamp(gitCommonDir, projectName) };
|
|
69
37
|
}
|
|
70
38
|
|
|
@@ -82,13 +50,11 @@ export function tearDownFeatureStack(feature, { root, worktreePath = null, volum
|
|
|
82
50
|
...(result.status === 'fail' && result.output ? { output: result.output } : {}),
|
|
83
51
|
};
|
|
84
52
|
} catch {
|
|
85
|
-
//
|
|
86
|
-
// outlive the stack it described.
|
|
53
|
+
// The stamp must not outlive the stack it described, even when the teardown itself blew up.
|
|
87
54
|
return { attempted: false, cwd, status: 'fail', stampDeleted: deleteStamp(gitCommonDir, projectName) };
|
|
88
55
|
}
|
|
89
56
|
}
|
|
90
57
|
|
|
91
|
-
/** BOM-tolerant config read; null when absent or malformed. */
|
|
92
58
|
function readConfig(dir) {
|
|
93
59
|
try {
|
|
94
60
|
const path = join(dir, '.morph', 'config', 'config.json');
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Architecture Scanner - Generates rich architectural context from project filesystem.
|
|
3
|
-
*
|
|
4
|
-
* Performs fast (<5s) glob-based scanning to produce a snapshot of:
|
|
5
|
-
* - .NET entities (sealed class/record)
|
|
6
|
-
* - API endpoints (IApiEndpoint implementations)
|
|
7
|
-
* - Next.js pages and components
|
|
8
|
-
* - Integrations (from appsettings.json)
|
|
9
|
-
* - Database schema (from EF migrations)
|
|
10
|
-
* - Stack dependencies (.csproj + package.json)
|
|
11
|
-
*
|
|
12
|
-
* All scanning is regex-based (no AST parsing) for speed.
|
|
13
|
-
*
|
|
14
|
-
* @module architecture-scanner
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
1
|
import fs from 'fs/promises';
|
|
18
2
|
import path from 'path';
|
|
19
3
|
import { fileURLToPath } from 'url';
|
|
@@ -25,16 +9,6 @@ import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
|
|
|
25
9
|
export const SKIP_DIRS = new Set(['node_modules', 'bin', 'obj', '.git', '.morph', '.next', 'dist', 'out', ...WORKTREE_DIR_NAMES]);
|
|
26
10
|
const MAX_FILES_PER_SCANNER = 200;
|
|
27
11
|
|
|
28
|
-
/**
|
|
29
|
-
* Recursively walk a directory, returning files matching a filter.
|
|
30
|
-
* Skips known heavy directories for performance.
|
|
31
|
-
*
|
|
32
|
-
* @param {string} dir - Directory to walk
|
|
33
|
-
* @param {function} filter - (relativePath) => boolean
|
|
34
|
-
* @param {string} [base] - Base path for relative calculation
|
|
35
|
-
* @param {number} [limit] - Max files to return
|
|
36
|
-
* @returns {Promise<Array<{abs: string, rel: string}>>}
|
|
37
|
-
*/
|
|
38
12
|
async function walkDir(dir, filter, base = dir, limit = MAX_FILES_PER_SCANNER) {
|
|
39
13
|
const results = [];
|
|
40
14
|
|
|
@@ -68,12 +42,6 @@ async function walkDir(dir, filter, base = dir, limit = MAX_FILES_PER_SCANNER) {
|
|
|
68
42
|
return results;
|
|
69
43
|
}
|
|
70
44
|
|
|
71
|
-
/**
|
|
72
|
-
* Scan .NET entities from Entities/ or Features/ directories.
|
|
73
|
-
*
|
|
74
|
-
* @param {string} projectPath
|
|
75
|
-
* @returns {Promise<Array<{name: string, type: string, path: string}>>}
|
|
76
|
-
*/
|
|
77
45
|
export async function scanEntities(projectPath) {
|
|
78
46
|
try {
|
|
79
47
|
const files = await walkDir(
|
|
@@ -91,7 +59,6 @@ export async function scanEntities(projectPath) {
|
|
|
91
59
|
entityRegex.lastIndex = 0;
|
|
92
60
|
while ((match = entityRegex.exec(content)) !== null) {
|
|
93
61
|
const line = content.slice(Math.max(0, match.index - 80), match.index + match[0].length);
|
|
94
|
-
// Only capture sealed/record types or classes in entity-like directories
|
|
95
62
|
if (/sealed|record/.test(line) || file.rel.includes('Entities/')) {
|
|
96
63
|
entities.push({
|
|
97
64
|
name: match[2],
|
|
@@ -100,7 +67,7 @@ export async function scanEntities(projectPath) {
|
|
|
100
67
|
});
|
|
101
68
|
}
|
|
102
69
|
}
|
|
103
|
-
} catch {
|
|
70
|
+
} catch {}
|
|
104
71
|
}
|
|
105
72
|
|
|
106
73
|
return entities;
|
|
@@ -109,12 +76,6 @@ export async function scanEntities(projectPath) {
|
|
|
109
76
|
}
|
|
110
77
|
}
|
|
111
78
|
|
|
112
|
-
/**
|
|
113
|
-
* Scan API endpoints implementing IApiEndpoint.
|
|
114
|
-
*
|
|
115
|
-
* @param {string} projectPath
|
|
116
|
-
* @returns {Promise<Array<{name: string, route: string, method: string, path: string}>>}
|
|
117
|
-
*/
|
|
118
79
|
export async function scanEndpoints(projectPath) {
|
|
119
80
|
try {
|
|
120
81
|
const files = await walkDir(
|
|
@@ -142,7 +103,7 @@ export async function scanEndpoints(projectPath) {
|
|
|
142
103
|
method: methodMatch ? (methodMatch[1] || methodMatch[2] || '').toUpperCase() : '',
|
|
143
104
|
path: file.rel
|
|
144
105
|
});
|
|
145
|
-
} catch {
|
|
106
|
+
} catch {}
|
|
146
107
|
}
|
|
147
108
|
|
|
148
109
|
return endpoints;
|
|
@@ -151,17 +112,10 @@ export async function scanEndpoints(projectPath) {
|
|
|
151
112
|
}
|
|
152
113
|
}
|
|
153
114
|
|
|
154
|
-
/**
|
|
155
|
-
* Scan Next.js pages and components.
|
|
156
|
-
*
|
|
157
|
-
* @param {string} projectPath
|
|
158
|
-
* @returns {Promise<{pages: Array<{route: string, path: string}>, layouts: Array<{path: string}>, components: Array<{name: string, path: string}>}>}
|
|
159
|
-
*/
|
|
160
115
|
export async function scanNextjsComponents(projectPath) {
|
|
161
116
|
try {
|
|
162
117
|
const result = { pages: [], layouts: [], components: [] };
|
|
163
118
|
|
|
164
|
-
// Scan pages and layouts from app directory
|
|
165
119
|
const appFiles = await walkDir(
|
|
166
120
|
projectPath,
|
|
167
121
|
(rel, name) =>
|
|
@@ -181,7 +135,6 @@ export async function scanNextjsComponents(projectPath) {
|
|
|
181
135
|
}
|
|
182
136
|
}
|
|
183
137
|
|
|
184
|
-
// Scan components
|
|
185
138
|
const componentFiles = await walkDir(
|
|
186
139
|
projectPath,
|
|
187
140
|
(rel, name) =>
|
|
@@ -191,7 +144,6 @@ export async function scanNextjsComponents(projectPath) {
|
|
|
191
144
|
|
|
192
145
|
for (const file of componentFiles) {
|
|
193
146
|
const name = path.basename(file.rel, path.extname(file.rel));
|
|
194
|
-
// Skip index files and test files
|
|
195
147
|
if (name === 'index' || name.includes('.test') || name.includes('.spec')) continue;
|
|
196
148
|
result.components.push({ name, path: file.rel });
|
|
197
149
|
}
|
|
@@ -202,12 +154,6 @@ export async function scanNextjsComponents(projectPath) {
|
|
|
202
154
|
}
|
|
203
155
|
}
|
|
204
156
|
|
|
205
|
-
/**
|
|
206
|
-
* Scan integrations from appsettings*.json files.
|
|
207
|
-
*
|
|
208
|
-
* @param {string} projectPath
|
|
209
|
-
* @returns {Promise<Array<{name: string, type: string, configured: boolean}>>}
|
|
210
|
-
*/
|
|
211
157
|
export async function scanIntegrations(projectPath) {
|
|
212
158
|
try {
|
|
213
159
|
const files = await walkDir(
|
|
@@ -217,7 +163,6 @@ export async function scanIntegrations(projectPath) {
|
|
|
217
163
|
20
|
|
218
164
|
);
|
|
219
165
|
|
|
220
|
-
// Also check .env files for common integrations
|
|
221
166
|
const envFiles = await walkDir(
|
|
222
167
|
projectPath,
|
|
223
168
|
(rel, name) => name === '.env' || name === '.env.local' || name === '.env.production',
|
|
@@ -242,7 +187,6 @@ export async function scanIntegrations(projectPath) {
|
|
|
242
187
|
|
|
243
188
|
const detected = new Map();
|
|
244
189
|
|
|
245
|
-
// Scan appsettings files
|
|
246
190
|
for (const file of files) {
|
|
247
191
|
try {
|
|
248
192
|
const content = JSON.parse(await fs.readFile(file.abs, 'utf8'));
|
|
@@ -256,7 +200,6 @@ export async function scanIntegrations(projectPath) {
|
|
|
256
200
|
}
|
|
257
201
|
}
|
|
258
202
|
}
|
|
259
|
-
// Check ConnectionStrings specifically
|
|
260
203
|
if (content.ConnectionStrings) {
|
|
261
204
|
for (const [key, val] of Object.entries(content.ConnectionStrings)) {
|
|
262
205
|
if (val && !String(val).startsWith('{{')) {
|
|
@@ -264,10 +207,9 @@ export async function scanIntegrations(projectPath) {
|
|
|
264
207
|
}
|
|
265
208
|
}
|
|
266
209
|
}
|
|
267
|
-
} catch {
|
|
210
|
+
} catch {}
|
|
268
211
|
}
|
|
269
212
|
|
|
270
|
-
// Scan .env files for known prefixes
|
|
271
213
|
const envPrefixes = {
|
|
272
214
|
'NEON_AUTH_': 'NeonAuth',
|
|
273
215
|
'DATABASE_URL': 'Neon',
|
|
@@ -289,7 +231,7 @@ export async function scanIntegrations(projectPath) {
|
|
|
289
231
|
}
|
|
290
232
|
}
|
|
291
233
|
}
|
|
292
|
-
} catch {
|
|
234
|
+
} catch {}
|
|
293
235
|
}
|
|
294
236
|
|
|
295
237
|
return Array.from(detected.values());
|
|
@@ -298,12 +240,6 @@ export async function scanIntegrations(projectPath) {
|
|
|
298
240
|
}
|
|
299
241
|
}
|
|
300
242
|
|
|
301
|
-
/**
|
|
302
|
-
* Scan database schema from EF Core migrations.
|
|
303
|
-
*
|
|
304
|
-
* @param {string} projectPath
|
|
305
|
-
* @returns {Promise<Array<{table: string, migration: string}>>}
|
|
306
|
-
*/
|
|
307
243
|
export async function scanDatabaseSchema(projectPath) {
|
|
308
244
|
try {
|
|
309
245
|
const files = await walkDir(
|
|
@@ -331,10 +267,9 @@ export async function scanDatabaseSchema(projectPath) {
|
|
|
331
267
|
}
|
|
332
268
|
}
|
|
333
269
|
}
|
|
334
|
-
} catch {
|
|
270
|
+
} catch {}
|
|
335
271
|
}
|
|
336
272
|
|
|
337
|
-
// Also scan database migrations (SQL)
|
|
338
273
|
const sqlFiles = await walkDir(
|
|
339
274
|
projectPath,
|
|
340
275
|
(rel, name) => name.endsWith('.sql') && rel.includes('migrations/'),
|
|
@@ -355,7 +290,7 @@ export async function scanDatabaseSchema(projectPath) {
|
|
|
355
290
|
tables.set(match[1], { table: match[1], migration: migrationName });
|
|
356
291
|
}
|
|
357
292
|
}
|
|
358
|
-
} catch {
|
|
293
|
+
} catch {}
|
|
359
294
|
}
|
|
360
295
|
|
|
361
296
|
return Array.from(tables.values());
|
|
@@ -364,17 +299,10 @@ export async function scanDatabaseSchema(projectPath) {
|
|
|
364
299
|
}
|
|
365
300
|
}
|
|
366
301
|
|
|
367
|
-
/**
|
|
368
|
-
* Detect stack from .csproj PackageReference and package.json dependencies.
|
|
369
|
-
*
|
|
370
|
-
* @param {string} projectPath
|
|
371
|
-
* @returns {Promise<{dotnet: Array<{name: string, version: string}>, npm: Array<{name: string, version: string}>}>}
|
|
372
|
-
*/
|
|
373
302
|
export async function scanStackDependencies(projectPath) {
|
|
374
303
|
try {
|
|
375
304
|
const result = { dotnet: [], npm: [] };
|
|
376
305
|
|
|
377
|
-
// Scan .csproj files
|
|
378
306
|
const csprojFiles = await walkDir(
|
|
379
307
|
projectPath,
|
|
380
308
|
(rel, name) => name.endsWith('.csproj'),
|
|
@@ -392,10 +320,9 @@ export async function scanStackDependencies(projectPath) {
|
|
|
392
320
|
while ((match = packageRefRegex.exec(content)) !== null) {
|
|
393
321
|
result.dotnet.push({ name: match[1], version: match[2] });
|
|
394
322
|
}
|
|
395
|
-
} catch {
|
|
323
|
+
} catch {}
|
|
396
324
|
}
|
|
397
325
|
|
|
398
|
-
// Scan package.json (root only)
|
|
399
326
|
const pkgPath = path.join(projectPath, 'package.json');
|
|
400
327
|
if (existsSync(pkgPath)) {
|
|
401
328
|
try {
|
|
@@ -404,10 +331,9 @@ export async function scanStackDependencies(projectPath) {
|
|
|
404
331
|
for (const [name, version] of Object.entries(allDeps)) {
|
|
405
332
|
result.npm.push({ name, version: String(version) });
|
|
406
333
|
}
|
|
407
|
-
} catch {
|
|
334
|
+
} catch {}
|
|
408
335
|
}
|
|
409
336
|
|
|
410
|
-
// Also check web/ or frontend/ subdirectory package.json
|
|
411
337
|
for (const subdir of ['web', 'frontend', 'client', 'src']) {
|
|
412
338
|
const subPkgPath = path.join(projectPath, subdir, 'package.json');
|
|
413
339
|
if (existsSync(subPkgPath)) {
|
|
@@ -415,12 +341,11 @@ export async function scanStackDependencies(projectPath) {
|
|
|
415
341
|
const pkg = JSON.parse(await fs.readFile(subPkgPath, 'utf8'));
|
|
416
342
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
417
343
|
for (const [name, version] of Object.entries(allDeps)) {
|
|
418
|
-
// Avoid duplicates
|
|
419
344
|
if (!result.npm.some(d => d.name === name)) {
|
|
420
345
|
result.npm.push({ name, version: String(version) });
|
|
421
346
|
}
|
|
422
347
|
}
|
|
423
|
-
} catch {
|
|
348
|
+
} catch {}
|
|
424
349
|
}
|
|
425
350
|
}
|
|
426
351
|
|
|
@@ -430,12 +355,6 @@ export async function scanStackDependencies(projectPath) {
|
|
|
430
355
|
}
|
|
431
356
|
}
|
|
432
357
|
|
|
433
|
-
/**
|
|
434
|
-
* Master scan — runs all scanners in parallel.
|
|
435
|
-
*
|
|
436
|
-
* @param {string} projectPath
|
|
437
|
-
* @returns {Promise<Object>} ArchitectureScanResult
|
|
438
|
-
*/
|
|
439
358
|
export async function scanArchitecture(projectPath) {
|
|
440
359
|
const [entities, endpoints, nextjs, integrations, database, stack, graph] = await Promise.all([
|
|
441
360
|
scanEntities(projectPath),
|
|
@@ -444,9 +363,7 @@ export async function scanArchitecture(projectPath) {
|
|
|
444
363
|
scanIntegrations(projectPath),
|
|
445
364
|
scanDatabaseSchema(projectPath),
|
|
446
365
|
scanStackDependencies(projectPath),
|
|
447
|
-
// Aditivo: sem grafo devolve listas vazias
|
|
448
|
-
// inventário acima continua sendo a espinha do arquivo — ele tem
|
|
449
|
-
// exaustividade que o grafo não tem.
|
|
366
|
+
// Aditivo: sem grafo devolve listas vazias; o inventário acima tem a exaustividade que o grafo não tem.
|
|
450
367
|
scanGraph(projectPath)
|
|
451
368
|
]);
|
|
452
369
|
|
|
@@ -462,20 +379,11 @@ export async function scanArchitecture(projectPath) {
|
|
|
462
379
|
};
|
|
463
380
|
}
|
|
464
381
|
|
|
465
|
-
|
|
466
|
-
* Resolve a template path: check .morph/framework/templates/ first, then framework/templates/,
|
|
467
|
-
* then fall back to the CLI package's own framework/templates/.
|
|
468
|
-
*
|
|
469
|
-
* @param {string} projectPath
|
|
470
|
-
* @param {string} templateRelPath
|
|
471
|
-
* @returns {string|null}
|
|
472
|
-
*/
|
|
382
|
+
// Project-local `.morph/framework/templates/` override first, then the CLI package's own.
|
|
473
383
|
function resolveTemplatePath(projectPath, templateRelPath) {
|
|
474
|
-
// 1. Project-local override
|
|
475
384
|
const projectLocal = path.join(projectPath, '.morph/framework/templates', templateRelPath);
|
|
476
385
|
if (existsSync(projectLocal)) return projectLocal;
|
|
477
386
|
|
|
478
|
-
// 2. CLI package framework/ (for running from source or during init)
|
|
479
387
|
const cliFramework = path.join(
|
|
480
388
|
cliRootDir(), 'framework', 'templates', templateRelPath
|
|
481
389
|
);
|
|
@@ -484,103 +392,64 @@ function resolveTemplatePath(projectPath, templateRelPath) {
|
|
|
484
392
|
return null;
|
|
485
393
|
}
|
|
486
394
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
*
|
|
490
|
-
* fileURLToPath, not `new URL(...).pathname`: the latter leaves the path
|
|
491
|
-
* percent-encoded, so an install under a directory with a space resolved to
|
|
492
|
-
* ".../Program%20Files/..." — a path that never exists. `resolveTemplatePath`
|
|
493
|
-
* then returned null and the caller fell through to its plain-text fallback
|
|
494
|
-
* WITHOUT any warning. The first lines of that fallback are identical to the
|
|
495
|
-
* template's, which is why nobody noticed the tables were gone. On Windows a
|
|
496
|
-
* global npm install lives under a spaced path by default, so this was the
|
|
497
|
-
* common case, not the edge one.
|
|
498
|
-
*/
|
|
395
|
+
// fileURLToPath, not `new URL(...).pathname`: the latter stays percent-encoded, so a path with a space
|
|
396
|
+
// (the default global npm install on Windows) never resolves and the template is silently dropped.
|
|
499
397
|
function cliRootDir() {
|
|
500
398
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
501
399
|
}
|
|
502
400
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
*
|
|
506
|
-
* QUEM LÊ ESTE ARQUIVO, E QUANDO: o passo 1c da skill `morph-plan` (mapa de
|
|
507
|
-
* reuso), antes de decidir REUSAR/ESTENDER/CRIAR cada conceito da feature. É a
|
|
508
|
-
* granularidade de CONCEITO — entidades, endpoints com rota, componentes com
|
|
509
|
-
* path — que o grafo de codebase, que casa por símbolo, não dá; e é o único
|
|
510
|
-
* inventário legível num projeto sem grafo (sem Python), antes do grep.
|
|
511
|
-
*
|
|
512
|
-
* Até 2026-08-27 este arquivo era gerado e NUNCA lido por skill nenhuma
|
|
513
|
-
* (docs/specs/code-reuse-gap/analysis.md §2.5). Se um dia o consumidor acima
|
|
514
|
-
* desaparecer do `morph-plan`, este gerador volta a queimar tempo por nada —
|
|
515
|
-
* a decisão então é remover os dois, não deixar o arquivo órfão de novo.
|
|
516
|
-
*
|
|
517
|
-
* @param {string} projectPath
|
|
518
|
-
* @param {Object} scanResult - Result from scanArchitecture()
|
|
519
|
-
* @param {Object} [options]
|
|
520
|
-
* @param {string} [options.projectName] - Override project name
|
|
521
|
-
* @returns {Promise<string>} Path to written file
|
|
522
|
-
*/
|
|
401
|
+
// Read by `morph-plan` for the concept-level inventory the graph doesn't give. If that reader goes away,
|
|
402
|
+
// remove this generator too.
|
|
523
403
|
export async function renderArchitectureContext(projectPath, scanResult, options = {}) {
|
|
524
404
|
const templatePath = resolveTemplatePath(projectPath, 'context/ARCHITECTURE.md');
|
|
525
405
|
|
|
526
|
-
// Determine project name
|
|
527
406
|
let projectName = options.projectName;
|
|
528
407
|
if (!projectName) {
|
|
529
|
-
// Try config.json
|
|
530
408
|
const configPath = path.join(projectPath, '.morph/config/config.json');
|
|
531
409
|
if (existsSync(configPath)) {
|
|
532
410
|
try {
|
|
533
411
|
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
534
412
|
projectName = config.project?.name;
|
|
535
|
-
} catch {
|
|
413
|
+
} catch {}
|
|
536
414
|
}
|
|
537
|
-
// Fallback to directory name
|
|
538
415
|
if (!projectName) {
|
|
539
416
|
projectName = path.basename(projectPath);
|
|
540
417
|
}
|
|
541
418
|
}
|
|
542
419
|
|
|
543
|
-
// Get morph-spec version
|
|
544
420
|
let morphVersion = 'unknown';
|
|
545
421
|
try {
|
|
546
422
|
const pkgPath = path.join(cliRootDir(), 'package.json');
|
|
547
423
|
if (existsSync(pkgPath)) {
|
|
548
424
|
morphVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
|
|
549
425
|
}
|
|
550
|
-
} catch {
|
|
426
|
+
} catch {}
|
|
551
427
|
|
|
552
428
|
const variables = {
|
|
553
429
|
projectName,
|
|
554
430
|
timestamp: new Date().toISOString().replace('T', ' ').split('.')[0],
|
|
555
431
|
morphVersion,
|
|
556
|
-
// Entities
|
|
557
432
|
hasDotnet: scanResult.entities.length > 0 || scanResult.endpoints.length > 0,
|
|
558
433
|
entities: scanResult.entities,
|
|
559
434
|
entityCount: scanResult.entities.length,
|
|
560
435
|
endpoints: scanResult.endpoints,
|
|
561
436
|
endpointCount: scanResult.endpoints.length,
|
|
562
|
-
// Next.js
|
|
563
437
|
hasNextjs: scanResult.nextjs.pages.length > 0 || scanResult.nextjs.components.length > 0,
|
|
564
438
|
pages: scanResult.nextjs.pages,
|
|
565
439
|
pageCount: scanResult.nextjs.pages.length,
|
|
566
440
|
layouts: scanResult.nextjs.layouts,
|
|
567
441
|
components: scanResult.nextjs.components,
|
|
568
442
|
componentCount: scanResult.nextjs.components.length,
|
|
569
|
-
// Integrations
|
|
570
443
|
hasIntegrations: scanResult.integrations.length > 0,
|
|
571
444
|
integrations: scanResult.integrations,
|
|
572
|
-
// Database
|
|
573
445
|
hasTables: scanResult.database.length > 0,
|
|
574
446
|
tables: scanResult.database,
|
|
575
447
|
tableCount: scanResult.database.length,
|
|
576
|
-
// Stack
|
|
577
448
|
hasDotnetPackages: scanResult.stack.dotnet.length > 0,
|
|
578
449
|
dotnetPackages: scanResult.stack.dotnet,
|
|
579
450
|
hasNpmPackages: scanResult.stack.npm.length > 0,
|
|
580
451
|
npmPackages: scanResult.stack.npm,
|
|
581
|
-
//
|
|
582
|
-
// Comunidade ainda com rótulo Community_N fica de fora: sem
|
|
583
|
-
// `graphify label` ela é ruído puro (441 páginas assim no eval).
|
|
452
|
+
// Comunidade ainda rotulada Community_N fica de fora: sem `graphify label` é ruído puro.
|
|
584
453
|
hasGraph: scanResult.graph?.available === true && (scanResult.graph.godNodes?.length ?? 0) > 0,
|
|
585
454
|
godNodes: scanResult.graph?.godNodes ?? [],
|
|
586
455
|
communities: (scanResult.graph?.communities ?? []).filter((c) => c.labeled),
|
|
@@ -591,11 +460,9 @@ export async function renderArchitectureContext(projectPath, scanResult, options
|
|
|
591
460
|
if (templatePath) {
|
|
592
461
|
content = renderTemplate(templatePath, variables);
|
|
593
462
|
} else {
|
|
594
|
-
// Fallback: generate minimal markdown without template
|
|
595
463
|
content = generateFallbackMarkdown(variables);
|
|
596
464
|
}
|
|
597
465
|
|
|
598
|
-
// Write to .morph/context/README.md
|
|
599
466
|
const contextDir = path.join(projectPath, '.morph/context');
|
|
600
467
|
await fs.mkdir(contextDir, { recursive: true });
|
|
601
468
|
const outputPath = path.join(contextDir, 'README.md');
|
|
@@ -604,11 +471,6 @@ export async function renderArchitectureContext(projectPath, scanResult, options
|
|
|
604
471
|
return outputPath;
|
|
605
472
|
}
|
|
606
473
|
|
|
607
|
-
/**
|
|
608
|
-
* Generate minimal markdown when template is not available.
|
|
609
|
-
* @param {Object} vars
|
|
610
|
-
* @returns {string}
|
|
611
|
-
*/
|
|
612
474
|
function generateFallbackMarkdown(vars) {
|
|
613
475
|
const lines = [`# ${vars.projectName} — Architecture Context\n`];
|
|
614
476
|
lines.push(`> Auto-generated by morph-spec context refresh`);
|
|
@@ -1,20 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* A fleet runs one Claude Code instance per feature worktree. What travels
|
|
5
|
-
* between sessions must be a HANDOFF, not an export: a compact document that
|
|
6
|
-
* points at the feature's own files instead of copying their contents, so a
|
|
7
|
-
* fresh instance starts focused instead of inheriting a wall of stale context.
|
|
8
|
-
*
|
|
9
|
-
* Deterministic by construction — everything here is derived from feature.json,
|
|
10
|
-
* the extracted plan checks, the slice reports and the worktree env manifest. The document is
|
|
11
|
-
* therefore REGENERABLE (never hand-edited, never merged): `morph-spec handoff`
|
|
12
|
-
* rebuilds it, and setup/create/advance/approve regenerate it at the moments a
|
|
13
|
-
* new session is most likely to pick the feature up.
|
|
14
|
-
*
|
|
15
|
-
* Every section is fail-open: missing or corrupt plan checks degrade that
|
|
16
|
-
* section, never the document.
|
|
17
|
-
*/
|
|
1
|
+
// Derived only from committed sources, so it is regenerated, never hand-edited or merged.
|
|
2
|
+
// Every section is fail-open.
|
|
18
3
|
|
|
19
4
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
20
5
|
import { join } from 'path';
|
|
@@ -22,7 +7,6 @@ import { ensureCommonExclude } from '../../../framework/hooks/shared/git-exclude
|
|
|
22
7
|
import { derivePhaseForFeature, getPendingGates } from '../../../framework/hooks/shared/state-reader.js';
|
|
23
8
|
import { sliceProgress, readySlices } from '../../../framework/hooks/shared/slice-progress.js';
|
|
24
9
|
|
|
25
|
-
/** Ordered pointer candidates — only existing paths make it into the doc. */
|
|
26
10
|
const POINTER_CANDIDATES = [
|
|
27
11
|
['2-plan/plan.md', 'plano: problema, cenários com prova, decisões e fatias'],
|
|
28
12
|
['2-plan/checks.json', 'cenários e fatias extraídos pelo plan-check'],
|
|
@@ -68,7 +52,7 @@ function nextAction(feature, phase, slices, featureName) {
|
|
|
68
52
|
|
|
69
53
|
switch (phase) {
|
|
70
54
|
case 'plan':
|
|
71
|
-
return `Escreva \`2-plan/plan.md\`, rode \`morph-spec plan-check ${featureName}\` até zero erro e aprove o gate do plano (\`morph-spec advance ${featureName}\`). Skill: /morph-
|
|
55
|
+
return `Escreva \`2-plan/plan.md\`, rode \`morph-spec plan-check ${featureName}\` até zero erro e aprove o gate do plano (\`morph-spec advance ${featureName}\`). Skill: /morph-plan ${featureName}.`;
|
|
72
56
|
case 'uiux':
|
|
73
57
|
return approved('uiux')
|
|
74
58
|
? `Gate de uiux aprovado — implemente as fatias do plano via /morph-apply ${featureName}.`
|
|
@@ -88,20 +72,8 @@ function nextAction(feature, phase, slices, featureName) {
|
|
|
88
72
|
}
|
|
89
73
|
}
|
|
90
74
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
* root or worktree — wherever the feature's `.morph/features/{f}` lives).
|
|
94
|
-
*
|
|
95
|
-
* @param {string} featureName
|
|
96
|
-
* @param {string} [cwd]
|
|
97
|
-
* @param {{ worktree?: {path:string, branch:string}|null }} [opts] - override
|
|
98
|
-
* the `worktree` bookkeeping instead of reading `feature.worktree`. Needed
|
|
99
|
-
* because that field is ROOT-OWNED and stripped from every branch-local
|
|
100
|
-
* copy at birth (create.js) — a handoff built FROM the worktree's own copy
|
|
101
|
-
* would otherwise always say "no worktree registered", even while sitting
|
|
102
|
-
* inside one. Pass `undefined` (the default) to fall back to `feature.worktree`.
|
|
103
|
-
* @returns {{ markdown: string, meta: { generatedAt: string, phase: string, slicesDone: number, slicesTotal: number } } | null}
|
|
104
|
-
*/
|
|
75
|
+
// `opts.worktree` overrides `feature.worktree`: that field is root-owned and stripped from branch-local
|
|
76
|
+
// copies, so a handoff built inside a worktree would otherwise say "no worktree registered".
|
|
105
77
|
export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
106
78
|
const featureDir = join(cwd, '.morph', 'features', featureName);
|
|
107
79
|
if (!existsSync(join(featureDir, 'feature.json'))) return null;
|
|
@@ -118,7 +90,7 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
118
90
|
try {
|
|
119
91
|
phase = derivePhaseForFeature(featureName, cwd) || 'plan';
|
|
120
92
|
pendingGates = getPendingGates(featureName, cwd) || [];
|
|
121
|
-
} catch {
|
|
93
|
+
} catch {}
|
|
122
94
|
|
|
123
95
|
const generatedAt = new Date().toISOString();
|
|
124
96
|
const slices = sliceProgress(featureDir);
|
|
@@ -193,29 +165,13 @@ export function buildHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
|
193
165
|
const HANDOFF_EXCLUDE_MARKER = '# morph-spec: derived handoff docs (managed — see handoff-generator)';
|
|
194
166
|
const HANDOFF_EXCLUDE_PATTERN = '/.morph/**/handoff.md';
|
|
195
167
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
* itself — regenerate > synchronize, the same principle as the materialized
|
|
199
|
-
* state.json. Left visible, every `worktree setup`/`advance` would drop an
|
|
200
|
-
* untracked file into the very `git status` the finish gates read. The
|
|
201
|
-
* common-dir `.git/info/exclude` hides it in every worktree of the repo
|
|
202
|
-
* (including the primary root); `**` also covers a feature folder after
|
|
203
|
-
* `archive` moves it. Idempotent and fail-open — a handoff that shows up as
|
|
204
|
-
* untracked is noise, never data loss.
|
|
205
|
-
*/
|
|
168
|
+
// Never committed: left visible it lands in the `git status` the finish gates read. The common-dir
|
|
169
|
+
// exclude hides it in every worktree; `**` also covers a feature folder after `archive` moves it.
|
|
206
170
|
function ensureHandoffExcluded(cwd) {
|
|
207
171
|
ensureCommonExclude(cwd, [HANDOFF_EXCLUDE_PATTERN], HANDOFF_EXCLUDE_MARKER);
|
|
208
172
|
}
|
|
209
173
|
|
|
210
|
-
/**
|
|
211
|
-
* Write `.morph/features/{f}/handoff.md`. Returns the written path, or null
|
|
212
|
-
* when the feature does not exist (nothing is created).
|
|
213
|
-
*
|
|
214
|
-
* @param {string} featureName
|
|
215
|
-
* @param {string} [cwd]
|
|
216
|
-
* @param {{ worktree?: {path:string, branch:string}|null }} [opts] - see buildHandoff
|
|
217
|
-
* @returns {string|null}
|
|
218
|
-
*/
|
|
174
|
+
/** The written path, or null when the feature does not exist (nothing is created). */
|
|
219
175
|
export function writeHandoff(featureName, cwd = process.cwd(), opts = {}) {
|
|
220
176
|
const result = buildHandoff(featureName, cwd, opts);
|
|
221
177
|
if (!result) return null;
|