@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +86 -21
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- 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 +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- 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 +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- 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.py +55 -41
- package/framework/hooks/package.json +3 -0
- 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 +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- 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 +14 -72
- 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 +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- 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/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- 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/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- 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/evals-with-cache.md +1 -2
- 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 +3 -3
- 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 +7 -7
- 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/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/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- 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 +18 -105
- 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 +17 -111
- 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-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- 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 +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- 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 +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- 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 +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- 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 +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- 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 +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- 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 +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- 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 +2 -1
- 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 +17 -17
- 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 +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- 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 +4 -26
- 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 +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- 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 +31 -126
- 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 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/dotnet-test-introspection.js +0 -632
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,41 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Todo mundo que pergunta lê daqui: o refresh em background
|
|
5
|
-
* (graph-refresh.js, decide se roda), o `graph status|explain|affected` e o
|
|
6
|
-
* `doctor` (via src/lib/graph/graph-loader.js `getFreshness`) e o
|
|
7
|
-
* `graph build|refresh` (grava o resultado). Mora em hooks/shared porque hook
|
|
8
|
-
* não importa `src/`. Uma segunda cópia da regra é como o status dizia
|
|
9
|
-
* "fresco" enquanto o refresh achava "velho", ou o contrário.
|
|
10
|
-
*
|
|
11
|
-
* Duas fontes, nesta ordem:
|
|
12
|
-
* 1. o carimbo `built_at_commit` do graph.json contra o HEAD;
|
|
13
|
-
* 2. o REGISTRO da última rodada, por árvore, em
|
|
14
|
-
* `<git-common-dir>/morph-spec/graph-refresh-state.json`:
|
|
15
|
-
* `{ [árvore]: { head, exit, ignoreHash?, at, unavailable?, seeded? } }`.
|
|
16
|
-
*
|
|
17
|
-
* O registro existe porque o `graphify update` do 0.9.50 NÃO recarimba quando
|
|
18
|
-
* a topologia não muda — "No code-graph topology changes detected; outputs
|
|
19
|
-
* left untouched" (watch.py): commit só de `.morph/`, corpo de método editado,
|
|
20
|
-
* commit num arquivo excluído. O carimbo fica no commit antigo e, sem o
|
|
21
|
-
* registro, o grafo parecia velho para sempre e cada disparo rodava o graphify
|
|
22
|
-
* de novo (~80 s no MORPH_GHLBrain). Rodada com exit 0 no HEAD H vale como
|
|
23
|
-
* grafo em dia em H: o 0.9.50 sai 1 quando recusa escrever (`_check_shrink`,
|
|
24
|
-
* grafo ilegível), e "untouched" sai 0.
|
|
25
|
-
*
|
|
26
|
-
* As regras do `.graphifyignore` entram por CONTEÚDO, nunca pelo relógio: o
|
|
27
|
-
* registro guarda o hash das regras sob as quais o grafo EM DISCO foi
|
|
28
|
-
* extraído (uma rodada que falhou não o muda), e um hash diferente do das
|
|
29
|
-
* regras que valem hoje é grafo velho — numa worktree sem arquivo próprio,
|
|
30
|
-
* as da raiz, que são as que o `graph refresh` usa ali. Comentário, linha
|
|
31
|
-
* em branco, BOM e CRLF não contam — trocar o cabeçalho não pede refresh.
|
|
32
|
-
* Grafo sem registro das regras (anterior a este estado) com regras valendo é
|
|
33
|
-
* `rules-unknown`: um refresh grava o registro e encerra.
|
|
34
|
-
*
|
|
35
|
-
* Fail-open: nada aqui lança.
|
|
36
|
-
*
|
|
37
|
-
* @module graph-freshness
|
|
38
|
-
*/
|
|
1
|
+
// Regra ÚNICA de "grafo em dia". O registro da rodada existe porque o `graphify
|
|
2
|
+
// update` não recarimba `built_at_commit` quando a topologia não muda; as regras
|
|
3
|
+
// do ignore entram por CONTEÚDO, nunca pelo relógio. Fail-open: nada aqui lança.
|
|
39
4
|
|
|
40
5
|
import { createHash } from 'crypto';
|
|
41
6
|
import { execFileSync } from 'child_process';
|
|
@@ -50,19 +15,12 @@ export const GRAPH_DIR = 'graphify-out';
|
|
|
50
15
|
export const GRAPHIFY_IGNORE = '.graphifyignore';
|
|
51
16
|
const STATE_FILE = 'graph-refresh-state.json';
|
|
52
17
|
|
|
53
|
-
/** @param {string} tree */
|
|
54
18
|
export function graphFile(tree) {
|
|
55
19
|
return join(tree, GRAPH_DIR, 'graph.json');
|
|
56
20
|
}
|
|
57
21
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* a leitura do fim do arquivo evita parsear 18 MB; a leitura inteira fica de
|
|
61
|
-
* reserva para qualquer outra forma.
|
|
62
|
-
*
|
|
63
|
-
* @param {string} tree
|
|
64
|
-
* @returns {string|null}
|
|
65
|
-
*/
|
|
22
|
+
// O graphify grava `built_at_commit` como a ÚLTIMA chave: ler o fim evita parsear
|
|
23
|
+
// o grafo inteiro, que fica de reserva para outra forma.
|
|
66
24
|
export function readBuiltAtCommit(tree) {
|
|
67
25
|
const file = graphFile(tree);
|
|
68
26
|
let fd;
|
|
@@ -86,10 +44,6 @@ export function readBuiltAtCommit(tree) {
|
|
|
86
44
|
}
|
|
87
45
|
}
|
|
88
46
|
|
|
89
|
-
/**
|
|
90
|
-
* @param {string} tree
|
|
91
|
-
* @returns {string|null}
|
|
92
|
-
*/
|
|
93
47
|
export function readHead(tree) {
|
|
94
48
|
try {
|
|
95
49
|
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
@@ -100,14 +54,7 @@ export function readHead(tree) {
|
|
|
100
54
|
}
|
|
101
55
|
}
|
|
102
56
|
|
|
103
|
-
|
|
104
|
-
* As REGRAS de um `.graphifyignore`, sem o que o graphify não lê: comentário,
|
|
105
|
-
* linha em branco, espaço no fim, BOM e CRLF. A ordem fica — no gitignore ela
|
|
106
|
-
* muda o resultado.
|
|
107
|
-
*
|
|
108
|
-
* @param {string} text
|
|
109
|
-
* @returns {string}
|
|
110
|
-
*/
|
|
57
|
+
// A ordem fica: no gitignore ela muda o resultado.
|
|
111
58
|
export function ignoreRules(text) {
|
|
112
59
|
return String(text ?? '')
|
|
113
60
|
.replace(/^\uFEFF/, '')
|
|
@@ -117,37 +64,22 @@ export function ignoreRules(text) {
|
|
|
117
64
|
.join('\n');
|
|
118
65
|
}
|
|
119
66
|
|
|
120
|
-
/**
|
|
121
|
-
* @param {string|null} text
|
|
122
|
-
* @returns {string|null} null quando não há arquivo
|
|
123
|
-
*/
|
|
124
67
|
export function rulesHash(text) {
|
|
125
68
|
if (text === null || text === undefined) return null;
|
|
126
69
|
return createHash('sha256').update(ignoreRules(text)).digest('hex').slice(0, 16);
|
|
127
70
|
}
|
|
128
71
|
|
|
129
|
-
/**
|
|
130
|
-
* A árvore principal do repositório — a única que o refresh em background
|
|
131
|
-
* toca. `null` fora de um repositório e quando o common dir não é o `.git` de
|
|
132
|
-
* uma árvore (repositório bare, `--separate-git-dir`): aí não há raiz.
|
|
133
|
-
*
|
|
134
|
-
* @param {string} dir - qualquer diretório do repositório, raiz ou worktree
|
|
135
|
-
* @returns {string|null}
|
|
136
|
-
*/
|
|
72
|
+
/** `null` também quando o common dir não é o `.git` de uma árvore (bare, `--separate-git-dir`). */
|
|
137
73
|
export function primaryTree(dir) {
|
|
138
74
|
const common = dir ? resolveGitCommonDir(dir) : null;
|
|
139
75
|
if (!common || basename(common) !== '.git') return null;
|
|
140
76
|
return dirname(common);
|
|
141
77
|
}
|
|
142
78
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
* durante a rodada. Comentário não muda as regras nem o hash, e é por ele que a
|
|
146
|
-
* sobra de uma rodada cujo processo morreu no meio é reconhecida.
|
|
147
|
-
*/
|
|
79
|
+
// 1ª linha da cópia temporária: reconhece a sobra de uma rodada que morreu no
|
|
80
|
+
// meio sem mudar as regras nem o hash.
|
|
148
81
|
export const TEMP_IGNORE_MARKER = '# morph-spec: cópia temporária do graph refresh, apagada ao fim da rodada';
|
|
149
82
|
|
|
150
|
-
/** @param {string|null} text */
|
|
151
83
|
export function isTemporaryIgnore(text) {
|
|
152
84
|
return String(text ?? '').replace(/^/, '').split(/\r?\n/, 1)[0] === TEMP_IGNORE_MARKER;
|
|
153
85
|
}
|
|
@@ -160,14 +92,8 @@ function readText(path) {
|
|
|
160
92
|
}
|
|
161
93
|
}
|
|
162
94
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
* numa worktree sem um (ou só com a sobra temporária), o da raiz, que é o que o
|
|
166
|
-
* `graph refresh` põe no lugar durante a rodada.
|
|
167
|
-
*
|
|
168
|
-
* @param {string} tree
|
|
169
|
-
* @returns {string|null} null quando nenhum arquivo vale
|
|
170
|
-
*/
|
|
95
|
+
// Numa worktree sem arquivo próprio valem as regras da raiz: é o que o
|
|
96
|
+
// `graph refresh` põe no lugar durante a rodada.
|
|
171
97
|
export function ignoreTextOf(tree) {
|
|
172
98
|
const own = readText(join(tree, GRAPHIFY_IGNORE));
|
|
173
99
|
if (own !== null && !isTemporaryIgnore(own)) return own;
|
|
@@ -177,37 +103,18 @@ export function ignoreTextOf(tree) {
|
|
|
177
103
|
return inherited !== null && !isTemporaryIgnore(inherited) ? inherited : null;
|
|
178
104
|
}
|
|
179
105
|
|
|
180
|
-
/**
|
|
181
|
-
* @param {string} tree
|
|
182
|
-
* @returns {string|null} null quando nenhum `.graphifyignore` vale para a árvore
|
|
183
|
-
*/
|
|
184
106
|
export function ignoreRulesHash(tree) {
|
|
185
107
|
return rulesHash(ignoreTextOf(tree));
|
|
186
108
|
}
|
|
187
109
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
* `.graphifyignore` temporário: ela é só uma cópia das regras da raiz, e o
|
|
191
|
-
* `?? .graphifyignore` que deixa fazia o `finish` e o
|
|
192
|
-
* `worktree remove --if-clean` recusarem. Só lê — quem confere a árvore chama
|
|
193
|
-
* isto antes das recusas, e `dropLeftoverIgnore` só depois delas.
|
|
194
|
-
*
|
|
195
|
-
* @param {string} tree
|
|
196
|
-
* @param {string[]} lines
|
|
197
|
-
* @returns {string[]}
|
|
198
|
-
*/
|
|
110
|
+
// Só lê: quem confere a árvore chama isto antes das recusas, e
|
|
111
|
+
// `dropLeftoverIgnore` só depois delas.
|
|
199
112
|
export function withoutLeftoverIgnore(tree, lines) {
|
|
200
113
|
const leftover = `?? ${GRAPHIFY_IGNORE}`;
|
|
201
114
|
if (!lines.includes(leftover) || !isTemporaryIgnore(readText(join(tree, GRAPHIFY_IGNORE)))) return lines;
|
|
202
115
|
return lines.filter((line) => line !== leftover);
|
|
203
116
|
}
|
|
204
117
|
|
|
205
|
-
/**
|
|
206
|
-
* Apaga a sobra do `.graphifyignore` temporário, reconhecida pelo marcador.
|
|
207
|
-
*
|
|
208
|
-
* @param {string} tree
|
|
209
|
-
* @returns {boolean} apagou
|
|
210
|
-
*/
|
|
211
118
|
export function dropLeftoverIgnore(tree) {
|
|
212
119
|
const path = join(tree, GRAPHIFY_IGNORE);
|
|
213
120
|
if (!isTemporaryIgnore(readText(path))) return false;
|
|
@@ -219,21 +126,12 @@ export function dropLeftoverIgnore(tree) {
|
|
|
219
126
|
}
|
|
220
127
|
}
|
|
221
128
|
|
|
222
|
-
|
|
223
|
-
* Chave de uma árvore no registro. No win32 o caminho não distingue caixa, e
|
|
224
|
-
* `R:\x` e `r:\x` são a mesma árvore.
|
|
225
|
-
*
|
|
226
|
-
* @param {string} tree
|
|
227
|
-
*/
|
|
129
|
+
// No win32 o caminho não distingue caixa.
|
|
228
130
|
export function treeKey(tree) {
|
|
229
131
|
const path = resolve(tree);
|
|
230
132
|
return process.platform === 'win32' ? path.toLowerCase() : path;
|
|
231
133
|
}
|
|
232
134
|
|
|
233
|
-
/**
|
|
234
|
-
* @param {string} dir - qualquer diretório do repositório
|
|
235
|
-
* @returns {string|null}
|
|
236
|
-
*/
|
|
237
135
|
export function refreshStatePath(dir) {
|
|
238
136
|
const common = dir ? resolveGitCommonDir(dir) : null;
|
|
239
137
|
return common ? join(common, 'morph-spec', STATE_FILE) : null;
|
|
@@ -248,10 +146,7 @@ function readState(path) {
|
|
|
248
146
|
}
|
|
249
147
|
}
|
|
250
148
|
|
|
251
|
-
/**
|
|
252
|
-
* @param {string} tree
|
|
253
|
-
* @returns {{head: string|null, exit: number|null, ignoreHash?: string|null, at: string, unavailable?: boolean, seeded?: boolean}|null}
|
|
254
|
-
*/
|
|
149
|
+
/** @returns {{head: string|null, exit: number|null, ignoreHash?: string|null, at: string, unavailable?: boolean, seeded?: boolean}|null} */
|
|
255
150
|
export function readRefreshRecord(tree) {
|
|
256
151
|
const path = refreshStatePath(tree);
|
|
257
152
|
if (!path) return null;
|
|
@@ -263,15 +158,7 @@ function pause(ms) {
|
|
|
263
158
|
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* sem espera */ }
|
|
264
159
|
}
|
|
265
160
|
|
|
266
|
-
|
|
267
|
-
* Grava o registro de `tree` (substitui o anterior). Por tmp + rename: quem lê
|
|
268
|
-
* no meio vê o arquivo velho ou o novo, nunca meio. As árvores que não existem
|
|
269
|
-
* mais saem na mesma escrita — uma worktree removida não deixa lixo.
|
|
270
|
-
*
|
|
271
|
-
* @param {string} tree
|
|
272
|
-
* @param {object} record
|
|
273
|
-
* @returns {object|null} o registro gravado, ou null (sem git, disco)
|
|
274
|
-
*/
|
|
161
|
+
// As árvores que não existem mais saem na mesma escrita.
|
|
275
162
|
export function writeRefreshRecord(tree, record) {
|
|
276
163
|
const path = refreshStatePath(tree);
|
|
277
164
|
if (!path) return null;
|
|
@@ -286,8 +173,7 @@ export function writeRefreshRecord(tree, record) {
|
|
|
286
173
|
}
|
|
287
174
|
state[key] = entry;
|
|
288
175
|
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
|
|
289
|
-
// No win32 o rename sobre um arquivo aberto por outro leitor falha por um
|
|
290
|
-
// instante (EPERM/EBUSY); o registro vale umas tentativas.
|
|
176
|
+
// No win32 o rename sobre um arquivo aberto por outro leitor falha por um instante.
|
|
291
177
|
for (let attempt = 0; ; attempt++) {
|
|
292
178
|
try {
|
|
293
179
|
renameSync(tmp, path);
|
|
@@ -303,15 +189,8 @@ export function writeRefreshRecord(tree, record) {
|
|
|
303
189
|
}
|
|
304
190
|
}
|
|
305
191
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
* uma rodada que falhou não mexeu no grafo, e o hash descreve o grafo em
|
|
309
|
-
* disco, não a tentativa.
|
|
310
|
-
*
|
|
311
|
-
* @param {string} tree
|
|
312
|
-
* @param {{head: string|null, exit: number|null, ignoreHash: string|null, unavailable?: boolean}} run
|
|
313
|
-
* `head` e `ignoreHash` lidos ANTES da rodada — é o que o graphify leu
|
|
314
|
-
*/
|
|
192
|
+
// O hash das regras só muda com exit 0: descreve o grafo em disco, não a
|
|
193
|
+
// tentativa. `head` e `ignoreHash` são lidos ANTES da rodada.
|
|
315
194
|
export function recordRefreshRun(tree, { head, exit, ignoreHash, unavailable = false }) {
|
|
316
195
|
const previous = readRefreshRecord(tree);
|
|
317
196
|
let rules = {};
|
|
@@ -325,23 +204,7 @@ export function recordRefreshRun(tree, { head, exit, ignoreHash, unavailable = f
|
|
|
325
204
|
});
|
|
326
205
|
}
|
|
327
206
|
|
|
328
|
-
/**
|
|
329
|
-
* O frescor do grafo de `tree`.
|
|
330
|
-
*
|
|
331
|
-
* missing sem graph.json
|
|
332
|
-
* unknown sem git, ou sem carimbo nem registro que diga o commit
|
|
333
|
-
* stale atrás do HEAD (`reason` ausente), ou em dia no commit mas com as
|
|
334
|
-
* regras do ignore mudadas (`ignore-changed`) ou desconhecidas
|
|
335
|
-
* (`rules-unknown`)
|
|
336
|
-
* fresh carimbo = HEAD, ou rodada com exit 0 neste HEAD — e as regras
|
|
337
|
-
* que valem para a árvore são as do grafo (`ignoreTextOf`; nenhum
|
|
338
|
-
* arquivo valendo não afirma nada)
|
|
339
|
-
*
|
|
340
|
-
* @param {string} tree - o diretório que contém `graphify-out/`
|
|
341
|
-
* @param {{graph?: object, head?: string|null}} [known] o que o chamador já
|
|
342
|
-
* leu: o grafo parseado (o carimbo sai dele) e o HEAD
|
|
343
|
-
* @returns {{state: 'missing'|'unknown'|'stale'|'fresh', built: string|null, head: string|null, reason?: 'ignore-changed'|'rules-unknown', record: object|null}}
|
|
344
|
-
*/
|
|
207
|
+
/** @returns {{state: 'missing'|'unknown'|'stale'|'fresh', built: string|null, head: string|null, reason?: 'ignore-changed'|'rules-unknown', record: object|null}} */
|
|
345
208
|
export function graphFreshness(tree, { graph, head } = {}) {
|
|
346
209
|
if (!graph && !existsSync(graphFile(tree))) {
|
|
347
210
|
return { state: 'missing', built: null, head: null, record: null };
|
|
@@ -363,15 +226,8 @@ export function graphFreshness(tree, { graph, head } = {}) {
|
|
|
363
226
|
return { state: 'fresh', ...base };
|
|
364
227
|
}
|
|
365
228
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
* FALHOU neste mesmo HEAD — essa só se repete quando o HEAD andar (ou por
|
|
369
|
-
* `morph-spec graph refresh`, que é explícito). "graphify ausente" não conta
|
|
370
|
-
* como falha: instalado, ele roda no próximo disparo.
|
|
371
|
-
*
|
|
372
|
-
* @param {{state: string, head: string|null, record: object|null}} freshness
|
|
373
|
-
* @returns {boolean}
|
|
374
|
-
*/
|
|
229
|
+
// Uma rodada que FALHOU neste HEAD só se repete quando o HEAD andar;
|
|
230
|
+
// "graphify ausente" não conta como falha.
|
|
375
231
|
export function refreshPending(freshness) {
|
|
376
232
|
if (freshness?.state !== 'stale') return false;
|
|
377
233
|
const r = freshness.record;
|
|
@@ -1,44 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Quem dispara: o elo `graph-refresh-kick` (PostToolUse, depois de um comando
|
|
5
|
-
* Bash ou PowerShell com git/gh/morph-spec) e o SessionStart
|
|
6
|
-
* (`worktree-context`). Os dois chamam `kickGraphRefresh`, que só olha o disco
|
|
7
|
-
* e solta este mesmo arquivo como processo DESTACADO (`--run <raiz>`): quem
|
|
8
|
-
* dispara nunca espera o graphify, que custa ~80 s no MORPH_GHLBrain mesmo sem
|
|
9
|
-
* mudança nenhuma.
|
|
10
|
-
*
|
|
11
|
-
* Só a raiz. O processo destacado segura o diretório em que roda, e no win32
|
|
12
|
-
* um diretório em uso não se apaga: com o refresh de uma worktree correndo, o
|
|
13
|
-
* `finish` e o `worktree remove` falhavam com "Permission denied" e deixavam a
|
|
14
|
-
* worktree meio removida. Por isso o worker nasce com `cwd` no git common dir,
|
|
15
|
-
* nunca recebe o caminho de uma worktree, e o `graphify update` roda só na
|
|
16
|
-
* raiz — que nenhum `finish` remove. A worktree nasce com o grafo semeado da
|
|
17
|
-
* raiz (`seedGraphFromRoot`) e, dali em diante, é atualizada por
|
|
18
|
-
* `morph-spec graph refresh`, em primeiro plano.
|
|
19
|
-
*
|
|
20
|
-
* Por que não o hook git do próprio graphify (`graphify hook install`), medido
|
|
21
|
-
* no 0.9.50:
|
|
22
|
-
* - ele sai sem fazer nada numa worktree vinculada (compara `--git-dir` com
|
|
23
|
-
* `--git-common-dir`);
|
|
24
|
-
* - não instala `post-merge`: merge, fast-forward e `git pull` — o caminho
|
|
25
|
-
* pelo qual toda feature volta para a raiz — nunca rebuildam;
|
|
26
|
-
* - o rebuild dele só re-extrai `git diff HEAD~1 HEAD`; um commit depois de
|
|
27
|
-
* um fast-forward carimba `built_at_commit` = HEAD num grafo SEM os
|
|
28
|
-
* arquivos que o merge trouxe — "fresh" mentiroso.
|
|
29
|
-
* Aqui o critério é o da regra única (graph-freshness.js): o carimbo e o
|
|
30
|
-
* registro da última rodada contra o HEAD, e as regras do ignore por conteúdo.
|
|
31
|
-
*
|
|
32
|
-
* Um lock por REPOSITÓRIO (no git common dir) serializa o refresh em
|
|
33
|
-
* background com o `graph build|refresh` manual — cada `graphify update` usa
|
|
34
|
-
* todos os núcleos e os dois escreveriam o mesmo graph.json. O dono renova o
|
|
35
|
-
* carimbo do lock antes de cada rodada, e a checagem é refeita depois de
|
|
36
|
-
* soltá-lo, para não perder o commit que chegou enquanto ele era segurado.
|
|
37
|
-
*
|
|
38
|
-
* Fail-open em tudo: sem git, sem graphify ou sem grafo, nada acontece.
|
|
39
|
-
*
|
|
40
|
-
* @module graph-refresh
|
|
41
|
-
*/
|
|
1
|
+
// Só a raiz: o worker destacado nasce com `cwd` no git common dir, porque no win32
|
|
2
|
+
// um diretório em uso não se apaga e o `finish` de uma worktree falhava no meio.
|
|
3
|
+
// A worktree anda por `morph-spec graph refresh`, em primeiro plano.
|
|
42
4
|
|
|
43
5
|
import { spawn, spawnSync } from 'child_process';
|
|
44
6
|
import {
|
|
@@ -58,14 +20,8 @@ import {
|
|
|
58
20
|
|
|
59
21
|
export { primaryTree };
|
|
60
22
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* `.graphify_root` guarda o caminho ABSOLUTO da raiz, e `cache/` é indexado
|
|
64
|
-
* pelo mtime da árvore de origem — medido: copiado, não poupou re-extração.
|
|
65
|
-
* O `.graphifyignore` também não vem: a worktree lê o do próprio checkout, e
|
|
66
|
-
* pôr `/.graphifyignore` no exclude do common dir escondia também o da RAIZ
|
|
67
|
-
* ainda não versionado (o `git add` dele passava a ser recusado).
|
|
68
|
-
*/
|
|
23
|
+
// Ficam fora de propósito: `.graphify_root` guarda o caminho absoluto da raiz, `cache/`
|
|
24
|
+
// é indexado pelo mtime da origem, e excluir `.graphifyignore` esconderia o da raiz.
|
|
69
25
|
const SEED_FILES = ['manifest.json', '.graphify_analysis.json', '.graphify_labels.json', 'graph.json'];
|
|
70
26
|
|
|
71
27
|
const EXCLUDE_MARKER = '# morph-spec: codebase graph inside worktrees (managed — see graph-refresh)';
|
|
@@ -77,10 +33,6 @@ const GRAPHIFY_TIMEOUT_MS = 20 * 60 * 1000;
|
|
|
77
33
|
const MAX_ROUNDS = 3;
|
|
78
34
|
const LOG_MAX_BYTES = 256 * 1024;
|
|
79
35
|
|
|
80
|
-
/**
|
|
81
|
-
* @param {string} dir
|
|
82
|
-
* @returns {boolean} `dir` está na árvore principal (o refresh automático a cobre)
|
|
83
|
-
*/
|
|
84
36
|
export function isPrimaryTree(dir) {
|
|
85
37
|
const top = dir ? workTreeTop(dir) : null;
|
|
86
38
|
const primary = primaryTree(dir);
|
|
@@ -92,7 +44,6 @@ function stateDir(dir) {
|
|
|
92
44
|
return common ? join(common, 'morph-spec') : null;
|
|
93
45
|
}
|
|
94
46
|
|
|
95
|
-
/** @param {string} dir */
|
|
96
47
|
export function refreshLockPath(dir) {
|
|
97
48
|
const d = stateDir(dir);
|
|
98
49
|
return d ? join(d, 'graph-refresh.lock') : null;
|
|
@@ -111,16 +62,8 @@ function mtimeOf(path) {
|
|
|
111
62
|
}
|
|
112
63
|
}
|
|
113
64
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* andamento". A idade conta da última renovação: um lock de processo morto,
|
|
117
|
-
* ou sem renovação há mais que o teto, é `live: false`, e o próximo refresh o
|
|
118
|
-
* recupera.
|
|
119
|
-
*
|
|
120
|
-
* @param {string} dir
|
|
121
|
-
* @param {{now?: number, isAlive?: (pid: number) => boolean}} [deps]
|
|
122
|
-
* @returns {{pid: number, since: string, renewed?: string, live: boolean}|null}
|
|
123
|
-
*/
|
|
65
|
+
// A idade conta da última renovação: processo morto ou sem renovação além do
|
|
66
|
+
// teto é `live: false`, e o próximo refresh recupera o lock.
|
|
124
67
|
export function readRefreshLock(dir, { now = Date.now(), isAlive = pidAlive } = {}) {
|
|
125
68
|
const path = refreshLockPath(dir);
|
|
126
69
|
if (!path) return null;
|
|
@@ -133,11 +76,6 @@ export function readRefreshLock(dir, { now = Date.now(), isAlive = pidAlive } =
|
|
|
133
76
|
}
|
|
134
77
|
}
|
|
135
78
|
|
|
136
|
-
/**
|
|
137
|
-
* @param {string} dir
|
|
138
|
-
* @param {{isAlive?: (pid: number) => boolean}} [deps]
|
|
139
|
-
* @returns {string|null} o caminho do lock adquirido, ou null (ocupado ou sem git)
|
|
140
|
-
*/
|
|
141
79
|
export function acquireRefreshLock(dir, deps = {}) {
|
|
142
80
|
const path = refreshLockPath(dir);
|
|
143
81
|
if (!path) return null;
|
|
@@ -162,13 +100,8 @@ export function acquireRefreshLock(dir, deps = {}) {
|
|
|
162
100
|
return null;
|
|
163
101
|
}
|
|
164
102
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
* cada rodada do graphify: uma posse que atravessa várias rodadas passaria do
|
|
168
|
-
* teto de idade e seria roubada no meio de uma escrita.
|
|
169
|
-
*
|
|
170
|
-
* @param {string|null} path
|
|
171
|
-
*/
|
|
103
|
+
// Uma posse que atravessa várias rodadas passaria do teto de idade e seria
|
|
104
|
+
// roubada no meio de uma escrita.
|
|
172
105
|
export function renewRefreshLock(path) {
|
|
173
106
|
if (!path) return;
|
|
174
107
|
try {
|
|
@@ -178,7 +111,6 @@ export function renewRefreshLock(path) {
|
|
|
178
111
|
} catch { /* solto por outro — o release confere de novo */ }
|
|
179
112
|
}
|
|
180
113
|
|
|
181
|
-
/** Solta o lock só se ainda for deste processo. */
|
|
182
114
|
export function releaseRefreshLock(path) {
|
|
183
115
|
if (!path) return;
|
|
184
116
|
try {
|
|
@@ -199,13 +131,7 @@ function appendRefreshLog(dir, entry) {
|
|
|
199
131
|
} catch { /* log é diagnóstico, nunca bloqueia */ }
|
|
200
132
|
}
|
|
201
133
|
|
|
202
|
-
|
|
203
|
-
* `graphify` no PATH. No win32 o libuv só acha `.exe` sem shell; `.cmd`/`.bat`
|
|
204
|
-
* (o que um shim ou um dublê de teste instala) passa pelo shell.
|
|
205
|
-
*
|
|
206
|
-
* @param {NodeJS.ProcessEnv} [env]
|
|
207
|
-
* @returns {{file: string, shell: boolean}|null}
|
|
208
|
-
*/
|
|
134
|
+
// No win32 o libuv só acha `.exe` sem shell; `.cmd`/`.bat` passa pelo shell.
|
|
209
135
|
export function resolveGraphify(env = process.env) {
|
|
210
136
|
const names = process.platform === 'win32'
|
|
211
137
|
? ['graphify.exe', 'graphify.cmd', 'graphify.bat']
|
|
@@ -214,23 +140,13 @@ export function resolveGraphify(env = process.env) {
|
|
|
214
140
|
return file ? { file, shell: /\.(cmd|bat)$/i.test(file) } : null;
|
|
215
141
|
}
|
|
216
142
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
* background E do `graph build|refresh` manual: o mesmo binário resolvido e o
|
|
220
|
-
* mesmo `PYTHONHASHSEED=0` — é o que o hook do próprio graphify fixa para o
|
|
221
|
-
* clustering não trocar as comunidades de lugar a cada rodada.
|
|
222
|
-
*
|
|
223
|
-
* @param {string} tree
|
|
224
|
-
* @param {string[]} args
|
|
225
|
-
* @param {{env?: NodeJS.ProcessEnv, timeout?: number|null}} [opts] `timeout: null` = sem teto
|
|
226
|
-
* @returns {{status: number|null, output: string, unavailable?: boolean, error?: string}}
|
|
227
|
-
*/
|
|
143
|
+
// `PYTHONHASHSEED=0` impede o clustering de trocar as comunidades de lugar a cada
|
|
144
|
+
// rodada. `timeout: null` = sem teto.
|
|
228
145
|
export function runGraphify(tree, args, { env = process.env, timeout = GRAPHIFY_TIMEOUT_MS } = {}) {
|
|
229
146
|
const bin = resolveGraphify(env);
|
|
230
147
|
if (!bin) return { status: null, unavailable: true, output: '' };
|
|
231
|
-
// Nunca com cwd na árvore: o graphify lê o alvo pelo argumento
|
|
232
|
-
//
|
|
233
|
-
// prende no win32 até terminar — o finish e o worktree remove falhavam.
|
|
148
|
+
// Nunca com cwd na árvore: o graphify lê o alvo pelo argumento, e um órfão com
|
|
149
|
+
// cwd numa worktree a prende no win32 até terminar.
|
|
234
150
|
const opts = {
|
|
235
151
|
cwd: resolveGitCommonDir(tree) ?? tree,
|
|
236
152
|
env: { ...env, PYTHONHASHSEED: '0' },
|
|
@@ -247,20 +163,8 @@ export function runGraphify(tree, args, { env = process.env, timeout = GRAPHIFY_
|
|
|
247
163
|
return { status: r.status, output, ...(r.error ? { error: r.error.code } : {}) };
|
|
248
164
|
}
|
|
249
165
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
* raiz, então o grafo copiado ou já é o do HEAD dela ou fica a um
|
|
253
|
-
* `morph-spec graph refresh` de distância — nunca a uma extração do zero.
|
|
254
|
-
*
|
|
255
|
-
* Só `graphify-out/` (no exclude do common dir, senão a worktree fica suja
|
|
256
|
-
* para o `finish`). O registro da worktree copia o que se SABE do grafo da
|
|
257
|
-
* raiz: o commit em que ele está em dia e as regras com que foi extraído —
|
|
258
|
-
* nada inventado. Raiz sem registro das regras deixa a semente sem elas.
|
|
259
|
-
*
|
|
260
|
-
* @param {string} tree
|
|
261
|
-
* @param {string} root
|
|
262
|
-
* @returns {{seeded: boolean, reason?: string}}
|
|
263
|
-
*/
|
|
166
|
+
// O registro da semente copia só o que se SABE do grafo da raiz; `graphify-out/`
|
|
167
|
+
// vai para o exclude do common dir, senão a worktree fica suja para o `finish`.
|
|
264
168
|
export function seedGraphFromRoot(tree, root) {
|
|
265
169
|
if (!tree || !root || treeKey(tree) === treeKey(root)) return { seeded: false, reason: 'is-root' };
|
|
266
170
|
if (existsSync(graphFile(tree))) return { seeded: false, reason: 'present' };
|
|
@@ -272,8 +176,7 @@ export function seedGraphFromRoot(tree, root) {
|
|
|
272
176
|
for (const name of SEED_FILES) {
|
|
273
177
|
const src = join(root, GRAPH_DIR, name);
|
|
274
178
|
if (!existsSync(src)) continue;
|
|
275
|
-
//
|
|
276
|
-
// semeadura completa, e quem lê no meio nunca vê meio arquivo.
|
|
179
|
+
// graph.json por último: a presença dele é o sinal de semeadura completa.
|
|
277
180
|
const tmp = join(dst, `${name}.seed.${process.pid}`);
|
|
278
181
|
copyFileSync(src, tmp);
|
|
279
182
|
renameSync(tmp, join(dst, name));
|
|
@@ -294,13 +197,6 @@ export function seedGraphFromRoot(tree, root) {
|
|
|
294
197
|
}
|
|
295
198
|
}
|
|
296
199
|
|
|
297
|
-
/**
|
|
298
|
-
* Semeia a árvore de `dir` a partir da raiz, se ela for uma worktree sem
|
|
299
|
-
* grafo — o que o SessionStart faz por uma worktree criada antes da semeadura
|
|
300
|
-
* existir.
|
|
301
|
-
*
|
|
302
|
-
* @param {string} dir
|
|
303
|
-
*/
|
|
304
200
|
export function seedFromPrimary(dir) {
|
|
305
201
|
const tree = dir ? workTreeTop(dir) : null;
|
|
306
202
|
const root = primaryTree(dir);
|
|
@@ -308,17 +204,8 @@ export function seedFromPrimary(dir) {
|
|
|
308
204
|
return seedGraphFromRoot(tree, root);
|
|
309
205
|
}
|
|
310
206
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
* chamam). Roda `graphify update` na raiz do repositório de `dir` enquanto o
|
|
314
|
-
* grafo dela precisar (`refreshPending`), até `MAX_ROUNDS` rodadas — um
|
|
315
|
-
* commit que chega no meio pede outra. Cada rodada vai para o registro
|
|
316
|
-
* (graph-freshness.js) e para o log.
|
|
317
|
-
*
|
|
318
|
-
* @param {string} dir - qualquer diretório do repositório; o alvo é sempre a raiz
|
|
319
|
-
* @param {{env?: NodeJS.ProcessEnv, runUpdate?: (tree: string) => {status: number|null, unavailable?: boolean, output?: string}}} [deps]
|
|
320
|
-
* @returns {{ran: boolean, locked?: boolean, reason?: string, results: object[]}}
|
|
321
|
-
*/
|
|
207
|
+
// A checagem é refeita depois de soltar o lock: um commit que chegou enquanto
|
|
208
|
+
// ele era segurado pede outra rodada.
|
|
322
209
|
export function runRefresh(dir, { env = process.env, runUpdate = (tree) => runGraphify(tree, ['update', tree], { env }) } = {}) {
|
|
323
210
|
const root = primaryTree(dir);
|
|
324
211
|
if (!root) return { ran: false, reason: 'no-primary-tree', results: [] };
|
|
@@ -357,23 +244,8 @@ export function runRefresh(dir, { env = process.env, runUpdate = (tree) => runGr
|
|
|
357
244
|
return { ran: results.length > 0, results };
|
|
358
245
|
}
|
|
359
246
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
* árvore do repositório. Só olha o disco, sem subprocesso: sem grafo na raiz
|
|
363
|
-
* não há o que refrescar; grafo que não pede refresh no HEAD lido do disco não
|
|
364
|
-
* paga um processo por comando git; com o graphify registrado como ausente e
|
|
365
|
-
* ainda fora do PATH, não há quem rode. HEAD que o disco não responde (reftable,
|
|
366
|
-
* branch sem commit) fica para o worker, que confere de novo sob o lock.
|
|
367
|
-
*
|
|
368
|
-
* O worker nasce com `cwd` no git common dir e só recebe a raiz: nenhum
|
|
369
|
-
* processo destacado segura o diretório de uma worktree.
|
|
370
|
-
*
|
|
371
|
-
* `MORPH_GRAPH_AUTO_REFRESH=0` desliga (CI, máquina sem folga de CPU).
|
|
372
|
-
*
|
|
373
|
-
* @param {string} dir
|
|
374
|
-
* @param {{spawnFn?: typeof spawn, env?: NodeJS.ProcessEnv}} [deps]
|
|
375
|
-
* @returns {{kicked: boolean, reason?: string, pid?: number}}
|
|
376
|
-
*/
|
|
247
|
+
// Só olha o disco: roda a cada comando git do agente. HEAD que o disco não
|
|
248
|
+
// responde (reftable, branch sem commit) fica para o worker, sob o lock.
|
|
377
249
|
export function kickGraphRefresh(dir, { spawnFn = spawn, env = process.env } = {}) {
|
|
378
250
|
if (env.MORPH_GRAPH_AUTO_REFRESH === '0') return { kicked: false, reason: 'disabled' };
|
|
379
251
|
const root = primaryTree(dir);
|
|
@@ -95,13 +95,8 @@ function kindOfTokens(tokens) {
|
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
* each `;`/`&&`/`||`/`|`/newline segment, so `morph-spec heavy run -- dotnet test` and `echo "dotnet build"`
|
|
101
|
-
* are not heavy: the first runs under its own slot, the second runs nothing.
|
|
102
|
-
* @param {string} command
|
|
103
|
-
* @returns {string|null}
|
|
104
|
-
*/
|
|
98
|
+
// Judges only each segment's executable: `morph-spec heavy run -- dotnet test` already holds its
|
|
99
|
+
// own slot, and `echo "dotnet build"` runs nothing.
|
|
105
100
|
export function heavyKind(command) {
|
|
106
101
|
for (const segment of splitSegments(command)) {
|
|
107
102
|
const tokens = skipWrappers(tokenize(segment.replace(/^[\s({]+/, '').replace(/[\s)}]+$/, '')));
|
|
@@ -111,11 +106,8 @@ export function heavyKind(command) {
|
|
|
111
106
|
return null;
|
|
112
107
|
}
|
|
113
108
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* The lease has no live process to watch, so this is the only thing that frees it after a denied or
|
|
117
|
-
* interrupted call.
|
|
118
|
-
*/
|
|
109
|
+
// The lease has no live process to watch: this TTL is the only thing that frees it after a
|
|
110
|
+
// denied or interrupted call.
|
|
119
111
|
const BASH_DEFAULT_TIMEOUT_MS = 2 * 60_000;
|
|
120
112
|
const BASH_MAX_TIMEOUT_MS = 10 * 60_000;
|
|
121
113
|
const LEASE_SLACK_MS = 30_000;
|