@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,44 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Two Claude Code sessions worked the SAME feature at the same time without
|
|
5
|
-
* seeing each other: one finished it and removed the worktree out from under
|
|
6
|
-
* the other's running sub-agent. A complete recap.md was discarded, a sub-agent
|
|
7
|
-
* wasted, and a decision taken on state that was already obsolete. There was no
|
|
8
|
-
* lock, no claim, no heartbeat — not even a warning.
|
|
9
|
-
*
|
|
10
|
-
* DELIBERATELY ADVISORY. This is not a lock: nothing here ever blocks a
|
|
11
|
-
* command. The framework is fail-open by principle, and a strong lock left
|
|
12
|
-
* behind by a crashed session would strand a feature for no gain. Being
|
|
13
|
-
* VISIBLE is the whole fix.
|
|
14
|
-
*
|
|
15
|
-
* Location: `<git-common-dir>/morph-spec/claims.json`, beside the fleet's
|
|
16
|
-
* env-registry, for the same three reasons — it can never dirty the `git
|
|
17
|
-
* status` the finish gates read, it survives `git clean -fdx`, and the COMMON
|
|
18
|
-
* dir is the one place every linked worktree of the repo shares. That last one
|
|
19
|
-
* is decisive here: in the observed incident one session sat inside the
|
|
20
|
-
* worktree and the other in the primary root, so a claim written to the
|
|
21
|
-
* branch-local `feature.json` would have been invisible to exactly the session
|
|
22
|
-
* that needed to read it.
|
|
23
|
-
*
|
|
24
|
-
* Writes are tmp+rename (torn file impossible); a corrupt registry reads as
|
|
25
|
-
* empty — the cost is a lost warning, never a crash.
|
|
26
|
-
*/
|
|
1
|
+
// ADVISORY, never a lock: a lock left by a crashed session would strand a feature.
|
|
2
|
+
// Lives in the git COMMON dir, the one place every worktree of the repo shares.
|
|
27
3
|
|
|
28
4
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
29
5
|
import { dirname, join } from 'path';
|
|
30
6
|
|
|
31
|
-
/**
|
|
32
|
-
* A claim with no heartbeat for this long is considered dead. The heartbeat
|
|
33
|
-
* fires on tool activity, so this is really "how long a session may sit idle
|
|
34
|
-
* before its claim stops warning others".
|
|
35
|
-
*/
|
|
36
7
|
export const CLAIM_TTL_MS = 30 * 60 * 1000;
|
|
37
8
|
|
|
38
|
-
/** Minimum gap between heartbeat WRITES. The heartbeat runs on every tool call. */
|
|
39
9
|
export const HEARTBEAT_THROTTLE_MS = 60 * 1000;
|
|
40
10
|
|
|
41
|
-
/** Claim file path for a repo's common git dir. */
|
|
42
11
|
export function claimsPath(gitCommonDir) {
|
|
43
12
|
return join(gitCommonDir, 'morph-spec', 'claims.json');
|
|
44
13
|
}
|
|
@@ -60,23 +29,13 @@ function writeRegistry(gitCommonDir, registry) {
|
|
|
60
29
|
renameSync(tmp, path);
|
|
61
30
|
}
|
|
62
31
|
|
|
63
|
-
/** Is this claim still within its TTL? A missing/unparseable heartbeat is dead. */
|
|
64
32
|
export function isLive(claim, now = Date.now()) {
|
|
65
33
|
const beat = Date.parse(claim?.heartbeatAt ?? '');
|
|
66
34
|
return Number.isFinite(beat) && now - beat < CLAIM_TTL_MS;
|
|
67
35
|
}
|
|
68
36
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
*
|
|
72
|
-
* A claim held by ANOTHER session is TAKEN OVER, not refused — advisory by
|
|
73
|
-
* design, and the caller has already been warned via `foreignClaim`. The
|
|
74
|
-
* takeover is recorded in `previousSessionId` so it is never silent.
|
|
75
|
-
*
|
|
76
|
-
* @param {string} feature
|
|
77
|
-
* @param {{ gitCommonDir: string, sessionId: string, pid?: number, path?: string }} opts
|
|
78
|
-
* @returns {object} the stored claim
|
|
79
|
-
*/
|
|
37
|
+
// Another session's claim is TAKEN OVER, not refused; `previousSessionId` keeps
|
|
38
|
+
// the takeover visible.
|
|
80
39
|
export function acquireClaim(feature, { gitCommonDir, sessionId, pid = process.pid, path = null }) {
|
|
81
40
|
const registry = readRegistry(gitCommonDir);
|
|
82
41
|
const previous = registry.claims[feature];
|
|
@@ -95,17 +54,7 @@ export function acquireClaim(feature, { gitCommonDir, sessionId, pid = process.p
|
|
|
95
54
|
return claim;
|
|
96
55
|
}
|
|
97
56
|
|
|
98
|
-
|
|
99
|
-
* Refresh our own claim's heartbeat. Throttled — this runs on every tool call,
|
|
100
|
-
* and a write per call would be pure I/O waste.
|
|
101
|
-
*
|
|
102
|
-
* Never touches a claim owned by another session: a heartbeat is proof of life,
|
|
103
|
-
* and forging someone else's would defeat the TTL.
|
|
104
|
-
*
|
|
105
|
-
* @param {string} feature
|
|
106
|
-
* @param {{ gitCommonDir: string, sessionId: string }} opts
|
|
107
|
-
* @returns {boolean} true when a write actually happened
|
|
108
|
-
*/
|
|
57
|
+
// Never another session's claim: forging its heartbeat would defeat the TTL.
|
|
109
58
|
export function heartbeatClaim(feature, { gitCommonDir, sessionId }) {
|
|
110
59
|
const registry = readRegistry(gitCommonDir);
|
|
111
60
|
const claim = registry.claims[feature];
|
|
@@ -119,45 +68,13 @@ export function heartbeatClaim(feature, { gitCommonDir, sessionId }) {
|
|
|
119
68
|
return true;
|
|
120
69
|
}
|
|
121
70
|
|
|
122
|
-
/** Local, git-independent heartbeat pre-check marker — see `shouldHeartbeat`. */
|
|
123
71
|
function heartbeatMarkPath(cwd) {
|
|
124
72
|
return join(cwd, '.morph', 'logs', '.heartbeat-check.json');
|
|
125
73
|
}
|
|
126
74
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
* but that registry lives under the git COMMON dir — resolving it (even via
|
|
131
|
-
* the pure-FS `git-paths.js`, never mind the old `execSync('git rev-parse
|
|
132
|
-
* --git-common-dir')`) is work `telemetry-log.js` pays on every tool call.
|
|
133
|
-
* This answers "is it due" from a tiny marker colocated with the CALLER's own
|
|
134
|
-
* `.morph/logs/` (already known, no git lookup of any kind) so that on ~99%
|
|
135
|
-
* of calls NOTHING git-related happens at all — not the subprocess, not even
|
|
136
|
-
* the FS walk.
|
|
137
|
-
*
|
|
138
|
-
* Side effect: when it returns `true` (heartbeat IS due), it immediately
|
|
139
|
-
* stamps the marker — mirroring `heartbeatClaim`'s own check-and-write
|
|
140
|
-
* pattern — so the NEXT call within the throttle window short-circuits here
|
|
141
|
-
* even if the caller's subsequent git resolution then fails (not actually a
|
|
142
|
-
* git repo, say): a failed attempt should not be retried on every single tool
|
|
143
|
-
* call either.
|
|
144
|
-
*
|
|
145
|
-
* This is a SEPARATE, LOCAL throttle from `claims.json`'s own authoritative
|
|
146
|
-
* `heartbeatAt` (keyed by git-common-dir, enforced inside `heartbeatClaim`) —
|
|
147
|
-
* the two can drift by up to one throttle window with no consequence: a
|
|
148
|
-
* missed local pre-check only costs a claim heartbeat that gets written on
|
|
149
|
-
* the very next call, and claims are advisory to begin with.
|
|
150
|
-
*
|
|
151
|
-
* Fail-open: any read/write error is treated as "due" — a broken marker must
|
|
152
|
-
* never permanently suppress the actual (authoritative) heartbeat. Not
|
|
153
|
-
* tmp+rename like `claims.json` — this file is low-stakes (worst case from a
|
|
154
|
-
* torn write is corrupt JSON, which just reads back as "due" next time) and
|
|
155
|
-
* shared by nobody outside this one project tree.
|
|
156
|
-
*
|
|
157
|
-
* @param {string} feature
|
|
158
|
-
* @param {{ cwd: string, sessionId: string, now?: number }} opts
|
|
159
|
-
* @returns {boolean}
|
|
160
|
-
*/
|
|
75
|
+
// Local pre-check so most tool calls skip resolving the git common dir. Stamps
|
|
76
|
+
// the marker when due, so a failed attempt is not retried on every call; an
|
|
77
|
+
// unreadable marker counts as due.
|
|
161
78
|
export function shouldHeartbeat(feature, { cwd, sessionId, now = Date.now() }) {
|
|
162
79
|
const markPath = heartbeatMarkPath(cwd);
|
|
163
80
|
try {
|
|
@@ -167,27 +84,16 @@ export function shouldHeartbeat(feature, { cwd, sessionId, now = Date.now() }) {
|
|
|
167
84
|
if (Number.isFinite(at) && now - at < HEARTBEAT_THROTTLE_MS) return false;
|
|
168
85
|
}
|
|
169
86
|
} catch {
|
|
170
|
-
// missing/corrupt marker — treat as due
|
|
171
87
|
}
|
|
172
88
|
|
|
173
89
|
try {
|
|
174
90
|
mkdirSync(dirname(markPath), { recursive: true });
|
|
175
91
|
writeFileSync(markPath, JSON.stringify({ feature, sessionId, at: new Date(now).toISOString() }));
|
|
176
92
|
} catch {
|
|
177
|
-
// best-effort — a failed stamp only costs a repeated attempt next call
|
|
178
93
|
}
|
|
179
94
|
return true;
|
|
180
95
|
}
|
|
181
96
|
|
|
182
|
-
/**
|
|
183
|
-
* The LIVE claim on this feature held by a session that is not us — the thing
|
|
184
|
-
* worth warning about. Returns null when the feature is unclaimed, claimed by
|
|
185
|
-
* us, or claimed by a session whose heartbeat went stale.
|
|
186
|
-
*
|
|
187
|
-
* @param {string} feature
|
|
188
|
-
* @param {{ gitCommonDir: string, sessionId?: string }} opts
|
|
189
|
-
* @returns {object|null}
|
|
190
|
-
*/
|
|
191
97
|
export function foreignClaim(feature, { gitCommonDir, sessionId }) {
|
|
192
98
|
const claim = readRegistry(gitCommonDir).claims[feature];
|
|
193
99
|
if (!claim) return null;
|
|
@@ -195,14 +101,6 @@ export function foreignClaim(feature, { gitCommonDir, sessionId }) {
|
|
|
195
101
|
return isLive(claim) ? claim : null;
|
|
196
102
|
}
|
|
197
103
|
|
|
198
|
-
/**
|
|
199
|
-
* Drop a feature's claim. Called by the manual `morph-spec claim <f> --release`
|
|
200
|
-
* (recovering from a session that died without releasing) and by `finish`.
|
|
201
|
-
*
|
|
202
|
-
* @param {string} feature
|
|
203
|
-
* @param {{ gitCommonDir: string }} opts
|
|
204
|
-
* @returns {boolean} false when there was nothing to release
|
|
205
|
-
*/
|
|
206
104
|
export function releaseClaim(feature, { gitCommonDir }) {
|
|
207
105
|
const registry = readRegistry(gitCommonDir);
|
|
208
106
|
if (!registry.claims[feature]) return false;
|
|
@@ -211,14 +109,7 @@ export function releaseClaim(feature, { gitCommonDir }) {
|
|
|
211
109
|
return true;
|
|
212
110
|
}
|
|
213
111
|
|
|
214
|
-
|
|
215
|
-
* Every claim on record, live and stale, newest heartbeat first. `live` is
|
|
216
|
-
* computed, never stored — a stored flag would go wrong the moment nobody
|
|
217
|
-
* rewrites the file.
|
|
218
|
-
*
|
|
219
|
-
* @param {string} gitCommonDir
|
|
220
|
-
* @returns {object[]}
|
|
221
|
-
*/
|
|
112
|
+
// `live` is computed, never stored: a stored flag goes wrong once nobody rewrites the file.
|
|
222
113
|
export function listClaims(gitCommonDir) {
|
|
223
114
|
const now = Date.now();
|
|
224
115
|
return Object.values(readRegistry(gitCommonDir).claims)
|
|
@@ -226,7 +117,6 @@ export function listClaims(gitCommonDir) {
|
|
|
226
117
|
.sort((a, b) => String(b.heartbeatAt).localeCompare(String(a.heartbeatAt)));
|
|
227
118
|
}
|
|
228
119
|
|
|
229
|
-
/** Human-readable age of a claim's last heartbeat ("3 min", "2 h", "—"). */
|
|
230
120
|
export function claimAge(claim, now = Date.now()) {
|
|
231
121
|
const beat = Date.parse(claim?.heartbeatAt ?? '');
|
|
232
122
|
if (!Number.isFinite(beat)) return '—';
|
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Lives in shared/ (not inside a hook) on purpose: every core hook ends in a
|
|
5
|
-
* standalone tail that reads stdin and exits, so a hook importing a sibling
|
|
6
|
-
* hook for a helper would fire THAT tail in its own process — consuming the
|
|
7
|
-
* payload and exiting before the importer ever ran. block-raw-worktree-add
|
|
8
|
-
* and block-destructive-worktree-removal both read paths off these tokens,
|
|
9
|
-
* so they agree on what a quoted path is.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Split a shell-ish command into tokens, honoring single and double quotes.
|
|
14
|
-
* @param {string} command
|
|
15
|
-
* @returns {string[]}
|
|
16
|
-
*/
|
|
1
|
+
// Lives in shared/: a hook importing a sibling hook would fire that hook's
|
|
2
|
+
// stdin-reading tail in its own process.
|
|
3
|
+
|
|
17
4
|
export function tokenize(command) {
|
|
18
5
|
const tokens = [];
|
|
19
6
|
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
@@ -22,7 +9,6 @@ export function tokenize(command) {
|
|
|
22
9
|
return tokens;
|
|
23
10
|
}
|
|
24
11
|
|
|
25
|
-
/** Index of the first pipe (`|`, not `||` nor `>|`) outside quotes, or -1. */
|
|
26
12
|
function firstUnquotedPipe(command) {
|
|
27
13
|
const s = String(command || '');
|
|
28
14
|
let quote = null;
|
|
@@ -43,24 +29,11 @@ function firstUnquotedPipe(command) {
|
|
|
43
29
|
return -1;
|
|
44
30
|
}
|
|
45
31
|
|
|
46
|
-
/**
|
|
47
|
-
* Does the command pipe one process into another outside quotes? Without
|
|
48
|
-
* `pipefail` the shell reports the LAST command's status, so the exit code of
|
|
49
|
-
* `dotnet test … | grep …` is grep's, not the test run's.
|
|
50
|
-
* @param {string} command
|
|
51
|
-
* @returns {boolean}
|
|
52
|
-
*/
|
|
53
32
|
export function hasUnquotedPipe(command) {
|
|
54
33
|
return firstUnquotedPipe(command) >= 0;
|
|
55
34
|
}
|
|
56
35
|
|
|
57
|
-
/**
|
|
58
|
-
* The work a command does, without the filter that shapes its output:
|
|
59
|
-
* `dotnet test 2>&1 | tail -25` and `dotnet test | tail -20` run the same
|
|
60
|
-
* tests. Whitespace collapsed, stderr redirections dropped.
|
|
61
|
-
* @param {string} command
|
|
62
|
-
* @returns {string}
|
|
63
|
-
*/
|
|
36
|
+
/** The command without the pipe filter and stderr redirections that shape its output. */
|
|
64
37
|
export function producerOf(command) {
|
|
65
38
|
const s = String(command || '');
|
|
66
39
|
const pipe = firstUnquotedPipe(s);
|
|
@@ -70,11 +43,6 @@ export function producerOf(command) {
|
|
|
70
43
|
.trim();
|
|
71
44
|
}
|
|
72
45
|
|
|
73
|
-
/**
|
|
74
|
-
* The command split at `;`, newline and background `&` outside quotes, plus
|
|
75
|
-
* whether a background `&` was seen. `&&`, `||` and `>&`/`&>` redirections
|
|
76
|
-
* stay inside their segment.
|
|
77
|
-
*/
|
|
78
46
|
function topLevelSegments(command) {
|
|
79
47
|
const s = String(command || '');
|
|
80
48
|
const segments = [];
|
|
@@ -102,14 +70,7 @@ function topLevelSegments(command) {
|
|
|
102
70
|
|
|
103
71
|
const REPORTER = /^(?:echo|printf|true|:|exit\s+0)\b/;
|
|
104
72
|
|
|
105
|
-
/**
|
|
106
|
-
* Is the exit code of this command the exit code of the work it ran? The
|
|
107
|
-
* shell reports the last segment of a `;` sequence: `dotnet test > log;
|
|
108
|
-
* echo "exit=$?"` always exits 0, and so does `dotnet test || true`. A pipe
|
|
109
|
-
* without `pipefail` reports the filter, and a background `&` reports 0.
|
|
110
|
-
* @param {string} command
|
|
111
|
-
* @returns {boolean}
|
|
112
|
-
*/
|
|
73
|
+
/** True when the shell reports something other than the work's exit code (`; echo`, `|| true`, pipe, `&`). */
|
|
113
74
|
export function exitCodeMasked(command) {
|
|
114
75
|
const { segments, background } = topLevelSegments(command);
|
|
115
76
|
if (background) return true;
|
|
@@ -117,3 +78,166 @@ export function exitCodeMasked(command) {
|
|
|
117
78
|
const last = segments[segments.length - 1] || '';
|
|
118
79
|
return (segments.length > 1 && REPORTER.test(last)) || /\|\|/.test(last.replace(/"[^"]*"|'[^']*'/g, ''));
|
|
119
80
|
}
|
|
81
|
+
|
|
82
|
+
const WRITE_REDIRECT = /^(?:[1&])?>>?\|?/;
|
|
83
|
+
|
|
84
|
+
function quotedTokens(text) {
|
|
85
|
+
const tokens = [];
|
|
86
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
87
|
+
let m;
|
|
88
|
+
while ((m = re.exec(text)) !== null) {
|
|
89
|
+
tokens.push({ text: m[1] ?? m[2] ?? m[3], quoted: m[3] === undefined, single: m[2] !== undefined });
|
|
90
|
+
}
|
|
91
|
+
return tokens;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const CHAIN = /^(?:&&|\|\||\||\|&)$/;
|
|
95
|
+
const ASSIGNMENT = /^([A-Za-z_]\w*)=(.*)$/;
|
|
96
|
+
const REDIRECT = /^(?:\d|&)?[<>]/;
|
|
97
|
+
const SCRIPT_RUNNER = /^(?:python[\d.]*|py|node|bun|deno)$/;
|
|
98
|
+
|
|
99
|
+
function simpleCommands(head) {
|
|
100
|
+
const commands = [];
|
|
101
|
+
for (const segment of topLevelSegments(head).segments) {
|
|
102
|
+
let current = [];
|
|
103
|
+
for (const token of quotedTokens(segment)) {
|
|
104
|
+
if (!token.quoted && CHAIN.test(token.text)) { commands.push(current); current = []; } else current.push(token);
|
|
105
|
+
}
|
|
106
|
+
commands.push(current);
|
|
107
|
+
}
|
|
108
|
+
return commands.filter((c) => c.length > 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function positionals(args, takesValue) {
|
|
112
|
+
const out = [];
|
|
113
|
+
const values = [];
|
|
114
|
+
for (let i = 0; i < args.length; i++) {
|
|
115
|
+
const { text, quoted } = args[i];
|
|
116
|
+
if (!quoted && REDIRECT.test(text)) { if (/[<>]$/.test(text)) i++; continue; }
|
|
117
|
+
if (!quoted && text.startsWith('-') && text !== '-') {
|
|
118
|
+
if (takesValue(text) && i + 1 < args.length) values.push(args[++i].text);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
out.push(text);
|
|
122
|
+
}
|
|
123
|
+
return { files: out, values };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sedTargets(args) {
|
|
127
|
+
const inPlace = args.some(({ text, quoted }) => !quoted && (/^-[A-Za-z]*i/.test(text) || text.startsWith('--in-place')));
|
|
128
|
+
if (!inPlace) return [];
|
|
129
|
+
const { files, values } = positionals(args, (o) => /^-[A-Za-z]*[ef]$/.test(o) || o === '--expression' || o === '--file');
|
|
130
|
+
return values.length > 0 ? files : files.slice(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function perlTargets(args) {
|
|
134
|
+
if (!args.some(({ text, quoted }) => !quoted && /^-[A-Za-z]*i/.test(text))) return [];
|
|
135
|
+
const { files, values } = positionals(args, (o) => /^-[A-Za-z.]*[eE]$/.test(o));
|
|
136
|
+
return values.length > 0 ? files : files.slice(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function copyTargets(args) {
|
|
140
|
+
if (args.some(({ text }) => text === '-t' || text.startsWith('--target-directory'))) return [];
|
|
141
|
+
const { files } = positionals(args, () => false);
|
|
142
|
+
if (files.length < 2) return [];
|
|
143
|
+
const dest = files[files.length - 1];
|
|
144
|
+
if (!dest.endsWith('/')) return [dest];
|
|
145
|
+
return files.slice(0, -1).map((src) => dest + src.replace(/^.*\//, ''));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const PY_OPEN_WRITE = /\bopen\(\s*([^,()]+?)\s*,\s*(?:mode\s*=\s*)?(['"])([^'"]*)\2/g;
|
|
149
|
+
const JS_WRITE = /\b(?:writeFileSync|writeFile|appendFileSync|appendFile)\(\s*([^,()]+?)\s*,/g;
|
|
150
|
+
const PY_PATH_WRITE = /\.write_(?:text|bytes)\(/;
|
|
151
|
+
const STRING_LITERAL = /(['"`])((?:(?!\1)[^\\\n]|\\.)*)\1/g;
|
|
152
|
+
const PATH_LITERAL = /^[\w@./-]*[\w-]\.[A-Za-z]{1,5}$/;
|
|
153
|
+
|
|
154
|
+
function literalOf(expr) {
|
|
155
|
+
const m = /^(['"`])([^'"`]*)\1$/.exec(expr.trim());
|
|
156
|
+
return m ? m[2] : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A write call with a computed path (`open(p, "w")`) counts every path-shaped literal in the script.
|
|
160
|
+
function scriptWrites(script) {
|
|
161
|
+
const literals = [];
|
|
162
|
+
let computed = PY_PATH_WRITE.test(script);
|
|
163
|
+
for (const m of script.matchAll(PY_OPEN_WRITE)) {
|
|
164
|
+
if (!/[wax+]/.test(m[3])) continue;
|
|
165
|
+
const lit = literalOf(m[1]);
|
|
166
|
+
if (lit === null) computed = true; else literals.push(lit);
|
|
167
|
+
}
|
|
168
|
+
for (const m of script.matchAll(JS_WRITE)) {
|
|
169
|
+
const lit = literalOf(m[1]);
|
|
170
|
+
if (lit === null) computed = true; else literals.push(lit);
|
|
171
|
+
}
|
|
172
|
+
if (!computed) return literals;
|
|
173
|
+
const mentioned = [...script.matchAll(STRING_LITERAL)].map((m) => m[2]).filter((s) => PATH_LITERAL.test(s));
|
|
174
|
+
return [...new Set([...literals, ...mentioned])];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function runnerScript(args, body) {
|
|
178
|
+
for (let i = 0; i < args.length; i++) {
|
|
179
|
+
if (['-c', '-e', '-p', '--eval', '--print'].includes(args[i].text) && !args[i].quoted) return args[i + 1]?.text ?? null;
|
|
180
|
+
}
|
|
181
|
+
const { files } = positionals(args, () => false);
|
|
182
|
+
return (files.length === 0 || files[0] === '-') ? body : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function heredocBody(lines, at) {
|
|
186
|
+
if (at < 0) return null;
|
|
187
|
+
const delim = /<<-?\s*(['"]?)([\w.-]+)\1/.exec(lines[at])?.[2];
|
|
188
|
+
if (!delim) return null;
|
|
189
|
+
const end = lines.findIndex((line, i) => i > at && line.trim() === delim);
|
|
190
|
+
return lines.slice(at + 1, end < 0 ? undefined : end).join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function commandWriteTargets(head, body) {
|
|
194
|
+
const vars = new Map();
|
|
195
|
+
const expand = ({ text, single }) => (single ? text : text.replace(/\$\{?([A-Za-z_]\w*)\}?/g, (all, name) => vars.get(name) ?? all));
|
|
196
|
+
const targets = [];
|
|
197
|
+
for (const tokens of simpleCommands(head)) {
|
|
198
|
+
let i = 0;
|
|
199
|
+
for (; i < tokens.length && !tokens[i].quoted && ASSIGNMENT.test(tokens[i].text); i++) {
|
|
200
|
+
const [, name, value] = ASSIGNMENT.exec(tokens[i].text);
|
|
201
|
+
vars.set(name, value);
|
|
202
|
+
}
|
|
203
|
+
if (i >= tokens.length) continue;
|
|
204
|
+
const word = tokens[i].text.replace(/^.*[\\/]/, '').replace(/\.exe$/i, '');
|
|
205
|
+
const args = tokens.slice(i + 1).map((t) => ({ ...t, text: expand(t) }));
|
|
206
|
+
if (word === 'sed') targets.push(...sedTargets(args));
|
|
207
|
+
else if (word === 'perl') targets.push(...perlTargets(args));
|
|
208
|
+
else if (word === 'cp' || word === 'mv') targets.push(...copyTargets(args));
|
|
209
|
+
else if (SCRIPT_RUNNER.test(word)) {
|
|
210
|
+
const script = runnerScript(args, body);
|
|
211
|
+
if (script) targets.push(...scriptWrites(script));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return targets;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// The redirect scan stops at the heredoc line: the body is data, and an
|
|
218
|
+
// apostrophe in it would derail the quote-aware split.
|
|
219
|
+
export function writeTargets(command) {
|
|
220
|
+
const lines = String(command || '').split('\n');
|
|
221
|
+
const heredocAt = lines.findIndex((line) => line.includes('<<'));
|
|
222
|
+
const head = (heredocAt >= 0 ? lines.slice(0, heredocAt + 1) : lines).join('\n');
|
|
223
|
+
const tokens = quotedTokens(head);
|
|
224
|
+
|
|
225
|
+
const targets = [];
|
|
226
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
227
|
+
const { text, quoted } = tokens[i];
|
|
228
|
+
if (quoted) continue;
|
|
229
|
+
const op = text.match(WRITE_REDIRECT);
|
|
230
|
+
if (op) {
|
|
231
|
+
const rest = text.slice(op[0].length);
|
|
232
|
+
const target = rest || tokens[i + 1]?.text;
|
|
233
|
+
if (target && !target.startsWith('&')) targets.push(target);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (text.replace(/^.*[\/]/, '') === 'tee') {
|
|
237
|
+
for (let j = i + 1; j < tokens.length && !/^[;&|]/.test(tokens[j].text); j++) {
|
|
238
|
+
if (!tokens[j].text.startsWith('-')) targets.push(tokens[j].text);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return [...new Set([...targets, ...commandWriteTargets(head, heredocBody(lines, heredocAt))])];
|
|
243
|
+
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* every case, judged only on the comments an edit ADDED. Whether a short
|
|
4
|
-
* comment restates the code is not decidable here; that stays with the
|
|
5
|
-
* Gate 3 evaluator and the `code-budget` density.
|
|
6
|
-
*
|
|
7
|
-
* @module comment-rules
|
|
8
|
-
*/
|
|
1
|
+
// Judges only the comments an edit ADDED, and only their shape: whether a
|
|
2
|
+
// comment restates the code is not decidable here.
|
|
9
3
|
|
|
10
4
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
11
5
|
import { dirname, join } from 'path';
|
|
@@ -58,13 +52,7 @@ function nearest(startDir, test) {
|
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
/**
|
|
62
|
-
* Doc-comments are allowed only on a package consumed outside the repo. When
|
|
63
|
-
* that cannot be read from the nearest project file, the rule stays quiet.
|
|
64
|
-
*
|
|
65
|
-
* @param {string} file - absolute path of the edited file
|
|
66
|
-
* @returns {boolean}
|
|
67
|
-
*/
|
|
55
|
+
/** Doc-comments are allowed only on a package consumed outside the repo; unreadable = quiet. */
|
|
68
56
|
export function docCommentsForbidden(file) {
|
|
69
57
|
const dir = dirname(file);
|
|
70
58
|
if (/\.cs$/i.test(file)) {
|
|
@@ -85,7 +73,7 @@ export function docCommentsForbidden(file) {
|
|
|
85
73
|
return JSON.parse(readFileSync(pkg, 'utf8')).private === true;
|
|
86
74
|
}
|
|
87
75
|
|
|
88
|
-
|
|
76
|
+
// Moved comments do not count as added.
|
|
89
77
|
function addedCommentLines(before, after, file) {
|
|
90
78
|
const remaining = new Map();
|
|
91
79
|
for (const text of commentLines(before, file).values()) {
|
|
@@ -115,10 +103,7 @@ function addedRegionLines(before, after) {
|
|
|
115
103
|
return out;
|
|
116
104
|
}
|
|
117
105
|
|
|
118
|
-
/**
|
|
119
|
-
* @param {{ file: string, before: string, after: string, docCommentsForbidden?: boolean }} input
|
|
120
|
-
* @returns {Array<{ line: number, rule: 'process'|'banner'|'doc-comment'|'long-block', text: string }>}
|
|
121
|
-
*/
|
|
106
|
+
/** @returns {Array<{ line: number, rule: 'process'|'banner'|'doc-comment'|'long-block', text: string }>} */
|
|
122
107
|
export function commentFindings({ file, before, after, docCommentsForbidden: docForbidden = false }) {
|
|
123
108
|
const isCs = /\.cs$/i.test(file);
|
|
124
109
|
const all = commentLines(after, file);
|
|
@@ -1,58 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* O comentário é o que `stripComments` apaga: o mesmo varredor que já separa
|
|
5
|
-
* comentário de string para o `doctor --ai`, com a sintaxe de TS/JS ligada
|
|
6
|
-
* fora do C#. Um regex por linha confundiria `https://` com comentário.
|
|
7
|
-
*
|
|
8
|
-
* @module comment-scanner
|
|
9
|
-
*/
|
|
1
|
+
// Comentário é o que `stripComments` apaga: um regex por linha confundiria
|
|
2
|
+
// `https://` com comentário.
|
|
10
3
|
|
|
11
4
|
import { stripComments } from './comment-stripper.js';
|
|
12
5
|
|
|
13
|
-
|
|
6
|
+
// A mesma grafia de `C{n}`/`F{n}` é tecla, métrica, célula ou o modelo C-quatro:
|
|
7
|
+
// esses vizinhos não contam.
|
|
8
|
+
const PLAN_ID = new RegExp(String.raw`(?<!\b(?:key|tecla|press|aperte|cell|c[eé]lula|column|coluna)\s+)(?<![\w.#$/-])[CF]\d{1,2}(?![\w.(-]|\s+(?:key|tecla|score|model|modelo|diagram|diagrama)\b)`, 'i');
|
|
9
|
+
|
|
14
10
|
const PROCESS_CITATION = [
|
|
15
11
|
/\bUC[-\s]?\d+/,
|
|
16
12
|
/\bADR[-\s]?\d+/,
|
|
17
13
|
/\bFR[-\s]?\d+/,
|
|
18
14
|
/\bgate\s?[1-3]\b/i,
|
|
19
|
-
/\
|
|
15
|
+
/\bplan\.md\b/i,
|
|
16
|
+
/\bchecks\.json\b/i,
|
|
20
17
|
/\bmockups?\b/i,
|
|
21
18
|
];
|
|
22
19
|
|
|
23
|
-
/**
|
|
24
|
-
* `T{n}` também é nome de parâmetro genérico. Não casa depois de `template`/
|
|
25
|
-
* `typename`/`generic`, nem colado a letra ou dígito (a hora ISO `24T10`).
|
|
26
|
-
*/
|
|
27
|
-
const TASK_ID = /(?<!\b(?:[Tt]emplate|[Tt]ypename|[Gg]eneric)\s+)(?<![\w"])T\d{1,3}\b(?!")/;
|
|
28
|
-
|
|
29
|
-
/** Lista de tipos `<…>` e `{…}` (`Func<T1, T2>`, `cref="Func{T1,T2}"`, XML doc), de dentro para fora. */
|
|
30
|
-
const TYPE_LIST = /<[^<>]*>|\{[^{}]*\}/g;
|
|
31
|
-
|
|
32
|
-
function withoutTypeLists(text) {
|
|
33
|
-
let previous;
|
|
34
|
-
let current = text;
|
|
35
|
-
do {
|
|
36
|
-
previous = current;
|
|
37
|
-
current = current.replace(TYPE_LIST, ' ');
|
|
38
|
-
} while (current !== previous);
|
|
39
|
-
return current;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* @param {string} commentText
|
|
44
|
-
* @returns {boolean} o comentário usa o vocabulário de processo acima
|
|
45
|
-
*/
|
|
46
20
|
export function citesProcess(commentText) {
|
|
47
|
-
return PROCESS_CITATION.some((re) => re.test(commentText))
|
|
48
|
-
|| TASK_ID.test(withoutTypeLists(commentText));
|
|
21
|
+
return PROCESS_CITATION.some((re) => re.test(commentText)) || PLAN_ID.test(commentText);
|
|
49
22
|
}
|
|
50
23
|
|
|
51
|
-
/**
|
|
52
|
-
* @param {string} content - conteúdo do arquivo
|
|
53
|
-
* @param {string} file - path, só para escolher a sintaxe
|
|
54
|
-
* @returns {Map<number, string>} nº da linha (1-based) → texto do comentário nela
|
|
55
|
-
*/
|
|
24
|
+
/** @returns {Map<number, string>} linha 1-based → texto do comentário; `file` só escolhe a sintaxe */
|
|
56
25
|
export function commentLines(content, file) {
|
|
57
26
|
const text = String(content || '');
|
|
58
27
|
const stripped = stripComments(text, { js: !/\.cs$/i.test(file) });
|
|
@@ -66,8 +35,7 @@ export function commentLines(content, file) {
|
|
|
66
35
|
if (first < 0) first = k;
|
|
67
36
|
last = k;
|
|
68
37
|
}
|
|
69
|
-
//
|
|
70
|
-
// primeiro ao último caractere apagado.
|
|
38
|
+
// Espaço dentro do comentário não muda ao ser apagado.
|
|
71
39
|
if (first >= 0) out.set(index + 1, row.slice(first, last + 1).trim());
|
|
72
40
|
start += row.length + 1;
|
|
73
41
|
});
|